Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions docs/mutation-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,23 @@ string. This is done using annotations directly on the parameters.
All annotations reside in the `com.code_intelligence.jazzer.mutation.annotation`
package.

| Annotation | Applies To | Notes |
|-------------------|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| `@Ascii` | `java.lang.String` | `String` should only contain ASCII characters |
| `@InRange` | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals |
| `@FloatInRange` | `float`, `Float` | Specifies `min` and `max` values of generated floats |
| `@DoubleInRange` | `double`, `Double` | Specifies `min` and `max` values of generated doubles |
| `@Positive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only positive values are generated |
| `@Negative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only negative values are generated |
| `@NonPositive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-positive values are generated |
| `@NonNegative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-negative values are generated |
| `@Finite` | `float`, `Float`, `double`, `Double` | Specifies that only finite values are generated |
| `@NotNull` | | Specifies that a reference type should not be `null` |
| `@WithLength` | `byte[]` | Specifies the length of the generated byte array |
| `@WithUtf8Length` | `java.lang.String` | Specifies the length of the generated string in UTF-8 bytes, see annotation Javadoc for further information |
| `@WithSize` | `java.util.List`, `java.util.Map` | Specifies the size of the generated collection |
| `@UrlSegment` | `java.lang.String` | `String` should only contain valid URL segment characters |
| Annotation | Applies To | Notes |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `@Ascii` | `java.lang.String` | `String` should only contain ASCII characters |
| `@InRange` | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals |
| `@FloatInRange` | `float`, `Float` | Specifies `min` and `max` values of generated floats |
| `@DoubleInRange` | `double`, `Double` | Specifies `min` and `max` values of generated doubles |
| `@Positive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only positive values are generated |
| `@Negative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only negative values are generated |
| `@NonPositive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-positive values are generated |
| `@NonNegative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-negative values are generated |
| `@Finite` | `float`, `Float`, `double`, `Double` | Specifies that only finite values are generated |
| `@ElementOf` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `char`, `Character`, `float`, `Float`, `double`, `Double`, `String` | Restricts the value to a fixed set; populate only the array matching the parameter type with at least two entries |
| `@NotNull` | | Specifies that a reference type should not be `null` |
| `@WithLength` | `byte[]` | Specifies the length of the generated byte array |
| `@WithUtf8Length` | `java.lang.String` | Specifies the length of the generated string in UTF-8 bytes, see annotation Javadoc for further information |
| `@WithSize` | `java.util.List`, `java.util.Map` | Specifies the size of the generated collection |
| `@UrlSegment` | `java.lang.String` | `String` should only contain valid URL segment characters |

The example below shows how Fuzz Test parameters can be annotated to provide
additional information to the mutation framework.
Expand All @@ -116,6 +117,15 @@ public void testSimpleTypeRecord(@NotNull @WithSize(min = 3, max = 100) List<Sim
}
```

Use `@ElementOf` when a parameter should only take one of a few constant values.

```java title="Example" showLineNumbers
@FuzzTest
void fuzz(@ElementOf(strings = {"one", "two", "three"}) String value) {
// value is always "one", "two", "three" or null
}
```

### Annotation constraints

Often, annotations should be applied to a type and all it's nested component
Expand Down Expand Up @@ -347,4 +357,3 @@ class ParserTests {
}
}
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.code_intelligence.jazzer.mutation.utils.AppliesTo;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Restricts generated values to a fixed set.
*
* <p>Populate exactly one of the type-specific arrays with at least two values. Only the array that
* matches the annotated parameter type is used; all others are ignored. For {@link String} and
* wrapper types, the mutator may still emit {@code null}; add {@link NotNull} (the only supported
* annotation in combination with {@code @ElementOf}) to prevent that.
*
* <p>Example usage:
*
* <pre>{@code
* @FuzzTest
* void fuzz(
* @ElementOf(integers = {1, 2, 3}) int option,
* @ElementOf(strings = {"one", "two"}) String label) {
* // option is always 1, 2, or 3; label is always "one" or "two".
* }
* }</pre>
*/
@Target(TYPE_USE)
@Retention(RUNTIME)
@AppliesTo({
byte.class,
Byte.class,
short.class,
Short.class,
int.class,
Integer.class,
long.class,
Long.class,
char.class,
Character.class,
float.class,
Float.class,
double.class,
Double.class,
String.class
})
public @interface ElementOf {
byte[] bytes() default {};

short[] shorts() default {};

int[] integers() default {};

long[] longs() default {};

char[] chars() default {};

float[] floats() default {};

double[] doubles() default {};

String[] strings() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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 com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices;
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap;
import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
import java.lang.reflect.AnnotatedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

final class ElementOfMutatorFactory implements MutatorFactory {
@Override
public Optional<SerializingMutator<?>> tryCreate(
AnnotatedType type, ExtendedMutatorFactory factory) {
ElementOf elementOf = type.getAnnotation(ElementOf.class);
if (elementOf == null) {
return Optional.empty();
}
if (!(type.getType() instanceof Class<?>)) {
return Optional.empty();
}
Class<?> rawType = (Class<?>) type.getType();

if (rawType == byte.class || rawType == Byte.class) {
return Optional.of(
elementOfMutator(boxBytes(elementOf.bytes()), "bytes", rawType.getSimpleName()));
} else if (rawType == short.class || rawType == Short.class) {
return Optional.of(
elementOfMutator(boxShorts(elementOf.shorts()), "shorts", rawType.getSimpleName()));
} else if (rawType == int.class || rawType == Integer.class) {
return Optional.of(
elementOfMutator(boxInts(elementOf.integers()), "integers", rawType.getSimpleName()));
} else if (rawType == long.class || rawType == Long.class) {
return Optional.of(
elementOfMutator(boxLongs(elementOf.longs()), "longs", rawType.getSimpleName()));
} else if (rawType == char.class || rawType == Character.class) {
return Optional.of(
elementOfMutator(boxChars(elementOf.chars()), "chars", rawType.getSimpleName()));
} else if (rawType == float.class || rawType == Float.class) {
return Optional.of(
elementOfMutator(boxFloats(elementOf.floats()), "floats", rawType.getSimpleName()));
} else if (rawType == double.class || rawType == Double.class) {
return Optional.of(
elementOfMutator(boxDoubles(elementOf.doubles()), "doubles", rawType.getSimpleName()));
} else if (rawType == String.class) {
return Optional.of(
elementOfMutator(Arrays.asList(elementOf.strings()), "strings", rawType.getSimpleName()));
}
return Optional.empty();
}

private static <T> SerializingMutator<T> elementOfMutator(
List<T> values, String fieldName, String targetTypeName) {
require(
values.size() > 1,
format(
"@ElementOf %s array must contain at least two values for %s",
fieldName, targetTypeName));
Map<T, Integer> inverse =
IntStream.range(0, values.size()).boxed().collect(Collectors.toMap(values::get, i -> i));
return mutateThenMap(
mutateIndices(values.size()),
values::get,
v -> inverse.getOrDefault(v, 0),
isInCycle -> format("@ElementOf<%s>[%d]", targetTypeName, values.size()));
}

private static List<Byte> boxBytes(byte[] values) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to only save distinct values?
Currently @ElementOf(bytes={1,1,1,2}) has 4 values, but effectively only two.
And 1 is three times more likely to be picked than 2. Is this what we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO tweaking the probabilities is the most intuitive option if a user wants to add duplicate values. Or do you see a problem with duplicate values?

List<Byte> result = new ArrayList<>(values.length);
for (byte value : values) {
result.add(value);
}
return result;
}

private static List<Short> boxShorts(short[] values) {
List<Short> result = new ArrayList<>(values.length);
for (short value : values) {
result.add(value);
}
return result;
}

private static List<Integer> boxInts(int[] values) {
return stream(values).boxed().collect(toList());
}

private static List<Long> boxLongs(long[] values) {
return Arrays.stream(values).boxed().collect(toList());
}

private static List<Character> boxChars(char[] values) {
List<Character> result = new ArrayList<>(values.length);
for (char value : values) {
result.add(value);
}
return result;
}

private static List<Float> boxFloats(float[] values) {
List<Float> result = new ArrayList<>(values.length);
for (float value : values) {
result.add(value);
}
return result;
}

private static List<Double> boxDoubles(double[] values) {
return Arrays.stream(values).boxed().collect(toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static Stream<MutatorFactory> newFactories(ValuePoolRegistry valuePoolReg
return Stream.of(
// DON'T EVER SORT THESE! The order is important for the mutator engine to work correctly.
new NullableMutatorFactory(),
new ElementOfMutatorFactory(),
new ValuePoolMutatorFactory(valuePoolRegistry),
new BooleanMutatorFactory(),
new FloatingPointMutatorFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl;
import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
import com.code_intelligence.jazzer.mutation.annotation.FloatInRange;
import com.code_intelligence.jazzer.mutation.annotation.InRange;
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
Expand Down Expand Up @@ -868,6 +869,13 @@ void singleParam(int parameter) {}
true,
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C),
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C)),
arguments(
new TypeHolder<
@ElementOf(strings = {"one", "two", "three"}) String>() {}.annotatedType(),
"Nullable<@ElementOf(strings, size=3) -> String>",
true,
exactly(null, "one", "two", "three"),
exactly(null, "one", "two", "three")),
arguments(
new TypeHolder<@NotNull @FloatInRange(min = 0f) Float>() {}.annotatedType(),
"Float",
Expand Down
Loading
Loading