From 9e456f4683c924a82ec37e2ea376eb814762c0ac Mon Sep 17 00:00:00 2001 From: Henry Coles Date: Mon, 6 Nov 2023 12:28:27 +0000 Subject: [PATCH] consider invoke dynamic calls when detecting static initializer only methods --- .../StaticInitializerInterceptor.java | 49 ++++++++++++++++--- .../EnumWithLambdaInConstructor.java | 24 +++++++++ ...stedEnumWithLambdaInStaticInitializer.java | 30 ++++++++++++ .../SingletonWithWorkInInitializer.java | 5 ++ .../groovy/GroovyFilterFactoryTest.java | 23 +++------ .../StaticInitializerInterceptorTest.java | 47 +++++++++++++++--- 6 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/EnumWithLambdaInConstructor.java create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/NestedEnumWithLambdaInStaticInitializer.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 4ca7ec4f8..e7dbea9a5 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,7 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; -import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.Handle; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.pitest.bytecode.analysis.ClassTree; import org.pitest.bytecode.analysis.MethodTree; @@ -11,6 +12,7 @@ import org.pitest.mutationtest.engine.Mutater; import org.pitest.mutationtest.engine.MutationDetails; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -64,7 +66,6 @@ private void analyseClass(ClassTree tree) { final Optional clinit = tree.methods().stream().filter(nameEquals("")).findFirst(); if (clinit.isPresent()) { - // 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() @@ -72,9 +73,11 @@ private void analyseClass(ClassTree tree) { .map(MethodTree::asLocation) .collect(Collectors.toSet()); + // 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 -> callsFor(tree, m).stream().map(c -> new Call(m.asLocation(), c))) + .flatMap(m -> allCallsFor(tree, m).stream().map(c -> new Call(m.asLocation(), c))) .filter(c -> privateMethods.contains(c.to())) .collect(Collectors.groupingBy(Call::from)); @@ -86,12 +89,27 @@ private void analyseClass(ClassTree tree) { } } - private List callsFor(ClassTree tree, MethodTree m) { + private boolean enumConstructor(MethodTree m, boolean isEnum) { + return isEnum && m.rawNode().name.equals(""); + } + + private List allCallsFor(ClassTree tree, MethodTree m) { + return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m)) + .collect(Collectors.toList()); + } + + private Stream callsFor(ClassTree tree, MethodTree m) { return m.instructions().stream() .flatMap(is(MethodInsnNode.class)) .filter(calls(tree.name())) - .map(this::asLocation) - .collect(Collectors.toList()); + .map(this::asLocation); + } + + private Stream invokeDynamicCallsFor(ClassTree tree, MethodTree m) { + return m.instructions().stream() + .flatMap(is(InvokeDynamicInsnNode.class)) + .filter(callsDynamically(tree.name())) + .flatMap(this::asLocation); } private void visit(Map> callTree, Set visited, Location l) { @@ -114,7 +132,24 @@ private Predicate calls(final ClassName self) { return a -> a.owner.equals(self.asInternalName()); } - private Function> is(final Class clazz) { + private Predicate callsDynamically(final ClassName self) { + return a -> asLocation(a) + .anyMatch(l -> l.getClassName().equals(self)); + + } + + private Stream asLocation(InvokeDynamicInsnNode call) { + return Arrays.stream(call.bsmArgs) + .flatMap(is(Handle.class)) + .flatMap(this::handleToLocation); + } + + private Stream handleToLocation(Handle handle) { + ClassName c = ClassName.fromString(handle.getOwner()); + return Stream.of(Location.location(c, handle.getName(), handle.getDesc())); + } + + private Function> is(final Class clazz) { return a -> { if (a.getClass().isAssignableFrom(clazz)) { return Stream.of((T)a); diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/EnumWithLambdaInConstructor.java b/pitest-entry/src/test/java/com/example/staticinitializers/EnumWithLambdaInConstructor.java new file mode 100644 index 000000000..a4e90d77d --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/EnumWithLambdaInConstructor.java @@ -0,0 +1,24 @@ +package com.example.staticinitializers; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; + +public enum EnumWithLambdaInConstructor { + A(asList("a","b")), + B(asList("a","b", "c")); + + private String s; + + EnumWithLambdaInConstructor(List ss) { + this.s = ss.stream() + .peek(this::doStuff) + .collect(Collectors.joining()); + } + + private void doStuff(String s) { + System.out.println(s); + } + +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/NestedEnumWithLambdaInStaticInitializer.java b/pitest-entry/src/test/java/com/example/staticinitializers/NestedEnumWithLambdaInStaticInitializer.java new file mode 100644 index 000000000..41f5071ee --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/NestedEnumWithLambdaInStaticInitializer.java @@ -0,0 +1,30 @@ +package com.example.staticinitializers; + +import java.util.stream.Stream; + +public class NestedEnumWithLambdaInStaticInitializer { + private String name = "Toto"; + + public NestedEnumWithLambdaInStaticInitializer(){} + + public String getName() { + return name; + } + + public enum TOYS { + BALL("his_ball"), + MONKEY("his_monkey"); + + private static final String[] toys = Stream.of(TOYS.values()).map(TOYS::getLink).toArray(String[]::new); + private String toy; + + TOYS(String theToy) { + this.toy = theToy; + } + + public String getLink() { + return toy; + } + + } +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/SingletonWithWorkInInitializer.java b/pitest-entry/src/test/java/com/example/staticinitializers/SingletonWithWorkInInitializer.java index 6630d4463..5a0893030 100644 --- a/pitest-entry/src/test/java/com/example/staticinitializers/SingletonWithWorkInInitializer.java +++ b/pitest-entry/src/test/java/com/example/staticinitializers/SingletonWithWorkInInitializer.java @@ -14,10 +14,15 @@ public static SingletonWithWorkInInitializer getInstance() { } public boolean isMember6() { + mutateMeCalledFromPublicMethod(); return 6 == num; } private void doNotMutateMethodCalledFromConstructor() { System.out.println("do not mutate"); } + + private void mutateMeCalledFromPublicMethod() { + System.out.println("do not mutate"); + } } \ No newline at end of file diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/groovy/GroovyFilterFactoryTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/groovy/GroovyFilterFactoryTest.java index 7e0629386..3efc8740e 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/groovy/GroovyFilterFactoryTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/groovy/GroovyFilterFactoryTest.java @@ -2,38 +2,31 @@ import org.junit.Test; import org.pitest.mutationtest.build.InterceptorType; -import org.pitest.mutationtest.build.intercept.exclude.FirstLineInterceptorFactory; -import org.pitest.mutationtest.build.intercept.staticinitializers.StaticInitializerInterceptorFactory; -import org.pitest.mutationtest.engine.gregor.mutators.NullMutateEverything; import org.pitest.verifier.interceptors.FactoryVerifier; -import org.pitest.verifier.interceptors.InterceptorVerifier; -import org.pitest.verifier.interceptors.VerifierStart; - -import static org.assertj.core.api.Assertions.assertThat; public class GroovyFilterFactoryTest { @Test public void isOnChain() { - FactoryVerifier.confirmFactory(new FirstLineInterceptorFactory()) + FactoryVerifier.confirmFactory(new GroovyFilterFactory()) .isOnChain(); } @Test - public void isOffByDefault() { - FactoryVerifier.confirmFactory(new FirstLineInterceptorFactory()) - .isOffByDefault(); + public void isOnByDefault() { + FactoryVerifier.confirmFactory(new GroovyFilterFactory()) + .isOnByDefault(); } @Test - public void featureIsCalledNoFirstLine() { - FactoryVerifier.confirmFactory(new FirstLineInterceptorFactory()) - .featureName().isEqualTo("nofirstline"); + public void featureIsCalledFGroovy() { + FactoryVerifier.confirmFactory(new GroovyFilterFactory()) + .featureName().isEqualTo("fgroovy"); } @Test public void createsFilters() { - FactoryVerifier.confirmFactory(new FirstLineInterceptorFactory()) + FactoryVerifier.confirmFactory(new GroovyFilterFactory()) .createsInterceptorsOfType(InterceptorType.FILTER); } } \ No newline at end of file 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 78b4831a4..7e92a1ff3 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,7 +1,9 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import com.example.staticinitializers.BrokenChain; +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.ThirdLevelPrivateMethods; @@ -77,14 +79,25 @@ public void doesNotFilterMutationsInOverriddenMethodsNotInvolvedInStaticInit() { .verify(); } - @Test - public void filtersMutantsInSingletonConstructor() { - v.forClass(SingletonWithWorkInInitializer.class) - .forMutantsMatching(inMethod("", "()V")) - .mutantsAreGenerated() - .allMutantsAreFiltered() - .verify(); - } + @Test + public void filtersMutantsInSingletonConstructor() { + v.forClass(SingletonWithWorkInInitializer.class) + .forMutantsMatching(inMethod("", "()V")) + .mutantsAreGenerated() + .allMutantsAreFiltered() + .verify(); + } + + @Test + public void mutatesPrivateMethodsCalledFromPublicMethodInSingleton() { + v.forClass(SingletonWithWorkInInitializer.class) + .forMutantsMatching(inMethod("mutateMeCalledFromPublicMethod", "()V")) + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test public void filtersMutantsCalledFromPrivateSingletonConstructor() { @@ -131,6 +144,24 @@ public void analysisDoesNotGetStuckInInfiniteLoop() { .verify(); } + @Test + public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { + v.forClass(EnumWithLambdaInConstructor.class) + .forMutantsMatching(inMethodStartingWith("doStuff")) + .mutantsAreGenerated() + .allMutantsAreFiltered() + .verify(); + } + + @Test + public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { + v.forClass(NestedEnumWithLambdaInStaticInitializer.TOYS.class) + .forMutantsMatching(inMethodStartingWith("lambda")) + .mutantsAreGenerated() + .allMutantsAreFiltered() + .verify(); + } + private Predicate inMethod(String name, String desc) { return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc); }