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

Mutation coverage fix within lambdas #1362

Merged
merged 6 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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");
};
}
}
Loading