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..6c5f9052c 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,14 @@ 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.SequenceQuery; +import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotWrite; import java.util.Arrays; import java.util.Collection; @@ -18,6 +29,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 +37,12 @@ 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.methodCallNamed; +import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction; +import static org.pitest.bytecode.analysis.OpcodeMatchers.PUTSTATIC; +import static org.pitest.sequence.Result.result; + /** * Identifies and marks mutations in code that is active during class * Initialisation. @@ -39,6 +57,48 @@ */ class StaticInitializerInterceptor implements MutationInterceptor { + static final Slot START = Slot.create(AbstractInsnNode.class); + + static final SequenceMatcher DELAYED_EXECUTION = QueryStart + .any(AbstractInsnNode.class) + // look for calls returning delayed execution types. Unfortunately this is not guarantee that we + // store the result to an appropriate type + .then(returnsDeferredExecutionCode().or(dynamicallyReturnsDeferredExecutionCode()).and(store(START.write()))) + // allow for other method calls etc + .zeroOrMore(QueryStart.match(anyInstruction())) + .then(enumConstructorCallAndStore().or(QueryStart.match(delayedExecutionField()))) + .zeroOrMore(QueryStart.match(anyInstruction())) + .compile(QueryParams.params(AbstractInsnNode.class) + .withIgnores(notAnInstruction()) + ); + + private static Match delayedExecutionField() { + return PUTSTATIC.and(isAUtilFunctionField()); + } + + private static Match isAUtilFunctionField() { + return (c,n) -> { + FieldInsnNode fieldNode = ((FieldInsnNode) n); + return result( fieldNode.desc.startsWith("Ljava/util/function/"), c); + }; + } + + private static Match dynamicallyReturnsDeferredExecutionCode() { + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c); + } + + private static Match returnsDeferredExecutionCode() { + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnDelayedExecutionType(((MethodInsnNode) n).desc), c); + } + + private static boolean returnDelayedExecutionType(String desc) { + int endOfParams = desc.indexOf(')'); + return endOfParams <= 0 || desc.substring(endOfParams + 1).startsWith("Ljava/util/function/"); + } + + private static SequenceQuery enumConstructorCallAndStore() { + return QueryStart.match(methodCallNamed("")).then(PUTSTATIC); + } private Predicate isStaticInitCode; @Override @@ -69,32 +129,53 @@ private void analyseClass(ClassTree tree) { // We can't see if a method *call* is private from the call site // so collect a set of private methods within the class first Set privateMethods = tree.methods().stream() - .filter(m -> m.isPrivate()) + .filter(MethodTree::isPrivate) .map(MethodTree::asLocation) .collect(Collectors.toSet()); + Set storedToSupplier = findCallsStoredToDelayedExecutionCode(tree); + // Get map of each private method to the private methods it calls - // Any call to a no private method breaks the chain + // Any call to a non 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<>(); - visit(callTree, visited, clinit.get().asLocation()); + Set calledOnlyFromStaticInitializer = new HashSet<>(); + + visit(callTree, calledOnlyFromStaticInitializer, clinit.get().asLocation()); - this.isStaticInitCode = m -> visited.contains(m.getId().getLocation()); + this.isStaticInitCode = m -> calledOnlyFromStaticInitializer.contains(m.getId().getLocation()); } } + private Set findCallsStoredToDelayedExecutionCode(ClassTree tree) { + return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree)); + } + + + private Set privateAndClinitCallsToDelayedExecutionCode(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 +204,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 +259,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 +281,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/delayedexecution/EnumFieldMethodRef.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumFieldMethodRef.java new file mode 100644 index 000000000..ef83c0928 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumFieldMethodRef.java @@ -0,0 +1,18 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.function.Supplier; + +public enum EnumFieldMethodRef { + A(EnumFieldMethodRef::canMutate), B(EnumFieldMethodRef::canMutate); + + private final Supplier supplier; + + + EnumFieldMethodRef(Supplier supplier) { + this.supplier = supplier; + } + + private static String canMutate() { + return "Do not mutate"; + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumFieldSupplier.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumFieldSupplier.java new file mode 100644 index 000000000..2dffdb64a --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumFieldSupplier.java @@ -0,0 +1,20 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.function.Supplier; + +public enum EnumFieldSupplier { + A(canMutate()), B(canMutate()); + + private final Supplier supplier; + + EnumFieldSupplier(Supplier supplier) { + this.supplier = supplier; + } + + 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/com/example/staticinitializers/delayedexecution/EnumMethodReferenceNotStored.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumMethodReferenceNotStored.java new file mode 100644 index 000000000..ec998203e --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumMethodReferenceNotStored.java @@ -0,0 +1,20 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public enum EnumMethodReferenceNotStored { + A(Arrays.asList(1,2,3)); + + private final List l; + EnumMethodReferenceNotStored(List list) { + l = list.stream() + .filter(this::doNotMutate) + .collect(Collectors.toList()); + } + + private boolean doNotMutate(Integer i) { + return i > 2; + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumMixedFields.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumMixedFields.java new file mode 100644 index 000000000..ae8e18324 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumMixedFields.java @@ -0,0 +1,23 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.function.Supplier; + +public enum EnumMixedFields { + A(EnumMixedFields::canMutate, doNotMutate()), B(EnumMixedFields::canMutate, doNotMutate()); + + private final Supplier supplier; + private final String s; + + EnumMixedFields(Supplier supplier, String s) { + this.supplier = supplier; + this.s = s; + } + + private static String canMutate() { + return "mutate me"; // mutate + } + + private static String doNotMutate() { + return "Do not mutate"; + } +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticFunctionField.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticFunctionField.java new file mode 100644 index 000000000..fb4a3dae3 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticFunctionField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers.delayedexecution; + +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/delayedexecution/StaticSupplierField.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticSupplierField.java new file mode 100644 index 000000000..55fd5cb3a --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticSupplierField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers.delayedexecution; + +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..8822bd49d 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 @@ -1,13 +1,18 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import com.example.staticinitializers.BrokenChain; +import com.example.staticinitializers.delayedexecution.EnumFieldMethodRef; +import com.example.staticinitializers.delayedexecution.EnumFieldSupplier; import com.example.staticinitializers.EnumWithLambdaInConstructor; import com.example.staticinitializers.MethodsCallsEachOtherInLoop; import com.example.staticinitializers.NestedEnumWithLambdaInStaticInitializer; import com.example.staticinitializers.SecondLevelPrivateMethods; import com.example.staticinitializers.SingletonWithWorkInInitializer; +import com.example.staticinitializers.delayedexecution.EnumMethodReferenceNotStored; +import com.example.staticinitializers.delayedexecution.EnumMixedFields; +import com.example.staticinitializers.delayedexecution.StaticFunctionField; +import com.example.staticinitializers.delayedexecution.StaticSupplierField; import com.example.staticinitializers.ThirdLevelPrivateMethods; -import org.junit.Ignore; import org.junit.Test; import org.pitest.mutationtest.engine.MutationDetails; import org.pitest.mutationtest.engine.gregor.mutators.NullMutateEverything; @@ -146,7 +151,6 @@ public void analysisDoesNotGetStuckInInfiniteLoop() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { v.forClass(EnumWithLambdaInConstructor.class) .forMutantsMatching(inMethodStartingWith("doStuff")) @@ -156,7 +160,6 @@ public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { v.forClass(NestedEnumWithLambdaInStaticInitializer.TOYS.class) .forMutantsMatching(inMethodStartingWith("lambda")) @@ -165,6 +168,77 @@ 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(); + } + + @Test + public void doesNotSuppressDownStreamMutationsForEnumFieldSuppliers() { + v.forClass(EnumFieldSupplier.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + + @Test + public void doesNotSuppressDownStreamMutationsForMethodRefsStoredToEnumFields() { + v.forClass(EnumFieldMethodRef.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotSuppressDownStreamMutationsForMethodRefsStoredToEnumFieldsWhenOtherFieldsInitialized() { + v.forClass(EnumMixedFields.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void suppressesMutationsForStringsStoredToEnumFields() { + v.forClass(EnumMixedFields.class) + .forMethod("doNotMutate") + .forAnyCode() + .mutantsAreGenerated() + .allMutantsAreFiltered() + .verify(); + } + + @Test + public void filtersMutationsForMethodReferencesUsedInEnumConstructor() { + v.forClass(EnumMethodReferenceNotStored.class) + .forMethod("doNotMutate") + .forAnyCode() + .mutantsAreGenerated() + .allMutantsAreFiltered() + .verify(); + } + private Predicate inMethod(String name, String desc) { return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc); } diff --git a/pitest/src/main/java/org/pitest/mutationtest/MutationStatusTestPair.java b/pitest/src/main/java/org/pitest/mutationtest/MutationStatusTestPair.java index 8889508e8..52d01b6ae 100644 --- a/pitest/src/main/java/org/pitest/mutationtest/MutationStatusTestPair.java +++ b/pitest/src/main/java/org/pitest/mutationtest/MutationStatusTestPair.java @@ -31,6 +31,12 @@ public final class MutationStatusTestPair implements Serializable { private final List coveringTests; + @Deprecated + // for backwards compatibility. Remove at next major release + public static MutationStatusTestPair notAnalysed(int testsRun, DetectionStatus status) { + return notAnalysed(testsRun, status, Collections.emptyList()); + } + public static MutationStatusTestPair notAnalysed(int testsRun, DetectionStatus status, List coveringTests) { return new MutationStatusTestPair(testsRun, status, Collections.emptyList(), Collections.emptyList(), coveringTests); }