Skip to content

Commit

Permalink
Merge pull request #1362 from see-quick/fix-lambdas
Browse files Browse the repository at this point in the history
Mutation coverage fix within lambdas
  • Loading branch information
hcoles authored Nov 11, 2024
2 parents f9c3553 + bbb2a20 commit 35a8ce8
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.pitest.mutationtest.build.intercept.annotations;

import org.objectweb.asm.Handle;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.pitest.bytecode.analysis.AnalysisFunctions;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.pitest.bytecode.analysis.ClassTree;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.functional.FCollection;
Expand All @@ -13,7 +15,11 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -24,7 +30,6 @@ public class ExcludedAnnotationInterceptor implements MutationInterceptor {
private boolean skipClass;
private Predicate<MutationDetails> annotatedMethodMatcher;


ExcludedAnnotationInterceptor(List<String> configuredAnnotations) {
this.configuredAnnotations = configuredAnnotations;
}
Expand All @@ -39,17 +44,93 @@ public void begin(ClassTree clazz) {
this.skipClass = clazz.annotations().stream()
.anyMatch(avoidedAnnotation());
if (!this.skipClass) {
final List<Predicate<MutationDetails>> methods = clazz.methods().stream()
// 1. Collect methods with avoided annotations or that override such methods
final List<MethodTree> avoidedMethods = clazz.methods().stream()
.filter(hasAvoidedAnnotation())
.map(AnalysisFunctions.matchMutationsInMethod())
.collect(Collectors.toList());
this.annotatedMethodMatcher = Prelude.or(methods);

// Collect method names along with descriptors to handle overloaded methods
final Set<MethodSignature> avoidedMethodSignatures = avoidedMethods.stream()
.map(method -> new MethodSignature(method.rawNode().name, method.rawNode().desc))
.collect(Collectors.toSet());

// Keep track of processed methods to avoid infinite loops
Set<MethodSignature> processedMethods = new HashSet<>(avoidedMethodSignatures);

// 2. For each avoided method, collect lambda methods recursively
for (MethodTree avoidedMethod : avoidedMethods) {
collectLambdaMethods(avoidedMethod, clazz, avoidedMethodSignatures, processedMethods);
}

// 3. Create a predicate to match mutations in methods to avoid
this.annotatedMethodMatcher = mutation -> {
MethodSignature mutationSignature = new MethodSignature(
mutation.getMethod(), mutation.getId().getLocation().getMethodDesc());
return avoidedMethodSignatures.contains(mutationSignature);
};
}
}

/**
* Recursively collects lambda methods defined within the given method.
*
* @param method The method to inspect for lambdas.
* @param clazz The class containing the methods.
* @param avoidedMethodSignatures The set of method signatures to avoid.
* @param processedMethods The set of already processed methods to prevent infinite loops.
*/
private void collectLambdaMethods(MethodTree method, ClassTree clazz,
Set<MethodSignature> avoidedMethodSignatures,
Set<MethodSignature> processedMethods) {
Queue<MethodTree> methodsToProcess = new LinkedList<>();
methodsToProcess.add(method);

while (!methodsToProcess.isEmpty()) {
MethodTree currentMethod = methodsToProcess.poll();

for (AbstractInsnNode insn : currentMethod.rawNode().instructions) {
if (insn instanceof InvokeDynamicInsnNode) {
InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;

for (Object bsmArg : indy.bsmArgs) {
if (bsmArg instanceof Handle) {
Handle handle = (Handle) bsmArg;
// Check if the method is in the same class and is a lambda method
if (handle.getOwner().equals(clazz.rawNode().name) && handle.getName().startsWith("lambda$")) {
MethodSignature lambdaMethodSignature = new MethodSignature(handle.getName(), handle.getDesc());
if (!avoidedMethodSignatures.contains(lambdaMethodSignature)
&& !processedMethods.contains(lambdaMethodSignature)) {
avoidedMethodSignatures.add(lambdaMethodSignature);
processedMethods.add(lambdaMethodSignature);
// Find the MethodTree for this lambda method
MethodTree lambdaMethod = findMethodTree(clazz, handle.getName(), handle.getDesc());
if (lambdaMethod != null) {
methodsToProcess.add(lambdaMethod);
}
}
}
}
}
}
}
}
}

private MethodTree findMethodTree(ClassTree clazz, String methodName, String methodDesc) {
return clazz.methods().stream()
.filter(m -> m.rawNode().name.equals(methodName) && m.rawNode().desc.equals(methodDesc))
.findFirst()
.orElse(null);
}

/**
* Creates a predicate that checks if a method has an avoided annotation.
*
* @return A predicate that returns true if the method should be avoided.
*/
private Predicate<MethodTree> hasAvoidedAnnotation() {
return a -> a.annotations().stream()
.anyMatch(avoidedAnnotation());
return methodTree ->
methodTree.annotations().stream().anyMatch(avoidedAnnotation());
}

private Predicate<AnnotationNode> avoidedAnnotation() {
Expand Down Expand Up @@ -81,4 +162,34 @@ boolean shouldAvoid(String desc) {
return false;
}

/**
* Represents a method signature with its name and descriptor.
* Used to uniquely identify methods, especially overloaded ones.
*/
private static class MethodSignature {
private final String name;
private final String desc;

MethodSignature(String name, String desc) {
this.name = name;
this.desc = desc;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MethodSignature that = (MethodSignature) obj;
return name.equals(that.name) && desc.equals(that.desc);
}

@Override
public int hashCode() {
return name.hashCode() * 31 + desc.hashCode();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,69 @@ public void shouldFilterMethodsWithGeneratedAnnotation() {
assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar");
}

@Test
public void shouldNotFilterMutationsInUnannotatedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(UnannotatedMethodClass.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, UnannotatedMethodClass.class);
assertThat(actual).containsExactlyElementsOf(mutations);
}

@Test
public void shouldFilterMutationsInAnnotatedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(AnnotatedMethodClass.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, AnnotatedMethodClass.class);
assertThat(actual).isEmpty();
}

@Test
public void shouldNotFilterMutationsInLambdaWithinUnannotatedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(LambdaInUnannotatedMethodClass.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, LambdaInUnannotatedMethodClass.class);
assertThat(actual).containsExactlyElementsOf(mutations);
}

@Test
public void shouldFilterMutationsInLambdaWithinAnnotatedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(LambdaInAnnotatedMethodClass.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, LambdaInAnnotatedMethodClass.class);
assertThat(actual).isEmpty();
}

@Test
public void shouldHandleOverloadedMethodsWithLambdas() {
final List<MutationDetails> mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, OverloadedMethods.class);

// Expect mutations from unannotated methods and their lambdas
assertThat(actual).hasSize(3); // bar, foo(int x), and its lambda
for (MutationDetails mutationDetails : actual) {
assertThat(mutationDetails.getId().getLocation().getMethodName())
.isIn("bar", "foo", "lambda$foo$0");
}
}

@Test
public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverloadedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class);

// Should include mutations from the unannotated method and its nested lambdas
assertThat(actual).anyMatch(mutation -> mutation.getId().getLocation().getMethodName().equals("baz"));
assertThat(actual).anyMatch(mutation -> {
String methodName = mutation.getId().getLocation().getMethodName();
return methodName.startsWith("lambda$baz$") || methodName.startsWith("lambda$null$");
});
}

@Test
public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverloadedMethod() {
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class));
final Collection<MutationDetails> actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class);

// Should not include mutations from the annotated method and its nested lambdas
assertThat(actual).noneMatch(mutation -> mutation.getId().getLocation().getMethodDesc().equals("(Ljava/lang/String;)V"));
}

private Collection<MutationDetails> runWithTestee(
Collection<MutationDetails> input, Class<?> clazz) {
this.testee.begin(treeFor(clazz));
Expand All @@ -82,7 +145,6 @@ ClassTree treeFor(Class<?> clazz) {
return ClassTree.fromBytes(source.getBytes(clazz.getName()).get());
}


}

class UnAnnotated {
Expand Down Expand Up @@ -120,4 +182,62 @@ public void bar() {

}

class UnannotatedMethodClass {
public void unannotatedMethod() {
System.out.println("This method is not annotated.");
}
}

class AnnotatedMethodClass {
@TestGeneratedAnnotation
public void annotatedMethod() {
System.out.println("This method is annotated.");
}
}

class LambdaInUnannotatedMethodClass {
public void methodWithLambda() {
Runnable runnable = () -> System.out.println("Lambda inside unannotated method.");
}
}

class LambdaInAnnotatedMethodClass {
@TestGeneratedAnnotation
public void methodWithLambda() {
Runnable runnable = () -> System.out.println("Lambda inside annotated method.");
}
}

class OverloadedMethods {
public void foo(int x) {
System.out.println("mutate me");
Runnable r = () -> System.out.println("Lambda in unannotated overloaded method with int");
}

@TestGeneratedAnnotation
public void foo(String x) {
System.out.println("don't mutate me");
Runnable r = () -> System.out.println("Lambda in annotated overloaded method with String");
}

public void bar() {
System.out.println("mutate me");
}
}

class NestedLambdaInOverloadedMethods {
public void baz(int x) {
System.out.println("mutate me");
Runnable outerLambda = () -> {
Runnable innerLambda = () -> System.out.println("Nested lambda in unannotated overloaded method with int");
};
}

@TestGeneratedAnnotation
public void baz(String x) {
System.out.println("don't mutate me");
Runnable outerLambda = () -> {
Runnable innerLambda = () -> System.out.println("Nested lambda in annotated overloaded method with String");
};
}
}

0 comments on commit 35a8ce8

Please sign in to comment.