From 2bca9d8c6d4cdb4b4ec5f8ee67588ec968e176af Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 10 Oct 2025 23:49:39 +0200 Subject: [PATCH 1/3] feat: allow creation of parameterized and generic array types In addition to primitive arrays, these types are now also supported: - List [] - List [][][] --- .../mutation/ArgumentsMutatorFuzzTest.java | 11 ++++++++++ .../collection/ArrayMutatorFactory.java | 18 +++++++++++------ .../jazzer/mutation/support/TypeSupport.java | 20 +++++++++++++++++++ .../jazzer/mutation/mutator/StressTest.java | 6 ++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java index f758644cd..36f7d2ee6 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java @@ -271,6 +271,17 @@ void fuzz_MapField3(Proto3.MapField3 o1) {} public static void fuzz_MutuallyReferringProtobufs( Proto2.TestProtobuf o1, Proto2.TestSubProtobuf o2) {} + @SelfFuzzTest + void fuzzStrings( + @NotNull @WithSize(max = 3) + List @NotNull @WithLength(max = 4) [] arrayOfListOfStrings, + @NotNull @WithSize(max = 1) + List @NotNull @WithLength(max = 2) [] @NotNull @WithLength(max = 3) [] + arrayOfArraysOfListOfIntegers) { + assertThat(arrayOfListOfStrings.length).isAtMost(4); + assertThat(arrayOfArraysOfListOfIntegers.length).isAtMost(2); + } + /** * @return all methods in this class annotated by @SelfFuzzTest. If any of those methods are * annotated by @Solo, only those are returned. diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java index 29b27eb8f..e822e23b1 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java @@ -19,6 +19,7 @@ import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction; import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.extractRawClass; import static java.lang.Math.min; import static java.lang.String.format; @@ -35,6 +36,7 @@ import java.lang.reflect.AnnotatedArrayType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Array; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Optional; import java.util.function.Predicate; @@ -53,12 +55,16 @@ public Optional> tryCreate( AnnotatedType elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType(); AnnotatedType propagatedElementType = propagatePropertyConstraints(type, elementType); - Class propagatedElementClazz = (Class) propagatedElementType.getType(); - return Optional.of(propagatedElementType) - .flatMap(factory::tryCreate) - .map( - elementMutator -> - new ArrayMutator<>(elementMutator, propagatedElementClazz, minLength, maxLength)); + Type rawType = propagatedElementType.getType(); + return extractRawClass(rawType) + .flatMap( + propagatedElementClass -> + Optional.of(propagatedElementType) + .flatMap(factory::tryCreate) + .map( + elementMutator -> + new ArrayMutator<>( + elementMutator, propagatedElementClass, minLength, maxLength))); } enum CrossOverAction { diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index 2480d4e93..bf080a163 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -37,9 +37,12 @@ import java.lang.reflect.AnnotatedTypeVariable; import java.lang.reflect.AnnotatedWildcardType; import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; @@ -687,4 +690,21 @@ public static boolean annotatedTypeEquals(AnnotatedType left, AnnotatedType righ return left.getType().equals(right.getType()) && Arrays.equals(left.getAnnotations(), right.getAnnotations()); } + + public static Optional> extractRawClass(Type type) { + if (type instanceof Class) { + return Optional.of((Class) type); + } else if (type instanceof ParameterizedType) { + return extractRawClass(((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + Optional> componentClass = extractRawClass(componentType); + return componentClass.map(aClass -> Array.newInstance(aClass, 0).getClass()); + } else if (type instanceof TypeVariable || type instanceof WildcardType) { + // Default fallback - assume Object array + return Optional.of(Object.class); + } else { + return Optional.empty(); + } + } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index 29e0aad35..07eb3842e 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -582,6 +582,12 @@ null, emptyList(), singletonList(null), singletonList(false), singletonList(true false, distinctElementsRatio(0.30), distinctElementsRatio(0.30)), + arguments( + new TypeHolder<@NotNull List<@NotNull Integer> @NotNull []>() {}.annotatedType(), + "List[]", + false, + manyDistinctElements(), + distinctElementsRatio(0.66)), arguments( new TypeHolder<@NotNull TestEnumThree @NotNull []>() {}.annotatedType(), "Enum[]", From 0d0e7efd18f374a67231792f8f58e9080ebb884e Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 27 Oct 2025 16:53:10 +0100 Subject: [PATCH 2/3] feat: add meta-annotation for multi-level annotation propagation Allow annotations to be inherited at multiple levels of the type hierarchy, enabling both broad and specific configuration of mutators. Use case: Configure mutators that share common types. For example, annotate a fuzz test method to apply default settings to all String mutators, while still allowing individual String parameters to override those settings with different values. Without this feature, an annotation could only appear once in the inheritance chain, preventing this layered configuration approach. --- .../support/PropertyConstraintSupport.java | 4 +- .../jazzer/mutation/support/TypeSupport.java | 4 ++ .../utils/IgnoreRecursiveConflicts.java | 38 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/PropertyConstraintSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/PropertyConstraintSupport.java index def5547db..14cd4ed08 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/PropertyConstraintSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/PropertyConstraintSupport.java @@ -19,6 +19,7 @@ import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; import static java.util.Arrays.stream; +import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts; import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; @@ -58,7 +59,8 @@ private static String constraintFrom(Annotation constraint) { } private static boolean hasConstraint(AnnotatedType target, Annotation constraint) { - return target.getAnnotation(constraint.annotationType()) != null; + return constraint.annotationType().getAnnotation(IgnoreRecursiveConflicts.class) == null + && target.getAnnotation(constraint.annotationType()) != null; } private PropertyConstraintSupport() {} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index bf080a163..9c7e51d1f 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -27,6 +27,7 @@ import com.code_intelligence.jazzer.mutation.annotation.NotNull; import com.code_intelligence.jazzer.mutation.annotation.WithLength; +import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts; import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; @@ -578,6 +579,9 @@ private static Annotation[] checkExtraAnnotations( .collect(Collectors.toCollection(HashSet::new)); for (Annotation annotation : extraAnnotations) { boolean added = existingAnnotationTypes.add(annotation.annotationType()); + if (annotation.annotationType().isAnnotationPresent(IgnoreRecursiveConflicts.class)) { + continue; + } require(added, annotation + " already directly present on " + base); } return extraAnnotations; diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java b/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java new file mode 100644 index 000000000..0349b934e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.utils; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A meta-annotation to turn off the check in {@code checkExtraAnnotations} that throws if some + * annotation is present multiple times on a type. This allows annotations to be propagated down the + * type hierarchy and accumulated along the way. + * + *

E.g. {@code @A("data1") List<@A("data2") String> arg} - the String mutator can see of + * {@code @A("data1")} and {@code @A("data2")}, but the List mutator can only see + * {@code @A("data1")}. + */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface IgnoreRecursiveConflicts {} From 00d05c297ca79390c74a01ac545e4de972d40b72 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 16 Oct 2025 17:56:42 +0200 Subject: [PATCH 3/3] feat: @ValuePool - propagate user values to types This adds a new user-facing annotation @ValuePool for wiring values directly to type mutators. ValuePoolMutatorFactory is prepended before (almost) every mutator. --- .../mutation/ArgumentsMutatorFuzzTest.java | 6 +- .../jazzer/junit/BUILD.bazel | 1 + .../jazzer/mutation/ArgumentsMutator.java | 18 +- .../jazzer/mutation/BUILD.bazel | 1 + .../jazzer/mutation/annotation/ValuePool.java | 89 +++++++ .../jazzer/mutation/mutator/Mutators.java | 9 +- .../mutation/mutator/lang/LangMutators.java | 6 + .../mutator/lang/StringMutatorFactory.java | 4 +- .../mutator/lang/ValuePoolMutatorFactory.java | 198 ++++++++++++++++ .../mutator/proto/BuilderMutatorFactory.java | 9 +- .../mutation/mutator/proto/ProtoMutators.java | 9 +- .../mutation/support/ValuePoolRegistry.java | 141 +++++++++++ .../jazzer/mutation/support/BUILD.bazel | 2 + .../mutation/support/ValuePoolsTest.java | 218 ++++++++++++++++++ 14 files changed, 700 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java index 36f7d2ee6..21ddd0bad 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java @@ -50,9 +50,9 @@ public class ArgumentsMutatorFuzzTest { static List mutators = methods.stream() .map( - m -> - ArgumentsMutator.forMethod(Mutators.newFactory(), m) - .orElseThrow(() -> new IllegalArgumentException("Invalid method: " + m))) + method -> + ArgumentsMutator.forMethod(Mutators.newFactory(), method) + .orElseThrow(() -> new IllegalArgumentException("Invalid method: " + method))) .collect(Collectors.toList()); static { diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel index 8338cb5c6..9e5074f49 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel @@ -38,6 +38,7 @@ java_library( "//examples/junit/src/test/java/com/example:__pkg__", "//selffuzz/src/test/java/com/code_intelligence/selffuzz:__subpackages__", "//src/test/java/com/code_intelligence/jazzer/junit:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/support:__pkg__", ], exports = [ ":lifecycle", 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 86bfdd67c..db42c791e 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -23,6 +23,7 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; +import com.code_intelligence.jazzer.mutation.annotation.ValuePool; import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; @@ -31,6 +32,8 @@ import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom; import com.code_intelligence.jazzer.mutation.mutator.Mutators; import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import com.code_intelligence.jazzer.utils.Log; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -75,7 +78,7 @@ private static String prettyPrintMethod(Method method) { } public static ArgumentsMutator forMethodOrThrow(Method method) { - return forMethod(Mutators.newFactory(), method) + return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method) .orElseThrow( () -> new IllegalArgumentException( @@ -83,7 +86,7 @@ public static ArgumentsMutator forMethodOrThrow(Method method) { } public static Optional forMethod(Method method) { - return forMethod(Mutators.newFactory(), method); + return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method); } public static Optional forMethod( @@ -97,11 +100,20 @@ public static Optional forMethod( Log.error(validationError.getMessage()); throw validationError; } + + ValuePool[] valuePools = method.getAnnotationsByType(ValuePool.class); + return toArrayOrEmpty( stream(method.getAnnotatedParameterTypes()) .map( type -> { - Optional> mutator = mutatorFactory.tryCreate(type); + // Forward all @ValuePool annotations of the fuzz test method to each + // arg. + AnnotatedType t = type; + for (ValuePool pool : valuePools) { + t = TypeSupport.withExtraAnnotations(t, pool); + } + Optional> mutator = mutatorFactory.tryCreate(t); if (!mutator.isPresent()) { Log.error( String.format( diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel index 73e0472a6..7f5901874 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -12,6 +12,7 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/mutation/engine", "//src/main/java/com/code_intelligence/jazzer/mutation/mutator", "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "//src/main/java/com/code_intelligence/jazzer/mutation/utils", "//src/main/java/com/code_intelligence/jazzer/utils:log", ], ) 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 new file mode 100644 index 000000000..ce636c515 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.annotation; + +import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts; +import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Provides values to user-selected mutator types to start fuzzing from. + * + *

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 + * {@link #constraint()} attribute. + * + *

Example usage: + * + *

{@code
+ * public class MyFuzzTargets {
+ *
+ *   static Stream valuesVisibleByAllArgumentMutators() {
+ *     return Stream.of("example1", "example2", "example3", 1232187321, -182371);
+ *   }
+ *
+ *   static Stream valuesVisibleOnlyByAnotherInput() {
+ *     return Stream.of("code-intelligence.com", "secret.url.1082h3u21ibsdsazuvbsa.com");
+ *   }
+ *
+ *   @ValuePool("valuesVisibleByAllArgumentMutators")
+ *   @FuzzTest
+ *   public void fuzzerTestOneInput(String input, @ValuePool("valuesVisibleOnlyByAnotherInput") String anotherInput) {
+ *     // Fuzzing logic here
+ *   }
+ * }
+ * }
+ * + * In this example, the mutator for the String parameter {@code input} of the fuzz test method + * {@code fuzzerTestOneInput} will be using the values returned by {@code + * valuesVisibleByAllArgumentMutators} method during mutation, while the mutator for String {@code + * anotherInput} will use values from both methods: from the method-level {@code ValuePool} + * annotation that uses {@code valuesVisibleByAllArgumentMutators} and the parameter-level {@code + * ValuePool} annotation that uses {@code valuesVisibleOnlyByAnotherInput}. + */ +@Target({ElementType.METHOD, TYPE_USE}) +@Retention(RUNTIME) +@IgnoreRecursiveConflicts +@PropertyConstraint +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 + * 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(); + + /** + * This {@code ValuePool} will be used with probability {@code p} by the mutator responsible for + * fitting types. + */ + double p() default 0.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 + * RECURSIVE}. + */ + String constraint() default RECURSIVE; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java index c0b810395..97bcab408 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java @@ -27,18 +27,23 @@ import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutators; import com.code_intelligence.jazzer.mutation.mutator.proto.ProtoMutators; import com.code_intelligence.jazzer.mutation.mutator.time.TimeMutators; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class Mutators { private Mutators() {} public static ExtendedMutatorFactory newFactory() { + return newFactory(null); + } + + public static ExtendedMutatorFactory newFactory(ValuePoolRegistry valuePoolRegistry) { return ChainedMutatorFactory.of( new IdentityCache(), NonNullableMutators.newFactories(), - LangMutators.newFactories(), + LangMutators.newFactories(valuePoolRegistry), CollectionMutators.newFactories(), - ProtoMutators.newFactories(), + ProtoMutators.newFactories(valuePoolRegistry), LibFuzzerMutators.newFactories(), TimeMutators.newFactories(), // Keep generic aggregate mutators last in case a concrete type is also an aggregate type. diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java index 0fcf278fa..7dc909ff1 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java @@ -17,15 +17,21 @@ package com.code_intelligence.jazzer.mutation.mutator.lang; import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class LangMutators { private LangMutators() {} public static Stream newFactories() { + return newFactories(null); + } + + public static Stream newFactories(ValuePoolRegistry valuePoolRegistry) { return Stream.of( // DON'T EVER SORT THESE! The order is important for the mutator engine to work correctly. new NullableMutatorFactory(), + new ValuePoolMutatorFactory(valuePoolRegistry), new BooleanMutatorFactory(), new FloatingPointMutatorFactory(), new IntegralMutatorFactory(), diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java index 32f8852c3..7d33c74e5 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java @@ -17,7 +17,9 @@ package com.code_intelligence.jazzer.mutation.mutator.lang; import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; -import static com.code_intelligence.jazzer.mutation.support.TypeSupport.*; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withLength; import com.code_intelligence.jazzer.mutation.annotation.Ascii; import com.code_intelligence.jazzer.mutation.annotation.UrlSegment; 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 new file mode 100644 index 000000000..3c2c94877 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java @@ -0,0 +1,198 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.mutator.lang; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.TypeHolder; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.List; +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. */ + @Target({TYPE_USE}) + @Retention(RUNTIME) + private @interface ValuePoolMarker {} + + public static final Annotation VALUE_POOL_MARKER = + new TypeHolder<@ValuePoolMarker String>() {}.annotatedType() + .getAnnotation(ValuePoolMarker.class); + + private final ValuePoolRegistry valuePoolRegistry; + + ValuePoolMutatorFactory(ValuePoolRegistry valuePoolRegistry) { + this.valuePoolRegistry = valuePoolRegistry; + } + + @Override + public Optional> tryCreate( + AnnotatedType type, ExtendedMutatorFactory factory) { + if (type.getAnnotation(ValuePoolMarker.class) != null) { + return Optional.empty(); + } + AnnotatedType markedType = TypeSupport.withExtraAnnotations(type, VALUE_POOL_MARKER); + return factory + .tryCreate(markedType) + .map(mutator -> ValuePoolMutator.wrapIfValuesExist(markedType, mutator, valuePoolRegistry)); + } + + private static final class ValuePoolMutator extends SerializingMutator { + private final SerializingMutator mutator; + private final List userValues; + private final double poolUsageProbability; + + ValuePoolMutator( + SerializingMutator mutator, List userValues, double poolUsageProbability) { + this.mutator = mutator; + this.userValues = userValues; + this.poolUsageProbability = poolUsageProbability; + } + + @SuppressWarnings("unchecked") + static SerializingMutator wrapIfValuesExist( + AnnotatedType type, SerializingMutator mutator, ValuePoolRegistry valuePoolRegistry) { + + if (valuePoolRegistry == null) { + return mutator; + } + + Optional> rawUserValues = valuePoolRegistry.extractRawValues(type); + if (!rawUserValues.isPresent()) { + return mutator; + } + + List userValues = + rawUserValues + .get() + // 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 + // pool. + .filter(value -> isSerializationStable(mutator, value)) + .map(value -> mutator.detach((T) value)) + .collect(Collectors.toList()); + + if (userValues.isEmpty()) { + return mutator; + } + + double p = valuePoolRegistry.extractFirstProbability(type); + return new ValuePoolMutator<>(mutator, userValues, p); + } + + /** + * Checks if {@code serialize(deserialize(serialize(value))) == serialize(value)}. + * + * @param mutator + * @param value + * @return true if the serialization is stable + * @param + */ + @SuppressWarnings("unchecked") + private static boolean isSerializationStable(SerializingMutator mutator, Object value) { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + try { + mutator.write((T) value, new DataOutputStream(byteStream)); + byte[] originalSerialized = byteStream.toByteArray(); + byteStream.reset(); + + T roundTrip = + mutator.read(new DataInputStream(new ByteArrayInputStream(originalSerialized))); + mutator.write(roundTrip, new DataOutputStream(byteStream)); + byte[] roundTripSerialized = byteStream.toByteArray(); + return Arrays.equals(originalSerialized, roundTripSerialized); + } catch (Exception e) { + return false; + } + } + + @Override + public String toDebugString(Predicate isInCycle) { + return String.format( + "%s (values: %d p: %.2f)", + mutator.toDebugString(isInCycle), userValues.size(), poolUsageProbability); + } + + @Override + public T read(DataInputStream in) throws IOException { + return mutator.read(in); + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + mutator.write(value, out); + } + + @Override + public T detach(T value) { + return mutator.detach(value); + } + + @Override + protected boolean computeHasFixedSize() { + return mutator.hasFixedSize(); + } + + @Override + public T init(PseudoRandom prng) { + if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { + return prng.pickIn(userValues); + } else { + return mutator.init(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); + } + } + return mutator.mutate(value, prng); + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + return mutator.crossOver(value, otherValue, prng); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java index 48c717b3c..d4f70c5dc 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java @@ -56,6 +56,7 @@ import com.code_intelligence.jazzer.mutation.mutator.collection.CollectionMutators; import com.code_intelligence.jazzer.mutation.mutator.lang.LangMutators; import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import com.google.protobuf.Any; import com.google.protobuf.CodedInputStream; import com.google.protobuf.Descriptors.Descriptor; @@ -87,6 +88,11 @@ import java.util.stream.Stream; public final class BuilderMutatorFactory implements MutatorFactory { + final ValuePoolRegistry valuePoolRegistry; + + public BuilderMutatorFactory(ValuePoolRegistry valuePoolRegistry) { + this.valuePoolRegistry = valuePoolRegistry; + } // Generous size limit for decoded protobuf messages. This is necessary to guard against OOM // errors when the corpus format changes e.g. due to a change in the fuzz test signature. @@ -175,7 +181,8 @@ private ExtendedMutatorFactory withDescriptorDependentMutatorFactoryIfNeeded( // to follow constructors or builders of the EnumValueDescriptor class. return ChainedMutatorFactory.of( Stream.concat( - Stream.concat(LangMutators.newFactories(), CollectionMutators.newFactories()), + Stream.concat( + LangMutators.newFactories(valuePoolRegistry), CollectionMutators.newFactories()), Stream.of(enumFactory))); } else if (field.getJavaType() == JavaType.MESSAGE) { if (field.isMapField()) { diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java index ec7b4b9a7..cace93938 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java @@ -17,16 +17,23 @@ package com.code_intelligence.jazzer.mutation.mutator.proto; import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class ProtoMutators { private ProtoMutators() {} public static Stream newFactories() { + return newFactories(null); + } + + public static Stream newFactories(ValuePoolRegistry valuePoolRegistry) { try { Class.forName("com.google.protobuf.Message"); return Stream.of( - new ByteStringMutatorFactory(), new MessageMutatorFactory(), new BuilderMutatorFactory()); + new ByteStringMutatorFactory(), + new MessageMutatorFactory(), + new BuilderMutatorFactory(valuePoolRegistry)); } catch (ClassNotFoundException e) { return Stream.empty(); } 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 new file mode 100644 index 000000000..8b7802122 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java @@ -0,0 +1,141 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.support; + +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.lang.reflect.AnnotatedType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +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 Map>> pools; + private final Method fuzzTestMethod; + + public ValuePoolRegistry(Method fuzzTestMethod) { + this.fuzzTestMethod = fuzzTestMethod; + this.pools = extractValueSuppliers(fuzzTestMethod); + } + + /** + * Extract probability of the very first {@code ValuePool} annotation on the given type. The + * {@code @ValuePool} annotation directly on the type is preferred; if there is none, the first + * one appended because of {@code PropertyConstraint.RECURSIVE} is used. Any further + * {@code @ValuePool} annotations appended later to this type because of {@code + * PropertyConstraint.RECURSIVE}, are ignored. Callers should ensure that at least one + * {@code @ValuePool} annotation is present on the type. + */ + public double extractFirstProbability(AnnotatedType type) { + // If there are several @ValuePool annotations on the type, this will take the most + // immediate one, because @ValuePool is not repeatable. + 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."); + } + double p = valuePoolAnnotations[0].p(); + require(p >= 0.0 && p <= 1.0, "@ValuePool p must be in [0.0, 1.0], but was " + p); + return p; + } + + public Optional> extractRawValues(AnnotatedType type) { + String[] poolNames = + Arrays.stream(type.getAnnotations()) + .filter(annotation -> annotation instanceof ValuePool) + .map(annotation -> (ValuePool) annotation) + .map(ValuePool::value) + .flatMap(Arrays::stream) + .toArray(String[]::new); + + if (poolNames.length == 0) { + return Optional.empty(); + } + + return Optional.of( + Arrays.stream(poolNames) + .flatMap( + name -> { + Supplier> supplier = pools.get(name); + if (supplier == null) { + throw new IllegalStateException( + "No method named '" + + name + + "' found for @ValuePool on type " + + type.getType().getTypeName() + + " in fuzz test method " + + fuzzTestMethod.getName() + + ". Available provider methods: " + + String.join(", ", pools.keySet())); + } + return supplier.get(); + }) + .distinct()); + } + + 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()) { + Log.warn("@ValuePool method " + method.getName() + " provided no values."); + return Stream.empty(); + } + } + + 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 IllegalStateException("Cannot access method " + method.getName(), e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error invoking method " + method.getName(), e.getCause()); + } + } +} 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 75b5d57b9..d69a7f0a2 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 @@ -30,6 +30,8 @@ java_test_suite( runner = "junit5", deps = [ ":test_support", + "//deploy:jazzer-project", + "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test", "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", "//src/main/java/com/code_intelligence/jazzer/mutation/support", "//src/main/java/com/code_intelligence/jazzer/mutation/utils", 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 new file mode 100644 index 000000000..56b9c72f6 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypeIfParameterized; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; +import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.DECLARATION; +import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE; +import static com.google.common.truth.Truth.assertThat; + +import com.code_intelligence.jazzer.mutation.annotation.ValuePool; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +class ValuePoolsTest { + + /* Dummy fuzz test method to add to MutatorRuntime. */ + public void dummyFuzzTestMethod() {} + + private static final ValuePoolRegistry valuePools; + + static { + try { + valuePools = new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod")); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public static Stream myPool() { + return Stream.of("value1", "value2", "value3"); + } + + public static Stream myPool2() { + return Stream.of("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractFirstProbability_Default() { + AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.1); + } + + @Test + void testExtractFirstProbability_OneUserDefined() { + AnnotatedType type = + new TypeHolder<@ValuePool(value = "myPool2", p = 0.2) String>() {}.annotatedType(); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.2); + } + + @Test + void testExtractFirstProbability_TwoWithLastUsed() { + AnnotatedType type = + withExtraAnnotations( + new TypeHolder<@ValuePool(value = "myPool", p = 0.2) String>() {}.annotatedType(), + withValuePoolImplementation(new String[] {"myPool2"}, 0.3)); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.2); + } + + @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"); + } + + @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"); + } + + @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"); + } + + @Test + void testExtractRawValues_JoinFromTwoSeparateAnnotations() { + AnnotatedType type = + withExtraAnnotations( + new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), + withValuePoolImplementation(new String[] {"myPool"}, 5)); + Optional> elements = valuePools.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void propagateAndJoinRecursiveValuePools() { + AnnotatedType sourceType = + new TypeHolder< + @ValuePool(value = "list", p = 1.0) List< + @ValuePool(value = "string", p = 0.9) String>>() {}.annotatedType(); + AnnotatedType targetType = parameterTypeIfParameterized(sourceType, List.class).get(); + + AnnotatedType propagatedType = propagatePropertyConstraints(sourceType, targetType); + + assertThat(extractValuesFromValuePools(propagatedType)).containsExactly("list", "string"); + assertThat(0.9).isEqualTo(valuePools.extractFirstProbability(propagatedType)); + } + + @Test + void dontPropagateNonRecursiveValuePool() { + AnnotatedType sourceType = + new TypeHolder< + @ValuePool(value = "list", p = 1.0, constraint = DECLARATION) List< + @ValuePool(value = "string", p = 0.9) String>>() {}.annotatedType(); + AnnotatedType targetType = parameterTypeIfParameterized(sourceType, List.class).get(); + + AnnotatedType propagatedType = propagatePropertyConstraints(sourceType, targetType); + + assertThat(extractValuesFromValuePools(propagatedType)).containsExactly("string"); + assertThat(0.9).isEqualTo(valuePools.extractFirstProbability(propagatedType)); + } + + private static ValuePool[] getValuePoolAnnotations(AnnotatedType type) { + return Arrays.stream(type.getAnnotations()) + .filter(annotation -> annotation instanceof ValuePool) + .toArray(ValuePool[]::new); + } + + private static Stream extractValuesFromValuePools(AnnotatedType type) { + return Arrays.stream(getValuePoolAnnotations(type)).flatMap(v -> Arrays.stream(v.value())); + } + + public static ValuePool withValuePoolImplementation(String[] value, double p) { + return withValuePoolImplementation(value, p, RECURSIVE); + } + + public static ValuePool withValuePoolImplementation(String[] value, double p, String constraint) { + return new ValuePool() { + @Override + public String[] value() { + return value; + } + + @Override + public double p() { + return p; + } + + @Override + public String constraint() { + return constraint; + } + + @Override + public Class annotationType() { + return ValuePool.class; + } + + @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() { + int hash = 0; + hash += Arrays.hashCode(value()) * 127; + hash += Double.hashCode(p()) * 31 * 127; + hash += constraint().hashCode() * 127; + return hash; + } + + @Override + public String toString() { + return "@" + + ValuePool.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, p=" + + p() + + ", constraint=" + + constraint() + + ")"; + } + }; + } +}