Skip to content

Commit 35a8ce8

Browse files
authored
Merge pull request #1362 from see-quick/fix-lambdas
Mutation coverage fix within lambdas
2 parents f9c3553 + bbb2a20 commit 35a8ce8

File tree

2 files changed

+239
-8
lines changed

2 files changed

+239
-8
lines changed

pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptor.java

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.pitest.mutationtest.build.intercept.annotations;
22

3+
import org.objectweb.asm.Handle;
4+
import org.objectweb.asm.tree.AbstractInsnNode;
35
import org.objectweb.asm.tree.AnnotationNode;
4-
import org.pitest.bytecode.analysis.AnalysisFunctions;
6+
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
57
import org.pitest.bytecode.analysis.ClassTree;
68
import org.pitest.bytecode.analysis.MethodTree;
79
import org.pitest.functional.FCollection;
@@ -13,7 +15,11 @@
1315

1416
import java.util.Collection;
1517
import java.util.Collections;
18+
import java.util.HashSet;
19+
import java.util.LinkedList;
1620
import java.util.List;
21+
import java.util.Queue;
22+
import java.util.Set;
1723
import java.util.function.Predicate;
1824
import java.util.stream.Collectors;
1925

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

27-
2833
ExcludedAnnotationInterceptor(List<String> configuredAnnotations) {
2934
this.configuredAnnotations = configuredAnnotations;
3035
}
@@ -39,17 +44,93 @@ public void begin(ClassTree clazz) {
3944
this.skipClass = clazz.annotations().stream()
4045
.anyMatch(avoidedAnnotation());
4146
if (!this.skipClass) {
42-
final List<Predicate<MutationDetails>> methods = clazz.methods().stream()
47+
// 1. Collect methods with avoided annotations or that override such methods
48+
final List<MethodTree> avoidedMethods = clazz.methods().stream()
4349
.filter(hasAvoidedAnnotation())
44-
.map(AnalysisFunctions.matchMutationsInMethod())
4550
.collect(Collectors.toList());
46-
this.annotatedMethodMatcher = Prelude.or(methods);
51+
52+
// Collect method names along with descriptors to handle overloaded methods
53+
final Set<MethodSignature> avoidedMethodSignatures = avoidedMethods.stream()
54+
.map(method -> new MethodSignature(method.rawNode().name, method.rawNode().desc))
55+
.collect(Collectors.toSet());
56+
57+
// Keep track of processed methods to avoid infinite loops
58+
Set<MethodSignature> processedMethods = new HashSet<>(avoidedMethodSignatures);
59+
60+
// 2. For each avoided method, collect lambda methods recursively
61+
for (MethodTree avoidedMethod : avoidedMethods) {
62+
collectLambdaMethods(avoidedMethod, clazz, avoidedMethodSignatures, processedMethods);
63+
}
64+
65+
// 3. Create a predicate to match mutations in methods to avoid
66+
this.annotatedMethodMatcher = mutation -> {
67+
MethodSignature mutationSignature = new MethodSignature(
68+
mutation.getMethod(), mutation.getId().getLocation().getMethodDesc());
69+
return avoidedMethodSignatures.contains(mutationSignature);
70+
};
4771
}
4872
}
4973

74+
/**
75+
* Recursively collects lambda methods defined within the given method.
76+
*
77+
* @param method The method to inspect for lambdas.
78+
* @param clazz The class containing the methods.
79+
* @param avoidedMethodSignatures The set of method signatures to avoid.
80+
* @param processedMethods The set of already processed methods to prevent infinite loops.
81+
*/
82+
private void collectLambdaMethods(MethodTree method, ClassTree clazz,
83+
Set<MethodSignature> avoidedMethodSignatures,
84+
Set<MethodSignature> processedMethods) {
85+
Queue<MethodTree> methodsToProcess = new LinkedList<>();
86+
methodsToProcess.add(method);
87+
88+
while (!methodsToProcess.isEmpty()) {
89+
MethodTree currentMethod = methodsToProcess.poll();
90+
91+
for (AbstractInsnNode insn : currentMethod.rawNode().instructions) {
92+
if (insn instanceof InvokeDynamicInsnNode) {
93+
InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;
94+
95+
for (Object bsmArg : indy.bsmArgs) {
96+
if (bsmArg instanceof Handle) {
97+
Handle handle = (Handle) bsmArg;
98+
// Check if the method is in the same class and is a lambda method
99+
if (handle.getOwner().equals(clazz.rawNode().name) && handle.getName().startsWith("lambda$")) {
100+
MethodSignature lambdaMethodSignature = new MethodSignature(handle.getName(), handle.getDesc());
101+
if (!avoidedMethodSignatures.contains(lambdaMethodSignature)
102+
&& !processedMethods.contains(lambdaMethodSignature)) {
103+
avoidedMethodSignatures.add(lambdaMethodSignature);
104+
processedMethods.add(lambdaMethodSignature);
105+
// Find the MethodTree for this lambda method
106+
MethodTree lambdaMethod = findMethodTree(clazz, handle.getName(), handle.getDesc());
107+
if (lambdaMethod != null) {
108+
methodsToProcess.add(lambdaMethod);
109+
}
110+
}
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
private MethodTree findMethodTree(ClassTree clazz, String methodName, String methodDesc) {
120+
return clazz.methods().stream()
121+
.filter(m -> m.rawNode().name.equals(methodName) && m.rawNode().desc.equals(methodDesc))
122+
.findFirst()
123+
.orElse(null);
124+
}
125+
126+
/**
127+
* Creates a predicate that checks if a method has an avoided annotation.
128+
*
129+
* @return A predicate that returns true if the method should be avoided.
130+
*/
50131
private Predicate<MethodTree> hasAvoidedAnnotation() {
51-
return a -> a.annotations().stream()
52-
.anyMatch(avoidedAnnotation());
132+
return methodTree ->
133+
methodTree.annotations().stream().anyMatch(avoidedAnnotation());
53134
}
54135

55136
private Predicate<AnnotationNode> avoidedAnnotation() {
@@ -81,4 +162,34 @@ boolean shouldAvoid(String desc) {
81162
return false;
82163
}
83164

165+
/**
166+
* Represents a method signature with its name and descriptor.
167+
* Used to uniquely identify methods, especially overloaded ones.
168+
*/
169+
private static class MethodSignature {
170+
private final String name;
171+
private final String desc;
172+
173+
MethodSignature(String name, String desc) {
174+
this.name = name;
175+
this.desc = desc;
176+
}
177+
178+
@Override
179+
public boolean equals(Object obj) {
180+
if (this == obj) {
181+
return true;
182+
}
183+
if (obj == null || getClass() != obj.getClass()) {
184+
return false;
185+
}
186+
MethodSignature that = (MethodSignature) obj;
187+
return name.equals(that.name) && desc.equals(that.desc);
188+
}
189+
190+
@Override
191+
public int hashCode() {
192+
return name.hashCode() * 31 + desc.hashCode();
193+
}
194+
}
84195
}

pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptorTest.java

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,69 @@ public void shouldFilterMethodsWithGeneratedAnnotation() {
6767
assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar");
6868
}
6969

70+
@Test
71+
public void shouldNotFilterMutationsInUnannotatedMethod() {
72+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(UnannotatedMethodClass.class));
73+
final Collection<MutationDetails> actual = runWithTestee(mutations, UnannotatedMethodClass.class);
74+
assertThat(actual).containsExactlyElementsOf(mutations);
75+
}
76+
77+
@Test
78+
public void shouldFilterMutationsInAnnotatedMethod() {
79+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(AnnotatedMethodClass.class));
80+
final Collection<MutationDetails> actual = runWithTestee(mutations, AnnotatedMethodClass.class);
81+
assertThat(actual).isEmpty();
82+
}
83+
84+
@Test
85+
public void shouldNotFilterMutationsInLambdaWithinUnannotatedMethod() {
86+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(LambdaInUnannotatedMethodClass.class));
87+
final Collection<MutationDetails> actual = runWithTestee(mutations, LambdaInUnannotatedMethodClass.class);
88+
assertThat(actual).containsExactlyElementsOf(mutations);
89+
}
90+
91+
@Test
92+
public void shouldFilterMutationsInLambdaWithinAnnotatedMethod() {
93+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(LambdaInAnnotatedMethodClass.class));
94+
final Collection<MutationDetails> actual = runWithTestee(mutations, LambdaInAnnotatedMethodClass.class);
95+
assertThat(actual).isEmpty();
96+
}
97+
98+
@Test
99+
public void shouldHandleOverloadedMethodsWithLambdas() {
100+
final List<MutationDetails> mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class));
101+
final Collection<MutationDetails> actual = runWithTestee(mutations, OverloadedMethods.class);
102+
103+
// Expect mutations from unannotated methods and their lambdas
104+
assertThat(actual).hasSize(3); // bar, foo(int x), and its lambda
105+
for (MutationDetails mutationDetails : actual) {
106+
assertThat(mutationDetails.getId().getLocation().getMethodName())
107+
.isIn("bar", "foo", "lambda$foo$0");
108+
}
109+
}
110+
111+
@Test
112+
public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverloadedMethod() {
113+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class));
114+
final Collection<MutationDetails> actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class);
115+
116+
// Should include mutations from the unannotated method and its nested lambdas
117+
assertThat(actual).anyMatch(mutation -> mutation.getId().getLocation().getMethodName().equals("baz"));
118+
assertThat(actual).anyMatch(mutation -> {
119+
String methodName = mutation.getId().getLocation().getMethodName();
120+
return methodName.startsWith("lambda$baz$") || methodName.startsWith("lambda$null$");
121+
});
122+
}
123+
124+
@Test
125+
public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverloadedMethod() {
126+
final Collection<MutationDetails> mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class));
127+
final Collection<MutationDetails> actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class);
128+
129+
// Should not include mutations from the annotated method and its nested lambdas
130+
assertThat(actual).noneMatch(mutation -> mutation.getId().getLocation().getMethodDesc().equals("(Ljava/lang/String;)V"));
131+
}
132+
70133
private Collection<MutationDetails> runWithTestee(
71134
Collection<MutationDetails> input, Class<?> clazz) {
72135
this.testee.begin(treeFor(clazz));
@@ -82,7 +145,6 @@ ClassTree treeFor(Class<?> clazz) {
82145
return ClassTree.fromBytes(source.getBytes(clazz.getName()).get());
83146
}
84147

85-
86148
}
87149

88150
class UnAnnotated {
@@ -120,4 +182,62 @@ public void bar() {
120182

121183
}
122184

185+
class UnannotatedMethodClass {
186+
public void unannotatedMethod() {
187+
System.out.println("This method is not annotated.");
188+
}
189+
}
123190

191+
class AnnotatedMethodClass {
192+
@TestGeneratedAnnotation
193+
public void annotatedMethod() {
194+
System.out.println("This method is annotated.");
195+
}
196+
}
197+
198+
class LambdaInUnannotatedMethodClass {
199+
public void methodWithLambda() {
200+
Runnable runnable = () -> System.out.println("Lambda inside unannotated method.");
201+
}
202+
}
203+
204+
class LambdaInAnnotatedMethodClass {
205+
@TestGeneratedAnnotation
206+
public void methodWithLambda() {
207+
Runnable runnable = () -> System.out.println("Lambda inside annotated method.");
208+
}
209+
}
210+
211+
class OverloadedMethods {
212+
public void foo(int x) {
213+
System.out.println("mutate me");
214+
Runnable r = () -> System.out.println("Lambda in unannotated overloaded method with int");
215+
}
216+
217+
@TestGeneratedAnnotation
218+
public void foo(String x) {
219+
System.out.println("don't mutate me");
220+
Runnable r = () -> System.out.println("Lambda in annotated overloaded method with String");
221+
}
222+
223+
public void bar() {
224+
System.out.println("mutate me");
225+
}
226+
}
227+
228+
class NestedLambdaInOverloadedMethods {
229+
public void baz(int x) {
230+
System.out.println("mutate me");
231+
Runnable outerLambda = () -> {
232+
Runnable innerLambda = () -> System.out.println("Nested lambda in unannotated overloaded method with int");
233+
};
234+
}
235+
236+
@TestGeneratedAnnotation
237+
public void baz(String x) {
238+
System.out.println("don't mutate me");
239+
Runnable outerLambda = () -> {
240+
Runnable innerLambda = () -> System.out.println("Nested lambda in annotated overloaded method with String");
241+
};
242+
}
243+
}

0 commit comments

Comments
 (0)