diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt index 4eb56f7ba2..d344ed6c72 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -439,7 +439,7 @@ class UtBotSymbolicEngine( packageName = executableId.classId.packageName } fuzz(thisMethodDescription, ObjectModelProvider(defaultIdGenerator).apply { - limitValuesCreatedByFieldAccessors = 500 + totalLimit = 500 }) }.withMutations( TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray() diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt index 392d9cbb4d..6b6d813af4 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -164,22 +164,28 @@ fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator): Mode } /** - * Creates a model provider for [ObjectModelProvider] that generates values for object constructor. + * Creates a model provider from a list of providers that we want to use by default in [RecursiveModelProvider] */ -fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator): ModelProvider { - return ModelProvider.of( +internal fun modelProviderForRecursiveCalls(idGenerator: IdentityPreservingIdGenerator, recursionDepth: Int): ModelProvider { + val nonRecursiveProviders = ModelProvider.of( CollectionModelProvider(idGenerator), - ArrayModelProvider(idGenerator), EnumModelProvider(idGenerator), StringConstantModelProvider, - RegexModelProvider, CharToStringModelProvider, ConstantsModelProvider, PrimitiveDefaultsModelProvider, PrimitiveWrapperModelProvider, ) + + return if (recursionDepth >= 0) + nonRecursiveProviders + .with(ObjectModelProvider(idGenerator, recursionDepth)) + .with(ArrayModelProvider(idGenerator, recursionDepth)) + else + nonRecursiveProviders } + fun defaultModelMutators(): List = listOf( StringRandomMutator, RegexStringModelMutator, diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt index 42e6eadc5b..4b28dc0dcf 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt @@ -41,6 +41,17 @@ fun interface ModelProvider { } } + /** + * Applies [transform] for current provider + */ + fun map(transform: (ModelProvider) -> ModelProvider): ModelProvider { + return if (this is Combined) { + Combined(providers.map(transform)) + } else { + transform(this) + } + } + /** * Creates [ModelProvider] that passes unprocessed classes to `modelProvider`. * @@ -123,7 +134,19 @@ fun interface ModelProvider { /** * Wrapper class that delegates implementation to the [providers]. */ - private class Combined(val providers: List): ModelProvider { + private class Combined(providers: List): ModelProvider { + val providers: List + + init { + // Flattening to avoid Combined inside Combined (for correct work of except, map, etc.) + this.providers = providers.flatMap { + if (it is Combined) + it.providers + else + listOf(it) + } + } + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { providers.forEach { provider -> provider.generate(description).forEach { diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt index 02767a832b..394344e3a7 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt @@ -1,34 +1,65 @@ 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.FuzzedParameter -import org.utbot.fuzzer.IdGenerator -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import java.util.function.IntSupplier +import org.utbot.fuzzer.IdentityPreservingIdGenerator class ArrayModelProvider( - private val idGenerator: IdGenerator -) : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap - .asSequence() - .filter { (classId, _) -> classId.isArray } - .forEach { (arrayClassId, indices) -> - yieldAllValues(indices, listOf(0, 10).map { arraySize -> - UtArrayModel( - id = idGenerator.createId(), - arrayClassId, - length = arraySize, - arrayClassId.elementClassId!!.defaultValueModel(), - mutableMapOf() - ).fuzzed { - this.summary = "%var% = ${arrayClassId.elementClassId!!.simpleName}[$arraySize]" - } - }) + idGenerator: IdentityPreservingIdGenerator, + recursionDepthLeft: Int = 1 +) : RecursiveModelProvider(idGenerator, recursionDepthLeft) { + override fun createNewInstance(parentProvider: RecursiveModelProvider, newTotalLimit: Int): 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 } + + override fun generateModelConstructors( + description: FuzzedMethodDescription, + classId: ClassId + ): List { + if (!classId.isArray) + return listOf() + val lengths = generateArrayLengths(description) + return lengths.map { length -> + ModelConstructor(List(length) { classId.elementClassId!! }) { values -> + createFuzzedArrayModel(classId, length, values.map { it.model } ) + } + } + } + + private fun generateArrayLengths(description: FuzzedMethodDescription): List { + 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?) = + UtArrayModel( + idGenerator.createId(), + arrayClassId, + length, + arrayClassId.elementClassId!!.defaultValueModel(), + values?.withIndex()?.associate { it.index to it.value }?.toMutableMap() ?: mutableMapOf() + ).fuzzed { + this.summary = "%var% = ${arrayClassId.elementClassId!!.simpleName}[$length]" + } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt index 4a45f34257..628e680988 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt @@ -1,6 +1,5 @@ package org.utbot.fuzzer.providers -import mu.KotlinLogging import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.FieldId @@ -14,18 +13,9 @@ import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.isPrimitiveWrapper import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.plugin.api.util.stringClassId -import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.FuzzedConcreteValue import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue -import org.utbot.fuzzer.TooManyCombinationsException -import org.utbot.fuzzer.exceptIsInstance -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.objectModelProviders import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed import java.lang.reflect.Constructor import java.lang.reflect.Field @@ -33,106 +23,88 @@ import java.lang.reflect.Member import java.lang.reflect.Method import java.lang.reflect.Modifier.* -private val logger by lazy { KotlinLogging.logger {} } - /** - * Creates [UtAssembleModel] for objects which have public constructors with primitives types and String as parameters. + * Creates [UtAssembleModel] for objects which have public constructors */ class ObjectModelProvider( - private val idGenerator: IdentityPreservingIdGenerator, - private val limit: Int = Int.MAX_VALUE, - private val recursion: Int = 1 -) : ModelProvider { + idGenerator: IdentityPreservingIdGenerator, + recursionDepthLeft: Int = 1, +) : RecursiveModelProvider(idGenerator, recursionDepthLeft) { + override fun createNewInstance(parentProvider: RecursiveModelProvider, newTotalLimit: Int): RecursiveModelProvider = + ObjectModelProvider(parentProvider.idGenerator, parentProvider.recursionDepthLeft - 1) + .copySettingsFrom(parentProvider) + .apply { + totalLimit = newTotalLimit + branchingLimit = 1 // When called recursively, we will use only simplest constructor + } - var modelProvider: ModelProvider = objectModelProviders(idGenerator) - var limitValuesCreatedByFieldAccessors: Int = 100 - set(value) { - field = maxOf(0, value) - } + override fun generateModelConstructors( + description: FuzzedMethodDescription, + classId: ClassId + ): List { + if (classId == stringClassId || classId.isPrimitiveWrapper) + return listOf() + + val constructors = collectConstructors(classId) { javaConstructor -> + isAccessible(javaConstructor, description.packageName) + }.sortedWith( + primitiveParameterizedConstructorsFirstAndThenByParameterCount + ) - private val nonRecursiveModelProvider: ModelProvider - get() { - val modelProviderWithoutRecursion = modelProvider.exceptIsInstance() - return if (recursion > 0) { - ObjectModelProvider(idGenerator, limit = 1, recursion - 1).with(modelProviderWithoutRecursion) - } else { - modelProviderWithoutRecursion.withFallback(NullModelProvider) - } - } + return buildList { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - val fuzzedValues = with(description) { - parameters.asSequence() - .filterNot { it == stringClassId || it.isPrimitiveWrapper } - .flatMap { classId -> - collectConstructors(classId) { javaConstructor -> - isAccessible(javaConstructor, description.packageName) - }.sortedWith( - primitiveParameterizedConstructorsFirstAndThenByParameterCount - ).take(limit) - } - .associateWith { constructorId -> - fuzzParameters( - constructorId, - nonRecursiveModelProvider + constructors.forEach { constructorId -> + with(constructorId) { + add( + ModelConstructor(parameters) { assembleModel(idGenerator.createId(), constructorId, it) } ) - } - .flatMap { (constructorId, fuzzedParameters) -> - if (constructorId.parameters.isEmpty()) { - sequenceOf(assembleModel(idGenerator.createId(), constructorId, emptyList())) + - generateModelsWithFieldsInitialization(constructorId, description, concreteValues) - } - else { - fuzzedParameters.map { params -> - assembleModel(idGenerator.createId(), constructorId, params) + if (parameters.isEmpty()) { + val fields = findSuitableFields(this.classId, description) + if (fields.isNotEmpty()) { + add( + ModelConstructor(fields.map { it.classId }) { + generateModelsWithFieldsInitialization(this, fields, it) + } + ) } } } - } - - fuzzedValues.forEach { fuzzedValue -> - description.parametersMap[fuzzedValue.model.classId]?.forEach { index -> - yieldValue(index, fuzzedValue) } } } - private fun generateModelsWithFieldsInitialization(constructorId: ConstructorId, description: FuzzedMethodDescription, concreteValues: Collection): Sequence { - if (limitValuesCreatedByFieldAccessors == 0) return emptySequence() - val fields = findSuitableFields(constructorId.classId, description) - val syntheticClassFieldsSetterMethodDescription = FuzzedMethodDescription( - "${constructorId.classId.simpleName}", - voidClassId, - fields.map { it.classId }, - concreteValues - ).apply { - packageName = description.packageName - } - - return fuzz(syntheticClassFieldsSetterMethodDescription, nonRecursiveModelProvider) - .take(limitValuesCreatedByFieldAccessors) // limit the number of fuzzed values in this particular case - .map { fieldValues -> - val fuzzedModel = assembleModel(idGenerator.createId(), constructorId, emptyList()) - val assembleModel = fuzzedModel.model as? UtAssembleModel ?: error("Expected UtAssembleModel but ${fuzzedModel.model::class.java} found") - val modificationChain = assembleModel.modificationsChain as? MutableList ?: error("Modification chain must be mutable") - fieldValues.asSequence().mapIndexedNotNull { index, value -> - val field = fields[index] - when { - field.canBeSetDirectly -> UtDirectSetFieldModel( - fuzzedModel.model, - FieldId(constructorId.classId, field.name), - value.model - ) - field.setter != null -> UtExecutableCallModel( - fuzzedModel.model, - MethodId(constructorId.classId, field.setter.name, field.setter.returnType.id, listOf(field.classId)), - listOf(value.model) - ) - else -> null - } - }.forEach(modificationChain::add) - fuzzedModel + private fun generateModelsWithFieldsInitialization( + constructorId: ConstructorId, + fields: List, + fieldValues: List + ): FuzzedValue { + val fuzzedModel = assembleModel(idGenerator.createId(), constructorId, emptyList()) + val assembleModel = fuzzedModel.model as? UtAssembleModel + ?: error("Expected UtAssembleModel but ${fuzzedModel.model::class.java} found") + val modificationChain = + assembleModel.modificationsChain as? MutableList ?: error("Modification chain must be mutable") + fieldValues.asSequence().mapIndexedNotNull { index, value -> + val field = fields[index] + when { + field.canBeSetDirectly -> UtDirectSetFieldModel( + fuzzedModel.model, + FieldId(constructorId.classId, field.name), + value.model + ) + field.setter != null -> UtExecutableCallModel( + fuzzedModel.model, + MethodId( + constructorId.classId, + field.setter.name, + field.setter.returnType.id, + listOf(field.classId) + ), + listOf(value.model) + ) + else -> null } + }.forEach(modificationChain::add) + return fuzzedModel } companion object { @@ -156,21 +128,6 @@ class ObjectModelProvider( return !hasAnyAccessModifier } - private fun FuzzedMethodDescription.fuzzParameters(constructorId: ConstructorId, vararg modelProviders: ModelProvider): Sequence> { - val fuzzedMethod = FuzzedMethodDescription( - executableId = constructorId, - concreteValues = this.concreteValues - ).apply { - this.packageName = this@fuzzParameters.packageName - } - return try { - fuzz(fuzzedMethod, *modelProviders) - } catch (t: TooManyCombinationsException) { - logger.warn(t) { "Number of combination of ${parameters.size} parameters is huge. Fuzzing is skipped for $name" } - emptySequence() - } - } - private fun assembleModel(id: Int, constructorId: ConstructorId, params: List): FuzzedValue { val instantiationChain = mutableListOf() return UtAssembleModel( @@ -231,4 +188,4 @@ class ObjectModelProvider( val setter: Method?, ) } -} +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt new file mode 100644 index 0000000000..e74bcf1670 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt @@ -0,0 +1,117 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdentityPreservingIdGenerator +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues +import org.utbot.fuzzer.exceptIsInstance +import org.utbot.fuzzer.fuzz +import org.utbot.fuzzer.modelProviderForRecursiveCalls + +/** + * Auxiliary data class that stores information describing how to construct model from parts (submodels) + * + * @param neededTypes list containing type ([ClassId]) of each submodel + * @param createModel lambda that takes subModels (they should have types listed in [neededTypes]) and generates a model using them + */ +data class ModelConstructor( + val neededTypes: List, + val createModel: (subModels: List) -> FuzzedValue +) + +/** + * Abstraction for providers that may call other providers recursively inside them. [generate] will firstly get possible + * model constructors (provided by [generateModelConstructors]) and then fuzz parameters for each of them using synthetic method + * + * @param recursionDepthLeft maximum recursion level, i.e. maximum number of nested calls produced by this provider + * + * @property modelProviderForRecursiveCalls providers that can be called by this provider. + * Note that if [modelProviderForRecursiveCalls] has instances of [RecursiveModelProvider] then this provider will use + * their copies created by [createNewInstance] rather than themselves (this is important because we have to change some + * properties like [recursionDepthLeft], [totalLimit], etc.) + * + * @property fallbackProvider provider that will be used instead [modelProviderForRecursiveCalls] after reaching maximum recursion level + * + * @property totalLimit maximum number of parameters produced by this provider + * + * @property branchingLimit maximum number of [ModelConstructor]s used by [generate] (see [generateModelConstructors]) + */ +abstract class RecursiveModelProvider( + val idGenerator: IdentityPreservingIdGenerator, + val recursionDepthLeft: Int +) : ModelProvider { + var modelProviderForRecursiveCalls: ModelProvider = modelProviderForRecursiveCalls(idGenerator, recursionDepthLeft - 1) + + var fallbackProvider: ModelProvider = NullModelProvider + + var totalLimit: Int = 1000 + + var branchingLimit: Int = Int.MAX_VALUE + + private fun getModelProvider(numOfBranches: Int): ModelProvider = + if (recursionDepthLeft > 0) + modelProviderForRecursiveCalls.map { + if (it is RecursiveModelProvider) + it.createNewInstance(this, totalLimit / numOfBranches) + else + it + } + else + modelProviderForRecursiveCalls + .exceptIsInstance() + .withFallback(fallbackProvider) + + /** + * Creates instance of the class on which it is called, assuming that it will be called recursively from [parentProvider] + */ + protected abstract fun createNewInstance(parentProvider: RecursiveModelProvider, newTotalLimit: Int): RecursiveModelProvider + + /** + * Creates [ModelProvider]s that will be used to generate values recursively. The order of elements in returned list is important: + * only first [branchingLimit] constructors will be used, so you should place most effective providers first + */ + protected abstract fun generateModelConstructors(description: FuzzedMethodDescription, classId: ClassId): List + + protected fun copySettingsFrom(otherProvider: RecursiveModelProvider): RecursiveModelProvider { + modelProviderForRecursiveCalls = otherProvider.modelProviderForRecursiveCalls + fallbackProvider = otherProvider.fallbackProvider + totalLimit = otherProvider.totalLimit + branchingLimit = otherProvider.branchingLimit + return this + } + + final override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.parametersMap.asSequence().forEach { (classId, indices) -> + val constructors = generateModelConstructors(description, classId).take(branchingLimit) + constructors.asSequence().forEach { constructor -> + val modelProvider = getModelProvider(constructors.size) + val valuesSets = + fuzzValuesRecursively(constructor.neededTypes, description, modelProvider) + .take(totalLimit / constructors.size) + yieldAllValues(indices, valuesSets.map(constructor.createModel)) + } + } + }.take(totalLimit) + + protected fun fuzzValuesRecursively( + types: List, + baseMethodDescription: FuzzedMethodDescription, + modelProvider: ModelProvider, + ): Sequence> { + if (types.isEmpty()) + return sequenceOf(listOf()) + val syntheticMethodDescription = FuzzedMethodDescription( + "", //TODO: maybe add more info here + voidClassId, + types, + baseMethodDescription.concreteValues + ).apply { + packageName = baseMethodDescription.packageName + } + return fuzz(syntheticMethodDescription, modelProvider) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt index a5dd58e8cf..b7d0320f9d 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt @@ -196,7 +196,7 @@ class ModelProviderTest { val classId = A::class.java.id val models = collect( ObjectModelProvider(ReferencePreservingIntIdGenerator(0)).apply { - modelProvider = ModelProvider.of(PrimitiveDefaultsModelProvider) + modelProviderForRecursiveCalls = ModelProvider.of(PrimitiveDefaultsModelProvider) }, parameters = listOf(classId) ) @@ -482,7 +482,7 @@ class ModelProviderTest { withUtContext(UtContext(this::class.java.classLoader)) { val result = collect(ObjectModelProvider(ReferencePreservingIntIdGenerator(0)).apply { - modelProvider = PrimitiveDefaultsModelProvider + modelProviderForRecursiveCalls = PrimitiveDefaultsModelProvider }, parameters = listOf(FieldSetterClass::class.java.id)) assertEquals(1, result.size) assertEquals(2, result[0]!!.size) @@ -510,7 +510,7 @@ class ModelProviderTest { withUtContext(UtContext(this::class.java.classLoader)) { val result = collect(ObjectModelProvider(ReferencePreservingIntIdGenerator(0)).apply { - modelProvider = PrimitiveDefaultsModelProvider + modelProviderForRecursiveCalls = PrimitiveDefaultsModelProvider }, parameters = listOf(PackagePrivateFieldAndClass::class.java.id)) { packageName = PackagePrivateFieldAndClass::class.java.`package`.name }