diff --git a/docs/mutation-framework.md b/docs/mutation-framework.md index 05c312156..7fc323533 100644 --- a/docs/mutation-framework.md +++ b/docs/mutation-framework.md @@ -342,13 +342,14 @@ You can apply `@ValuePool` in two places: ```java @FuzzTest void fuzzTest(Map<@ValuePool(value = {"mySupplier"}) String, Integer> foo) { - // Strings from mySupplier feed the Map's String mutator + // 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 + // Map mutator would use supplier values if it contained any Map objects } @FuzzTest @@ -358,6 +359,7 @@ void yetAnotherFuzzTest(Map foo, String bar) { // - String mutator for Map keys in 'foo' // - String mutator for 'bar' // - Integer mutator for Map values in 'foo' + // - Map mutator would use supplier values if it contained any Map objects } static Stream mySupplier() { @@ -372,7 +374,7 @@ Jazzer automatically routes values to mutators based on type: - 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). +**Type propagation happens recursively by default**, so a `@ValuePool` on a `Map` will feed three mutators: the String mutator (for keys), the Integer mutator (for values), and the `Map` mutator. --- @@ -382,7 +384,12 @@ Jazzer automatically routes values to mutators based on type: Provide the names of static methods that return `Stream`: ```java -@ValuePool(value = {"mySupplier", "anotherSupplier"}) +// The supplier methods mySupplier and anotherSupplier should be in the class of the fuzz test method +// Supplier methods from other classes can be used by giving fully qualified names: +// com.example.MyClass#mySupplierMethod and com.example.OuterClass$InnerClass#mySupplierMethod +@ValuePool(value = {"mySupplier", "anotherSupplier", + "com.example.MyClass#mySupplierMethod", + "com.example.OuterClass$InnerClass#mySupplierMethod"}) ``` **Requirements:** @@ -414,82 +421,84 @@ Load files as `byte[]` arrays using glob patterns: #### 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 +@ValuePool(value = {"mySupplier"}, p = 0.3) T // Use pool values 30% of the time ``` -**Default:** `p = 0.1` (10% of mutations use pool values) -**Range:** 0.0 to 1.0 +**Default:** `p = 0.1` - 10% of mutations use pool values + +**Range:** `[0.0; 1.0]` + + +#### Max Mutations (`maxMutations` field) +After selecting a value from the pool, the underlying type mutator can additionaly apply a randomly chosen number of mutations in the inclusive interval of [0; `maxMutations`] to the value. +Setting `maxMutations = 0` means that no additional mutations are applied, and the values from the pool are passed directly to the fuzz test method. + +**Default:** `maxMutations = 1` - mutates at most one time after selecting a value from the pool + +**Range:** `[0; Integer.MAX_VALUE]` + #### 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) +// With constraint=RECURSIVE (default): supplier values propagate to both Map keys AND values +@ValuePool(value = {"valuesSupplier"}) Map data, ... -// DECLARATION - applies only to the annotated type, not subtypes -@ValuePool(value = {"mySupplier"}, constraint = Constraint.DECLARATION) +// With constraint=DECLARATION: supplier values only propagate to the Map; NOT keys or values---the supplier should return Map instances to have effect +@ValuePool(value = {"valuesSupplier"}, constraint = DECLARATION) Map data, ... ``` -**Example of the difference:** -```java -// With RECURSIVE (default): -@ValuePool(value = {"valuesSupplier"}) Map data -// The supplier feed both Map keys AND values +**Default:** `constraint = RECURSIVE` - values propagate to all matching types recursively -// 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 -``` +**Range:** `{RECURSIVE, DECLARATION}` --- ### Complete Example ```java class MyFuzzTest { - static Stream edgeCases() { - Map map = new HashMap<>(); - map.put("one", 1); - map.put("two", 2); - 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 - } + static Stream edgeCases() { + Map map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + return Stream.of( + "", // Strings + "null", + "alert('xss')", + 0, // Integers + -1, + Integer.MAX_VALUE, + new byte[] {0x00, 0x7F}, // A byte array + map); // A Map + } + + static Stream justStrings() { + return Stream.of("{\"hello\": \"json\"}", "{\"__proto__\": {\"test\": \"value\"}}"); + } + + @FuzzTest + @ValuePool( + value = {"edgeCases"}, + files = {"test-inputs/*.bin"}, + p = 0.25) // Use pool values 25% of the time + void testParser( + @ValuePool("justStrings") String input, + Map config, + byte[] data) { + // All three parameters get values from the pool: + // - 'input' gets Strings from two suppliers: 'edgeCases()' and 'justStrings()' + // - 'config' keys get Strings, values get Integers, Map itself gets the `map` objects, all from supplier method 'edgeCases()' + // - 'data' gets byte arrays from both edgeCases() and *.bin files + // In addition, the Integer values of the Map 'config' have a different configuration: + // the values from the value pool will be taken with probability 0.01, + // and at most 10 mutations will be applied on top of those values. + } } ``` --- -#### 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 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 29085786d..8f87f14ec 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 @@ -71,6 +71,11 @@ * 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. + * + *

Suppliers in the fuzz test class can be referenced by their method name, while suppliers in + * other classes must be referenced by their fully qualified method name (e.g. {@code + * com.example.MyClass#mySupplierMethod}), or for nested classes: {@code + * com.example.OuterClass$InnerClass#mySupplierMethod}. */ String[] value() default {}; 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 69e8538d4..9d19a4d23 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 @@ -32,14 +32,13 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; public class ValuePoolRegistry { private final Method fuzzTestMethod; private final Path baseDir; - private final Map>> pools; + private final Map> supplierValuesCache = new LinkedHashMap<>(); private final Map> pathToBytesCache = new LinkedHashMap<>(); public ValuePoolRegistry(Method fuzzTestMethod) { @@ -48,7 +47,6 @@ public ValuePoolRegistry(Method fuzzTestMethod) { protected ValuePoolRegistry(Method fuzzTestMethod, Path baseDir) { this.fuzzTestMethod = fuzzTestMethod; - this.pools = extractValueSuppliers(fuzzTestMethod); this.baseDir = baseDir; } @@ -96,25 +94,11 @@ public Stream extractUserValues(AnnotatedType type) { .map(ValuePool::value) .flatMap(Arrays::stream) .filter(name -> !name.isEmpty()) - .flatMap( - name -> { - Supplier> supplier = pools.get(name); - if (supplier == null) { - throw new IllegalStateException( - "@ValuePool: No method named '" - + name - + "' found for type " - + type.getType().getTypeName() - + " in fuzz test method " - + fuzzTestMethod.getName() - + ". Available provider methods: " - + String.join(", ", pools.keySet())); - } - return supplier.get(); - }) + .map(String::trim) + .flatMap(this::loadUserValuesFromSupplier) .distinct(); - // Walking the file system only makes sense for ValuePool's that annotate byte[] types. + // Walking the file system only makes sense for pools that annotate byte[] types. if (type.getType() == byte[].class) { return Stream.concat(valuesFromSourceMethods, extractByteArraysFromPatterns(type)); } else { @@ -122,6 +106,131 @@ public Stream extractUserValues(AnnotatedType type) { } } + private Stream loadUserValuesFromSupplier(String supplierRef) { + Method supplier = resolveSupplier(supplierRef); + return supplierValuesCache + .computeIfAbsent(supplier, s -> loadValuesFromMethod(s, supplierRef)) + .stream(); + } + + private Method resolveSupplier(String supplierRef) { + if (supplierRef.isEmpty()) { + throw new IllegalArgumentException("@ValuePool: Supplier method cannot be blank"); + } + + int hashIndex = supplierRef.indexOf('#'); + + // Supplier method is in the fuzz test class + if (hashIndex == -1) { + return resolveSupplier(fuzzTestMethod.getDeclaringClass(), supplierRef); + } + + // Supplier method is not in the fuzz test class + // Validate the format of the supplier reference before loading the class + if (hashIndex != supplierRef.lastIndexOf('#')) { + throw new IllegalArgumentException( + "@ValuePool: Invalid supplier method reference (multiple '#'): " + supplierRef); + } + if (hashIndex == 0 || hashIndex == supplierRef.length() - 1) { + throw new IllegalArgumentException( + "@ValuePool: Invalid supplier method reference (expected 'ClassName#methodName'): " + + supplierRef); + } + + String className = supplierRef.substring(0, hashIndex); + String methodName = supplierRef.substring(hashIndex + 1); + if (className.isEmpty() || methodName.isEmpty()) { + throw new IllegalArgumentException( + "@ValuePool: Invalid supplier method reference (expected 'ClassName#methodName'): " + + supplierRef); + } + + Class clazz = loadClass(className); + return resolveSupplier(clazz, methodName); + } + + private Method resolveSupplier(Class clazz, String methodName) { + try { + return clazz.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + "@ValuePool: No supplier method named '" + methodName + "' found in class " + clazz, e); + } + } + + private Class loadClass(String className) { + ClassLoader fuzzTestLoader = fuzzTestMethod.getDeclaringClass().getClassLoader(); + try { + return Class.forName(className, false, fuzzTestLoader); + } catch (ClassNotFoundException | LinkageError | SecurityException firstFailure) { + // Retry with the context class loader + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + if (contextLoader != null && contextLoader != fuzzTestLoader) { + try { + return Class.forName(className, false, contextLoader); + } catch (ClassNotFoundException | LinkageError | SecurityException secondFailure) { + IllegalArgumentException ex = + new IllegalArgumentException( + "@ValuePool: Failed to load class '" + + className + + "' (fuzzTestLoader=" + + fuzzTestLoader + + ", contextLoader=" + + contextLoader + + ")", + firstFailure); + ex.addSuppressed(secondFailure); + throw ex; + } + } + if (firstFailure instanceof ClassNotFoundException) { + throw new IllegalArgumentException( + "@ValuePool: No class named '" + className + "' found", firstFailure); + } + throw new IllegalArgumentException( + "@ValuePool: Failed to load class '" + + className + + "' using class loader " + + fuzzTestLoader, + firstFailure); + } + } + + private List loadValuesFromMethod(Method supplier, String supplierRef) { + if (!Modifier.isStatic(supplier.getModifiers())) { + throw new IllegalStateException( + "@ValuePool: supplier method '" + + supplierRef + + "' must be static in fuzz test method " + + fuzzTestMethod.getName()); + } + if (!Stream.class.equals(supplier.getReturnType())) { + throw new IllegalStateException( + "@ValuePool: supplier method '" + + supplierRef + + "' must return a Stream in fuzz test method " + + fuzzTestMethod.getName()); + } + + supplier.setAccessible(true); + + try { + List values = ((Stream) supplier.invoke(null)).collect(Collectors.toList()); + if (values.isEmpty()) { + throw new IllegalStateException( + "@ValuePool: supplier method '" + supplierRef + "' returned no values."); + } + return values; + } catch (IllegalAccessException e) { + throw new RuntimeException("@ValuePool: Access denied for supplier method " + supplierRef, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + throw new RuntimeException( + "@ValuePool: Supplier method " + supplierRef + " threw an exception", + cause != null ? cause : e); + } + } + private Stream extractByteArraysFromPatterns(AnnotatedType type) { List annotations = getValuePoolAnnotations(type); @@ -170,51 +279,4 @@ private Optional tryReadFile(Path path) { } }); } - - private static Map>> extractValueSuppliers(Method fuzzTestMethod) { - return Arrays.stream(fuzzTestMethod.getDeclaringClass().getDeclaredMethods()) - .filter(m -> m.getParameterCount() == 0) - .filter(m -> Stream.class.equals(m.getReturnType())) - .filter(m -> Modifier.isStatic(m.getModifiers())) - .collect(Collectors.toMap(Method::getName, ValuePoolRegistry::createLazyStreamSupplier)); - } - - private static Supplier> createLazyStreamSupplier(Method method) { - return new Supplier>() { - private volatile List cachedData = null; - - @Override - public Stream get() { - if (cachedData == null) { - synchronized (this) { - if (cachedData == null) { - cachedData = loadDataFromMethod(method); - } - } - if (cachedData.isEmpty()) { - throw new IllegalStateException( - "@ValuePool: method '" - + method.getName() - + "' returned no values. Value pool methods must return at least one value."); - } - } - return cachedData.stream(); - } - }; - } - - private static List loadDataFromMethod(Method method) { - method.setAccessible(true); - try { - Stream stream = (Stream) method.invoke(null); - return stream.collect(Collectors.toList()); - } catch (IllegalAccessException e) { - throw new RuntimeException("@ValuePool: Access denied for method " + method.getName(), e); - } catch (InvocationTargetException e) { - 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/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel index 69d1304d2..1a15cd76d 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel @@ -6,6 +6,7 @@ java_library( srcs = [ "GlobTestSupport.java", "TestSupport.java", + "ValuePoolsTestSupport.java", ], visibility = ["//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__"], exports = JUNIT5_DEPS + [ 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 12ede2f53..6830cf7a0 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 @@ -65,6 +65,33 @@ public static Stream myPool2() { return Stream.of("value1", "value2", "value3", "value4"); } + private static Stream myPrivatePool() { + return Stream.of("private!"); + } + + public static Stream poolOfInts() { + return Stream.of(1, 2, 3); + } + + public static List listSupplier() { + return Arrays.asList(1, 2, 3); + } + + public static Stream badPool(int arg) { + return Stream.of(1, 2, 3); + } + + public static Stream emptyPool() { + return Stream.empty(); + } + + private static int sideEffectCounter = 0; + + static Stream poolWithSideEffect() { + sideEffectCounter++; + return Stream.of("only once"); + } + @Test void testExtractFirstProbability_Default() { AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); @@ -166,6 +193,212 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); } + @Test + void testExtractRawValues_PrivatePool() { + AnnotatedType type = new TypeHolder<@ValuePool("myPrivatePool") String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("private!"); + } + + @Test + void testExtractRawValues_SupplierInAnotherClass() { + AnnotatedType type = + new TypeHolder< + @ValuePool("com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#myPool") + String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements) + .containsExactly("external1", "external2", "external3", 1232187321, -182371); + } + + @Test + void testExtractRawValues_SupplierInAnotherClassNotPresent() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#nonexistent") + String>() {}.annotatedType(); + assertThrows( + IllegalArgumentException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_EmptyAnnotationIsNoop() { + AnnotatedType type = new TypeHolder<@ValuePool(p = 0.2) String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isEmpty(); + } + + @Test + void testExtractRawValues_ThrowWhenSupplierReturnsNoValues() { + AnnotatedType type = new TypeHolder<@ValuePool("emptyPool") String>() {}.annotatedType(); + assertThat( + assertThrows( + IllegalStateException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList()))) + .hasMessageThat() + .contains("returned no values"); + } + + @Test + void testExtractRawValues_InvalidMethodReference_MissingClass() { + AnnotatedType type = new TypeHolder<@ValuePool("#myPool") String>() {}.annotatedType(); + assertThrows( + IllegalArgumentException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_InvalidMethodReference_MissingMethod() { + AnnotatedType type = + new TypeHolder< + @ValuePool("com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#") + String>() {}.annotatedType(); + assertThrows( + IllegalArgumentException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_InvalidMethodReference_MultipleHashes() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#myPool#x") + String>() {}.annotatedType(); + assertThrows( + IllegalArgumentException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_InvalidSupplier_ReturnsList() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#listSupplier") + String>() {}.annotatedType(); + assertThrows( + IllegalStateException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_InvalidLocalSupplier_ReturnsList() { + AnnotatedType type = new TypeHolder<@ValuePool("listSupplier") String>() {}.annotatedType(); + assertThrows( + IllegalStateException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_BadPool() { + AnnotatedType type = new TypeHolder<@ValuePool("badPool") String>() {}.annotatedType(); + assertThrows( + IllegalArgumentException.class, + () -> valuePools.extractUserValues(type).collect(Collectors.toList())); + } + + @Test + void testExtractRawValues_StreamOfConcreteTypeSupplier() { + AnnotatedType type = new TypeHolder<@ValuePool("poolOfInts") String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly(1, 2, 3); + } + + @Test + void testExtractRawValues_OverloadedSupplier() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport#myPrivatePoolWithOverload") + String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements) + .containsExactly("external1", "external2", "external3", 1232187321, -182371); + } + + @Test + void testExtractRawValues_SupplierInNestedClass() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport$Nested#myPool") + String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("nested"); + } + + @Test + void testExtractRawValues_MergeSuppliersFromDifferentClasses() { + AnnotatedType type = + new TypeHolder< + @ValuePool( + value = { + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTestSupport$Nested#myPool", + "myPool" + }) + String>() {}.annotatedType(); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("nested", "value1", "value2", "value3"); + } + + @Test + void testExtractRawValues_SuppliersCalledOncePerRegistry() { + // Each supplier method is called once per fuzz test method (i.e. per ValuePoolRegistry) + ValuePoolRegistry valuePools = new ValuePoolRegistry(fuzzTestMethod); + sideEffectCounter = 0; + AnnotatedType type = + new TypeHolder<@ValuePool("poolWithSideEffect") String>() {}.annotatedType(); + List elements1 = valuePools.extractUserValues(type).collect(Collectors.toList()); + List elements2 = valuePools.extractUserValues(type).collect(Collectors.toList()); + List elements3 = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements1).containsExactly("only once"); + assertThat(elements2).containsExactly("only once"); + assertThat(elements3).containsExactly("only once"); + assertThat(sideEffectCounter).isEqualTo(1); + } + + @Test + void testExtractRawValues_SupplierNameInvariance() { + // Each supplier method is called once per fuzz test method (i.e. per ValuePoolRegistry) + ValuePoolRegistry valuePools = new ValuePoolRegistry(fuzzTestMethod); + sideEffectCounter = 0; + AnnotatedType type1 = + new TypeHolder<@ValuePool("poolWithSideEffect") String>() {}.annotatedType(); + AnnotatedType type2 = + new TypeHolder< + @ValuePool( + "com.code_intelligence.jazzer.mutation.support.ValuePoolsTest#poolWithSideEffect") + String>() {}.annotatedType(); + List elements1 = valuePools.extractUserValues(type1).collect(Collectors.toList()); + List elements2 = valuePools.extractUserValues(type2).collect(Collectors.toList()); + assertThat(elements1).containsExactly("only once"); + assertThat(elements2).containsExactly("only once"); + assertThat(sideEffectCounter).isEqualTo(1); + } + + @Test + void changeParametersOnly() { + AnnotatedType sourceType = + new TypeHolder< + @ValuePool(value = "list", p = 1.0, maxMutations = 0) List< + @ValuePool(p = 0.9, maxMutations = 100) String>>() {}.annotatedType(); + AnnotatedType targetType = parameterTypeIfParameterized(sourceType, List.class).get(); + AnnotatedType propagatedType = propagatePropertyConstraints(sourceType, targetType); + + assertThat(extractValuesFromValuePools(propagatedType)).containsExactly("list"); + assertThat(0.9).isEqualTo(valuePools.extractFirstProbability(propagatedType)); + assertThat(100).isEqualTo(valuePools.extractFirstMaxMutations(propagatedType)); + } + @Test void propagateAndJoinRecursiveValuePools() { AnnotatedType sourceType = diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTestSupport.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTestSupport.java new file mode 100644 index 000000000..447c24d50 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTestSupport.java @@ -0,0 +1,46 @@ +/* + * 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 java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +public class ValuePoolsTestSupport { + + public static Stream myPool() { + return Stream.of("external1", "external2", "external3", 1232187321, -182371); + } + + private static Stream myPrivatePoolWithOverload() { + return Stream.of("external1", "external2", "external3", 1232187321, -182371); + } + + private static Stream myPrivatePoolWithOverload(int ignored) { + return Stream.of("should not be used"); + } + + public static final class Nested { + public static Stream myPool() { + return Stream.of("nested"); + } + } + + private static List listSupplier() { + return Arrays.asList(1, 2, 3); + } +}