Skip to content

Commit

Permalink
Merge pull request #1274 from hcoles/feature/invoke_dynamic_static_in…
Browse files Browse the repository at this point in the history
…itializer

consider invoke dynamic calls when detecting static initializer only …
  • Loading branch information
hcoles authored Nov 6, 2023
2 parents 7822a40 + 9e456f4 commit 917b9ce
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -64,17 +66,18 @@ private void analyseClass(ClassTree tree) {
final Optional<MethodTree> clinit = tree.methods().stream().filter(nameEquals("<clinit>")).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<Location> privateMethods = tree.methods().stream()
.filter(m -> m.isPrivate())
.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<Location, List<Call>> callTree = tree.methods().stream()
.filter(m -> m.isPrivate() || m.rawNode().name.equals("<clinit>"))
.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));

Expand All @@ -86,12 +89,27 @@ private void analyseClass(ClassTree tree) {
}
}

private List<Location> callsFor(ClassTree tree, MethodTree m) {
private boolean enumConstructor(MethodTree m, boolean isEnum) {
return isEnum && m.rawNode().name.equals("<init>");
}

private List<Location> allCallsFor(ClassTree tree, MethodTree m) {
return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m))
.collect(Collectors.toList());
}

private Stream<Location> 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<Location> invokeDynamicCallsFor(ClassTree tree, MethodTree m) {
return m.instructions().stream()
.flatMap(is(InvokeDynamicInsnNode.class))
.filter(callsDynamically(tree.name()))
.flatMap(this::asLocation);
}

private void visit(Map<Location, List<Call>> callTree, Set<Location> visited, Location l) {
Expand All @@ -114,7 +132,24 @@ private Predicate<MethodInsnNode> calls(final ClassName self) {
return a -> a.owner.equals(self.asInternalName());
}

private <T extends AbstractInsnNode> Function<AbstractInsnNode,Stream<T>> is(final Class<T> clazz) {
private Predicate<InvokeDynamicInsnNode> callsDynamically(final ClassName self) {
return a -> asLocation(a)
.anyMatch(l -> l.getClassName().equals(self));

}

private Stream<Location> asLocation(InvokeDynamicInsnNode call) {
return Arrays.stream(call.bsmArgs)
.flatMap(is(Handle.class))
.flatMap(this::handleToLocation);
}

private Stream<Location> handleToLocation(Handle handle) {
ClassName c = ClassName.fromString(handle.getOwner());
return Stream.of(Location.location(c, handle.getName(), handle.getDesc()));
}

private <T> Function<Object,Stream<T>> is(final Class<T> clazz) {
return a -> {
if (a.getClass().isAssignableFrom(clazz)) {
return Stream.of((T)a);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> ss) {
this.s = ss.stream()
.peek(this::doStuff)
.collect(Collectors.joining());
}

private void doStuff(String s) {
System.out.println(s);
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -77,14 +79,25 @@ public void doesNotFilterMutationsInOverriddenMethodsNotInvolvedInStaticInit() {
.verify();
}

@Test
public void filtersMutantsInSingletonConstructor() {
v.forClass(SingletonWithWorkInInitializer.class)
.forMutantsMatching(inMethod("<init>", "()V"))
.mutantsAreGenerated()
.allMutantsAreFiltered()
.verify();
}
@Test
public void filtersMutantsInSingletonConstructor() {
v.forClass(SingletonWithWorkInInitializer.class)
.forMutantsMatching(inMethod("<init>", "()V"))
.mutantsAreGenerated()
.allMutantsAreFiltered()
.verify();
}

@Test
public void mutatesPrivateMethodsCalledFromPublicMethodInSingleton() {
v.forClass(SingletonWithWorkInInitializer.class)
.forMutantsMatching(inMethod("mutateMeCalledFromPublicMethod", "()V"))
.mutantsAreGenerated()
.noMutantsAreFiltered()
.verify();
}



@Test
public void filtersMutantsCalledFromPrivateSingletonConstructor() {
Expand Down Expand Up @@ -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<MutationDetails> inMethod(String name, String desc) {
return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc);
}
Expand Down

0 comments on commit 917b9ce

Please sign in to comment.