diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollection.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollection.java new file mode 100644 index 000000000..cb96139b3 --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollection.java @@ -0,0 +1,57 @@ +package org.pitest.mutationtest.build.intercept.defensive; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.pitest.bytecode.analysis.MethodTree; +import org.pitest.classinfo.ClassName; +import org.pitest.mutationtest.build.intercept.Region; +import org.pitest.mutationtest.build.intercept.RegionInterceptor; +import org.pitest.sequence.Context; +import org.pitest.sequence.Match; +import org.pitest.sequence.QueryParams; +import org.pitest.sequence.QueryStart; +import org.pitest.sequence.SequenceMatcher; +import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotWrite; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.isA; +import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallTo; +import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction; +import static org.pitest.bytecode.analysis.OpcodeMatchers.ARETURN; +import static org.pitest.bytecode.analysis.OpcodeMatchers.INVOKESTATIC; +import static org.pitest.sequence.Result.result; + +public class ReturnUnmodifiableCollection extends RegionInterceptor { + + private static final ClassName COLLECTIONS = ClassName.fromClass(Collections.class); + + static final Slot MUTATED_INSTRUCTION = Slot.create(AbstractInsnNode.class); + + static final SequenceMatcher DEFENSIVE_RETURN = QueryStart + .any(AbstractInsnNode.class) + .then(INVOKESTATIC.and(methodCallTo(COLLECTIONS, "unmodifiableSet")).and(store(MUTATED_INSTRUCTION.write()))) + .then(ARETURN) + .zeroOrMore(QueryStart.match(anyInstruction())) + .compile(QueryParams.params(AbstractInsnNode.class) + .withIgnores(notAnInstruction().or(isA(LabelNode.class))) + ); + + + @Override + protected List computeRegions(MethodTree method) { + Context context = Context.start(); + return DEFENSIVE_RETURN.contextMatches(method.instructions(), context).stream() + .map(c -> c.retrieve(MUTATED_INSTRUCTION.read()).get()) + .map(n -> new Region(n, n)) + .collect(Collectors.toList()); + } + + private static Match store(SlotWrite slot) { + return (c,n) -> result(true, c.store(slot, n)); + } +} diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactory.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactory.java new file mode 100644 index 000000000..0543537d4 --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactory.java @@ -0,0 +1,25 @@ +package org.pitest.mutationtest.build.intercept.defensive; + +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 ReturnUnmodifiableCollectionFactory implements MutationInterceptorFactory { + @Override + public MutationInterceptor createInterceptor(InterceptorParameters params) { + return new ReturnUnmodifiableCollection(); + } + + @Override + public Feature provides() { + return Feature.named("DEFENSIVERETURN") + .withOnByDefault(true) + .withDescription(description()); + } + + @Override + public String description() { + return "Filter mutations to defensive return wrappers such as unmodifiableCollection"; + } +} diff --git a/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory b/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory index 3a8988333..441e2915d 100755 --- a/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory +++ b/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory @@ -24,5 +24,7 @@ org.pitest.mutationtest.build.intercept.equivalent.EquivalentReturnMutationFilte org.pitest.mutationtest.build.intercept.exclude.FirstLineInterceptorFactory org.pitest.mutationtest.build.intercept.equivalent.DivisionByMinusOneFilterFactory org.pitest.mutationtest.build.intercept.lombok.LombokFilter +org.pitest.mutationtest.build.intercept.defensive.ReturnUnmodifiableCollectionFactory + org.pitest.plugin.export.MutantExportFactory diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactoryTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactoryTest.java new file mode 100644 index 000000000..9ed6cced7 --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/defensive/ReturnUnmodifiableCollectionFactoryTest.java @@ -0,0 +1,97 @@ +package org.pitest.mutationtest.build.intercept.defensive; + +import org.junit.Test; +import org.pitest.mutationtest.build.InterceptorType; +import org.pitest.mutationtest.build.MutationInterceptorFactory; +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 java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.pitest.bytecode.analysis.OpcodeMatchers.INVOKESTATIC; + + +public class ReturnUnmodifiableCollectionFactoryTest { + private final MutationInterceptorFactory underTest = new ReturnUnmodifiableCollectionFactory(); + InterceptorVerifier v = VerifierStart.forInterceptorFactory(underTest) + .usingMutator(new NullMutateEverything()); + + @Test + public void isOnChain() { + FactoryVerifier.confirmFactory(underTest) + .isOnChain(); + } + + @Test + public void isOnByDefault() { + FactoryVerifier.confirmFactory(underTest) + .isOnByDefault(); + } + + @Test + public void featureIsCalledLombok() { + FactoryVerifier.confirmFactory(underTest) + .featureName().isEqualTo("defensivereturn"); + } + + @Test + public void createsFilters() { + FactoryVerifier.confirmFactory(underTest) + .createsInterceptorsOfType(InterceptorType.FILTER); + } + + + @Test + public void filtersMutationsToReturnUnmodifiableSet() { + v.forClass(HasUnmodifiableSetReturn.class) + .forCodeMatching(INVOKESTATIC.asPredicate()) + .allMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotFilterOtherCode() { + v.forClass(HasUnmodifiableSetReturn.class) + .forCodeMatching(INVOKESTATIC.asPredicate().negate()) + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotFilterOtherCallsToUnModifiableSet() { + v.forClass(HasUnmodifiableSetNonReturn.class) + .forAnyCode() + .noMutantsAreFiltered() + .verify(); + } +} + +class HasUnmodifiableSetReturn { + private final Set s = new HashSet<>(); + + public Set mutateMe(int i) { + if (i != 1) { + return Collections.unmodifiableSet(s); + } + + return s; + } +} + +class HasUnmodifiableSetNonReturn { + private final Set s = new HashSet<>(); + private Set copy; + + + public Set dontMutateME(int i) { + if (i != 1) { + copy = Collections.unmodifiableSet(s); + } + + return s; + } +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/equivalent/EqualsPerformanceShortcutFilterTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/equivalent/EqualsPerformanceShortcutFilterTest.java index 6a14186c4..57059f686 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/equivalent/EqualsPerformanceShortcutFilterTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/equivalent/EqualsPerformanceShortcutFilterTest.java @@ -17,7 +17,6 @@ import org.pitest.mutationtest.engine.gregor.MethodMutatorFactory; import org.pitest.mutationtest.engine.gregor.config.Mutator; import org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanFalseReturnValsMutator; -import org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator; public class EqualsPerformanceShortcutFilterTest { diff --git a/pitest/src/main/java/org/pitest/util/StreamUtil.java b/pitest/src/main/java/org/pitest/util/StreamUtil.java index f0eae4455..17cc25d90 100644 --- a/pitest/src/main/java/org/pitest/util/StreamUtil.java +++ b/pitest/src/main/java/org/pitest/util/StreamUtil.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -35,7 +36,7 @@ private static void copy(final InputStream input, final OutputStream output) final WritableByteChannel dest = Channels.newChannel(output); final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (src.read(buffer) != -1) { - buffer.flip(); + ((Buffer)buffer).flip(); dest.write(buffer); buffer.compact(); }