Skip to content

Fuzzer should generate tests for simple collection with generic types #875 #988

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
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 @@ -103,6 +103,8 @@ import kotlinx.coroutines.job
import kotlinx.coroutines.yield
import org.utbot.framework.plugin.api.UtExecutionSuccess
import org.utbot.framework.plugin.api.UtLambdaModel
import org.utbot.framework.plugin.api.util.executable
import org.utbot.fuzzer.toFuzzerType

val logger = KotlinLogging.logger {}
val pathLogger = KotlinLogging.logger(logger.name + ".path")
Expand Down Expand Up @@ -426,6 +428,7 @@ class UtBotSymbolicEngine(
packageName = classUnderTest.packageName
val names = graph.body.method.tags.filterIsInstance<ParamNamesTag>().firstOrNull()?.names
parameterNameMap = { index -> names?.getOrNull(index) }
fuzzerType = { try { toFuzzerType(methodUnderTest.executable.genericParameterTypes[it]) } catch (_: Throwable) { null } }
}
val coveredInstructionTracker = Trie(Instruction::id)
val coveredInstructionValues = linkedMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.utbot.fuzzer

import mu.KotlinLogging
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.classId
import org.utbot.framework.plugin.api.util.booleanClassId
import org.utbot.framework.plugin.api.util.byteClassId
import org.utbot.framework.plugin.api.util.charClassId
import org.utbot.framework.plugin.api.util.doubleClassId
import org.utbot.framework.plugin.api.util.floatClassId
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.intClassId
import org.utbot.framework.plugin.api.util.longClassId
import org.utbot.framework.plugin.api.util.objectClassId
import org.utbot.framework.plugin.api.util.shortClassId
import org.utbot.framework.plugin.api.util.stringClassId
import org.utbot.framework.util.executableId
Expand Down Expand Up @@ -46,6 +49,11 @@ import soot.jimple.internal.JStaticInvokeExpr
import soot.jimple.internal.JTableSwitchStmt
import soot.jimple.internal.JVirtualInvokeExpr
import soot.toolkits.graph.ExceptionalUnitGraph
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.TypeVariable
import java.lang.reflect.WildcardType

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -292,3 +300,23 @@ private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) {
}

private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()

fun toFuzzerType(type: Type): FuzzedType {
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 GenericArrayType -> {
val genericComponentType = type.genericComponentType
val fuzzerType = toFuzzerType(genericComponentType)
val classId = if (genericComponentType !is GenericArrayType) {
ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId)
} else {
ClassId("[" + fuzzerType.classId.name, fuzzerType.classId)
}
FuzzedType(classId)
}
is Class<*> -> FuzzedType(type.id)
else -> error("Unknown type: $type")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ class FuzzedMethodDescription(
*/
var parameterNameMap: (Int) -> String? = { null }

/**
* For every parameter returns a list with acceptable types.
* Usually it keeps upper bound.
*
* Every parameter can have several parameter types.
* For example [Map] has two type parameters, [Collection] has only one.
*
* Fuzzer doesn't care about interconnection between these types, therefore it waits
* that function already has all necessary information to bound this values.
*/
var fuzzerType: (Int) -> FuzzedType? = { null }

/**
* Map class id to indices of this class in parameters list.
*/
Expand Down
20 changes: 20 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.utbot.fuzzer

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,
* but at the moment it doesn't fully support all necessary operations.
*
* @see ClassId.typeParameters
*/
class FuzzedType(
val classId: ClassId,
val generics: List<FuzzedType> = emptyList()
)
57 changes: 40 additions & 17 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.utbot.fuzzer

import mu.KotlinLogging
import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.util.intClassId
import org.utbot.framework.plugin.api.util.voidClassId
import org.utbot.fuzzer.mutators.NumberRandomMutator
import org.utbot.fuzzer.mutators.RegexStringModelMutator
import org.utbot.fuzzer.mutators.StringRandomMutator
import org.utbot.fuzzer.providers.ArrayModelProvider
import org.utbot.fuzzer.providers.CharToStringModelProvider
import org.utbot.fuzzer.providers.CollectionModelProvider
import org.utbot.fuzzer.providers.CollectionWithEmptyStatesModelProvider
import org.utbot.fuzzer.providers.ConstantsModelProvider
import org.utbot.fuzzer.providers.EnumModelProvider
import org.utbot.fuzzer.providers.CollectionWithModificationModelProvider
import org.utbot.fuzzer.providers.NumberClassModelProvider
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
Expand All @@ -19,6 +24,8 @@ import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.random.Random
import org.utbot.fuzzer.providers.DateConstantModelProvider
import org.utbot.fuzzer.providers.PrimitiveRandomModelProvider
import org.utbot.fuzzer.providers.RecursiveModelProvider

private val logger by lazy { KotlinLogging.logger {} }

Expand Down Expand Up @@ -146,46 +153,62 @@ fun <T> Sequence<List<FuzzedValue>>.withMutations(statistics: FuzzerStatistics<T
while (yieldMutated(statistics, description, mutatorList, random)) {}
}

/**
* Create a sequence that contains all [defaultValues] plus any value which is found as fuzzing with concrete values.
*
* Method is useful for generating some bound values,
* for example, when for code `array.length > 4` there are 2 values in concrete value: 4 and 5.
*
* All values after filtering are cast to [Int].
*/
fun fuzzNumbers(concreteValues: Collection<FuzzedConcreteValue>, vararg defaultValues: Int, filter: (Number) -> Boolean = { true }): Sequence<Int> {
val description = FuzzedMethodDescription("helper: number fuzzing", voidClassId, listOf(intClassId), concreteValues)
val fuzzedValues = fuzz(description, ConstantsModelProvider)
.mapNotNull { ((it.single().model as? UtPrimitiveModel)?.value as? Number) }
.filter(filter)
.map { it.toInt() }
return (defaultValues.asSequence() + fuzzedValues).distinct()
}

/**
* Creates a model provider from a list of default providers.
*/
fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): ModelProvider {
return ModelProvider.of(
ObjectModelProvider(idGenerator),
CollectionModelProvider(idGenerator),
ArrayModelProvider(idGenerator),
EnumModelProvider(idGenerator),
DateConstantModelProvider(idGenerator),
ConstantsModelProvider,
StringConstantModelProvider,
RegexModelProvider,
CharToStringModelProvider,
PrimitivesModelProvider,
PrimitiveWrapperModelProvider,
)
return modelProviderForRecursiveCalls(idGenerator, recursionDepth = -1)
.with(ObjectModelProvider(idGenerator))
.with(ArrayModelProvider(idGenerator))
.with(CollectionWithModificationModelProvider(idGenerator))
.exceptIsInstance<PrimitiveRandomModelProvider>()
.except(PrimitiveDefaultsModelProvider)
.with(PrimitivesModelProvider)
}

/**
* Creates a model provider from a list of providers that we want to use by default in [RecursiveModelProvider]
*/
internal fun modelProviderForRecursiveCalls(idGenerator: IdentityPreservingIdGenerator<Int>, recursionDepth: Int): ModelProvider {
val nonRecursiveProviders = ModelProvider.of(
CollectionModelProvider(idGenerator),
CollectionWithEmptyStatesModelProvider(idGenerator),
EnumModelProvider(idGenerator),
DateConstantModelProvider(idGenerator),
RegexModelProvider,
StringConstantModelProvider,
CharToStringModelProvider,
ConstantsModelProvider,
PrimitiveRandomModelProvider(Random(0)),
PrimitiveDefaultsModelProvider,
PrimitiveWrapperModelProvider,
NumberClassModelProvider(idGenerator, Random(0)),
)

return if (recursionDepth >= 0)
return if (recursionDepth >= 0) {
nonRecursiveProviders
.with(ObjectModelProvider(idGenerator, recursionDepth))
.with(ArrayModelProvider(idGenerator, recursionDepth))
else
.with(CollectionWithModificationModelProvider(idGenerator, recursionDepth))
} else {
nonRecursiveProviders
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package org.utbot.fuzzer.objects

import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.ConstructorId
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.UtAssembleModel
import org.utbot.framework.plugin.api.UtExecutableCallModel
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtStatementModel
import org.utbot.framework.plugin.api.util.voidClassId
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.hex
Expand All @@ -19,3 +24,86 @@ fun ModelProvider.assembleModel(id: Int, constructorId: ConstructorId, params: L
summary = "%var% = ${constructorId.classId.simpleName}(${constructorId.parameters.joinToString { it.simpleName }})"
}
}

fun ClassId.create(
block: AssembleModelDsl.() -> Unit
): UtAssembleModel {
return AssembleModelDsl(this).apply(block).build()
}

class AssembleModelDsl internal constructor(
val classId: ClassId
) {
val using = KeyWord.Using
val call = KeyWord.Call
val constructor = KeyWord.Constructor(classId)
val method = KeyWord.Method(classId)

var id: () -> Int? = { null }
var name: (Int?) -> String = { "<dsl generated model>" }

private lateinit var initialization: () -> UtExecutableCallModel
private val modChain = mutableListOf<(UtAssembleModel) -> UtStatementModel>()

fun params(vararg params: ClassId) = params.toList()

fun values(vararg model: UtModel) = model.toList()

infix fun <T : ExecutableId> KeyWord.Using.instance(executableId: T) = UsingDsl(executableId)

infix fun <T : ExecutableId> KeyWord.Call.instance(executableId: T) = CallDsl(executableId, false)

infix fun <T : ExecutableId> KeyWord.Using.static(executableId: T) = UsingDsl(executableId)

infix fun <T : ExecutableId> KeyWord.Call.static(executableId: T) = CallDsl(executableId, true)

infix fun KeyWord.Using.empty(ignored: KeyWord.Constructor) {
initialization = { UtExecutableCallModel(null, ConstructorId(classId, emptyList()), emptyList()) }
}

infix fun ConstructorId.with(models: List<UtModel>) {
initialization = { UtExecutableCallModel(null, this, models) }
}

infix fun UsingDsl.with(models: List<UtModel>) {
initialization = { UtExecutableCallModel(null, executableId, models) }
}

infix fun CallDsl.with(models: List<UtModel>) {
modChain += { UtExecutableCallModel(it, executableId, models.toList()) }
}

internal fun build(): UtAssembleModel {
val objectId = id()
return UtAssembleModel(
id = objectId,
classId = classId,
modelName = name(objectId),
instantiationCall = initialization()
) {
modChain.map { it(this) }
}
}

sealed class KeyWord {
object Using : KeyWord()
object Call : KeyWord()
class Constructor(val classId: ClassId) : KeyWord() {
operator fun invoke(vararg params: ClassId): ConstructorId {
return ConstructorId(classId, params.toList())
}
}
class Method(val classId: ClassId) : KeyWord() {
operator fun invoke(name: String, params: List<ClassId> = emptyList(), returns: ClassId = voidClassId): MethodId {
return MethodId(classId, name, returns, params)
}

operator fun invoke(classId: ClassId, name: String, params: List<ClassId> = emptyList(), returns: ClassId = voidClassId): MethodId {
return MethodId(classId, name, returns, params)
}
}
}

class UsingDsl(val executableId: ExecutableId)
class CallDsl(val executableId: ExecutableId, val isStatic: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,36 @@ package org.utbot.fuzzer.providers
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.UtArrayModel
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.util.defaultValueModel
import org.utbot.framework.plugin.api.util.intClassId
import org.utbot.framework.plugin.api.util.isArray
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedType
import org.utbot.fuzzer.IdentityPreservingIdGenerator
import org.utbot.fuzzer.fuzzNumbers

class ArrayModelProvider(
idGenerator: IdentityPreservingIdGenerator<Int>,
recursionDepthLeft: Int = 1
) : RecursiveModelProvider(idGenerator, recursionDepthLeft) {
override fun createNewInstance(parentProvider: RecursiveModelProvider, newTotalLimit: Int): RecursiveModelProvider =

override fun newInstance(parentProvider: RecursiveModelProvider): RecursiveModelProvider =
ArrayModelProvider(parentProvider.idGenerator, parentProvider.recursionDepthLeft - 1)
.copySettingsFrom(parentProvider)
.apply {
totalLimit = newTotalLimit
if (parentProvider is ArrayModelProvider)
branchingLimit = 1 // This improves performance but also forbids generating "jagged" arrays
}
.copySettings(parentProvider)

override fun generateModelConstructors(
description: FuzzedMethodDescription,
classId: ClassId
): List<ModelConstructor> {
if (!classId.isArray)
return listOf()
val lengths = generateArrayLengths(description)
return lengths.map { length ->
ModelConstructor(List(length) { classId.elementClassId!! }) { values ->
parameterIndex: Int,
classId: ClassId,
): Sequence<ModelConstructor> = sequence {
if (!classId.isArray) return@sequence
val lengths = fuzzNumbers(description.concreteValues, 0, 3) { it in 1..10 }
lengths.forEach { length ->
yield(ModelConstructor(listOf(FuzzedType(classId.elementClassId!!)), repeat = length) { values ->
createFuzzedArrayModel(classId, length, values.map { it.model } )
}
})
}
}

private fun generateArrayLengths(description: FuzzedMethodDescription): List<Int> {
val fuzzedLengths = fuzzValuesRecursively(
types = listOf(intClassId),
baseMethodDescription = description,
modelProvider = ConstantsModelProvider
)

// Firstly we will use most "interesting" default sizes, then we will use small sizes obtained from src
return listOf(3, 0) + fuzzedLengths
.map { (it.single().model as UtPrimitiveModel).value as Int }
.filter { it in 1..10 && it != 3 }
.toSortedSet()
.toList()
}

private fun createFuzzedArrayModel(arrayClassId: ClassId, length: Int, values: List<UtModel>?) =
UtArrayModel(
idGenerator.createId(),
Expand Down
Loading