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 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
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ fun collectConstantsForFuzzer(graph: ExceptionalUnitGraph): Set<FuzzedConcreteVa
try {
finder.find(graph, unit, value)
} catch (e: Exception) {
logger.warn(e) { "Cannot process constant value of type '${value.type}}'" }
// ignore known values that don't have a value field and can be met
if (value !is NullConstant) {
logger.warn(e) { "Cannot process constant value of type '${value.type}'" }
}
emptyList()
}
}.let { result ->
Expand Down
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
@@ -0,0 +1,7 @@
/**
* @deprecated Code is migrating to the new fuzzing platform.
* @see org.utbot.fuzzing.Fuzzing
* @see org.utbot.fuzzing.JavaLanguageKt#runJavaFuzzing
*/
@Deprecated
package org.utbot.fuzzer;
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
109 changes: 91 additions & 18 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 kotlin.random.Random

private val logger = KotlinLogging.logger {}

Expand All @@ -19,6 +20,8 @@ typealias JavaValueProvider = ValueProvider<FuzzedType, FuzzedValue, FuzzedDescr
class FuzzedDescription(
val description: FuzzedMethodDescription,
val tracer: Trie<Instruction, *>,
val typeCache: MutableMap<Type, FuzzedType>,
val random: Random,
) : Description<FuzzedType>(
description.parameters.mapIndexed { index, classId ->
description.fuzzerType(index) ?: FuzzedType(classId)
Expand All @@ -39,6 +42,7 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator<Int>) = lis
EnumValueProvider(idGenerator),
ListSetValueProvider(idGenerator),
MapValueProvider(idGenerator),
IteratorValueProvider(idGenerator),
EmptyCollectionValueProvider(idGenerator),
DateValueProvider(idGenerator),
// NullValueProvider,
Expand All @@ -52,13 +56,18 @@ suspend fun runJavaFuzzing(
providers: List<ValueProvider<FuzzedType, FuzzedValue, FuzzedDescription>> = defaultValueProviders(idGenerator),
exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List<FuzzedValue>) -> BaseFeedback<Trie.Node<Instruction>, FuzzedType, FuzzedValue>
) {
val random = Random(0)
val classUnderTest = methodUnderTest.classId
val name = methodUnderTest.classId.simpleName + "." + methodUnderTest.name
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 = hashMapOf<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 +88,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,15 +103,15 @@ 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, random)
val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer, typeCache, random)
try {
logger.info { "Starting fuzzing for method: $methodUnderTest" }
logger.info { "\tuse thisInstance = ${thisInstance != null}" }
logger.info { "\tparameters = $parameters" }
var totalExecutionCalled = 0
val totalFuzzingTime = measureNanoTime {
runFuzzing(ValueProvider.of(providers), descriptionWithOptionalThisInstance) { _, t ->
runFuzzing(ValueProvider.of(providers), descriptionWithOptionalThisInstance, random) { _, t ->
totalExecutionCalled++
if (thisInstance == null) {
exec(null, descriptionWithOnlyParameters, t)
Expand All @@ -118,22 +127,86 @@ suspend fun runJavaFuzzing(
}
}

private fun toFuzzerType(type: Type): FuzzedType {
/**
* Resolve a fuzzer type that has class info and some generics.
*
* @param type to be resolved
* @param cache is used to store same [FuzzedType] for same java types
*/
internal fun toFuzzerType(type: Type, cache: MutableMap<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: MutableMap<Type, FuzzedType>
): FuzzedType {
val g = mutableListOf<FuzzedType>()
val t = type.replaceWithUpperBoundUntilNotTypeVariable()
var target = cache[t]
if (target == null) {
target = FuzzedType(classId(t), g)
cache[t] = target
g += generics(t).map {
toFuzzerType(it, classId, generics, cache)
}
}
return target
}

internal fun Type.replaceWithUpperBoundUntilNotTypeVariable() : Type {
var type: Type = this
while (type is TypeVariable<*>) {
type = type.bounds.firstOrNull() ?: java.lang.Object::class.java
}
return type
}

private fun toClassId(type: Type, cache: MutableMap<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 GenericArrayType -> {
val genericComponentType = type.genericComponentType
val fuzzerType = toFuzzerType(genericComponentType)
val classId = if (genericComponentType !is GenericArrayType) {
ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId)
val classId = toFuzzerType(genericComponentType, cache).classId
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 WildcardType -> t.upperBounds.firstOrNull()?.let { toGenerics(it) } ?: emptyArray()
is GenericArrayType -> arrayOf(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,24 @@ 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>[] returns List<Number>.
*/
private fun resolveArrayElementType(arrayType: FuzzedType): FuzzedType = when {
!arrayType.classId.isArray -> error("$arrayType is not array")
arrayType.generics.size == 1 -> arrayType.generics.first()
arrayType.generics.isEmpty() -> FuzzedType(
arrayType.classId.elementClassId ?: error("Element classId of $arrayType is not found")
)

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 @@ -56,10 +54,25 @@ class EmptyCollectionValueProvider(
classId = classId,
modelName = "",
instantiationCall = UtExecutableCallModel(null, executableId, value.map { it.model })
).fuzzed {
summary = "%var% = ${executableId.classId.simpleName}#${executableId.name}"
}
},
empty = Routine.Empty {
if (executableId.parameters.isEmpty()) {
UtAssembleModel(
id = idGenerator.createId(),
classId = classId,
modelName = "",
instantiationCall = UtExecutableCallModel(null, executableId, emptyList())

).fuzzed()
).fuzzed{
summary = "%var% = ${executableId.classId.simpleName}#${executableId.name}"
}
} else {
UtNullModel(classId).fuzzed { summary = "%var% = null" }
}
},
empty = Routine.Empty { UtNullModel(classId).fuzzed { summary = "%var% = null" } }
))
}
}
Expand Down Expand Up @@ -205,4 +218,51 @@ 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 {
val id = idGenerator.createId()
UtAssembleModel(
id = id,
classId = type.classId,
modelName = "emptyIterator#${id.hex()}",
instantiationCall = UtExecutableCallModel(
instance = null,
executable = MethodId(java.util.Collections::class.id, "emptyIterator", type.classId, emptyList()),
params = emptyList()
)
).fuzzed {
summary = "%var% = empty iterator"
}
}
))
}
}
Loading