Skip to content

Pelevin/improve java fuzzing type system #1594

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add IteratorValueProvider and improve Java type system
  • Loading branch information
Markoutte committed Dec 26, 2022
commit e945f86c274edf17b1e66f506ca9872ba341cba5
14 changes: 7 additions & 7 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import org.utbot.framework.plugin.api.ClassId
/**
* Contains information about classId and generic types, that should be applied.
*
* Currently, there's some limitation for generics that are supported:
* 1. Only concrete types and collections are supported
* 2. No relative types like: `Map<T, V extends T>`
*
* Note, that this class can be replaced by API mechanism for collecting parametrized types,
* Note that this class can be replaced by the API mechanism for collecting parametrized types,
* but at the moment it doesn't fully support all necessary operations.
*
* @see ClassId.typeParameters
*/
data class FuzzedType(
class FuzzedType(
val classId: ClassId,
val generics: List<FuzzedType> = emptyList(),
)
) {
override fun toString(): String {
return "FuzzedType(classId=$classId, generics=${generics.map { it.classId }})"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.utbot.fuzzer.IdentityPreservingIdGenerator
import org.utbot.fuzzer.objects.FuzzerMockableMethodId
import org.utbot.fuzzer.objects.assembleModel
import org.utbot.fuzzing.providers.FieldDescription
import org.utbot.fuzzing.providers.findAccessibleModifableFields
import org.utbot.fuzzing.providers.findAccessibleModifiableFields
import org.utbot.fuzzing.providers.isAccessible

/**
Expand Down Expand Up @@ -64,10 +64,10 @@ class ObjectModelProvider(
// and mutate some fields. Only if there's no option next block
// with empty constructor should be used.
if (constructorId.parameters.isEmpty()) {
val fields = findAccessibleModifableFields(constructorId.classId, description.packageName)
val fields = findAccessibleModifiableFields(null, constructorId.classId, description.packageName)
if (fields.isNotEmpty()) {
yield(
ModelConstructor(fields.map { FuzzedType(it.classId) }) {
ModelConstructor(fields.map { it.type }) {
generateModelsWithFieldsInitialization(constructorId, fields, it)
}
)
Expand Down Expand Up @@ -103,7 +103,7 @@ class ObjectModelProvider(
constructorId.classId,
field.setter.name,
field.setter.returnType.id,
listOf(field.classId),
listOf(field.type.classId),
mock = {
field.getter?.let { g ->
val getterMethodID = MethodId(
Expand Down
94 changes: 77 additions & 17 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.utbot.fuzzing.utils.Trie
import java.lang.reflect.*
import java.util.concurrent.TimeUnit
import kotlin.system.measureNanoTime
import java.util.IdentityHashMap

private val logger = KotlinLogging.logger {}

Expand All @@ -19,6 +20,7 @@ typealias JavaValueProvider = ValueProvider<FuzzedType, FuzzedValue, FuzzedDescr
class FuzzedDescription(
val description: FuzzedMethodDescription,
val tracer: Trie<Instruction, *>,
val typeCache: IdentityHashMap<Type, FuzzedType>,
) : Description<FuzzedType>(
description.parameters.mapIndexed { index, classId ->
description.fuzzerType(index) ?: FuzzedType(classId)
Expand All @@ -39,6 +41,7 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator<Int>) = lis
EnumValueProvider(idGenerator),
ListSetValueProvider(idGenerator),
MapValueProvider(idGenerator),
IteratorValueProvider(idGenerator),
EmptyCollectionValueProvider(idGenerator),
DateValueProvider(idGenerator),
// NullValueProvider,
Expand All @@ -57,8 +60,12 @@ suspend fun runJavaFuzzing(
val returnType = methodUnderTest.returnType
val parameters = methodUnderTest.parameters

// For a concrete fuzzing run we need to track types we create.
// Because of generics can be declared as recursive structures like `<T extends Iterable<T>>`,
// we should track them by reference and do not call `equals` and `hashCode` recursively.
val typeCache = IdentityHashMap<Type, FuzzedType>()
/**
* To fuzz this instance the class of it is added into head of parameters list.
* To fuzz this instance, the class of it is added into head of parameters list.
* Done for compatibility with old fuzzer logic and should be reworked more robust way.
*/
fun createFuzzedMethodDescription(self: ClassId?) = FuzzedMethodDescription(
Expand All @@ -79,9 +86,9 @@ suspend fun runJavaFuzzing(
fuzzerType = {
try {
when {
self != null && it == 0 -> toFuzzerType(methodUnderTest.executable.declaringClass)
self != null -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it - 1])
else -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it])
self != null && it == 0 -> toFuzzerType(methodUnderTest.executable.declaringClass, typeCache)
self != null -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it - 1], typeCache)
else -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it], typeCache)
}
} catch (_: Throwable) {
null
Expand All @@ -94,8 +101,8 @@ suspend fun runJavaFuzzing(
if (!isStatic && !isConstructor) { classUnderTest } else { null }
}
val tracer = Trie(Instruction::id)
val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer)
val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer)
val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer, typeCache)
val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer, typeCache)
try {
logger.info { "Starting fuzzing for method: $methodUnderTest" }
logger.info { "\tuse thisInstance = ${thisInstance != null}" }
Expand All @@ -118,22 +125,75 @@ suspend fun runJavaFuzzing(
}
}

private fun toFuzzerType(type: Type): FuzzedType {
/**
* Resolve a fuzzer type that has class info and some generics.
*/
internal fun toFuzzerType(type: Type, cache: IdentityHashMap<Type, FuzzedType>): FuzzedType {
return toFuzzerType(
type = type,
classId = { t -> toClassId(t, cache) },
generics = { t -> toGenerics(t) },
cache = cache
)
}

/**
* Resolve a fuzzer type that has class info and some generics.
*
* Cache is used to stop recursive call in case of some recursive class definition like:
*
* ```
* public <T extends Iterable<T>> call(T type) { ... }
* ```
*
* @param type to be resolved into a fuzzed type.
* @param classId is a function that produces classId by general type.
* @param generics is a function that produced a list of generics for this concrete type.
* @param cache is used to store all generated types.
*/
private fun toFuzzerType(
type: Type,
classId: (type: Type) -> ClassId,
generics: (parent: Type) -> Array<out Type>,
cache: IdentityHashMap<Type, FuzzedType>
): FuzzedType {
val g = mutableListOf<FuzzedType>()
var target = cache[type]
if (target == null) {
target = FuzzedType(classId(type), g)
cache[type] = target
g += generics(type).map {
toFuzzerType(it, classId, generics, cache)
}
}
return target
}

private fun toClassId(type: Type, cache: IdentityHashMap<Type, FuzzedType>): ClassId {
return when (type) {
is WildcardType -> type.upperBounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId)
is TypeVariable<*> -> type.bounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId)
is ParameterizedType -> FuzzedType((type.rawType as Class<*>).id, type.actualTypeArguments.map { toFuzzerType(it) })
is WildcardType -> type.upperBounds.firstOrNull()?.let { toClassId(it, cache) } ?: objectClassId
is TypeVariable<*> -> type.bounds.firstOrNull()?.let { toClassId(it, cache) } ?: objectClassId
is GenericArrayType -> {
val genericComponentType = type.genericComponentType
val fuzzerType = toFuzzerType(genericComponentType)
val classId = if (genericComponentType !is GenericArrayType) {
ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId)
val classId = toClassId(genericComponentType, cache)
if (genericComponentType !is GenericArrayType) {
ClassId("[L${classId.name};", classId)
} else {
ClassId("[" + fuzzerType.classId.name, fuzzerType.classId)
ClassId("[" + classId.name, classId)
}
FuzzedType(classId)
}
is Class<*> -> FuzzedType(type.id, type.typeParameters.map { toFuzzerType(it) })
else -> error("Unknown type: $type")
is ParameterizedType -> (type.rawType as Class<*>).id
is Class<*> -> type.id
else -> error("unknown type: $type")
}
}

private fun toGenerics(t: Type) : Array<out Type> {
return when (t) {
is TypeVariable<*> -> arrayOf(t.bounds.firstOrNull() ?: java.lang.Object::class.java)
is GenericArrayType -> toGenerics(t.genericComponentType)
is ParameterizedType -> t.actualTypeArguments
is Class<*> -> t.typeParameters
else -> emptyArray()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import org.utbot.fuzzer.FuzzedType
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.IdGenerator
import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed
import org.utbot.fuzzing.FuzzedDescription
import org.utbot.fuzzing.Routine
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.ValueProvider
import org.utbot.fuzzing.*

class ArrayValueProvider(
val idGenerator: IdGenerator<Int>,
Expand All @@ -35,9 +32,28 @@ class ArrayValueProvider(
summary = "%var% = ${type.classId.elementClassId!!.simpleName}[$it]"
}
},
modify = Routine.ForEach(listOf(FuzzedType(type.classId.elementClassId!!))) { self, i, values ->
modify = Routine.ForEach(listOf(resolveArrayElementType(type))) { self, i, values ->
(self.model as UtArrayModel).stores[i] = values.first().model
}
))
}

/**
* Resolves array's element type. In case a generic type is used, like `T[]` the type should pass generics.
*
* For example, List<Number>[] is the target type to create, therefore the element type is set according
* to the generic type: List, that has only one generic type: Number.
*/
private fun resolveArrayElementType(arrayType: FuzzedType): FuzzedType {
if (!arrayType.classId.isArray) error("$arrayType is not array")
val elementClassId = arrayType.classId.elementClassId ?: error("Element classId of $arrayType is not found")
return if (arrayType.generics.size == 1) {
assert(elementClassId == arrayType.generics.first().classId)
FuzzedType(elementClassId, arrayType.generics.first().generics)
} else if (arrayType.generics.isEmpty()) {
FuzzedType(elementClassId)
} else {
error("Array has ${arrayType.generics.size} generic type for ($arrayType), that should not happen")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import org.utbot.fuzzer.FuzzedType
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.IdGenerator
import org.utbot.fuzzer.providers.PrimitivesModelProvider.fuzzed
import org.utbot.fuzzing.FuzzedDescription
import org.utbot.fuzzing.Routine
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.ValueProvider
import org.utbot.fuzzing.*
import org.utbot.fuzzing.utils.hex
import kotlin.reflect.KClass

class EmptyCollectionValueProvider(
Expand Down Expand Up @@ -205,4 +203,37 @@ abstract class CollectionValueProvider(
// return isSubtypeOf(another)
return klass.java.isAssignableFrom(this.jClass)
}
}

class IteratorValueProvider(val idGenerator: IdGenerator<Int>) : ValueProvider<FuzzedType, FuzzedValue, FuzzedDescription> {
override fun accept(type: FuzzedType): Boolean {
return type.classId == Iterator::class.id
}

override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> {
val generic = type.generics.firstOrNull() ?: FuzzedType(objectClassId)
return sequenceOf(Seed.Recursive(
construct = Routine.Create(listOf(FuzzedType(iterableClassId, listOf(generic)))) { v ->
val id = idGenerator.createId()
val iterable = when (val model = v.first().model) {
is UtAssembleModel -> model
is UtNullModel -> return@Create v.first()
else -> error("Model can be only UtNullModel or UtAssembleModel, but $model is met")
}
UtAssembleModel(
id = id,
classId = type.classId,
modelName = "iterator#${id.hex()}",
instantiationCall = UtExecutableCallModel(
instance = iterable,
executable = MethodId(iterableClassId, "iterator", type.classId, emptyList()),
params = emptyList()
)
).fuzzed {
summary = "%var% = ${iterable.classId.simpleName}#iterator()"
}
},
empty = Routine.Empty { UtNullModel(type.classId).fuzzed { summary = "%var% = null" } }
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import java.lang.reflect.Field
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.*

class ObjectValueProvider(
val idGenerator: IdGenerator<Int>,
Expand Down Expand Up @@ -43,7 +44,9 @@ class ObjectValueProvider(

private fun createValue(classId: ClassId, constructorId: ConstructorId, description: FuzzedDescription): Seed.Recursive<FuzzedType, FuzzedValue> {
return Seed.Recursive(
construct = Routine.Create(constructorId.parameters.map { FuzzedType(it) }) { values ->
construct = Routine.Create(constructorId.executable.genericParameterTypes.map {
toFuzzerType(it, description.typeCache)
}) { values ->
val id = idGenerator.createId()
UtAssembleModel(
id = id,
Expand All @@ -59,10 +62,10 @@ class ObjectValueProvider(
}
},
modify = sequence {
findAccessibleModifableFields(classId, description.description.packageName).forEach { fd ->
findAccessibleModifiableFields(description, classId, description.description.packageName).forEach { fd ->
when {
fd.canBeSetDirectly -> {
yield(Routine.Call(listOf(FuzzedType(fd.classId))) { self, values ->
yield(Routine.Call(listOf(fd.type)) { self, values ->
val model = self.model as UtAssembleModel
model.modificationsChain as MutableList += UtDirectSetFieldModel(
model,
Expand All @@ -73,7 +76,7 @@ class ObjectValueProvider(
}

fd.setter != null && fd.getter != null -> {
yield(Routine.Call(listOf(FuzzedType(fd.classId))) { self, values ->
yield(Routine.Call(listOf(fd.type)) { self, values ->
val model = self.model as UtAssembleModel
model.modificationsChain as MutableList += UtExecutableCallModel(
model,
Expand Down Expand Up @@ -143,19 +146,19 @@ internal class PublicSetterGetter(

internal class FieldDescription(
val name: String,
val classId: ClassId,
val type: FuzzedType,
val canBeSetDirectly: Boolean,
val setter: Method?,
val getter: Method?
)

internal fun findAccessibleModifableFields(classId: ClassId, packageName: String?): List<FieldDescription> {
internal fun findAccessibleModifiableFields(description: FuzzedDescription?, classId: ClassId, packageName: String?): List<FieldDescription> {
val jClass = classId.jClass
return jClass.declaredFields.map { field ->
val setterAndGetter = jClass.findPublicSetterGetterIfHasPublicGetter(field, packageName)
FieldDescription(
name = field.name,
classId = field.type.id,
type = if (description != null) toFuzzerType(field.type, description.typeCache) else FuzzedType(field.type.id),
canBeSetDirectly = isAccessible(
field,
packageName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ public class Stubs {

public static void name(String value) {}

public static <T extends Iterable<T>> int resolve(T recursive) {
int result = 0;
for (T t : recursive) {
result++;
}
return result;
}
}
Loading