Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/improved delayed execution #1345

Merged
merged 6 commits into from
Aug 30, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
identify fields with a prescan
hcoles committed Aug 29, 2024

Verified

This commit was signed with the committer’s verified signature.
commit 45d9f34ef657ec9d71dcc0d1622cd5444b8a2279
Original file line number Diff line number Diff line change
@@ -4,8 +4,10 @@
import org.objectweb.asm.Opcodes;
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 +23,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,11 +56,11 @@
* 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);
static final Slot<Set<String>> DELAYED_EXECUTION_FIELDS = Slot.createSet(String.class);

static final SequenceMatcher<AbstractInsnNode> DELAYED_EXECUTION = QueryStart
.any(AbstractInsnNode.class)
@@ -66,36 +69,41 @@ class StaticInitializerInterceptor implements MutationInterceptor {
.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());
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);
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);
return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnsDelayedExecutionType(((MethodInsnNode) n).desc), c);
}

private static boolean returnDelayedExecutionType(String desc) {
private static boolean returnsDelayedExecutionType(String desc) {
int endOfParams = desc.indexOf(')');
return endOfParams <= 0 || desc.substring(endOfParams + 1).startsWith("Ljava/util/function/");
}


private static boolean isADelayedExecutionType(String type) {
return type.startsWith("java/util/function/");
}

private static SequenceQuery<AbstractInsnNode> enumConstructorCallAndStore() {
return QueryStart.match(methodCallNamed("<init>")).then(PUTSTATIC);
}
@@ -126,14 +134,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
@@ -153,20 +170,25 @@ 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(StaticInitializerInterceptor::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();
private List<Location> delayedExecutionCall(MethodTree method, Set<String> delayedExecutionFields) {
Context context = Context.start().store(DELAYED_EXECUTION_FIELDS.write(), delayedExecutionFields);
return DELAYED_EXECUTION.contextMatches(method.instructions(), context).stream()
.map(c -> c.retrieve(START.read()).get())
.flatMap(this::nodeToLocation)
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) {
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<String>> supplier;

EnumListOfSuppliers(Supplier<String> supplier) {
this.supplier = asList(supplier);
}

private static String canMutate() {
return "mutate me"; // mutate
}

}
Original file line number Diff line number Diff line change
@@ -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<Function<Integer, Integer>> FUNCTIONS =
asList(StaticListOfFunctions::canMutate, StaticListOfFunctions::canAlsoMutate);

private static Integer canMutate(Integer a) {
return a + 1;
}

private static Integer canAlsoMutate(Integer a) {
return a + 2;
}
}
Original file line number Diff line number Diff line change
@@ -8,9 +8,11 @@
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.StaticListOfFunctions;
import com.example.staticinitializers.delayedexecution.StaticSupplierField;
import com.example.staticinitializers.ThirdLevelPrivateMethods;
import org.junit.Test;
@@ -239,6 +241,28 @@ 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();
}



private Predicate<MutationDetails> inMethod(String name, String desc) {
return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc);
}
31 changes: 31 additions & 0 deletions pitest/src/main/java/org/pitest/bytecode/SignatureParser.java
Original file line number Diff line number Diff line change
@@ -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<String> extractTypes(String signature) {
if (signature == null) {
return Collections.emptySet();
}

Set<String> 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;
}
}
25 changes: 25 additions & 0 deletions pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java
Original file line number Diff line number Diff line change
@@ -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<Ljava/lang/String;>;";
assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/function/Supplier", "java/lang/String");
}

@Test
public void parsesListsOfFunctions() {
String signature = "Ljava/util/List<Ljava/util/function/Function<Ljava/lang/Integer;Ljava/lang/Integer;>;>;";
assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/List", "java/util/function/Function", "java/lang/Integer");
}
}