Skip to content

Generate test by fuzzing for methods with no parameters #511 #515

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 1 commit into from
Jul 14, 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 @@ -64,6 +64,7 @@ import org.utbot.framework.plugin.api.EnvironmentModels
import org.utbot.framework.plugin.api.Instruction
import org.utbot.framework.plugin.api.MissingState
import org.utbot.framework.plugin.api.Step
import org.utbot.framework.plugin.api.UtAssembleModel
import org.utbot.framework.plugin.api.UtConcreteExecutionFailure
import org.utbot.framework.plugin.api.UtError
import org.utbot.framework.plugin.api.UtExecution
Expand All @@ -79,6 +80,7 @@ import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.utContext
import org.utbot.framework.plugin.api.util.description
import org.utbot.framework.util.jimpleBody
import org.utbot.framework.plugin.api.util.voidClassId
import org.utbot.fuzzer.FallbackModelProvider
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
Expand All @@ -89,10 +91,12 @@ import org.utbot.fuzzer.defaultModelProviders
import org.utbot.fuzzer.fuzz
import org.utbot.fuzzer.names.MethodBasedNameSuggester
import org.utbot.fuzzer.names.ModelBasedNameSuggester
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.instrumentation.ConcreteExecutor
import soot.jimple.Stmt
import soot.tagkit.ParamNamesTag
import java.lang.reflect.Method
import kotlin.random.Random
import kotlin.system.measureTimeMillis

val logger = KotlinLogging.logger {}
Expand Down Expand Up @@ -384,6 +388,7 @@ class UtBotSymbolicEngine(
}

val fallbackModelProvider = FallbackModelProvider { nextDefaultModelId++ }
val constantValues = collectConstantsForFuzzer(graph)

val thisInstance = when {
methodUnderTest.isStatic -> null
Expand All @@ -396,7 +401,9 @@ class UtBotSymbolicEngine(
null
}
else -> {
fallbackModelProvider.toModel(methodUnderTest.clazz).apply {
ObjectModelProvider { nextDefaultModelId++ }.withFallback(fallbackModelProvider).generate(
FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues)
).take(10).shuffled(Random(0)).map { it.value.model }.first().apply {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you purposely using the fixed seed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fuzzer should randomize but be reproducable for same method

if (this is UtNullModel) { // it will definitely fail because of NPE,
return@flow
}
Expand All @@ -411,17 +418,34 @@ class UtBotSymbolicEngine(
val names = graph.body.method.tags.filterIsInstance<ParamNamesTag>().firstOrNull()?.names
parameterNameMap = { index -> names?.getOrNull(index) }
}
val modelProviderWithFallback = modelProvider(defaultModelProviders { nextDefaultModelId++ }).withFallback(fallbackModelProvider::toModel)
val coveredInstructionTracker = Trie(Instruction::id)
val coveredInstructionValues = mutableMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
var attempts = UtSettings.fuzzingMaxAttempts
fuzz(methodUnderTestDescription, modelProviderWithFallback).forEach { values ->
val hasMethodUnderTestParametersToFuzz = executableId.parameters.isNotEmpty()
val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) {
fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders { nextDefaultModelId++ }))
} else {
// in case a method with no parameters is passed fuzzing tries to fuzz this instance with different constructors, setters and field mutators
val thisMethodDescription = FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues).apply {
className = executableId.classId.simpleName
packageName = executableId.classId.packageName
}
fuzz(thisMethodDescription, ObjectModelProvider { nextDefaultModelId++ }.apply {
limitValuesCreatedByFieldAccessors = 500
})
}
fuzzedValues.forEach { values ->
if (System.currentTimeMillis() >= until) {
logger.info { "Fuzzing overtime: $methodUnderTest" }
return@flow
}

val initialEnvironmentModels = EnvironmentModels(thisInstance, values.map { it.model }, mapOf())
val initialEnvironmentModels = if (hasMethodUnderTestParametersToFuzz) {
EnvironmentModels(thisInstance, values.map { it.model }, mapOf())
} else {
check(values.size == 1 && values.first().model is UtAssembleModel)
EnvironmentModels(values.first().model, emptyList(), mapOf())
}

try {
val concreteExecutionResult =
Expand Down Expand Up @@ -462,7 +486,7 @@ class UtBotSymbolicEngine(
fullPath = emptyList(),
coverage = concreteExecutionResult.coverage,
testMethodName = testMethodName?.testName,
displayName = testMethodName?.displayName
displayName = testMethodName?.takeIf { hasMethodUnderTestParametersToFuzz }?.displayName
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this code right, we will not have display name for tests without parameters at all to avoid explicit "this" argument in the name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's true

)
)
} catch (e: CancellationException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.utbot.framework.plugin.api.util.withUtContext
import org.utbot.framework.plugin.api.util.wrapperByPrimitive
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.ModelProvider.Companion.yieldValue
import org.utbot.instrumentation.ConcreteExecutor
import org.utbot.instrumentation.execute
import java.lang.reflect.Method
Expand Down Expand Up @@ -175,11 +176,13 @@ object UtBotJavaApi {
}
?.map { UtPrimitiveModel(it) } ?: emptySequence()

val customModelProvider = ModelProvider { description, consumer ->
description.parametersMap.forEach { (classId, indices) ->
createPrimitiveModels(primitiveValuesSupplier, classId).forEach { model ->
indices.forEach { index ->
consumer.accept(index, FuzzedValue(model))
val customModelProvider = ModelProvider { description ->
sequence {
description.parametersMap.forEach { (classId, indices) ->
createPrimitiveModels(primitiveValuesSupplier, classId).forEach { model ->
indices.forEach { index ->
yieldValue(index, FuzzedValue(model))
}
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedParameter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.utbot.fuzzer

/**
* Fuzzed parameter of a method.
*
* @param index of the parameter in method signature
* @param value fuzzed values
*/
class FuzzedParameter(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this class be a data class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, data classes creates hashCode/equals/toString that aren't necessary in most cases

val index: Int,
val value: FuzzedValue
) {
operator fun component1() = index
operator fun component2() = value
}
2 changes: 1 addition & 1 deletion utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private val logger = KotlinLogging.logger {}
fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence<List<FuzzedValue>> {
val values = List<MutableList<FuzzedValue>>(description.parameters.size) { mutableListOf() }
modelProviders.forEach { fuzzingProvider ->
fuzzingProvider.generate(description) { index, model ->
fuzzingProvider.generate(description).forEach { (index, model) ->
values[index].add(model)
}
}
Expand Down
76 changes: 41 additions & 35 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ package org.utbot.fuzzer

import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.ClassId
import java.util.function.BiConsumer

fun interface ModelProvider {

/**
* Generates values for the method.
*
* @param description a fuzzed method description
* @param consumer accepts index in the parameter list and [UtModel] for this parameter.
* @return sequence that produces [FuzzedParameter].
*/
fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>)
fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter>

/**
* Combines this model provider with `anotherModelProvider` into one instance.
Expand Down Expand Up @@ -51,25 +50,24 @@ fun interface ModelProvider {
* @param modelProvider is called and every value of [ClassId] is collected which wasn't created by this model provider.
*/
fun withFallback(modelProvider: ModelProvider) : ModelProvider {
return ModelProvider { description, consumer ->
val providedByDelegateMethodParameters = mutableMapOf<Int, MutableList<FuzzedValue>>()
this@ModelProvider.generate(description) { index, model ->
providedByDelegateMethodParameters.computeIfAbsent(index) { mutableListOf() }.add(model)
}
providedByDelegateMethodParameters.forEach { (index, models) ->
models.forEach { model ->
consumer.accept(index, model)
val thisModelProvider = this
return ModelProvider { description ->
sequence {
val providedByDelegateMethodParameters = mutableSetOf<Int>()
thisModelProvider.generate(description).forEach { (index, model) ->
providedByDelegateMethodParameters += index
yieldValue(index, model)
}
}
val missingParameters =
(0 until description.parameters.size).filter { !providedByDelegateMethodParameters.containsKey(it) }
if (missingParameters.isNotEmpty()) {
val values = mutableMapOf<Int, MutableList<FuzzedValue>>()
modelProvider.generate(description) { i, m -> values.computeIfAbsent(i) { mutableListOf() }.add(m) }
missingParameters.forEach { index ->
values[index]?.let { models ->
models.forEach { model ->
consumer.accept(index, model)
val missingParameters =
(0 until description.parameters.size).filter { !providedByDelegateMethodParameters.contains(it) }
if (missingParameters.isNotEmpty()) {
val values = mutableMapOf<Int, MutableList<FuzzedValue>>()
modelProvider.generate(description).forEach { (i, m) -> values.computeIfAbsent(i) { mutableListOf() }.add(m) }
missingParameters.forEach { index ->
values[index]?.let { models ->
models.forEach { model ->
yieldValue(index, model)
}
}
}
}
Expand All @@ -86,15 +84,17 @@ fun interface ModelProvider {
* @param fallbackModelSupplier is called for every [ClassId] which wasn't created by this model provider.
*/
fun withFallback(fallbackModelSupplier: (ClassId) -> UtModel?) : ModelProvider {
return withFallback { description, consumer ->
description.parametersMap.forEach { (classId, indices) ->
fallbackModelSupplier(classId)?.let { model ->
indices.forEach { index ->
consumer.accept(index, model.fuzzed())
return withFallback( ModelProvider { description ->
sequence {
description.parametersMap.forEach { (classId, indices) ->
fallbackModelSupplier(classId)?.let { model ->
indices.forEach { index ->
yieldValue(index, model.fuzzed())
}
}
}
}
}
})
}

companion object {
Expand All @@ -103,26 +103,32 @@ fun interface ModelProvider {
return Combined(providers.toList())
}

fun BiConsumer<Int, FuzzedValue>.consumeAll(indices: List<Int>, models: Sequence<FuzzedValue>) {
models.forEach { model ->
indices.forEach { index ->
accept(index, model)
suspend fun SequenceScope<FuzzedParameter>.yieldValue(index: Int, value: FuzzedValue) {
yield(FuzzedParameter(index, value))
}

suspend fun SequenceScope<FuzzedParameter>.yieldAllValues(indices: List<Int>, models: Sequence<FuzzedValue>) {
indices.forEach { index ->
models.forEach { model ->
yieldValue(index, model)
}
}
}

fun BiConsumer<Int, FuzzedValue>.consumeAll(indices: List<Int>, models: List<FuzzedValue>) {
consumeAll(indices, models.asSequence())
suspend fun SequenceScope<FuzzedParameter>.yieldAllValues(indices: List<Int>, models: List<FuzzedValue>) {
yieldAllValues(indices, models.asSequence())
}
}

/**
* Wrapper class that delegates implementation to the [providers].
*/
private class Combined(val providers: List<ModelProvider>): ModelProvider {
override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>) {
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
providers.forEach { provider ->
provider.generate(description, consumer)
provider.generate(description).forEach {
yieldValue(it.index, it.value)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ package org.utbot.fuzzer.providers

import org.utbot.framework.plugin.api.*
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.FuzzedParameter
import org.utbot.fuzzer.ModelProvider
import java.util.function.BiConsumer
import org.utbot.fuzzer.ModelProvider.Companion.yieldValue

/**
* Simple model implementation.
*/
@Suppress("unused")
abstract class AbstractModelProvider: ModelProvider {
override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>) {
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence{
description.parametersMap.forEach { (classId, indices) ->
toModel(classId)?.let { defaultModel ->
indices.forEach { index ->
consumer.accept(index, defaultModel.fuzzed())
yieldValue(index, defaultModel.fuzzed())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ import org.utbot.framework.plugin.api.UtArrayModel
import org.utbot.framework.plugin.api.util.defaultValueModel
import org.utbot.framework.plugin.api.util.isArray
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.FuzzedParameter
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.ModelProvider.Companion.consumeAll
import java.util.function.BiConsumer
import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues
import java.util.function.IntSupplier

class ArrayModelProvider(
private val idGenerator: IntSupplier
) : ModelProvider {
override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>) {
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
description.parametersMap
.asSequence()
.filter { (classId, _) -> classId.isArray }
.forEach { (arrayClassId, indices) ->
consumer.consumeAll(indices, listOf(0, 10).map { arraySize ->
yieldAllValues(indices, listOf(0, 10).map { arraySize ->
UtArrayModel(
id = idGenerator.asInt,
arrayClassId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@ import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.util.charClassId
import org.utbot.framework.plugin.api.util.stringClassId
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.FuzzedParameter
import org.utbot.fuzzer.ModelProvider
import java.util.function.BiConsumer
import org.utbot.fuzzer.ModelProvider.Companion.yieldValue

/**
* Collects all char constants and creates string with them.
*/
object CharToStringModelProvider : ModelProvider {
override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>) {
val indices = description.parametersMap[stringClassId] ?: return
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
val indices = description.parametersMap[stringClassId] ?: return@sequence
if (indices.isNotEmpty()) {
val string = description.concreteValues.asSequence()
.filter { it.classId == charClassId }
.map { it.value }
.filterIsInstance<Char>()
.joinToString(separator = "")
if (string.isNotEmpty()) {
val model = UtPrimitiveModel(string).fuzzed()
indices.forEach {
consumer.accept(it, model)
sequenceOf(string.reversed(), string).forEach { str ->
val model = UtPrimitiveModel(str).fuzzed()
indices.forEach {
yieldValue(it, model)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import org.utbot.framework.plugin.api.UtStatementModel
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.jClass
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.FuzzedParameter
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.ModelProvider.Companion.consumeAll
import java.util.function.BiConsumer
import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues
import java.util.function.IntSupplier

/**
Expand All @@ -36,12 +35,12 @@ class CollectionModelProvider(
java.util.Iterator::class.java to ::createIteratorModels,
)

override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer<Int, FuzzedValue>) {
override fun generate(description: FuzzedMethodDescription): Sequence<FuzzedParameter> = sequence {
description.parametersMap
.asSequence()
.forEach { (classId, indices) ->
generators[classId.jClass]?.let { createModels ->
consumer.consumeAll(indices, createModels().map { it.fuzzed() })
yieldAllValues(indices, createModels().map { it.fuzzed() })
}
}
}
Expand Down
Loading