From 70e3fbbe8f858a80f4ec2b3310546e7e2074593f Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 2 Dec 2024 11:37:08 +0100 Subject: [PATCH] feat: add class level mutation control (for method only) --- .../RedirectMethodEngineShould.cs | 76 ++++++++++++++++ .../Mutants/MutantPlacerTests.cs | 1 + .../Compiling/CSharpRollbackProcess.cs | 2 +- .../Stryker.Core/Helpers/RoslynHelper.cs | 33 +++++++ .../Instrumentation/BaseEngine.cs | 6 ++ .../Instrumentation/IInstrumentCode.cs | 2 + .../IfInstrumentationEngine.cs | 7 +- .../Instrumentation/RedirectMethodEngine.cs | 91 +++++++++++++++++++ .../BaseFunctionOrchestrator.cs | 6 ++ .../Stryker.Core/Mutants/MutantPlacer.cs | 14 +++ .../Stryker.Core/Mutants/MutationStore.cs | 2 +- 11 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs create mode 100644 src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs new file mode 100644 index 0000000000..d7974a0394 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs @@ -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().Single(); + var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod); + var originalMethod = parsedClass.Members.OfType().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().Single(); + var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod); + var originalMethod = parsedClass.Members.OfType().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().First( p=> p.Identifier.ToString() == originalMethod.Identifier.ToString()); + var rolledBackClass = engine.RemoveInstrumentationFrom(injected ,mutatedEntry); + + rolledBackClass.ToString().ShouldBeSemantically(OriginalClass); + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/MutantPlacerTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/MutantPlacerTests.cs index 8a522dbec9..49300f7a46 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/MutantPlacerTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/MutantPlacerTests.cs @@ -47,6 +47,7 @@ public void MutantPlacer_ShouldPlaceWithIfStatement(int id) 1 + 8; }"); + var removedResult = MutantPlacer.RemoveMutant(result); removedResult.ToString().ShouldBeSemantically(originalNode.ToString()); diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs index 74b18d7bf1..93af38afd2 100644 --- a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs @@ -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; diff --git a/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs b/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs index da554217a2..d00f160359 100644 --- a/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs +++ b/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs @@ -197,4 +197,37 @@ public static bool ContainsNodeThatVerifies(this SyntaxNode node, Func + /// Ensure a statement is in a syntax bock. + /// + /// the statement to put into a block. + /// a block containing , or if it is already a block + public static BlockSyntax AsBlock(this StatementSyntax statement) => statement as BlockSyntax ?? SyntaxFactory.Block(statement); + + /// + /// Ensure an expression is in a syntax bock. + /// + /// the expression to put into a block. + /// a block containing + public static BlockSyntax AsBlock(this ExpressionSyntax expression) =>SyntaxFactory.ExpressionStatement(expression).AsBlock(); + + /// + /// Ensure a is followed by a trailing newline + /// + /// Type of node, must be a SyntaxNode + /// Node + /// with a trailing newline + public static T WithTrailingNewLine(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); } diff --git a/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs b/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs index ffbf8b4118..2e6865ce1b 100644 --- a/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs +++ b/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs @@ -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); + } } diff --git a/src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs b/src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs index 2817faac10..3f138aa741 100644 --- a/src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs +++ b/src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs @@ -21,4 +21,6 @@ public interface IInstrumentCode /// returns a node without the instrumentation. /// if the node was not instrumented (by this instrumentingEngine) SyntaxNode RemoveInstrumentation(SyntaxNode node); + + SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation); } diff --git a/src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs b/src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs index 89d242a7db..98ca635f7c 100644 --- a/src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs +++ b/src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Stryker.Core.Helpers; namespace Stryker.Core.Instrumentation; @@ -20,10 +21,8 @@ internal class IfInstrumentationEngine : BaseEngine /// This method works with statement and block. 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); /// /// Returns the original code. diff --git a/src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs b/src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs new file mode 100644 index 0000000000..9121d0f6fe --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs @@ -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 +{ + 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; + } + +} diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs index d36a5557ac..cab3cb6e74 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs @@ -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); + } + /// protected override T InjectMutations(T sourceNode, T targetNode, SemanticModel semanticModel, MutationContext context) { diff --git a/src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs b/src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs index a432eee2fa..995bfa8df0 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs @@ -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}'"); + } + /// /// Returns true if the node contains a mutation requiring all child mutations to be removed when it has to be removed /// diff --git a/src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs b/src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs index ca761024b5..69bb66b965 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs @@ -10,7 +10,6 @@ namespace Stryker.Core.Mutants; - /// /// This enum is used to track the syntax 'level' of mutations that are injected in the code. /// @@ -143,6 +142,7 @@ public bool StoreMutationsAtDesiredLevel(IEnumerable 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) {