Skip to content

Commit ccb073c

Browse files
committed
feat: add class level mutation control (for method only)
1 parent f3d18d2 commit ccb073c

File tree

10 files changed

+233
-6
lines changed

10 files changed

+233
-6
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using Shouldly;
6+
using Stryker.Core.Instrumentation;
7+
8+
namespace Stryker.Core.UnitTest.Instrumentation;
9+
10+
[TestClass]
11+
public class RedirectMethodEngineShould
12+
{
13+
[TestMethod]
14+
public void InjectSimpleMutatedMethod()
15+
{
16+
const string OriginalClass = """
17+
class Test
18+
{
19+
public void Basic(int x)
20+
{
21+
x++;
22+
}
23+
}
24+
""";
25+
const string MutatedMethod = @"public void Basic(int x) {x--;}";
26+
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
27+
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
28+
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();
29+
30+
var engine = new RedirectMethodEngine();
31+
32+
var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);
33+
34+
injected.Members.Count.ShouldBe(3);
35+
36+
injected.ToString().ShouldBeEquivalentTo("""
37+
class Test
38+
{
39+
public void Basic(int x)
40+
{if(ActiveMutation(2)){Basic_1(x);}else{Basic_0(x);}}
41+
public void Basic_0(int x)
42+
{
43+
x++;
44+
}
45+
public void Basic_1(int x) {x--;}
46+
}
47+
""");
48+
}
49+
50+
[TestMethod]
51+
public void RollbackMutatedMethod()
52+
{
53+
const string OriginalClass = """
54+
class Test
55+
{
56+
public void Basic(int x)
57+
{
58+
x++;
59+
}
60+
}
61+
""";
62+
const string MutatedMethod = @"public void Basic(int x) {x--;}";
63+
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
64+
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
65+
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();
66+
67+
var engine = new RedirectMethodEngine();
68+
var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);
69+
70+
// find the entry point
71+
var mutatedEntry = injected.Members.OfType<MethodDeclarationSyntax>().First( p=> p.Identifier.ToString() == originalMethod.Identifier.ToString());
72+
var rolledBackClass = engine.RemoveInstrumentationFrom(injected ,mutatedEntry);
73+
74+
rolledBackClass.ToString().ShouldBeSemantically(OriginalClass);
75+
}
76+
}

src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ private SyntaxTree RemoveCompileErrorMutations(SyntaxTree originalTree, IEnumera
222222
// find the mutated node in the new tree
223223
var nodeToRemove = trackedTree.GetCurrentNode(brokenMutation);
224224
// remove the mutated node using its MutantPlacer remove method and update the tree
225-
trackedTree = trackedTree.ReplaceNode(nodeToRemove, MutantPlacer.RemoveMutant(nodeToRemove));
225+
trackedTree = MutantPlacer.RemoveMutation(nodeToRemove);
226226
}
227227

228228
return trackedTree.SyntaxTree;

src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,37 @@ public static bool ContainsNodeThatVerifies(this SyntaxNode node, Func<SyntaxNod
197197
return (child.Parent is not AnonymousFunctionExpressionSyntax function || function.ExpressionBody != child)
198198
&& (child.Parent is not LocalFunctionStatementSyntax localFunction || localFunction.ExpressionBody != child);
199199
} ).Any(predicate);
200+
201+
202+
/// <summary>
203+
/// Ensure a statement is in a syntax bock.
204+
/// </summary>
205+
/// <param name="statement">the statement to put into a block.</param>
206+
/// <returns>a block containing <paramref name="statement"/>, or <paramref name="statement"/> if it is already a block</returns>
207+
public static BlockSyntax AsBlock(this StatementSyntax statement) => statement as BlockSyntax ?? SyntaxFactory.Block(statement);
208+
209+
/// <summary>
210+
/// Ensure an expression is in a syntax bock.
211+
/// </summary>
212+
/// <param name="expression">the expression to put into a block.</param>
213+
/// <returns>a block containing <paramref name="expression"/></returns>
214+
public static BlockSyntax AsBlock(this ExpressionSyntax expression) =>SyntaxFactory.ExpressionStatement(expression).AsBlock();
215+
216+
/// <summary>
217+
/// Ensure a <see cref="SyntaxNode"/> is followed by a trailing newline
218+
/// </summary>
219+
/// <typeparam name="T">Type of node, must be a SyntaxNode</typeparam>
220+
/// <param name="node">Node</param>
221+
/// <returns><paramref name="node"/> with a trailing newline</returns>
222+
public static T WithTrailingNewLine<T>(this T node) where T: SyntaxNode
223+
=> node.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
224+
225+
226+
public static ClassDeclarationSyntax RemoveNamedMember(this ClassDeclarationSyntax classNode, string memberName) =>
227+
classNode.RemoveNode(classNode.Members.First( m => m switch
228+
{
229+
MethodDeclarationSyntax method => method.Identifier.ToString() == memberName,
230+
PropertyDeclarationSyntax field => field.Identifier.ToString() == memberName,
231+
_ => false
232+
}), SyntaxRemoveOptions.KeepNoTrivia);
200233
}

src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,10 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
4646
}
4747
throw new InvalidOperationException($"Expected a {typeof(T).Name}, found:\n{node.ToFullString()}.");
4848
}
49+
50+
public virtual SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
51+
{
52+
var restoredNode = RemoveInstrumentation(instrumentation);
53+
return tree.ReplaceNode(instrumentation, restoredNode);
54+
}
4955
}

src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ public interface IInstrumentCode
2121
/// <returns>returns a node without the instrumentation.</returns>
2222
/// <exception cref="InvalidOperationException">if the node was not instrumented (by this instrumentingEngine)</exception>
2323
SyntaxNode RemoveInstrumentation(SyntaxNode node);
24+
25+
SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation);
2426
}

src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.CodeAnalysis;
33
using Microsoft.CodeAnalysis.CSharp;
44
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Stryker.Core.Helpers;
56

67
namespace Stryker.Core.Instrumentation;
78

@@ -20,10 +21,8 @@ internal class IfInstrumentationEngine : BaseEngine<IfStatementSyntax>
2021
/// <remarks>This method works with statement and block.</remarks>
2122
public IfStatementSyntax InjectIf(ExpressionSyntax condition, StatementSyntax originalNode, StatementSyntax mutatedNode) =>
2223
SyntaxFactory.IfStatement(condition,
23-
AsBlock(mutatedNode),
24-
SyntaxFactory.ElseClause(AsBlock(originalNode))).WithAdditionalAnnotations(Marker);
25-
26-
private static BlockSyntax AsBlock(StatementSyntax code) => code as BlockSyntax ?? SyntaxFactory.Block(code);
24+
mutatedNode.AsBlock(),
25+
SyntaxFactory.ElseClause(originalNode.AsBlock())).WithAdditionalAnnotations(Marker);
2726

2827
/// <summary>
2928
/// Returns the original code.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Stryker.Core.Helpers;
7+
8+
namespace Stryker.Core.Instrumentation;
9+
10+
internal class RedirectMethodEngine : BaseEngine<MethodDeclarationSyntax>
11+
{
12+
private const string _redirectHints = "RedirectHints";
13+
14+
public ClassDeclarationSyntax InjectRedirect(ClassDeclarationSyntax originalClass,
15+
ExpressionSyntax condition,
16+
MethodDeclarationSyntax originalMethod,
17+
MethodDeclarationSyntax mutatedMethod)
18+
{
19+
if (!originalClass.Contains(originalMethod))
20+
{
21+
throw new ArgumentException($"Syntax tree does not contains {originalMethod.Identifier}.", nameof(originalMethod));
22+
}
23+
24+
// we need to rename the original method
25+
var index = 0;
26+
var newNameForOriginal = FindNewName(originalClass, originalMethod, ref index);
27+
var newNameForMutated = FindNewName(originalClass, originalMethod, ref index);
28+
29+
// generates a redirecting method
30+
// call to original method
31+
var originalCall = GenerateRedirectedInvocation(originalMethod, newNameForOriginal);
32+
var mutatedCall = GenerateRedirectedInvocation(originalMethod, newNameForMutated);
33+
34+
var redirectHints = new SyntaxAnnotation(_redirectHints, $"{originalMethod.Identifier.ToString()},{newNameForOriginal},{newNameForMutated}");
35+
36+
var redirector = originalMethod
37+
.WithBody(SyntaxFactory.Block(
38+
SyntaxFactory.IfStatement(condition, mutatedCall.AsBlock(),
39+
SyntaxFactory.ElseClause(originalCall.AsBlock())
40+
))).WithExpressionBody(null).WithoutLeadingTrivia();
41+
42+
// update the class
43+
var resultingClass = originalClass.RemoveNode(originalMethod, SyntaxRemoveOptions.KeepNoTrivia)
44+
?.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints),
45+
originalMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForOriginal)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints),
46+
mutatedMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForMutated)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints)]);
47+
return resultingClass;
48+
}
49+
50+
private static InvocationExpressionSyntax GenerateRedirectedInvocation(MethodDeclarationSyntax originalMethod, string redirectedName)
51+
=> SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(redirectedName),
52+
SyntaxFactory.ArgumentList( SyntaxFactory.SeparatedList(
53+
originalMethod.ParameterList.Parameters.Select(p => SyntaxFactory.Argument( SyntaxFactory.IdentifierName(p.Identifier))))));
54+
55+
private static string FindNewName(ClassDeclarationSyntax originalClass, MethodDeclarationSyntax originalMethod, ref int index)
56+
{
57+
string newNameForOriginal;
58+
do
59+
{
60+
newNameForOriginal = $"{originalMethod.Identifier}_{index++}";
61+
}
62+
while (originalClass.Members.Any(m => m is MethodDeclarationSyntax method && method.Identifier.ToFullString() == newNameForOriginal));
63+
return newNameForOriginal;
64+
}
65+
66+
protected override SyntaxNode Revert(MethodDeclarationSyntax node) => throw new NotSupportedException("Cannot revert node in place.");
67+
68+
public override SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
69+
{
70+
var annotation = instrumentation.GetAnnotations(_redirectHints).FirstOrDefault()?.Data;
71+
if (string.IsNullOrEmpty(annotation))
72+
{
73+
throw new InvalidOperationException($"Unable to find details to rollback this instrumentation: '{instrumentation}'");
74+
}
75+
76+
var method = (MethodDeclarationSyntax) instrumentation;
77+
var names = annotation.Split(',').ToList();
78+
79+
80+
var parentClass = (ClassDeclarationSyntax) method.Parent;
81+
var renamedMethod = (MethodDeclarationSyntax) parentClass.Members.
82+
First( m=> m is MethodDeclarationSyntax meth && meth.Identifier.Text == names[1]);
83+
parentClass = parentClass.TrackNodes(renamedMethod);
84+
// we need to remove redirection method and replacement method and restore the name of the original method
85+
parentClass = parentClass.RemoveNamedMember(names[2]).RemoveNamedMember(names[0]);
86+
var oldNode = parentClass.GetCurrentNode(renamedMethod);
87+
parentClass = parentClass.ReplaceNode(oldNode, renamedMethod.WithIdentifier(SyntaxFactory.Identifier(names[0])));
88+
return parentClass;
89+
}
90+
91+
}

src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
9999
return SwitchToThisBodies(typedNode, null, expression).WithoutAnnotations(Marker);
100100
}
101101

102+
public SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
103+
{
104+
var restoredNode = RemoveInstrumentation(instrumentation);
105+
return tree.ReplaceNode(instrumentation, restoredNode);
106+
}
107+
102108
/// <inheritdoc/>
103109
protected override T InjectMutations(T sourceNode, T targetNode, SemanticModel semanticModel, MutationContext context)
104110
{

src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ public static SyntaxNode RemoveMutant(SyntaxNode nodeToRemove)
152152
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
153153
}
154154

155+
public static SyntaxNode RemoveMutation(SyntaxNode nodeToRemove)
156+
{
157+
var annotatedNode = nodeToRemove.GetAnnotatedNodes(Injector).FirstOrDefault();
158+
if (annotatedNode != null)
159+
{
160+
var id = annotatedNode.GetAnnotations(Injector).First().Data;
161+
if (!string.IsNullOrEmpty(id))
162+
{
163+
return instrumentEngines[id].engine.RemoveInstrumentationFrom(nodeToRemove.SyntaxTree.GetRoot(), annotatedNode);
164+
}
165+
}
166+
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
167+
}
168+
155169
/// <summary>
156170
/// Returns true if the node contains a mutation requiring all child mutations to be removed when it has to be removed
157171
/// </summary>

src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
namespace Stryker.Core.Mutants;
1212

13-
1413
/// <summary>
1514
/// This enum is used to track the syntax 'level' of mutations that are injected in the code.
1615
/// </summary>
@@ -143,6 +142,7 @@ public bool StoreMutationsAtDesiredLevel(IEnumerable<Mutant> store, MutationCont
143142
controller.StoreMutations(store);
144143
return true;
145144
}
145+
146146
Logger.LogDebug("There is no structure to control {MutationsCount} mutations. They are dropped.", store.Count());
147147
foreach (var mutant in store)
148148
{

0 commit comments

Comments
 (0)