diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/CompoundInterceptorFactory.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/CompoundInterceptorFactory.java index cccc4d34a..ecab99aab 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/CompoundInterceptorFactory.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/CompoundInterceptorFactory.java @@ -1,6 +1,7 @@ package org.pitest.mutationtest.build; import org.pitest.classinfo.ClassByteArraySource; +import org.pitest.classpath.CodeSource; import org.pitest.coverage.CoverageDatabase; import org.pitest.mutationtest.config.ReportOptions; import org.pitest.plugin.FeatureSelector; @@ -24,10 +25,11 @@ public CompoundMutationInterceptor createInterceptor( ReportOptions data, CoverageDatabase coverage, ClassByteArraySource source, - TestPrioritiser testPrioritiser + TestPrioritiser testPrioritiser, + CodeSource code ) { - final List interceptors = this.features.getActiveFeatures().stream() - .map(toInterceptor(this.features, data, coverage, source, testPrioritiser)) + List interceptors = this.features.getActiveFeatures().stream() + .map(toInterceptor(this.features, data, coverage, source, testPrioritiser, code)) .collect(Collectors.toList()); return new CompoundMutationInterceptor(interceptors); } @@ -38,10 +40,11 @@ private static Function toInter ReportOptions data, CoverageDatabase coverage, ClassByteArraySource source, - TestPrioritiser testPrioritiser + TestPrioritiser testPrioritiser, + CodeSource code ) { - return a -> a.createInterceptor(new InterceptorParameters(features.getSettingForFeature(a.provides().name()), data, coverage, source, testPrioritiser)); + return a -> a.createInterceptor(new InterceptorParameters(features.getSettingForFeature(a.provides().name()), data, coverage, source, testPrioritiser, code)); } } \ No newline at end of file diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/InterceptorParameters.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/InterceptorParameters.java index 09a94fe0b..cc36e6903 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/InterceptorParameters.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/InterceptorParameters.java @@ -1,6 +1,7 @@ package org.pitest.mutationtest.build; import org.pitest.classinfo.ClassByteArraySource; +import org.pitest.classpath.CodeSource; import org.pitest.coverage.CoverageDatabase; import org.pitest.mutationtest.config.ReportOptions; import org.pitest.plugin.FeatureParameter; @@ -16,17 +17,23 @@ public final class InterceptorParameters { private final ReportOptions data; private final ClassByteArraySource source; private final CoverageDatabase coverage; + private final CodeSource code; private final TestPrioritiser testPrioritiser; - public InterceptorParameters(FeatureSetting conf, ReportOptions data, CoverageDatabase coverage, - ClassByteArraySource source, TestPrioritiser testPrioritiser) { + public InterceptorParameters(FeatureSetting conf, + ReportOptions data, + CoverageDatabase coverage, + ClassByteArraySource source, + TestPrioritiser testPrioritiser, + CodeSource code) { this.conf = conf; this.data = data; this.coverage = coverage; this.source = source; this.testPrioritiser = testPrioritiser; + this.code = code; } public ReportOptions data() { @@ -50,6 +57,10 @@ public TestPrioritiser testPrioritiser() { return this.testPrioritiser; } + public CodeSource code() { + return code; + } + public Optional getString(FeatureParameter limit) { if (this.conf == null) { return Optional.empty(); diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScanner.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScanner.java new file mode 100644 index 000000000..d777d5593 --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScanner.java @@ -0,0 +1,30 @@ +package org.pitest.mutationtest.build.intercept.staticinitializers; + +import org.objectweb.asm.tree.AnnotationNode; +import org.pitest.bytecode.analysis.ClassTree; +import org.pitest.classpath.CodeSource; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FunctionalInterfaceScanner implements Function> { + @Override + public Set apply(CodeSource codeSource) { + return codeSource.codeTrees() + .filter(this::isFunctionalInterface) + .map(c -> c.rawNode().name) + .collect(Collectors.toSet()); + } + + private boolean isFunctionalInterface(ClassTree classTree) { + List annotations = classTree.rawNode().visibleAnnotations; + if (annotations == null) { + return false; + } + + return annotations.stream() + .anyMatch(a -> a.desc.equals("Ljava/lang/FunctionalInterface;")); + } +} 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 6c5f9052c..28e81bcca 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 @@ -2,10 +2,13 @@ import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; +import org.pitest.bytecode.SignatureParser; import org.pitest.bytecode.analysis.ClassTree; import org.pitest.bytecode.analysis.MethodTree; import org.pitest.classinfo.ClassName; @@ -21,6 +24,7 @@ import org.pitest.sequence.SequenceMatcher; import org.pitest.sequence.SequenceQuery; import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotRead; import org.pitest.sequence.SlotWrite; import java.util.Arrays; @@ -53,47 +57,63 @@ * 1. In a static initializer (i.e ) * 2. In a private method or constructor called from or another private method in the call tree * - * */ class StaticInitializerInterceptor implements MutationInterceptor { - static final Slot START = Slot.create(AbstractInsnNode.class); + private final Set delayedExecutionTypes; + + private static final Slot START = Slot.create(AbstractInsnNode.class); + private static final Slot> DELAYED_EXECUTION_FIELDS = Slot.createSet(String.class); - static final SequenceMatcher DELAYED_EXECUTION = QueryStart + private final SequenceMatcher delayedExecution = 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()))) + .then(enumConstructorCallAndStore().or(QueryStart.match(delayedExecutionField(DELAYED_EXECUTION_FIELDS.read())))) .zeroOrMore(QueryStart.match(anyInstruction())) .compile(QueryParams.params(AbstractInsnNode.class) .withIgnores(notAnInstruction()) ); - private static Match delayedExecutionField() { - return PUTSTATIC.and(isAUtilFunctionField()); + StaticInitializerInterceptor(Set delayedExecutionTypes) { + this.delayedExecutionTypes = delayedExecutionTypes; + } + + private static Match delayedExecutionField(SlotRead> delayedFields) { + return PUTSTATIC.and(isADelayedExecutionField(delayedFields)); } - private static Match isAUtilFunctionField() { + private static Match isADelayedExecutionField(SlotRead> delayedFields) { return (c,n) -> { FieldInsnNode fieldNode = ((FieldInsnNode) n); - return result( fieldNode.desc.startsWith("Ljava/util/function/"), c); + return result( c.retrieve(delayedFields).get().contains(fieldNode.name), c); }; } - private static Match dynamicallyReturnsDeferredExecutionCode() { - return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c); + private Match dynamicallyReturnsDeferredExecutionCode() { + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnsDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c); } - private static Match returnsDeferredExecutionCode() { - return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnDelayedExecutionType(((MethodInsnNode) n).desc), c); + private Match returnsDeferredExecutionCode() { + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnsDelayedExecutionType(((MethodInsnNode) n).desc), c); + } + + private boolean returnsDelayedExecutionType(String desc) { + Type returnType = Type.getReturnType(desc); + // fixme Arrays? + if (returnType.getSort() != Type.OBJECT) { + return false; + } + + return isADelayedExecutionType(returnType.getInternalName()); } - private static boolean returnDelayedExecutionType(String desc) { - int endOfParams = desc.indexOf(')'); - return endOfParams <= 0 || desc.substring(endOfParams + 1).startsWith("Ljava/util/function/"); + + private boolean isADelayedExecutionType(String type) { + return delayedExecutionTypes.contains(type); } private static SequenceQuery enumConstructorCallAndStore() { @@ -126,6 +146,15 @@ private void analyseClass(ClassTree tree) { final Optional clinit = tree.methods().stream().filter(nameEquals("")).findFirst(); if (clinit.isPresent()) { + + // Find delayed execution fields (Function, Supplier, List etc). Full generic signature is available in the + // declaration, but not on call. + Set delayedExecutionFields = tree.rawNode().fields.stream() + .filter(this::isDelayedExecutionField) + .map(n -> n.name) + .collect(Collectors.toSet()); + + // 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() @@ -133,7 +162,7 @@ private void analyseClass(ClassTree tree) { .map(MethodTree::asLocation) .collect(Collectors.toSet()); - Set storedToSupplier = findCallsStoredToDelayedExecutionCode(tree); + Set storedToSupplier = findCallsStoredToDelayedExecutionCode(tree, delayedExecutionFields); // Get map of each private method to the private methods it calls // Any call to a non private method breaks the chain @@ -153,21 +182,26 @@ private void analyseClass(ClassTree tree) { } } - private Set findCallsStoredToDelayedExecutionCode(ClassTree tree) { - return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree)); + private boolean isDelayedExecutionField(FieldNode fieldNode) { + return SignatureParser.extractTypes(fieldNode.signature).stream() + .anyMatch(this::isADelayedExecutionType); + } + + private Set findCallsStoredToDelayedExecutionCode(ClassTree tree, Set delayedExecutionFields) { + return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree, delayedExecutionFields)); } - private Set privateAndClinitCallsToDelayedExecutionCode(ClassTree tree) { + private Set privateAndClinitCallsToDelayedExecutionCode(ClassTree tree, Set delayedExecutionFields) { return tree.methods().stream() .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) - .flatMap(m -> delayedExecutionCall(m).stream().map(c -> new Call(m.asLocation(), c))) + .flatMap(m -> delayedExecutionCall(m, delayedExecutionFields).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() + private List delayedExecutionCall(MethodTree method, Set delayedExecutionFields) { + Context context = Context.start().store(DELAYED_EXECUTION_FIELDS.write(), delayedExecutionFields); + return delayedExecution.contextMatches(method.instructions(), context).stream() .map(c -> c.retrieve(START.read()).get()) .flatMap(this::nodeToLocation) .collect(Collectors.toList()); diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorFactory.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorFactory.java index afcaac29a..2057f010c 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorFactory.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorFactory.java @@ -5,22 +5,42 @@ import org.pitest.mutationtest.build.MutationInterceptorFactory; import org.pitest.plugin.Feature; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; + public class StaticInitializerInterceptorFactory implements MutationInterceptorFactory { - @Override - public String description() { - return "Static initializer code detector plugin"; - } + @Override + public String description() { + return "Static initializer code detector plugin"; + } + + @Override + public MutationInterceptor createInterceptor(InterceptorParameters params) { + return new StaticInitializerInterceptor(functionalInterfaces()); + } - @Override - public MutationInterceptor createInterceptor(InterceptorParameters params) { - return new StaticInitializerInterceptor(); - } + @Override + public Feature provides() { + return Feature.named("FSTATI") + .withOnByDefault(true) + .withDescription("Filters mutations in static initializers and code called only from them"); + } - @Override - public Feature provides() { - return Feature.named("FSTATI") - .withOnByDefault(true) - .withDescription("Filters mutations in static initializers and code called only from them"); - } + private Set functionalInterfaces() { + Set classes = new HashSet<>(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/functional_interfaces.txt")))) { + String line = r.readLine(); + while (line != null) { + classes.add(line); + line = r.readLine(); + } + return classes; + } catch (IOException e) { + throw new RuntimeException("Could not read embedded list of functional interfaces!"); + } + } } diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/MutationCoverage.java b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/MutationCoverage.java index 2529233d0..c776404a5 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/MutationCoverage.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/MutationCoverage.java @@ -65,7 +65,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Predicate; -import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -337,7 +336,7 @@ private List buildMutationTests(CoverageDatabase coverageD coverageData); final MutationInterceptor interceptor = this.settings.getInterceptor() - .createInterceptor(this.data, coverageData, bas, testPrioritiser) + .createInterceptor(this.data, coverageData, bas, testPrioritiser, code) .filter(interceptorFilter); interceptor.initialise(this.code); @@ -377,9 +376,8 @@ private CoverageGenerator coverage() { return this.strategies.coverage(); } - // For reasons not yet understood classes from rt.jar are not resolved for some - // projects during static analysis phase. For now fall back to the classloader when - // a class not provided by project classpath + // Since java 9 rt.jar is no longer on the classpath so jdk classes will not resolve from + // the filesystem and must be pulled out via the classloader private ClassByteArraySource fallbackToClassLoader(final ClassByteArraySource bas) { final ClassByteArraySource clSource = ClassloaderByteArraySource.fromContext(); return clazz -> { @@ -387,7 +385,6 @@ private ClassByteArraySource fallbackToClassLoader(final ClassByteArraySource ba if (maybeBytes.isPresent()) { return maybeBytes; } - LOG.log(Level.FINE, "Could not find " + clazz + " on classpath for analysis. Falling back to classloader"); return clSource.getBytes(clazz); }; } diff --git a/pitest-entry/src/main/java/org/pitest/sequence/Slot.java b/pitest-entry/src/main/java/org/pitest/sequence/Slot.java index 06da33173..798eb7028 100644 --- a/pitest-entry/src/main/java/org/pitest/sequence/Slot.java +++ b/pitest-entry/src/main/java/org/pitest/sequence/Slot.java @@ -1,6 +1,7 @@ package org.pitest.sequence; import java.util.List; +import java.util.Set; public final class Slot { public static Slot create(Class clazz) { @@ -11,6 +12,10 @@ public static Slot> createList(Class clazz) { return new Slot<>(); } + public static Slot> createSet(Class clazz) { + return new Slot<>(); + } + public SlotWrite write() { return new SlotWrite<>(this); } diff --git a/pitest-entry/src/main/resources/functional_interfaces.txt b/pitest-entry/src/main/resources/functional_interfaces.txt new file mode 100644 index 000000000..25eee8bf9 --- /dev/null +++ b/pitest-entry/src/main/resources/functional_interfaces.txt @@ -0,0 +1,158 @@ +java/io/FileFilter +java/io/FilenameFilter +java/io/ObjectInputFilter +java/lang/Runnable +java/util/Comparator +java/util/concurrent/Callable +java/util/function/BiConsumer +java/util/function/BiFunction +java/util/function/BiPredicate +java/util/function/BinaryOperator +java/util/function/BooleanSupplier +java/util/function/Consumer +java/util/function/DoubleBinaryOperator +java/util/function/DoubleConsumer +java/util/function/DoubleFunction +java/util/function/DoublePredicate +java/util/function/DoubleSupplier +java/util/function/DoubleToIntFunction +java/util/function/DoubleToLongFunction +java/util/function/DoubleUnaryOperator +java/util/function/Function +java/util/function/IntBinaryOperator +java/util/function/IntConsumer +java/util/function/IntFunction +java/util/function/IntPredicate +java/util/function/IntSupplier +java/util/function/IntToDoubleFunction +java/util/function/IntToLongFunction +java/util/function/IntUnaryOperator +java/util/function/LongBinaryOperator +java/util/function/LongConsumer +java/util/function/LongFunction +java/util/function/LongPredicate +java/util/function/LongSupplier +java/util/function/LongToDoubleFunction +java/util/function/LongToIntFunction +java/util/function/LongUnaryOperator +java/util/function/ObjDoubleConsumer +java/util/function/ObjIntConsumer +java/util/function/ObjLongConsumer +java/util/function/Predicate +java/util/function/Supplier +java/util/function/ToDoubleBiFunction +java/util/function/ToDoubleFunction +java/util/function/ToIntBiFunction +java/util/function/ToIntFunction +java/util/function/ToLongBiFunction +java/util/function/ToLongFunction +java/util/function/UnaryOperator +java/util/stream/DoubleStream$DoubleMapMultiConsumer +java/util/stream/IntStream$IntMapMultiConsumer +java/util/stream/LongStream$LongMapMultiConsumer +org/pitest/sequence/Match +org/apache/commons/text/lookup/BiStringLookup +org/apache/commons/text/lookup/StringLookup +org/apache/commons/lang3/Functions$FailableBiConsumer +org/apache/commons/lang3/Functions$FailableBiFunction +org/apache/commons/lang3/Functions$FailableBiPredicate +org/apache/commons/lang3/Functions$FailableCallable +org/apache/commons/lang3/Functions$FailableConsumer +org/apache/commons/lang3/Functions$FailableFunction +org/apache/commons/lang3/Functions$FailablePredicate +org/apache/commons/lang3/Functions$FailableRunnable +org/apache/commons/lang3/Functions$FailableSupplier +org/apache/commons/lang3/ThreadUtils$ThreadGroupPredicate +org/apache/commons/lang3/ThreadUtils$ThreadPredicate +org/apache/commons/lang3/function/FailableBiConsumer +org/apache/commons/lang3/function/FailableBiFunction +org/apache/commons/lang3/function/FailableBiPredicate +org/apache/commons/lang3/function/FailableBooleanSupplier +org/apache/commons/lang3/function/FailableCallable +org/apache/commons/lang3/function/FailableConsumer +org/apache/commons/lang3/function/FailableDoubleBinaryOperator +org/apache/commons/lang3/function/FailableDoubleConsumer +org/apache/commons/lang3/function/FailableDoubleFunction +org/apache/commons/lang3/function/FailableDoublePredicate +org/apache/commons/lang3/function/FailableDoubleSupplier +org/apache/commons/lang3/function/FailableDoubleToIntFunction +org/apache/commons/lang3/function/FailableDoubleToLongFunction +org/apache/commons/lang3/function/FailableFunction +org/apache/commons/lang3/function/FailableIntBinaryOperator +org/apache/commons/lang3/function/FailableIntConsumer +org/apache/commons/lang3/function/FailableIntFunction +org/apache/commons/lang3/function/FailableIntPredicate +org/apache/commons/lang3/function/FailableIntSupplier +org/apache/commons/lang3/function/FailableIntToDoubleFunction +org/apache/commons/lang3/function/FailableIntToLongFunction +org/apache/commons/lang3/function/FailableLongBinaryOperator +org/apache/commons/lang3/function/FailableLongConsumer +org/apache/commons/lang3/function/FailableLongFunction +org/apache/commons/lang3/function/FailableLongPredicate +org/apache/commons/lang3/function/FailableLongSupplier +org/apache/commons/lang3/function/FailableLongToDoubleFunction +org/apache/commons/lang3/function/FailableLongToIntFunction +org/apache/commons/lang3/function/FailableObjDoubleConsumer +org/apache/commons/lang3/function/FailableObjIntConsumer +org/apache/commons/lang3/function/FailableObjLongConsumer +org/apache/commons/lang3/function/FailablePredicate +org/apache/commons/lang3/function/FailableRunnable +org/apache/commons/lang3/function/FailableShortSupplier +org/apache/commons/lang3/function/FailableSupplier +org/apache/commons/lang3/function/FailableToDoubleBiFunction +org/apache/commons/lang3/function/FailableToDoubleFunction +org/apache/commons/lang3/function/FailableToIntBiFunction +org/apache/commons/lang3/function/FailableToIntFunction +org/apache/commons/lang3/function/FailableToLongBiFunction +org/apache/commons/lang3/function/FailableToLongFunction +org/apache/commons/lang3/function/ToBooleanBiFunction +org/apache/commons/lang3/function/TriFunction +com/google/errorprone/matchers/Matcher +com/google/common/base/Function +com/google/common/base/Predicate +com/google/common/base/Supplier +com/google/common/cache/RemovalListener +com/google/common/cache/Weigher +com/google/common/collect/Maps$EntryTransformer +com/google/common/util/concurrent/AsyncCallable +com/google/common/util/concurrent/AsyncFunction +com/google/common/util/concurrent/ClosingFuture$AsyncClosingCallable +com/google/common/util/concurrent/ClosingFuture$AsyncClosingFunction +com/google/common/util/concurrent/ClosingFuture$ClosingCallable +com/google/common/util/concurrent/ClosingFuture$ClosingFunction +com/google/common/util/concurrent/ClosingFuture$Combiner$AsyncCombiningCallable +com/google/common/util/concurrent/ClosingFuture$Combiner$CombiningCallable +com/google/common/util/concurrent/ClosingFuture$Combiner2$AsyncClosingFunction2 +com/google/common/util/concurrent/ClosingFuture$Combiner2$ClosingFunction2 +com/google/common/util/concurrent/ClosingFuture$Combiner3$AsyncClosingFunction3 +com/google/common/util/concurrent/ClosingFuture$Combiner3$ClosingFunction3 +com/google/common/util/concurrent/ClosingFuture$Combiner4$AsyncClosingFunction4 +com/google/common/util/concurrent/ClosingFuture$Combiner4$ClosingFunction4 +com/google/common/util/concurrent/ClosingFuture$Combiner5$AsyncClosingFunction5 +com/google/common/util/concurrent/ClosingFuture$Combiner5$ClosingFunction5 +com/google/common/util/concurrent/ClosingFuture$ValueAndCloserConsumer +kotlin/Function +kotlin/jvm/functions/Function0 +kotlin/jvm/functions/Function1 +kotlin/jvm/functions/Function10 +kotlin/jvm/functions/Function11 +kotlin/jvm/functions/Function12 +kotlin/jvm/functions/Function13 +kotlin/jvm/functions/Function14 +kotlin/jvm/functions/Function15 +kotlin/jvm/functions/Function16 +kotlin/jvm/functions/Function17 +kotlin/jvm/functions/Function18 +kotlin/jvm/functions/Function19 +kotlin/jvm/functions/Function2 +kotlin/jvm/functions/Function20 +kotlin/jvm/functions/Function21 +kotlin/jvm/functions/Function22 +kotlin/jvm/functions/Function3 +kotlin/jvm/functions/Function4 +kotlin/jvm/functions/Function5 +kotlin/jvm/functions/Function6 +kotlin/jvm/functions/Function7 +kotlin/jvm/functions/Function8 +kotlin/jvm/functions/Function9 +kotlin/jvm/functions/FunctionN diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunction.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunction.java new file mode 100644 index 000000000..1bd897418 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunction.java @@ -0,0 +1,6 @@ +package com.example.staticinitializers.delayedexecution; + +@FunctionalInterface +public interface CustomFunction { + R apply(T t); +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunctionNotAnnotated.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunctionNotAnnotated.java new file mode 100644 index 000000000..4aad1e7ed --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/CustomFunctionNotAnnotated.java @@ -0,0 +1,6 @@ +package com.example.staticinitializers.delayedexecution; + +// NOT annotated as a functional interface +public interface CustomFunctionNotAnnotated { + R apply(T t); +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java new file mode 100644 index 000000000..4a12684b8 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java @@ -0,0 +1,21 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.List; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; + +public enum EnumListOfSuppliers { + A(EnumListOfSuppliers::canMutate), B(EnumListOfSuppliers::canMutate); + + private final List> supplier; + + EnumListOfSuppliers(Supplier supplier) { + this.supplier = asList(supplier); + } + + private static String canMutate() { + return "mutate me"; // mutate + } + +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctionalInterface.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctionalInterface.java new file mode 100644 index 000000000..dac8bb08f --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctionalInterface.java @@ -0,0 +1,19 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.List; + +import static java.util.Arrays.asList; + +public class StaticListOfFunctionalInterface { + public static final List> FUNCTIONS = + asList(StaticListOfFunctionalInterface::canMutate, StaticListOfFunctionalInterface::canAlsoMutate); + + private static Integer canMutate(Integer a) { + return a + 1; + } + + private static Integer canAlsoMutate(Integer a) { + return a + 2; + } +} + diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java new file mode 100644 index 000000000..cea2da4c4 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java @@ -0,0 +1,19 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.function.Function; +import java.util.List; + +import static java.util.Arrays.asList; + +public class StaticListOfFunctions { + public static final List> FUNCTIONS = + asList(StaticListOfFunctions::canMutate, StaticListOfFunctions::canAlsoMutate); + + private static Integer canMutate(Integer a) { + return a + 1; + } + + private static Integer canAlsoMutate(Integer a) { + return a + 2; + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfUnannotatedInterfaces.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfUnannotatedInterfaces.java new file mode 100644 index 000000000..ccd220eb8 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfUnannotatedInterfaces.java @@ -0,0 +1,18 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.List; + +import static java.util.Arrays.asList; + +public class StaticListOfUnannotatedInterfaces { + public static final List> FUNCTIONS = + asList(StaticListOfUnannotatedInterfaces::canMutate, StaticListOfUnannotatedInterfaces::canAlsoMutate); + + private static Integer canMutate(Integer a) { + return a + 1; + } + + private static Integer canAlsoMutate(Integer a) { + return a + 2; + } + } \ No newline at end of file diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/FixedCodeSource.java b/pitest-entry/src/test/java/org/pitest/mutationtest/FixedCodeSource.java new file mode 100644 index 000000000..75dd7ed59 --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/FixedCodeSource.java @@ -0,0 +1,89 @@ +package org.pitest.mutationtest; + +import org.pitest.bytecode.analysis.ClassTree; +import org.pitest.classinfo.ClassHash; +import org.pitest.classinfo.ClassName; +import org.pitest.classpath.ClassPath; +import org.pitest.classpath.ClassloaderByteArraySource; +import org.pitest.classpath.CodeSource; +import org.pitest.util.IsolationUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FixedCodeSource implements CodeSource { + + private final List classes; + + public FixedCodeSource(Class ... classes) { + this(Arrays.stream(classes) + .map(FixedCodeSource::toTree).collect(Collectors.toList())); + } + + public FixedCodeSource(List classes) { + this.classes = classes; + } + + private static ClassTree toTree(Class aClass) { + ClassloaderByteArraySource cba = new ClassloaderByteArraySource(IsolationUtils.getContextClassLoader()); + return ClassTree.fromBytes(cba.getBytes(aClass.getName()).get()); + } + + @Override + public Stream codeTrees() { + return classes.stream(); + } + + @Override + public Set getCodeUnderTestNames() { + return classes.stream() + .map(ClassTree::name) + .collect(Collectors.toSet()); + } + + @Override + public Set getTestClassNames() { + return Collections.emptySet(); + } + + @Override + public Stream testTrees() { + return Stream.empty(); + } + + @Override + public ClassPath getClassPath() { + throw new UnsupportedOperationException(); + } + + @Override + public Optional findTestee(String className) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional fetchClassBytes(ClassName clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional fetchClassHash(ClassName clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection fetchClassHashes(Collection classes) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBytes(String clazz) { + throw new UnsupportedOperationException(); + } +} diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/InterceptorParametersTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/InterceptorParametersTest.java index 96a8179e6..9f6128cce 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/InterceptorParametersTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/InterceptorParametersTest.java @@ -30,7 +30,7 @@ public void shouldReturnNoneWhenValueAbsent() { @Test public void shouldReturnNoneWhenFeatureSettingsAbsent() { - this.testee = new InterceptorParameters(null, null, null, null, null); + this.testee = new InterceptorParameters(null, null, null, null, null, null); assertThat(this.testee.getString(FeatureParameter.named("foo"))).isEqualTo(Optional.empty()); } @@ -50,7 +50,7 @@ private InterceptorParameters makeFor(String key, String ... vals) { final Map> values = new HashMap<>(); values.put(key, Arrays.asList(vals)); final FeatureSetting fs = new FeatureSetting(null, null, values); - return new InterceptorParameters(fs, null, null,null, null); + return new InterceptorParameters(fs, null, null,null, null, null); } } diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java index 1fda5c933..8f1009bc8 100755 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java @@ -6,6 +6,7 @@ import org.pitest.classinfo.ClassName; import org.pitest.classpath.ClassloaderByteArraySource; import org.pitest.mutationtest.EngineArguments; +import org.pitest.mutationtest.FixedCodeSource; import org.pitest.mutationtest.MutationConfig; import org.pitest.mutationtest.build.intercept.javafeatures.AnEnum; import org.pitest.mutationtest.build.intercept.javafeatures.ForEachFilterTest.HasForEachLoop; @@ -277,7 +278,7 @@ MutationSource createSource(ClassByteArraySource source) { final SettingsFactory settings = new SettingsFactory(this.data, PluginServices.makeForContextLoader()); final MutationInterceptor interceptor = settings.getInterceptor() - .createInterceptor(this.data, null, source, null); + .createInterceptor(this.data, null, source, null, new FixedCodeSource()); final MutationEngine engine = new GregorEngineFactory().createEngine( EngineArguments.arguments().withExcludedMethods(this.data.getExcludedMethods()) diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScannerTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScannerTest.java new file mode 100644 index 000000000..aef14f206 --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/FunctionalInterfaceScannerTest.java @@ -0,0 +1,85 @@ +package org.pitest.mutationtest.build.intercept.staticinitializers; + +import org.junit.Test; +import org.pitest.bytecode.analysis.ClassTree; +import org.pitest.classinfo.ClassByteArraySource; +import org.pitest.classpath.ClassloaderByteArraySource; +import org.pitest.classpath.CodeSource; +import org.pitest.mutationtest.FixedCodeSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Function; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + + +public class FunctionalInterfaceScannerTest { + + private final ClassByteArraySource source = ClassloaderByteArraySource.fromContext(); + + FunctionalInterfaceScanner underTest = new FunctionalInterfaceScanner(); + + @Test + public void recognisesFunctionalInterfaces() { + ClassTree functional = ClassTree.fromBytes(bytesFor(Function.class)); + ClassTree not = ClassTree.fromBytes(bytesFor(String.class)); + + assertThat(underTest.apply(codeSourceFor(functional, not))).containsExactly("java/util/function/Function"); + } + + + /* + @Test + public void wholeClassPath() throws Exception { + ClassPath cp = new ClassPath(); + List classes = cp.classNames().stream() + .map(n -> source.getBytes(n)) + .map(b -> ClassTree.fromBytes(b.get())) + .filter(ClassTree::isInterface) + .flatMap(t -> underTest.apply(codeSourceFor(t)).stream()) + .distinct() + .collect(Collectors.toList()); + + System.out.println(classes.stream() + .collect(Collectors.joining("\n"))); + } +*/ + + /* + // not a test and only works with java 9+ + public void crudelyScanJdk() throws Exception { + FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/")); + Path modules = fs.getPath(""); + + List jdkClasses = Files.walk(modules, FileVisitOption.FOLLOW_LINKS) + .filter(f -> f.getFileName().toString().endsWith(".class")) + .map(this::toBytes) + .map(ClassTree::fromBytes) + .flatMap(t -> underTest.apply(codeSourceFor(t)).stream()) + .distinct() + .collect(Collectors.toList()); + + + System.out.println(jdkClasses.stream() + .collect(Collectors.joining("\n"))); + }*/ + + private byte[] toBytes(Path p) { + try { + return Files.readAllBytes(p); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + private CodeSource codeSourceFor(ClassTree... classes) { + return new FixedCodeSource(asList(classes)); + } + + byte[] bytesFor(Class clazz) { + return this.source.getBytes(clazz.getName()).get(); + } +} \ 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 8822bd49d..3bea6dcd7 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,6 +1,8 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import com.example.staticinitializers.BrokenChain; +import com.example.staticinitializers.delayedexecution.CustomFunction; +import com.example.staticinitializers.delayedexecution.CustomFunctionNotAnnotated; import com.example.staticinitializers.delayedexecution.EnumFieldMethodRef; import com.example.staticinitializers.delayedexecution.EnumFieldSupplier; import com.example.staticinitializers.EnumWithLambdaInConstructor; @@ -8,12 +10,18 @@ import com.example.staticinitializers.NestedEnumWithLambdaInStaticInitializer; import com.example.staticinitializers.SecondLevelPrivateMethods; import com.example.staticinitializers.SingletonWithWorkInInitializer; +import com.example.staticinitializers.delayedexecution.EnumListOfSuppliers; import com.example.staticinitializers.delayedexecution.EnumMethodReferenceNotStored; import com.example.staticinitializers.delayedexecution.EnumMixedFields; import com.example.staticinitializers.delayedexecution.StaticFunctionField; +import com.example.staticinitializers.delayedexecution.StaticListOfFunctionalInterface; +import com.example.staticinitializers.delayedexecution.StaticListOfFunctions; +import com.example.staticinitializers.delayedexecution.StaticListOfUnannotatedInterfaces; import com.example.staticinitializers.delayedexecution.StaticSupplierField; import com.example.staticinitializers.ThirdLevelPrivateMethods; +import org.junit.Ignore; import org.junit.Test; +import org.pitest.mutationtest.FixedCodeSource; import org.pitest.mutationtest.engine.MutationDetails; import org.pitest.mutationtest.engine.gregor.mutators.NullMutateEverything; import org.pitest.verifier.interceptors.InterceptorVerifier; @@ -23,7 +31,8 @@ public class StaticInitializerInterceptorTest { - InterceptorVerifier v = VerifierStart.forInterceptorFactory(new StaticInitializerInterceptorFactory()) + InterceptorVerifier v = VerifierStart.forInterceptorFactory(new StaticInitializerInterceptorFactory() + , new FixedCodeSource(CustomFunction.class, CustomFunctionNotAnnotated.class)) .usingMutator(new NullMutateEverything()); @@ -239,6 +248,48 @@ public void filtersMutationsForMethodReferencesUsedInEnumConstructor() { .verify(); } + @Test + public void mutatesMethodsStoredInListOfSuppliers() { + v.forClass(EnumListOfSuppliers.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void mutatesMethodsStoredInStaticListOfFunctions() { + v.forClass(StaticListOfFunctions.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + @Ignore("functionality disabled, may get re-introduced") + public void recognisesCustomFunctionalInterfaces() { + v.forClass(StaticListOfFunctionalInterface.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void stillMutatesIfClassUsedAsFunctionIsntAnnotated() { + v.forClass(StaticListOfUnannotatedInterfaces.class) + .forMethod("canMutate") + .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-entry/src/test/java/org/pitest/verifier/interceptors/FactoryVerifier.java b/pitest-entry/src/test/java/org/pitest/verifier/interceptors/FactoryVerifier.java index ba7134a36..1e9762855 100644 --- a/pitest-entry/src/test/java/org/pitest/verifier/interceptors/FactoryVerifier.java +++ b/pitest-entry/src/test/java/org/pitest/verifier/interceptors/FactoryVerifier.java @@ -64,7 +64,7 @@ private static void factoryIsOnChain(Class factory) { } public static InterceptorParameters emptyParams(ReportOptions data) { - return new InterceptorParameters(null, data, null, null, null); + return new InterceptorParameters(null, data, null, null, null, null); } public static ReportOptions emptyOptions() { diff --git a/pitest-entry/src/test/java/org/pitest/verifier/interceptors/VerifierStart.java b/pitest-entry/src/test/java/org/pitest/verifier/interceptors/VerifierStart.java index a7e18b7d5..14032e35c 100644 --- a/pitest-entry/src/test/java/org/pitest/verifier/interceptors/VerifierStart.java +++ b/pitest-entry/src/test/java/org/pitest/verifier/interceptors/VerifierStart.java @@ -1,5 +1,7 @@ package org.pitest.verifier.interceptors; +import org.pitest.classpath.CodeSource; +import org.pitest.mutationtest.build.InterceptorParameters; import org.pitest.mutationtest.build.MutationInterceptor; import org.pitest.mutationtest.build.MutationInterceptorFactory; @@ -10,7 +12,11 @@ public static InterceptorVerifier forInterceptor(MutationInterceptor interceptor } public static InterceptorVerifier forInterceptorFactory(MutationInterceptorFactory f) { - return new InterceptorVerifier(f.createInterceptor(null)); + return new InterceptorVerifier(f.createInterceptor( new InterceptorParameters(null, null, null, null, null, null))); + } + + public static InterceptorVerifier forInterceptorFactory(MutationInterceptorFactory f, CodeSource code) { + return new InterceptorVerifier(f.createInterceptor( new InterceptorParameters(null, null, null, null, null, code))); } } diff --git a/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java b/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java new file mode 100644 index 000000000..5b9112b86 --- /dev/null +++ b/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java @@ -0,0 +1,31 @@ +package org.pitest.bytecode; + +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class SignatureParser { + + /** + * Extracts all types referenced in a field generic signature + */ + public static Set extractTypes(String signature) { + if (signature == null) { + return Collections.emptySet(); + } + + Set types = new HashSet<>(); + SignatureReader r = new SignatureReader(signature); + SignatureVisitor visitor = new SignatureVisitor(ASMVersion.asmVersion()) { + @Override + public void visitClassType(String name) { + types.add(name); + } + }; + r.acceptType(visitor); + return types; + } +} diff --git a/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java b/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java new file mode 100644 index 000000000..93fc1334d --- /dev/null +++ b/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java @@ -0,0 +1,25 @@ +package org.pitest.bytecode; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SignatureParserTest { + + @Test + public void noTypesForNullSignature() { + assertThat(SignatureParser.extractTypes(null)).isEmpty(); + } + + @Test + public void parsesSupplierOfStrings() { + String signature = "Ljava/util/function/Supplier;"; + assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/function/Supplier", "java/lang/String"); + } + + @Test + public void parsesListsOfFunctions() { + String signature = "Ljava/util/List;>;"; + assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/List", "java/util/function/Function", "java/lang/Integer"); + } +} \ No newline at end of file