Skip to content

Commit e29923f

Browse files
committed
Fuzzer refactoring: id generator interface and caching id generator
A custom id generator interface hierarchy is used instead of `IntSupplier`: * `IdGenerator` that allows to create fresh identifiers, * `IdentityPreservingIdGenerator`: `IdGenerator` that can return the same id for the same object. A default implementation of `IdentityPreservingIdGenerator` is used in fuzzer. It uses reference equality for object comparison, that allows to create distinct models of equal object (in `equals` sense), and to always assign the same id to the same enum value.
1 parent 0130eff commit e29923f

File tree

10 files changed

+260
-92
lines changed

10 files changed

+260
-92
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt

+8
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ val ClassId.isFloatType: Boolean
117117
val ClassId.isDoubleType: Boolean
118118
get() = this == doubleClassId || this == doubleWrapperClassId
119119

120+
val ClassId.isClassType: Boolean
121+
get() = this == classClassId
122+
120123
val voidClassId = ClassId("void")
121124
val booleanClassId = ClassId("boolean")
122125
val byteClassId = ClassId("byte")
@@ -138,6 +141,8 @@ val longWrapperClassId = java.lang.Long::class.id
138141
val floatWrapperClassId = java.lang.Float::class.id
139142
val doubleWrapperClassId = java.lang.Double::class.id
140143

144+
val classClassId = java.lang.Class::class.id
145+
141146
// We consider void wrapper as primitive wrapper
142147
// because voidClassId is considered primitive here
143148
val primitiveWrappers = setOf(
@@ -285,6 +290,9 @@ val ClassId.isMap: Boolean
285290
val ClassId.isIterableOrMap: Boolean
286291
get() = isIterable || isMap
287292

293+
val ClassId.isEnum: Boolean
294+
get() = jClass.isEnum
295+
288296
fun ClassId.findFieldByIdOrNull(fieldId: FieldId): Field? {
289297
if (isNotSubtypeOf(fieldId.declaringClass)) {
290298
return null

utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import org.utbot.framework.plugin.api.util.utContext
8282
import org.utbot.framework.plugin.api.util.description
8383
import org.utbot.framework.util.jimpleBody
8484
import org.utbot.framework.plugin.api.util.voidClassId
85+
import org.utbot.fuzzer.ReferencePreservingIntIdGenerator
8586
import org.utbot.fuzzer.FallbackModelProvider
8687
import org.utbot.fuzzer.FuzzedMethodDescription
8788
import org.utbot.fuzzer.FuzzedValue
@@ -114,8 +115,7 @@ class EngineController {
114115
//for debugging purpose only
115116
private var stateSelectedCount = 0
116117

117-
//all id values of synthetic default models must be greater that for real ones
118-
private var nextDefaultModelId = 1500_000_000
118+
private val defaultIdGenerator = ReferencePreservingIntIdGenerator()
119119

120120
private fun pathSelector(graph: InterProceduralUnitGraph, typeRegistry: TypeRegistry) =
121121
when (pathSelectorType) {
@@ -391,7 +391,7 @@ class UtBotSymbolicEngine(
391391
return@flow
392392
}
393393

394-
val fallbackModelProvider = FallbackModelProvider { nextDefaultModelId++ }
394+
val fallbackModelProvider = FallbackModelProvider(defaultIdGenerator)
395395
val constantValues = collectConstantsForFuzzer(graph)
396396

397397
val thisInstance = when {
@@ -405,7 +405,7 @@ class UtBotSymbolicEngine(
405405
null
406406
}
407407
else -> {
408-
ObjectModelProvider { nextDefaultModelId++ }.withFallback(fallbackModelProvider).generate(
408+
ObjectModelProvider(ReferencePreservingIntIdGenerator()).withFallback(fallbackModelProvider).generate(
409409
FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues)
410410
).take(10).shuffled(Random(0)).map { it.value.model }.first().apply {
411411
if (this is UtNullModel) { // it will definitely fail because of NPE,
@@ -427,14 +427,14 @@ class UtBotSymbolicEngine(
427427
var attempts = UtSettings.fuzzingMaxAttempts
428428
val hasMethodUnderTestParametersToFuzz = executableId.parameters.isNotEmpty()
429429
val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) {
430-
fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders { nextDefaultModelId++ }))
430+
fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders(defaultIdGenerator)))
431431
} else {
432432
// in case a method with no parameters is passed fuzzing tries to fuzz this instance with different constructors, setters and field mutators
433433
val thisMethodDescription = FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues).apply {
434434
className = executableId.classId.simpleName
435435
packageName = executableId.classId.packageName
436436
}
437-
fuzz(thisMethodDescription, ObjectModelProvider { nextDefaultModelId++ }.apply {
437+
fuzz(thisMethodDescription, ObjectModelProvider(defaultIdGenerator).apply {
438438
limitValuesCreatedByFieldAccessors = 500
439439
})
440440
}

utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt

+35-23
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import org.utbot.framework.plugin.api.util.defaultValueModel
1414
import org.utbot.framework.plugin.api.util.executableId
1515
import org.utbot.framework.plugin.api.util.id
1616
import org.utbot.framework.plugin.api.util.isArray
17+
import org.utbot.framework.plugin.api.util.isClassType
18+
import org.utbot.framework.plugin.api.util.isEnum
1719
import org.utbot.framework.plugin.api.util.isIterable
1820
import org.utbot.framework.plugin.api.util.isPrimitive
1921
import org.utbot.framework.plugin.api.util.jClass
@@ -31,7 +33,7 @@ import kotlin.reflect.KClass
3133
* Used as a fallback implementation until other providers cover every type.
3234
*/
3335
open class FallbackModelProvider(
34-
private val idGenerator: IntSupplier
36+
private val idGenerator: IdGenerator<Int>
3537
): AbstractModelProvider() {
3638

3739
override fun toModel(classId: ClassId): UtModel {
@@ -46,11 +48,11 @@ open class FallbackModelProvider(
4648
it.parameters.isEmpty() && it.isPublic
4749
}
4850
return when {
49-
classId.isPrimitive ->
51+
classId.isPrimitive || classId.isEnum || classId.isClassType ->
5052
classId.defaultValueModel()
5153
classId.isArray ->
5254
UtArrayModel(
53-
id = idGenerator.asInt,
55+
id = idGenerator.createId(),
5456
classId,
5557
length = 0,
5658
classId.elementClassId!!.defaultValueModel(),
@@ -81,26 +83,36 @@ open class FallbackModelProvider(
8183
val defaultConstructor = kclass.java.constructors.firstOrNull {
8284
it.parameters.isEmpty() && it.isPublic // check constructor is public
8385
}
84-
return if (kclass.isAbstract) { // sealed class is abstract by itself
85-
UtNullModel(kclass.java.id)
86-
} else if (defaultConstructor != null) {
87-
val chain = mutableListOf<UtStatementModel>()
88-
val model = UtAssembleModel(
89-
id = idGenerator.asInt,
90-
kclass.id,
91-
kclass.id.toString(),
92-
chain
93-
)
94-
chain.add(
95-
UtExecutableCallModel(model, defaultConstructor.executableId, listOf(), model)
96-
)
97-
model
98-
} else {
99-
UtCompositeModel(
100-
id = idGenerator.asInt,
101-
kclass.id,
102-
isMock = false
103-
)
86+
return when {
87+
kclass.isAbstract -> {
88+
// sealed class is abstract by itself
89+
UtNullModel(kclass.java.id)
90+
91+
}
92+
kclass.java.isEnum || kclass == java.lang.Class::class -> {
93+
// No sensible fallback solution for these classes except returning default `null` value
94+
UtNullModel(kclass.java.id)
95+
}
96+
defaultConstructor != null -> {
97+
val chain = mutableListOf<UtStatementModel>()
98+
val model = UtAssembleModel(
99+
id = idGenerator.createId(),
100+
kclass.id,
101+
kclass.id.toString(),
102+
chain
103+
)
104+
chain.add(
105+
UtExecutableCallModel(model, defaultConstructor.executableId, listOf(), model)
106+
)
107+
model
108+
}
109+
else -> {
110+
UtCompositeModel(
111+
id = idGenerator.createId(),
112+
kclass.id,
113+
isMock = false
114+
)
115+
}
104116
}
105117
}
106118
}

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt

+80-8
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,89 @@ import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
1212
import org.utbot.fuzzer.providers.EnumModelProvider
1313
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
1414
import java.lang.IllegalArgumentException
15+
import java.util.IdentityHashMap
1516
import java.util.concurrent.atomic.AtomicInteger
16-
import java.util.function.IntSupplier
1717
import kotlin.random.Random
1818

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

21+
/**
22+
* Identifier generator interface for fuzzer model providers.
23+
*
24+
* Provides fresh identifiers for generated models.
25+
*
26+
* Warning: specific generators are not guaranteed to be thread-safe.
27+
*
28+
* @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers)
29+
*/
30+
interface IdGenerator<Id> {
31+
/**
32+
* Create a fresh identifier. Each subsequent call should return a different value.
33+
*
34+
* The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee.
35+
*/
36+
fun createId(): Id
37+
}
38+
39+
/**
40+
* Identity preserving identifier generator interface.
41+
*
42+
* It allows to optionally save identifiers assigned to specific objects and later get the same identifiers
43+
* for these objects instead of fresh identifiers. This feature is necessary, for example, to implement reference
44+
* equality for enum models.
45+
*
46+
* Warning: specific generators are not guaranteed to be thread-safe.
47+
*
48+
* @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers)
49+
*/
50+
interface IdentityPreservingIdGenerator<Id> : IdGenerator<Id> {
51+
/**
52+
* Return an identifier for a specified non-null object. If an identifier has already been assigned
53+
* to an object, subsequent calls should return the same identifier for this object.
54+
*
55+
* Note: the interface does not specify whether reference equality or regular `equals`/`compareTo` equality
56+
* will be used to compare objects. Each implementation may provide these guarantees by itself.
57+
*
58+
* The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee.
59+
*/
60+
fun getOrCreateIdForValue(value: Any): Id
61+
}
62+
63+
/**
64+
* An identity preserving id generator for fuzzer value providers that returns identifiers of type [Int].
65+
*
66+
* When identity-preserving identifier is requested, objects are compared by reference.
67+
* The generator is not thread-safe.
68+
*
69+
* @param lowerBound an integer value so that any generated identifier is strictly greater than it.
70+
*
71+
* Warning: when generating [UtReferenceModel] identifiers, no identifier should be equal to zero,
72+
* as this value is reserved for [UtNullModel]. To guarantee it, [lowerBound] should never be negative.
73+
* Avoid using custom lower bounds (maybe except fuzzer unit tests), use the predefined default value instead.
74+
*/
75+
class ReferencePreservingIntIdGenerator(lowerBound: Int = DEFAULT_LOWER_BOUND) : IdentityPreservingIdGenerator<Int> {
76+
private val lastId: AtomicInteger = AtomicInteger(lowerBound)
77+
private val cache: IdentityHashMap<Any?, Int> = IdentityHashMap()
78+
79+
override fun getOrCreateIdForValue(value: Any): Int {
80+
return cache.getOrPut(value) { createId() }
81+
}
82+
83+
override fun createId(): Int {
84+
return lastId.incrementAndGet()
85+
}
86+
87+
companion object {
88+
/**
89+
* The default lower bound (all generated integer identifiers will be greater than it).
90+
*
91+
* It is defined as a large value because all synthetic [UtModel] instances
92+
* must have greater identifiers than the real models.
93+
*/
94+
const val DEFAULT_LOWER_BOUND: Int = 1500_000_000
95+
}
96+
}
97+
2198
fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence<List<FuzzedValue>> {
2299
if (modelProviders.isEmpty()) {
23100
throw IllegalArgumentException("At least one model provider is required")
@@ -42,7 +119,7 @@ fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvi
42119
/**
43120
* Creates a model provider from a list of default providers.
44121
*/
45-
fun defaultModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): ModelProvider {
122+
fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): ModelProvider {
46123
return ModelProvider.of(
47124
ObjectModelProvider(idGenerator),
48125
CollectionModelProvider(idGenerator),
@@ -59,7 +136,7 @@ fun defaultModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): Model
59136
/**
60137
* Creates a model provider for [ObjectModelProvider] that generates values for object constructor.
61138
*/
62-
fun objectModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): ModelProvider {
139+
fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): ModelProvider {
63140
return ModelProvider.of(
64141
CollectionModelProvider(idGenerator),
65142
ArrayModelProvider(idGenerator),
@@ -71,8 +148,3 @@ fun objectModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): ModelP
71148
PrimitiveWrapperModelProvider,
72149
)
73150
}
74-
75-
class SimpleIdGenerator : IntSupplier {
76-
private val id = AtomicInteger()
77-
override fun getAsInt() = id.incrementAndGet()
78-
}

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import org.utbot.framework.plugin.api.util.defaultValueModel
55
import org.utbot.framework.plugin.api.util.isArray
66
import org.utbot.fuzzer.FuzzedMethodDescription
77
import org.utbot.fuzzer.FuzzedParameter
8+
import org.utbot.fuzzer.IdGenerator
89
import org.utbot.fuzzer.ModelProvider
910
import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues
1011
import java.util.function.IntSupplier
1112

1213
class ArrayModelProvider(
13-
private val idGenerator: IntSupplier
14+
private val idGenerator: IdGenerator<Int>
1415
) : ModelProvider {
1516
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
1617
description.parametersMap
@@ -19,7 +20,7 @@ class ArrayModelProvider(
1920
.forEach { (arrayClassId, indices) ->
2021
yieldAllValues(indices, listOf(0, 10).map { arraySize ->
2122
UtArrayModel(
22-
id = idGenerator.asInt,
23+
id = idGenerator.createId(),
2324
arrayClassId,
2425
length = arraySize,
2526
arrayClassId.elementClassId!!.defaultValueModel(),

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionModelProvider.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.util.id
1111
import org.utbot.framework.plugin.api.util.jClass
1212
import org.utbot.fuzzer.FuzzedMethodDescription
1313
import org.utbot.fuzzer.FuzzedParameter
14+
import org.utbot.fuzzer.IdGenerator
1415
import org.utbot.fuzzer.ModelProvider
1516
import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues
1617
import java.util.function.IntSupplier
@@ -23,7 +24,7 @@ import java.util.function.IntSupplier
2324
* a non-modifiable collection and tries to add values.
2425
*/
2526
class CollectionModelProvider(
26-
private val idGenerator: IntSupplier
27+
private val idGenerator: IdGenerator<Int>
2728
) : ModelProvider {
2829

2930
private val generators = mapOf(
@@ -92,7 +93,7 @@ class CollectionModelProvider(
9293

9394
private fun Class<*>.createdBy(init: ExecutableId, params: List<UtModel> = emptyList()): UtAssembleModel {
9495
val instantiationChain = mutableListOf<UtStatementModel>()
95-
val genId = idGenerator.asInt
96+
val genId = idGenerator.createId()
9697
return UtAssembleModel(
9798
genId,
9899
id,

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/EnumModelProvider.kt

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,21 @@
11
package org.utbot.fuzzer.providers
22

33
import org.utbot.framework.plugin.api.UtEnumConstantModel
4-
import org.utbot.framework.plugin.api.util.id
5-
import org.utbot.framework.plugin.api.util.isSubtypeOf
64
import org.utbot.framework.plugin.api.util.jClass
5+
import org.utbot.fuzzer.IdentityPreservingIdGenerator
76
import org.utbot.fuzzer.FuzzedMethodDescription
87
import org.utbot.fuzzer.FuzzedParameter
98
import org.utbot.fuzzer.ModelProvider
109
import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues
11-
import java.util.function.IntSupplier
12-
13-
class EnumModelProvider : ModelProvider {
14-
private val idGenerator: IntSupplier
15-
private val limit: Int
16-
private val idCache: MutableMap<Enum<*>, Int> = mutableMapOf()
17-
18-
constructor(idGenerator: IntSupplier) : this(idGenerator, Int.MAX_VALUE)
19-
20-
constructor(idGenerator: IntSupplier, limit: Int) {
21-
this.idGenerator = idGenerator
22-
this.limit = limit
23-
}
2410

11+
class EnumModelProvider(private val idGenerator: IdentityPreservingIdGenerator<Int>) : ModelProvider {
2512
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
2613
description.parametersMap
2714
.asSequence()
2815
.filter { (classId, _) -> classId.jClass.isEnum }
2916
.forEach { (classId, indices) ->
3017
yieldAllValues(indices, classId.jClass.enumConstants.filterIsInstance<Enum<*>>().map {
31-
val id = idCache[it] ?: idGenerator.asInt
18+
val id = idGenerator.getOrCreateIdForValue(it)
3219
UtEnumConstantModel(id, classId, it).fuzzed { summary = "%var% = ${it.name}" }
3320
})
3421
}

0 commit comments

Comments
 (0)