Skip to content

Mutate values to improve code coverage #213 #713

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 4 commits into from
Aug 24, 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 @@ -88,9 +88,13 @@ import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.ReferencePreservingIntIdGenerator
import org.utbot.fuzzer.Trie
import org.utbot.fuzzer.TrieBasedFuzzerStatistics
import org.utbot.fuzzer.UtFuzzedExecution
import org.utbot.fuzzer.withMutations
import org.utbot.fuzzer.collectConstantsForFuzzer
import org.utbot.fuzzer.defaultModelProviders
import org.utbot.fuzzer.defaultModelMutators
import org.utbot.fuzzer.flipCoin
import org.utbot.fuzzer.fuzz
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.instrumentation.ConcreteExecutor
Expand Down Expand Up @@ -392,6 +396,7 @@ class UtBotSymbolicEngine(
val fallbackModelProvider = FallbackModelProvider(defaultIdGenerator)
val constantValues = collectConstantsForFuzzer(graph)

val random = Random(0)
val thisInstance = when {
methodUnderTest.isStatic -> null
methodUnderTest.isConstructor -> if (
Expand All @@ -405,7 +410,7 @@ class UtBotSymbolicEngine(
else -> {
ObjectModelProvider(defaultIdGenerator).withFallback(fallbackModelProvider).generate(
FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues)
).take(10).shuffled(Random(0)).map { it.value.model }.first().apply {
).take(10).shuffled(random).map { it.value.model }.first().apply {
if (this is UtNullModel) { // it will definitely fail because of NPE,
return@flow
}
Expand All @@ -421,8 +426,9 @@ class UtBotSymbolicEngine(
parameterNameMap = { index -> names?.getOrNull(index) }
}
val coveredInstructionTracker = Trie(Instruction::id)
val coveredInstructionValues = mutableMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
var attempts = UtSettings.fuzzingMaxAttempts
val coveredInstructionValues = linkedMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
var attempts = 0
val attemptsLimit = UtSettings.fuzzingMaxAttempts
val hasMethodUnderTestParametersToFuzz = executableId.parameters.isNotEmpty()
val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) {
fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders(defaultIdGenerator)))
Expand All @@ -435,7 +441,9 @@ class UtBotSymbolicEngine(
fuzz(thisMethodDescription, ObjectModelProvider(defaultIdGenerator).apply {
limitValuesCreatedByFieldAccessors = 500
})
}
}.withMutations(
TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray()
)
fuzzedValues.forEach { values ->
if (controller.job?.isActive == false || System.currentTimeMillis() >= until) {
logger.info { "Fuzzing overtime: $methodUnderTest" }
Expand Down Expand Up @@ -473,14 +481,19 @@ class UtBotSymbolicEngine(

val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions
if (coveredInstructions.isNotEmpty()) {
val count = coveredInstructionTracker.add(coveredInstructions)
if (count.count > 1) {
if (--attempts < 0) {
val coverageKey = coveredInstructionTracker.add(coveredInstructions)
if (coverageKey.count > 1) {
if (++attempts >= attemptsLimit) {
return@flow
}
// Update the seeded values sometimes
// This is necessary because some values cannot do a good values in mutation in any case
if (random.flipCoin(probability = 50)) {
coveredInstructionValues[coverageKey] = values
}
return@forEach
}
coveredInstructionValues[count] = values
coveredInstructionValues[coverageKey] = values
} else {
logger.error { "Coverage is empty for $methodUnderTest with ${values.map { it.model }}" }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ private object StringConstant: ConstantsFinder {
// if string constant is called from String class let's pass it as modification
if (value.method.declaringClass.name == "java.lang.String") {
val stringConstantWasPassedAsArg = unit.useBoxes.findFirstInstanceOf<Constant>()?.plainValue
if (stringConstantWasPassedAsArg != null) {
if (stringConstantWasPassedAsArg != null && stringConstantWasPassedAsArg is String) {
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedOp.CH))
}
val stringConstantWasPassedAsThis = graph.getPredsOf(unit)
Expand All @@ -213,7 +213,7 @@ private object StringConstant: ConstantsFinder {
?.useBoxes
?.findFirstInstanceOf<Constant>()
?.plainValue
if (stringConstantWasPassedAsThis != null) {
if (stringConstantWasPassedAsThis != null && stringConstantWasPassedAsThis is String) {
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedOp.CH))
}
}
Expand Down Expand Up @@ -250,4 +250,4 @@ private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) {
else -> FuzzedOp.NONE
}

private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()
private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.utbot.framework.plugin.api.UtModel
*/
class FuzzedValue(
val model: UtModel,
val createdBy: ModelProvider? = null
val createdBy: ModelProvider? = null,
) {

/**
Expand Down
90 changes: 83 additions & 7 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package org.utbot.fuzzer

import org.utbot.fuzzer.providers.ConstantsModelProvider
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.fuzzer.providers.PrimitivesModelProvider
import org.utbot.fuzzer.providers.StringConstantModelProvider
import mu.KotlinLogging
import org.utbot.fuzzer.mutators.NumberRandomMutator
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.PrimitiveDefaultsModelProvider
import org.utbot.fuzzer.providers.ConstantsModelProvider
import org.utbot.fuzzer.providers.EnumModelProvider
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
import java.lang.IllegalArgumentException
import java.util.IdentityHashMap
import org.utbot.fuzzer.providers.PrimitivesModelProvider
import org.utbot.fuzzer.providers.StringConstantModelProvider
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.random.Random

Expand Down Expand Up @@ -95,6 +96,9 @@ class ReferencePreservingIntIdGenerator(lowerBound: Int = DEFAULT_LOWER_BOUND) :
}
}

/**
* Generated by fuzzer sequence of values which can be passed into the method.
*/
fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence<List<FuzzedValue>> {
if (modelProviders.isEmpty()) {
throw IllegalArgumentException("At least one model provider is required")
Expand All @@ -116,6 +120,29 @@ fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvi
return CartesianProduct(values, Random(0L)).asSequence()
}

/**
* Wraps sequence of values, iterates through them and mutates.
*
* Mutation when possible is generated after every value of source sequence and then supplies values until it needed.
* [statistics] should not be updated by this method, but can be changed by caller.
*/
fun <T> Sequence<List<FuzzedValue>>.withMutations(statistics: FuzzerStatistics<T>, description: FuzzedMethodDescription, vararg mutators: ModelMutator) = sequence {
val fvi = iterator()
val mutatorList = mutators.toList()
val random = Random(0L)
while (fvi.hasNext()) {
// Takes a value that was generated by model providers and submits it
yield(fvi.next())
// Fuzzing can generate values that don't recover new paths.
// So, fuzzing tries to mutate values on each loop
// if there are too many attempts to find new paths without mutations.
yieldMutated(statistics, description, mutatorList, random)
}
// try mutations if fuzzer tried all combinations if any seeds are available
@Suppress("ControlFlowWithEmptyBody")
while (yieldMutated(statistics, description, mutatorList, random)) {}
}

/**
* Creates a model provider from a list of default providers.
*/
Expand Down Expand Up @@ -148,3 +175,52 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Model
PrimitiveWrapperModelProvider,
)
}

fun defaultModelMutators(): List<ModelMutator> = listOf(StringRandomMutator, NumberRandomMutator)

/**
* Tries to mutate a random value from the seed.
*
* Returns `null` if didn't try to do any mutation.
*/
fun <T> mutateRandomValueOrNull(
statistics: FuzzerStatistics<T>,
description: FuzzedMethodDescription,
mutators: List<ModelMutator> = defaultModelMutators(),
random: Random = Random,
): List<FuzzedValue>? {
if (mutators.isEmpty()) return null
val values = statistics.takeIf { it.isNotEmpty() }?.randomValues(random) ?: return null
var newValues :MutableList<FuzzedValue>? = null
mutators.asSequence()
.forEach { mut ->
mut.mutate(description, values, random).forEach { (index, value) ->
newValues = (newValues ?: values.toMutableList())
newValues?.set(index, value)
}
}
return newValues
}

/**
* Run mutations and yields values into the sequence.
*
* Mutations are supplied infinitely until [repeat] returns true. [repeat] is run before mutation.
*
* @param statistics coverage-based seed
* @param description method description
* @param mutators mutators which are applied to the random value
* @param random instance that is used to choose random index from the [statistics]
*/
suspend fun <T> SequenceScope<List<FuzzedValue>>.yieldMutated(
statistics: FuzzerStatistics<T>,
description: FuzzedMethodDescription,
mutators: List<ModelMutator>,
random: Random
) : Boolean {
mutateRandomValueOrNull(statistics, description, mutators, random)?.let {
yield(it)
return true
}
return false
}
71 changes: 71 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.utbot.fuzzer

import kotlin.math.pow
import kotlin.random.Random

/**
* Stores information that can be useful for fuzzing such as coverage, run count, etc.
*/
interface FuzzerStatistics<K> {

val seeds: Collection<K>

/**
* Returns a random seed to process.
*/
fun randomSeed(random: Random): K?

fun randomValues(random: Random): List<FuzzedValue>?

fun executions(seed: K): Int

operator fun get(seed: K): List<FuzzedValue>?

fun isEmpty(): Boolean

fun isNotEmpty(): Boolean {
return !isEmpty()
}
}

class TrieBasedFuzzerStatistics<V>(
private val values: LinkedHashMap<Trie.Node<V>, List<FuzzedValue>> = linkedMapOf()
) : FuzzerStatistics<Trie.Node<V>> {

override val seeds: Collection<Trie.Node<V>>
get() = values.keys

override fun randomSeed(random: Random): Trie.Node<V>? {
return values.keys.elementAtOrNull(randomIndex(random))
}

override fun isEmpty(): Boolean {
return values.isEmpty()
}

override fun isNotEmpty(): Boolean {
return values.isNotEmpty()
}

override fun randomValues(random: Random): List<FuzzedValue>? {
return values.values.elementAtOrNull(randomIndex(random))
}

private fun randomIndex(random: Random): Int {
val frequencies = DoubleArray(values.size).also { f ->
values.keys.forEachIndexed { index, key ->
f[index] = 1 / key.count.toDouble().pow(2)
}
}
return random.chooseOne(frequencies)
}

override fun get(seed: Trie.Node<V>): List<FuzzedValue>? {
return values[seed]
}

override fun executions(seed: Trie.Node<V>): Int {
return seed.count
}

}
46 changes: 46 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.utbot.fuzzer

import org.utbot.framework.plugin.api.UtModel
import kotlin.random.Random

/**
* Mutates values and returns it.
*/
interface ModelMutator {

/**
* Mutates values set.
*
* Default implementation iterates through values and delegates to `mutate(FuzzedMethodDescription, Int, Random)`.
*/
fun mutate(
description: FuzzedMethodDescription,
parameters: List<FuzzedValue>,
random: Random,
) : List<FuzzedParameter> {
return parameters
.asSequence()
.mapIndexedNotNull { index, fuzzedValue ->
mutate(description, index, fuzzedValue, random)?.let { mutated ->
FuzzedParameter(index, mutated)
}
}
.toList()
}

/**
* Mutate a single value if it is possible.
*/
fun mutate(
description: FuzzedMethodDescription,
index: Int,
value: FuzzedValue,
random: Random
) : FuzzedValue? {
return null
}

fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue {
return FuzzedValue(this, template.createdBy).apply(block)
}
}
36 changes: 36 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.utbot.fuzzer

import kotlin.random.Random

/**
* Chooses a random value using frequencies.
*
* If value has greater frequency value then it would be chosen with greater probability.
*
* @return the index of the chosen item.
*/
fun Random.chooseOne(frequencies: DoubleArray): Int {
val total = frequencies.sum()
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps, is frequencies could be a big array, we can create a custom collection for such frequencies that knowns information about its elements, such as total sum, statistics and other

val value = nextDouble(total)
var nextBound = 0.0
frequencies.forEachIndexed { index, bound ->
check(bound >= 0) { "Frequency must not be negative" }
nextBound += bound
if (value < nextBound) return index
}
error("Cannot find next index")
}

/**
* Tries a value.
*
* If a random value is less than [probability] returns true.
*/
fun Random.flipCoin(probability: Int): Boolean {
check(probability in 0 .. 100) { "probability must in range [0, 100] but $probability is provided" }
return nextInt(1, 101) <= probability
}

fun Long.invertBit(bitIndex: Int): Long {
return this xor (1L shl bitIndex)
}
Loading