Skip to content

Commit

Permalink
mutation: Add support for sealed classes
Browse files Browse the repository at this point in the history
  • Loading branch information
fmeum authored and oetr committed Jan 17, 2025
1 parent ed5d042 commit 31cff97
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.code_intelligence.jazzer.mutation.api.Serializer;
import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
import com.code_intelligence.jazzer.mutation.support.Preconditions;
import com.google.errorprone.annotations.ImmutableTypeParameter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
Expand Down Expand Up @@ -428,6 +429,102 @@ public String toDebugString(Predicate<Debuggable> isInCycle) {
};
}

/**
* Mutates a sum type (e.g. a sealed interface), preferring to mutate the current state but
* occasionally switching to a different state.
*
* @param getState a function that returns the current state of the sum type as an index into
* {@code perStateMutators}, or -1 if the state is indeterminate.
* @param perStateMutators the mutators for each state
* @return a mutator that mutates the sum type
*/
@SafeVarargs
public static <T> SerializingMutator<?> mutateSum(
ToIntFunction<T> getState, SerializingMutator<T>... perStateMutators) {
Preconditions.require(perStateMutators.length > 0, "At least one mutator must be provided");
if (perStateMutators.length == 1) {
return perStateMutators[0];
}
boolean hasFixedSize = stream(perStateMutators).allMatch(SerializingMutator::hasFixedSize);
final SerializingMutator<T>[] mutators =
Arrays.copyOf(perStateMutators, perStateMutators.length);
return new SerializingMutator<T>() {
@Override
public T init(PseudoRandom prng) {
return mutators[prng.indexIn(mutators)].init(prng);
}

@Override
public T mutate(T value, PseudoRandom prng) {
int currentState = getState.applyAsInt(value);
if (currentState == -1) {
// The value is in an indeterminate state, initialize it.
return init(prng);
}
if (prng.trueInOneOutOf(100)) {
// Initialize to a different state.
return mutators[prng.otherIndexIn(mutators, currentState)].init(prng);
}
// Mutate within the current state.
return mutators[currentState].mutate(value, prng);
}

@Override
public T crossOver(T value, T otherValue, PseudoRandom prng) {
// Try to cross over in current state and leave state changes to the mutate step.
int currentState = getState.applyAsInt(value);
int otherState = getState.applyAsInt(otherValue);
if (currentState == -1) {
// If reference is not initialized to a concrete state yet, try to do so in
// the state of other reference, as that's at least some progress.
if (otherState == -1) {
// If both states are indeterminate, cross over can not be performed.
return value;
}
return mutators[otherState].init(prng);
}
if (currentState == otherState) {
return mutators[currentState].crossOver(value, otherValue, prng);
}
return value;
}

@Override
public T detach(T value) {
int currentState = getState.applyAsInt(value);
if (currentState == -1) {
return value;
}
return mutators[currentState].detach(value);
}

@Override
public T read(DataInputStream in) throws IOException {
int currentState = Math.floorMod(in.readInt(), mutators.length);
return mutators[currentState].read(in);
}

@Override
public void write(T value, DataOutputStream out) throws IOException {
int currentState = getState.applyAsInt(value);
out.writeInt(currentState);
mutators[currentState].write(value, out);
}

@Override
public boolean hasFixedSize() {
return hasFixedSize;
}

@Override
public String toDebugString(Predicate<Debuggable> isInCycle) {
return stream(mutators)
.map(mutator -> mutator.toDebugString(isInCycle))
.collect(joining(" | ", "(", ")"));
}
};
}

/**
* Use {@link #markAsRequiringRecursionBreaking(SerializingMutator)} instead for {@link
* com.code_intelligence.jazzer.mutation.api.ValueMutator}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public static ExtendedMutatorFactory newFactory() {
CollectionMutators.newFactories(),
ProtoMutators.newFactories(),
LibFuzzerMutators.newFactories(),
AggregateMutators.newFactories(),
TimeMutators.newFactories());
TimeMutators.newFactories(),
// Keep generic aggregate mutators last in case a concrete type is also an aggregate type.
AggregateMutators.newFactories());
}

// Mutators for which the NullableMutatorFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,43 @@ private AggregateMutators() {}

public static Stream<MutatorFactory> newFactories() {
// Register the record mutator first as it is more specific.
return Stream.concat(
newRecordMutatorFactoryIfSupported(),
Stream.of(
new SetterBasedBeanMutatorFactory(),
new ConstructorBasedBeanMutatorFactory(),
new CachedConstructorMutatorFactory()));
return Stream.of(
newRecordMutatorFactoryIfSupported(),
newSealedClassMutatorFactoryIfSupported(),
Stream.of(
new SetterBasedBeanMutatorFactory(),
new ConstructorBasedBeanMutatorFactory(),
new CachedConstructorMutatorFactory()))
.flatMap(s -> s);
}

private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
if (!supportsRecords()) {
try {
Class.forName("java.lang.Record");
return Stream.of(instantiateMutatorFactory("RecordMutatorFactory"));
} catch (ClassNotFoundException ignored) {
return Stream.empty();
}
}

private static Stream<MutatorFactory> newSealedClassMutatorFactoryIfSupported() {
try {
Class.class.getMethod("getPermittedSubclasses");
return Stream.of(instantiateMutatorFactory("SealedClassMutatorFactory"));
} catch (NoSuchMethodException e) {
return Stream.empty();
}
}

private static MutatorFactory instantiateMutatorFactory(String simpleClassName) {
try {
// Instantiate RecordMutatorFactory via reflection as making it a compile time dependency
// breaks the r8 step in the Android build.
Class<? extends MutatorFactory> recordMutatorFactory;
recordMutatorFactory =
Class.forName(AggregateMutators.class.getPackage().getName() + ".RecordMutatorFactory")
// Instantiate factory via reflection as making it a compile time dependency breaks the r8
// step in the Android build.
Class<? extends MutatorFactory> factory;
factory =
Class.forName(AggregateMutators.class.getPackage().getName() + "." + simpleClassName)
.asSubclass(MutatorFactory.class);
return Stream.of(recordMutatorFactory.getDeclaredConstructor().newInstance());
return factory.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException
| NoSuchMethodException
| InstantiationException
Expand All @@ -53,13 +70,4 @@ private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
throw new IllegalStateException(e);
}
}

private static boolean supportsRecords() {
try {
Class.forName("java.lang.Record");
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ java_library(
"AggregatesHelper.java",
"BeanSupport.java",
"RecordMutatorFactory.java",
"SealedClassMutatorFactory.java",
],
),
visibility = [
Expand All @@ -16,6 +17,7 @@ java_library(
"@platforms//os:android": [],
"//conditions:default": [
":record_mutator_factory",
":sealed_class_mutator_factory",
],
}),
deps = [
Expand All @@ -40,6 +42,20 @@ java_library(
],
)

java_library(
name = "sealed_class_mutator_factory",
srcs = ["SealedClassMutatorFactory.java"],
javacopts = [
"--release",
"17",
],
deps = [
"//src/main/java/com/code_intelligence/jazzer/mutation/api",
"//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
"//src/main/java/com/code_intelligence/jazzer/mutation/support",
],
)

java_library(
name = "aggregates_helper",
srcs = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.aggregate;

import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty;
import static java.util.Arrays.stream;

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 com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators;
import com.code_intelligence.jazzer.mutation.support.TypeSupport;
import java.lang.reflect.AnnotatedType;
import java.util.Optional;
import java.util.function.ToIntFunction;

final class SealedClassMutatorFactory<T> implements MutatorFactory {
@Override
public Optional<SerializingMutator<?>> tryCreate(
AnnotatedType type, ExtendedMutatorFactory factory) {
if (!(type.getType() instanceof Class<?>)) {
return Optional.empty();
}
Class<T>[] permittedSubclasses =
(Class<T>[]) ((Class<T>) type.getType()).getPermittedSubclasses();
if (permittedSubclasses == null) {
return Optional.empty();
}

ToIntFunction<T> getState =
(value) -> {
// We can't use value.getClass() as it might be a subclass of the permitted (direct)
// subclasses.
for (int i = 0; i < permittedSubclasses.length; i++) {
if (permittedSubclasses[i].isInstance(value)) {
return i;
}
}
return -1;
};
return toArrayOrEmpty(
stream(permittedSubclasses)
.map(TypeSupport::asAnnotatedType)
.map(TypeSupport::notNull)
.map(factory::tryCreate),
SerializingMutator<?>[]::new)
.map(
mutators -> MutatorCombinators.mutateSum(getState, (SerializingMutator<T>[]) mutators));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.code_intelligence.jazzer.mutation.annotation.WithLength;
import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.AnnotatedArrayType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedParameterizedType;
Expand Down Expand Up @@ -94,6 +95,65 @@ public static <T> Optional<Class<? extends T>> asSubclassOrEmpty(
return Optional.of(actualClazz.asSubclass(superclass));
}

/**
* Synthesizes an {@link AnnotatedType} for the given {@link Class}.
*
* <p>Usage of this method should be avoided in favor of obtaining annotated types in a natural
* way if possible (e.g. prefer {@link Class#getAnnotatedSuperclass()} to {@link
* Class#getSuperclass()}.
*/
public static AnnotatedType asAnnotatedType(Class<?> clazz) {
requireNonNull(clazz);
return new AnnotatedType() {
@Override
public Type getType() {
return clazz;
}

@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return annotatedElementGetAnnotation(this, annotationClass);
}

@Override
public Annotation[] getAnnotations() {
// No directly present annotations, look for inheritable present annotations on the
// superclass.
if (clazz.getSuperclass() == null) {
return new Annotation[0];
}
return stream(clazz.getSuperclass().getAnnotations())
.filter(
annotation ->
annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null)
.toArray(Annotation[]::new);
}

@Override
public Annotation[] getDeclaredAnnotations() {
// No directly present annotations.
return new Annotation[0];
}

@Override
public String toString() {
return annotatedTypeToString(this);
}

@Override
public int hashCode() {
throw new UnsupportedOperationException(
"hashCode() is not supported as its behavior isn't specified");
}

@Override
public boolean equals(Object obj) {
throw new UnsupportedOperationException(
"equals() is not supported as its behavior isn't specified");
}
};
}

/**
* Visits the individual classes and their directly present annotations that make up the given
* type.
Expand Down
Loading

0 comments on commit 31cff97

Please sign in to comment.