Skip to content

Commit

Permalink
Decoupling file excludability from mutant spans
Browse files Browse the repository at this point in the history
  • Loading branch information
psfinaki committed Aug 18, 2023
1 parent 0884dc1 commit 9e6ee57
Show file tree
Hide file tree
Showing 24 changed files with 388 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Shouldly;
using Stryker.Core.MutantFilters;
using Stryker.Core.Mutants;
using Stryker.Core.Options;
using Stryker.Core.ProjectComponents;
using Xunit;

Expand All @@ -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");
}

Expand All @@ -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(
Expand All @@ -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);
Expand Down
20 changes: 11 additions & 9 deletions src/Stryker.Core/Stryker.Core.UnitTest/Options/FilePatternTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using DotNet.Globbing;
using Microsoft.CodeAnalysis.Text;
using Shouldly;
using Stryker.Core.ProjectComponents;
using Xunit;

namespace Stryker.Core.UnitTest.Options
Expand All @@ -23,11 +24,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);
Expand All @@ -45,10 +46,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);
Expand All @@ -62,17 +64,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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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("*");
}
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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("**", "*"));
Expand All @@ -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"));
Expand All @@ -48,14 +49,13 @@ 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"));
result.First().IsExclude.ShouldBeTrue();
result.Last().Glob.ToString().ShouldBe(Path.Combine("**", "*"));
result.Last().IsExclude.ShouldBeFalse();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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("") },
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
public class ProjectComponentExtensionsTests : TestBase
public class TextSpanHelperTests : TestBase
{
[Theory]
[InlineData(new int[0], new int[0])]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Stryker.Core.Exceptions;
using Stryker.Core.Mutants;
using Stryker.Core.Options;
using Stryker.Core.ProjectComponents;

namespace Stryker.Core.DiffProviders
{
Expand Down Expand Up @@ -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();
Expand Down
91 changes: 7 additions & 84 deletions src/Stryker.Core/Stryker.Core/FilePattern.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <summary>
/// Contains information about which files and which parts of a file should be in- or excluded.
/// </summary>
public sealed class FilePattern : IEquatable<FilePattern>
{
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<TextSpan> textSpans)
public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection<MutantSpan> mutantSpans)
{
Glob = glob;
IsExclude = isExclude;
TextSpans = textSpans;
MutantSpans = mutantSpans;
}

/// <summary>
Expand All @@ -37,81 +32,9 @@ public FilePattern(Glob glob, bool isExclude, IReadOnlyCollection<TextSpan> text
/// <summary>
/// Gets the the text spans of the file this pattern matches.
/// </summary>
public IReadOnlyCollection<TextSpan> TextSpans { get; }

/// <summary>
/// Parses a given file pattern string.
/// Format: (!)&lt;glob&gt;({&lt;spanStart&gt;..&lt;spanEnd&gt;})*
/// </summary>
/// <param name="pattern">The pattern to parse.</param>
/// <returns>The <see cref="FilePattern"/></returns>
public static FilePattern Parse(string pattern) => Parse(pattern, spansEnabled: true);

/// <summary>
/// Parses a given file pattern string.
/// Format: (!)&lt;glob&gt;({&lt;spanStart&gt;..&lt;spanEnd&gt;})*
/// </summary>
/// <param name="pattern">The pattern to parse.</param>
/// <param name="spansEnabled">Enable or disable span parsing.</param>
/// <returns>The <see cref="FilePattern"/></returns>
public static FilePattern Parse(string pattern, bool spansEnabled)
{
var exclude = false;
IReadOnlyCollection<TextSpan> 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);
}

/// <summary>
/// Checks whether a given file path and span matches the current file pattern.
/// </summary>
/// <param name="filePath">The full file path.</param>
/// <param name="textSpan">The span of the text to check.</param>
/// <returns>True if the file and span matches the pattern.</returns>
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<MutantSpan> 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)
{
Expand Down Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit 9e6ee57

Please sign in to comment.