Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Refactor C# mutation orchestration to a simpler design #2831

Merged
merged 36 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
17e6a5f
WIP
dupdob Dec 2, 2023
2986819
wip
dupdob Dec 14, 2023
d5d45e7
Merge branch 'master' into refactor_orchestration
dupdob Dec 14, 2023
a2c44e2
wip
dupdob Dec 16, 2023
36dd277
Merge branch 'master' into refactor_orchestration
dupdob Jan 9, 2024
0e833ac
wip
dupdob Jan 11, 2024
ef53d9f
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Jan 12, 2024
7bf6443
wip
dupdob Jan 14, 2024
cf3c159
wip: fix last failing tests
dupdob Jan 15, 2024
85e1aa1
wip: slight simplifications
dupdob Jan 16, 2024
9ee086f
chore: cleanup
dupdob Jan 16, 2024
759e2e2
chore: fix sonar warnings
dupdob Jan 16, 2024
7140833
chore: minor refactoring and code slimming
dupdob Jan 16, 2024
4ffd903
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Jan 16, 2024
8d31fd8
chore: redesign method/accessor/functions orchestrator
dupdob Jan 21, 2024
d458290
fix: sonar issues
dupdob Jan 21, 2024
2138f15
chore: remove useless engines and adjust related tests
dupdob Jan 21, 2024
30bce7a
chore: simplify further
dupdob Jan 21, 2024
adada5b
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Jan 22, 2024
97d69de
chore: fixes after end to end tests
dupdob Jan 23, 2024
3595fb0
chore: clean up code and add comments
dupdob Jan 26, 2024
f7a877e
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Jan 29, 2024
014af1f
chore: keep on refining design
dupdob Jan 31, 2024
6fbb419
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 2, 2024
38b3f3b
fix: correct spacing in Mutation Orchestration Design.md
dupdob Feb 3, 2024
d89f12d
misc: improve the new document about mutation orchestration
dupdob Feb 5, 2024
b2bd437
Apply suggestions from code review
dupdob Feb 7, 2024
cffeaa5
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 7, 2024
e00fcce
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 15, 2024
c0ff0b6
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 16, 2024
73b7cf8
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 16, 2024
5ad2cf4
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 22, 2024
fe5ea05
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Feb 26, 2024
e93bbb6
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Mar 1, 2024
f78a179
Apply suggestions from code review
dupdob Mar 1, 2024
aad490b
Merge remote-tracking branch 'origin/master' into refactor_orchestration
dupdob Mar 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 327 additions & 0 deletions docs/technical-reference/Mutation Orchestration Design.md

Large diffs are not rendered by default.

20 changes: 6 additions & 14 deletions src/Stryker.Core/Stryker.Core.UnitTest/AssertExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ public static class AssertionExtensions
/// </summary>
/// <param name="actual">Resulted code</param>
/// <param name="expected">Expected code</param>
public static void ShouldBeSemantically(this string actual, string expected)
{
ShouldBeSemantically(CSharpSyntaxTree.ParseText(actual).GetRoot(), CSharpSyntaxTree.ParseText(expected).GetRoot());
}
public static void ShouldBeSemantically(this string actual, string expected) => ShouldBeSemantically(CSharpSyntaxTree.ParseText(actual), CSharpSyntaxTree.ParseText(expected));

/// <summary>
/// Compares two syntax trees and asserts equality
Expand All @@ -29,14 +26,9 @@ public static void ShouldBeSemantically(this string actual, string expected)
/// <param name="expected">Expected code</param>
/// <remarks>Warning: this code tries to pinpoint the first different lines, but it will work on string comparison, so it may pinpoint spaces
/// or new line variations, hiding the real differences.</remarks>
public static void ShouldBeSemantically(this SyntaxNode actual, SyntaxNode expected)
public static void ShouldBeSemantically(this SyntaxTree actual, SyntaxTree expected)
{
// for some reason, nodes can be different while being textually the same
if (actual.ToFullString() == expected.ToFullString())
{
return;
}
var isSame = SyntaxFactory.AreEquivalent(actual, expected);
var isSame = actual.IsEquivalentTo(expected);

if (!isSame)
{
Expand All @@ -63,12 +55,12 @@ public static void ShouldBeWithNewlineReplace(this string actual, string expecte
actual.ShouldBe(replaced);
}

public static void ShouldNotContainErrors(this SyntaxNode actual)
public static void ShouldNotContainErrors(this SyntaxTree actual)
{
var errors = actual.SyntaxTree.GetDiagnostics().Count(x => x.Severity == DiagnosticSeverity.Error);
var errors = actual.GetDiagnostics().Count(x => x.Severity == DiagnosticSeverity.Error);
if (errors > 0)
{
errors.ShouldBe(0, $"Actual code has build errors!\n{actual.ToFullString()}\nerrors: {string.Join(Environment.NewLine, actual.SyntaxTree.GetDiagnostics())}");
errors.ShouldBe(0, $"Actual code has build errors!\n{actual.GetText()}\nerrors: {string.Join(Environment.NewLine, actual.GetDiagnostics())}");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ public void SomeLinq()
helpers.Add(CSharpSyntaxTree.ParseText(code, path: name, encoding: Encoding.UTF32));
}

var mutant = mutator.Mutate(syntaxTree.GetRoot(), null);
helpers.Add(mutant.SyntaxTree);
var mutant = mutator.Mutate(syntaxTree, null);
helpers.Add(mutant);

var references = new List<string> {
typeof(object).Assembly.Location,
Expand Down Expand Up @@ -203,8 +203,9 @@ private void RefreshAccountNumber()
helpers.Add(CSharpSyntaxTree.ParseText(code, path: name, encoding: Encoding.UTF32));
}

var mutant = mutator.Mutate(syntaxTree.GetRoot(), null);
helpers.Add(mutant.SyntaxTree);
var mutant = mutator.Mutate(syntaxTree, null);

helpers.Add(mutant);

var references = new List<string> {
typeof(object).Assembly.Location,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Shouldly;
Expand Down Expand Up @@ -105,7 +104,7 @@ public void ShouldMutateBlockStatements()
expected = @"private int Move()
{if(StrykerNamespace.MutantControl.IsActive(1)){}else {
;
} return default(int);}";
}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -114,10 +113,10 @@ public void ShouldMutateBlockStatements()
public void ShouldAddReturnDefaultToOperator()
{
string source = @"public static string operator+ (TestClass value, TestClass other)
{;
{ while(true) return value;
}";
string expected = @"public static string operator+ (TestClass value, TestClass other)
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{;
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{ while((StrykerNamespace.MutantControl.IsActive(2)?!(true):(StrykerNamespace.MutantControl.IsActive(1)?false:true))) return value;
}return default(string);}";
ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -154,10 +153,11 @@ public void ShouldProperlyMutatePrefixUnitaryExpressionStatement()
public void ShouldMutateExpressionBodiedLocalFunction()
{
string source = @"void TestMethod(){
void SomeMethod() => (true && SomeOtherMethod(out var x)) ? x : 5;
int SomeMethod() => (true && SomeOtherMethod(out var x)) ? x : 5;
}";
string expected = @"void TestMethod(){if(StrykerNamespace.MutantControl.IsActive(0)){}else{
void SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(1)){return!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(2)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{((StrykerNamespace.MutantControl.IsActive(3)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}};}}";
int SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(1)){return!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(2)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{return ((StrykerNamespace.MutantControl.IsActive(3)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}};}}
}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -500,9 +500,9 @@ string SomeLocalFunction()
public void ShouldMutateConditionalExpressionOnArrayDeclaration()
{
var source =
@"public static IEnumerable<int> Foo() => new int[] { }.ToArray().Any(x => x==1)?.OrderBy(e => e).ToList();";
@"public static IEnumerable<int> Foo() => new int[] { }.ToArray()!.Any(x => x==1)?.OrderBy(e => e).ToList();";
var expected =
@"public static IEnumerable<int> Foo() => (StrykerNamespace.MutantControl.IsActive(2)?new int[] { }.ToArray().Any(x => x==1)?.OrderByDescending(e => e).ToList():(StrykerNamespace.MutantControl.IsActive(0)?new int[] { }.ToArray().All(x => x==1):new int[] { }.ToArray().Any(x => (StrykerNamespace.MutantControl.IsActive(1)?x!=1:x==1)))?.OrderBy(e => e).ToList());";
@"public static IEnumerable<int> Foo() => (StrykerNamespace.MutantControl.IsActive(2)?new int[] { }.ToArray()!.Any(x => x==1)?.OrderByDescending(e => e).ToList():(StrykerNamespace.MutantControl.IsActive(0)?new int[] { }.ToArray()!.All(x => x==1):new int[] { }.ToArray()!.Any(x => (StrykerNamespace.MutantControl.IsActive(1)?x!=1:x==1)))?.OrderBy(e => e).ToList());";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -515,7 +515,7 @@ public void ShouldMutateSuppressNullableWarningExpressionOnArrayDeclaration()
var employeePerson = group.First().Entitlement!.Employee.Person;}";
var expected =
@"public static void Foo(){if(StrykerNamespace.MutantControl.IsActive(0)){}else{
var employeePerson = (StrykerNamespace.MutantControl.IsActive(1)?group.FirstOrDefault().Entitlement!.Employee.Person:group.First().Entitlement!.Employee.Person);}}";
var employeePerson = (StrykerNamespace.MutantControl.IsActive(1)?group.FirstOrDefault():group.First()).Entitlement!.Employee.Person;}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -557,7 +557,7 @@ public void ShouldMutateArrayInitializer()
int[] test = {};
}else{
int[] test = { 1 };
}}return default(int[]);}";
}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -729,7 +729,7 @@ public void ShouldMutateComplexExpressionBodiedMethod()
[Fact]
public void ShouldMutateExpressionBodiedStaticConstructor()
{
string source = @"static Test() => (true && SomeOtherMethod(out var x)) ? x : 5;";
string source = @"static Test() => (true && SomeOtherMethod(out var x)) ? x : 5;";
string expected =
@"static Test() {using(new StrykerNamespace.MutantContext()){if(StrykerNamespace.MutantControl.IsActive(0)){!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(1)){(true || SomeOtherMethod(out var x)) ? x : 5;}else{((StrykerNamespace.MutantControl.IsActive(2)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}}}";

Expand Down Expand Up @@ -1222,7 +1222,7 @@ public void MutationsShouldHaveLinespan()
{
var test3 = 2 + 5;
}";
_target.Mutate(CSharpSyntaxTree.ParseText(source).GetRoot(), null);
_target.Mutate(CSharpSyntaxTree.ParseText(source), null);

var mutants = _target.GetLatestMutantBatch().ToList();
mutants.Count.ShouldBe(2);
Expand All @@ -1249,7 +1249,7 @@ public static bool InvokeIfNotNull(this Action a)
}
}
}";
_target.Mutate(CSharpSyntaxTree.ParseText(source).GetRoot(), null);
_target.Mutate(CSharpSyntaxTree.ParseText(source), null);

var mutants = _target.GetLatestMutantBatch().ToList();
mutants.Count.ShouldBe(7);
Expand All @@ -1268,11 +1268,11 @@ public void ShouldAddReturnDefaultToMethods()
{
string source = @"bool TestMethod()
{
;
while(true) return false;
}";
string expected = @"bool TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
;
while((StrykerNamespace.MutantControl.IsActive(2)?!(true):(StrykerNamespace.MutantControl.IsActive(1)?false:true))) return (StrykerNamespace.MutantControl.IsActive(3)?true:false);
}return default(bool);}";
ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -1281,10 +1281,10 @@ public void ShouldAddReturnDefaultToMethods()
public void ShouldAddReturnDefaultToConversion()
{
string source = @"public static explicit operator string(TestClass value)
{;
{ while(true) return value;
}";
string expected = @"public static explicit operator string(TestClass value)
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{;
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{ while((StrykerNamespace.MutantControl.IsActive(2)?!(true):(StrykerNamespace.MutantControl.IsActive(1)?false:true))) return value;
}return default(string);}";
ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -1303,12 +1303,10 @@ public void ShouldNotMutateConstDeclaration()
public void ShouldAddReturnDefaultToAsyncMethods()
{
string source = @"public async Task<bool> TestMethod()
{
;
{ while(true) return false;
}";
string expected = @"public async Task<bool> TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
;
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{ while((StrykerNamespace.MutantControl.IsActive(2)?!(true):(StrykerNamespace.MutantControl.IsActive(1)?false:true))) return (StrykerNamespace.MutantControl.IsActive(3)?true:false);
}return default(bool);}";
ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -1366,12 +1364,10 @@ public static IEnumerable<object> Extracting<T>(this IEnumerable<T> enumerable)
public void ShouldAddReturnDefaultToAsyncWithFullNamespaceMethods()
{
string source = @"public async System.Threading.Tasks.Task<bool> TestMethod()
{
;
{ while(true) return false;
}";
string expected = @"public async System.Threading.Tasks.Task<bool> TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
;
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{ while((StrykerNamespace.MutantControl.IsActive(2)?!(true):(StrykerNamespace.MutantControl.IsActive(1)?false:true))) return (StrykerNamespace.MutantControl.IsActive(3)?true:false);
}return default(bool);}";
ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -1404,6 +1400,21 @@ public void ShouldNotAddReturnDefaultToMethodsWithReturnTypeVoid()
ShouldMutateSourceInClassToExpected(source, expected);
}

[Theory]
[InlineData("{return 0;}")]
[InlineData("{return 1; throw Exception();}")]
[InlineData("{return 1;yield return 0;}")]
[InlineData("{return 1;yield break;}")]
[InlineData("{;}")]
public void ShouldNotAddReturnOnSomeBlocks(string code)
{
string source = @$"
int TestMethod()
// Stryker disable all
{code}";
ShouldMutateSourceInClassToExpected(source, source);
}

[Fact]
public void ShouldSkipStringsInSwitchExpression()
{
Expand Down Expand Up @@ -1478,7 +1489,7 @@ public void ShouldMutatePropertiesInArrowFormEvenWithComplexConstruction()
static TestClass(){}}";

var expected = @"class Test {
string Value {get {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Out(out var x))? ""empty"": """";}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(1)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");}return default(string);}}
string Value {get {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Out(out var x))? ""empty"": """";}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(1)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");}}}
static TestClass(){using(new StrykerNamespace.MutantContext()){}}}";

ShouldMutateSourceInClassToExpected(source, expected);
Expand All @@ -1496,8 +1507,7 @@ static string Value
static TestClass(){}";

var expected = @"static string Value
{
get {if(StrykerNamespace.MutantControl.IsActive(0)){}else{ return (StrykerNamespace.MutantControl.IsActive(1)?"""":""TestDescription"");
{get {if(StrykerNamespace.MutantControl.IsActive(0)){}else{ return (StrykerNamespace.MutantControl.IsActive(1)?"""":""TestDescription"");
}
return default(string);
}
Expand All @@ -1508,7 +1518,7 @@ static string Value
ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
[Fact]
public void ShouldMarkStaticMutationStaticInPropertiesInitializer()
{
var source = @"class Test {
Expand All @@ -1521,6 +1531,18 @@ public void ShouldMarkStaticMutationStaticInPropertiesInitializer()
_target.Mutants.First().IsStaticValue.ShouldBeTrue();
}

[Fact]
public void ShouldMutateStaticPropertiesInArrowFormEvenWithComplexConstruction()
{
var source = @"class Test {
static string Value => Out(out var x)? ""empty"": """";}";

var expected = @"class Test {
static string Value {get {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Out(out var x))? ""empty"": """";}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(1)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");}}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
public void ShouldMutatePropertiesAsArrowExpression()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public MutantOrchestratorTestsBase()

protected void ShouldMutateSourceToExpected(string actual, string expected)
{
var actualNode = _target.Mutate(CSharpSyntaxTree.ParseText(actual).GetRoot(), null);
actual = actualNode.ToFullString();
var actualNode = _target.Mutate(CSharpSyntaxTree.ParseText(actual), null);
actual = actualNode.GetRoot().ToFullString();
actual = actual.Replace(_injector.HelperNamespace, "StrykerNamespace");
actualNode = CSharpSyntaxTree.ParseText(actual).GetRoot();
var expectedNode = CSharpSyntaxTree.ParseText(expected).GetRoot();
actualNode = CSharpSyntaxTree.ParseText(actual);
var expectedNode = CSharpSyntaxTree.ParseText(expected);
actualNode.ShouldBeSemantically(expectedNode);
actualNode.ShouldNotContainErrors();
}
Expand Down
Loading
Loading