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 000000000..d7974a039 --- /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/Stryker.Core.UnitTest.csproj b/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj index 609aedb5a..a08b5922d 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj @@ -2,35 +2,6 @@ false - - - - - - - - - - - - - Always - - - Always - - - Always - - - Always - - - - - Always - - diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs index 0f1a8d5c4..0d9cf3ffd 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 45c5bd79d..3367427b3 100644 --- a/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs +++ b/src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs @@ -199,6 +199,38 @@ 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); /// /// Cleaned trivia from a node /// diff --git a/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs b/src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs index ffbf8b411..2e6865ce1 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 2817faac1..3f138aa74 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 992b96ce0..96037267d 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,13 +21,11 @@ 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.WithoutTrivia()))). + mutatedNode.AsBlock(), + SyntaxFactory.ElseClause(originalNode.WithoutTrivia().AsBlock())). WithTriviaFrom(originalNode). WithAdditionalAnnotations(Marker); - private static BlockSyntax AsBlock(StatementSyntax code) => code as BlockSyntax ?? SyntaxFactory.Block(code); - /// /// 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 000000000..033a1e814 --- /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)); + } + + // 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; + } + +} diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs index d36a5557a..cab3cb6e7 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 cde1c1993..5dfc88f38 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 f035610e6..d46beff83 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) { diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/Stryker.TestRunner.VsTest.UnitTest.csproj b/src/Stryker.TestRunner.VsTest.UnitTest/Stryker.TestRunner.VsTest.UnitTest.csproj index 8d90c7324..6b6c5a6c1 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/Stryker.TestRunner.VsTest.UnitTest.csproj +++ b/src/Stryker.TestRunner.VsTest.UnitTest/Stryker.TestRunner.VsTest.UnitTest.csproj @@ -6,6 +6,31 @@ enable + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleSourceFile.cs b/src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleSourceFile.cs similarity index 100% rename from src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleSourceFile.cs rename to src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleSourceFile.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFileA.cs b/src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFileA.cs similarity index 100% rename from src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFileA.cs rename to src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFileA.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFileB.cs b/src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFileB.cs similarity index 100% rename from src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFileB.cs rename to src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFileB.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFilePreprocessorSymbols.cs b/src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFilePreprocessorSymbols.cs similarity index 100% rename from src/Stryker.Core/Stryker.Core.UnitTest/TestResources/ExampleTestFilePreprocessorSymbols.cs rename to src/Stryker.TestRunner.VsTest.UnitTest/TestResources/ExampleTestFilePreprocessorSymbols.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestResources/StrongNameKeyFile.snk b/src/Stryker.TestRunner.VsTest.UnitTest/TestResources/StrongNameKeyFile.snk similarity index 100% rename from src/Stryker.Core/Stryker.Core.UnitTest/TestResources/StrongNameKeyFile.snk rename to src/Stryker.TestRunner.VsTest.UnitTest/TestResources/StrongNameKeyFile.snk diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs index a5de818dd..ad7e1dcef 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs +++ b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs @@ -54,7 +54,7 @@ public class VsTestMockingHelper : TestBase public VsTestMockingHelper() { - var currentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var currentDirectory = ".";Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); _filesystemRoot = Path.GetPathRoot(currentDirectory); var sourceFile = File.ReadAllText(currentDirectory + "/TestResources/ExampleSourceFile.cs");