From 9922117cd48c4f7394a2e73de0b23d3a7ea3475b Mon Sep 17 00:00:00 2001 From: Henry Coles Date: Mon, 22 Jan 2024 17:34:16 +0000 Subject: [PATCH] mutate delayed execution code stored in statics We don't want to mutate code executed only during static initialization. Unfortunately the current filtering also picks up code that is executed after static initialization as lambdas. This change implelements an imperfect comprimise where code stored as Suppliers, Functions etc will be considered for mutation. This will fail to mutate delayed execution code stored in other types, and will incorrectly mutate code that is executed only during initialization if it is within a method that returns a Supplier etc. Although imperfect, it is an improvement. --- .../StaticInitializerInterceptor.java | 89 +++++++++++++++++-- .../StaticFunctionField.java | 14 +++ .../StaticSupplierField.java | 14 +++ .../StaticInitializerInterceptorTest.java | 24 ++++- 4 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java index ed8a23e97..4bd2b09c7 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java @@ -1,6 +1,9 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.pitest.bytecode.analysis.ClassTree; @@ -11,6 +14,13 @@ import org.pitest.mutationtest.engine.Location; import org.pitest.mutationtest.engine.Mutater; import org.pitest.mutationtest.engine.MutationDetails; +import org.pitest.sequence.Context; +import org.pitest.sequence.Match; +import org.pitest.sequence.QueryParams; +import org.pitest.sequence.QueryStart; +import org.pitest.sequence.SequenceMatcher; +import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotWrite; import java.util.Arrays; import java.util.Collection; @@ -18,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -25,6 +36,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.isA; +import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction; +import static org.pitest.sequence.Result.result; + /** * Identifies and marks mutations in code that is active during class * Initialisation. @@ -39,6 +55,21 @@ */ class StaticInitializerInterceptor implements MutationInterceptor { + static final Slot START = Slot.create(AbstractInsnNode.class); + + static final SequenceMatcher DELAYED_EXECUTION = QueryStart + .any(AbstractInsnNode.class) + .then(isA(MethodInsnNode.class).or(isA(InvokeDynamicInsnNode.class)).and(store(START.write()))) + .then(storeToDeferredExecitionField()) + .zeroOrMore(QueryStart.match(anyInstruction())) + .compile(QueryParams.params(AbstractInsnNode.class) + .withIgnores(notAnInstruction()) + ); + + private static Match storeToDeferredExecitionField() { + return (c,n) -> result(n.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) n).desc.startsWith("Ljava/util/function/"), c); + } + private Predicate isStaticInitCode; @Override @@ -73,12 +104,15 @@ private void analyseClass(ClassTree tree) { .map(MethodTree::asLocation) .collect(Collectors.toSet()); + Set storedToSupplier = findsCallsStoredToSuppliers(tree); + // Get map of each private method to the private methods it calls // Any call to a no private method breaks the chain Map> callTree = tree.methods().stream() .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) .flatMap(m -> allCallsFor(tree, m).stream().map(c -> new Call(m.asLocation(), c))) .filter(c -> privateMethods.contains(c.to())) + .filter(c -> !storedToSupplier.contains(c)) .collect(Collectors.groupingBy(Call::from)); Set visited = new HashSet<>(); @@ -89,12 +123,24 @@ private void analyseClass(ClassTree tree) { } } + private Set findsCallsStoredToSuppliers(ClassTree tree) { + return tree.methods().stream() + .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) + .flatMap(m -> delayedExecutionCall(m).stream().map(c -> new Call(m.asLocation(), c))) + .collect(Collectors.toSet()); + } + + private List delayedExecutionCall(MethodTree method) { + Context context = Context.start(); + return DELAYED_EXECUTION.contextMatches(method.instructions(), context).stream() + .map(c -> c.retrieve(START.read()).get()) + .flatMap(this::nodeToLocation) + .collect(Collectors.toList()); + } + private List allCallsFor(ClassTree tree, MethodTree m) { - // temporarily disable dynamic calls as they are more likely to be involved - // in storing delayed execution code within static fields. - return callsFor(tree,m).collect(Collectors.toList()); - // return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m)) - // .collect(Collectors.toList()); + return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m)) + .collect(Collectors.toList()); } private Stream callsFor(ClassTree tree, MethodTree m) { @@ -123,6 +169,18 @@ private void visit(Map> callTree, Set visited, Lo } } + private Stream nodeToLocation(AbstractInsnNode n) { + if (n instanceof MethodInsnNode) { + return Stream.of(asLocation((MethodInsnNode) n)); + } + + if (n instanceof InvokeDynamicInsnNode) { + return asLocation((InvokeDynamicInsnNode) n); + } + + return Stream.empty(); + } + private Location asLocation(MethodInsnNode call) { return Location.location(ClassName.fromString(call.owner), call.name, call.desc); } @@ -166,6 +224,10 @@ public InterceptorType type() { return InterceptorType.FILTER; } + private static Match store(SlotWrite slot) { + return (c, n) -> result(true, c.store(slot, n)); + } + } class Call { @@ -184,4 +246,21 @@ Location from() { Location to() { return to; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Call call = (Call) o; + return Objects.equals(from, call.from) && Objects.equals(to, call.to); + } + + @Override + public int hashCode() { + return Objects.hash(from, to); + } } \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java b/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java new file mode 100644 index 000000000..aef6d53e0 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers; + +import java.util.function.Function; + +public class StaticFunctionField { + private static final Function FOO = canMutate(); + + private static Function canMutate() { + // don't mutate + System.out.println("ideally would mutate me"); + + return s -> s + "foo"; + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java b/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java new file mode 100644 index 000000000..c7a82c0df --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers; + +import java.util.function.Supplier; + +public class StaticSupplierField { + final static Supplier SUPPLER = canMutate(); + + private static Supplier canMutate() { + // don't mutate + System.out.println("ideally would mutate me"); + + return () -> "Do not mutate"; // mutate + } +} diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java index 644f1a972..53a0c39e9 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java @@ -6,6 +6,8 @@ import com.example.staticinitializers.NestedEnumWithLambdaInStaticInitializer; import com.example.staticinitializers.SecondLevelPrivateMethods; import com.example.staticinitializers.SingletonWithWorkInInitializer; +import com.example.staticinitializers.StaticFunctionField; +import com.example.staticinitializers.StaticSupplierField; import com.example.staticinitializers.ThirdLevelPrivateMethods; import org.junit.Ignore; import org.junit.Test; @@ -146,7 +148,6 @@ public void analysisDoesNotGetStuckInInfiniteLoop() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { v.forClass(EnumWithLambdaInConstructor.class) .forMutantsMatching(inMethodStartingWith("doStuff")) @@ -156,7 +157,6 @@ public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { v.forClass(NestedEnumWithLambdaInStaticInitializer.TOYS.class) .forMutantsMatching(inMethodStartingWith("lambda")) @@ -165,6 +165,26 @@ public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { .verify(); } + @Test + public void doesNotSuppressDownStreamMutationsForCodeStoredInSuppliers() { + v.forClass(StaticSupplierField.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotSuppressDownStreamMutationsForCodeStoredInFunctions() { + v.forClass(StaticFunctionField.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + private Predicate inMethod(String name, String desc) { return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc); }