Skip to content

Commit

Permalink
Merge pull request #1345 from hcoles/feature/improved_delayed_execution
Browse files Browse the repository at this point in the history
Feature/improved delayed execution
  • Loading branch information
hcoles authored Aug 30, 2024
2 parents 54dd01a + b374736 commit 5b8e2c1
Show file tree
Hide file tree
Showing 23 changed files with 691 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,10 +25,11 @@ public CompoundMutationInterceptor createInterceptor(
ReportOptions data,
CoverageDatabase coverage,
ClassByteArraySource source,
TestPrioritiser testPrioritiser
TestPrioritiser testPrioritiser,
CodeSource code
) {
final List<MutationInterceptor> interceptors = this.features.getActiveFeatures().stream()
.map(toInterceptor(this.features, data, coverage, source, testPrioritiser))
List<MutationInterceptor> interceptors = this.features.getActiveFeatures().stream()
.map(toInterceptor(this.features, data, coverage, source, testPrioritiser, code))
.collect(Collectors.toList());
return new CompoundMutationInterceptor(interceptors);
}
Expand All @@ -38,10 +40,11 @@ private static Function<MutationInterceptorFactory, MutationInterceptor> 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));

}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -50,6 +57,10 @@ public TestPrioritiser testPrioritiser() {
return this.testPrioritiser;
}

public CodeSource code() {
return code;
}

public Optional<String> getString(FeatureParameter limit) {
if (this.conf == null) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CodeSource, Set<String>> {
@Override
public Set<String> apply(CodeSource codeSource) {
return codeSource.codeTrees()
.filter(this::isFunctionalInterface)
.map(c -> c.rawNode().name)
.collect(Collectors.toSet());
}

private boolean isFunctionalInterface(ClassTree classTree) {
List<AnnotationNode> annotations = classTree.rawNode().visibleAnnotations;
if (annotations == null) {
return false;
}

return annotations.stream()
.anyMatch(a -> a.desc.equals("Ljava/lang/FunctionalInterface;"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -53,47 +57,63 @@
* 1. In a static initializer (i.e <clinit>)
* 2. In a private method or constructor called from <clinit> or another private method in the call tree
*
*
*/
class StaticInitializerInterceptor implements MutationInterceptor {

static final Slot<AbstractInsnNode> START = Slot.create(AbstractInsnNode.class);
private final Set<String> delayedExecutionTypes;

private static final Slot<AbstractInsnNode> START = Slot.create(AbstractInsnNode.class);
private static final Slot<Set<String>> DELAYED_EXECUTION_FIELDS = Slot.createSet(String.class);

static final SequenceMatcher<AbstractInsnNode> DELAYED_EXECUTION = QueryStart
private final SequenceMatcher<AbstractInsnNode> 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<AbstractInsnNode> delayedExecutionField() {
return PUTSTATIC.and(isAUtilFunctionField());
StaticInitializerInterceptor(Set<String> delayedExecutionTypes) {
this.delayedExecutionTypes = delayedExecutionTypes;
}

private static Match<AbstractInsnNode> delayedExecutionField(SlotRead<Set<String>> delayedFields) {
return PUTSTATIC.and(isADelayedExecutionField(delayedFields));
}

private static Match<AbstractInsnNode> isAUtilFunctionField() {
private static Match<AbstractInsnNode> isADelayedExecutionField(SlotRead<Set<String>> 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<AbstractInsnNode> dynamicallyReturnsDeferredExecutionCode() {
return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c);
private Match<AbstractInsnNode> dynamicallyReturnsDeferredExecutionCode() {
return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnsDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c);
}

private static Match<AbstractInsnNode> returnsDeferredExecutionCode() {
return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnDelayedExecutionType(((MethodInsnNode) n).desc), c);
private Match<AbstractInsnNode> 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<AbstractInsnNode> enumConstructorCallAndStore() {
Expand Down Expand Up @@ -126,14 +146,23 @@ private void analyseClass(ClassTree tree) {
final Optional<MethodTree> clinit = tree.methods().stream().filter(nameEquals("<clinit>")).findFirst();

if (clinit.isPresent()) {

// Find delayed execution fields (Function, Supplier, List<Supplier> etc). Full generic signature is available in the
// declaration, but not on call.
Set<String> 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<Location> privateMethods = tree.methods().stream()
.filter(MethodTree::isPrivate)
.map(MethodTree::asLocation)
.collect(Collectors.toSet());

Set<Call> storedToSupplier = findCallsStoredToDelayedExecutionCode(tree);
Set<Call> 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
Expand All @@ -153,21 +182,26 @@ private void analyseClass(ClassTree tree) {
}
}

private Set<Call> findCallsStoredToDelayedExecutionCode(ClassTree tree) {
return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree));
private boolean isDelayedExecutionField(FieldNode fieldNode) {
return SignatureParser.extractTypes(fieldNode.signature).stream()
.anyMatch(this::isADelayedExecutionType);
}

private Set<Call> findCallsStoredToDelayedExecutionCode(ClassTree tree, Set<String> delayedExecutionFields) {
return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree, delayedExecutionFields));
}


private Set<Call> privateAndClinitCallsToDelayedExecutionCode(ClassTree tree) {
private Set<Call> privateAndClinitCallsToDelayedExecutionCode(ClassTree tree, Set<String> delayedExecutionFields) {
return tree.methods().stream()
.filter(m -> m.isPrivate() || m.rawNode().name.equals("<clinit>"))
.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<Location> delayedExecutionCall(MethodTree method) {
Context context = Context.start();
return DELAYED_EXECUTION.contextMatches(method.instructions(), context).stream()
private List<Location> delayedExecutionCall(MethodTree method, Set<String> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> functionalInterfaces() {
Set<String> 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!");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -337,7 +336,7 @@ private List<MutationAnalysisUnit> 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);
Expand Down Expand Up @@ -377,17 +376,15 @@ 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 -> {
final Optional<byte[]> maybeBytes = bas.getBytes(clazz);
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);
};
}
Expand Down
5 changes: 5 additions & 0 deletions pitest-entry/src/main/java/org/pitest/sequence/Slot.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.pitest.sequence;

import java.util.List;
import java.util.Set;

public final class Slot<T> {
public static <T> Slot<T> create(Class<T> clazz) {
Expand All @@ -11,6 +12,10 @@ public static <T> Slot<List<T>> createList(Class<T> clazz) {
return new Slot<>();
}

public static <T> Slot<Set<T>> createSet(Class<T> clazz) {
return new Slot<>();
}

public SlotWrite<T> write() {
return new SlotWrite<>(this);
}
Expand Down
Loading

0 comments on commit 5b8e2c1

Please sign in to comment.