-
Notifications
You must be signed in to change notification settings - Fork 353
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #919 from hcoles/records
Filter junk mutations from java records
- Loading branch information
Showing
26 changed files
with
536 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
...ntry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/RecordFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package org.pitest.mutationtest.build.intercept.javafeatures; | ||
|
||
import org.objectweb.asm.Type; | ||
import org.objectweb.asm.tree.AbstractInsnNode; | ||
import org.objectweb.asm.tree.InvokeDynamicInsnNode; | ||
import org.objectweb.asm.tree.RecordComponentNode; | ||
import org.pitest.bytecode.analysis.ClassTree; | ||
import org.pitest.bytecode.analysis.MethodTree; | ||
import org.pitest.mutationtest.build.InterceptorType; | ||
import org.pitest.mutationtest.build.MutationInterceptor; | ||
import org.pitest.mutationtest.engine.Mutater; | ||
import org.pitest.mutationtest.engine.MutationDetails; | ||
|
||
import java.util.Collection; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.function.Predicate; | ||
import java.util.stream.Collectors; | ||
|
||
public class RecordFilter implements MutationInterceptor { | ||
|
||
private boolean isRecord; | ||
private ClassTree currentClass; | ||
|
||
@Override | ||
public InterceptorType type() { | ||
return InterceptorType.FILTER; | ||
} | ||
|
||
@Override | ||
public void begin(ClassTree clazz) { | ||
isRecord = "java/lang/Record".equals(clazz.rawNode().superName); | ||
currentClass = clazz; | ||
} | ||
|
||
@Override | ||
public Collection<MutationDetails> intercept(Collection<MutationDetails> mutations, Mutater m) { | ||
if (isRecord) { | ||
return mutations.stream() | ||
.filter(makeMethodFilter(currentClass)) | ||
.collect(Collectors.toList()); | ||
} | ||
return mutations; | ||
} | ||
|
||
private Predicate<MutationDetails> makeMethodFilter(ClassTree currentClass) { | ||
Set<String> accessorNames = currentClass.recordComponents().stream() | ||
.map(this::toName) | ||
.collect(Collectors.toSet()); | ||
return m -> !accessorNames.contains(m.getMethod().name()) && !isStandardMethod(accessorNames.size(), m); | ||
} | ||
|
||
private boolean isStandardMethod(int numberOfComponents, MutationDetails m) { | ||
return isRecordInit(m, numberOfComponents) | ||
|| isRecordEquals(m) | ||
|| isRecordHashCode(m) | ||
|| isRecordToString(m); | ||
} | ||
|
||
private boolean isRecordInit(MutationDetails m, int numberOfComponents) { | ||
// constructors with the same airty as the generated ones, but different | ||
// types won't get mutated. They're probably rare enough that this doesn't matter. | ||
int airty = Type.getArgumentTypes(m.getId().getLocation().getMethodDesc()).length; | ||
return m.getMethod().name().equals("<init>") && airty == numberOfComponents; | ||
} | ||
|
||
private boolean isRecordEquals(MutationDetails m) { | ||
return m.getId().getLocation().getMethodDesc().equals("(Ljava/lang/Object;)Z") | ||
&& m.getMethod().name().equals("equals") | ||
&& hasDynamicObjectMethodsCall(m); | ||
} | ||
|
||
private boolean isRecordHashCode(MutationDetails m) { | ||
return m.getId().getLocation().getMethodDesc().equals("()I") | ||
&& m.getMethod().name().equals("hashCode") | ||
&& hasDynamicObjectMethodsCall(m); | ||
} | ||
|
||
private boolean isRecordToString(MutationDetails m) { | ||
return m.getId().getLocation().getMethodDesc().equals("()Ljava/lang/String;") | ||
&& m.getMethod().name().equals("toString") | ||
&& hasDynamicObjectMethodsCall(m); | ||
} | ||
|
||
private boolean hasDynamicObjectMethodsCall(MutationDetails mutation) { | ||
// java/lang/runtime/ObjectMethods was added to support records and can be used as a marker | ||
// for an auth generated equals method. It's not likely that a custom method would | ||
// contain a dynamic call to it | ||
Optional<MethodTree> method = currentClass.method(mutation.getId().getLocation()); | ||
return method.filter(m -> m.instructions().stream().anyMatch(this::isInvokeDynamicCallToObjectMethods)) | ||
.isPresent(); | ||
} | ||
|
||
private boolean isInvokeDynamicCallToObjectMethods(AbstractInsnNode node) { | ||
if (node instanceof InvokeDynamicInsnNode) { | ||
InvokeDynamicInsnNode call = (InvokeDynamicInsnNode) node; | ||
return call.bsm.getOwner().equals("java/lang/runtime/ObjectMethods") | ||
&& call.bsm.getName().equals("bootstrap"); | ||
|
||
} | ||
return false; | ||
} | ||
|
||
private String toName(RecordComponentNode recordComponentNode) { | ||
return recordComponentNode.name; | ||
} | ||
|
||
@Override | ||
public void end() { | ||
currentClass = null; | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
...c/main/java/org/pitest/mutationtest/build/intercept/javafeatures/RecordFilterFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.pitest.mutationtest.build.intercept.javafeatures; | ||
|
||
import org.pitest.mutationtest.build.InterceptorParameters; | ||
import org.pitest.mutationtest.build.MutationInterceptor; | ||
import org.pitest.mutationtest.build.MutationInterceptorFactory; | ||
import org.pitest.plugin.Feature; | ||
|
||
public class RecordFilterFactory implements MutationInterceptorFactory { | ||
|
||
@Override | ||
public String description() { | ||
return "Record junk mutation filter"; | ||
} | ||
|
||
@Override | ||
public MutationInterceptor createInterceptor(InterceptorParameters params) { | ||
return new RecordFilter(); | ||
} | ||
|
||
@Override | ||
public Feature provides() { | ||
return Feature.named("FRECORD") | ||
.withOnByDefault(true) | ||
.withDescription("Filters mutations in compiler generated record code"); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
...-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/NotARecord.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.pitest.mutationtest.build.intercept.javafeatures; | ||
|
||
public class NotARecord { | ||
private final String value; | ||
|
||
public NotARecord(String value) { | ||
this.value = value; | ||
} | ||
|
||
public final String value() { | ||
return value; | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
.../src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/RecordFilterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package org.pitest.mutationtest.build.intercept.javafeatures; | ||
|
||
import org.junit.Test; | ||
import org.pitest.mutationtest.build.InterceptorType; | ||
import org.pitest.mutationtest.engine.MutationDetails; | ||
import org.pitest.mutationtest.engine.gregor.config.Mutator; | ||
|
||
import java.util.function.Predicate; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
public class RecordFilterTest { | ||
private static final String PATH = "records/{0}_{1}"; | ||
|
||
RecordFilter testee = new RecordFilter(); | ||
FilterTester verifier = new FilterTester(PATH, this.testee, Mutator.all()); | ||
|
||
@Test | ||
public void declaresTypeAsFilter() { | ||
assertThat(this.testee.type()).isEqualTo(InterceptorType.FILTER); | ||
} | ||
|
||
@Test | ||
public void findsNoMutantsInEmptyRecord() { | ||
this.verifier.assertLeavesNMutants(0, "NoDataRecord"); | ||
} | ||
|
||
@Test | ||
public void findsNoMutantsInPureRecord() { | ||
this.verifier.assertLeavesNMutants(0, "PureRecord"); | ||
} | ||
|
||
@Test | ||
public void findsMutantsInNormalClass() { | ||
this.verifier.assertFiltersNoMutationsMatching(m -> true, NotARecord.class); | ||
} | ||
|
||
@Test | ||
public void mutatesNonRecordMethods() { | ||
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("extraMethod"), "CustomRecord"); | ||
} | ||
|
||
@Test | ||
public void mutatesCustomConstructors() { | ||
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("<init>").and(removesSysOutCall()), | ||
"RecordWithCustomConstructor"); | ||
} | ||
|
||
@Test | ||
public void mutatesCustomEqualsMethods() { | ||
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("equals"), | ||
"RecordWithCustomEquals"); | ||
} | ||
|
||
@Test | ||
public void mutatesCustomHashCodeMethods() { | ||
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("hashCode"), | ||
"RecordWithCustomHashCode"); | ||
} | ||
|
||
@Test | ||
public void mutatesCustomToStringMethods() { | ||
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("toString"), | ||
"RecordWithCustomToString"); | ||
} | ||
|
||
private Predicate<MutationDetails> removesSysOutCall() { | ||
return m -> m.getDescription().contains("PrintStream::println"); | ||
} | ||
|
||
private Predicate<MutationDetails> inMethodCalled(String name) { | ||
return m -> m.getMethod().name().equals(name); | ||
} | ||
|
||
} |
Binary file added
BIN
+1.85 KB
pitest-entry/src/test/resources/sampleClasses/records/CustomRecord_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.15 KB
pitest-entry/src/test/resources/sampleClasses/records/NoDataRecord_javac.class.bin
Binary file not shown.
Binary file added
BIN
+457 Bytes
pitest-entry/src/test/resources/sampleClasses/records/NotARecord_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.51 KB
pitest-entry/src/test/resources/sampleClasses/records/PureRecord_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.91 KB
...ntry/src/test/resources/sampleClasses/records/RecordWithCustomConstructor_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.75 KB
pitest-entry/src/test/resources/sampleClasses/records/RecordWithCustomEquals_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.75 KB
...t-entry/src/test/resources/sampleClasses/records/RecordWithCustomHashCode_javac.class.bin
Binary file not shown.
Binary file added
BIN
+1.73 KB
...t-entry/src/test/resources/sampleClasses/records/RecordWithCustomToString_javac.class.bin
Binary file not shown.
Oops, something went wrong.