Skip to content

Commit 8058002

Browse files
committed
feat: add @ElementOf annotation
This allows picking from a fixed set of values during mutation.
1 parent 1cfba07 commit 8058002

File tree

5 files changed

+302
-0
lines changed

5 files changed

+302
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2024 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.annotation;
18+
19+
import static java.lang.annotation.ElementType.TYPE_USE;
20+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
21+
22+
import com.code_intelligence.jazzer.mutation.utils.AppliesTo;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.Target;
25+
26+
@Target(TYPE_USE)
27+
@Retention(RUNTIME)
28+
@AppliesTo({
29+
byte.class,
30+
Byte.class,
31+
short.class,
32+
Short.class,
33+
int.class,
34+
Integer.class,
35+
long.class,
36+
Long.class,
37+
char.class,
38+
Character.class,
39+
float.class,
40+
Float.class,
41+
double.class,
42+
Double.class,
43+
String.class
44+
})
45+
public @interface ElementOf {
46+
byte[] bytes() default {};
47+
48+
short[] shorts() default {};
49+
50+
int[] integers() default {};
51+
52+
long[] longs() default {};
53+
54+
char[] chars() default {};
55+
56+
float[] floats() default {};
57+
58+
double[] doubles() default {};
59+
60+
String[] strings() default {};
61+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.mutator.lang;
18+
19+
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices;
20+
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap;
21+
import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
22+
import static java.lang.String.format;
23+
import static java.util.Arrays.stream;
24+
import static java.util.stream.Collectors.toList;
25+
26+
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
27+
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
28+
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
29+
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
30+
import java.lang.reflect.AnnotatedType;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.List;
34+
import java.util.Optional;
35+
36+
final class ElementOfMutatorFactory implements MutatorFactory {
37+
@Override
38+
public Optional<SerializingMutator<?>> tryCreate(
39+
AnnotatedType type, ExtendedMutatorFactory factory) {
40+
ElementOf elementOf = type.getAnnotation(ElementOf.class);
41+
if (elementOf == null) {
42+
return Optional.empty();
43+
}
44+
if (!(type.getType() instanceof Class<?>)) {
45+
return Optional.empty();
46+
}
47+
Class<?> rawType = (Class<?>) type.getType();
48+
49+
if (rawType == byte.class || rawType == Byte.class) {
50+
return Optional.of(
51+
elementOfMutator(boxBytes(elementOf.bytes()), "bytes", rawType.getSimpleName()));
52+
} else if (rawType == short.class || rawType == Short.class) {
53+
return Optional.of(
54+
elementOfMutator(boxShorts(elementOf.shorts()), "shorts", rawType.getSimpleName()));
55+
} else if (rawType == int.class || rawType == Integer.class) {
56+
return Optional.of(
57+
elementOfMutator(boxInts(elementOf.integers()), "integers", rawType.getSimpleName()));
58+
} else if (rawType == long.class || rawType == Long.class) {
59+
return Optional.of(
60+
elementOfMutator(boxLongs(elementOf.longs()), "longs", rawType.getSimpleName()));
61+
} else if (rawType == char.class || rawType == Character.class) {
62+
return Optional.of(
63+
elementOfMutator(boxChars(elementOf.chars()), "chars", rawType.getSimpleName()));
64+
} else if (rawType == float.class || rawType == Float.class) {
65+
return Optional.of(
66+
elementOfMutator(boxFloats(elementOf.floats()), "floats", rawType.getSimpleName()));
67+
} else if (rawType == double.class || rawType == Double.class) {
68+
return Optional.of(
69+
elementOfMutator(boxDoubles(elementOf.doubles()), "doubles", rawType.getSimpleName()));
70+
} else if (rawType == String.class) {
71+
return Optional.of(
72+
elementOfMutator(Arrays.asList(elementOf.strings()), "strings", rawType.getSimpleName()));
73+
}
74+
return Optional.empty();
75+
}
76+
77+
private static <T> SerializingMutator<T> elementOfMutator(
78+
List<T> values, String fieldName, String targetTypeName) {
79+
require(
80+
!values.isEmpty(),
81+
format(
82+
"@ElementOf %s array must contain at least one value for %s",
83+
fieldName, targetTypeName));
84+
85+
return mutateThenMap(
86+
mutateIndices(values.size()),
87+
values::get,
88+
value -> {
89+
int index = values.indexOf(value);
90+
require(
91+
index >= 0,
92+
"@ElementOf produced value not contained in the declared value set for "
93+
+ targetTypeName);
94+
return index;
95+
},
96+
isInCycle ->
97+
format("@ElementOf(%s, size=%d) -> %s", fieldName, values.size(), targetTypeName));
98+
}
99+
100+
private static List<Byte> boxBytes(byte[] values) {
101+
List<Byte> result = new ArrayList<>(values.length);
102+
for (byte value : values) {
103+
result.add(value);
104+
}
105+
return result;
106+
}
107+
108+
private static List<Short> boxShorts(short[] values) {
109+
List<Short> result = new ArrayList<>(values.length);
110+
for (short value : values) {
111+
result.add(value);
112+
}
113+
return result;
114+
}
115+
116+
private static List<Integer> boxInts(int[] values) {
117+
return stream(values).boxed().collect(toList());
118+
}
119+
120+
private static List<Long> boxLongs(long[] values) {
121+
return Arrays.stream(values).boxed().collect(toList());
122+
}
123+
124+
private static List<Character> boxChars(char[] values) {
125+
List<Character> result = new ArrayList<>(values.length);
126+
for (char value : values) {
127+
result.add(value);
128+
}
129+
return result;
130+
}
131+
132+
private static List<Float> boxFloats(float[] values) {
133+
List<Float> result = new ArrayList<>(values.length);
134+
for (float value : values) {
135+
result.add(value);
136+
}
137+
return result;
138+
}
139+
140+
private static List<Double> boxDoubles(double[] values) {
141+
return Arrays.stream(values).boxed().collect(toList());
142+
}
143+
}

src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static Stream<MutatorFactory> newFactories(ValuePoolRegistry valuePoolReg
3232
// DON'T EVER SORT THESE! The order is important for the mutator engine to work correctly.
3333
new NullableMutatorFactory(),
3434
new ValuePoolMutatorFactory(valuePoolRegistry),
35+
new ElementOfMutatorFactory(),
3536
new BooleanMutatorFactory(),
3637
new FloatingPointMutatorFactory(),
3738
new IntegralMutatorFactory(),

src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
4040
import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl;
4141
import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
42+
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
4243
import com.code_intelligence.jazzer.mutation.annotation.FloatInRange;
4344
import com.code_intelligence.jazzer.mutation.annotation.InRange;
4445
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
@@ -868,6 +869,13 @@ void singleParam(int parameter) {}
868869
true,
869870
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C),
870871
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C)),
872+
arguments(
873+
new TypeHolder<
874+
@ElementOf(strings = {"one", "two", "three"}) String>() {}.annotatedType(),
875+
"Nullable<@ElementOf(strings, size=3) -> String>",
876+
true,
877+
exactly(null, "one", "two", "three"),
878+
exactly(null, "one", "two", "three")),
871879
arguments(
872880
new TypeHolder<@NotNull @FloatInRange(min = 0f) Float>() {}.annotatedType(),
873881
"Float",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.mutator.lang;
18+
19+
import static com.code_intelligence.jazzer.mutation.support.TestSupport.mockPseudoRandom;
20+
import static com.google.common.truth.Truth.assertThat;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
23+
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
24+
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
25+
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
26+
import com.code_intelligence.jazzer.mutation.engine.ChainedMutatorFactory;
27+
import com.code_intelligence.jazzer.mutation.support.TestSupport.MockPseudoRandom;
28+
import com.code_intelligence.jazzer.mutation.support.TypeHolder;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
32+
@SuppressWarnings("unchecked")
33+
class ElementOfMutatorFactoryTest {
34+
private ChainedMutatorFactory factory;
35+
36+
@BeforeEach
37+
void setUp() {
38+
factory = ChainedMutatorFactory.of(LangMutators.newFactories());
39+
}
40+
41+
@Test
42+
void usesProvidedIntegers() {
43+
SerializingMutator<Integer> mutator =
44+
(SerializingMutator<Integer>)
45+
factory.createOrThrow(
46+
new TypeHolder<
47+
@ElementOf(integers = {1, 2, 3}) @NotNull Integer>() {}.annotatedType());
48+
49+
int value;
50+
try (MockPseudoRandom prng = mockPseudoRandom(0)) {
51+
value = mutator.init(prng);
52+
}
53+
assertThat(value).isEqualTo(1);
54+
55+
try (MockPseudoRandom prng = mockPseudoRandom(2)) {
56+
value = mutator.mutate(value, prng);
57+
}
58+
assertThat(value).isEqualTo(3);
59+
}
60+
61+
@Test
62+
void usesProvidedStrings() {
63+
SerializingMutator<String> mutator =
64+
(SerializingMutator<String>)
65+
factory.createOrThrow(
66+
new TypeHolder<
67+
@ElementOf(strings = {"one", "two"}) @NotNull String>() {}.annotatedType());
68+
69+
String value;
70+
try (MockPseudoRandom prng = mockPseudoRandom(0)) {
71+
value = mutator.init(prng);
72+
}
73+
assertThat(value).isEqualTo("one");
74+
75+
try (MockPseudoRandom prng = mockPseudoRandom(1)) {
76+
value = mutator.mutate(value, prng);
77+
}
78+
assertThat(value).isEqualTo("two");
79+
}
80+
81+
@Test
82+
void rejectsEmptyArrayForMatchingType() {
83+
assertThrows(
84+
IllegalArgumentException.class,
85+
() ->
86+
factory.createOrThrow(
87+
new TypeHolder<@ElementOf(bytes = {0, 1, 2}) Integer>() {}.annotatedType()));
88+
}
89+
}

0 commit comments

Comments
 (0)