From 5fd4d6ebef6f98ac9ac7fa572d4fb30e38e87649 Mon Sep 17 00:00:00 2001 From: Maksim Pelevin Date: Thu, 11 Aug 2022 15:58:07 +0300 Subject: [PATCH 1/4] Mutate values to improve code coverage #213 --- .../kotlin/org/utbot/framework/UtSettings.kt | 7 ++ .../org/utbot/engine/UtBotSymbolicEngine.kt | 49 ++++++++++-- .../org/utbot/fuzzer/FuzzerFunctions.kt | 27 ++++++- .../org/utbot/fuzzer/FrequencyRandom.kt | 41 ++++++++++ .../kotlin/org/utbot/fuzzer/FuzzedValue.kt | 3 +- .../main/kotlin/org/utbot/fuzzer/Fuzzer.kt | 17 +++-- .../kotlin/org/utbot/fuzzer/ModelMutator.kt | 51 +++++++++++++ .../fuzzer/mutators/NumberRandomMutator.kt | 70 +++++++++++++++++ .../fuzzer/mutators/StringRandomMutator.kt | 76 +++++++++++++++++++ .../providers/StringConstantModelProvider.kt | 45 ++++------- .../plugin/api/FrequencyRandomTest.kt | 62 +++++++++++++++ 11 files changed, 400 insertions(+), 48 deletions(-) create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt create mode 100644 utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt index edbc16f2f5..16ed4f380b 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt @@ -276,6 +276,13 @@ object UtSettings { */ var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE) + /** + * Fuzzing tries to mutate values using this factor. + * + * If any mutation is successful then counter is reset. + */ + var fuzzingRandomMutationsFactor: Int by getIntProperty(10_000) + /** * Fuzzer tries to generate and run tests during this time. */ 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 7c91ada79d..259b33083a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -83,6 +83,7 @@ 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.FrequencyRandom import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelProvider @@ -91,6 +92,7 @@ import org.utbot.fuzzer.Trie import org.utbot.fuzzer.UtFuzzedExecution import org.utbot.fuzzer.collectConstantsForFuzzer import org.utbot.fuzzer.defaultModelProviders +import org.utbot.fuzzer.findNextMutatedValue import org.utbot.fuzzer.fuzz import org.utbot.fuzzer.providers.ObjectModelProvider import org.utbot.instrumentation.ConcreteExecutor @@ -421,8 +423,9 @@ class UtBotSymbolicEngine( parameterNameMap = { index -> names?.getOrNull(index) } } val coveredInstructionTracker = Trie(Instruction::id) - val coveredInstructionValues = mutableMapOf, List>() - var attempts = UtSettings.fuzzingMaxAttempts + val coveredInstructionValues = linkedMapOf, List>() + var attempts = 0 + val attemptsLimit = UtSettings.fuzzingMaxAttempts val hasMethodUnderTestParametersToFuzz = executableId.parameters.isNotEmpty() val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) { fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders(defaultIdGenerator))) @@ -436,7 +439,34 @@ class UtBotSymbolicEngine( limitValuesCreatedByFieldAccessors = 500 }) } - fuzzedValues.forEach { values -> + + val frequencyRandom = FrequencyRandom(Random(221)) + val factor = maxOf(0, UtSettings.fuzzingRandomMutationsFactor) + val fuzzedValueSequence: Sequence> = sequence { + val fvi = fuzzedValues.iterator() + while (fvi.hasNext()) { + yield(fvi.next()) + // if we stuck switch to "next + several mutations" mode + if ((attempts + 1) % (factor / 100) == 0 && coveredInstructionValues.isNotEmpty()) { + for (i in 0 until (factor / 100)) { + findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription)?.let { yield(it) } + } + } + } + // try mutations if fuzzer tried all combinations + var tryLocal = factor + while (tryLocal-- >= 0) { + val value = findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription) + if (value != null) { + val before = coveredInstructionValues.size + yield(value) + if (coveredInstructionValues.size != before) { + tryLocal = factor + } + } + } + } + fuzzedValueSequence.forEach { values -> if (controller.job?.isActive == false || System.currentTimeMillis() >= until) { logger.info { "Fuzzing overtime: $methodUnderTest" } return@flow @@ -473,14 +503,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 (frequencyRandom.random.nextInt(1, 101) <= 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 }}" } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt index 9efdcb7ad4..d4d7237106 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt @@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.stringClassId import mu.KotlinLogging +import org.utbot.framework.plugin.api.Instruction import soot.BooleanType import soot.ByteType import soot.CharType @@ -42,6 +43,7 @@ import soot.jimple.internal.JNeExpr import soot.jimple.internal.JTableSwitchStmt import soot.jimple.internal.JVirtualInvokeExpr import soot.toolkits.graph.ExceptionalUnitGraph +import kotlin.math.pow private val logger = KotlinLogging.logger {} @@ -204,7 +206,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()?.plainValue - if (stringConstantWasPassedAsArg != null) { + if (stringConstantWasPassedAsArg != null && stringConstantWasPassedAsArg is String) { return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedOp.CH)) } val stringConstantWasPassedAsThis = graph.getPredsOf(unit) @@ -213,7 +215,7 @@ private object StringConstant: ConstantsFinder { ?.useBoxes ?.findFirstInstanceOf() ?.plainValue - if (stringConstantWasPassedAsThis != null) { + if (stringConstantWasPassedAsThis != null && stringConstantWasPassedAsThis is String) { return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedOp.CH)) } } @@ -250,4 +252,23 @@ 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() \ No newline at end of file +private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() + +fun findNextMutatedValue( + frequencyRandom: FrequencyRandom, + coverage: LinkedHashMap, List>, + description: FuzzedMethodDescription, +): List? { + if (coverage.isEmpty()) return null + frequencyRandom.prepare(coverage.map { it.key.count }) { 1 / it.toDouble().pow(2) } + val values = coverage.values.drop(frequencyRandom.nextIndex()).firstOrNull() + if (values != null) { + val mutatedValue = defaultMutators().fold(values) { v, mut -> + mut.mutate(description, v, frequencyRandom.random) + } + if (mutatedValue != values) { + return mutatedValue + } + } + return null +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt new file mode 100644 index 0000000000..033b6f2897 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt @@ -0,0 +1,41 @@ +package org.utbot.fuzzer + +import kotlin.random.Random + +/** + * Frequency random returns [nextIndex] with given probabilities. + */ +class FrequencyRandom(var random: Random = Random) { + private var total = 1.0 + private var bound: DoubleArray = DoubleArray(1) { 1.0 } + val size: Int + get() = bound.size + + /** + * Updates internals to generate values using [frequencies] and custom function [forEach]. + * + * For example, it is possible implement logic where the source list of [frequencies] is inverted. + * + * ``` + * prepare(listOf(20., 80.), forEach = { 100 - it }) + * + * In this case second value (that has 80.)in the [frequencies] list will be found in 20% of all cases. + */ + fun prepare(frequencies: List, forEach: (T) -> Double = { it.toDouble() }) { + bound = DoubleArray(frequencies.size) { forEach(frequencies[it]) } + for (i in 1 until bound.size) { + bound[i] = bound[i] + bound[i - 1] + } + total = if (bound.isEmpty()) 0.0 else bound.last() + } + + fun nextIndex(): Int { + val value = random.nextDouble(total) + for (index in bound.indices) { + if (value < bound[index]) { + return index + } + } + error("Cannot find next index") + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt index 3919f111cd..d480eb5d52 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -8,7 +8,8 @@ import org.utbot.framework.plugin.api.UtModel */ class FuzzedValue( val model: UtModel, - val createdBy: ModelProvider? = null + val createdBy: ModelProvider? = null, + val mutatedBy: List = emptyList(), ) { /** 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 c0dde7d1c5..80f9cb2eae 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -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 @@ -148,3 +149,5 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator): Model PrimitiveWrapperModelProvider, ) } + +fun defaultMutators(): List = listOf(StringRandomMutator(50), NumberRandomMutator(50)) \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt new file mode 100644 index 0000000000..718dfb7235 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt @@ -0,0 +1,51 @@ +package org.utbot.fuzzer + +import org.utbot.framework.plugin.api.UtModel +import kotlin.random.Random + +/** + * Mutates values and returns it. + * + * Mutator can be not applied using [probability] as pivot. + * In this case unchanged values is returned. + */ +interface ModelMutator { + + /** + * The probability of applying this mutator. Can be ignored in some implementations. + */ + val probability: Int + + /** + * Mutates values set. + * + * Default implementation iterates through values and delegates to `mutate(FuzzedMethodDescription, Int, Random)`. + */ + fun mutate( + description: FuzzedMethodDescription, + parameters: List, + random: Random, + ) : List { + return parameters.mapIndexed { index, fuzzedValue -> + mutate(description, index, fuzzedValue, random) ?: fuzzedValue + } + } + + /** + * Mutate a single value if it is possible. + * + * Default implementation mutates the value with given [probability] or returns `null`. + */ + fun mutate( + description: FuzzedMethodDescription, + index: Int, + value: FuzzedValue, + random: Random + ) : FuzzedValue? { + return if (random.nextInt(1, 101) < probability) value else null + } + + fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue { + return FuzzedValue(this, template.createdBy, template.mutatedBy + this@ModelMutator).apply(block) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt new file mode 100644 index 0000000000..c9eac811a7 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt @@ -0,0 +1,70 @@ +package org.utbot.fuzzer.mutators + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelMutator +import kotlin.random.Random + +/** + * Mutates any [Number] changing random bit. + */ +class NumberRandomMutator(override val probability: Int) : ModelMutator { + + init { + check(probability in 0 .. 100) { "Probability must be in range 0..100" } + } + + override fun mutate( + description: FuzzedMethodDescription, + index: Int, + value: FuzzedValue, + random: Random + ): FuzzedValue? { + if (random.nextInt(1, 101) <= probability) return null + val model = value.model + return if (model is UtPrimitiveModel && model.value is Number) { + val newValue = changeRandomBit(random, model.value as Number) + UtPrimitiveModel(newValue).mutatedFrom(value) { + summary = "%var% = $newValue (mutated from ${model.value})" + } + } else null + } + + private fun changeRandomBit(random: Random, number: Number): Number { + val size = when (number) { + is Byte -> Byte.SIZE_BITS + is Short -> Short.SIZE_BITS + is Int -> Int.SIZE_BITS + is Float -> Float.SIZE_BITS + is Long -> Long.SIZE_BITS + is Double -> Double.SIZE_BITS + else -> error("Unknown type: ${number.javaClass}") + } + val asLong = when (number) { + is Byte, is Short, is Int -> number.toLong() + is Long -> number + is Float -> number.toRawBits().toLong() + is Double -> number.toRawBits() + else -> error("Unknown type: ${number.javaClass}") + } + val bitIndex = random.nextInt(size) + val mutated = invertBit(asLong, bitIndex) + return when (number) { + is Byte -> mutated.toByte() + is Short -> mutated.toShort() + is Int -> mutated.toInt() + is Float -> Float.fromBits(mutated.toInt()) + is Long -> mutated + is Double -> Double.fromBits(mutated) + else -> error("Unknown type: ${number.javaClass}") + } + } + + private fun invertBit(value: Long, bitIndex: Int): Long { + val b = ((value shr bitIndex) and 1).inv() + val mask = 1L shl bitIndex + return value and mask.inv() or (b shl bitIndex and mask) + } +} + diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt new file mode 100644 index 0000000000..360fc16917 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt @@ -0,0 +1,76 @@ +package org.utbot.fuzzer.mutators + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelMutator +import kotlin.random.Random + +/** + * Mutates string by adding and/or removal symbol at random position. + * + * Adding or removal can be applied with a given [probability]. + */ +class StringRandomMutator(override val probability: Int = 50) : ModelMutator { + + init { + check(probability in 0 .. 100) { "Probability must be in range 0..100" } + } + + override fun mutate( + description: FuzzedMethodDescription, + index: Int, + value: FuzzedValue, + random: Random + ): FuzzedValue? { + return (value.model as? UtPrimitiveModel) + ?.takeIf { it.classId == stringClassId } + ?.let { model -> + mutate(random, model.value as String).let { + UtPrimitiveModel(it).mutatedFrom(value) { + summary = "%var% = mutated string" + } + } + } + } + + private fun mutate(random: Random, string: String): String { + // we can miss some mutation for a purpose + val position = random.nextInt(string.length + 1) + var result: String = string + if (random.nextInt(1, 101) <= probability) { + result = tryRemoveChar(random, result, position) ?: string + } + if (random.nextInt(1, 101) <= probability) { + result = tryAddChar(random, result, position) + } + return result + } + + private fun tryAddChar(random: Random, value: String, position: Int): String { + val charToMutate = if (value.isNotEmpty()) { + value[random.nextInt(value.length)] + } else { + random.nextInt(1, 65536).toChar() + } + return buildString { + append(value.substring(0, position)) + if (random.nextBoolean()) { + append(charToMutate - random.nextInt(1, 128)) + } else { + append(charToMutate + random.nextInt(1, 128)) + } + append(value.substring(position, value.length)) + } + } + + private fun tryRemoveChar(random: Random, value: String, position: Int): String? { + if (value.isEmpty() || position >= value.length) return null + val toRemove = random.nextInt(value.length) + return buildString { + append(value.substring(0, toRemove)) + append(value.substring(toRemove + 1, value.length)) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt index cc2e22dba4..1f9a47238d 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt @@ -1,48 +1,33 @@ package org.utbot.fuzzer.providers 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.FuzzedOp import org.utbot.fuzzer.FuzzedParameter import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues import org.utbot.fuzzer.ModelProvider.Companion.yieldValue -import kotlin.random.Random object StringConstantModelProvider : ModelProvider { override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - val random = Random(72923L) description.concreteValues .asSequence() .filter { (classId, _) -> classId == stringClassId } - .forEach { (_, value, op) -> - listOf(value, mutate(random, value as? String, op)) - .asSequence() - .filterNotNull() - .map { UtPrimitiveModel(it) }.forEach { model -> - description.parametersMap.getOrElse(model.classId) { emptyList() }.forEach { index -> - yieldValue(index, model.fuzzed { summary = "%var% = string" }) - } - } + .forEach { (_, value, _) -> + description.parametersMap.getOrElse(stringClassId) { emptyList() }.forEach { index -> + yieldValue(index, UtPrimitiveModel(value).fuzzed { summary = "%var% = string" }) + } } - } - - private fun mutate(random: Random, value: String?, op: FuzzedOp): String? { - if (value == null || value.isEmpty() || op != FuzzedOp.CH) return null - val indexOfMutation = random.nextInt(value.length) - return value.replaceRange(indexOfMutation, indexOfMutation + 1, SingleCharacterSequence(value[indexOfMutation] - random.nextInt(1, 128))) - } - - private class SingleCharacterSequence(private val character: Char) : CharSequence { - override val length: Int - get() = 1 - - override fun get(index: Int): Char = character - - override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { - throw UnsupportedOperationException() - } - + val charsAsStrings = description.concreteValues + .asSequence() + .filter { (classId, _) -> classId == charClassId } + .map { (_, value, _) -> + UtPrimitiveModel((value as Char).toString()).fuzzed { + summary = "%var% = $value" + } + } + yieldAllValues(description.parametersMap.getOrElse(stringClassId) { emptyList() }, charsAsStrings) } } \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt new file mode 100644 index 0000000000..4d9e88b584 --- /dev/null +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt @@ -0,0 +1,62 @@ +package org.utbot.framework.plugin.api + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.utbot.fuzzer.FrequencyRandom +import kotlin.math.abs +import kotlin.random.Random + +class FrequencyRandomTest { + + @Test + fun `default implementation always returns 0`() { + val random = FrequencyRandom() + (0 until 1000).forEach { _ -> + assertEquals(0, random.nextIndex()) + } + } + + @ParameterizedTest(name = "seed{arguments}") + @ValueSource(ints = [0, 100, -123, 99999, 84]) + fun `with default forEach function frequencies is equal`(seed: Int) { + val random = FrequencyRandom(Random(seed)) + val frequencies = listOf(10, 20, 30, 40) + val result = IntArray(frequencies.size) + random.prepare(frequencies) + assertEquals(100, frequencies.sum()) { "In this test every frequency value represents a percent. The sum must be equal to 100" } + assertEquals(frequencies.size, random.size) + val tries = 100_000 + val errors = tries / 100 // 1% + (0 until tries).forEach { _ -> + result[random.nextIndex()]++ + } + val expected = frequencies.map { tries * it / 100} + result.forEachIndexed { index, value -> + assertTrue(abs(expected[index] - value) < errors) { + "The error should not extent $errors for $tries cases, but ${expected[index]} and $value too far" + } + } + } + + @Test + fun `inverting probabilities from the documentation`() { + val frequencies = listOf(20.0, 80.0) + val random = FrequencyRandom(Random(0)).apply { + prepare(frequencies) { 100.0 - it } + } + val result = IntArray(frequencies.size) + val tries = 10_000 + val errors = tries / 100 // 1% + (0 until tries).forEach { _ -> + result[random.nextIndex()]++ + } + result.forEachIndexed { index, value -> + val expected = frequencies[frequencies.size - 1 - index] * errors + assertTrue(abs(value - expected) < errors) { + "The error should not extent 100 for 10 000 cases, but $expected and $value too far" + } + } + } +} \ No newline at end of file From 71aac5e6899113d1252b4e78099b84036849ae9d Mon Sep 17 00:00:00 2001 From: Maksim Pelevin Date: Fri, 12 Aug 2022 11:21:37 +0300 Subject: [PATCH 2/4] Fix tests because StringConstantModelProvider doesn't mutate value anymore --- .../framework/plugin/api/ModelProviderTest.kt | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) 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 782ce6ee38..dbe3225fde 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 @@ -160,27 +160,12 @@ class ModelProviderTest { ) assertEquals(1, models.size) - assertEquals(2, models[0]!!.size) - listOf("nonemptystring", "nonemptystr`ng").forEach { + assertEquals(1, models[0]!!.size) + listOf("nonemptystring").forEach { assertTrue( models[0]!!.contains(UtPrimitiveModel(it))) { "Failed to find string $it in list ${models[0]}" } } } - @Test - fun `test mutation creates the same values between different runs`() { - repeat(10) { - val models = collect(StringConstantModelProvider, - parameters = listOf(stringClassId), - constants = listOf( - FuzzedConcreteValue(stringClassId, "anotherstring", FuzzedOp.CH), - ) - ) - listOf("anotherstring", "anotherskring").forEach { - assertTrue( models[0]!!.contains(UtPrimitiveModel(it))) { "Failed to find string $it in list ${models[0]}" } - } - } - } - @Test @Suppress("unused", "UNUSED_PARAMETER", "RemoveEmptySecondaryConstructorBody") fun `test default object model creation for simple constructors`() { From a2aa85c3ecdb0a61309a5385a9ce617ec3c11c49 Mon Sep 17 00:00:00 2001 From: Maksim Pelevin Date: Thu, 18 Aug 2022 15:52:52 +0300 Subject: [PATCH 3/4] Refactoring fuzzing mutations --- .../kotlin/org/utbot/framework/UtSettings.kt | 7 -- .../org/utbot/engine/UtBotSymbolicEngine.kt | 44 +++-------- .../org/utbot/fuzzer/FuzzerFunctions.kt | 21 ------ .../org/utbot/fuzzer/FrequencyRandom.kt | 41 ---------- .../kotlin/org/utbot/fuzzer/FuzzedValue.kt | 1 - .../main/kotlin/org/utbot/fuzzer/Fuzzer.kt | 74 ++++++++++++++++++- .../org/utbot/fuzzer/FuzzerStatistcs.kt | 71 ++++++++++++++++++ .../kotlin/org/utbot/fuzzer/ModelMutator.kt | 28 +++---- .../org/utbot/fuzzer/RandomExtensions.kt | 36 +++++++++ .../fuzzer/mutators/NumberRandomMutator.kt | 16 +--- .../fuzzer/mutators/StringRandomMutator.kt | 24 +++--- .../framework/plugin/api/ModelMutatorTest.kt | 37 ++++++++++ ...yRandomTest.kt => RandomExtensionsTest.kt} | 42 +++++++---- 13 files changed, 281 insertions(+), 161 deletions(-) delete mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt create mode 100644 utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt rename utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/{FrequencyRandomTest.kt => RandomExtensionsTest.kt} (59%) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt index 16ed4f380b..edbc16f2f5 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt @@ -276,13 +276,6 @@ object UtSettings { */ var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE) - /** - * Fuzzing tries to mutate values using this factor. - * - * If any mutation is successful then counter is reset. - */ - var fuzzingRandomMutationsFactor: Int by getIntProperty(10_000) - /** * Fuzzer tries to generate and run tests during this time. */ 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 259b33083a..4eb56f7ba2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -83,16 +83,18 @@ 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.FrequencyRandom import org.utbot.fuzzer.FuzzedMethodDescription 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.findNextMutatedValue +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 @@ -394,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 ( @@ -407,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 } @@ -438,35 +441,10 @@ class UtBotSymbolicEngine( fuzz(thisMethodDescription, ObjectModelProvider(defaultIdGenerator).apply { limitValuesCreatedByFieldAccessors = 500 }) - } - - val frequencyRandom = FrequencyRandom(Random(221)) - val factor = maxOf(0, UtSettings.fuzzingRandomMutationsFactor) - val fuzzedValueSequence: Sequence> = sequence { - val fvi = fuzzedValues.iterator() - while (fvi.hasNext()) { - yield(fvi.next()) - // if we stuck switch to "next + several mutations" mode - if ((attempts + 1) % (factor / 100) == 0 && coveredInstructionValues.isNotEmpty()) { - for (i in 0 until (factor / 100)) { - findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription)?.let { yield(it) } - } - } - } - // try mutations if fuzzer tried all combinations - var tryLocal = factor - while (tryLocal-- >= 0) { - val value = findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription) - if (value != null) { - val before = coveredInstructionValues.size - yield(value) - if (coveredInstructionValues.size != before) { - tryLocal = factor - } - } - } - } - fuzzedValueSequence.forEach { values -> + }.withMutations( + TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray() + ) + fuzzedValues.forEach { values -> if (controller.job?.isActive == false || System.currentTimeMillis() >= until) { logger.info { "Fuzzing overtime: $methodUnderTest" } return@flow @@ -510,7 +488,7 @@ class UtBotSymbolicEngine( } // Update the seeded values sometimes // This is necessary because some values cannot do a good values in mutation in any case - if (frequencyRandom.random.nextInt(1, 101) <= 50) { + if (random.flipCoin(probability = 50)) { coveredInstructionValues[coverageKey] = values } return@forEach diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt index d4d7237106..3c32087b6b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt @@ -11,7 +11,6 @@ import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.stringClassId import mu.KotlinLogging -import org.utbot.framework.plugin.api.Instruction import soot.BooleanType import soot.ByteType import soot.CharType @@ -43,7 +42,6 @@ import soot.jimple.internal.JNeExpr import soot.jimple.internal.JTableSwitchStmt import soot.jimple.internal.JVirtualInvokeExpr import soot.toolkits.graph.ExceptionalUnitGraph -import kotlin.math.pow private val logger = KotlinLogging.logger {} @@ -253,22 +251,3 @@ 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 findNextMutatedValue( - frequencyRandom: FrequencyRandom, - coverage: LinkedHashMap, List>, - description: FuzzedMethodDescription, -): List? { - if (coverage.isEmpty()) return null - frequencyRandom.prepare(coverage.map { it.key.count }) { 1 / it.toDouble().pow(2) } - val values = coverage.values.drop(frequencyRandom.nextIndex()).firstOrNull() - if (values != null) { - val mutatedValue = defaultMutators().fold(values) { v, mut -> - mut.mutate(description, v, frequencyRandom.random) - } - if (mutatedValue != values) { - return mutatedValue - } - } - return null -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt deleted file mode 100644 index 033b6f2897..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FrequencyRandom.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.utbot.fuzzer - -import kotlin.random.Random - -/** - * Frequency random returns [nextIndex] with given probabilities. - */ -class FrequencyRandom(var random: Random = Random) { - private var total = 1.0 - private var bound: DoubleArray = DoubleArray(1) { 1.0 } - val size: Int - get() = bound.size - - /** - * Updates internals to generate values using [frequencies] and custom function [forEach]. - * - * For example, it is possible implement logic where the source list of [frequencies] is inverted. - * - * ``` - * prepare(listOf(20., 80.), forEach = { 100 - it }) - * - * In this case second value (that has 80.)in the [frequencies] list will be found in 20% of all cases. - */ - fun prepare(frequencies: List, forEach: (T) -> Double = { it.toDouble() }) { - bound = DoubleArray(frequencies.size) { forEach(frequencies[it]) } - for (i in 1 until bound.size) { - bound[i] = bound[i] + bound[i - 1] - } - total = if (bound.isEmpty()) 0.0 else bound.last() - } - - fun nextIndex(): Int { - val value = random.nextDouble(total) - for (index in bound.indices) { - if (value < bound[index]) { - return index - } - } - error("Cannot find next index") - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt index d480eb5d52..0414fc99fe 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -9,7 +9,6 @@ import org.utbot.framework.plugin.api.UtModel class FuzzedValue( val model: UtModel, val createdBy: ModelProvider? = null, - val mutatedBy: List = emptyList(), ) { /** 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 80f9cb2eae..1349876984 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -96,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> { if (modelProviders.isEmpty()) { throw IllegalArgumentException("At least one model provider is required") @@ -117,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 is not updated by this method, but can be changed by caller. + */ +fun Sequence>.withMutations(statistics: FuzzerStatistics, 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 doesn't recover new paths. + // So, fuzzing tries to mutate values on each loop + // if there are too much 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. */ @@ -150,4 +176,50 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator): Model ) } -fun defaultMutators(): List = listOf(StringRandomMutator(50), NumberRandomMutator(50)) \ No newline at end of file +fun defaultModelMutators(): List = listOf(StringRandomMutator, NumberRandomMutator) + +/** + * Tries to mutate a random value from the seed. + * + * Returns `null` if didn't try to do any mutation. + */ +fun mutateRandomValueOrNull( + statistics: FuzzerStatistics, + description: FuzzedMethodDescription, + mutators: List = defaultModelMutators(), + random: Random = Random, +): List? { + if (mutators.isEmpty()) return null + val values = statistics.takeIf { it.isNotEmpty() }?.randomValues(random) ?: return null + val newValues = values.toMutableList() + mutators.asSequence() + .forEach { mut -> + mut.mutate(description, values, random).forEach { (index, value) -> + newValues[index] = value + } + } + return newValues.takeIf { it != values } +} + +/** + * 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 SequenceScope>.yieldMutated( + statistics: FuzzerStatistics, + description: FuzzedMethodDescription, + mutators: List, + random: Random +) : Boolean { + mutateRandomValueOrNull(statistics, description, mutators, random)?.let { + yield(it) + return true + } + return false +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt new file mode 100644 index 0000000000..521fa7457d --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt @@ -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 { + + val seeds: Collection + + /** + * Returns a random seed to process. + */ + fun randomSeed(random: Random): K? + + fun randomValues(random: Random): List? + + fun executions(seed: K): Int + + operator fun get(seed: K): List? + + fun isEmpty(): Boolean + + fun isNotEmpty(): Boolean { + return !isEmpty() + } +} + +class TrieBasedFuzzerStatistics( + private val values: LinkedHashMap, List> = linkedMapOf() +) : FuzzerStatistics> { + + override val seeds: Collection> + get() = values.keys + + override fun randomSeed(random: Random): Trie.Node? { + 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? { + 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): List? { + return values[seed] + } + + override fun executions(seed: Trie.Node): Int { + return seed.count + } + +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt index 718dfb7235..0e2e5abb4e 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt @@ -5,17 +5,9 @@ import kotlin.random.Random /** * Mutates values and returns it. - * - * Mutator can be not applied using [probability] as pivot. - * In this case unchanged values is returned. */ interface ModelMutator { - /** - * The probability of applying this mutator. Can be ignored in some implementations. - */ - val probability: Int - /** * Mutates values set. * @@ -25,16 +17,20 @@ interface ModelMutator { description: FuzzedMethodDescription, parameters: List, random: Random, - ) : List { - return parameters.mapIndexed { index, fuzzedValue -> - mutate(description, index, fuzzedValue, random) ?: fuzzedValue - } + ) : List { + return parameters + .asSequence() + .mapIndexed { index, fuzzedValue -> + mutate(description, index, fuzzedValue, random)?.let { mutated -> + FuzzedParameter(index, mutated) + } + } + .filterNotNull() + .toList() } /** * Mutate a single value if it is possible. - * - * Default implementation mutates the value with given [probability] or returns `null`. */ fun mutate( description: FuzzedMethodDescription, @@ -42,10 +38,10 @@ interface ModelMutator { value: FuzzedValue, random: Random ) : FuzzedValue? { - return if (random.nextInt(1, 101) < probability) value else null + return null } fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue { - return FuzzedValue(this, template.createdBy, template.mutatedBy + this@ModelMutator).apply(block) + return FuzzedValue(this, template.createdBy).apply(block) } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt new file mode 100644 index 0000000000..61e022bb4d --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt @@ -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() + 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) +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt index c9eac811a7..9634d46112 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt @@ -4,16 +4,13 @@ import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelMutator +import org.utbot.fuzzer.invertBit import kotlin.random.Random /** * Mutates any [Number] changing random bit. */ -class NumberRandomMutator(override val probability: Int) : ModelMutator { - - init { - check(probability in 0 .. 100) { "Probability must be in range 0..100" } - } +object NumberRandomMutator : ModelMutator { override fun mutate( description: FuzzedMethodDescription, @@ -21,7 +18,6 @@ class NumberRandomMutator(override val probability: Int) : ModelMutator { value: FuzzedValue, random: Random ): FuzzedValue? { - if (random.nextInt(1, 101) <= probability) return null val model = value.model return if (model is UtPrimitiveModel && model.value is Number) { val newValue = changeRandomBit(random, model.value as Number) @@ -49,7 +45,7 @@ class NumberRandomMutator(override val probability: Int) : ModelMutator { else -> error("Unknown type: ${number.javaClass}") } val bitIndex = random.nextInt(size) - val mutated = invertBit(asLong, bitIndex) + val mutated = asLong.invertBit(bitIndex) return when (number) { is Byte -> mutated.toByte() is Short -> mutated.toShort() @@ -60,11 +56,5 @@ class NumberRandomMutator(override val probability: Int) : ModelMutator { else -> error("Unknown type: ${number.javaClass}") } } - - private fun invertBit(value: Long, bitIndex: Int): Long { - val b = ((value shr bitIndex) and 1).inv() - val mask = 1L shl bitIndex - return value and mask.inv() or (b shl bitIndex and mask) - } } diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt index 360fc16917..a709ceba65 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt @@ -5,18 +5,13 @@ import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelMutator +import org.utbot.fuzzer.flipCoin import kotlin.random.Random /** * Mutates string by adding and/or removal symbol at random position. - * - * Adding or removal can be applied with a given [probability]. */ -class StringRandomMutator(override val probability: Int = 50) : ModelMutator { - - init { - check(probability in 0 .. 100) { "Probability must be in range 0..100" } - } +object StringRandomMutator : ModelMutator { override fun mutate( description: FuzzedMethodDescription, @@ -39,10 +34,10 @@ class StringRandomMutator(override val probability: Int = 50) : ModelMutator { // we can miss some mutation for a purpose val position = random.nextInt(string.length + 1) var result: String = string - if (random.nextInt(1, 101) <= probability) { + if (random.flipCoin(probability = 50)) { result = tryRemoveChar(random, result, position) ?: string } - if (random.nextInt(1, 101) <= probability) { + if (random.flipCoin(probability = 50)) { result = tryAddChar(random, result, position) } return result @@ -52,21 +47,24 @@ class StringRandomMutator(override val probability: Int = 50) : ModelMutator { val charToMutate = if (value.isNotEmpty()) { value[random.nextInt(value.length)] } else { - random.nextInt(1, 65536).toChar() + // use any meaningful character from the ascii table + random.nextInt(33, 127).toChar() } return buildString { append(value.substring(0, position)) + // try to change char to some that is close enough to origin char + val charTableSpread = 64 if (random.nextBoolean()) { - append(charToMutate - random.nextInt(1, 128)) + append(charToMutate - random.nextInt(1, charTableSpread)) } else { - append(charToMutate + random.nextInt(1, 128)) + append(charToMutate + random.nextInt(1, charTableSpread)) } append(value.substring(position, value.length)) } } private fun tryRemoveChar(random: Random, value: String, position: Int): String? { - if (value.isEmpty() || position >= value.length) return null + if (position >= value.length) return null val toRemove = random.nextInt(value.length) return buildString { append(value.substring(0, toRemove)) diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt new file mode 100644 index 0000000000..35bc58d284 --- /dev/null +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt @@ -0,0 +1,37 @@ +package org.utbot.framework.plugin.api + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.utbot.fuzzer.invertBit +import kotlin.random.Random + +class ModelMutatorTest { + + @Test + fun `invert bit works for long`() { + var attempts = 100_000 + val random = Random(2210) + sequence { + while (true) { + yield(random.nextLong()) + } + }.forEach { value -> + if (attempts-- <= 0) { return } + for (bit in 0 until Long.SIZE_BITS) { + val newValue = value.invertBit(bit) + val oldBinary = value.toBinaryString() + val newBinary = newValue.toBinaryString() + assertEquals(oldBinary.length, newBinary.length) + for (test in Long.SIZE_BITS - 1 downTo 0) { + if (test != Long.SIZE_BITS - 1 - bit) { + assertEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } + } else { + assertNotEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } + } + } + } + } + } + + private fun Long.toBinaryString() = java.lang.Long.toBinaryString(this).padStart(Long.SIZE_BITS, '0') +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt similarity index 59% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt rename to utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt index 4d9e88b584..764d13219d 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FrequencyRandomTest.kt +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt @@ -4,33 +4,32 @@ import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.utbot.fuzzer.FrequencyRandom +import org.utbot.fuzzer.chooseOne +import org.utbot.fuzzer.flipCoin import kotlin.math.abs import kotlin.random.Random -class FrequencyRandomTest { +class RandomExtensionsTest { @Test fun `default implementation always returns 0`() { - val random = FrequencyRandom() + val frequencies = doubleArrayOf(1.0) (0 until 1000).forEach { _ -> - assertEquals(0, random.nextIndex()) + assertEquals(0, Random.chooseOne(frequencies)) } } @ParameterizedTest(name = "seed{arguments}") @ValueSource(ints = [0, 100, -123, 99999, 84]) fun `with default forEach function frequencies is equal`(seed: Int) { - val random = FrequencyRandom(Random(seed)) - val frequencies = listOf(10, 20, 30, 40) + val random = Random(seed) + val frequencies = doubleArrayOf(10.0, 20.0, 30.0, 40.0) val result = IntArray(frequencies.size) - random.prepare(frequencies) - assertEquals(100, frequencies.sum()) { "In this test every frequency value represents a percent. The sum must be equal to 100" } - assertEquals(frequencies.size, random.size) + assertEquals(100.0, frequencies.sum()) { "In this test every frequency value represents a percent. The sum must be equal to 100" } val tries = 100_000 val errors = tries / 100 // 1% (0 until tries).forEach { _ -> - result[random.nextIndex()]++ + result[random.chooseOne(frequencies)]++ } val expected = frequencies.map { tries * it / 100} result.forEachIndexed { index, value -> @@ -42,15 +41,13 @@ class FrequencyRandomTest { @Test fun `inverting probabilities from the documentation`() { - val frequencies = listOf(20.0, 80.0) - val random = FrequencyRandom(Random(0)).apply { - prepare(frequencies) { 100.0 - it } - } + val frequencies = doubleArrayOf(20.0, 80.0) + val random = Random(0) val result = IntArray(frequencies.size) val tries = 10_000 val errors = tries / 100 // 1% (0 until tries).forEach { _ -> - result[random.nextIndex()]++ + result[random.chooseOne(DoubleArray(frequencies.size) { 100.0 - frequencies[it] })]++ } result.forEachIndexed { index, value -> val expected = frequencies[frequencies.size - 1 - index] * errors @@ -59,4 +56,19 @@ class FrequencyRandomTest { } } } + + @Test + fun `flip the coin is fair enough`() { + val random = Random(0) + var result = 0 + val probability = 20 + val experiments = 1_000_000 + for (i in 0 until experiments) { + if (random.flipCoin(probability)) { + result++ + } + } + val error = experiments / 1000 // 0.1 % + assertTrue(abs(result - experiments * probability / 100) < error) + } } \ No newline at end of file From 2716a86a819b7954d397b9165770578f30a112fd Mon Sep 17 00:00:00 2001 From: Maksim Pelevin Date: Tue, 23 Aug 2022 13:54:46 +0300 Subject: [PATCH 4/4] Small changes after review --- .../src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt | 13 +++++++------ .../{FuzzerStatistcs.kt => FuzzerStatistics.kt} | 0 .../main/kotlin/org/utbot/fuzzer/ModelMutator.kt | 3 +-- .../utbot/fuzzer/mutators/StringRandomMutator.kt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) rename utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/{FuzzerStatistcs.kt => FuzzerStatistics.kt} (100%) 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 1349876984..35a5c67ca8 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -124,7 +124,7 @@ fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvi * 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 is not updated by this method, but can be changed by caller. + * [statistics] should not be updated by this method, but can be changed by caller. */ fun Sequence>.withMutations(statistics: FuzzerStatistics, description: FuzzedMethodDescription, vararg mutators: ModelMutator) = sequence { val fvi = iterator() @@ -133,9 +133,9 @@ fun Sequence>.withMutations(statistics: FuzzerStatistics mutateRandomValueOrNull( ): List? { if (mutators.isEmpty()) return null val values = statistics.takeIf { it.isNotEmpty() }?.randomValues(random) ?: return null - val newValues = values.toMutableList() + var newValues :MutableList? = null mutators.asSequence() .forEach { mut -> mut.mutate(description, values, random).forEach { (index, value) -> - newValues[index] = value + newValues = (newValues ?: values.toMutableList()) + newValues?.set(index, value) } } - return newValues.takeIf { it != values } + return newValues } /** diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt similarity index 100% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistcs.kt rename to utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt index 0e2e5abb4e..8f9db6f05f 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt @@ -20,12 +20,11 @@ interface ModelMutator { ) : List { return parameters .asSequence() - .mapIndexed { index, fuzzedValue -> + .mapIndexedNotNull { index, fuzzedValue -> mutate(description, index, fuzzedValue, random)?.let { mutated -> FuzzedParameter(index, mutated) } } - .filterNotNull() .toList() } diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt index a709ceba65..1b05cf7826 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt @@ -45,7 +45,7 @@ object StringRandomMutator : ModelMutator { private fun tryAddChar(random: Random, value: String, position: Int): String { val charToMutate = if (value.isNotEmpty()) { - value[random.nextInt(value.length)] + value.random(random) } else { // use any meaningful character from the ascii table random.nextInt(33, 127).toChar()