-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add class level mutation control (for method only)
- Loading branch information
Showing
11 changed files
with
234 additions
and
6 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters