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

Add class level mutation control (for method only) #3131

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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));
}

// find alternative names
var index = 0;
var newNameForOriginal = FindNewName(originalClass, originalMethod, ref index);
var newNameForMutated = FindNewName(originalClass, originalMethod, ref index);

// generates a redirecting method
// generate calls to the redirected 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, Marker),
originalMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForOriginal)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker),
mutatedMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForMutated)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker)]);
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
Loading