diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs index a82f3937ee..f18739c610 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs @@ -213,7 +213,7 @@ public void ScanDiff_Throws_Stryker_Input_Exception_When_Commit_null() public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles() { // Arrange - var diffIgnoreFiles = new[] { new FilePattern(Glob.Parse("/c/Users/JohnDoe/Project/Tests/Test.cs"), false, null) }; + var diffIgnoreFiles = new[] { new ExclusionPattern("/c/Users/JohnDoe/Project/Tests/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -289,7 +289,7 @@ public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles() public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles_Single_Asterisk() { // Arrange - var diffIgnoreFiles = new[] { new FilePattern(Glob.Parse("/c/Users/JohnDoe/Project/*/Test.cs"), false, null) }; + var diffIgnoreFiles = new[] { new ExclusionPattern("/c/Users/JohnDoe/Project/*/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -365,7 +365,7 @@ public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles_Singl public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles_Multi_Asterisk() { // Arrange - var diffIgnoreFiles = new[] { new FilePattern(Glob.Parse("**/Test.cs"), false, null) }; + var diffIgnoreFiles = new[] { new ExclusionPattern("**/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -441,7 +441,7 @@ public void ScanDiffReturnsListOfFiles_ExcludingTestFilesInDiffIgnoreFiles_Multi public void ScanDiffReturnsListOfFiles_ExcludingFilesInDiffIgnoreFiles_Multi_Asterisk() { // Arrange - var diffIgnoreFiles = new[] { new FilePattern(Glob.Parse("**/file.cs"), false, null) }; + var diffIgnoreFiles = new[] { new ExclusionPattern("**/file.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/ExclusionPatternTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/ExclusionPatternTests.cs new file mode 100644 index 0000000000..faed616953 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/ExclusionPatternTests.cs @@ -0,0 +1,36 @@ +using System; +using Shouldly; +using Xunit; + +namespace Stryker.Core.UnitTest +{ + public class ExclusionPatternTests : TestBase + { + [Fact] + public void ExclusionPattern_Null() + { + _ = Assert.Throws(() => new ExclusionPattern(null)); + } + + [Fact] + public void ExclusionPattern_Globs() + { + var s1 = new ExclusionPattern(@"Person.cs"); + var s2 = new ExclusionPattern(@"!Person.cs"); + + s1.IsExcluded.ShouldBeFalse(); + s2.IsExcluded.ShouldBeTrue(); + s1.Glob.ToString().ShouldBe(s2.Glob.ToString()); + } + + [Fact] + public void ExclusionPattern_MutantSpans() + { + var s1 = new ExclusionPattern(@"src/Person.cs{10..100}"); + var s2 = new ExclusionPattern(@"src/Person.cs"); + + s1.MutantSpans.ShouldBe(new [] { (10, 100)}); + s2.MutantSpans.ShouldBeEmpty(); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/FilePatternTests.cs similarity index 98% rename from src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/FilePatternTests.cs index 432f764ef8..d8f8db2aeb 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/FilePatternTests.cs @@ -5,7 +5,7 @@ using Shouldly; using Xunit; -namespace Stryker.Core.UnitTest.Options +namespace Stryker.Core.UnitTest { public class FilePatternTests : TestBase { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs index 0e539a1fb5..6e7d1eab64 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs @@ -26,7 +26,7 @@ public void ShouldCopyValues() DashboardUrl = "url", DevMode = true, Since = true, - DiffIgnoreChanges = new[] { new FilePattern(Glob.Parse("**"), true, null) }, + DiffIgnoreChanges = new[] { new ExclusionPattern("**") }, ExcludedMutations = new[] { Mutator.Bitwise }, FallbackVersion = "main", IgnoredMethods = new[] { new Regex("") }, diff --git a/src/Stryker.Core/Stryker.Core/ExclusionPattern.cs b/src/Stryker.Core/Stryker.Core/ExclusionPattern.cs new file mode 100644 index 0000000000..1dd75a2dfc --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/ExclusionPattern.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DotNet.Globbing; + +namespace Stryker.Core +{ + public readonly struct ExclusionPattern + { + private static readonly Regex _mutantSpanGroupRegex = new("(\\{(\\d+)\\.\\.(\\d+)\\})+$"); + private static readonly Regex _mutantSpanRegex = new Regex("\\{(\\d+)\\.\\.(\\d+)\\}"); + + public ExclusionPattern(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + IsExcluded = s.StartsWith('!'); + + var pattern = IsExcluded ? s[1..] : s; + var mutantSpansRegex = _mutantSpanGroupRegex.Match(pattern); + if (mutantSpansRegex.Success) + { + var filePathPart = pattern[..^mutantSpansRegex.Length]; + var normalized = FilePathUtils.NormalizePathSeparators(filePathPart); + Glob = Glob.Parse(normalized); + + MutantSpans = _mutantSpanRegex + .Matches(mutantSpansRegex.Value) + .Select(x => (int.Parse(x.Groups[1].Value), int.Parse(x.Groups[2].Value))); + } + else + { + var normalized = FilePathUtils.NormalizePathSeparators(pattern); + Glob = Glob.Parse(normalized); + MutantSpans = Enumerable.Empty<(int, int)>(); + } + } + + public bool IsExcluded { get; } + + public Glob Glob { get; } + + public IEnumerable<(int Start, int End)> MutantSpans { get; } + } +} diff --git a/src/Stryker.Core/Stryker.Core/FilePattern.cs b/src/Stryker.Core/Stryker.Core/FilePattern.cs index 040bae02c4..44f7fb1dd2 100644 --- a/src/Stryker.Core/Stryker.Core/FilePattern.cs +++ b/src/Stryker.Core/Stryker.Core/FilePattern.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; namespace Stryker.Core { @@ -13,8 +12,6 @@ namespace Stryker.Core /// public sealed class FilePattern : IEquatable { - private static readonly Regex _textSpanGroupRegex = new Regex("(\\{(\\d+)\\.\\.(\\d+)\\})+$"); - private static readonly Regex _textSpanRegex = new Regex("\\{(\\d+)\\.\\.(\\d+)\\}"); private static readonly TextSpan _textSpanMaxValue = new TextSpan(0, int.MaxValue); public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection textSpans) @@ -45,47 +42,25 @@ public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection text /// /// The pattern to parse. /// The - public static FilePattern Parse(string pattern) => Parse(pattern, spansEnabled: true); - - /// - /// Parses a given file pattern string. - /// Format: (!)<glob>({<spanStart>..<spanEnd>})* - /// - /// The pattern to parse. - /// Enable or disable span parsing. - /// The - public static FilePattern Parse(string pattern, bool spansEnabled) + public static FilePattern Parse(string pattern) { - var exclude = false; + var s = new ExclusionPattern(pattern); IReadOnlyCollection textSpans; - if (pattern.StartsWith('!')) - { - exclude = true; - pattern = pattern[1..]; - } - - var textSpanGroupMatch = _textSpanGroupRegex.Match(pattern); - if (!spansEnabled || !textSpanGroupMatch.Success) + if (!s.MutantSpans.Any()) { // If there are no spans specified, we add one that will cover the whole file. textSpans = new[] { _textSpanMaxValue }; } else { - // If we have one ore more spans we parse them. - var textSpansMatches = _textSpanRegex.Matches(textSpanGroupMatch.Value); - textSpans = textSpansMatches - .Select(x => TextSpan.FromBounds(int.Parse(x.Groups[1].Value), int.Parse(x.Groups[2].Value))) + textSpans = s.MutantSpans + .Select(x => TextSpan.FromBounds(x.Start, x.End)) .Reduce() .ToList(); - - pattern = pattern.Substring(0, pattern.Length - textSpanGroupMatch.Length); } - var glob = Glob.Parse(FilePathUtils.NormalizePathSeparators(pattern)); - - return new FilePattern(glob, exclude, textSpans); + return new FilePattern(s.Glob, s.IsExcluded, textSpans); } /// diff --git a/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs b/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs index 614e786a31..805ac68822 100644 --- a/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs +++ b/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs @@ -14,19 +14,19 @@ Any non-excluded files will trigger all mutants to be tested because we cannot d Use glob syntax for wildcards: https://en.wikipedia.org/wiki/Glob_(programming) Example: ['**/*Assets.json','**/favicon.ico']"; - public IEnumerable Validate() + public IEnumerable Validate() { if (SuppliedInput is { }) { - var diffIgnoreFilePatterns = new List(); + var diffIgnoreStrings = new List(); foreach (var pattern in SuppliedInput) { - diffIgnoreFilePatterns.Add(FilePattern.Parse(FilePathUtils.NormalizePathSeparators(pattern), spansEnabled: false)); + diffIgnoreStrings.Add(new ExclusionPattern(FilePathUtils.NormalizePathSeparators(pattern))); } - return diffIgnoreFilePatterns; + return diffIgnoreStrings; } - return Enumerable.Empty(); + return Enumerable.Empty(); } } } diff --git a/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs b/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs index 8d62d19a49..78e4b43512 100644 --- a/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs +++ b/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs @@ -153,7 +153,7 @@ public class StrykerOptions /// Context: When using the since feature, all tests are run again if files in the test project change (as these could impact the test results) /// When the file is present in this option the tests should not run again as the file does not impact test results. /// - public IEnumerable DiffIgnoreChanges { get; init; } = Enumerable.Empty(); + public IEnumerable DiffIgnoreChanges { get; init; } = Enumerable.Empty(); /// /// When no previous report can be found for the since feature, this commitish is used to se a baseline.