Skip to content

Commit 8ecb438

Browse files
committed
Introduce fork for fuzzing tasks
1 parent b0a6da0 commit 8ecb438

File tree

10 files changed

+199
-134
lines changed

10 files changed

+199
-134
lines changed

utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt

Lines changed: 90 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@file:JvmName("FuzzingApi")
22
package org.utbot.fuzzing
33

4-
import kotlinx.coroutines.yield
4+
import kotlinx.coroutines.*
55
import mu.KotlinLogging
66
import org.utbot.fuzzing.seeds.KnownValue
77
import org.utbot.fuzzing.utils.MissedSeed
@@ -47,24 +47,31 @@ interface Fuzzing<TYPE, RESULT, DESCRIPTION : Description<TYPE>, FEEDBACK : Feed
4747
suspend fun handle(description: DESCRIPTION, values: List<RESULT>): FEEDBACK
4848

4949
/**
50-
* This method is called before every fuzzing attempt.
51-
*
52-
* Usually, is used to update configuration or manipulate some data in the statistics
53-
* (for example, clear some [Statistic.missedTypes] types).
54-
*
55-
* @param description contains user-defined information about current run. Can be used as a state of the run.
56-
* @param statistic statistic about fuzzing generation like elapsed time or number of the runs.
57-
* @param configuration current used configuration; it can be changes for tuning fuzzing.
50+
* Starts fuzzing with new description but with copy of [Statistic].
51+
*/
52+
suspend fun fork(description: DESCRIPTION, statistics: Statistic<TYPE, RESULT>) {
53+
fuzz(description, StatisticImpl(statistics))
54+
}
55+
56+
/**
57+
* Checks whether the fuzzer should stop.
5858
*/
59-
suspend fun update(description: DESCRIPTION, statistic: Statistic<TYPE, RESULT>, configuration: Configuration) {}
59+
suspend fun isCancelled(description: DESCRIPTION, stats: Statistic<TYPE, RESULT>): Boolean {
60+
return description.parameters.isEmpty()
61+
}
62+
63+
suspend fun beforeIteration(description: DESCRIPTION, statistics: Statistic<TYPE, RESULT>) { }
64+
suspend fun afterIteration(description: DESCRIPTION, statistics: Statistic<TYPE, RESULT>) { }
6065
}
6166

6267
/**
6368
* Some description of current fuzzing run. Usually, it contains the name of the target method and its parameter list.
6469
*/
6570
open class Description<TYPE>(
66-
val parameters: List<TYPE>
67-
)
71+
parameters: List<TYPE>
72+
) {
73+
val parameters: List<TYPE> = parameters.toList()
74+
}
6875

6976
/**
7077
* Input value that fuzzing knows how to build and use them.
@@ -115,7 +122,7 @@ sealed interface Seed<TYPE, RESULT> {
115122
* Routine is a task that is used to build a value.
116123
*
117124
* There are several types of a routine, which all are generally only functions.
118-
* These function accepts some data and generates target value.
125+
* These functions accept some data and generate target value.
119126
*/
120127
sealed class Routine<T, R>(val types: List<T>) : Iterable<T> by types {
121128

@@ -211,13 +218,6 @@ enum class Control {
211218
*/
212219
CONTINUE,
213220

214-
/**
215-
* Reset type cache and continue.
216-
*
217-
* Current seed and result will be analysed and cached.
218-
*/
219-
RESET_TYPE_CACHE_AND_CONTINUE,
220-
221221
/**
222222
* Do not process this feedback and just start next value generation.
223223
*/
@@ -253,81 +253,71 @@ class NoSeedValueException internal constructor(
253253
val type: Any?
254254
) : Exception("No seed candidates generated for type: $type")
255255

256+
suspend fun <T, R, D : Description<T>, F : Feedback<T, R>> Fuzzing<T, R, D, F>.fuzz(
257+
description: D,
258+
random: Random = Random(0),
259+
configuration: Configuration = Configuration()
260+
) {
261+
fuzz(description, StatisticImpl(random = random, configuration = configuration))
262+
}
263+
256264
/**
257265
* Starts fuzzing for this [Fuzzing] object.
258266
*
259267
* This is an entry point for every fuzzing.
260268
*/
261-
suspend fun <T, R, D : Description<T>, F : Feedback<T, R>> Fuzzing<T, R, D, F>.fuzz(
269+
private suspend fun <T, R, D : Description<T>, F : Feedback<T, R>> Fuzzing<T, R, D, F>.fuzz(
262270
description: D,
263-
random: Random = Random(0),
264-
configuration: Configuration = Configuration()
271+
statistic: StatisticImpl<T, R, F>,
265272
) {
266-
class StatImpl(
267-
override var totalRuns: Long = 0,
268-
val startTime: Long = System.nanoTime(),
269-
override var missedTypes: MissedSeed<T, R> = MissedSeed(),
270-
) : Statistic<T, R> {
271-
override val elapsedTime: Long
272-
get() = System.nanoTime() - startTime
273-
}
274-
val userStatistic = StatImpl()
273+
val random = statistic.random
274+
val configuration = statistic.configuration
275275
val fuzzing = this
276276
val typeCache = hashMapOf<T, List<Seed<T, R>>>()
277-
fun fuzzOne(): Node<T, R> = fuzz(
278-
parameters = description.parameters,
277+
fun fuzzOne(parameters: List<T>): Node<T, R> = fuzz(
278+
parameters = parameters,
279279
fuzzing = fuzzing,
280280
description = description,
281281
random = random,
282282
configuration = configuration,
283283
builder = PassRoutine("Main Routine"),
284-
state = State(1, typeCache, userStatistic.missedTypes),
284+
state = State(1, typeCache, statistic.missedTypes),
285285
)
286-
val dynamicallyGenerated = mutableListOf<Node<T, R>>()
287-
val seeds = Statistics<T, R, F>()
288-
run breaking@ {
289-
sequence {
290-
while (description.parameters.isNotEmpty()) {
291-
if (dynamicallyGenerated.isNotEmpty()) {
292-
yield(dynamicallyGenerated.removeFirst())
293-
} else {
294-
val fuzzOne = fuzzOne()
295-
// fuzz one value, seems to be bad, when have only a few and simple values
296-
yield(fuzzOne)
297-
298-
val randomSeed = seeds.getRandomSeed(random, configuration)
299-
if (randomSeed != null) {
300-
dynamicallyGenerated += mutate(
301-
randomSeed,
302-
fuzzing,
303-
random,
304-
configuration,
305-
State(1, typeCache, userStatistic.missedTypes)
306-
)
307-
}
308-
}
286+
287+
while (!fuzzing.isCancelled(description, statistic)) {
288+
beforeIteration(description, statistic)
289+
val seed = if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) {
290+
statistic.getRandomSeed(random, configuration)
291+
} else {
292+
val actualParameters = description.parameters
293+
// fuzz one value, seems to be bad, when have only a few and simple values
294+
fuzzOne(actualParameters)
295+
}
296+
val values = mutate(
297+
seed,
298+
fuzzing,
299+
random,
300+
configuration,
301+
State(1, typeCache, statistic.missedTypes)
302+
)
303+
afterIteration(description, statistic)
304+
305+
yield()
306+
statistic.apply {
307+
totalRuns++
308+
}
309+
check(values.parameters.size == values.result.size) { "Cannot create value for ${values.parameters}" }
310+
val valuesCache = mutableMapOf<Result<T, R>, R>()
311+
val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } }
312+
val feedback = fuzzing.handle(description, result)
313+
when (feedback.control) {
314+
Control.CONTINUE -> {
315+
statistic.put(random, configuration, feedback, values)
309316
}
310-
}.forEach execution@ { values ->
311-
yield()
312-
fuzzing.update(description, userStatistic.apply {
313-
totalRuns++
314-
}, configuration)
315-
check(values.parameters.size == values.result.size) { "Cannot create value for ${values.parameters}" }
316-
val valuesCache = mutableMapOf<Result<T, R>, R>()
317-
val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } }
318-
val feedback = fuzzing.handle(description, result)
319-
when (feedback.control) {
320-
Control.CONTINUE -> {
321-
seeds.put(random, configuration, feedback, values)
322-
}
323-
Control.RESET_TYPE_CACHE_AND_CONTINUE -> {
324-
dynamicallyGenerated.clear()
325-
typeCache.clear()
326-
seeds.put(random, configuration, feedback, values)
327-
}
328-
Control.STOP -> { return@breaking }
329-
Control.PASS -> {}
317+
Control.STOP -> {
318+
break
330319
}
320+
Control.PASS -> {}
331321
}
332322
}
333323
}
@@ -678,8 +668,24 @@ private class Node<TYPE, RESULT>(
678668
val builder: Routine<TYPE, RESULT>,
679669
)
680670

671+
private class StatisticImpl<TYPE, RESULT, FEEDBACK : Feedback<TYPE, RESULT>>(
672+
override var totalRuns: Long = 0,
673+
override val startTime: Long = System.nanoTime(),
674+
override var missedTypes: MissedSeed<TYPE, RESULT> = MissedSeed(),
675+
override val random: Random,
676+
override val configuration: Configuration,
677+
) : Statistic<TYPE, RESULT> {
678+
679+
constructor(source: Statistic<TYPE, RESULT>) : this(
680+
totalRuns = source.totalRuns,
681+
startTime = source.startTime,
682+
missedTypes = source.missedTypes,
683+
random = source.random,
684+
configuration = source.configuration.copy(),
685+
)
681686

682-
private class Statistics<TYPE, RESULT, FEEDBACK : Feedback<TYPE, RESULT>> {
687+
override val elapsedTime: Long
688+
get() = System.nanoTime() - startTime
683689
private val seeds = linkedMapOf<FEEDBACK, Node<TYPE, RESULT>>()
684690
private val count = linkedMapOf<FEEDBACK, Long>()
685691

@@ -692,8 +698,8 @@ private class Statistics<TYPE, RESULT, FEEDBACK : Feedback<TYPE, RESULT>> {
692698
count[feedback] = count.getOrDefault(feedback, 0L) + 1L
693699
}
694700

695-
fun getRandomSeed(random: Random, configuration: Configuration): Node<TYPE, RESULT>? {
696-
if (seeds.isEmpty()) return null
701+
fun getRandomSeed(random: Random, configuration: Configuration): Node<TYPE, RESULT> {
702+
if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed")
697703
val entries = seeds.entries.toList()
698704
val frequencies = DoubleArray(seeds.size).also { f ->
699705
entries.forEachIndexed { index, (key, _) ->
@@ -703,6 +709,7 @@ private class Statistics<TYPE, RESULT, FEEDBACK : Feedback<TYPE, RESULT>> {
703709
val index = random.chooseOne(frequencies)
704710
return entries[index].value
705711
}
706-
}
707712

708-
///endregion
713+
fun isNotEmpty() = seeds.isNotEmpty()
714+
}
715+
///endregion

utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import kotlin.math.pow
55
/**
66
* Configures fuzzing behaviour. Usually, it is not required to tune anything.
77
*/
8-
class Configuration(
8+
data class Configuration(
9+
10+
/**
11+
* Choose between already generated values and new generation of values.
12+
*/
13+
var probSeedRetrievingInsteadGenerating: Int = 70,
14+
915
/**
1016
* Fuzzer creates a tree of object for generating values. At some point this recursion should be stopped.
1117
*

utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ class BaseFuzzing<T, R, D : Description<T>, F : Feedback<T, R>>(
2929
val exec: suspend (description: D, values: List<R>) -> F
3030
) : Fuzzing<T, R, D, F> {
3131

32-
var update: suspend (D, Statistic<T, R>, Configuration) -> Unit = { d, s, c -> super.update(d, s, c) }
33-
3432
constructor(vararg providers: ValueProvider<T, R, D>, exec: suspend (description: D, values: List<R>) -> F) : this(providers.toList(), exec)
3533

3634
override fun generate(description: D, type: T): Sequence<Seed<T, R>> {
@@ -51,10 +49,6 @@ class BaseFuzzing<T, R, D : Description<T>, F : Feedback<T, R>>(
5149
override suspend fun handle(description: D, values: List<R>): F {
5250
return exec(description, values)
5351
}
54-
55-
override suspend fun update(description: D, statistic: Statistic<T, R>, configuration: Configuration) {
56-
update.invoke(description, statistic, configuration)
57-
}
5852
}
5953

6054
/**
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package org.utbot.fuzzing
22

33
import org.utbot.fuzzing.utils.MissedSeed
4+
import kotlin.random.Random
45

56
/**
67
* User class that holds data about current fuzzing running.
7-
*
8-
* Concrete implementation is passed to the [Fuzzing.update].
98
*/
109
interface Statistic<TYPE, RESULT> {
10+
val startTime: Long
1111
val totalRuns: Long
1212
val elapsedTime: Long
1313
val missedTypes: MissedSeed<TYPE, RESULT>
14+
val random: Random
15+
val configuration: Configuration
1416
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.utbot.fuzzing.demo
2+
3+
import kotlinx.coroutines.launch
4+
import kotlinx.coroutines.runBlocking
5+
import org.utbot.fuzzing.*
6+
import java.util.concurrent.atomic.AtomicLong
7+
8+
private enum class Type {
9+
ANY, CONCRETE, MORE_CONCRETE
10+
}
11+
12+
fun main(): Unit = runBlocking {
13+
launch {
14+
object : Fuzzing<Type, String, Description<Type>, Feedback<Type, String>> {
15+
16+
private val runs = mutableMapOf<Type, AtomicLong>()
17+
18+
override fun generate(description: Description<Type>, type: Type): Sequence<Seed<Type, String>> {
19+
return sequenceOf(Seed.Simple(type.name))
20+
}
21+
22+
override suspend fun handle(description: Description<Type>, values: List<String>): Feedback<Type, String> {
23+
description.parameters.forEach {
24+
runs[it]!!.incrementAndGet()
25+
}
26+
println(values)
27+
return emptyFeedback()
28+
}
29+
30+
override suspend fun afterIteration(
31+
description: Description<Type>,
32+
stats: Statistic<Type, String>,
33+
) {
34+
if (stats.totalRuns % 10 == 0L && description.parameters.size == 1) {
35+
val newTypes = when (description.parameters[0]) {
36+
Type.ANY -> listOf(Type.CONCRETE)
37+
Type.CONCRETE -> listOf(Type.MORE_CONCRETE)
38+
Type.MORE_CONCRETE -> listOf()
39+
}
40+
if (newTypes.isNotEmpty()) {
41+
val d = Description(newTypes)
42+
fork(d, stats)
43+
// Description can be used as a transfer object,
44+
// that collects information about the current running.
45+
println("Fork ended: ${d.parameters}")
46+
}
47+
}
48+
}
49+
50+
override suspend fun isCancelled(description: Description<Type>, stats: Statistic<Type, String>): Boolean {
51+
println("info: ${description.parameters} runs ${stats.totalRuns}")
52+
return description.parameters.all { runs.computeIfAbsent(it) { AtomicLong(0) }.get() >= 10 }
53+
}
54+
}.fuzz(Description(listOf(Type.ANY)))
55+
}
56+
// .cancel()
57+
}

utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.utbot.fuzzing.*
44
import org.utbot.fuzzing.seeds.BitVectorValue
55
import org.utbot.fuzzing.seeds.Signed
66
import org.utbot.fuzzing.seeds.StringValue
7+
import java.util.concurrent.atomic.AtomicLong
78
import kotlin.random.Random
89

910
private enum class CustomType {
@@ -32,6 +33,7 @@ private class JsonBuilder(
3233
@Suppress("RemoveExplicitTypeArguments")
3334
suspend fun main() {
3435
var count = 0
36+
val set = mutableMapOf<String, AtomicLong>()
3537
BaseFuzzing<CustomType, JsonBuilder, Description<CustomType>, Feedback<CustomType, JsonBuilder>>(
3638
TypeProvider(CustomType.INT) { _, _ ->
3739
for (b in Signed.values()) {
@@ -79,8 +81,13 @@ suspend fun main() {
7981
}
8082
},
8183
) { _, values ->
82-
println(values)
83-
if (++count < 1000) emptyFeedback() else error("")
84+
val result = values.toString()
85+
println(result)
86+
set.computeIfAbsent(result) { AtomicLong() }.incrementAndGet()
87+
if (++count < 10000) emptyFeedback() else {
88+
println("Duplication ratio:" + set.size / count.toDouble())
89+
error("Forced from the example")
90+
}
8491
}.fuzz(
8592
Description(listOf(CustomType.LST, CustomType.OBJ)),
8693
Random(0),

0 commit comments

Comments
 (0)