Skip to content

Commit a2aa85c

Browse files
committed
Refactoring fuzzing mutations
1 parent 71aac5e commit a2aa85c

File tree

13 files changed

+281
-161
lines changed

13 files changed

+281
-161
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,6 @@ object UtSettings {
276276
*/
277277
var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE)
278278

279-
/**
280-
* Fuzzing tries to mutate values using this factor.
281-
*
282-
* If any mutation is successful then counter is reset.
283-
*/
284-
var fuzzingRandomMutationsFactor: Int by getIntProperty(10_000)
285-
286279
/**
287280
* Fuzzer tries to generate and run tests during this time.
288281
*/

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

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,18 @@ import org.utbot.framework.plugin.api.util.description
8383
import org.utbot.framework.util.jimpleBody
8484
import org.utbot.framework.plugin.api.util.voidClassId
8585
import org.utbot.fuzzer.FallbackModelProvider
86-
import org.utbot.fuzzer.FrequencyRandom
8786
import org.utbot.fuzzer.FuzzedMethodDescription
8887
import org.utbot.fuzzer.FuzzedValue
8988
import org.utbot.fuzzer.ModelProvider
9089
import org.utbot.fuzzer.ReferencePreservingIntIdGenerator
9190
import org.utbot.fuzzer.Trie
91+
import org.utbot.fuzzer.TrieBasedFuzzerStatistics
9292
import org.utbot.fuzzer.UtFuzzedExecution
93+
import org.utbot.fuzzer.withMutations
9394
import org.utbot.fuzzer.collectConstantsForFuzzer
9495
import org.utbot.fuzzer.defaultModelProviders
95-
import org.utbot.fuzzer.findNextMutatedValue
96+
import org.utbot.fuzzer.defaultModelMutators
97+
import org.utbot.fuzzer.flipCoin
9698
import org.utbot.fuzzer.fuzz
9799
import org.utbot.fuzzer.providers.ObjectModelProvider
98100
import org.utbot.instrumentation.ConcreteExecutor
@@ -394,6 +396,7 @@ class UtBotSymbolicEngine(
394396
val fallbackModelProvider = FallbackModelProvider(defaultIdGenerator)
395397
val constantValues = collectConstantsForFuzzer(graph)
396398

399+
val random = Random(0)
397400
val thisInstance = when {
398401
methodUnderTest.isStatic -> null
399402
methodUnderTest.isConstructor -> if (
@@ -407,7 +410,7 @@ class UtBotSymbolicEngine(
407410
else -> {
408411
ObjectModelProvider(defaultIdGenerator).withFallback(fallbackModelProvider).generate(
409412
FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues)
410-
).take(10).shuffled(Random(0)).map { it.value.model }.first().apply {
413+
).take(10).shuffled(random).map { it.value.model }.first().apply {
411414
if (this is UtNullModel) { // it will definitely fail because of NPE,
412415
return@flow
413416
}
@@ -438,35 +441,10 @@ class UtBotSymbolicEngine(
438441
fuzz(thisMethodDescription, ObjectModelProvider(defaultIdGenerator).apply {
439442
limitValuesCreatedByFieldAccessors = 500
440443
})
441-
}
442-
443-
val frequencyRandom = FrequencyRandom(Random(221))
444-
val factor = maxOf(0, UtSettings.fuzzingRandomMutationsFactor)
445-
val fuzzedValueSequence: Sequence<List<FuzzedValue>> = sequence {
446-
val fvi = fuzzedValues.iterator()
447-
while (fvi.hasNext()) {
448-
yield(fvi.next())
449-
// if we stuck switch to "next + several mutations" mode
450-
if ((attempts + 1) % (factor / 100) == 0 && coveredInstructionValues.isNotEmpty()) {
451-
for (i in 0 until (factor / 100)) {
452-
findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription)?.let { yield(it) }
453-
}
454-
}
455-
}
456-
// try mutations if fuzzer tried all combinations
457-
var tryLocal = factor
458-
while (tryLocal-- >= 0) {
459-
val value = findNextMutatedValue(frequencyRandom, coveredInstructionValues, methodUnderTestDescription)
460-
if (value != null) {
461-
val before = coveredInstructionValues.size
462-
yield(value)
463-
if (coveredInstructionValues.size != before) {
464-
tryLocal = factor
465-
}
466-
}
467-
}
468-
}
469-
fuzzedValueSequence.forEach { values ->
444+
}.withMutations(
445+
TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray()
446+
)
447+
fuzzedValues.forEach { values ->
470448
if (controller.job?.isActive == false || System.currentTimeMillis() >= until) {
471449
logger.info { "Fuzzing overtime: $methodUnderTest" }
472450
return@flow
@@ -510,7 +488,7 @@ class UtBotSymbolicEngine(
510488
}
511489
// Update the seeded values sometimes
512490
// This is necessary because some values cannot do a good values in mutation in any case
513-
if (frequencyRandom.random.nextInt(1, 101) <= 50) {
491+
if (random.flipCoin(probability = 50)) {
514492
coveredInstructionValues[coverageKey] = values
515493
}
516494
return@forEach

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import org.utbot.framework.plugin.api.util.longClassId
1111
import org.utbot.framework.plugin.api.util.shortClassId
1212
import org.utbot.framework.plugin.api.util.stringClassId
1313
import mu.KotlinLogging
14-
import org.utbot.framework.plugin.api.Instruction
1514
import soot.BooleanType
1615
import soot.ByteType
1716
import soot.CharType
@@ -43,7 +42,6 @@ import soot.jimple.internal.JNeExpr
4342
import soot.jimple.internal.JTableSwitchStmt
4443
import soot.jimple.internal.JVirtualInvokeExpr
4544
import soot.toolkits.graph.ExceptionalUnitGraph
46-
import kotlin.math.pow
4745

4846
private val logger = KotlinLogging.logger {}
4947

@@ -253,22 +251,3 @@ private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) {
253251
}
254252

255253
private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()
256-
257-
fun findNextMutatedValue(
258-
frequencyRandom: FrequencyRandom,
259-
coverage: LinkedHashMap<Trie.Node<Instruction>, List<FuzzedValue>>,
260-
description: FuzzedMethodDescription,
261-
): List<FuzzedValue>? {
262-
if (coverage.isEmpty()) return null
263-
frequencyRandom.prepare(coverage.map { it.key.count }) { 1 / it.toDouble().pow(2) }
264-
val values = coverage.values.drop(frequencyRandom.nextIndex()).firstOrNull()
265-
if (values != null) {
266-
val mutatedValue = defaultMutators().fold(values) { v, mut ->
267-
mut.mutate(description, v, frequencyRandom.random)
268-
}
269-
if (mutatedValue != values) {
270-
return mutatedValue
271-
}
272-
}
273-
return null
274-
}

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

Lines changed: 0 additions & 41 deletions
This file was deleted.

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import org.utbot.framework.plugin.api.UtModel
99
class FuzzedValue(
1010
val model: UtModel,
1111
val createdBy: ModelProvider? = null,
12-
val mutatedBy: List<ModelMutator> = emptyList(),
1312
) {
1413

1514
/**

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

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ class ReferencePreservingIntIdGenerator(lowerBound: Int = DEFAULT_LOWER_BOUND) :
9696
}
9797
}
9898

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

123+
/**
124+
* Wraps sequence of values, iterates through them and mutates.
125+
*
126+
* Mutation when possible is generated after every value of source sequence and then supplies values until it needed.
127+
* [statistics] should is not updated by this method, but can be changed by caller.
128+
*/
129+
fun <T> Sequence<List<FuzzedValue>>.withMutations(statistics: FuzzerStatistics<T>, description: FuzzedMethodDescription, vararg mutators: ModelMutator) = sequence {
130+
val fvi = iterator()
131+
val mutatorList = mutators.toList()
132+
val random = Random(0L)
133+
while (fvi.hasNext()) {
134+
// Takes a value that was generated by model providers and submits it
135+
yield(fvi.next())
136+
// Fuzzing can generate values that doesn't recover new paths.
137+
// So, fuzzing tries to mutate values on each loop
138+
// if there are too much attempts to find new paths without mutations.
139+
yieldMutated(statistics, description, mutatorList, random)
140+
}
141+
// try mutations if fuzzer tried all combinations if any seeds are available
142+
@Suppress("ControlFlowWithEmptyBody")
143+
while (yieldMutated(statistics, description, mutatorList, random)) {}
144+
}
145+
120146
/**
121147
* Creates a model provider from a list of default providers.
122148
*/
@@ -150,4 +176,50 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Model
150176
)
151177
}
152178

153-
fun defaultMutators(): List<ModelMutator> = listOf(StringRandomMutator(50), NumberRandomMutator(50))
179+
fun defaultModelMutators(): List<ModelMutator> = listOf(StringRandomMutator, NumberRandomMutator)
180+
181+
/**
182+
* Tries to mutate a random value from the seed.
183+
*
184+
* Returns `null` if didn't try to do any mutation.
185+
*/
186+
fun <T> mutateRandomValueOrNull(
187+
statistics: FuzzerStatistics<T>,
188+
description: FuzzedMethodDescription,
189+
mutators: List<ModelMutator> = defaultModelMutators(),
190+
random: Random = Random,
191+
): List<FuzzedValue>? {
192+
if (mutators.isEmpty()) return null
193+
val values = statistics.takeIf { it.isNotEmpty() }?.randomValues(random) ?: return null
194+
val newValues = values.toMutableList()
195+
mutators.asSequence()
196+
.forEach { mut ->
197+
mut.mutate(description, values, random).forEach { (index, value) ->
198+
newValues[index] = value
199+
}
200+
}
201+
return newValues.takeIf { it != values }
202+
}
203+
204+
/**
205+
* Run mutations and yields values into the sequence.
206+
*
207+
* Mutations are supplied infinitely until [repeat] returns true. [repeat] is run before mutation.
208+
*
209+
* @param statistics coverage-based seed
210+
* @param description method description
211+
* @param mutators mutators which are applied to the random value
212+
* @param random instance that is used to choose random index from the [statistics]
213+
*/
214+
suspend fun <T> SequenceScope<List<FuzzedValue>>.yieldMutated(
215+
statistics: FuzzerStatistics<T>,
216+
description: FuzzedMethodDescription,
217+
mutators: List<ModelMutator>,
218+
random: Random
219+
) : Boolean {
220+
mutateRandomValueOrNull(statistics, description, mutators, random)?.let {
221+
yield(it)
222+
return true
223+
}
224+
return false
225+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.utbot.fuzzer
2+
3+
import kotlin.math.pow
4+
import kotlin.random.Random
5+
6+
/**
7+
* Stores information that can be useful for fuzzing such as coverage, run count, etc.
8+
*/
9+
interface FuzzerStatistics<K> {
10+
11+
val seeds: Collection<K>
12+
13+
/**
14+
* Returns a random seed to process.
15+
*/
16+
fun randomSeed(random: Random): K?
17+
18+
fun randomValues(random: Random): List<FuzzedValue>?
19+
20+
fun executions(seed: K): Int
21+
22+
operator fun get(seed: K): List<FuzzedValue>?
23+
24+
fun isEmpty(): Boolean
25+
26+
fun isNotEmpty(): Boolean {
27+
return !isEmpty()
28+
}
29+
}
30+
31+
class TrieBasedFuzzerStatistics<V>(
32+
private val values: LinkedHashMap<Trie.Node<V>, List<FuzzedValue>> = linkedMapOf()
33+
) : FuzzerStatistics<Trie.Node<V>> {
34+
35+
override val seeds: Collection<Trie.Node<V>>
36+
get() = values.keys
37+
38+
override fun randomSeed(random: Random): Trie.Node<V>? {
39+
return values.keys.elementAtOrNull(randomIndex(random))
40+
}
41+
42+
override fun isEmpty(): Boolean {
43+
return values.isEmpty()
44+
}
45+
46+
override fun isNotEmpty(): Boolean {
47+
return values.isNotEmpty()
48+
}
49+
50+
override fun randomValues(random: Random): List<FuzzedValue>? {
51+
return values.values.elementAtOrNull(randomIndex(random))
52+
}
53+
54+
private fun randomIndex(random: Random): Int {
55+
val frequencies = DoubleArray(values.size).also { f ->
56+
values.keys.forEachIndexed { index, key ->
57+
f[index] = 1 / key.count.toDouble().pow(2)
58+
}
59+
}
60+
return random.chooseOne(frequencies)
61+
}
62+
63+
override fun get(seed: Trie.Node<V>): List<FuzzedValue>? {
64+
return values[seed]
65+
}
66+
67+
override fun executions(seed: Trie.Node<V>): Int {
68+
return seed.count
69+
}
70+
71+
}

0 commit comments

Comments
 (0)