diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs index a82f3937ee..ec825b26f2 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/DiffProviders/GitDiffProviderTests.cs @@ -9,6 +9,7 @@ using Stryker.Core.DiffProviders; using Stryker.Core.Exceptions; using Stryker.Core.Options; +using Stryker.Core.ProjectComponents; using Xunit; namespace Stryker.Core.UnitTest.DiffProviders @@ -213,7 +214,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 ExcludableString("/c/Users/JohnDoe/Project/Tests/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -289,7 +290,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 ExcludableString("/c/Users/JohnDoe/Project/*/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -365,7 +366,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 ExcludableString("**/Test.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() @@ -441,7 +442,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 ExcludableString("**/file.cs") }; var basePath = FilePathUtils.NormalizePathSeparators("/c/Users/JohnDoe/Project/Tests"); var options = new StrykerOptions() diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs new file mode 100644 index 0000000000..337c089771 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs @@ -0,0 +1,335 @@ +using System.Linq; +using FSharp.Compiler.Text; +using Shouldly; +using Stryker.Core.Helpers; +using Xunit; + +namespace Stryker.Core.UnitTest.Helpers +{ + public class RangeHelperTests : TestBase + { + [Fact] + public void IsEmpty_ZeroRange() + { + var range = Range.Zero; + + range.IsEmpty().ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_HollowRange() + { + var range = GetRange((42, 42), (42, 42)); + + range.IsEmpty().ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_NotEmptyRange() + { + var range = GetRange((0, 0), (42, 42)); + + range.IsEmpty().ShouldBeFalse(); + } + + [Fact] + public void Max_Greater() + { + var position1 = PositionModule.mkPos(42, 42); + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void Max_Equal() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void Max_Less() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.mkPos(42, 42); + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Greater() + { + var position1 = PositionModule.mkPos(42, 42); + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Equal() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Less() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.mkPos(42, 42); + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void OverlapsWith_Overlapping_Left() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_Overlapping_Right() + { + var range1 = GetRange((11, 11), (33, 33)); + var range2 = GetRange((0, 0), (22, 22)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_Overlapping_Between() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((11, 11), (22, 22)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_NotOverlapping() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_Empty_Left() + { + var range1 = GetRange((0, 0), (42, 42)); + var range2 = Range.Zero; + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_Empty_Right() + { + var range1 = Range.Zero; + var range2 = GetRange((0, 0), (42, 42)); + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_Empty_Both() + { + var range1 = Range.Zero; + var range2 = Range.Zero; + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void Overlap_Overlapping() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var expectedOverlap = GetRange((11, 11), (22, 22)); + + range1.Overlap(range2, filePath).ShouldBe(expectedOverlap); + } + + [Fact] + public void Overlap_NotOverlapping() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.Overlap(range2, filePath).ShouldBeNull(); + } + + [Fact] + public void IntersectsWith_Intersecting_Left() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_Intersecting_Right() + { + var range1 = GetRange((11, 11), (33, 33)); + var range2 = GetRange((0, 0), (22, 22)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_Intersecting_Between() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((11, 11), (22, 22)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_NoIntersection() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.IntersectsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void Reduce_Zero() + { + var result = RangeHelper.Reduce("test.fs", Enumerable.Empty()); + + result.ShouldBeEmpty(); + } + + [Fact] + public void Reduce_One() + { + var filePath = "test.fs"; + + var range = GetRange((0, 0), (42, 42)); + + var ranges = new[] { range }; + + var result = RangeHelper.Reduce(filePath, ranges); + + result.ShouldBe(ranges); + } + + [Fact] + public void Reduce_Two_Intersecting() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var expectedRange = GetRange((0, 0), (33, 33)); + + var result = RangeHelper.Reduce(filePath, new[] { range1, range2 }); + + result.ShouldBe(new[] { expectedRange }); + } + + [Fact] + public void Reduce_Two_NonIntersecting() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + var ranges = new[] { range1, range2 }; + + var actual = RangeHelper.Reduce(filePath, ranges); + + actual.ShouldBe(ranges); + } + + [Fact] + public void Reduce_Three_PartiallyIntersecting() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var range3 = GetRange((44, 44), (55, 55)); + var expectedIntersection = GetRange((0, 0), (33, 33)); + var expectedRanges = new[] { expectedIntersection, range3 }; + + var result = RangeHelper.Reduce(filePath, new[] { range1, range2, range3 }); ; + + result.ShouldBe(expectedRanges, ignoreOrder: true); + } + + [Fact] + public void RemoveOverlap_Overlapping_Partially() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (22, 22)); + var expectedRange = GetRange((0, 0), (11, 11)); + + var result = RangeHelper.RemoveOverlap(new[] { range1 }, new [] { range2 }, filePath); + + result.ShouldBe(new[] { expectedRange }); + } + + [Fact] + public void RemoveOverlap_Overlapping_Completely() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (42, 42)); + var range2 = GetRange((0, 0), (42, 42)); + + var result = RangeHelper.RemoveOverlap(new[] { range1 }, new[] { range2 }, filePath); + + result.ShouldBeEmpty(); + } + + [Fact] + public void RemoveOverlap_NotOverlapping() + { + var filePath = "test.fs"; + + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + var result = RangeHelper.RemoveOverlap(new[] { range1 }, new[] { range2 }, filePath); + + result.ShouldBe(new[] { range1 }); + } + + private static Range GetRange((int Line, int Column) start, (int Line, int Column) end) => + RangeModule.mkRange( + "test.fs", + PositionModule.mkPos(start.Line, start.Column), + PositionModule.mkPos(end.Line, end.Column)); + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs similarity index 92% rename from src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs index 88c3551606..e1b09d70f2 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs @@ -1,14 +1,14 @@ using Microsoft.CodeAnalysis.Text; using Shouldly; -using Stryker.Core.ProjectComponents; +using Stryker.Core.Helpers; using System; using System.Collections.Generic; using System.Linq; using Xunit; -namespace Stryker.Core.UnitTest.ProjectComponents +namespace Stryker.Core.UnitTest.Helpers { - public class ProjectComponentExtensionsTests : TestBase + public class TextSpanHelperTests : TestBase { [Theory] [InlineData(new int[0], new int[0])] @@ -26,7 +26,7 @@ public void Reduce_should_reduce_correctly(int[] inputSpans, int[] outputSpans) var textSpans = ConvertToSpans(inputSpans); // Act - var result = textSpans.Reduce(); + var result = TextSpanHelper.Reduce(textSpans); // Assert result.SequenceEqual(ConvertToSpans(outputSpans)).ShouldBeTrue(); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/MutantFilters/FilePatternMutantFilterTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/MutantFilters/FilePatternMutantFilterTests.cs index ce8f029f84..bae1d67eef 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/MutantFilters/FilePatternMutantFilterTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/MutantFilters/FilePatternMutantFilterTests.cs @@ -5,7 +5,6 @@ using Shouldly; using Stryker.Core.MutantFilters; using Stryker.Core.Mutants; -using Stryker.Core.Options; using Stryker.Core.ProjectComponents; using Xunit; @@ -16,7 +15,7 @@ public class FilePatternMutantFilterTests : TestBase [Fact] public static void ShouldHaveName() { - var target = new FilePatternMutantFilter(new StrykerOptions()) as IMutantFilter; + var target = new FilePatternMutantFilter(); target.DisplayName.ShouldBe("mutate filter"); } @@ -42,8 +41,8 @@ public void FilterMutants_should_filter_included_and_excluded_files( bool shouldKeepFile) { // Arrange - var options = new StrykerOptions() { Mutate = patterns.Select(FilePattern.Parse) }; - var file = new CsharpFileLeaf { RelativePath = filePath, FullPath = Path.Combine("C:/test/", filePath) }; + var strings = patterns.Select(ExcludableString.Parse); + var file = new CsharpFileLeaf(strings) { RelativePath = filePath, FullPath = Path.Combine("C:/test/", filePath) }; // Create token with the correct text span var syntaxToken = SyntaxFactory.Identifier( @@ -54,7 +53,7 @@ public void FilterMutants_should_filter_included_and_excluded_files( var mutant = new Mutant { Mutation = new Mutation { OriginalNode = SyntaxFactory.IdentifierName(syntaxToken) } }; - var sut = new FilePatternMutantFilter(options); + var sut = new FilePatternMutantFilter(); // Act var result = sut.FilterMutants(new[] { mutant }, file, null); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs index 432f764ef8..fe663f81de 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs @@ -1,8 +1,8 @@ using System; using System.Linq; using DotNet.Globbing; -using Microsoft.CodeAnalysis.Text; using Shouldly; +using Stryker.Core.ProjectComponents; using Xunit; namespace Stryker.Core.UnitTest.Options @@ -23,11 +23,11 @@ public class FilePatternTests : TestBase public void IsMatch_should_match_glob_pattern(string file, string glob, bool isMatch) { // Arrange - var textSpan = new TextSpan(0, 1); - var sut = new FilePattern(Glob.Parse(glob), false, new[] { textSpan }); + var mutantSpan = new MutantSpan(0, 1); + var pattern = new FilePattern(Glob.Parse(glob), false, new[] { mutantSpan }); // Act - var result = sut.IsMatch(file, textSpan); + var result = new CsharpFileLeaf().IsMatch(pattern, file, mutantSpan); // Assert result.ShouldBe(isMatch); @@ -45,10 +45,11 @@ public void IsMatch_should_match_glob_pattern(string file, string glob, bool isM public void IsMatch_should_match_textSpans(string spanPattern, int spanStart, int spanEnd, bool isMatch) { // Arrange - var sut = FilePattern.Parse("*.*" + spanPattern); + var mutantSpan = new MutantSpan(spanStart, spanEnd); + var pattern = new CsharpFileLeaf().Parse(new ExcludableString("*.*" + spanPattern)); // Act - var result = sut.IsMatch($"test.cs", TextSpan.FromBounds(spanStart, spanEnd)); + var result = new CsharpFileLeaf().IsMatch(pattern, mutantSpan); // Assert result.ShouldBe(isMatch); @@ -62,17 +63,17 @@ public void IsMatch_should_match_textSpans(string spanPattern, int spanStart, in public void Parse_should_parse_correctly(string spanPattern, string glob, bool isExclude, int[] spans) { // Arrange - var textSpans = Enumerable.Range(0, spans.Length) + var mutantSpans = Enumerable.Range(0, spans.Length) .GroupBy(i => Math.Floor(i / 2d)) - .Select(x => TextSpan.FromBounds(spans[x.First()], spans[x.Skip(1).First()])); + .Select(x => new MutantSpan(spans[x.First()], spans[x.Skip(1).First()])); // Act - var result = FilePattern.Parse(spanPattern); + var result = new CsharpFileLeaf().Parse(new ExcludableString(spanPattern)); // Assert result.Glob.ToString().ShouldBe(FilePathUtils.NormalizePathSeparators(glob)); result.IsExclude.ShouldBe(isExclude); - result.TextSpans.SequenceEqual(textSpans).ShouldBe(true); + result.MutantSpans.SequenceEqual(mutantSpans).ShouldBe(true); } } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/DiffIgnoreChangesInputTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/DiffIgnoreChangesInputTests.cs index a60448bbff..e1dd3a04b9 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/DiffIgnoreChangesInputTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/DiffIgnoreChangesInputTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Shouldly; using Stryker.Core.Options.Inputs; +using Stryker.Core.ProjectComponents; using Xunit; namespace Stryker.Core.UnitTest.Options.Inputs @@ -24,7 +25,7 @@ public void ShouldAcceptGlob() { var target = new DiffIgnoreChangesInput { SuppliedInput = new[] { "*" } }; - var result = target.Validate(); + var result = new SimpleFileLeaf(target.Validate()).Patterns; result.ShouldHaveSingleItem().Glob.ToString().ShouldBe("*"); } @@ -34,7 +35,7 @@ public void ShouldParseAll() { var target = new DiffIgnoreChangesInput { SuppliedInput = new[] { "*", "MyFile.cs" } }; - var result = target.Validate(); + var result = new SimpleFileLeaf(target.Validate()).Patterns; result.Count().ShouldBe(2); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/MutateInputTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/MutateInputTests.cs index 22f0daa9e7..1e7c6e02b2 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/MutateInputTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/MutateInputTests.cs @@ -2,6 +2,7 @@ using System.Linq; using Shouldly; using Stryker.Core.Options.Inputs; +using Stryker.Core.ProjectComponents; using Xunit; namespace Stryker.Core.UnitTest.Options.Inputs @@ -24,7 +25,7 @@ public void ShouldHaveDefault() { var target = new MutateInput { SuppliedInput = new string[] { } }; - var result = target.Validate(); + var result = new SimpleFileLeaf(target.Validate()).Patterns; var item = result.ShouldHaveSingleItem(); item.Glob.ToString().ShouldBe(Path.Combine("**", "*")); @@ -36,7 +37,7 @@ public void ShouldReturnFiles() { var target = new MutateInput { SuppliedInput = new[] { Path.Combine("**", "*.cs") } }; - var result = target.Validate(); + var result = new SimpleFileLeaf(target.Validate()).Patterns; var item = result.ShouldHaveSingleItem(); item.Glob.ToString().ShouldBe(Path.Combine("**", "*.cs")); @@ -48,7 +49,7 @@ public void ShouldExcludeAll() { var target = new MutateInput { SuppliedInput = new[] { "!" + Path.Combine("**", "Test.cs") } }; - var result = target.Validate(); + var result = new SimpleFileLeaf(target.Validate()).Patterns; result.Count().ShouldBe(2); result.First().Glob.ToString().ShouldBe(Path.Combine("**", "Test.cs")); @@ -56,6 +57,5 @@ public void ShouldExcludeAll() result.Last().Glob.ToString().ShouldBe(Path.Combine("**", "*")); result.Last().IsExclude.ShouldBeFalse(); } - } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs index 0e539a1fb5..1c17edd1c5 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerOptionsTests.cs @@ -5,6 +5,7 @@ using Shouldly; using Stryker.Core.Mutators; using Stryker.Core.Options; +using Stryker.Core.ProjectComponents; using Xunit; namespace Stryker.Core.UnitTest.Options @@ -26,7 +27,7 @@ public void ShouldCopyValues() DashboardUrl = "url", DevMode = true, Since = true, - DiffIgnoreChanges = new[] { new FilePattern(Glob.Parse("**"), true, null) }, + DiffIgnoreChanges = new[] { new ExcludableString("**") }, ExcludedMutations = new[] { Mutator.Bitwise }, FallbackVersion = "main", IgnoredMethods = new[] { new Regex("") }, @@ -37,7 +38,7 @@ public void ShouldCopyValues() LogToFile = true }, ModuleName = "module", - Mutate = new[] { new FilePattern(Glob.Parse("**"), true, null) }, + Mutate = new[] { new ExcludableString("**") }, MutationLevel = MutationLevel.Complete, OptimizationMode = OptimizationModes.DisableBail, OutputPath = "output", diff --git a/src/Stryker.Core/Stryker.Core/DiffProviders/GitDiffProvider.cs b/src/Stryker.Core/Stryker.Core/DiffProviders/GitDiffProvider.cs index a659784978..97700cb869 100644 --- a/src/Stryker.Core/Stryker.Core/DiffProviders/GitDiffProvider.cs +++ b/src/Stryker.Core/Stryker.Core/DiffProviders/GitDiffProvider.cs @@ -6,6 +6,7 @@ using Stryker.Core.Exceptions; using Stryker.Core.Mutants; using Stryker.Core.Options; +using Stryker.Core.ProjectComponents; namespace Stryker.Core.DiffProviders { @@ -71,7 +72,7 @@ public DiffResult ScanDiff() private void RemoveFilteredOutFiles(DiffResult diffResult) { - foreach (var glob in _options.DiffIgnoreChanges.Select(d => d.Glob)) + foreach (var glob in new SimpleFileLeaf(_options.DiffIgnoreChanges).Patterns.Select(d => d.Glob)) { diffResult.ChangedSourceFiles = diffResult.ChangedSourceFiles.Where(diffResultFile => !glob.IsMatch(diffResultFile)).ToList(); diffResult.ChangedTestFiles = diffResult.ChangedTestFiles.Where(diffResultFile => !glob.IsMatch(diffResultFile)).ToList(); diff --git a/src/Stryker.Core/Stryker.Core/FilePattern.cs b/src/Stryker.Core/Stryker.Core/FilePattern.cs index 49fe5facfa..f3d581878a 100644 --- a/src/Stryker.Core/Stryker.Core/FilePattern.cs +++ b/src/Stryker.Core/Stryker.Core/FilePattern.cs @@ -1,27 +1,22 @@ using DotNet.Globbing; -using Microsoft.CodeAnalysis.Text; -using Stryker.Core.ProjectComponents; using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; namespace Stryker.Core { + public record MutantSpan(int Start, int End); + /// /// Contains information about which files and which parts of a file should be in- or excluded. /// 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) + public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection mutantSpans) { Glob = glob; IsExclude = isExclude; - TextSpans = textSpans; + MutantSpans = mutantSpans; } /// @@ -37,81 +32,9 @@ public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection text /// /// Gets the the text spans of the file this pattern matches. /// - public IReadOnlyCollection TextSpans { get; } - - /// - /// Parses a given file pattern string. - /// Format: (!)<glob>({<spanStart>..<spanEnd>})* - /// - /// 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) - { - var exclude = false; - IReadOnlyCollection textSpans; - - if (pattern.StartsWith('!')) - { - exclude = true; - pattern = pattern[1..]; - } - - var textSpanGroupMatch = _textSpanGroupRegex.Match(pattern); - if (!spansEnabled || !textSpanGroupMatch.Success) - { - // 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))) - .Reduce() - .ToList(); - - pattern = pattern.Substring(0, pattern.Length - textSpanGroupMatch.Length); - } - - var glob = Glob.Parse(FilePathUtils.NormalizePathSeparators(pattern)); - - return new FilePattern(glob, exclude, textSpans); - } - - /// - /// Checks whether a given file path and span matches the current file pattern. - /// - /// The full file path. - /// The span of the text to check. - /// True if the file and span matches the pattern. - public bool IsMatch(string filePath, TextSpan textSpan) - { - // Check if the file path is matched. - if (!Glob.IsMatch(FilePathUtils.NormalizePathSeparators(filePath))) - { - return false; - } - - // Check if any span fully contains the specified span - if (TextSpans.Any(span => span.Contains(textSpan))) - { - return true; - } - - return false; - } + public IReadOnlyCollection MutantSpans { get; } - public bool Equals(FilePattern other) => Glob.ToString() == other.Glob.ToString() && IsExclude == other.IsExclude && TextSpans.SequenceEqual(other.TextSpans); + public bool Equals(FilePattern other) => Glob.ToString() == other.Glob.ToString() && IsExclude == other.IsExclude && MutantSpans.SequenceEqual(other.MutantSpans); public override bool Equals(object obj) { @@ -139,7 +62,7 @@ public override int GetHashCode() { var hashCode = Glob != null ? Glob.GetHashCode() : 0; hashCode = (hashCode * 397) ^ IsExclude.GetHashCode(); - hashCode = (hashCode * 397) ^ (TextSpans != null ? UncheckedSum(TextSpans.Select(t => t.GetHashCode())) : 0); + hashCode = (hashCode * 397) ^ (MutantSpans != null ? UncheckedSum(MutantSpans.Select(t => t.GetHashCode())) : 0); return hashCode; } diff --git a/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs b/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs new file mode 100644 index 0000000000..2d41cd11e2 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Linq; +using FSharp.Compiler.Text; + +namespace Stryker.Core.Helpers +{ + public static class RangeHelper + { + /// + /// Reduces a set of ranges to the smallest set of ranges possible. + /// Two can be combined if they intersect. + /// + /// The set of s to reduce. + /// The reduced set. + public static IReadOnlyCollection Reduce(string filePath, IEnumerable ranges) + { + var rangeList = new List(ranges); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in rangeList) + { + // Check if any of the other spans intersects with the current one + var other = rangeList.FirstOrDefault(s => !RangeModule.equals(s, current) && s.IntersectsWith(current)); + if (!RangeModule.equals(other, Range.Zero)) + { + // Remove the original spans + rangeList.Remove(current); + rangeList.Remove(other); + + // Add the newly combined span. + rangeList.Add(FromBounds(filePath, Min(current.Start, other.Start), Max(current.End, other.End))); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return rangeList.Distinct().Where(x => !x.IsEmpty()).ToList(); + } + + /// + /// Removes all overlaps of two sets of and returns the resulting set. + /// + /// The first set. + /// The second set. + /// All ranges and part of ranges of that do not overlap with any ranges in . + public static IReadOnlyCollection RemoveOverlap(this IEnumerable left, IEnumerable right, string filePath) + { + var rangeList = new List(left); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in rangeList) + { + // Check if any span overlaps the current span. + var other = right.FirstOrDefault(o => o.OverlapsWith(current)); + + if (!RangeModule.equals(other, Range.Zero)) + { + // Remove the current span add the new span(s). + rangeList.Remove(current); + rangeList.AddRange(RemoveOverlap(current, other)); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return rangeList; + + IReadOnlyCollection RemoveOverlap(Range current, Range other) + { + // The the current range is completely contained inside the other, nothing will be left. + if (RangeModule.rangeContainsRange(other, current)) + return System.Array.Empty(); + + // Check if there is any overlap. + var overlap = current.Overlap(other, filePath); + + if (!overlap.HasValue) + { + return new[] { current }; + } + + return new[] { FromBounds(filePath, current.Start, overlap.Value.Start), FromBounds(filePath, overlap.Value.End, current.End) }.Where(s => !s.IsEmpty()).ToList(); + } + } + + public static bool OverlapsWith(this Range range1, Range range2) + { + var overlapStart = Max(range1.Start, range2.Start); + var overlapEnd = Min(range1.End, range2.End); + + return PositionModule.posLt(overlapStart, overlapEnd); + } + + public static Range? Overlap(this Range range1, Range range2, string filePath) + { + var overlapStart = Max(range1.Start, range2.Start); + var overlapEnd = Min(range1.End, range2.End); + + return PositionModule.posLt(overlapStart, overlapEnd) + ? FromBounds(filePath, overlapStart, overlapEnd) + : null; + } + + public static Position Max(Position pos1, Position pos2) + { + return PositionModule.posGeq(pos1, pos2) ? pos1 : pos2; + } + + public static Position Min(Position pos1, Position pos2) + { + return PositionModule.posLt(pos1, pos2) ? pos1 : pos2; + } + + public static bool IsEmpty(this Range range) + { + return PositionModule.posEq(range.Start, range.End); + } + + public static bool IntersectsWith(this Range range1, Range range2) + { + return PositionModule.posGeq(range1.End, range2.Start) + && PositionModule.posGeq(range2.End, range1.Start); + } + + public static Position GetPosition(string text, int index) + { + var line = 0; + var col = 0; + + for (var i = 0; i < System.Math.Min(index, text.Length); i++) + { + if (text[i] == '\n') + { + line++; + col = 0; + } + else + { + col++; + } + } + + return PositionModule.mkPos(line, col); + } + + public static int GetIndex(string text, Position pos) + { + var line = 0; + var col = 0; + + for (var i = 0; i < text.Length; i++) + { + if (line == pos.Line && col == pos.Column) + { + return i; + } + + if (text[i] == '\n') + { + line++; + col = 0; + } + else + { + col++; + } + } + + return -1; + } + + public static Range FromBounds(string filePath, Position startPos, Position endPos) + { + return RangeModule.mkRange(filePath, startPos, endPos); + } + + public static Range FromBounds(string filePath, string text, int startIndex, int endIndex) + { + var startPos = GetPosition(text, startIndex); + var endPos = GetPosition(text, endIndex); + return RangeModule.mkRange(filePath, startPos, endPos); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs b/src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs similarity index 69% rename from src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs rename to src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs index 6b8d7ef9df..bb2bee520b 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs +++ b/src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs @@ -3,48 +3,20 @@ using System.Collections.Generic; using System.Linq; -namespace Stryker.Core.ProjectComponents +namespace Stryker.Core.Helpers { /// /// Contains extension methods for project components. /// - public static class IProjectComponentsExtensions + public static class TextSpanHelper { - /// - /// Checks with the given s whether all parts of the component are excluded. - /// - /// The file to check. - /// The file patters to check with. - /// If any parts of the file are included false; otherwise true. - public static bool IsComponentExcluded(this IReadOnlyProjectComponent projectComponent, IEnumerable filePatterns) - { - var includePattern = filePatterns.Where(x => !x.IsExclude).ToList(); - var excludePattern = filePatterns.Where(x => x.IsExclude).ToList(); - - // Get in- and excluded spans - var includedSpans = includePattern.Where(MatchesFilePattern).SelectMany(x => x.TextSpans).Reduce(); - var excludedSpans = excludePattern.Where(MatchesFilePattern).SelectMany(x => x.TextSpans).Reduce(); - - // If there are only included spans, the file is not excluded. - if ((includedSpans.Any() && !excludedSpans.Any()) || projectComponent.Parent?.Parent == null) - { - return false; - } - - return !includedSpans.RemoveOverlap(excludedSpans).Any(); - - bool MatchesFilePattern(FilePattern pattern) => - pattern.Glob.IsMatch(projectComponent.FullPath) || - pattern.Glob.IsMatch(projectComponent.RelativePath); - } - /// /// Reduces a set of text spans to the smallest set of text spans possible. /// Two can be combined if they intersect. /// /// The set of s to reduce. /// The reduced set. - public static IReadOnlyCollection Reduce(this IEnumerable textSpans) + public static IReadOnlyCollection Reduce(IEnumerable textSpans) { var spans = new List(textSpans); var shouldContinue = true; diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs index 3c6d98719c..23b905effb 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs @@ -89,7 +89,7 @@ private CsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult a var relativePath = Path.GetRelativePath(sourceProjectDir, sourceFile); var folderComposite = GetOrBuildFolderComposite(cache, Path.GetDirectoryName(relativePath), sourceProjectDir, projectUnderTestFolderComposite); - var file = new CsharpFileLeaf() + var file = new CsharpFileLeaf(options.Mutate) { SourceCode = FileSystem.File.ReadAllText(sourceFile), FullPath = sourceFile, @@ -197,7 +197,7 @@ private CsharpFolderComposite FindInputFiles(string path, string sourceProjectDi // Roslyn cannot compile xaml.cs files generated by xamarin. // Since the files are generated they should not be mutated anyway, so skip these files. - var fileLeaf = new CsharpFileLeaf() + var fileLeaf = new CsharpFileLeaf(_options.Mutate) { SourceCode = FileSystem.File.ReadAllText(file), FullPath = file, diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/FsharpProjectComponentsBuilder.cs b/src/Stryker.Core/Stryker.Core/Initialisation/FsharpProjectComponentsBuilder.cs index 6ca5df381c..60a4e54242 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/FsharpProjectComponentsBuilder.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/FsharpProjectComponentsBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.FSharp.Control; using Stryker.Core.Exceptions; +using Stryker.Core.Options; using Stryker.Core.ProjectComponents; using Stryker.Core.ProjectComponents.SourceProjects; using System; @@ -19,12 +20,14 @@ namespace Stryker.Core.Initialisation internal class FsharpProjectComponentsBuilder : ProjectComponentsBuilder { private readonly SourceProjectInfo _projectInfo; + private readonly StrykerOptions _options; private readonly string[] _foldersToExclude; private readonly ILogger _logger; - public FsharpProjectComponentsBuilder(SourceProjectInfo projectInfo, string[] foldersToExclude, ILogger logger, IFileSystem fileSystem) : base(fileSystem) + public FsharpProjectComponentsBuilder(SourceProjectInfo projectInfo, StrykerOptions options, string[] foldersToExclude, ILogger logger, IFileSystem fileSystem) : base(fileSystem) { _projectInfo = projectInfo; + _options = options; _foldersToExclude = foldersToExclude; _logger = logger; } @@ -89,7 +92,7 @@ private FsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult a var folderComposite = GetOrBuildFolderComposite(cache, Path.GetDirectoryName(relativePath), sourceProjectDir, projectRoot, inputFiles); var fileName = Path.GetFileName(sourceFile); - var file = new FsharpFileLeaf() + var file = new FsharpFileLeaf(_options.Mutate) { SourceCode = FileSystem.File.ReadAllText(sourceFile), RelativePath = FileSystem.Path.Combine(folderComposite.RelativePath, fileName), @@ -252,7 +255,7 @@ private FsharpFolderComposite FindInputFiles(string path, string sourceProjectDi { var fileName = Path.GetFileName(file); - var fileLeaf = new FsharpFileLeaf() + var fileLeaf = new FsharpFileLeaf(_options.Mutate) { SourceCode = FileSystem.File.ReadAllText(file), RelativePath = Path.Combine(folderComposite.RelativePath, fileName), diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 7241c49f6a..6364301662 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -370,6 +370,7 @@ private ProjectComponentsBuilder GetProjectComponentBuilder( Language.Fsharp => new FsharpProjectComponentsBuilder( projectInfo, + options, _foldersToExclude, _logger, FileSystem), diff --git a/src/Stryker.Core/Stryker.Core/MutantFilters/FilePatternMutantFilter.cs b/src/Stryker.Core/Stryker.Core/MutantFilters/FilePatternMutantFilter.cs index 97270e8d19..2c0ad94532 100644 --- a/src/Stryker.Core/Stryker.Core/MutantFilters/FilePatternMutantFilter.cs +++ b/src/Stryker.Core/Stryker.Core/MutantFilters/FilePatternMutantFilter.cs @@ -13,14 +13,6 @@ public class FilePatternMutantFilter : IMutantFilter { public MutantFilter Type => MutantFilter.FilePattern; public string DisplayName => "mutate filter"; - private readonly IEnumerable _includePattern; - private readonly IEnumerable _excludePattern; - - public FilePatternMutantFilter(StrykerOptions options) - { - _includePattern = options.Mutate.Where(x => !x.IsExclude).ToList(); - _excludePattern = options.Mutate.Where(x => x.IsExclude).ToList(); - } public IEnumerable FilterMutants(IEnumerable mutants, IReadOnlyFileLeaf file, StrykerOptions options) { @@ -28,26 +20,15 @@ public IEnumerable FilterMutants(IEnumerable mutants, IReadOnlyF bool IsMutantIncluded(Mutant mutant) { - // Check if the the mutant is included. - if (!_includePattern.Any(MatchesPattern)) + // if we do not have the original node, we cannot exclude the mutation according to its location + if (mutant.Mutation.OriginalNode == null) { return false; } - // Check if the mutant is excluded. - return !_excludePattern.Any(MatchesPattern); - - bool MatchesPattern(FilePattern pattern) - { - // if we do not have the original node, we cannot exclude the mutation according to its location - if (mutant.Mutation.OriginalNode == null) - { - return false; - } - // We check both the full and the relative path to allow for relative paths. - return pattern.IsMatch(file.FullPath, mutant.Mutation.OriginalNode.Span) || - pattern.IsMatch(file.RelativePath, mutant.Mutation.OriginalNode.Span); - } + var textSpan = mutant.Mutation.OriginalNode.Span; + var mutantSpan = new MutantSpan(textSpan.Start, textSpan.End); + return file.IsMatch(mutantSpan); } } } diff --git a/src/Stryker.Core/Stryker.Core/MutantFilters/MutantFilterFactory.cs b/src/Stryker.Core/Stryker.Core/MutantFilters/MutantFilterFactory.cs index 11cf27a452..aacc07d597 100644 --- a/src/Stryker.Core/Stryker.Core/MutantFilters/MutantFilterFactory.cs +++ b/src/Stryker.Core/Stryker.Core/MutantFilters/MutantFilterFactory.cs @@ -35,7 +35,7 @@ public static IMutantFilter Create(StrykerOptions options, MutationTestInput mut private static IEnumerable DetermineEnabledMutantFilters(StrykerOptions options) { var enabledFilters = new SortedSet(new ByMutantFilterType()) { - new FilePatternMutantFilter(options), + new FilePatternMutantFilter(), new IgnoredMethodMutantFilter(), new IgnoreMutationMutantFilter(), new ExcludeFromCodeCoverageFilter(), diff --git a/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs b/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs index 614e786a31..e83bf42068 100644 --- a/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs +++ b/src/Stryker.Core/Stryker.Core/Options/Inputs/DiffIgnoreChangesInput.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Stryker.Core.ProjectComponents; namespace Stryker.Core.Options.Inputs { @@ -14,19 +15,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 ExcludableString(FilePathUtils.NormalizePathSeparators(pattern))); } - return diffIgnoreFilePatterns; + return diffIgnoreStrings; } - return Enumerable.Empty(); + return Enumerable.Empty(); } } } diff --git a/src/Stryker.Core/Stryker.Core/Options/Inputs/MutateInput.cs b/src/Stryker.Core/Stryker.Core/Options/Inputs/MutateInput.cs index 48697d686a..94b59936c8 100644 --- a/src/Stryker.Core/Stryker.Core/Options/Inputs/MutateInput.cs +++ b/src/Stryker.Core/Stryker.Core/Options/Inputs/MutateInput.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Stryker.Core.ProjectComponents; namespace Stryker.Core.Options.Inputs { @@ -14,26 +15,26 @@ public class MutateInput : Input> Use '{..}' at the end of a pattern to specify spans of text in files to in- or exclude. Example: ['**/*Service.cs','!**/MySpecialService.cs', '**/MyOtherService.cs{1..10}{32..45}']"; - public IEnumerable Validate() + public IEnumerable Validate() { if (SuppliedInput is { } && SuppliedInput.Any()) { - var filesToInclude = new List(); + var filesToInclude = new List(); foreach (var pattern in SuppliedInput) { - filesToInclude.Add(FilePattern.Parse(FilePathUtils.NormalizePathSeparators(pattern))); + filesToInclude.Add(new ExcludableString(FilePathUtils.NormalizePathSeparators(pattern))); } if (filesToInclude.All(f => f.IsExclude)) { // If there are only exclude patterns, we add a pattern that matches every file. - filesToInclude.Add(FilePattern.Parse(_defaultInput)); + filesToInclude.Add(new ExcludableString(_defaultInput)); } return filesToInclude; } - return Default.Select(FilePattern.Parse).ToList(); + return Default.Select(ExcludableString.Parse).ToList(); } } } diff --git a/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs b/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs index 8d62d19a49..2c0966efcb 100644 --- a/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs +++ b/src/Stryker.Core/Stryker.Core/Options/StrykerOptions.cs @@ -6,6 +6,7 @@ using Stryker.Core.Baseline.Providers; using Stryker.Core.Mutators; using Stryker.Core.Options.Inputs; +using Stryker.Core.ProjectComponents; using Stryker.Core.Reporters; namespace Stryker.Core.Options @@ -153,7 +154,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. @@ -173,7 +174,7 @@ public class StrykerOptions /// /// Files that should be mutated or ignored. Uses GLOB as pattern matching. Also parts of files can be ignored by format {10..21} /// - public IEnumerable Mutate { get; init; } = new[] { FilePattern.Parse("**/*") }; + public IEnumerable Mutate { get; init; } = new[] { ExcludableString.Parse("**/*") }; /// /// Method call mutations that should not be tested. The implementation of the method may still be mutated and tested. diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/Csharp/CsharpFileLeaf.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/Csharp/CsharpFileLeaf.cs index 5bba4a5338..5577944d9a 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/Csharp/CsharpFileLeaf.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/Csharp/CsharpFileLeaf.cs @@ -1,11 +1,19 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Stryker.Core.Helpers; using Stryker.Core.Mutants; using System.Collections.Generic; +using System.Linq; namespace Stryker.Core.ProjectComponents { - public class CsharpFileLeaf : ProjectComponent, IFileLeaf + public class CsharpFileLeaf : ExcludableProjectComponent, IFileLeaf { + // only needed for tests + internal CsharpFileLeaf() : base() { } + + public CsharpFileLeaf(IEnumerable strings) : base(strings) { } + public string SourceCode { get; set; } /// @@ -33,5 +41,24 @@ public override void Display() { DisplayFile(this); } + + public override bool IsMatch(FilePattern pattern, MutantSpan mutantSpan) + { + var textSpan = FromMutantSpan(mutantSpan); + + return pattern.MutantSpans.Any(span => FromMutantSpan(span).Contains(textSpan)); + } + + public override IEnumerable Reduce(IEnumerable spans) + => TextSpanHelper.Reduce(spans); + + public override IEnumerable RemoveOverlap(IEnumerable left, IEnumerable right) + => left.RemoveOverlap(right); + + public override MutantSpan ToMutantSpan(TextSpan span) + => new(span.Start, span.End); + + public override TextSpan FromMutantSpan(MutantSpan span) + => TextSpan.FromBounds(span.Start, span.End); } } diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableProjectComponent.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableProjectComponent.cs new file mode 100644 index 0000000000..ad2a4e8aca --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableProjectComponent.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DotNet.Globbing; + +namespace Stryker.Core.ProjectComponents +{ + public abstract class ExcludableProjectComponent : ProjectComponent + { + private static readonly Regex _mutantSpanGroupRegex = new Regex("(\\{(\\d+)\\.\\.(\\d+)\\})+$"); + private static readonly Regex _mutantSpanRegex = new Regex("\\{(\\d+)\\.\\.(\\d+)\\}"); + private static readonly MutantSpan _mutantSpanMaxValue = new(0, int.MaxValue); + + public IEnumerable Patterns { get; } = Enumerable.Empty(); + + public ExcludableProjectComponent(IEnumerable strings) + { + Patterns = strings.Select(Parse); + } + + protected ExcludableProjectComponent() + { + Patterns = (new[] { new ExcludableString("**/*") }).Select(Parse); + } + + public bool IsMatch(MutantSpan span) + { + var includePatterns = Patterns.Where(x => !x.IsExclude); + var excludePatterns = Patterns.Where(x => x.IsExclude); + + if (!includePatterns.Any(MatchesPattern)) + { + return false; + } + + return !excludePatterns.Any(MatchesPattern); + + bool MatchesPattern(FilePattern pattern) + { + // We check both the full and the relative path to allow for relative paths. + return IsMatch(pattern, FullPath, span) || + IsMatch(pattern, RelativePath, span); + } + } + + public bool IsComponentExcluded() + { + var includePattern = Patterns.Where(x => !x.IsExclude).ToList(); + var excludePattern = Patterns.Where(x => x.IsExclude).ToList(); + + // Get in- and excluded spans + var includedSpans = Reduce(includePattern.Where(MatchesFilePattern).SelectMany(x => x.MutantSpans).Select(FromMutantSpan)); + var excludedSpans = Reduce(excludePattern.Where(MatchesFilePattern).SelectMany(x => x.MutantSpans).Select(FromMutantSpan)); + + // If there are only included spans, the file is not excluded. + if ((includedSpans.Any() && !excludedSpans.Any()) || Parent?.Parent == null) + { + return false; + } + + return !RemoveOverlap(includedSpans, excludedSpans).Any(); + + bool MatchesFilePattern(FilePattern pattern) => + pattern.Glob.IsMatch(FullPath) || + pattern.Glob.IsMatch(RelativePath); + } + + internal FilePattern Parse(ExcludableString s) + { + IReadOnlyCollection mutantSpans; + + var pattern = s.Pattern; + var mutantSpanGroupMatch = _mutantSpanGroupRegex.Match(s.Pattern); + if (!mutantSpanGroupMatch.Success) + { + // If there are no spans specified, we add one that will cover the whole file. + mutantSpans = new[] { _mutantSpanMaxValue }; + } + else + { + // If we have one or more spans we parse them. + var mutantSpanMatches = _mutantSpanRegex.Matches(mutantSpanGroupMatch.Value); + var allMutantSpans = mutantSpanMatches + .Select(x => new MutantSpan(int.Parse(x.Groups[1].Value), int.Parse(x.Groups[2].Value))) + .Select(FromMutantSpan); + + mutantSpans = + Reduce(allMutantSpans).Select(ToMutantSpan).ToList(); + + pattern = pattern.Substring(0, pattern.Length - mutantSpanGroupMatch.Length); + } + + var glob = Glob.Parse(FilePathUtils.NormalizePathSeparators(pattern)); + + return new FilePattern(glob, s.IsExclude, mutantSpans); + } + + public bool IsMatch(FilePattern pattern, string filePath, MutantSpan span) + { + // Check if the file path is matched. + if (!pattern.Glob.IsMatch(FilePathUtils.NormalizePathSeparators(filePath))) + { + return false; + } + + return IsMatch(pattern, span); + } + + public abstract MutantSpan ToMutantSpan(TSpan span); + + public abstract TSpan FromMutantSpan(MutantSpan span); + + public abstract IEnumerable Reduce(IEnumerable spans); + + public abstract IEnumerable RemoveOverlap(IEnumerable left, IEnumerable right); + + public abstract bool IsMatch(FilePattern pattern, MutantSpan span); + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableString.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableString.cs new file mode 100644 index 0000000000..cca122ee1b --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/ExcludableString.cs @@ -0,0 +1,17 @@ +namespace Stryker.Core.ProjectComponents +{ + public class ExcludableString + { + public ExcludableString(string s) + { + IsExclude = s.StartsWith('!'); + Pattern = IsExclude ? s[1..] : s; + } + + public bool IsExclude { get; } + + public string Pattern { get; } + + public static ExcludableString Parse(string s) => new(s); + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/Fsharp/FsharpFileLeaf.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/Fsharp/FsharpFileLeaf.cs index 7a44d72d90..af27809611 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/Fsharp/FsharpFileLeaf.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/Fsharp/FsharpFileLeaf.cs @@ -1,11 +1,19 @@ using FSharp.Compiler.Syntax; +using FSharp.Compiler.Text; +using Stryker.Core.Helpers; using Stryker.Core.Mutants; using System.Collections.Generic; +using System.Linq; namespace Stryker.Core.ProjectComponents { - public class FsharpFileLeaf : ProjectComponent, IFileLeaf + public class FsharpFileLeaf : ExcludableProjectComponent, IFileLeaf { + // only needed for tests + internal FsharpFileLeaf() : base() { } + + public FsharpFileLeaf(IEnumerable strings) : base(strings) { } + public string SourceCode { get; set; } /// @@ -33,5 +41,28 @@ public override void Display() { DisplayFile(this); } + + public override bool IsMatch(FilePattern pattern, MutantSpan span) + { + var range = FromMutantSpan(span); + + return pattern.MutantSpans.Any(span => RangeModule.rangeContainsRange(FromMutantSpan(span), range)); + } + + public override IEnumerable Reduce(IEnumerable spans) + => RangeHelper.Reduce(FullPath, spans); + + public override IEnumerable RemoveOverlap(IEnumerable left, IEnumerable right) + => left.RemoveOverlap(right, FullPath); + + public override MutantSpan ToMutantSpan(Range range) + { + var startIndex = RangeHelper.GetIndex(SourceCode, range.Start); + var endIndex = RangeHelper.GetIndex(SourceCode, range.End); + return new (startIndex, endIndex); + } + + public override Range FromMutantSpan(MutantSpan span) + => RangeHelper.FromBounds(FullPath, SourceCode, span.Start, span.End); } } diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/IFileLeaf.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/IFileLeaf.cs index c56e1e047d..8d51edd68a 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/IFileLeaf.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/IFileLeaf.cs @@ -3,6 +3,10 @@ namespace Stryker.Core.ProjectComponents public interface IReadOnlyFileLeaf : IReadOnlyProjectComponent { string SourceCode { get; } + + bool IsComponentExcluded(); + + bool IsMatch(MutantSpan span); } public interface IFileLeaf : IFileLeaf diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SimpleFileLeaf.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SimpleFileLeaf.cs new file mode 100644 index 0000000000..77ddf5ede7 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SimpleFileLeaf.cs @@ -0,0 +1,39 @@ +using DotNet.Globbing; +using Stryker.Core.Mutants; +using System.Collections.Generic; +using System.Linq; + +namespace Stryker.Core.ProjectComponents +{ + public class SimpleFileLeaf: ProjectComponent, IReadOnlyFileLeaf + { + public SimpleFileLeaf(IEnumerable strings) + { + Patterns = strings.Select(Parse); + } + + public IEnumerable Patterns { get; set; } + + public string SourceCode { get; set; } + + public override IEnumerable Mutants { get; set; } + + public override void Display() + { + DisplayFile(this); + } + + public override IEnumerable GetAllFiles() => Enumerable.Empty(); + + public bool IsComponentExcluded() => false; + + public bool IsMatch(MutantSpan span) => true; + + private FilePattern Parse(ExcludableString s) + { + var glob = Glob.Parse(FilePathUtils.NormalizePathSeparators(s.Pattern)); + var spans = new[] { new MutantSpan(0, int.MaxValue) }; + return new FilePattern(glob, s.IsExclude, spans); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Reporters/ClearTextReporter.cs b/src/Stryker.Core/Stryker.Core/Reporters/ClearTextReporter.cs index 76e2c1b308..dc6e8e7252 100644 --- a/src/Stryker.Core/Stryker.Core/Reporters/ClearTextReporter.cs +++ b/src/Stryker.Core/Stryker.Core/Reporters/ClearTextReporter.cs @@ -78,7 +78,13 @@ private void DisplayComponent(IReadOnlyProjectComponent inputComponent, Table ta var mutationScore = inputComponent.GetMutationScore(); - if (inputComponent.IsComponentExcluded(_options.Mutate)) + var isExcluded = inputComponent switch + { + IReadOnlyFileLeaf leaf => leaf.IsComponentExcluded(), + _ => false + }; + + if (isExcluded) { columns.Add(new Markup("[Gray]Excluded[/]")); } diff --git a/src/Stryker.Core/Stryker.Core/Reporters/ClearTextTreeReporter.cs b/src/Stryker.Core/Stryker.Core/Reporters/ClearTextTreeReporter.cs index cb342d73e6..1ef32d234d 100644 --- a/src/Stryker.Core/Stryker.Core/Reporters/ClearTextTreeReporter.cs +++ b/src/Stryker.Core/Stryker.Core/Reporters/ClearTextTreeReporter.cs @@ -109,7 +109,13 @@ private string DisplayComponent(IReadOnlyProjectComponent inputComponent) // Convert the threshold integer values to decimal values stringBuilder.Append($" [[{ inputComponent.DetectedMutants().Count()}/{ inputComponent.TotalMutants().Count()} "); - if (inputComponent.IsComponentExcluded(_options.Mutate)) + var isExcluded = inputComponent switch + { + IReadOnlyFileLeaf leaf => leaf.IsComponentExcluded(), + _ => false + }; + + if (isExcluded) { stringBuilder.Append("[Gray](Excluded)[/]"); }