diff --git a/docs/mutation-framework.md b/docs/mutation-framework.md index 59c6e4c58..138a03bef 100644 --- a/docs/mutation-framework.md +++ b/docs/mutation-framework.md @@ -317,6 +317,163 @@ class SimpleClassFuzzTests { } ``` +## @ValuePool: Guide fuzzing with custom values + +The `@ValuePool` annotation lets you provide concrete example values that Jazzer's mutators will use when generating test inputs. +This helps guide fuzzing toward realistic or edge-case values relevant to your application. + +### Basic Usage + +You can apply `@ValuePool` in two places: +- **On method parameters** - values apply only to that parameter's type +- **On the test method itself** - values propagate to all matching types across all parameters + +**Example:** +```java +@FuzzTest +void fuzzTest(@ValuePool(value = {"mySupplier"}) Map foo) { + // Strings from mySupplier feed the Map's String mutator + // Integers from mySupplier feed the Map's Integer mutator +} + +@FuzzTest +@ValuePool(value = {"mySupplier"}) +void anotherFuzzTest(Map foo, String bar) { + // Values propagate to ALL matching types: + // - String mutator for Map keys in 'foo' + // - String mutator for 'bar' + // - Integer mutator for Map values in 'foo' +} + +static Stream mySupplier() { + return Stream.of("example1", "example2", "example3", 1232187321, -182371); +} +``` + +### How Type Matching Works + +Jazzer automatically routes values to mutators based on type: +- Strings in your value pool → String mutators +- Integers in your value pool → Integer mutators +- Byte arrays in your value pool → byte[] mutators + +**Type propagation happens recursively by default**, so a `@ValuePool` on a `Map` will feed both the String mutator (for keys) and Integer mutator (for values). + +--- + +### Supplying Values: Two Mechanisms + +#### 1. Supplier Methods (`value` field) + +Provide the names of static methods that return `Stream`: +```java +@ValuePool(value = {"mySupplier", "anotherSupplier"}) +``` + +**Requirements:** +- Methods must be `static` +- Must return `Stream` +- Can contain mixed types (Jazzer routes by type automatically) + +#### 2. File Patterns (`files` field) + +Load files as `byte[]` arrays using glob patterns: +```java +@ValuePool(files = {"*.jpeg"}) // All JPEGs in working dir +@ValuePool(files = {"**.xml"}) // All XMLs recursively +@ValuePool(files = {"/absolute/path/**"}) // All files from absolute path +@ValuePool(files = {"*.jpg", "**.png"}) // Multiple patterns +``` + +**Glob syntax:** Follows `java.nio.file.PathMatcher` with `glob:` pattern rules. + +**You can combine both mechanisms:** +```java +@ValuePool(value = {"mySupplier"}, files = {"test-data/*.json"}) +``` + +--- + +### Configuration Options + +#### Mutation Probability (`p` field) +Controls how often values from the pool are used versus randomly generated values. +```java +@ValuePool(value = {"mySupplier"}, p = 0.3) // Use pool values 30% of the time +``` + +**Default:** `p = 0.1` (10% of mutations use pool values) +**Range:** 0.0 to 1.0 + +#### Type Propagation (`constraint` field) + +Controls whether the annotation affects nested types: +```java +// Default: RECURSIVE - applies to all nested types +@ValuePool(value = {"mySupplier"}, constraint = Constraint.RECURSIVE) + +// DECLARATION - applies only to the annotated type, not subtypes +@ValuePool(value = {"mySupplier"}, constraint = Constraint.DECLARATION) +``` + +**Example of the difference:** +```java +// With RECURSIVE (default): +@ValuePool(value = {"stringSupplier"}) Map> data +// Strings feed both Map keys AND List elements + +// With DECLARATION: +@ValuePool(value = {"stringSupplier"}, constraint = DECLARATION) Map> data +// Strings only feed Map keys, NOT List elements +``` + +--- + +### Complete Example +```java +class MyFuzzTest { + static Stream edgeCases() { + return Stream.of( + "", "null", "alert('xss')", // Strings + 0, -1, Integer.MAX_VALUE, // Integers + new byte[]{0x00, 0xFF} // A byte array + ); + } + + @FuzzTest + @ValuePool( + value = {"edgeCases"}, + files = {"test-inputs/*.bin"}, + p = 0.25 // Use pool values 25% of the time + ) + void testParser(String input, Map config, byte[] data) { + // All three parameters get values from the pool: + // - 'input' gets Strings + // - 'config' keys get Strings, values get Integers + // - 'data' gets bytes from both edgeCases() and *.bin files + } +} +``` + +--- + +#### Max Mutations (`maxMutations` field) + +After selecting a value from the pool, the mutator can apply additional random mutations to it. +```java +@ValuePool(value = {"mySupplier"}, maxMutations = 5) +``` + +**Default:** `maxMutations = 1` (one additional mutation applied) +**Range:** 0 or higher + +**How it works:** If `maxMutations = 5`, Jazzer will: +1. Select a value from your pool (e.g., `"example"`) +2. Apply up to 5 random mutations (e.g., `"example"` → `"exAmple"` → `"exAmple123"` → ...) + +This helps explore variations of your seed values while staying close to realistic inputs. + + ## FuzzedDataProvider The `FuzzedDataProvider` is an alternative approach commonly used in programming diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel index 46b209ef2..7dc25be62 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel @@ -2,6 +2,7 @@ load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") java_fuzz_target_test( name = "ArgumentsMutatorFuzzTest", + timeout = "long", srcs = [ "ArgumentsMutatorFuzzTest.java", "BeanWithParent.java", diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java index 49294ad8c..d88cfc325 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -178,6 +178,8 @@ private static Path addInputAndSeedDirs( Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse("")) .toAbsolutePath(); + System.setProperty("jazzer.internal.basedir", baseDir.toString()); + // Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class // directory under the project root. // The path is specified relative to the current working directory, which with JUnit is the diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java index db42c791e..27490c3ee 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -43,13 +43,17 @@ import java.lang.reflect.AnnotatedType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; public final class ArgumentsMutator { private final ExtendedMutatorFactory mutatorFactory; private final Method method; private final InPlaceProductMutator productMutator; + private static final Map mutatorsCache = new ConcurrentHashMap<>(); + private Object[] arguments; /** @@ -78,15 +82,14 @@ private static String prettyPrintMethod(Method method) { } public static ArgumentsMutator forMethodOrThrow(Method method) { - return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method) - .orElseThrow( - () -> - new IllegalArgumentException( - "Failed to construct mutator for " + prettyPrintMethod(method))); - } - - public static Optional forMethod(Method method) { - return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method); + return mutatorsCache.computeIfAbsent( + method, + m -> + forMethod(Mutators.newFactory(new ValuePoolRegistry(m)), m) + .orElseThrow( + () -> + new IllegalArgumentException( + "Failed to construct mutator for " + prettyPrintMethod(m)))); } public static Optional forMethod( diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java index ce636c515..f6be6252c 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java @@ -27,7 +27,7 @@ import java.lang.annotation.Target; /** - * Provides values to user-selected mutator types to start fuzzing from. + * Provides values to user-selected types that will be used during mutation. * *

This annotation can be applied to fuzz test methods and any parameter type or subtype. By * default, this annotation is propagated to all nested subtypes unless specified otherwise via the @@ -72,7 +72,28 @@ * don't need to match the type of the annotated method or parameter. The mutation framework will * extract only the values that are compatible with the target type. */ - String[] value(); + String[] value() default {}; + + /** + * Specifies glob patterns matching files that should be provided as {@code byte []} to the + * annotated type. The syntax follows closely to Java's {@link + * java.nio.file.FileSystem#getPathMatcher(String) PathMatcher} "glob:" syntax. + * + *

Relative glob patterns are resolved against the working directory. + * + *

Examples: + * + *

    + *
  • {@code *.jpeg} - matches all jpegs in the working directory + *
  • {@code **.xml} - matches all xml files recursively + *
  • {@code src/test/resources/dict/*.txt} - matches txt files in a specific directory + *
  • {@code /absolute/path/to/some/directory/**} - matches all files in an absolute path + * recursively + *
  • {@code {"*.jpg", "**.png"}} - matches all jpg in the working directory, and png files + * recursively + *
+ */ + String[] files() default {}; /** * This {@code ValuePool} will be used with probability {@code p} by the mutator responsible for @@ -80,9 +101,15 @@ */ double p() default 0.1; + /** + * If the mutator selects a value from this {@code ValuePool}, it will perform up to {@code + * maxMutations} additional mutations on the selected value. + */ + int maxMutations() default 1; + /** * Defines the scope of the annotation. Possible values are defined in {@link - * com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default it's {@code + * com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default, it's {@code * RECURSIVE}. */ String constraint() default RECURSIVE; diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java index 3c2c94877..7c1825dbc 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java @@ -41,7 +41,6 @@ import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; public class ValuePoolMutatorFactory implements MutatorFactory { /** Types annotated with this marker wil not be re-wrapped by this factory. */ @@ -75,12 +74,17 @@ private static final class ValuePoolMutator extends SerializingMutator { private final SerializingMutator mutator; private final List userValues; private final double poolUsageProbability; + private final int maxMutations; ValuePoolMutator( - SerializingMutator mutator, List userValues, double poolUsageProbability) { + SerializingMutator mutator, + List userValues, + double poolUsageProbability, + int maxMutations) { this.mutator = mutator; this.userValues = userValues; this.poolUsageProbability = poolUsageProbability; + this.maxMutations = maxMutations; } @SuppressWarnings("unchecked") @@ -91,14 +95,9 @@ static SerializingMutator wrapIfValuesExist( return mutator; } - Optional> rawUserValues = valuePoolRegistry.extractRawValues(type); - if (!rawUserValues.isPresent()) { - return mutator; - } - List userValues = - rawUserValues - .get() + valuePoolRegistry + .extractUserValues(type) // Values whose round trip serialization is not stable violate either some user // annotations on the type (e.g. @InRange), or the default mutator limits (e.g. // default List size limits) and are therefore not suitable for inclusion in the value @@ -112,7 +111,8 @@ static SerializingMutator wrapIfValuesExist( } double p = valuePoolRegistry.extractFirstProbability(type); - return new ValuePoolMutator<>(mutator, userValues, p); + int maxMutations = valuePoolRegistry.extractFirstMaxMutations(type); + return new ValuePoolMutator<>(mutator, userValues, p, maxMutations); } /** @@ -144,8 +144,8 @@ private static boolean isSerializationStable(SerializingMutator mutator, @Override public String toDebugString(Predicate isInCycle) { return String.format( - "%s (values: %d p: %.2f)", - mutator.toDebugString(isInCycle), userValues.size(), poolUsageProbability); + "%s (values: %d p: %.2f, maxMutations: %d)", + mutator.toDebugString(isInCycle), userValues.size(), poolUsageProbability, maxMutations); } @Override @@ -180,19 +180,28 @@ public T init(PseudoRandom prng) { @Override public T mutate(T value, PseudoRandom prng) { if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { - if (prng.choice()) { - return prng.pickIn(userValues); - } else { - // treat the value from valuePool as a starting point for mutation - return mutator.mutate(prng.pickIn(userValues), prng); + value = prng.pickIn(userValues); + // Treat the user value as a starting point for mutation + for (int i = 0; i < prng.closedRange(0, maxMutations); i++) { + value = mutator.mutate(value, prng); } + return value; } return mutator.mutate(value, prng); } @Override public T crossOver(T value, T otherValue, PseudoRandom prng) { - return mutator.crossOver(value, otherValue, prng); + if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { + value = prng.pickIn(userValues); + // Treat the user value as a starting point for crossOver + for (int i = 0; i < prng.closedRange(0, maxMutations); i++) { + value = mutator.crossOver(value, prng.pickIn(userValues), prng); + } + return value; + } else { + return mutator.crossOver(value, otherValue, prng); + } } } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java index 8b7802122..7f8704c7b 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java @@ -20,11 +20,23 @@ import com.code_intelligence.jazzer.mutation.annotation.ValuePool; import com.code_intelligence.jazzer.utils.Log; +import java.io.File; +import java.io.IOException; import java.lang.reflect.AnnotatedType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,12 +45,25 @@ import java.util.stream.Stream; public class ValuePoolRegistry { - private final Map>> pools; private final Method fuzzTestMethod; + private final Path baseDir; + private final Map>> pools; + private final Map> pathToBytesCache = new LinkedHashMap<>(); public ValuePoolRegistry(Method fuzzTestMethod) { + this(fuzzTestMethod, computeBaseDir()); + } + + protected ValuePoolRegistry(Method fuzzTestMethod, Path baseDir) { this.fuzzTestMethod = fuzzTestMethod; this.pools = extractValueSuppliers(fuzzTestMethod); + this.baseDir = baseDir; + } + + private static Path computeBaseDir() { + return System.getProperty("jazzer.internal.basedir") == null + ? Paths.get("").toAbsolutePath().normalize() + : Paths.get(System.getProperty("jazzer.internal.basedir")); } /** @@ -62,21 +87,23 @@ public double extractFirstProbability(AnnotatedType type) { return p; } - public Optional> extractRawValues(AnnotatedType type) { - String[] poolNames = - Arrays.stream(type.getAnnotations()) - .filter(annotation -> annotation instanceof ValuePool) - .map(annotation -> (ValuePool) annotation) - .map(ValuePool::value) - .flatMap(Arrays::stream) - .toArray(String[]::new); - - if (poolNames.length == 0) { - return Optional.empty(); + public int extractFirstMaxMutations(AnnotatedType type) { + ValuePool[] valuePoolAnnotations = type.getAnnotationsByType(ValuePool.class); + if (valuePoolAnnotations.length == 0) { + // If we are here, it's a bug in the caller. + throw new IllegalStateException("Expected to find @ValuePool, but found none."); } + int maxMutations = valuePoolAnnotations[0].maxMutations(); + require(maxMutations >= 0, "@ValuePool maxMutations must be >= 0, but was " + maxMutations); + return maxMutations; + } - return Optional.of( - Arrays.stream(poolNames) + public Stream extractUserValues(AnnotatedType type) { + Stream valuesFromSourceMethods = + getValuePoolAnnotations(type).stream() + .map(ValuePool::value) + .flatMap(Arrays::stream) + .filter(name -> !name.isEmpty()) .flatMap( name -> { Supplier> supplier = pools.get(name); @@ -93,7 +120,127 @@ public Optional> extractRawValues(AnnotatedType type) { } return supplier.get(); }) - .distinct()); + .distinct(); + + // Walking patterns and reading files is expensive - we only do it when the mutator target type + // is byte[] + Stream valuesFromFiles = + type.getType() == byte[].class ? extractByteArraysFromPatterns(type) : Stream.empty(); + + return Stream.concat(valuesFromSourceMethods, valuesFromFiles).distinct(); + } + + private Stream extractByteArraysFromPatterns(AnnotatedType type) { + List annotations = getValuePoolAnnotations(type); + + return annotations.stream() + .map(ValuePool::files) + .flatMap(Arrays::stream) + .filter(glob -> !glob.isEmpty()) + .distinct() + .flatMap(glob -> collectPathsForGlob(baseDir, glob)) + .distinct() + .map(this::tryReadFile) + .filter(Optional::isPresent) + .map(Optional::get); + } + + private List getValuePoolAnnotations(AnnotatedType type) { + return Arrays.stream(type.getAnnotations()) + .filter(annotation -> annotation instanceof ValuePool) + .map(annotation -> (ValuePool) annotation) + .collect(Collectors.toList()); + } + + protected static Stream collectPathsForGlob(Path baseDir, String glob) { + int firstGlobChar = indexOfFirstGlobChar(glob); + if (firstGlobChar == -1) { + Path path = Paths.get(glob); + if (!Files.exists(path)) { + return Stream.empty(); + } + return Files.isRegularFile(path) ? Stream.of(path) : Stream.empty(); + } + + String prefix = glob.substring(0, firstGlobChar); + int lastSeparator; + if (File.separatorChar == '\\') { + int lastSlash = prefix.lastIndexOf('/'); + int lastBackSlash = prefix.lastIndexOf('\\'); + lastSeparator = Math.max(lastSlash, lastBackSlash); + } else { + lastSeparator = prefix.lastIndexOf('/'); + } + + // start path is always absolute + Path start; + if (lastSeparator == -1) { + start = baseDir.toAbsolutePath().normalize(); + } else { // absolute + if (Paths.get(prefix).isAbsolute()) { + start = Paths.get(prefix); + } else { + start = baseDir.toAbsolutePath().normalize().resolve(prefix.substring(0, lastSeparator)); + } + } + + if (!Files.exists(start)) { + return Stream.empty(); + } + + String remainingPattern = lastSeparator == -1 ? glob : glob.substring(lastSeparator + 1); + // matcher is always relative to start path + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + remainingPattern); + + List matches = new ArrayList<>(); + try { + Files.walkFileTree( + start, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (!Files.isRegularFile(file)) { + return FileVisitResult.CONTINUE; + } + Path relativePath = start.relativize(file); + if (matcher.matches(relativePath)) { + matches.add(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + return Stream.empty(); + } + return matches.stream(); + } + + private Optional tryReadFile(Path path) { + Path normalizedPath = path.toAbsolutePath().normalize(); + return pathToBytesCache.computeIfAbsent( + normalizedPath, + p -> { + try { + return Optional.of(Files.readAllBytes(path)); + } catch (IOException e) { + return Optional.empty(); + } + }); + } + + private static int indexOfFirstGlobChar(String glob) { + for (int i = 0; i < glob.length(); i++) { + char c = glob.charAt(i); + if (c == '*' || c == '?' || c == '{' || c == '[') { + return i; + } + } + return -1; } private static Map>> extractValueSuppliers(Method fuzzTestMethod) { @@ -121,7 +268,6 @@ public Stream get() { return Stream.empty(); } } - return cachedData.stream(); } }; diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java index 56b9c72f6..c28651baa 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -20,17 +20,22 @@ import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypeIfParameterized; import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.DECLARATION; -import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE; import static com.google.common.truth.Truth.assertThat; import com.code_intelligence.jazzer.mutation.annotation.ValuePool; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; -import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class ValuePoolsTest { @@ -75,35 +80,75 @@ void testExtractFirstProbability_TwoWithLastUsed() { AnnotatedType type = withExtraAnnotations( new TypeHolder<@ValuePool(value = "myPool", p = 0.2) String>() {}.annotatedType(), - withValuePoolImplementation(new String[] {"myPool2"}, 0.3)); + new ValuePoolBuilder().value("myPool2").p(0.3).build()); double p = valuePools.extractFirstProbability(type); assertThat(p).isEqualTo(0.2); } + @Test + void testExtractFirstMaxMutations_Default() { + AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(1); + } + + @Test + void testExtractFirstMaxMutations_OneUserDefined() { + AnnotatedType type = + new TypeHolder< + @ValuePool(value = "myPool2", maxMutations = 10) String>() {}.annotatedType(); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(10); + } + + @Test + void testExtractMaxMutations_TwoWithLastUsed() { + AnnotatedType type = + withExtraAnnotations( + new TypeHolder< + @ValuePool(value = "myPool", maxMutations = 2) String>() {}.annotatedType(), + new ValuePoolBuilder().value("myPool2").maxMutations(10).build()); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(2); + } + + // assert that maxMutatiosn throws when negative + @Test + void testExtractFirstMaxMutations_Negative() { + AnnotatedType type = + new TypeHolder< + @ValuePool(value = "myPool2", maxMutations = -1) String>() {}.annotatedType(); + try { + valuePools.extractFirstMaxMutations(type); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("@ValuePool maxMutations must be >= 0"); + } + } + @Test void testExtractRawValues_OneAnnotation() { AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); - Optional> elements = valuePools.extractRawValues(type); - assertThat(elements).isPresent(); - assertThat(elements.get()).containsExactly("value1", "value2", "value3"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3"); } @Test void testExtractProviderStreams_JoinStreamsInOneProvider() { AnnotatedType type = new TypeHolder<@ValuePool({"myPool", "myPool2"}) String>() {}.annotatedType(); - Optional> elements = valuePools.extractRawValues(type); - assertThat(elements).isPresent(); - assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); } @Test void testExtractRawValues_JoinTwoFromOne() { AnnotatedType type = new TypeHolder<@ValuePool({"myPool", "myPool2"}) String>() {}.annotatedType(); - Optional> elements = valuePools.extractRawValues(type); - assertThat(elements).isPresent(); - assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); } @Test @@ -111,10 +156,62 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { AnnotatedType type = withExtraAnnotations( new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), - withValuePoolImplementation(new String[] {"myPool"}, 5)); - Optional> elements = valuePools.extractRawValues(type); - assertThat(elements).isPresent(); - assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + new ValuePoolBuilder().value("myPool").build()); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void test_collectPathsForGlob_RelativePattern(@TempDir Path tempDir) throws IOException { + + testCollectPathsForGlob( + tempDir, + "**.json", + "sub/b.json", + "sub/deep/c.json", + "sub/deeper/than/foo.json", + "test/c/d/foo.json"); + } + + @Test + void test_collectPathsForGlob_SubDirectory(@TempDir Path tempDir) throws IOException { + + String relative = "sub/deep/**.txt"; + String absolute = + tempDir.toString() + FileSystems.getDefault().getSeparator() + "sub/deep/**.txt"; + + String[] expected = {"sub/deep/c.txt", "sub/deep/corpus/d.txt"}; + + testCollectPathsForGlob(tempDir, relative, expected); + testCollectPathsForGlob(tempDir, absolute, expected); + } + + @Test + void test_collectPathsForGlob_EqualAbsoluteRelative(@TempDir Path tempDir) throws IOException { + + String relative = "**.json"; + String absolute = tempDir.toString() + FileSystems.getDefault().getSeparator() + "**.json"; + + String[] expected = { + "sub/b.json", "sub/deep/c.json", "sub/deeper/than/foo.json", "test/c/d/foo.json" + }; + + testCollectPathsForGlob(tempDir, relative, expected); + testCollectPathsForGlob(tempDir, absolute, expected); + } + + @Test + void test_collectPathsForGlob_DedupAbsoluteRelative(@TempDir Path tempDir) throws IOException { + + String relative = "**.json"; + String absolute = tempDir.toString() + FileSystems.getDefault().getSeparator() + "**.json"; + + String[] expected = { + "sub/b.json", "sub/deep/c.json", "sub/deeper/than/foo.json", "test/c/d/foo.json" + }; + + testCollectPathsForGlob(tempDir, "{" + absolute + "," + relative + "}", expected); } @Test @@ -145,6 +242,175 @@ void dontPropagateNonRecursiveValuePool() { assertThat(0.9).isEqualTo(valuePools.extractFirstProbability(propagatedType)); } + @Test + void testExtractRawValues_Files_NonRecursive(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + String glob = tempDir + "/*.txt"; + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(glob).build()); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements).containsExactly("a.txt", "c.zip.txt"); + } + + @Test + void testExtractRawValues_Files_Recursive(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + String glob = tempDir + "/**/*.txt"; + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(glob).build()); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_OverlappingPatternsAreDeduped(@TempDir Path tempDir) + throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = tempDir + "/*.txt"; + String relativeRecursiveGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(recursiveGlob, directGlob, relativeRecursiveGlob).build()); + + ValuePoolRegistry valuePools; + try { + valuePools = + new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod"), tempDir); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_GlobsWithMethodSources(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = tempDir + "/*.txt"; + String relativeRecursiveGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder() + .value("myPool") + .files(recursiveGlob, directGlob, relativeRecursiveGlob) + .build()); + + ValuePoolRegistry valuePools; + try { + valuePools = + new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod"), tempDir); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + List elements = + valuePools + .extractUserValues(type) + // if byte[], convert to string + .map( + value -> { + if (value instanceof byte[]) { + return new String((byte[]) value, StandardCharsets.UTF_8); + } else { + return value; + } + }) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + // method sources + "value1", + "value2", + "value3", + // globs + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_OverlappingAbsoluteRelativePatternsAreDeduped( + @TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(recursiveGlob, directGlob).build()); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + private static void writeUtf8(Path path, String content) throws IOException { + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + } + private static ValuePool[] getValuePoolAnnotations(AnnotatedType type) { return Arrays.stream(type.getAnnotations()) .filter(annotation -> annotation instanceof ValuePool) @@ -155,64 +421,179 @@ private static Stream extractValuesFromValuePools(AnnotatedType type) { return Arrays.stream(getValuePoolAnnotations(type)).flatMap(v -> Arrays.stream(v.value())); } - public static ValuePool withValuePoolImplementation(String[] value, double p) { - return withValuePoolImplementation(value, p, RECURSIVE); - } - - public static ValuePool withValuePoolImplementation(String[] value, double p, String constraint) { - return new ValuePool() { - @Override - public String[] value() { - return value; + private static class ValuePoolBuilder { + private String[] value; + private String[] files; + private double p; + private int maxMutations; + private String constraint; + + public ValuePoolBuilder() { + try { + value = (String[]) getDefault("value"); + files = (String[]) getDefault("files"); + p = (double) getDefault("p"); + maxMutations = (int) getDefault("maxMutations"); + constraint = (String) getDefault("constraint"); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Could not load ValuePool defaults", e); } + } - @Override - public double p() { - return p; - } + private Object getDefault(String methodName) throws NoSuchMethodException { + return ValuePool.class.getDeclaredMethod(methodName).getDefaultValue(); + } - @Override - public String constraint() { - return constraint; - } + public ValuePoolBuilder value(String... values) { + this.value = values; + return this; + } - @Override - public Class annotationType() { - return ValuePool.class; - } + public ValuePoolBuilder p(double p) { + this.p = p; + return this; + } - @Override - public boolean equals(Object o) { - if (!(o instanceof ValuePool)) { - return false; + public ValuePoolBuilder maxMutations(int maxMutations) { + this.maxMutations = maxMutations; + return this; + } + + public ValuePoolBuilder constraint(String constraint) { + this.constraint = constraint; + return this; + } + + public ValuePoolBuilder files(String... files) { + this.files = files; + return this; + } + + public ValuePool build() { + final String[] value = this.value; + final String[] files = this.files; + final double p = this.p; + final int maxMutations = this.maxMutations; + final String constraint = this.constraint; + + return new ValuePool() { + @Override + public Class annotationType() { + return ValuePool.class; } - ValuePool other = (ValuePool) o; - return Arrays.equals(this.value(), other.value()) - && this.p() == other.p() - && this.constraint().equals(other.constraint()); - } - @Override - public int hashCode() { - int hash = 0; - hash += Arrays.hashCode(value()) * 127; - hash += Double.hashCode(p()) * 31 * 127; - hash += constraint().hashCode() * 127; - return hash; - } + @Override + public String[] value() { + return value; + } - @Override - public String toString() { - return "@" - + ValuePool.class.getName() - + "(value={" - + String.join(", ", value()) - + "}, p=" - + p() - + ", constraint=" - + constraint() - + ")"; - } - }; + @Override + public String[] files() { + return files; + } + + @Override + public double p() { + return p; + } + + @Override + public int maxMutations() { + return maxMutations; + } + + @Override + public String constraint() { + return constraint; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ValuePool)) { + return false; + } + ValuePool other = (ValuePool) o; + return Arrays.equals(this.value(), other.value()) + && Arrays.equals(this.files(), other.files()) + && this.p() == other.p() + && this.constraint().equals(other.constraint()); + } + + @Override + public int hashCode() { + int hash = 0; + hash += Arrays.hashCode(value()) * 127; + hash += Arrays.hashCode(files()) * 31 * 127; + hash += Double.hashCode(p()) * 31 * 31 * 127; + hash += Integer.hashCode(maxMutations()) * 31 * 31 * 31 * 127; + hash += constraint().hashCode() * 31 * 31 * 31 * 31 * 127; + return hash; + } + + @Override + public String toString() { + return "@" + + ValuePool.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, files={" + + String.join(", ", files()) + + "}, p=" + + p() + + ", maxMutations=" + + maxMutations() + + ", constraint=" + + constraint() + + ")"; + } + }; + } + } + + void testCollectPathsForGlob(Path tempDir, String glob, String... expected) throws IOException { + mockSourceDirectory(tempDir); + List matchedPaths = + ValuePoolRegistry.collectPathsForGlob(tempDir, glob) + .map(Path::toAbsolutePath) + .collect(Collectors.toList()); + + List expectedPaths = + Arrays.stream(expected) + .map(tempDir::resolve) + .map(Path::toAbsolutePath) + .collect(Collectors.toList()); + + assertThat(matchedPaths).containsExactlyElementsIn(expectedPaths); + } + + private void makeFiles(Path base, String... paths) throws IOException { + for (String path : paths) { + Path file = base.resolve(path); + Files.createDirectories(file.getParent()); + writeUtf8(file, path); + } + } + + private void mockSourceDirectory(Path base) throws IOException { + makeFiles( + base, + // top level + "a.txt", + "b.zip", + "c.zip.txt", + "sub/b.txt", + "sub/b.json", + "sub/b.xml", + "sub/c.zip", + "sub/deep/c.txt", + "sub/deep/c.json", + "sub/deep/c.xml", + "sub/deep/corpus/d.xml", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "sub/deeper/than/foo.json", + "sub/deeper/than/bar.xml", + "test/c/d/foo.json", + "test/c/d/bar.txt"); } }