Skip to content

Commit

Permalink
feat: add class level mutation control (for method only)
Browse files Browse the repository at this point in the history
  • Loading branch information
dupdob committed Dec 2, 2024
1 parent f3d18d2 commit ccb073c
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using Stryker.Core.Instrumentation;

namespace Stryker.Core.UnitTest.Instrumentation;

[TestClass]
public class RedirectMethodEngineShould
{
[TestMethod]
public void InjectSimpleMutatedMethod()
{
const string OriginalClass = """
class Test
{
public void Basic(int x)
{
x++;
}
}
""";
const string MutatedMethod = @"public void Basic(int x) {x--;}";
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();

var engine = new RedirectMethodEngine();

var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);

injected.Members.Count.ShouldBe(3);

injected.ToString().ShouldBeEquivalentTo("""
class Test
{
public void Basic(int x)
{if(ActiveMutation(2)){Basic_1(x);}else{Basic_0(x);}}
public void Basic_0(int x)
{
x++;
}
public void Basic_1(int x) {x--;}
}
""");
}

[TestMethod]
public void RollbackMutatedMethod()
{
const string OriginalClass = """
class Test
{
public void Basic(int x)
{
x++;
}
}
""";
const string MutatedMethod = @"public void Basic(int x) {x--;}";
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();

var engine = new RedirectMethodEngine();
var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);

// find the entry point
var mutatedEntry = injected.Members.OfType<MethodDeclarationSyntax>().First( p=> p.Identifier.ToString() == originalMethod.Identifier.ToString());
var rolledBackClass = engine.RemoveInstrumentationFrom(injected ,mutatedEntry);

rolledBackClass.ToString().ShouldBeSemantically(OriginalClass);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private SyntaxTree RemoveCompileErrorMutations(SyntaxTree originalTree, IEnumera
// find the mutated node in the new tree
var nodeToRemove = trackedTree.GetCurrentNode(brokenMutation);
// remove the mutated node using its MutantPlacer remove method and update the tree
trackedTree = trackedTree.ReplaceNode(nodeToRemove, MutantPlacer.RemoveMutant(nodeToRemove));
trackedTree = MutantPlacer.RemoveMutation(nodeToRemove);
}

return trackedTree.SyntaxTree;
Expand Down
33 changes: 33 additions & 0 deletions src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,37 @@ public static bool ContainsNodeThatVerifies(this SyntaxNode node, Func<SyntaxNod
return (child.Parent is not AnonymousFunctionExpressionSyntax function || function.ExpressionBody != child)
&& (child.Parent is not LocalFunctionStatementSyntax localFunction || localFunction.ExpressionBody != child);
} ).Any(predicate);


/// <summary>
/// Ensure a statement is in a syntax bock.
/// </summary>
/// <param name="statement">the statement to put into a block.</param>
/// <returns>a block containing <paramref name="statement"/>, or <paramref name="statement"/> if it is already a block</returns>
public static BlockSyntax AsBlock(this StatementSyntax statement) => statement as BlockSyntax ?? SyntaxFactory.Block(statement);

/// <summary>
/// Ensure an expression is in a syntax bock.
/// </summary>
/// <param name="expression">the expression to put into a block.</param>
/// <returns>a block containing <paramref name="expression"/></returns>
public static BlockSyntax AsBlock(this ExpressionSyntax expression) =>SyntaxFactory.ExpressionStatement(expression).AsBlock();

/// <summary>
/// Ensure a <see cref="SyntaxNode"/> is followed by a trailing newline
/// </summary>
/// <typeparam name="T">Type of node, must be a SyntaxNode</typeparam>
/// <param name="node">Node</param>
/// <returns><paramref name="node"/> with a trailing newline</returns>
public static T WithTrailingNewLine<T>(this T node) where T: SyntaxNode
=> node.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);


public static ClassDeclarationSyntax RemoveNamedMember(this ClassDeclarationSyntax classNode, string memberName) =>
classNode.RemoveNode(classNode.Members.First( m => m switch
{
MethodDeclarationSyntax method => method.Identifier.ToString() == memberName,
PropertyDeclarationSyntax field => field.Identifier.ToString() == memberName,
_ => false
}), SyntaxRemoveOptions.KeepNoTrivia);
}
6 changes: 6 additions & 0 deletions src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
}
throw new InvalidOperationException($"Expected a {typeof(T).Name}, found:\n{node.ToFullString()}.");
}

public virtual SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var restoredNode = RemoveInstrumentation(instrumentation);
return tree.ReplaceNode(instrumentation, restoredNode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public interface IInstrumentCode
/// <returns>returns a node without the instrumentation.</returns>
/// <exception cref="InvalidOperationException">if the node was not instrumented (by this instrumentingEngine)</exception>
SyntaxNode RemoveInstrumentation(SyntaxNode node);

SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Stryker.Core.Helpers;

namespace Stryker.Core.Instrumentation;

Expand All @@ -20,10 +21,8 @@ internal class IfInstrumentationEngine : BaseEngine<IfStatementSyntax>
/// <remarks>This method works with statement and block.</remarks>
public IfStatementSyntax InjectIf(ExpressionSyntax condition, StatementSyntax originalNode, StatementSyntax mutatedNode) =>
SyntaxFactory.IfStatement(condition,
AsBlock(mutatedNode),
SyntaxFactory.ElseClause(AsBlock(originalNode))).WithAdditionalAnnotations(Marker);

private static BlockSyntax AsBlock(StatementSyntax code) => code as BlockSyntax ?? SyntaxFactory.Block(code);
mutatedNode.AsBlock(),
SyntaxFactory.ElseClause(originalNode.AsBlock())).WithAdditionalAnnotations(Marker);

/// <summary>
/// Returns the original code.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Stryker.Core.Helpers;

namespace Stryker.Core.Instrumentation;

internal class RedirectMethodEngine : BaseEngine<MethodDeclarationSyntax>
{
private const string _redirectHints = "RedirectHints";

public ClassDeclarationSyntax InjectRedirect(ClassDeclarationSyntax originalClass,
ExpressionSyntax condition,
MethodDeclarationSyntax originalMethod,
MethodDeclarationSyntax mutatedMethod)
{
if (!originalClass.Contains(originalMethod))
{
throw new ArgumentException($"Syntax tree does not contains {originalMethod.Identifier}.", nameof(originalMethod));
}

// we need to rename the original method
var index = 0;
var newNameForOriginal = FindNewName(originalClass, originalMethod, ref index);
var newNameForMutated = FindNewName(originalClass, originalMethod, ref index);

// generates a redirecting method
// call to original method
var originalCall = GenerateRedirectedInvocation(originalMethod, newNameForOriginal);
var mutatedCall = GenerateRedirectedInvocation(originalMethod, newNameForMutated);

var redirectHints = new SyntaxAnnotation(_redirectHints, $"{originalMethod.Identifier.ToString()},{newNameForOriginal},{newNameForMutated}");

var redirector = originalMethod
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(condition, mutatedCall.AsBlock(),
SyntaxFactory.ElseClause(originalCall.AsBlock())
))).WithExpressionBody(null).WithoutLeadingTrivia();

// update the class
var resultingClass = originalClass.RemoveNode(originalMethod, SyntaxRemoveOptions.KeepNoTrivia)
?.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints),
originalMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForOriginal)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints),
mutatedMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForMutated)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints)]);
return resultingClass;
}

private static InvocationExpressionSyntax GenerateRedirectedInvocation(MethodDeclarationSyntax originalMethod, string redirectedName)
=> SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(redirectedName),
SyntaxFactory.ArgumentList( SyntaxFactory.SeparatedList(
originalMethod.ParameterList.Parameters.Select(p => SyntaxFactory.Argument( SyntaxFactory.IdentifierName(p.Identifier))))));

private static string FindNewName(ClassDeclarationSyntax originalClass, MethodDeclarationSyntax originalMethod, ref int index)
{
string newNameForOriginal;
do
{
newNameForOriginal = $"{originalMethod.Identifier}_{index++}";
}
while (originalClass.Members.Any(m => m is MethodDeclarationSyntax method && method.Identifier.ToFullString() == newNameForOriginal));
return newNameForOriginal;
}

protected override SyntaxNode Revert(MethodDeclarationSyntax node) => throw new NotSupportedException("Cannot revert node in place.");

public override SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var annotation = instrumentation.GetAnnotations(_redirectHints).FirstOrDefault()?.Data;
if (string.IsNullOrEmpty(annotation))
{
throw new InvalidOperationException($"Unable to find details to rollback this instrumentation: '{instrumentation}'");
}

var method = (MethodDeclarationSyntax) instrumentation;
var names = annotation.Split(',').ToList();


var parentClass = (ClassDeclarationSyntax) method.Parent;
var renamedMethod = (MethodDeclarationSyntax) parentClass.Members.
First( m=> m is MethodDeclarationSyntax meth && meth.Identifier.Text == names[1]);
parentClass = parentClass.TrackNodes(renamedMethod);
// we need to remove redirection method and replacement method and restore the name of the original method
parentClass = parentClass.RemoveNamedMember(names[2]).RemoveNamedMember(names[0]);
var oldNode = parentClass.GetCurrentNode(renamedMethod);
parentClass = parentClass.ReplaceNode(oldNode, renamedMethod.WithIdentifier(SyntaxFactory.Identifier(names[0])));
return parentClass;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
return SwitchToThisBodies(typedNode, null, expression).WithoutAnnotations(Marker);
}

public SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var restoredNode = RemoveInstrumentation(instrumentation);
return tree.ReplaceNode(instrumentation, restoredNode);
}

/// <inheritdoc/>
protected override T InjectMutations(T sourceNode, T targetNode, SemanticModel semanticModel, MutationContext context)
{
Expand Down
14 changes: 14 additions & 0 deletions src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ public static SyntaxNode RemoveMutant(SyntaxNode nodeToRemove)
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
}

public static SyntaxNode RemoveMutation(SyntaxNode nodeToRemove)
{
var annotatedNode = nodeToRemove.GetAnnotatedNodes(Injector).FirstOrDefault();
if (annotatedNode != null)
{
var id = annotatedNode.GetAnnotations(Injector).First().Data;
if (!string.IsNullOrEmpty(id))
{
return instrumentEngines[id].engine.RemoveInstrumentationFrom(nodeToRemove.SyntaxTree.GetRoot(), annotatedNode);
}
}
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
}

/// <summary>
/// Returns true if the node contains a mutation requiring all child mutations to be removed when it has to be removed
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

namespace Stryker.Core.Mutants;


/// <summary>
/// This enum is used to track the syntax 'level' of mutations that are injected in the code.
/// </summary>
Expand Down Expand Up @@ -143,6 +142,7 @@ public bool StoreMutationsAtDesiredLevel(IEnumerable<Mutant> store, MutationCont
controller.StoreMutations(store);
return true;
}

Logger.LogDebug("There is no structure to control {MutationsCount} mutations. They are dropped.", store.Count());
foreach (var mutant in store)
{
Expand Down

0 comments on commit ccb073c

Please sign in to comment.