From de566923b09c5f3d3e88f3cbc354b5f385692c47 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 20:01:17 +0100 Subject: [PATCH 1/6] chore: increase test timeout for the selffuzz test --- .../java/com/code_intelligence/selffuzz/mutation/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) 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", From bbe5f0128c1ee4907ccedb639eca7128d78e45fb Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 16:26:17 +0100 Subject: [PATCH 2/6] chore: build ValuePool using a builder in tests The number of fields in ValuePool is getting large, so that construction using positional args is becoming confusing. Using a builder simplifies the tests. --- .../mutation/support/ValuePoolsTest.java | 137 +++++++++++------- 1 file changed, 84 insertions(+), 53 deletions(-) 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..9aea5c498 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,7 +20,6 @@ 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; @@ -28,6 +27,7 @@ import java.lang.reflect.AnnotatedType; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -75,7 +75,7 @@ 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); } @@ -111,7 +111,7 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { AnnotatedType type = withExtraAnnotations( new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), - withValuePoolImplementation(new String[] {"myPool"}, 5)); + new ValuePoolBuilder().value("myPool").build()); Optional> elements = valuePools.extractRawValues(type); assertThat(elements).isPresent(); assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); @@ -155,64 +155,95 @@ 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); - } + private static class ValuePoolBuilder { + private String[] value; + private double p; + private String constraint; - public static ValuePool withValuePoolImplementation(String[] value, double p, String constraint) { - return new ValuePool() { - @Override - public String[] value() { - return value; + public ValuePoolBuilder() { + try { + value = (String[]) getDefault("value"); + p = (double) getDefault("p"); + 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; + } + + public ValuePoolBuilder constraint(String constraint) { + this.constraint = constraint; + return this; + } - @Override - public boolean equals(Object o) { - if (!(o instanceof ValuePool)) { - return false; + public ValuePool build() { + final String[] value = this.value; + final double p = this.p; + 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 double p() { + return p; + } + + @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()) + && this.p() == other.p() + && this.constraint().equals(other.constraint()); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(value()), p(), constraint()); + } + + @Override + public String toString() { + return "@" + + ValuePool.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, p=" + + p() + + ", constraint=" + + constraint() + + ")"; + } + }; + } } } From 3da5408c85163a54fcca05dda769cf607a8e0e76 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 19 Jan 2026 16:58:14 +0100 Subject: [PATCH 3/6] feat: cache mutators and value pools for each method Don't generate the same mutator for the same fuzz test method multiple times. Before this change, a mutator was generated for each crash file in fuzzing mode. --- .../jazzer/mutation/ArgumentsMutator.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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( From c763be80cb1345a3fa26a8c1cb43c41fa1725071 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 19 Dec 2025 17:00:29 +0100 Subject: [PATCH 4/6] feat: ValuePool can load files that match directory patterns --- .../jazzer/junit/FuzzTestExecutor.java | 2 + .../jazzer/mutation/annotation/ValuePool.java | 32 +- .../mutator/lang/ValuePoolMutatorFactory.java | 10 +- .../jazzer/mutation/support/GlobUtils.java | 202 ++++++++++++ .../mutation/support/ValuePoolRegistry.java | 114 +++++-- .../mutation/support/GlobUtilsTest.java | 292 ++++++++++++++++++ .../jazzer/mutation/support/TestSupport.java | 83 +++++ .../mutation/support/ValuePoolsTest.java | 240 +++++++++++++- 8 files changed, 925 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/support/GlobUtils.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/support/GlobUtilsTest.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/annotation/ValuePool.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java index ce636c515..a8d6479ad 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 @@ -68,11 +68,35 @@ public @interface ValuePool { /** * Specifies supplier methods that generate values for fuzzing the annotated method or type. The - * specified supplier methods must be static and return a {@code Stream } of values. The values + * specified supplier methods must be static and return a {@code Stream} of values. The values * 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. + * + *

Patterns that start with { or [@code are treated as relative to + * 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 + *
  • {"*.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 @@ -82,7 +106,7 @@ /** * 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..ddf618639 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. */ @@ -91,14 +90,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 diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/GlobUtils.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/GlobUtils.java new file mode 100644 index 000000000..fa7cb2c01 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/GlobUtils.java @@ -0,0 +1,202 @@ +/* + * Copyright 2026 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.support; + +import static java.util.Collections.EMPTY_LIST; + +import java.io.IOException; +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.List; + +public class GlobUtils { + protected static List collectPathsForGlob(Path baseDir, String glob) { + if (ON_WINDOWS) { + glob = glob.replace("\\\\", "/"); + } + int firstGlobChar = indexOfFirstGlobChar(glob); + if (firstGlobChar == -1) { + Path target = baseDir.resolve(unescapeGlobChars(glob)).toAbsolutePath().normalize(); + return Files.isRegularFile(target) ? Arrays.asList(target) : EMPTY_LIST; + } + + String prefix = glob.substring(0, firstGlobChar); + int lastSeparator = + ON_WINDOWS + ? Math.max(prefix.lastIndexOf('/'), prefix.lastIndexOf("\\\\")) + : prefix.lastIndexOf('/'); + + // The 'start' path is always absolute + Path start; + String remainingPattern; + if (lastSeparator == -1) { + start = baseDir.toAbsolutePath().normalize(); + remainingPattern = glob; + } else { + String basePrefix = prefix.substring(0, lastSeparator); + start = baseDir.resolve(unescapeGlobChars(basePrefix)).toAbsolutePath().normalize(); + remainingPattern = glob.substring(lastSeparator + 1); + } + if (!Files.exists(start)) { + return EMPTY_LIST; + } + + // The 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 ignored) { + // Best effort - return what we found so far + } + return matches; + } + + private static final String SPECIAL_CHARS = "*{}[]-?\\"; + + private static boolean isSpecialChar(char c) { + return SPECIAL_CHARS.indexOf(c) != -1; + } + + protected static Path unescapeGlobChars(Path glob) { + StringBuilder sb = new StringBuilder(); + char[] chars = glob.toString().toCharArray(); + boolean escaped = false; + + for (char c : chars) { + if (escaped) { + if (!isSpecialChar(c)) { + sb.append('\\'); + } + sb.append(c); + escaped = false; + } else if (c == '\\') { + escaped = true; + } else { + sb.append(c); + } + } + + return Paths.get(sb.toString()); + } + + protected static Path unescapeGlobChars(String glob) { + StringBuilder sb = new StringBuilder(); + char[] chars = glob.toCharArray(); + boolean escaped = false; + + for (char c : chars) { + if (escaped) { + if (!isSpecialChar(c)) { + sb.append('\\'); + } + sb.append(c); + escaped = false; + } else if (c == '\\') { + escaped = true; + } else { + sb.append(c); + } + } + + return Paths.get(sb.toString()); + } + + private static final String GLOB_CHARS = "*?[{"; + + private static boolean isGlobChar(char c) { + return GLOB_CHARS.indexOf(c) != -1; + } + + protected static final boolean ON_WINDOWS = FileSystems.getDefault().getSeparator().equals("\\"); + + private static int indexOfFirstGlobChar(String glob) { + char[] chars = glob.toCharArray(); + boolean escaped = false; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (escaped) { + escaped = false; + continue; + } else if (c == '\\') { + escaped = true; + } + if (isGlobChar(c)) { + return i; + } + } + return -1; + } + + /** + * Finds if the glob contains unescaped glob star + * + * @param glob + * @return true if the glob contains unescaped "**" + */ + protected static boolean isRecursive(String glob) { + char[] chars = glob.toCharArray(); + boolean escaped = false; + boolean starFound = false; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (escaped) { + escaped = false; + continue; + } else if (c == '\\') { + escaped = true; + } + if (c == '*') { + if (starFound) { + return true; + } + starFound = true; + } else { + starFound = false; + } + } + return false; + } +} 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..aaeccbba2 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 @@ -19,12 +19,16 @@ import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; import com.code_intelligence.jazzer.mutation.annotation.ValuePool; -import com.code_intelligence.jazzer.utils.Log; +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.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,12 +37,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,29 +79,20 @@ 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) + public Stream extractUserValues(AnnotatedType type) { + Stream valuesFromSourceMethods = + getValuePoolAnnotations(type).stream() .map(ValuePool::value) .flatMap(Arrays::stream) - .toArray(String[]::new); - - if (poolNames.length == 0) { - return Optional.empty(); - } - - return Optional.of( - Arrays.stream(poolNames) + .filter(name -> !name.isEmpty()) .flatMap( name -> { Supplier> supplier = pools.get(name); if (supplier == null) { throw new IllegalStateException( - "No method named '" + "@ValuePool: No method named '" + name - + "' found for @ValuePool on type " + + "' found for type " + type.getType().getTypeName() + " in fuzz test method " + fuzzTestMethod.getName() @@ -93,7 +101,63 @@ public Optional> extractRawValues(AnnotatedType type) { } return supplier.get(); }) - .distinct()); + .distinct(); + + // Walking the file system only makes sense for ValuePool's that annotate byte[] types. + if (type.getType() == byte[].class) { + return Stream.concat(valuesFromSourceMethods, extractByteArraysFromPatterns(type)); + } else { + return valuesFromSourceMethods; + } + } + + 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 -> { + List paths = GlobUtils.collectPathsForGlob(baseDir, glob); + if (paths.isEmpty()) { + throw new IllegalArgumentException( + "@ValuePool: No files matched glob pattern '" + + glob + + "' for type " + + type.getType().getTypeName() + + " in fuzz test method " + + fuzzTestMethod.getName() + + "."); + } + return paths.stream(); + }) + .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()); + } + + private Optional tryReadFile(Path path) { + Path normalizedPath = path.toAbsolutePath().normalize(); + return pathToBytesCache.computeIfAbsent( + normalizedPath, + p -> { + try { + return Optional.of(Files.readAllBytes(p)); + } catch (IOException e) { + return Optional.empty(); + } + }); } private static Map>> extractValueSuppliers(Method fuzzTestMethod) { @@ -117,11 +181,12 @@ public Stream get() { } } if (cachedData.isEmpty()) { - Log.warn("@ValuePool method " + method.getName() + " provided no values."); - return Stream.empty(); + throw new IllegalStateException( + "@ValuePool: method '" + + method.getName() + + "' returned no values. Value pool methods must return at least one value."); } } - return cachedData.stream(); } }; @@ -133,9 +198,12 @@ private static List loadDataFromMethod(Method method) { Stream stream = (Stream) method.invoke(null); return stream.collect(Collectors.toList()); } catch (IllegalAccessException e) { - throw new IllegalStateException("Cannot access method " + method.getName(), e); + throw new RuntimeException("@ValuePool: Access denied for method " + method.getName(), e); } catch (InvocationTargetException e) { - throw new RuntimeException("Error invoking method " + method.getName(), e.getCause()); + Throwable cause = e.getCause(); + throw new RuntimeException( + "@ValuePool: Method " + method.getName() + " threw an exception", + cause != null ? cause : e); } } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/GlobUtilsTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/GlobUtilsTest.java new file mode 100644 index 000000000..9bf497abc --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/GlobUtilsTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2026 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.support; + +import static com.code_intelligence.jazzer.mutation.support.GlobUtils.isRecursive; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.mockSourceDirectory; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class GlobUtilsTest { + + static Path tempDir; + + @BeforeAll + static void prepareTestFile(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + GlobUtilsTest.tempDir = tempDir; + } + + @Test + void collectPathsForGlob_OneFile_AbsRel() { + testCollectPathsForGlob_AbsRel(tempDir, "sub/deep/c.json", "sub/deep/c.json"); + } + + @Test + void collectPathsForGlob_AbsoluteRelativePattern() { + testCollectPathsForGlob_AbsRel( + tempDir, + "**.json", + "sub/b.json", + "sub/deep/c.json", + "sub/deeper/than/foo.json", + "test/c/d/foo.json"); + } + + @Test + void collectPathsForGlob_SubDirectory() { + testCollectPathsForGlob_AbsRel( + tempDir, "sub/deep/**.txt", "sub/deep/c.txt", "sub/deep/corpus/d.txt"); + } + + @Test + void collectPathsForGlob_AnySubdir() { + testCollectPathsForGlob_AbsRel( + tempDir, + "sub/*/**.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt"); + } + + @Test + void collectPathsForGlob_StartDirDoesNotExist() { + testCollectPathsForGlob_AbsRel( + tempDir, "nonexistent/**/*.json" + // expects no matches + ); + } + + @Test + void collectPathsForGlob_AbsoluteRelative() { + testCollectPathsForGlob_AbsRel(tempDir, "sub/deeper/than/fo*.json", "sub/deeper/than/foo.json"); + } + + @Test + void collectPathsForGlob_PatternCharacters() { + testCollectPathsForGlob_AbsRel( + tempDir, "weird/*.glob", "weird/{}{braces}.glob", "weird/[]{}.glob"); + } + + static Stream patternCharactersEscape() { + return new TestCaseBuilder() + .withExpected("weird/[]{}.glob") + .patterns("weird/\\[*.glob", "weird/\\[\\]*.glob", "weird/\\[\\]\\{\\}.glob") + .build(); + } + + static Stream directoryHasEscapedChar() { + return new TestCaseBuilder() + .withExpected("escaped/[/test.escaped") + .patterns( + "escaped/**.escaped", + "escaped/\\[/*.escaped", + "escaped/\\[/**.escaped", + "e*/\\[/*.escaped", + "e**/\\[/*.escaped", + "*/\\[/*.escaped") + .build(); + } + + @Test + void collectPathsForGlob_StarDetectionResets() { + testCollectPathsForGlob_AbsRel(tempDir, "extra/*a*"); + } + + static Stream usefulPatterns() { + return new TestCaseBuilder() + .withExpected("a.txt", "b.zip") + .patterns("?.???", "[ab].{txt,zip}", "{a.txt,b.zip}") + .withExpected("sub/deep/c.json", "sub/deeper/than/foo.json") + .patterns("sub/{deep,deeper/than}/*.json", "sub/**/{c,foo}.json") + .withExpected( + "sub/deep/c.txt", + "sub/deep/c.json", + "sub/deep/c.xml", + "sub/deep/corpus/d.xml", + "sub/deep/corpus/d.txt") + .patterns( + "sub/deep/**", + "???/????/**.{json,txt,xml}", + "sub/deep/**.{json,txt,xml}", + "sub/dee?/**.{json,txt,xml}", + "sub/?ee?/**.{json,txt,xml}") + .withExpected("alpha-numeric/1a.numeric", "alpha-numeric/5h.numeric") + .patterns("*/[0-5][a-h].numeric", "**/[0-5][a-h].numeric", "**/[!69][!IZ].numeric") + .withExpected("alpha-numeric/6I.numeric", "alpha-numeric/9Z.numeric") + .patterns("*/[6-9][I-Z].numeric", "**/[6-9][I-Z].numeric", "**/[6-9][I-Z].*") + .build(); + } + + @ParameterizedTest + @MethodSource({"directoryHasEscapedChar", "patternCharactersEscape", "usefulPatterns"}) + void test_provideCollectPathsForGlob_AllOperatingSystems(String glob, String[] expected) { + testCollectPathsForGlob_AbsRel(tempDir, glob, expected); + } + + static Stream EscapedPatternCharacters_Relative_NoWindows() { + return new TestCaseBuilder() + .withExpected("no-windows/asdf[sdf*df]f.glob") + .patterns( + "no-windows/asdf\\[*.glob", "no-windows/asdf\\[sdf\\**.glob", "no-windows/*f.glob") + .withExpected("no-windows/hey?ho.glob") + .patterns( + "no-windows/he*.glob", + "no-windows/hey\\?*.glob", + "no-windows/hey\\?h*.glob", + "no-windows/hey\\?h**", + "no-windows/hey\\?ho.glob") + .withExpected("no-windows/stars****stars.glob") + .patterns( + "no-windows/stars*.glob", + "no-windows/stars\\**.glob", + "no-windows/stars\\***.glob", + "no-windows/stars\\*\\*\\*\\**.glob", + "no-windows/stars\\*\\*\\*\\*?????.glob") + .withExpected("no-windows/*?hello[there]{}.glob") + .patterns( + "no-windows/*there*.glob", + "no-windows/\\*\\?*.glob", + "no-windows/\\*\\?**.glob", + "no-windows/\\*\\?hello*.glob", + "no-windows/\\*\\?hello\\[*.glob", + "no-windows/\\*\\?hello\\[**.glob", + "no-windows/??hello\\[**.glob", + "no-windows/[\\*]?hello\\[**.glob", + "no-windows/[*]?hello\\[**.glob", + "no-windows/[*][?]hello\\[**.glob", + "no-windows/\\*\\?hello\\[there\\]\\{*.glob", + "no-windows/\\*\\?hello\\[there\\]\\{\\}.glob") + .withExpected("no-windows/backslash\\\\-es.glob") + .patterns( + "no-windows/back*.glob", + "no-windows/backslash\\\\*-es.glob", + "no-windows/backslash\\\\\\\\*-es.glob", + "no-windows/backslash\\\\**.glob", + "no-windows/backslash??-es.glob", + "no-windows/backslash\\\\\\\\-es.glob") + .build(); + } + + @DisabledOnOs(OS.WINDOWS) + @ParameterizedTest + @MethodSource("EscapedPatternCharacters_Relative_NoWindows") + void test_provideCollectPathsForGlob_NoWindows(String glob, String[] expected) { + testCollectPathsForGlob_AbsRel(tempDir, glob, expected); + } + + static Stream windowsMixingDirSeparators() { + return new TestCaseBuilder() + .withExpected("sub/deep/corpus/d.xml") + .patterns( + "sub\\\\deep/corpus\\\\d.xml", + "sub/deep\\\\corpus\\\\d.xml", + "sub\\\\deep{/,t}corpus/d.xml") + .build(); + } + + @EnabledOnOs(OS.WINDOWS) + @ParameterizedTest + @MethodSource("windowsMixingDirSeparators") + void test_provideCollectPathsForGlob_Windows(String glob, String[] expected) { + testCollectPathsForGlob_AbsRel(tempDir, glob, expected); + } + + @Test + void test_isRecursive() { + assertThat(isRecursive("*a*")).isFalse(); + assertThat(isRecursive("\\**")).isFalse(); + assertThat(isRecursive("*\\**")).isFalse(); + assertThat(isRecursive("\\**\\*")).isFalse(); + assertThat(isRecursive("*\\[*")).isFalse(); + assertThat(isRecursive("*\\?*")).isFalse(); + assertThat(isRecursive("*.*")).isFalse(); + assertThat(isRecursive("**")).isTrue(); + assertThat(isRecursive("\\***")).isTrue(); + assertThat(isRecursive("*\\***")).isTrue(); + assertThat(isRecursive("////////**")).isTrue(); + } + + /** + * Helper to test both relative and absolute glob patterns. This expects only relative patterns. + * The absolute patterns are automatically constructed based on the provided tempDir and the + * relative glob pattern. + * + * @param tempDir - base directory for glob matching + * @param glob - relative-only glob pattern + * @param expected - expected paths relative to tempDir + */ + private static void testCollectPathsForGlob_AbsRel( + Path tempDir, String glob, String... expected) { + assertAll( + () -> testGlob(tempDir, glob, expected), + () -> testGlob(tempDir, tempDir + "/" + glob, expected)); + } + + private static void testGlob(Path tempDir, String glob, String... expected) { + List matchedPaths = + GlobUtils.collectPathsForGlob(tempDir, glob).stream() + .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 static class TestCaseBuilder { + private final List arguments = new ArrayList<>(); + private String[] currentExpected; + + TestCaseBuilder withExpected(String... expected) { + this.currentExpected = expected; + return this; + } + + TestCaseBuilder patterns(String... pattern) { + for (String p : pattern) { + arguments.add(Arguments.of(p, currentExpected)); + } + return this; + } + + Stream build() { + return arguments.stream(); + } + } +} 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 be16da5d6..0b6bd4157 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 @@ -33,11 +33,16 @@ import com.google.errorprone.annotations.MustBeClosed; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -519,4 +524,82 @@ public static Stream prependArgs(Stream base, Object... ar }); } } + + private static final boolean ON_WINDOWS = FileSystems.getDefault().getSeparator().equals("\\"); + + public static void mockSourceDirectory(Path base) throws IOException { + // if base exists and has files, assume already populated + if (Files.exists(base) && Files.list(base).findAny().isPresent()) { + return; + } + makeFiles( + base, + // top level + "a.txt", + "b.zip", + "c.zip.txt", + // subdirectories + "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", + "alpha-numeric/1a.numeric", + "alpha-numeric/5h.numeric", + "alpha-numeric/6I.numeric", + "alpha-numeric/9Z.numeric", + "escaped/[/test.escaped", + // files with special glob characters + // * and ? are not allowed on Windows + "weird/{}{braces}.glob", + "weird/[]{}.glob"); + + if (!ON_WINDOWS) { + // * and ? are not allowed in filenames on Windows + makeFiles( + base, + "no-windows/asdf[sdf*df]f.glob", + "no-windows/hey?ho.glob", + "no-windows/stars****stars.glob", + "no-windows/*?hello[there]{}.glob", + "no-windows/backslash\\\\-es.glob", + // \\ is a directory separator on Windows + "no-windows/file\\ with backslash.glob"); + + // On Windows, we cannot make files or directories unreadable in a way that the JVM respects. + // Make an unreadable directory to ensure it's skipped without error + Path unreadableDir = base.resolve("unreadable"); + Files.createDirectory(unreadableDir); + unreadableDir.toFile().setReadable(false); + unreadableDir.toFile().setExecutable(false); + + // Make an unreadable file to ensure it's skipped without error + Path unreadableFile = base.resolve("unreadable_file.txt"); + writeUtf8(unreadableFile, "unreadable_file.txt"); + unreadableFile.toFile().setReadable(false); + unreadableFile.toFile().setWritable(false); + unreadableFile.toFile().setExecutable(false); + } + } + + private static 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 static void writeUtf8(Path path, String content) throws IOException { + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + } } 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 9aea5c498..c2da94afd 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 @@ -17,20 +17,26 @@ package com.code_intelligence.jazzer.mutation.support; import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; +import static com.code_intelligence.jazzer.mutation.support.TestSupport.mockSourceDirectory; 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.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.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Objects; -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 { @@ -39,12 +45,15 @@ public void dummyFuzzTestMethod() {} private static final ValuePoolRegistry valuePools; + private static Method fuzzTestMethod; + static { try { - valuePools = new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod")); + fuzzTestMethod = ValuePoolsTest.class.getMethod("dummyFuzzTestMethod"); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } + valuePools = new ValuePoolRegistry(fuzzTestMethod); } public static Stream myPool() { @@ -83,27 +92,27 @@ void testExtractFirstProbability_TwoWithLastUsed() { @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 @@ -112,9 +121,9 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { withExtraAnnotations( new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), new ValuePoolBuilder().value("myPool").build()); - 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 @@ -145,6 +154,191 @@ 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_RelativePatternInsideBrackets(@TempDir Path tempDir) + throws IOException { + mockSourceDirectory(tempDir); + String glob = "{" + "**/*.txt}"; + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(glob).build()); + + // Need to create ValuePoolRegistry with tempDir as working directory + ValuePoolRegistry valuePools = new ValuePoolRegistry(fuzzTestMethod, tempDir); + + 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()); + + // Need to create ValuePoolRegistry with tempDir as "working directory" + 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_PatternsWithGlobSymbols(@TempDir Path tempDir) + throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/weird/\\[\\]\\{\\}.glob"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + new ValuePoolBuilder().files(recursiveGlob).build()); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements).containsExactly("weird/[]{}.glob"); + } + private static ValuePool[] getValuePoolAnnotations(AnnotatedType type) { return Arrays.stream(type.getAnnotations()) .filter(annotation -> annotation instanceof ValuePool) @@ -157,12 +351,14 @@ private static Stream extractValuesFromValuePools(AnnotatedType type) { private static class ValuePoolBuilder { private String[] value; + private String[] files; private double p; private String constraint; public ValuePoolBuilder() { try { value = (String[]) getDefault("value"); + files = (String[]) getDefault("files"); p = (double) getDefault("p"); constraint = (String) getDefault("constraint"); } catch (NoSuchMethodException e) { @@ -179,6 +375,11 @@ public ValuePoolBuilder value(String... values) { return this; } + public ValuePoolBuilder files(String... files) { + this.files = files; + return this; + } + public ValuePoolBuilder p(double p) { this.p = p; return this; @@ -205,6 +406,11 @@ public String[] value() { return value; } + @Override + public String[] files() { + return files; + } + @Override public double p() { return p; @@ -222,13 +428,15 @@ public boolean equals(Object o) { } 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() { - return Objects.hash(Arrays.hashCode(value()), p(), constraint()); + return Objects.hash( + Arrays.hashCode(value()), Arrays.hashCode(files()), p(), constraint()); } @Override @@ -237,6 +445,8 @@ public String toString() { + ValuePool.class.getName() + "(value={" + String.join(", ", value()) + + "}, files=" + + String.join(", ", files()) + "}, p=" + p() + ", constraint=" From 469d9044c2098b2fad91fbf5f5884ce3654b40f0 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 13:26:11 +0100 Subject: [PATCH 5/6] feat: add configurable max number of mutations for ValuePools --- .../jazzer/mutation/annotation/ValuePool.java | 6 ++ .../mutator/lang/ValuePoolMutatorFactory.java | 37 ++++++++--- .../mutation/support/ValuePoolRegistry.java | 11 ++++ .../mutation/support/ValuePoolsTest.java | 62 ++++++++++++++++++- 4 files changed, 105 insertions(+), 11 deletions(-) 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 a8d6479ad..bd7fb8822 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 @@ -104,6 +104,12 @@ */ 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 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 ddf618639..1f3990339 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 @@ -74,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") @@ -106,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); } /** @@ -138,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 @@ -174,19 +180,30 @@ 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 + int mutations = prng.closedRange(0, maxMutations); + for (int i = 0; i < mutations; 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 + int mutations = prng.closedRange(0, maxMutations); + for (int i = 0; i < mutations; 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 aaeccbba2..69e8538d4 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 @@ -79,6 +79,17 @@ public double extractFirstProbability(AnnotatedType type) { return p; } + 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; + } + public Stream extractUserValues(AnnotatedType type) { Stream valuesFromSourceMethods = getValuePoolAnnotations(type).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 c2da94afd..482ba0abd 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 @@ -22,6 +22,7 @@ import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.DECLARATION; import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.code_intelligence.jazzer.mutation.annotation.ValuePool; import java.io.IOException; @@ -89,6 +90,45 @@ void testExtractFirstProbability_TwoWithLastUsed() { 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); + } + + @Test + void testExtractFirstMaxMutations_Negative() { + AnnotatedType type = + new TypeHolder< + @ValuePool(value = "myPool2", maxMutations = -1) String>() {}.annotatedType(); + assertThat( + assertThrows( + IllegalArgumentException.class, () -> valuePools.extractFirstMaxMutations(type))) + .hasMessageThat() + .contains("@ValuePool maxMutations must be >= 0"); + } + @Test void testExtractRawValues_OneAnnotation() { AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); @@ -353,6 +393,7 @@ private static class ValuePoolBuilder { private String[] value; private String[] files; private double p; + private int maxMutations; private String constraint; public ValuePoolBuilder() { @@ -360,6 +401,7 @@ public ValuePoolBuilder() { 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); @@ -385,6 +427,11 @@ public ValuePoolBuilder p(double p) { return this; } + public ValuePoolBuilder maxMutations(int maxMutations) { + this.maxMutations = maxMutations; + return this; + } + public ValuePoolBuilder constraint(String constraint) { this.constraint = constraint; return this; @@ -393,6 +440,7 @@ public ValuePoolBuilder constraint(String constraint) { public ValuePool build() { final String[] value = this.value; final double p = this.p; + final int maxMutations = this.maxMutations; final String constraint = this.constraint; return new ValuePool() { @@ -416,6 +464,11 @@ public double p() { return p; } + @Override + public int maxMutations() { + return maxMutations; + } + @Override public String constraint() { return constraint; @@ -430,13 +483,18 @@ public boolean equals(Object o) { return Arrays.equals(this.value(), other.value()) && Arrays.equals(this.files(), other.files()) && this.p() == other.p() + && this.maxMutations() == other.maxMutations() && this.constraint().equals(other.constraint()); } @Override public int hashCode() { return Objects.hash( - Arrays.hashCode(value()), Arrays.hashCode(files()), p(), constraint()); + Arrays.hashCode(value()), + Arrays.hashCode(files()), + p(), + maxMutations(), + constraint()); } @Override @@ -449,6 +507,8 @@ public String toString() { + String.join(", ", files()) + "}, p=" + p() + + ", maxMutations=" + + maxMutations() + ", constraint=" + constraint() + ")"; From d7004dabb75d659e3b27907fddb702581f41c587 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 13:03:31 +0100 Subject: [PATCH 6/6] feat: document @ValuePool --- docs/mutation-framework.md | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/docs/mutation-framework.md b/docs/mutation-framework.md index 59c6e4c58..19c3ab50e 100644 --- a/docs/mutation-framework.md +++ b/docs/mutation-framework.md @@ -317,6 +317,170 @@ class SimpleClassFuzzTests { } ``` +## @ValuePool: Guide fuzzing with custom values + +The `@ValuePool` annotation lets you provide concrete example values of any [supported type](#supported-types) (except for cache-based mutators) 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 parameter (sub-)types** - values apply only to the annotated types +- **On the test method itself** - values propagate to all matching types across all parameters + +**Example:** +```java +@FuzzTest +void fuzzTest(Map<@ValuePool(value = {"mySupplier"}) String, Integer> foo) { + // Strings from mySupplier feed the Map's String mutator +} + +@FuzzTest +void anotherFuzzTest(@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 yetAnotherFuzzTest(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 other mutation strategies. +```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 = {"valuesSupplier"}) Map data +// The supplier feed both Map keys AND values + +// With DECLARATION: +@ValuePool(value = {"valuesSupplier"}, constraint = DECLARATION) Map data +// The supplier only feeds the Map, NOT keys or values---it should contain Map instances to have effect +``` + +--- + +### Complete Example +```java +class MyFuzzTest { + Map map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + static Stream edgeCases() { + return Stream.of( + "", "null", "alert('xss')", // Strings + 0, -1, Integer.MAX_VALUE, // Integers + new byte[]{0x00, 0x7F}, // A byte array + map // A Map + ); + } + + @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, Map itself gets the `map` object + // - '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` (at most one additional mutation applied) +**Range:** 0 or higher + +**How it works:** If `maxMutations = 5`, and Jazzer selects the value pool as mutation strategy, Jazzer will: +1. Select a random value from your pool (e.g., `"alert('xss')"`) +2. Apply up to 5 random mutations in a row (e.g., `"alert('xss')"` → `"alert(x"` → `"AAAt(x"` → ...) + +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