From 0bfb60e4cf322f483c0c2660af5a6349ea131286 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 25 Sep 2025 17:35:58 +0200 Subject: [PATCH 1/2] test: use shadowed annotations; assert that max length is respected --- .../mutation/ArgumentsMutatorFuzzTest.java | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java index f5d2a6372..d3be74e3a 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java @@ -20,13 +20,13 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.junit.FuzzTest; -import com.code_intelligence.jazzer.mutation.annotation.WithSize; -import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; import com.code_intelligence.jazzer.protobuf.Proto2; import com.code_intelligence.jazzer.protobuf.Proto3; import com.code_intelligence.selffuzz.jazzer.mutation.ArgumentsMutator; import com.code_intelligence.selffuzz.jazzer.mutation.annotation.NotNull; import com.code_intelligence.selffuzz.jazzer.mutation.annotation.WithLength; +import com.code_intelligence.selffuzz.jazzer.mutation.annotation.WithSize; +import com.code_intelligence.selffuzz.jazzer.mutation.annotation.WithUtf8Length; import com.code_intelligence.selffuzz.jazzer.mutation.mutator.Mutators; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -102,21 +102,32 @@ void fuzzStrings( @NotNull String s1, @NotNull @WithUtf8Length(min = 10, max = 20) String s2) {} - @SelfFuzzTest // BUG: null pointer exception - void fuzzListOfMaps(Map nullableMap) {} + @SelfFuzzTest + void fuzzListOfMaps(@WithSize(max = 4) Map nullableMap) { + if (nullableMap != null) { + assertThat(nullableMap.size()).isAtMost(4); + } + } @SelfFuzzTest void fuzzListOfLists(List<@NotNull List> nullableMap, List> nullableList) {} @SelfFuzzTest - void fuzzPPrimitiveArrays( - int @WithLength(max = 10) [] a0, boolean[] a2, int @WithLength(max = 8193) [] a3) {} + void fuzzPrimitiveArrays( + int @WithLength(max = 10) [] a0, boolean[] a2, int @WithLength(max = 8193) [] a3) { + if (a0 != null) assertThat(a0.length).isAtMost(10); + if (a3 != null) assertThat(a3.length).isAtMost(8193); + } @SelfFuzzTest void fuzzBean(@NotNull ConstructorPropertiesAnnotatedBean bean, BeanWithParent beanWithParent) {} @SelfFuzzTest - void fuzzListOfBeans(@WithSize(max = 4) List beanWithParent) {} + void fuzzListOfBeans(@WithSize(max = 4) List beanWithParent) { + if (beanWithParent != null) { + assertThat(beanWithParent.size()).isAtMost(4); + } + } @SelfFuzzTest void fuzzListOfListOfBeans( @@ -189,7 +200,15 @@ void fuzzPrimitiveArrays( Byte @WithLength(max = 3) [] by0, byte[] by1, Short @WithLength(max = 3) [] s0, - short[] s1) {} + short[] s1) { + if (i0 != null) assertThat(i0.length).isAtMost(3); + if (b0 != null) assertThat(b0.length).isAtMost(3); + if (d0 != null) assertThat(d0.length).isAtMost(3); + if (f0 != null) assertThat(f0.length).isAtMost(3); + if (l0 != null) assertThat(l0.length).isAtMost(3); + if (by0 != null) assertThat(by0.length).isAtMost(3); + if (s0 != null) assertThat(s0.length).isAtMost(3); + } enum MyEnum { A, From ccb46bcef16d7e508bcc7f6536d99e1eb3562faa Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 25 Sep 2025 18:04:58 +0200 Subject: [PATCH 2/2] feat: add set mutator --- .../mutation/ArgumentsMutatorFuzzTest.java | 8 + .../jazzer/mutation/annotation/WithSize.java | 3 +- .../mutator/collection/ChunkCrossOvers.java | 87 +++ .../mutator/collection/ChunkMutations.java | 51 +- .../collection/CollectionMutators.java | 6 +- .../mutator/collection/SetMutatorFactory.java | 207 +++++++ .../jazzer/mutation/mutator/StressTest.java | 51 +- .../mutator/collection/SetMutatorTest.java | 512 ++++++++++++++++++ .../jazzer/mutation/support/BUILD.bazel | 1 + .../jazzer/mutation/support/TestSupport.java | 42 ++ 10 files changed, 963 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorFactory.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorTest.java diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java index d3be74e3a..06b1df4a3 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java @@ -42,6 +42,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class ArgumentsMutatorFuzzTest { @@ -109,6 +110,13 @@ void fuzzListOfMaps(@WithSize(max = 4) Map nullableMap) { } } + @SelfFuzzTest + void fuzzListOfSets(@WithSize(max = 10) @NotNull Set<@NotNull Integer> setWithSize) { + if (setWithSize != null) { + assertThat(setWithSize.size()).isAtMost(10); + } + } + @SelfFuzzTest void fuzzListOfLists(List<@NotNull List> nullableMap, List> nullableList) {} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java index 5d735158b..0a78ae7aa 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java @@ -26,6 +26,7 @@ import java.lang.annotation.Target; import java.util.List; import java.util.Map; +import java.util.Set; /** * Generates a {@link List} or {@link Map} with the specified size. @@ -35,7 +36,7 @@ */ @Target(TYPE_USE) @Retention(RUNTIME) -@AppliesTo({List.class, Map.class}) +@AppliesTo({List.class, Map.class, Set.class}) @ValidateContainerDimensions @PropertyConstraint public @interface WithSize { diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java index 40321cb66..6a4f76958 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java @@ -22,9 +22,11 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; final class ChunkCrossOvers { private ChunkCrossOvers() {} @@ -98,6 +100,25 @@ static void insertChunk( } } + static void insertChunk( + Set set, Set otherSet, int maxSize, PseudoRandom prng, boolean hasFixedSize) { + int originalSize = set.size(); + int maxChunkSize = Math.min(maxSize - originalSize, otherSet.size()); + int chunkSize = prng.sizeInClosedRange(1, maxChunkSize, hasFixedSize); + int fromChunkOffset = prng.closedRange(0, otherSet.size() - chunkSize); + Iterator fromIterator = otherSet.iterator(); + for (int i = 0; i < fromChunkOffset; i++) { + fromIterator.next(); + } + // insertChunk only inserts new entries and does not overwrite existing + // ones. As skipping those entries would lead to fewer insertions than + // requested, loop over the rest of the set to fill the chunk if possible. + while (set.size() < originalSize + chunkSize && fromIterator.hasNext()) { + K key = fromIterator.next(); + set.add(key); + } + } + static void overwriteChunk( Map map, Map otherMap, PseudoRandom prng, boolean hasFixedSize) { onCorrespondingChunks( @@ -117,6 +138,24 @@ static void overwriteChunk( hasFixedSize); } + static void overwriteChunk( + Set set, Set otherSet, PseudoRandom prng, boolean hasFixedSize) { + onCorrespondingChunks( + set, + otherSet, + prng, + (fromIterator, toIterator, chunkSize) -> { + Set elementsToAdd = new LinkedHashSet<>(chunkSize); + for (int i = 0; i < chunkSize; i++) { + toIterator.next(); + toIterator.remove(); + elementsToAdd.add(fromIterator.next()); + } + set.addAll(elementsToAdd); + }, + hasFixedSize); + } + static void crossOverChunk( Map map, Map otherMap, @@ -130,6 +169,23 @@ static void crossOverChunk( } } + static void crossOverChunk( + Set set, Set otherSet, SerializingMutator mutator, PseudoRandom prng) { + onCorrespondingChunks( + set, + otherSet, + prng, + (fromIterator, toIterator, chunkSize) -> { + Set elementsToAdd = new LinkedHashSet<>(chunkSize); + for (int i = 0; i < chunkSize; i++) { + elementsToAdd.add(mutator.crossOver(toIterator.next(), fromIterator.next(), prng)); + toIterator.remove(); + } + set.addAll(elementsToAdd); + }, + mutator.hasFixedSize()); + } + private static void crossOverChunkKeys( Map map, Map otherMap, SerializingMutator keyMutator, PseudoRandom prng) { onCorrespondingChunks( @@ -198,6 +254,11 @@ private interface ChunkMapOperation { void apply(Iterator> fromIterator, Iterator> toIterator, int chunkSize); } + @FunctionalInterface + private interface ChunkSetOperation { + void apply(Iterator fromIterator, Iterator toIterator, int chunkSize); + } + static void onCorrespondingChunks( Map map, Map otherMap, @@ -219,6 +280,32 @@ static void onCorrespondingChunks( operation.apply(fromIterator, toIterator, chunkSize); } + static void onCorrespondingChunks( + Set set, + Set otherSet, + PseudoRandom prng, + ChunkSetOperation operation, + boolean hasFixedSize) { + + if (set.isEmpty() || otherSet.isEmpty()) { + return; + } + + int maxChunkSize = Math.min(set.size(), otherSet.size()); + int chunkSize = prng.sizeInClosedRange(1, maxChunkSize, hasFixedSize); + int fromChunkOffset = prng.closedRange(0, otherSet.size() - chunkSize); + int toChunkOffset = prng.closedRange(0, set.size() - chunkSize); + Iterator fromIterator = otherSet.iterator(); + for (int i = 0; i < fromChunkOffset; i++) { + fromIterator.next(); + } + Iterator toIterator = set.iterator(); + for (int i = 0; i < toChunkOffset; i++) { + toIterator.next(); + } + operation.apply(fromIterator, toIterator, chunkSize); + } + public enum CrossOverAction { INSERT_CHUNK, OVERWRITE_CHUNK, diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java index b32d2e6af..3d7838936 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java @@ -16,10 +16,11 @@ package com.code_intelligence.jazzer.mutation.mutator.collection; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; + import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; import com.code_intelligence.jazzer.mutation.api.ValueMutator; -import com.code_intelligence.jazzer.mutation.support.Preconditions; import java.util.AbstractList; import java.util.ArrayDeque; import java.util.ArrayList; @@ -162,6 +163,52 @@ static boolean mutateRandomKeysChunk( return grownBy > 0; } + static boolean mutateRandomChunk( + Set set, SerializingMutator elementMutator, PseudoRandom prng) { + int originalSize = set.size(); + require(originalSize > 0, "mutateRandomSetChunk requires set size > 0"); + int chunkSize = prng.sizeInClosedRange(1, originalSize, elementMutator.hasFixedSize()); + int chunkOffset = prng.closedRange(0, originalSize - chunkSize); + + Iterator iterator = set.iterator(); + + // Skip to chunk offset + for (int i = 0; i < chunkOffset; i++) { + iterator.next(); + } + + // Collect elements to mutate and remove + List originalElements = new ArrayList<>(chunkSize); + List elementsToMutate = new ArrayList<>(chunkSize); + for (int i = 0; i < chunkSize; i++) { + E element = iterator.next(); + originalElements.add(element); + elementsToMutate.add(elementMutator.detach(element)); + } + + // Try mutating each chunk element into a yet-not-present element in the set. + // Abort after MAX_FAILED_INSERTION_ATTEMPTS failed mutations in total. + int successCount = 0; + int failedAttemptsCount = 0; + for (E element : elementsToMutate) { + while (failedAttemptsCount < MAX_FAILED_INSERTION_ATTEMPTS) { + // Each element keeps getting mutated until we get a completely novel element. + element = elementMutator.mutate(element, prng); + if (set.add(element)) { + successCount++; + break; + } else { + failedAttemptsCount++; + } + } + } + + for (int i = 0; i < successCount; i++) { + set.remove(originalElements.get(i)); + } + return successCount > 0; + } + public static void mutateRandomValuesChunk( Map map, ValueMutator valueMutator, PseudoRandom prng) { Collection> collection = map.entrySet(); @@ -182,7 +229,7 @@ public static void mutateRandomValuesChunk( static boolean growBy( Set set, Consumer addIfNew, int delta, Supplier candidateSupplier) { int oldSize = set.size(); - Preconditions.require(delta >= 0); + require(delta >= 0); final int targetSize = oldSize + delta; int remainingAttempts = MAX_FAILED_INSERTION_ATTEMPTS; diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java index 88152589e..e8ba34d60 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java @@ -23,6 +23,10 @@ public final class CollectionMutators { private CollectionMutators() {} public static Stream newFactories() { - return Stream.of(new ListMutatorFactory(), new MapMutatorFactory(), new ArrayMutatorFactory()); + return Stream.of( + new ListMutatorFactory(), + new MapMutatorFactory(), + new SetMutatorFactory(), + new ArrayMutatorFactory()); } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorFactory.java new file mode 100644 index 000000000..a85b7cfe4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorFactory.java @@ -0,0 +1,207 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.mutator.collection; + +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.CrossOverAction.pickRandomCrossOverAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.crossOverChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.insertChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.overwriteChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.deleteRandomChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.growBy; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.insertRandomChunk; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypesIfParameterized; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.util.stream.Collectors.toSet; + +import com.code_intelligence.jazzer.mutation.annotation.WithSize; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.RandomSupport; +import com.code_intelligence.jazzer.mutation.support.StreamSupport; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +final class SetMutatorFactory implements MutatorFactory { + @Override + public Optional> tryCreate( + AnnotatedType type, ExtendedMutatorFactory factory) { + return parameterTypesIfParameterized(type, Set.class) + .map( + parameterTypes -> + parameterTypes.stream() + .map(innerType -> propagatePropertyConstraints(type, innerType)) + .map(factory::tryCreate) + .flatMap(StreamSupport::getOrEmpty) + .collect(Collectors.toList())) + .filter(elementMutator -> elementMutator.size() == 1) + .map( + elementMutator -> { + int min = SetMutator.DEFAULT_MIN_SIZE; + int max = SetMutator.DEFAULT_MAX_SIZE; + for (Annotation annotation : type.getDeclaredAnnotations()) { + if (annotation instanceof WithSize) { + WithSize withSize = (WithSize) annotation; + min = withSize.min(); + max = withSize.max(); + } + } + + return new SetMutator<>(elementMutator.get(0), min, max); + }); + } + + private static final class SetMutator extends SerializingInPlaceMutator> { + private static final int DEFAULT_MIN_SIZE = 0; + private static final int DEFAULT_MAX_SIZE = 1000; + + private final SerializingMutator keyMutator; + private final int minSize; + private final int maxSize; + + SetMutator(SerializingMutator keyMutator, int minSize, int maxSize) { + this.keyMutator = keyMutator; + this.minSize = minSize; + this.maxSize = maxSize; + + require(maxSize >= 1, format("WithSize#max=%d needs to be greater than 0", maxSize)); + // TODO: Add support for min > 0 to set. If min > 0, then #read can fail to construct + // sufficiently many distinct keys, but the mutation framework currently doesn't offer + // a way to handle this situation gracefully. It is also not clear what behavior users + // could reasonably expect in this situation in both regression test and fuzzing mode. + require(minSize == 0, "@WithSize#min != 0 is not yet supported for Set"); + } + + @Override + public Set read(DataInputStream in) throws IOException { + int size = RandomSupport.clamp(in.readInt(), minSize, maxSize); + Set set = new LinkedHashSet<>(size); + for (int i = 0; i < size; i++) { + set.add(keyMutator.read(in)); + } + // set may have less than size entries due to the potential for duplicates, but this is fine + // as we currently assert that minSize == 0. + return set; + } + + @Override + public void write(Set set, DataOutputStream out) throws IOException { + out.writeInt(set.size()); + for (K entry : set) { + keyMutator.write(entry, out); + } + } + + @Override + protected Set makeDefaultInstance() { + return new LinkedHashSet<>(maxInitialSize()); + } + + @Override + public void initInPlace(Set set, PseudoRandom prng) { + int targetSize = prng.closedRange(minInitialSize(), maxInitialSize()); + set.clear(); + growBy(set, set::add, targetSize, () -> keyMutator.init(prng)); + if (set.size() < minSize) { + throw new IllegalStateException( + String.format( + "Failed to create %d distinct elements of type %s to satisfy the @WithSize#minSize" + + " constraint on Set", + minSize, keyMutator)); + } + } + + @Override + public void mutateInPlace(Set set, PseudoRandom prng) { + switch (pickRandomMutationAction(set, minSize, maxSize, prng)) { + case DELETE_CHUNK: + deleteRandomChunk(set, minSize, prng, entriesHaveFixedSize()); + break; + case INSERT_CHUNK: + insertRandomChunk(set, set::add, maxSize, keyMutator, prng); + break; + case MUTATE_CHUNK: + ChunkMutations.mutateRandomChunk(set, keyMutator, prng); + break; + default: + throw new IllegalStateException("unsupported action"); + } + } + + @Override + public void crossOverInPlace(Set reference, Set otherReference, PseudoRandom prng) { + switch (pickRandomCrossOverAction(reference, otherReference, maxSize, prng)) { + case INSERT_CHUNK: + insertChunk(reference, otherReference, maxSize, prng, entriesHaveFixedSize()); + break; + case OVERWRITE_CHUNK: + overwriteChunk(reference, otherReference, prng, entriesHaveFixedSize()); + break; + case CROSS_OVER_CHUNK: + crossOverChunk(reference, otherReference, keyMutator, prng); + break; + default: + // Both sets are empty or could otherwise not be crossed over. + } + } + + private boolean entriesHaveFixedSize() { + return keyMutator.hasFixedSize(); + } + + @Override + public boolean hasFixedSize() { + return false; + } + + @Override + public Set detach(Set value) { + return value.stream().map(keyMutator::detach).collect(toSet()); + } + + @Override + public String toDebugString(Predicate isInCycle) { + return "Set<" + keyMutator.toDebugString(isInCycle) + ">"; + } + + private int minInitialSize() { + return minSize; + } + + private int maxInitialSize() { + if (keyMutator.requiresRecursionBreaking()) { + return minInitialSize(); + } + return min(maxSize, minSize + 1); + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index ab5125223..29e0aad35 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -21,6 +21,7 @@ import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; import static com.code_intelligence.jazzer.mutation.support.TestSupport.anyPseudoRandom; import static com.code_intelligence.jazzer.mutation.support.TestSupport.asMap; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.asSet; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.lang.Math.floor; @@ -95,6 +96,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -635,6 +637,31 @@ null, emptyList(), singletonList(null), singletonList(false), singletonList(true asMap(false, false, true, true), asMap(false, true, true, false), asMap(false, true, true, true))), + arguments( + new TypeHolder<@NotNull Set<@NotNull String>>() {}.annotatedType(), + "Set", + false, + distinctElementsRatio(0.45), + distinctElementsRatio(0.45)), + arguments( + new TypeHolder>() {}.annotatedType(), + "Nullable>", + false, + distinctElementsRatio(0.46), + distinctElementsRatio(0.48)), + arguments( + new TypeHolder<@WithSize(max = 3) @NotNull Set<@NotNull Integer>>() {}.annotatedType(), + "Set", + false, + // Half of all sets are empty, the other half is heavily biased towards special values. + all(setSizeInClosedRange(0, 3), distinctElementsRatio(0.09)), + all(setSizeInClosedRange(0, 3), manyDistinctElements())), + arguments( + new TypeHolder<@NotNull Set<@NotNull Boolean>>() {}.annotatedType(), + "Set", + false, + exactly(asSet(), asSet(false), asSet(true)), + exactly(asSet(), asSet(false), asSet(true), asSet(false, true))), arguments( new ParameterHolder() { void singleParam(byte parameter) {} @@ -1378,6 +1405,24 @@ public void close() {} }; } + private static CloseableConsumer setSizeInClosedRange(int min, int max) { + return new CloseableConsumer() { + @Override + public void accept(Object set) { + if (set instanceof Set) { + assertThat(((Set) set).size()).isAtLeast(min); + assertThat(((Set) set).size()).isAtMost(max); + } else { + throw new IllegalArgumentException( + "Expected a list of sets, got list of " + set.getClass().getName()); + } + } + + @Override + public void close() {} + }; + } + interface CloseableConsumer extends AutoCloseable, Consumer {} @SuppressWarnings("rawtypes") @@ -1400,8 +1445,12 @@ void genericMutatorStressTest( // Even with a fallback to mutating map values when no new key can be constructed, the map // {false: true, true: false} will not change its equality class when the fallback picks both // values to mutate. + // Likewise, for sets containing {true, false}, some mutations (e.g. insertion) will not change + // the equality class. boolean mayPerformNoopMutations = - mutatorTree.contains("FixedValue(") || mutatorTree.contains("Map"); + mutatorTree.contains("FixedValue(") + || mutatorTree.contains("Map") + || mutatorTree.contains("Set"); PseudoRandom rng = anyPseudoRandom(); diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorTest.java new file mode 100644 index 000000000..ff04064a0 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/SetMutatorTest.java @@ -0,0 +1,512 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.mutator.collection; + +import static com.code_intelligence.jazzer.mutation.support.TestSupport.ParameterizedTestUtils.prependArgs; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.asSet; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.getCallerMethodName; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.mockPseudoRandom; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.engine.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.mutator.lang.LangMutators; +import com.code_intelligence.jazzer.mutation.support.TestSupport.MockPseudoRandom; +import com.code_intelligence.jazzer.mutation.support.TypeHolder; +import java.lang.reflect.AnnotatedType; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings("unchecked") +class SetMutatorTest { + static ChainedMutatorFactory factory; + + static final int DELETE_CHUNK = ChunkMutations.MutationAction.DELETE_CHUNK.ordinal(); + static final int INSERT_CHUNK = ChunkMutations.MutationAction.INSERT_CHUNK.ordinal(); + static final int MUTATE_CHUNK = ChunkMutations.MutationAction.MUTATE_CHUNK.ordinal(); + + static final int CROSSOVER_INSERT = ChunkCrossOvers.CrossOverAction.INSERT_CHUNK.ordinal(); + static final int CROSSOVER_OVERWRITE = ChunkCrossOvers.CrossOverAction.OVERWRITE_CHUNK.ordinal(); + static final int CROSSOVER_CHUNK = ChunkCrossOvers.CrossOverAction.CROSS_OVER_CHUNK.ordinal(); + + static final int MUT_INT = 2; + + @BeforeEach + void createFactory() { + factory = + ChainedMutatorFactory.of(LangMutators.newFactories(), CollectionMutators.newFactories()); + } + + static Stream deleteChunks() { + Stream base = + Stream.of( + // (DELETE_CHUNK, chunkSize, offset) + arguments(false, arguments(DELETE_CHUNK, 1, 0), asSet(1, 2, 3, 4), asSet(2, 3, 4)), + arguments(false, arguments(DELETE_CHUNK, 1, 1), asSet(1, 2, 3, 4), asSet(1, 3, 4)), + arguments(false, arguments(DELETE_CHUNK, 1, 2), asSet(1, 2, 3, 4), asSet(1, 2, 4)), + arguments(false, arguments(DELETE_CHUNK, 1, 3), asSet(1, 2, 3, 4), asSet(1, 2, 3)), + arguments(false, arguments(DELETE_CHUNK, 2, 0), asSet(1, 2, 3, 4), asSet(3, 4)), + // Deleting several times in a row + arguments( + true, + arguments(DELETE_CHUNK, 1, 0, DELETE_CHUNK, 1, 0, DELETE_CHUNK, 1, 0), + asSet(1, 2, 3, 4), + asSet(4)), + arguments( + true, + arguments(DELETE_CHUNK, 1, 3, DELETE_CHUNK, 1, 2, DELETE_CHUNK, 1, 1), + asSet(1, 2, 3, 4), + asSet(1)), + arguments( + true, + arguments(DELETE_CHUNK, 1, 0, DELETE_CHUNK, 1, 2, DELETE_CHUNK, 1, 0), + asSet(1, 2, 3, 4), + asSet(3)), + arguments( + true, + arguments(DELETE_CHUNK, 2, 1, DELETE_CHUNK, 1, 1), + asSet(1, 2, 3, 4), + asSet(1))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, getCallerMethodName(), type); + } + + static Stream insertChunks() { + Stream base = + Stream.of( + // (INSERT_CHUNK, chunkSize, select new and not specialValue (>= 4), newValue) - + // IntegralMutators use .init() to generate new values + arguments( + false, + arguments(INSERT_CHUNK, 1, 4, 10L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10)), + arguments( + false, + arguments(INSERT_CHUNK, 2, 4, 10L, 4, 20L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10, 20)), + arguments( + false, + arguments(INSERT_CHUNK, 3, 4, 10L, 4, 20L, 4, 30L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10, 20, 30)), + // Insert with retries due to duplicates + arguments( + false, + arguments(INSERT_CHUNK, 1, 4, 1L, 4, 2L, 4, 3L, 4, 4L, 4, 10L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10)), + arguments( + false, + arguments(INSERT_CHUNK, 2, 4, 1L, 4, 2L, 4, 3L, 4, 4L, 4, 10L, 4, 20L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10, 20)), + arguments( + false, + arguments(INSERT_CHUNK, 3, 4, 1L, 4, 2L, 4, 30L, 4, 3L, 4, 4L, 4, 10L, 4, 20L), + asSet(1, 2, 3, 4), + asSet(1, 2, 3, 4, 10, 20, 30))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, getCallerMethodName(), type); + } + + static Stream mutateRandomChunks() { + Stream base = + Stream.of( + // (MUTATE_CHUNK, chunkSize, chunkOffset, MUT, newValue) + arguments( + false, + arguments(MUTATE_CHUNK, 1, 0, MUT_INT, 10L), + asSet(1, 2, 3, 4), + asSet(2, 3, 4, 10)), + arguments( + false, + arguments(MUTATE_CHUNK, 2, 0, MUT_INT, 10L, MUT_INT, 20L), + asSet(1, 2, 3, 4), + asSet(3, 4, 10, 20)), + arguments( + false, + arguments(MUTATE_CHUNK, 2, 1, MUT_INT, 10L, MUT_INT, 20L), + asSet(1, 2, 3, 4), + asSet(1, 4, 10, 20)), + arguments( + false, + arguments(MUTATE_CHUNK, 2, 2, MUT_INT, 10L, MUT_INT, 20L), + asSet(1, 2, 3, 4), + asSet(1, 2, 10, 20)), + arguments( + false, + arguments(MUTATE_CHUNK, 3, 0, MUT_INT, 10L, MUT_INT, 20L, MUT_INT, 30L), + asSet(1, 2, 3, 4), + asSet(10, 20, 30, 4)), + arguments( + false, + arguments(MUTATE_CHUNK, 3, 1, MUT_INT, 10L, MUT_INT, 20L, MUT_INT, 30L), + asSet(1, 2, 3, 4), + asSet(10, 20, 30, 1))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, "mutateRandomChunks", type); + } + + static Stream mutateRandomChunksReachMaxRepetition() { + final AnnotatedType bools = new TypeHolder<@NotNull Set<@NotNull Boolean>>() {}.annotatedType(); + Stream base = + Stream.of( + // (MUTATE_CHUNK, chunkSize, chunkOffset, MUT, newValue) + arguments( + bools, + false, + arguments(MUTATE_CHUNK, 1, 0), + asSet(true, false), + asSet(true, false))); + return prependArgs(base, getCallerMethodName()); + } + + @ParameterizedTest(name = "{index} {0}: input={4}, expected={5}") + @MethodSource({ + "deleteChunks", + "insertChunks", + "mutateRandomChunks", + "mutateRandomChunksReachMaxRepetition" + }) + void testMutatorOperations( + String sourceName, // used for better readability of test names, ignored in method + AnnotatedType type, + boolean allowMultipleMutations, + Arguments prngContent, + Set input, + Set expected) { + SerializingMutator> mutator = (SerializingMutator>) factory.createOrThrow(type); + Set mutated = input; + + try (MockPseudoRandom prng = mockPseudoRandom(prngContent.get())) { + do { + mutated = mutator.mutate(mutated, prng); + } while (allowMultipleMutations && !prng.isEmpty()); + + assertThat(mutated).isEqualTo(expected); + } + } + + static Stream crossOverInsertChunks() { + Stream base = + Stream.of( + // (CROSSOVER_INSERT, chunkSize, chunkOffset) + arguments( + arguments(CROSSOVER_INSERT, 1, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10)), + arguments( + arguments(CROSSOVER_INSERT, 1, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10)), + arguments( + arguments(CROSSOVER_INSERT, 1, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 20)), + arguments( + arguments(CROSSOVER_INSERT, 1, 3), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 30)), + arguments( + arguments(CROSSOVER_INSERT, 1, 4), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 40)), + arguments( + arguments(CROSSOVER_INSERT, 2, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20)), + arguments( + arguments(CROSSOVER_INSERT, 2, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20)), + arguments( + arguments(CROSSOVER_INSERT, 2, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 20, 30)), + arguments( + arguments(CROSSOVER_INSERT, 2, 3), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 30, 40)), + arguments( + arguments(CROSSOVER_INSERT, 3, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20, 30)), + arguments( + arguments(CROSSOVER_INSERT, 3, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20, 30)), + arguments( + arguments(CROSSOVER_INSERT, 3, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 20, 30, 40)), + arguments( + arguments(CROSSOVER_INSERT, 4, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20, 30, 40)), + arguments( + arguments(CROSSOVER_INSERT, 4, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3, 10, 20, 30, 40))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, getCallerMethodName(), type); + } + + static Stream crossOverOverwriteChunks() { + Stream base = + Stream.of( + // (CROSSOVER_OVERWRITE, chunkSize, fromChunkOffset, toChunkOffset) + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 1, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(10, 2, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 2, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(20, 2, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 3, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(30, 2, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 4, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(40, 2, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 0, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 1, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 10, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 2, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 20, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 3, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 30, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 4, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 40, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 0, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 1, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 10)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 2, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 20)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 3, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 30)), + arguments( + arguments(CROSSOVER_OVERWRITE, 1, 4, 2), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 40)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 10, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 1, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(10, 20, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 2, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(20, 30, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 3, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(30, 40, 3)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 0, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 10)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 1, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 10, 20)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 2, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 20, 30)), + arguments( + arguments(CROSSOVER_OVERWRITE, 2, 3, 1), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 30, 40)), + arguments( + arguments(CROSSOVER_OVERWRITE, 3, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 10, 20)), + arguments( + arguments(CROSSOVER_OVERWRITE, 3, 1, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(10, 20, 30)), + arguments( + arguments(CROSSOVER_OVERWRITE, 3, 2, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(20, 30, 40))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, getCallerMethodName(), type); + } + + static Stream crossOverCrossOver() { + Stream base = + Stream.of( + // (CROSSOVER_CHUNK, chunkSize, fromChunkOffset, toChunkOffset, [cross over operation (0 + // == mean)]*) + arguments( + arguments(CROSSOVER_CHUNK, 1, 0, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 2, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 1, 1, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(5, 2, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 1, 2, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(10, 2, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 1, 3, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(15, 2, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 1, 4, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(20, 2, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 0, 0, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 6, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 1, 0, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(5, 11, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 2, 0, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(10, 16, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 3, 0, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(15, 21, 3)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 1, 1, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 6, 11)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 2, 1, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 11, 16)), + arguments( + arguments(CROSSOVER_CHUNK, 2, 3, 1, 0, 0), + asSet(1, 2, 3), + asSet(1, 10, 20, 30, 40), + asSet(1, 16, 21))); + final AnnotatedType type = new TypeHolder<@NotNull Set<@NotNull Integer>>() {}.annotatedType(); + return prependArgs(base, getCallerMethodName(), type); + } + + @ParameterizedTest(name = "{index} {0}: input={3}, otherInput={4}, expected={5}") + @MethodSource({"crossOverInsertChunks", "crossOverOverwriteChunks", "crossOverCrossOver"}) + void testCrossoverOperations( + String sourceName, // used for better readability of test names, ignored in method + AnnotatedType type, + Arguments prngContent, + Set input1, + Set input2, + Set expected) { + SerializingMutator> mutator = (SerializingMutator>) factory.createOrThrow(type); + try (MockPseudoRandom prng = mockPseudoRandom(prngContent.get())) { + Set crossedOver = mutator.crossOver(input1, input2, prng); + assertThat(crossedOver).isEqualTo(expected); + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel index a87f8a391..75b5d57b9 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel @@ -19,6 +19,7 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/mutation/support", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_truth_truth", + "@maven//:org_junit_jupiter_junit_jupiter_params", ], ) diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/TestSupport.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/TestSupport.java index 8417ed238..be16da5d6 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/TestSupport.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/TestSupport.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Queue; import java.util.function.BiConsumer; @@ -50,6 +51,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; public final class TestSupport { private static final DataOutputStream nullDataOutputStream = @@ -262,6 +265,10 @@ public String toString() { return "PRNG: " + Arrays.toString(elements.toArray()); } + public boolean isEmpty() { + return elements.isEmpty(); + } + @Override public boolean choice() { assertThat(elements).isNotEmpty(); @@ -428,6 +435,10 @@ public static LinkedHashMap asMap(Object... objs) { return map; } + public static LinkedHashSet asSet(K... objs) { + return new LinkedHashSet<>(Arrays.asList(objs)); + } + @SafeVarargs public static ArrayList asMutableList(T... objs) { return stream(objs).collect(toCollection(ArrayList::new)); @@ -477,4 +488,35 @@ public static SerializingMutator createOrThrow( ExtendedMutatorFactory factory, TypeHolder typeHolder) { return (SerializingMutator) factory.createOrThrow(typeHolder.annotatedType()); } + + public static String getCallerMethodName() { + StackTraceElement[] s = Thread.currentThread().getStackTrace(); + if (s.length < 3) { + throw new IllegalStateException( + "Not enough stack trace elements to determine caller method name"); + } + return s[2].getMethodName(); + } + + public static final class ParameterizedTestUtils { + private ParameterizedTestUtils() {} + + /** + * Adds an argument to the beginning of each Arguments in a stream. + * + * @param base - the base stream of Arguments + * @param arguments - the arguments to prepend + * @return a stream of Arguments with the given argument prepended before the original ones. + */ + public static Stream prependArgs(Stream base, Object... arguments) { + return base.map( + args -> { + Object[] original = args.get(); + Object[] withArg = new Object[original.length + arguments.length]; + System.arraycopy(arguments, 0, withArg, 0, arguments.length); + System.arraycopy(original, 0, withArg, arguments.length, original.length); + return Arguments.of(withArg); + }); + } + } }