From 1f010440bdc9e0762e7507134d6164f9d666a041 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 26 Jul 2023 14:28:07 +0300 Subject: [PATCH 01/25] Add result type as the second generic parameter to Description class: Description => Description --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 18 ++++++------- .../kotlin/org/utbot/fuzzing/Providers.kt | 14 +++++----- .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 6 ++--- .../org/utbot/fuzzing/demo/ForkFuzzing.kt | 12 ++++----- .../org/utbot/fuzzing/demo/HttpRequest.kt | 6 ++--- .../org/utbot/fuzzing/demo/JavaFuzzing.kt | 6 ++--- .../org/utbot/fuzzing/demo/JsonFuzzing.kt | 2 +- .../org/utbot/fuzzing/FuzzerSmokeTest.kt | 26 +++++++++---------- .../kotlin/org/utbot/fuzzing/ProvidersTest.kt | 18 ++++++------- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 2 +- .../org/utbot/fuzzing/JavaFuzzingTest.kt | 2 +- .../org/utbot/python/fuzzing/PythonApi.kt | 2 +- 12 files changed, 57 insertions(+), 57 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index a33cba2ea0..57e2ac953f 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -19,7 +19,7 @@ private val logger by lazy { KotlinLogging.logger {} } * @see [org.utbot.fuzzing.demo.JavaFuzzing] * @see [org.utbot.fuzzing.demo.JsonFuzzingKt] */ -interface Fuzzing, FEEDBACK : Feedback> { +interface Fuzzing, FEEDBACK : Feedback> { /** * Before producing seeds, this method is called to recognize, @@ -76,12 +76,12 @@ interface Fuzzing, FEEDBACK : Feed /** * Some description of the current fuzzing run. Usually, it contains the name of the target method and its parameter list. */ -open class Description( +open class Description( parameters: List ) { val parameters: List = parameters.toList() - open fun clone(scope: Scope): Description { + open fun clone(scope: Scope): Description { error("Scope was changed for $this, but method clone is not specified") } } @@ -298,7 +298,7 @@ class NoSeedValueException internal constructor( get() = "No seed candidates generated for type: $type" } -suspend fun , F : Feedback> Fuzzing.fuzz( +suspend fun , F : Feedback> Fuzzing.fuzz( description: D, random: Random = Random(0), configuration: Configuration = Configuration() @@ -311,7 +311,7 @@ suspend fun , F : Feedback> Fuzzing.f * * This is an entry point for every fuzzing. */ -private suspend fun , F : Feedback> Fuzzing.fuzz( +private suspend fun , F : Feedback> Fuzzing.fuzz( description: D, statistic: StatisticImpl, ) { @@ -372,7 +372,7 @@ private suspend fun , F : Feedback> Fuzzing, FEEDBACK : Feedback> fuzz( +private fun , FEEDBACK : Feedback> fuzz( parameters: List, fuzzing: Fuzzing, description: DESCRIPTION, @@ -399,7 +399,7 @@ private fun , FEEDBACK : Feedback< return Node(result, parameters, builder) } -private fun , FEEDBACK : Feedback> produce( +private fun , FEEDBACK : Feedback> produce( type: TYPE, fuzzing: Fuzzing, description: DESCRIPTION, @@ -436,7 +436,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Collection] type. When `configuration.recursionTreeDepth` limit is reached it creates * an empty collection and doesn't do any modification to it. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Collection, fuzzing: Fuzzing, description: DESCRIPTION, @@ -496,7 +496,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Recursive] type. When `configuration.recursionTreeDepth` limit is reached it calls * `Seed.Recursive#empty` routine to create an empty object. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Recursive, fuzzing: Fuzzing, description: DESCRIPTION, diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt index f64fb7d996..56d907e67a 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt @@ -8,7 +8,7 @@ private val logger by lazy { KotlinLogging.logger {} } /** * Entry point to run fuzzing. */ -suspend fun , FEEDBACK : Feedback> runFuzzing( +suspend fun , FEEDBACK : Feedback> runFuzzing( provider: ValueProvider, description: DESCRIPTION, random: Random = Random(0), @@ -24,7 +24,7 @@ suspend fun , FEEDBACK : Feedback< * @param providers is a list of "type to values" generator * @param exec this function is called when fuzzer generates values of type R to run it with target program. */ -class BaseFuzzing, F : Feedback>( +class BaseFuzzing, F : Feedback>( val providers: List>, val exec: suspend (description: D, values: List) -> F ) : Fuzzing { @@ -60,7 +60,7 @@ class BaseFuzzing, F : Feedback>( /** * Value provider generates [Seed] and has other methods to combine providers. */ -fun interface ValueProvider> { +fun interface ValueProvider> { fun enrich(description: D, type: T, scope: Scope) {} @@ -144,7 +144,7 @@ fun interface ValueProvider> { return if (this is Fallback) { provider.unwrapIfFallback() } else { this } } - private class Fallback>( + private class Fallback>( val provider: ValueProvider, val fallback: ValueProvider, ): ValueProvider { @@ -172,7 +172,7 @@ fun interface ValueProvider> { /** * Wrapper class that delegates implementation to the [providers]. */ - private class Combined>(providers: List>): ValueProvider { + private class Combined>(providers: List>): ValueProvider { val providers: List> init { @@ -203,7 +203,7 @@ fun interface ValueProvider> { } companion object { - fun > of(valueProviders: List>): ValueProvider { + fun > of(valueProviders: List>): ValueProvider { return Combined(valueProviders) } } @@ -215,7 +215,7 @@ fun interface ValueProvider> { * @param type that is used as a filter to call this provider * @param generate yields values for the type */ -class TypeProvider>( +class TypeProvider>( val type: T, val generate: suspend SequenceScope>.(description: D, type: T) -> Unit ) : ValueProvider { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index 4969ccf7f4..bd3ebc1b2c 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -30,14 +30,14 @@ private const val searchString = suspend fun main() { // Define fuzzing description to start searching. - object : Fuzzing, BaseFeedback> { + object : Fuzzing, BaseFeedback> { /** * Generate method returns several samples or seeds which are used as a base for fuzzing. * * In this particular case only 1 value is provided which is an empty string. Also, a mutation * is defined for any string value. This mutation adds a random character from ASCII table. */ - override fun generate(description: Description, type: Unit) = sequenceOf>( + override fun generate(description: Description, type: Unit) = sequenceOf>( Seed.Simple("") { s, r -> s + Char(r.nextInt(1, 256)) } ) @@ -47,7 +47,7 @@ suspend fun main() { * This implementation just calls the target function and returns a result. After it returns an empty feedback. * If some returned value equals to the length of the source string then feedback returns 'stop' signal. */ - override suspend fun handle(description: Description, values: List): BaseFeedback { + override suspend fun handle(description: Description, values: List): BaseFeedback { check(values.size == 1) { "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt index 99be17e156..ead8debe12 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt @@ -11,15 +11,15 @@ private enum class Type { fun main(): Unit = runBlocking { launch { - object : Fuzzing, Feedback> { + object : Fuzzing, Feedback> { private val runs = mutableMapOf() - override fun generate(description: Description, type: Type): Sequence> { + override fun generate(description: Description, type: Type): Sequence> { return sequenceOf(Seed.Simple(type.name)) } - override suspend fun handle(description: Description, values: List): Feedback { + override suspend fun handle(description: Description, values: List): Feedback { description.parameters.forEach { runs[it]!!.incrementAndGet() } @@ -28,7 +28,7 @@ fun main(): Unit = runBlocking { } override suspend fun afterIteration( - description: Description, + description: Description, stats: Statistic, ) { if (stats.totalRuns % 10 == 0L && description.parameters.size == 1) { @@ -38,7 +38,7 @@ fun main(): Unit = runBlocking { Type.MORE_CONCRETE -> listOf() } if (newTypes.isNotEmpty()) { - val d = Description(newTypes) + val d = Description(newTypes) fork(d, stats) // Description can be used as a transfer object, // that collects information about the current running. @@ -47,7 +47,7 @@ fun main(): Unit = runBlocking { } } - override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { + override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { println("info: ${description.parameters} runs ${stats.totalRuns}") return description.parameters.all { runs.computeIfAbsent(it) { AtomicLong(0) }.get() >= 10 } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt index 844336aad1..cfe9af33c5 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt @@ -7,8 +7,8 @@ import java.util.concurrent.TimeUnit fun main() = runBlocking { withTimeout(TimeUnit.SECONDS.toMillis(10)) { - object : Fuzzing, Feedback> { - override fun generate(description: Description, type: String) = sequence> { + object : Fuzzing, Feedback> { + override fun generate(description: Description, type: String) = sequence> { when (type) { "url" -> yield(Seed.Recursive( construct = Routine.Create( @@ -49,7 +49,7 @@ fun main() = runBlocking { } override suspend fun handle( - description: Description, + description: Description, values: List ): Feedback { println(values[0]) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt index 0d3d490dd7..dd901204a3 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt @@ -10,8 +10,8 @@ import org.utbot.fuzzing.seeds.StringValue * This example implements some basics for Java fuzzing that supports only a few types: * integers, strings, primitive arrays and user class [A]. */ -object JavaFuzzing : Fuzzing, Any?, Description>, Feedback, Any?>> { - override fun generate(description: Description>, type: Class<*>) = sequence, Any?>> { +object JavaFuzzing : Fuzzing, Any?, Description, Any?>, Feedback, Any?>> { + override fun generate(description: Description, Any?>, type: Class<*>) = sequence, Any?>> { if (type == Boolean::class.javaPrimitiveType) { yield(Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) yield(Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) @@ -71,7 +71,7 @@ object JavaFuzzing : Fuzzing, Any?, Description>, Feedback>, values: List): Feedback, Any?> { + override suspend fun handle(description: Description, Any?>, values: List): Feedback, Any?> { println(values.joinToString { when (it) { is BooleanArray -> it.contentToString() diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt index 80863e4053..fdd8f3d72d 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt @@ -33,7 +33,7 @@ private class JsonBuilder( suspend fun main() { var count = 0 val set = mutableMapOf() - BaseFuzzing, Feedback>( + BaseFuzzing, Feedback>( TypeProvider(CustomType.INT) { _, _ -> for (b in Signed.values()) { yield(Seed.Known(BitVectorValue(3, b)) { bvv -> diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt index 260b824942..bb82022ae9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt @@ -18,7 +18,7 @@ class FuzzerSmokeTest { fun `fuzzing runs with empty parameters`() { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( { _, _ -> sequenceOf() }, Description(emptyList()) ) { _, _ -> @@ -34,7 +34,7 @@ class FuzzerSmokeTest { assertThrows { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( provider = { _, _ -> sequenceOf() }, description = Description(listOf(Unit)), configuration = Configuration( @@ -55,7 +55,7 @@ class FuzzerSmokeTest { var count = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> count += 1 BaseFeedback(Unit, Control.STOP) @@ -71,7 +71,7 @@ class FuzzerSmokeTest { var executions = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> executions++ BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) @@ -91,7 +91,7 @@ class FuzzerSmokeTest { generations++ sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) } @@ -108,7 +108,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> throw SpecialException() } @@ -128,7 +128,7 @@ class FuzzerSmokeTest { var depth = 0 var count = 0 runFuzzing( - ValueProvider, Node?, Description>> { _, _ -> + { _, _ -> sequenceOf(Seed.Recursive( construct = Routine.Create(listOf(Node::class, Node::class)) { v -> Node(v[0], v[1]) @@ -162,7 +162,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10 000 ms") @@ -181,7 +181,7 @@ class FuzzerSmokeTest { val start = System.currentTimeMillis() runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -197,7 +197,7 @@ class FuzzerSmokeTest { @Test fun `fuzzer generate same result when random is seeded`() { data class B(var a: Int) - val provider = ValueProvider> { _, _ -> + val provider = ValueProvider> { _, _ -> sequenceOf( Seed.Simple(B(0)) { p, r -> B(p.a + r.nextInt()) }, Seed.Known(BitVectorValue(32, Signed.POSITIVE)) { B(it.toInt()) }, @@ -245,7 +245,7 @@ class FuzzerSmokeTest { flow { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -273,7 +273,7 @@ class FuzzerSmokeTest { modify = sequenceOf(Routine.Call(emptyList()) { s, _ -> s.append("1") }), empty = Routine.Empty { fail("Empty is called despite construct requiring no args") } )) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, (s) -> if (s.isEmpty()) { seenEmpty = true @@ -299,7 +299,7 @@ class FuzzerSmokeTest { modify = emptySequence(), empty = Routine.Empty { null } )}.asSequence() }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> seenAnything = true BaseFeedback(Unit, Control.STOP) diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt index 74b2d8c204..29965929c9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt @@ -5,23 +5,23 @@ import org.junit.jupiter.api.Test class ProvidersTest { - private fun p(supplier: () -> T) : ValueProvider> { + private fun p(supplier: () -> T) : ValueProvider> { return ValueProvider { _, _ -> sequenceOf(Seed.Simple(supplier())) } } private fun p( accept: () -> Boolean, supplier: () -> T - ) : ValueProvider> { - return object : ValueProvider> { + ) : ValueProvider> { + return object : ValueProvider> { override fun accept(type: Unit) = accept() - override fun generate(description: Description, type: Unit) = sequence> { + override fun generate(description: Description, type: Unit) = sequence> { yield(Seed.Simple(supplier())) } } } - private val description = Description(listOf(Unit)) + private val description = Description(listOf(Unit)) @Test fun `test common provider API`() { @@ -170,9 +170,9 @@ class ProvidersTest { val seq = ValueProvider.of(listOf( p({ false }, { 1 }), p({ false }, { 2 }), p({ false }, { 3 }), )).withFallback( - object : ValueProvider> { + object : ValueProvider> { override fun accept(type: Unit) = false - override fun generate(description: Description, type: Unit) = emptySequence>() + override fun generate(description: Description, type: Unit) = emptySequence>() } ).generate(description, Unit) Assertions.assertEquals(0, seq.count()) @@ -180,12 +180,12 @@ class ProvidersTest { @Test fun `type providers check exactly the type`() { - val seq1 = TypeProvider>('A') { _, _ -> + val seq1 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'A') Assertions.assertEquals(1, seq1.count()) - val seq2 = TypeProvider>('A') { _, _ -> + val seq2 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'B') Assertions.assertEquals(0, seq2.count()) diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index f6cc458787..f8a8eec6d3 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -23,7 +23,7 @@ class FuzzedDescription( val typeCache: MutableMap, val random: Random, val scope: Scope? = null -) : Description( +) : Description( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) } diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt index 10f6a8d2a9..81d811b081 100644 --- a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt @@ -365,7 +365,7 @@ class JavaFuzzingTest { } } -class MarkerValueProvider>( +class MarkerValueProvider>( val name: String ) : ValueProvider { var enrich: Int = 0 diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt index e3b7b1931c..8f67d030ca 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt @@ -29,7 +29,7 @@ class PythonMethodDescription( val pythonTypeStorage: PythonTypeStorage, val tracer: Trie, val random: Random, -) : Description(parameters) +) : Description(parameters) sealed interface FuzzingExecutionFeedback class ValidExecution(val utFuzzedExecution: PythonUtExecution): FuzzingExecutionFeedback From d19d90f9878feb940842733aee2b6bddaed3c391 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 26 Jul 2023 16:14:39 +0300 Subject: [PATCH 02/25] Add reporting mechanics with basic logging reporter --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 132 +++++++++++++++++- .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 2 +- 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 57e2ac953f..6206adb2f1 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -8,6 +8,10 @@ import org.utbot.fuzzing.utils.MissedSeed import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import org.utbot.fuzzing.utils.transformIfNotEmpty +import java.io.File +import java.io.FileOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.random.Random private val logger by lazy { KotlinLogging.logger {} } @@ -73,6 +77,7 @@ interface Fuzzing, FEEDBAC suspend fun afterIteration(description: DESCRIPTION, statistics: Statistic) { } } +///region Description /** * Some description of the current fuzzing run. Usually, it contains the name of the target method and its parameter list. */ @@ -84,8 +89,109 @@ open class Description( open fun clone(scope: Scope): Description { error("Scope was changed for $this, but method clone is not specified") } + + open fun setUp() {} + + open fun updatePerIteration(values: Node, feedback: Feedback) {} + + open fun conclude(statistic: Statistic) {} +} + +abstract class ReportingDescription ( + parameters: List, + private val reporter: Reporter +) : Description(parameters) { + + override fun setUp() { + reporter.setUp(this) + } + + override fun updatePerIteration(values : Node, feedback : Feedback) { + reporter.update(values, feedback) + } + + override fun conclude(statistic: Statistic) { + reporter.conclude(statistic) + } +} + +class LoggingDescription ( + parameters: List, + path: String +) : ReportingDescription( + parameters, + LoggingReporter(path = path) +) + + +///region Reporter +abstract class Reporter( + private val reportPath : String, +) { + private lateinit var description: Description + fun setUp(description: Description) { + this.description = description + } + abstract fun update(values : Node, feedback : Feedback) + abstract fun conclude(statistic: Statistic) } + +class LoggingReporter( + path: String +) : Reporter(path) { + + private val actualPath = if (path.startsWith("~/")) { + System.getProperty("user.home") + path.drop(1) + } else { + path + } + + private val logFile = File("$actualPath/log") + private val overviewFile = File("$actualPath/overview.txt") + + init { + File(actualPath).mkdirs() + logFile.apply { delete(); createNewFile() } + overviewFile.apply { delete(); createNewFile() } + } + + override fun update(values: Node, feedback: Feedback) { + + fun alignString(obj: Any, length: Int) : String { + val aligned = obj.toString().replace("\n", "/") + return if (aligned.length > length) { + aligned.take((length-1)/2) + ".." + aligned.takeLast((length-1)/2) + } else { + aligned + " ".repeat(length - aligned.length) + } + } + + val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) + + FileOutputStream(logFile, true).bufferedWriter().use { + it.write("[$time] | ${alignString(values, 53)} | ${alignString(feedback, 30)}\n") + } + } + + override fun conclude(statistic: Statistic) { + File(actualPath).mkdirs() + File("$actualPath/overview.txt").apply{ createNewFile() }.printWriter().use { out -> + out.println("Total runs: ${statistic.totalRuns}") + + val seconds = statistic.elapsedTime / 1_000_000_000 + val minutes = (seconds % 3600) / 60 + val remainingSeconds = (seconds % 3600) % 60 + val formattedTime = String.format("%02d min %02d.%03d sec", minutes, remainingSeconds, statistic.elapsedTime % 1_000_000) + out.println("Elapsed time: $formattedTime") + } + } +} +///endregion +///endregion + + + class Scope( val parameterIndex: Int, val recursionDepth: Int, @@ -245,7 +351,11 @@ interface Feedback : AsKey { data class BaseFeedback( val result: VALUE, override val control: Control, -) : Feedback +) : Feedback { + override fun toString(): String { + return "$result | $control" + } +} /** * Controls fuzzing execution. @@ -330,6 +440,8 @@ private suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing, R>() val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } val feedback = fuzzing.handle(description, result) + + description.updatePerIteration(values, feedback) + when (feedback.control) { Control.CONTINUE -> { statistic.put(random, configuration, feedback, values) } Control.STOP -> { + description.conclude(statistic) break } Control.PASS -> {} @@ -685,7 +801,19 @@ class Node( val result: List>, val parameters: List, val builder: Routine, -) +) { + override fun toString() : String { + return result.map { + when(it) { + is Result.Empty -> "_" + is Result.Simple -> it.result + is Result.Known<*, *, *> -> it.value.toString() + is Result.Collection -> it.modify.joinToString(", ", "[", "]") + is Result.Recursive -> it.modify.joinToString(", ", "{", "}") + } + }.joinToString(", ") + } +} private class StatisticImpl>( override var totalRuns: Long = 0, diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index bd3ebc1b2c..4dd9d16551 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -59,5 +59,5 @@ suspend fun main() { control = if (result == searchString.length) Control.STOP else Control.CONTINUE ) } - }.fuzz(Description(listOf(Unit))) + }.fuzz(LoggingDescription(listOf(Unit), "~/.utbot/AbcFuzzing")) } \ No newline at end of file From 366ecf04e5e124f6d4e7a6d03210cb6928551aa2 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 26 Jul 2023 16:21:34 +0300 Subject: [PATCH 03/25] Add linear order on feedback with WeightedFeedback --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 18 ++++++++++++++++++ .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 7 ++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 6206adb2f1..5cba624417 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -357,6 +357,24 @@ data class BaseFeedback( } } +interface WeightedFeedback> : Feedback { + val weight: WEIGHT +} + +data class BaseWeightedFeedback>( + val result: VALUE, + override val weight: WEIGHT, + override val control: Control, +) : WeightedFeedback, Comparable> { + override fun compareTo(other: WeightedFeedback): Int { + return weight.compareTo(other.weight) + } + override fun toString(): String { + return "$result with weight $weight | $control" + } +} + + /** * Controls fuzzing execution. */ diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index 4dd9d16551..9ab6d90b0d 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -30,7 +30,7 @@ private const val searchString = suspend fun main() { // Define fuzzing description to start searching. - object : Fuzzing, BaseFeedback> { + object : Fuzzing, BaseWeightedFeedback> { /** * Generate method returns several samples or seeds which are used as a base for fuzzing. * @@ -47,15 +47,16 @@ suspend fun main() { * This implementation just calls the target function and returns a result. After it returns an empty feedback. * If some returned value equals to the length of the source string then feedback returns 'stop' signal. */ - override suspend fun handle(description: Description, values: List): BaseFeedback { + override suspend fun handle(description: Description, values: List): BaseWeightedFeedback { check(values.size == 1) { "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" } val input = values.first() val result = searchString.findMaxSubstring(input) println("findMaxSubstring(\"$input\") = $result") - return BaseFeedback( + return BaseWeightedFeedback( result = result, + weight = result, control = if (result == searchString.length) Control.STOP else Control.CONTINUE ) } From 691a5b5f078be9ba3377e25fbb5050c8978a91b8 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 26 Jul 2023 18:11:19 +0300 Subject: [PATCH 04/25] Add ValueStorage, Minset and minset based Statistic implementation --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 62 +----- .../kotlin/org/utbot/fuzzing/Configuration.kt | 4 +- .../kotlin/org/utbot/fuzzing/Statistic.kt | 210 +++++++++++++++++- 3 files changed, 223 insertions(+), 53 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 5cba624417..bfef0d24a6 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.* import mu.KotlinLogging import org.utbot.fuzzing.seeds.KnownValue import org.utbot.fuzzing.utils.MissedSeed -import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import org.utbot.fuzzing.utils.transformIfNotEmpty import java.io.File @@ -63,7 +62,8 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, StatisticImpl(statistics)) + fuzz(description, BasicSingleValueMinsetStatistic(statistics, SingleValueSelectionStrategy.LAST) + ) } /** @@ -431,7 +431,9 @@ suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing, F : Feedback> Fuzzing.fuzz( description: D, - statistic: StatisticImpl, + statistic: SeedsMaintainingStatistic, ) { val random = statistic.random val configuration = statistic.configuration @@ -490,6 +492,11 @@ private suspend fun , F : Feedback> Fuzzing { statistic.put(random, configuration, feedback, values) @@ -502,8 +509,6 @@ private suspend fun , F : Feedback> Fuzzing, FEEDBACK : Feedback> fuzz( @@ -832,51 +837,6 @@ class Node( }.joinToString(", ") } } - -private class StatisticImpl>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, -) : Statistic { - - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - ) - - override val elapsedTime: Long - get() = System.nanoTime() - startTime - private val seeds = linkedMapOf>() - private val count = linkedMapOf() - - fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { - if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { - seeds[feedback] = seed - } else { - seeds.putIfAbsent(feedback, seed) - } - count[feedback] = count.getOrDefault(feedback, 0L) + 1L - } - - fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") - val entries = seeds.entries.toList() - val frequencies = DoubleArray(seeds.size).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) - } - } - val index = random.chooseOne(frequencies) - return entries[index].value - } - - fun isNotEmpty() = seeds.isNotEmpty() -} ///endregion diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index a6c19926c9..d07dffed76 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -95,4 +95,6 @@ data class Configuration( * Limits maximum number of recursive seed modifications */ var maxNumberOfRecursiveSeedModifications: Int = 10, -) \ No newline at end of file +) + +enum class SingleValueSelectionStrategy { FIRST, LAST } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 877937cedb..7c4d9e6595 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -1,6 +1,8 @@ package org.utbot.fuzzing import org.utbot.fuzzing.utils.MissedSeed +import org.utbot.fuzzing.utils.chooseOne +import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random /** @@ -13,4 +15,210 @@ interface Statistic { val missedTypes: MissedSeed val random: Random val configuration: Configuration -} \ No newline at end of file +} + +///region Statistic Implementations + +interface SeedsMaintainingStatistic>: Statistic { + override var totalRuns: Long + fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) + fun getRandomSeed(random: Random, configuration: Configuration): Node + fun isNotEmpty() : Boolean +} + +open class BaseStatisticImpl>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration +) : SeedsMaintainingStatistic { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy(), + ) + + override val elapsedTime: Long + get() = System.nanoTime() - startTime + private val seeds = linkedMapOf>() + private val count = linkedMapOf() + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { + if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { + seeds[feedback] = seed + } else { + seeds.putIfAbsent(feedback, seed) + } + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + } + + override fun getRandomSeed(random: Random, configuration: Configuration): Node { + if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") + val entries = seeds.entries.toList() + val frequencies = DoubleArray(seeds.size).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) + } + } + val index = random.chooseOne(frequencies) + return entries[index].value + } + + override fun isNotEmpty() = seeds.isNotEmpty() +} + +open class SingleValueMinsetStatistic, STORAGE : SingleValueStorage>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, + private val generateStorage: () -> STORAGE +) : SeedsMaintainingStatistic { + override val elapsedTime: Long + get() = System.nanoTime() - startTime + + private val minset: Minset = SingleValueMinset( + configuration = SingleValueSelectionStrategy.LAST, + valueStorageGenerator = generateStorage + ) + + constructor(source: Statistic, generateStorage: () -> STORAGE) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy(), + generateStorage = generateStorage + ) + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { + minset.put(seed, feedback) + } + + override fun getRandomSeed(random: Random, configuration: Configuration): Node { + if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") + val entries = minset.seeds.entries.toList() + val frequencies = DoubleArray(minset.seeds.size).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction( minset.count.getOrDefault(key, 0L)) + } + } + val index = random.chooseOne(frequencies) + return entries[index].value.next() + } + + override fun isNotEmpty() = minset.isNotEmpty() +} + +class BasicSingleValueMinsetStatistic>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, + private val seedSelectionStrategy: SingleValueSelectionStrategy +) : SingleValueMinsetStatistic>( + totalRuns, startTime, missedTypes, random, configuration, { SingleValueStorage(seedSelectionStrategy) } +) { + constructor(source: Statistic, seedSelectionStrategy: SingleValueSelectionStrategy) : this ( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy(), + seedSelectionStrategy = seedSelectionStrategy + ) +} + +///endregion + +///region Minset +abstract class Minset, STORAGE : ValueStorage> ( + open val valueStorageGenerator: () -> STORAGE, + val seeds: LinkedHashMap = linkedMapOf(), + val count: LinkedHashMap = linkedMapOf() +) { + operator fun get(feedback: FEEDBACK): STORAGE? { + return seeds[feedback] + } + + fun put(value: Node, feedback: FEEDBACK) { + seeds.getOrPut(feedback) { valueStorageGenerator.invoke() }.put(value, feedback) + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + } + + fun isNotEmpty(): Boolean { + return seeds.isNotEmpty() + } + + fun isEmpty(): Boolean { + return seeds.isEmpty() + } +} + +class SingleValueMinset, STORAGE : SingleValueStorage> ( + val configuration : SingleValueSelectionStrategy, + override val valueStorageGenerator: () -> STORAGE, +) : Minset(valueStorageGenerator) +///endregion + +///region Value storages +interface InfiniteIterator : Iterator { + override operator fun next(): T + override operator fun hasNext(): Boolean +} + +interface ValueStorage : InfiniteIterator> { + fun put(value: Node, feedback: Feedback) +} + +open class SingleValueStorage ( + private val strategy : SingleValueSelectionStrategy +) : ValueStorage { + + private var storedValue: Node? = null + override fun put(value: Node, feedback: Feedback) { + storedValue = when (strategy) { + SingleValueSelectionStrategy.FIRST -> storedValue ?: value + SingleValueSelectionStrategy.LAST -> value + } + } + + override fun next(): Node { + if (storedValue == null) { + error("Next value requested but no value stored") + } else { + return storedValue as Node + } + } + + override fun hasNext(): Boolean { + return storedValue != null + } +} + +//@Suppress("UNCHECKED_CAST") +//class SingleDistributableValueStorage>( +// strategy: SingleValueSelectionStrategy, +// var distributionTree: DistributionNode +//) : SingleValueStorage(strategy) { +// fun put(value: Node, feedback: BaseWeightedFeedback<*, TYPE, RESULT, *>) { +// super.put(value, feedback) +// value.result.forEach { +//// distributionTree.put(it) +// when(it) { +// is Result.Empty -> {} +// is Result.Simple -> distributionTree.put(it as RESULT) +// is Result.Collection -> distributionTree.put(it as RESULT) +// is Result.Known<*, *, *> -> distributionTree.put(it as RESULT) +// is Result.Recursive -> distributionTree.put(it as RESULT) +// } +// } +// } +//} + +///endregion \ No newline at end of file From d552623b0d021dbe72243db15cac2094b02d9306 Mon Sep 17 00:00:00 2001 From: grigory Date: Fri, 28 Jul 2023 10:50:21 +0300 Subject: [PATCH 05/25] Improve logging and attach it to java fuzzing --- .../utbot/framework/plugin/api/CoverageApi.kt | 4 ++ .../org/utbot/engine/UtBotSymbolicEngine.kt | 18 +++---- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 53 ++++++++++--------- .../kotlin/org/utbot/fuzzing/Statistic.kt | 53 ++++++++++++++++--- .../org/utbot/fuzzing/seeds/StringValue.kt | 4 ++ .../kotlin/org/utbot/fuzzing/utils/Trie.kt | 10 +++- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 38 +++++++++++-- 7 files changed, 134 insertions(+), 46 deletions(-) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 06c4c9350f..10e24df356 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -17,6 +17,10 @@ data class Instruction( val id: Long ) { val className: String get() = internalName.replace('/', '.') + + override fun toString(): String { + return "class name: ${className.split(".").last()} | method: $methodSignature | trace id: $id | line number: $lineNumber" + } } /** 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 46d16f234d..0c8b968d02 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -412,13 +412,13 @@ class UtBotSymbolicEngine( if (controller.job?.isActive == false || diff <= thresholdMillisForFuzzingOperation) { logger.info { "Fuzzing overtime: $methodUnderTest" } logger.info { "Test created by fuzzer: $testEmittedByFuzzer" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } if (thisInstance?.model is UtNullModel) { // We should not try to run concretely any models with null-this. // But fuzzer does generate such values, because it can fail to generate any "good" values. - return@runJavaFuzzing BaseFeedback(Trie.emptyNode(), Control.PASS) + return@runJavaFuzzing JavaFeedback(Trie.emptyNode(), Control.PASS) } val stateBefore = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf()) @@ -435,17 +435,17 @@ class UtBotSymbolicEngine( } // in case an exception occurred from the concrete execution - concreteExecutionResult ?: return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + concreteExecutionResult ?: return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) // in case of processed failure in the concrete execution concreteExecutionResult.processedFailure()?.let { failure -> logger.debug { "Instrumented process failed with exception ${failure.exception} before concrete execution started" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } if (concreteExecutionResult.violatesUtMockAssumption()) { logger.debug { "Generated test case by fuzzer violates the UtMock assumption: $concreteExecutionResult" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } val result = concreteExecutionResult.result @@ -462,16 +462,16 @@ class UtBotSymbolicEngine( coverageToMinStateBeforeSize[trieNode] = curStateBeforeSize else { if (++attempts >= attemptsLimit) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } - return@runJavaFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) + return@runJavaFuzzing JavaFeedback(result = trieNode, control = Control.CONTINUE) } } else { logger.error { "Coverage is empty for $methodUnderTest with $values" } if (result is UtSandboxFailure) { val stackTraceElements = result.exception.stackTrace.reversed() if (errorStackTraceTracker.add(stackTraceElements).count > 1) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } } } @@ -489,7 +489,7 @@ class UtBotSymbolicEngine( ) testEmittedByFuzzer++ - BaseFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) + JavaFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index bfef0d24a6..ca6254cc2b 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -92,9 +92,9 @@ open class Description( open fun setUp() {} - open fun updatePerIteration(values: Node, feedback: Feedback) {} + open fun updatePerIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} - open fun conclude(statistic: Statistic) {} + open fun finalizeReport(statistic: Statistic) {} } abstract class ReportingDescription ( @@ -106,16 +106,16 @@ abstract class ReportingDescription ( reporter.setUp(this) } - override fun updatePerIteration(values : Node, feedback : Feedback) { - reporter.update(values, feedback) + override fun updatePerIteration(values : Node, feedback : Feedback, event: MinsetEvent) { + reporter.update(values, feedback, event) } - override fun conclude(statistic: Statistic) { + override fun finalizeReport(statistic: Statistic) { reporter.conclude(statistic) } } -class LoggingDescription ( +open class LoggingDescription ( parameters: List, path: String ) : ReportingDescription( @@ -128,19 +128,25 @@ class LoggingDescription ( abstract class Reporter( private val reportPath : String, ) { + abstract fun clone() : Reporter + private lateinit var description: Description fun setUp(description: Description) { this.description = description } - abstract fun update(values : Node, feedback : Feedback) + abstract fun update(values : Node, feedback : Feedback, event : MinsetEvent, additionalMessage: String = "") abstract fun conclude(statistic: Statistic) } class LoggingReporter( - path: String + val path: String ) : Reporter(path) { + override fun clone() : LoggingReporter { + return LoggingReporter(path) + } + private val actualPath = if (path.startsWith("~/")) { System.getProperty("user.home") + path.drop(1) } else { @@ -152,11 +158,11 @@ class LoggingReporter( init { File(actualPath).mkdirs() - logFile.apply { delete(); createNewFile() } - overviewFile.apply { delete(); createNewFile() } +// logFile.apply { delete(); createNewFile() } +// overviewFile.apply { delete(); createNewFile() } } - override fun update(values: Node, feedback: Feedback) { + override fun update(values: Node, feedback: Feedback, event : MinsetEvent, additionalMessage: String) { fun alignString(obj: Any, length: Int) : String { val aligned = obj.toString().replace("\n", "/") @@ -170,7 +176,8 @@ class LoggingReporter( val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) FileOutputStream(logFile, true).bufferedWriter().use { - it.write("[$time] | ${alignString(values, 53)} | ${alignString(feedback, 30)}\n") +// it.write("[$time] | ${alignString(values, 53)} | ${alignString(feedback, 100)}\n") + it.write("[$time] | ${alignString(values, 50)} | $feedback | $event | $additionalMessage\n") } } @@ -353,7 +360,7 @@ data class BaseFeedback( override val control: Control, ) : Feedback { override fun toString(): String { - return "$result | $control" + return "$result" } } @@ -370,7 +377,7 @@ data class BaseWeightedFeedback return weight.compareTo(other.weight) } override fun toString(): String { - return "$result with weight $weight | $control" + return "$result with weight $weight" } } @@ -432,7 +439,7 @@ suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing create(r) } } val feedback = fuzzing.handle(description, result) - description.updatePerIteration(values, feedback) - - if ((statistic.totalRuns % 50).toInt() == 0) { - println(statistic.totalRuns) - } - + val minsetResponse = statistic.put(random, configuration, feedback, values) when (feedback.control) { Control.CONTINUE -> { - statistic.put(random, configuration, feedback, values) + description.updatePerIteration(values, feedback, minsetResponse) + description.finalizeReport(statistic) } Control.STOP -> { - description.conclude(statistic) + description.finalizeReport(statistic) break } - Control.PASS -> {} + Control.PASS -> { + description.finalizeReport(statistic) + } } } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 7c4d9e6595..530dc649a0 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -15,13 +15,14 @@ interface Statistic { val missedTypes: MissedSeed val random: Random val configuration: Configuration +// val minsetSize: Long } ///region Statistic Implementations interface SeedsMaintainingStatistic>: Statistic { override var totalRuns: Long - fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) + fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent fun getRandomSeed(random: Random, configuration: Configuration): Node fun isNotEmpty() : Boolean } @@ -46,13 +47,24 @@ open class BaseStatisticImpl>( private val seeds = linkedMapOf>() private val count = linkedMapOf() - override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent { + var result = MinsetEvent.NOTHING_NEW + + if (!seeds.containsKey(feedback)) { + result = MinsetEvent.NEW_FEEDBACK + } + if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { + if (seeds[feedback] != seed) { + result = MinsetEvent.NEW_VALUE + } seeds[feedback] = seed } else { seeds.putIfAbsent(feedback, seed) } count[feedback] = count.getOrDefault(feedback, 0L) + 1L + + return result } override fun getRandomSeed(random: Random, configuration: Configuration): Node { @@ -95,8 +107,8 @@ open class SingleValueMinsetStatistic) { - minset.put(seed, feedback) + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent { + return minset.put(seed, feedback) } override fun getRandomSeed(random: Random, configuration: Configuration): Node { @@ -137,6 +149,9 @@ class BasicSingleValueMinsetStatistic, STORAGE : ValueStorage> ( open val valueStorageGenerator: () -> STORAGE, val seeds: LinkedHashMap = linkedMapOf(), @@ -146,9 +161,22 @@ abstract class Minset, STORAGE : return seeds[feedback] } - fun put(value: Node, feedback: FEEDBACK) { - seeds.getOrPut(feedback) { valueStorageGenerator.invoke() }.put(value, feedback) + fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { + val result: MinsetEvent + + if (seeds.containsKey(feedback)) { + result = if( seeds[feedback]!!.put(value, feedback) ) { MinsetEvent.NEW_VALUE } else { MinsetEvent.NOTHING_NEW } + } else { + result = MinsetEvent.NEW_FEEDBACK + seeds[feedback] = valueStorageGenerator.invoke() + seeds[feedback]!!.put(value, feedback) + } + +// seeds.getOrPut(feedback) { valueStorageGenerator.invoke() }.put(value, feedback) + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + + return result } fun isNotEmpty(): Boolean { @@ -173,7 +201,7 @@ interface InfiniteIterator : Iterator { } interface ValueStorage : InfiniteIterator> { - fun put(value: Node, feedback: Feedback) + fun put(value: Node, feedback: Feedback) : Boolean } open class SingleValueStorage ( @@ -181,11 +209,20 @@ open class SingleValueStorage ( ) : ValueStorage { private var storedValue: Node? = null - override fun put(value: Node, feedback: Feedback) { + override fun put(value: Node, feedback: Feedback) : Boolean { + + var result = false; + + if(storedValue == null) { + result = true + } + storedValue = when (strategy) { SingleValueSelectionStrategy.FIRST -> storedValue ?: value SingleValueSelectionStrategy.LAST -> value } + + return result || (strategy == SingleValueSelectionStrategy.LAST) } override fun next(): Node { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt index cd054754b8..c66d96c79a 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -24,4 +24,8 @@ open class StringValue( StringMutations.ShuffleCharacters, ) } + + override fun toString(): String { + return "\"" + value + "\"" + } } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index 7bc4190f64..4e9928d034 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -168,13 +168,21 @@ open class Trie( val parent: NodeImpl?, override var count: Int = 0, val children: MutableMap> = HashMap(), - ) : Node + ) : Node { + override fun toString(): String { + return "$data" + } + } private object EmptyNode : Node { override val data: Any get() = error("empty node has no data") override val count: Int get() = 0 + + override fun toString(): String { + return "EMPTY" + } } companion object { diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index f8a8eec6d3..1145e0f128 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -17,16 +17,19 @@ private val logger = KotlinLogging.logger {} typealias JavaValueProvider = ValueProvider + + class FuzzedDescription( val description: FuzzedMethodDescription, val tracer: Trie, val typeCache: MutableMap, val random: Random, - val scope: Scope? = null -) : Description( + val scope: Scope? = null, +) : LoggingDescription( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) - } + }, + "~/.utbot/JavaFuzzing" ) { val constants: Sequence get() = description.concreteValues.asSequence() @@ -36,6 +39,7 @@ class FuzzedDescription( } } + fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( BooleanValueProvider, IntegerValueProvider, @@ -53,13 +57,39 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis NullValueProvider, ) + +class JavaFeedback( + val result: Trie.Node, + override val control: Control, + ) : Feedback { + override fun equals(other: Any?): Boolean { + if (other is JavaFeedback) { + return this.result == other.result && this.control == other.control + } + return false + } + + override fun hashCode(): Int { + return result.hashCode() * 31 + control.hashCode() + } + + override fun toString(): String { + if (result.count == 0) { + return " FAIL | EMPTY TRACE" + } + return "$result | ${if(result.count == 1) { "NEW_TRACE" } else { "trace count: " + String.format("%4s", result.count) } }" + } +} + + + suspend fun runJavaFuzzing( idGenerator: IdentityPreservingIdGenerator, methodUnderTest: ExecutableId, constants: Collection, names: List, providers: List> = defaultValueProviders(idGenerator), - exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> + exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> JavaFeedback ) { val random = Random(0) val classUnderTest = methodUnderTest.classId From 4ed57ab4fc807da82eeffe18e9fc3a4498ac7291 Mon Sep 17 00:00:00 2001 From: grigory Date: Fri, 28 Jul 2023 12:53:17 +0300 Subject: [PATCH 06/25] Fix java fuzzing logging and add SingleEntryMinsetStatistic --- .../utbot/framework/plugin/api/CoverageApi.kt | 2 +- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 13 +++-- .../kotlin/org/utbot/fuzzing/Statistic.kt | 56 +++++++++++++++++-- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 2 +- 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 10e24df356..1c8528043a 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -19,7 +19,7 @@ data class Instruction( val className: String get() = internalName.replace('/', '.') override fun toString(): String { - return "class name: ${className.split(".").last()} | method: $methodSignature | trace id: $id | line number: $lineNumber" + return "class name: ${className.split(".").last()} | method: $methodSignature | line number: $lineNumber" } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index ca6254cc2b..f3b39d000e 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -62,7 +62,7 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, BasicSingleValueMinsetStatistic(statistics, SingleValueSelectionStrategy.LAST) + fuzz(description, SingleEntryMinsetStatistic(statistics) ) } @@ -176,16 +176,19 @@ class LoggingReporter( val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) FileOutputStream(logFile, true).bufferedWriter().use { -// it.write("[$time] | ${alignString(values, 53)} | ${alignString(feedback, 100)}\n") it.write("[$time] | ${alignString(values, 50)} | $feedback | $event | $additionalMessage\n") } } override fun conclude(statistic: Statistic) { File(actualPath).mkdirs() - File("$actualPath/overview.txt").apply{ createNewFile() }.printWriter().use { out -> + overviewFile.apply{ createNewFile() }.printWriter().use { out -> out.println("Total runs: ${statistic.totalRuns}") + if (statistic is SeedsMaintainingStatistic<*, *, *>) { + out.println("Final minset size: ${statistic.getMinsetSize()}") + } + val seconds = statistic.elapsedTime / 1_000_000_000 val minutes = (seconds % 3600) / 60 val remainingSeconds = (seconds % 3600) % 60 @@ -438,9 +441,7 @@ suspend fun , F : Feedback> Fuzzing) : MinsetEvent fun getRandomSeed(random: Random, configuration: Configuration): Node fun isNotEmpty() : Boolean + fun getMinsetSize() : Int } open class BaseStatisticImpl>( @@ -80,6 +81,9 @@ open class BaseStatisticImpl>( } override fun isNotEmpty() = seeds.isNotEmpty() + override fun getMinsetSize(): Int { + return seeds.size + } } open class SingleValueMinsetStatistic, STORAGE : SingleValueStorage>( @@ -94,7 +98,6 @@ open class SingleValueMinsetStatistic = SingleValueMinset( - configuration = SingleValueSelectionStrategy.LAST, valueStorageGenerator = generateStorage ) @@ -114,7 +117,7 @@ open class SingleValueMinsetStatistic { if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") val entries = minset.seeds.entries.toList() - val frequencies = DoubleArray(minset.seeds.size).also { f -> + val frequencies = DoubleArray(minset.getSize()).also { f -> entries.forEachIndexed { index, (key, _) -> f[index] = configuration.energyFunction( minset.count.getOrDefault(key, 0L)) } @@ -124,9 +127,12 @@ open class SingleValueMinsetStatistic>( +open class BasicSingleValueMinsetStatistic>( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), @@ -146,6 +152,24 @@ class BasicSingleValueMinsetStatistic>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, +) : BasicSingleValueMinsetStatistic( + totalRuns, startTime, missedTypes, random, configuration, SingleValueSelectionStrategy.LAST +) { + constructor(source: Statistic) : this ( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) +} + ///endregion ///region Minset @@ -161,7 +185,7 @@ abstract class Minset, STORAGE : return seeds[feedback] } - fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { + open fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { val result: MinsetEvent if (seeds.containsKey(feedback)) { @@ -186,12 +210,32 @@ abstract class Minset, STORAGE : fun isEmpty(): Boolean { return seeds.isEmpty() } + + fun getSize() : Int { + return seeds.size + } } -class SingleValueMinset, STORAGE : SingleValueStorage> ( - val configuration : SingleValueSelectionStrategy, +open class SingleValueMinset, STORAGE : SingleValueStorage> ( override val valueStorageGenerator: () -> STORAGE, ) : Minset(valueStorageGenerator) + +@Suppress("UNCHECKED_CAST") +class SingleEntryMinset, STORAGE : SingleValueStorage>: + SingleValueMinset( { SingleValueStorage(SingleValueSelectionStrategy.LAST) as STORAGE} ) { + override fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { + val result : MinsetEvent = if (seeds.containsKey(feedback)) { + MinsetEvent.NEW_VALUE + } else { + seeds.clear() + seeds[feedback] = SingleValueStorage(SingleValueSelectionStrategy.LAST) as STORAGE + MinsetEvent.NEW_FEEDBACK + } + seeds[feedback]!!.put(value, feedback) + return result + } + } + ///endregion ///region Value storages diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 1145e0f128..da1bd7c601 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -77,7 +77,7 @@ class JavaFeedback( if (result.count == 0) { return " FAIL | EMPTY TRACE" } - return "$result | ${if(result.count == 1) { "NEW_TRACE" } else { "trace count: " + String.format("%4s", result.count) } }" + return "$result | trace hash: ${result.hashCode()} | instruction count: ${String.format("%4s", result.count)}" } } From f1c2c9e5fbd154456585bb7efbe9dfa894b9e265 Mon Sep 17 00:00:00 2001 From: grigory Date: Fri, 28 Jul 2023 13:45:31 +0300 Subject: [PATCH 07/25] Prettify java fuzzing logging --- utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt | 5 +++-- .../src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt | 6 +++++- .../src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index f3b39d000e..b9c88f8d06 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -62,8 +62,8 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, SingleEntryMinsetStatistic(statistics) - ) + fuzz(description, SingleEntryMinsetStatistic(statistics)) +// fuzz(description, BasicSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) } /** @@ -442,6 +442,7 @@ suspend fun , F : Feedback> Fuzzing Date: Mon, 31 Jul 2023 12:10:06 +0300 Subject: [PATCH 08/25] Add mutations logging --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 30 +++++++++--- .../kotlin/org/utbot/fuzzing/Mutations.kt | 48 +++++++++++++++---- .../org/utbot/fuzzing/demo/JavaFuzzing.kt | 13 +++-- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index b9c88f8d06..6884ace400 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -176,7 +176,7 @@ class LoggingReporter( val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) FileOutputStream(logFile, true).bufferedWriter().use { - it.write("[$time] | ${alignString(values, 50)} | $feedback | $event | $additionalMessage\n") + it.write("[$time] | $values | $feedback | $event | $additionalMessage\n") } } @@ -422,6 +422,10 @@ private object EmptyFeedback : Feedback { override fun hashCode(): Int { return 0 } + + override fun toString(): String { + return "EMPTY_FEEDBACK" + } } class NoSeedValueException internal constructor( @@ -798,13 +802,18 @@ sealed interface Result { /** * Known value. */ - class Known>(val value: V, val build: (V) -> RESULT) : Result + class Known>(val value: V, val build: (V) -> RESULT, val mutations: Set> = setOf()) : Result { + override fun toString(): String { + return this.value.toString() + } + } /** * A tree of object that has constructor and some modifications. */ class Recursive( val construct: Node, val modify: List>, + val mutations: Set> = setOf(), ) : Result /** @@ -814,6 +823,7 @@ sealed interface Result { val construct: Node, val modify: List>, val iterations: Int, + val mutations: Set> = setOf(), ) : Result /** @@ -833,13 +843,21 @@ class Node( val builder: Routine, ) { override fun toString() : String { - return result.map { + return result.map { it -> when(it) { is Result.Empty -> "_" is Result.Simple -> it.result - is Result.Known<*, *, *> -> it.value.toString() - is Result.Collection -> it.modify.joinToString(", ", "[", "]") - is Result.Recursive -> it.modify.joinToString(", ", "{", "}") + is Result.Known<*, *, *> -> "$it : ${it.mutations + .map { it.toString().substringAfter("$").substringBefore('@') + }.joinToString(", ", "(", ")")}" + is Result.Collection -> it.modify + .joinToString(", ", "[", "]") + " : ${it.mutations.map { + it.toString().substringAfter("$").substringBefore('@') + }.joinToString(", ", "(", ")") }}" + is Result.Recursive -> it.modify + .joinToString(", ", "{", "}")+ " : ${it.mutations.map { + it.toString().substringAfter("$").substringBefore('@') + }.joinToString(", ", "(", ")") }" } }.joinToString(", ") } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 9868be39b8..4cc7b2f94f 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -9,6 +9,14 @@ import kotlin.random.Random class MutationFactory { + private val memo: HashMap> = linkedMapOf>() + fun memorizeMutation(source: TYPE, mutation: Mutation) { + if (memo.containsKey(source)) { + error("New mutation for memorized value") + } + memo[source] = mutation + } + fun mutate(node: Node, random: Random, configuration: Configuration): Node { if (node.result.isEmpty()) return node val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) @@ -16,7 +24,10 @@ class MutationFactory { mutate(n, r, c) } val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { - is Result.Simple -> Result.Simple(resultToMutate.mutation(resultToMutate.result, random), resultToMutate.mutation) + is Result.Simple -> Result.Simple( + resultToMutate.mutation(resultToMutate.result, random), + resultToMutate.mutation + ) is Result.Known -> { val mutations = resultToMutate.value.mutations() if (mutations.isNotEmpty()) { @@ -29,19 +40,25 @@ class MutationFactory { when { resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> RecursiveMutations.Constructor() + random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> RecursiveMutations.ShuffleAndCutModifications() + else -> RecursiveMutations.Mutate() +// }.mutateWithMemo(resultToMutate, recursive, random, configuration, this) }.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { when { random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> CollectionMutations.Mutate() + else -> CollectionMutations.Shuffle() }.mutate(resultToMutate, recursive, random, configuration) +// }.mutateWithMemo(resultToMutate, recursive, random, configuration, this) + } else { resultToMutate } @@ -82,12 +99,17 @@ class MutationFactory { } @Suppress("UNCHECKED_CAST") - private fun > Result.Known.mutate(mutation: Mutation, random: Random, configuration: Configuration): Result.Known { + private fun > Result.Known.mutate( + mutation: Mutation, + random: Random, + configuration: Configuration + ): Result.Known { val source: T = value as T val mutate = mutation.mutate(source, random, configuration) return Result.Known( mutate, - build as (T) -> RESULT + build as (T) -> RESULT, + mutations = setOf(mutation) ) } } @@ -106,6 +128,11 @@ fun emptyMutation(): (RESULT, random: Random) -> RESULT { */ fun interface Mutation { fun mutate(source: T, random: Random, configuration: Configuration): T + + fun mutateWithMemo(source: T, random: Random, configuration: Configuration, factory: MutationFactory): T { + factory.memorizeMutation(source, this) + return mutate(source, random, configuration) + } } sealed class BitVectorMutations : Mutation { @@ -244,7 +271,8 @@ sealed interface CollectionMutations : Mutation : Mutation : Mutation { return Result.Recursive( construct = recursive.mutate(source.construct,random, configuration), - modify = source.modify + modify = source.modify, + mutations = source.mutations + setOf(this), ) } } @@ -309,7 +339,8 @@ sealed interface RecursiveMutations : Mutation { return Result.Recursive( construct = source.construct, - modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)) + modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)), + mutations = source.mutations + setOf(this), ) } } @@ -326,7 +357,8 @@ sealed interface RecursiveMutations : Mutation, Any?, Description, Any?>, Feedback, Any?>> { - override fun generate(description: Description, Any?>, type: Class<*>) = sequence, Any?>> { +object JavaFuzzing : Fuzzing, Any?, LoggingDescription, Any?>, Feedback, Any?>> { + override fun generate(description: LoggingDescription, Any?>, type: Class<*>) = sequence, Any?>> { if (type == Boolean::class.javaPrimitiveType) { yield(Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) yield(Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) @@ -71,7 +71,7 @@ object JavaFuzzing : Fuzzing, Any?, Description, Any?>, Feedba } } - override suspend fun handle(description: Description, Any?>, values: List): Feedback, Any?> { + override suspend fun handle(description: LoggingDescription, Any?>, values: List): Feedback, Any?> { println(values.joinToString { when (it) { is BooleanArray -> it.contentToString() @@ -88,7 +88,12 @@ object JavaFuzzing : Fuzzing, Any?, Description, Any?>, Feedba } suspend fun main() { +// JavaFuzzing.fuzz( +// Description(listOf(Int::class.javaPrimitiveType!!, CharArray::class.java, A::class.java)), +// ) + JavaFuzzing.fuzz( - Description(listOf(Int::class.javaPrimitiveType!!, CharArray::class.java, A::class.java)), + LoggingDescription(listOf(Int::class.javaPrimitiveType!!, CharArray::class.java, A::class.java), + "~/.utbot/DemoJavaFuzzing") ) } \ No newline at end of file From abd7d4942adcb2ad6a0325882423456059346daa Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 31 Jul 2023 14:03:29 +0300 Subject: [PATCH 09/25] Log only last mutation --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 40 +++++++++++-------- .../kotlin/org/utbot/fuzzing/Mutations.kt | 27 +++---------- .../kotlin/org/utbot/fuzzing/Statistic.kt | 39 +++++++++--------- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 6884ace400..3e596f1d8d 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -62,7 +62,7 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, SingleEntryMinsetStatistic(statistics)) + fuzz(description, LastKeepingSingleValueMinsetStatistic(statistics)) // fuzz(description, BasicSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) } @@ -445,7 +445,7 @@ suspend fun , F : Feedback> Fuzzing { /** * Known value. */ - class Known>(val value: V, val build: (V) -> RESULT, val mutations: Set> = setOf()) : Result { + class Known>(val value: V, val build: (V) -> RESULT, val lastMutation: Mutation? = null) : Result { override fun toString(): String { return this.value.toString() } @@ -813,7 +813,7 @@ sealed interface Result { class Recursive( val construct: Node, val modify: List>, - val mutations: Set> = setOf(), + val lastMutation: RecursiveMutations? = null, ) : Result /** @@ -823,7 +823,7 @@ sealed interface Result { val construct: Node, val modify: List>, val iterations: Int, - val mutations: Set> = setOf(), + val lastMutation: CollectionMutations? = null, ) : Result /** @@ -843,21 +843,27 @@ class Node( val builder: Routine, ) { override fun toString() : String { - return result.map { it -> + return result.map { when(it) { is Result.Empty -> "_" is Result.Simple -> it.result - is Result.Known<*, *, *> -> "$it : ${it.mutations - .map { it.toString().substringAfter("$").substringBefore('@') - }.joinToString(", ", "(", ")")}" - is Result.Collection -> it.modify - .joinToString(", ", "[", "]") + " : ${it.mutations.map { - it.toString().substringAfter("$").substringBefore('@') - }.joinToString(", ", "(", ")") }}" - is Result.Recursive -> it.modify - .joinToString(", ", "{", "}")+ " : ${it.mutations.map { - it.toString().substringAfter("$").substringBefore('@') - }.joinToString(", ", "(", ")") }" + is Result.Known<*, *, *> -> { + if (it.lastMutation != null) { + "${it.value} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.value.toString() } + } + is Result.Collection -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "[", "]")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.modify.joinToString(", ", "[", "]") } + } + is Result.Recursive -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "{", "}")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { + it.modify.joinToString(", ", "{", "}") + } + } } }.joinToString(", ") } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 4cc7b2f94f..e1fa80ee1f 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -8,15 +8,6 @@ import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random class MutationFactory { - - private val memo: HashMap> = linkedMapOf>() - fun memorizeMutation(source: TYPE, mutation: Mutation) { - if (memo.containsKey(source)) { - error("New mutation for memorized value") - } - memo[source] = mutation - } - fun mutate(node: Node, random: Random, configuration: Configuration): Node { if (node.result.isEmpty()) return node val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) @@ -57,7 +48,6 @@ class MutationFactory { else -> CollectionMutations.Shuffle() }.mutate(resultToMutate, recursive, random, configuration) -// }.mutateWithMemo(resultToMutate, recursive, random, configuration, this) } else { resultToMutate @@ -109,7 +99,7 @@ class MutationFactory { return Result.Known( mutate, build as (T) -> RESULT, - mutations = setOf(mutation) + lastMutation = mutation ) } } @@ -128,11 +118,6 @@ fun emptyMutation(): (RESULT, random: Random) -> RESULT { */ fun interface Mutation { fun mutate(source: T, random: Random, configuration: Configuration): T - - fun mutateWithMemo(source: T, random: Random, configuration: Configuration, factory: MutationFactory): T { - factory.memorizeMutation(source, this) - return mutate(source, random, configuration) - } } sealed class BitVectorMutations : Mutation { @@ -272,7 +257,7 @@ sealed interface CollectionMutations : Mutation : Mutation : Mutation : Mutation : Mutation>( } } -open class SingleValueMinsetStatistic, STORAGE : SingleValueStorage>( +open class SingleValueMinsetStatistic, STORAGE : SingleValueStorage> ( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), @@ -152,7 +152,7 @@ open class BasicSingleValueMinsetStatistic>( +class LastKeepingSingleValueMinsetStatistic>( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), @@ -161,6 +161,7 @@ class SingleEntryMinsetStatistic ) : BasicSingleValueMinsetStatistic( totalRuns, startTime, missedTypes, random, configuration, SingleValueSelectionStrategy.LAST ) { + private var lastSeed: Node? = null constructor(source: Statistic) : this ( totalRuns = source.totalRuns, startTime = source.startTime, @@ -168,6 +169,23 @@ class SingleEntryMinsetStatistic random = source.random, configuration = source.configuration.copy() ) + + override fun put( + random: Random, + configuration: Configuration, + feedback: FEEDBACK, + seed: Node + ): MinsetEvent { + lastSeed = seed + return super.put(random, configuration, feedback, seed) + } + + override fun getRandomSeed(random: Random, configuration: Configuration): Node { + if (lastSeed == null) { + error("Seed requested but no seed stored") + } + return lastSeed!! + } } ///endregion @@ -196,8 +214,6 @@ abstract class Minset, STORAGE : seeds[feedback]!!.put(value, feedback) } -// seeds.getOrPut(feedback) { valueStorageGenerator.invoke() }.put(value, feedback) - count[feedback] = count.getOrDefault(feedback, 0L) + 1L return result @@ -220,21 +236,6 @@ open class SingleValueMinset, ST override val valueStorageGenerator: () -> STORAGE, ) : Minset(valueStorageGenerator) -@Suppress("UNCHECKED_CAST") -class SingleEntryMinset, STORAGE : SingleValueStorage>: - SingleValueMinset( { SingleValueStorage(SingleValueSelectionStrategy.LAST) as STORAGE} ) { - override fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { - val result : MinsetEvent = if (seeds.containsKey(feedback)) { - MinsetEvent.NEW_VALUE - } else { - seeds.clear() - seeds[feedback] = SingleValueStorage(SingleValueSelectionStrategy.LAST) as STORAGE - MinsetEvent.NEW_FEEDBACK - } - seeds[feedback]!!.put(value, feedback) - return result - } - } ///endregion From 7f6593b88933d6e2ebb4dcba743c304050e083db Mon Sep 17 00:00:00 2001 From: grigory Date: Tue, 1 Aug 2023 13:34:47 +0300 Subject: [PATCH 10/25] Keep last mutation for each result node --- utbot-fuzzing/src/main/resources/loganal.py | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 utbot-fuzzing/src/main/resources/loganal.py diff --git a/utbot-fuzzing/src/main/resources/loganal.py b/utbot-fuzzing/src/main/resources/loganal.py new file mode 100644 index 0000000000..ba310a72a3 --- /dev/null +++ b/utbot-fuzzing/src/main/resources/loganal.py @@ -0,0 +1,89 @@ +import os +import sys +import re +import pandas as pd +from tqdm import tqdm +from matplotlib import pyplot as plt + +def build_dataframe(logpath: str): + pattern = r'(?<=: )\w+(?=,|)' + mutation_labels = set() + + file_path = os.path.expanduser(logpath) + with open(file_path, 'r') as log: + lines_counter = 0 + for line in log.readlines(): + try: + if 'class name' in line.split("|")[1]: + continue + + mutations = re.findall(pattern, line.split("|")[1]) + + for mutation in mutations: + mutation_labels.add(mutation) + lines_counter+= 1 + except: + pass + + log.seek(0) + + data = [] + for line in tqdm(log, total = lines_counter): + try: + (time, value, class_name, method, line_number, trace_hash, trace_count, event, _) = line.split(' | ') + mutations = re.findall(pattern, value) + mutations_mapping = [ 1 if mutation in mutations else 0 for mutation in mutation_labels ] + + data.append( + [ + class_name.split(': ')[1], + method.split(': ', 1), + value + ] + mutations_mapping + [ + line_number.split(': ')[1], + trace_hash.split(': ')[1], + trace_count.split(': ')[1], + event, + time[1:-1] + ] + ) + except: + pass + + return (pd.DataFrame(data, + columns = ['class_name', 'method', 'value'] + list(mutation_labels) + ['line_number', 'trace_hash', 'trace_count', 'event', 'time'] + ), + list(mutation_labels)) + + +if __name__ == "__main__": + (df, mutation_labels) = build_dataframe(sys.argv[1]) + df = df.assign(new_feedbacks_count = (df.event == 'NEW_FEEDBACK').cumsum()) + + plt.figure(0) + ax1 = plt.subplot2grid((2,3), (0,0), colspan=2) + ax2 = plt.subplot2grid((2,3), (0,2)) + ax3 = plt.subplot2grid((2,3), (1,0), colspan=3) + + ax1.set_title('Specific effectiveness') + [ ax1.plot( df.index, (df[label]*df.new_feedbacks_count.diff()).cumsum() / df[label].cumsum()) for label in mutation_labels ] + ax1.legend(mutation_labels) + ax1.set_xlabel('Iteration') + ax1.set_ylabel('Specific effectiveness') + ax1.set_yscale('log') + ax1.grid() + + ax2.set_title('Total specific effectiveness') + ax2.bar(mutation_labels, [ (df[label] * df.new_feedbacks_count.diff()).sum() / df[label].sum() for label in mutation_labels] ) + # ax2.grid(axis='y') + + ax3.set_title('Number of discovered traces') + [ ax3.plot(df.index, (df.new_feedbacks_count.diff() * df[label]).cumsum()) for label in mutation_labels ] + plt.plot(df.index, df.new_feedbacks_count) + ax3.legend(labels = mutation_labels + ['Overall']) + ax3.set_xlabel('Iteration') + ax3.set_ylabel('Number of traces') + ax3.grid() + + plt.suptitle('Regular traces') + plt.show() From 907c8c81587a27d3ff45d4e8d4227211b107e72b Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 2 Aug 2023 14:29:12 +0300 Subject: [PATCH 11/25] Correct mutation probabilities for known values --- utbot-fuzzing/src/main/resources/loganal.py | 48 +++++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/utbot-fuzzing/src/main/resources/loganal.py b/utbot-fuzzing/src/main/resources/loganal.py index ba310a72a3..701b26a273 100644 --- a/utbot-fuzzing/src/main/resources/loganal.py +++ b/utbot-fuzzing/src/main/resources/loganal.py @@ -1,6 +1,7 @@ import os import sys import re +import argparse import pandas as pd from tqdm import tqdm from matplotlib import pyplot as plt @@ -50,20 +51,19 @@ def build_dataframe(logpath: str): except: pass - return (pd.DataFrame(data, - columns = ['class_name', 'method', 'value'] + list(mutation_labels) + ['line_number', 'trace_hash', 'trace_count', 'event', 'time'] - ), - list(mutation_labels)) + df = pd.DataFrame(data, columns = + ['class_name', 'method', 'value'] + list(mutation_labels) + + ['line_number', 'trace_hash', 'trace_count', 'event', 'time']) + df = df.assign(new_feedbacks_count = (df.event == 'NEW_FEEDBACK').cumsum()) + return (df, list(mutation_labels)) -if __name__ == "__main__": - (df, mutation_labels) = build_dataframe(sys.argv[1]) - df = df.assign(new_feedbacks_count = (df.event == 'NEW_FEEDBACK').cumsum()) - +def draw_report(df: pd.DataFrame, mutations_labels: list, title: str): plt.figure(0) - ax1 = plt.subplot2grid((2,3), (0,0), colspan=2) - ax2 = plt.subplot2grid((2,3), (0,2)) - ax3 = plt.subplot2grid((2,3), (1,0), colspan=3) + ax1 = plt.subplot2grid((3,3), (0,0), colspan=2) + ax2 = plt.subplot2grid((3,3), (0,2)) + ax3 = plt.subplot2grid((3,3), (1,0), colspan=3) + ax4 = plt.subplot2grid((3,3), (2,0), colspan=3) ax1.set_title('Specific effectiveness') [ ax1.plot( df.index, (df[label]*df.new_feedbacks_count.diff()).cumsum() / df[label].cumsum()) for label in mutation_labels ] @@ -75,15 +75,35 @@ def build_dataframe(logpath: str): ax2.set_title('Total specific effectiveness') ax2.bar(mutation_labels, [ (df[label] * df.new_feedbacks_count.diff()).sum() / df[label].sum() for label in mutation_labels] ) - # ax2.grid(axis='y') + ax2.grid(axis='y') ax3.set_title('Number of discovered traces') [ ax3.plot(df.index, (df.new_feedbacks_count.diff() * df[label]).cumsum()) for label in mutation_labels ] - plt.plot(df.index, df.new_feedbacks_count) + ax3.plot(df.index, df.new_feedbacks_count) ax3.legend(labels = mutation_labels + ['Overall']) ax3.set_xlabel('Iteration') ax3.set_ylabel('Number of traces') ax3.grid() - plt.suptitle('Regular traces') + [ ax4.plot(df.index[50:], (df[label].cumsum() / df.index)[50:]) for label in mutation_labels ] + ax4.legend(labels=mutation_labels) + ax4.set_xlabel('Iteration') + ax4.set_ylabel('Number of mutations') + ax4.grid() + + if title: + plt.suptitle(title) + plt.show() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('path', help='Log file path', type=str) + parser.add_argument('--title', help='Report title', type=str) + args = parser.parse_args() + + (df, mutation_labels) = build_dataframe(args.path) + + draw_report(df, mutation_labels, args.title) + From c15937e56091ac2daea222d3213b8148d82706e7 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 2 Aug 2023 16:03:06 +0300 Subject: [PATCH 12/25] Move investigations iterations number in Configuration and add MutationsCountingSingleValueStorage --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 10 ++- .../kotlin/org/utbot/fuzzing/Configuration.kt | 5 ++ .../kotlin/org/utbot/fuzzing/Mutations.kt | 38 +++++++- .../kotlin/org/utbot/fuzzing/Statistic.kt | 89 ++++++++++++++++++- 4 files changed, 131 insertions(+), 11 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 3e596f1d8d..e32a70fa7b 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -62,8 +62,9 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, LastKeepingSingleValueMinsetStatistic(statistics)) +// fuzz(description, LastKeepingSingleValueMinsetStatistic(statistics)) // fuzz(description, BasicSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) + fuzz(description, MutationsCountingSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) } /** @@ -445,8 +446,9 @@ suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing>, Float>.randomMutation(random: Random): Mutation>? { + val weightsSum = entries.sumOf { it.value.toDouble() } + + if (weightsSum == 0.0) { + return null + } + + var randomWeight = random.nextDouble( weightsSum ) + + for (entry in entries) { + randomWeight -= entry.value.toDouble() + if (randomWeight <= 0) { + return entry.key + } + } + + return null +} + class MutationFactory { - fun mutate(node: Node, random: Random, configuration: Configuration): Node { + fun mutate( + node: Node, + random: Random, + configuration: Configuration, + statistic: SeedsMaintainingStatistic + ): Node { if (node.result.isEmpty()) return node val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) val recursive: NodeMutation = NodeMutation { n, r, c -> - mutate(n, r, c) + mutate(n, r, c, statistic) } val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { is Result.Simple -> Result.Simple( @@ -21,8 +45,15 @@ class MutationFactory { ) is Result.Known -> { val mutations = resultToMutate.value.mutations() + + val mutation = if (configuration.investigationPeriodIterations > 0 && statistic.totalRuns > configuration.investigationPeriodIterations) { + statistic.getMutationsEfficiencies().randomMutation(random) ?: mutations.random(random) + } else { + mutations.random(random) + } + if (mutations.isNotEmpty()) { - resultToMutate.mutate(mutations.random(random), random, configuration) + resultToMutate.mutate(mutation, random, configuration) } else { resultToMutate } @@ -37,7 +68,6 @@ class MutationFactory { else -> RecursiveMutations.Mutate() -// }.mutateWithMemo(resultToMutate, recursive, random, configuration, this) }.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 64b1a58c70..7056eed51c 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -1,5 +1,6 @@ package org.utbot.fuzzing +import org.utbot.fuzzing.seeds.KnownValue import org.utbot.fuzzing.utils.MissedSeed import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin @@ -26,6 +27,9 @@ interface SeedsMaintainingStatistic fun isNotEmpty() : Boolean fun getMinsetSize() : Int + fun getMutationsEfficiencies(): Map>, Float> { + return emptyMap() + } } open class BaseStatisticImpl>( @@ -152,6 +156,26 @@ open class BasicSingleValueMinsetStatistic>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, + private val seedSelectionStrategy: SingleValueSelectionStrategy +) : SingleValueMinsetStatistic>( + totalRuns, startTime, missedTypes, random, configuration, { MutationsCountingSingleValueStorage(seedSelectionStrategy) } +) { + constructor(source: Statistic, seedSelectionStrategy: SingleValueSelectionStrategy) : this ( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy(), + seedSelectionStrategy = seedSelectionStrategy + ) +} + class LastKeepingSingleValueMinsetStatistic>( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), @@ -161,7 +185,12 @@ class LastKeepingSingleValueMinsetStatistic( totalRuns, startTime, missedTypes, random, configuration, SingleValueSelectionStrategy.LAST ) { + private val successCumSums: HashMap, Int> = hashMapOf() + private val overallCumSums: HashMap, Int> = hashMapOf() + private val knownValueMutationsEfficiencies: HashMap>, Float> = hashMapOf() + private var lastSeed: Node? = null + constructor(source: Statistic) : this ( totalRuns = source.totalRuns, startTime = source.startTime, @@ -177,7 +206,41 @@ class LastKeepingSingleValueMinsetStatistic ): MinsetEvent { lastSeed = seed - return super.put(random, configuration, feedback, seed) + val event = super.put(random, configuration, feedback, seed) + + seed.result.forEach { result -> + when(result) { + is Result.Known -> { + result.lastMutation?.let { + overallCumSums[it] = overallCumSums.getOrDefault(it, 0) + 1 + } + } + else -> {} + } + } + + if (event == MinsetEvent.NEW_FEEDBACK) { + seed.result.forEach { result -> + when(result) { + is Result.Known -> { + result.lastMutation.let { + it?.let { + successCumSums[it] = successCumSums.getOrDefault(it, 0) + 1 + knownValueMutationsEfficiencies[it as Mutation>] = + successCumSums[it]!!.toFloat() / overallCumSums[it]!!.toFloat() + } + } + } + else -> {} + } + } + } + + return event + } + + override fun getMutationsEfficiencies() : Map>, Float> { + return knownValueMutationsEfficiencies } override fun getRandomSeed(random: Random, configuration: Configuration): Node { @@ -235,8 +298,6 @@ abstract class Minset, STORAGE : open class SingleValueMinset, STORAGE : SingleValueStorage> ( override val valueStorageGenerator: () -> STORAGE, ) : Minset(valueStorageGenerator) - - ///endregion ///region Value storages @@ -283,6 +344,28 @@ open class SingleValueStorage ( } } +class MutationsCountingSingleValueStorage(strategy: SingleValueSelectionStrategy) : + SingleValueStorage(strategy) { + private val knownValueMutationsCount: HashMap, Int> = hashMapOf() + + override fun put(value: Node, feedback: Feedback): Boolean { + value.result.forEach { result -> + when(result) { + is Result.Known<*, *, *> -> { + result.lastMutation.let { + it?.let { + knownValueMutationsCount[it] = knownValueMutationsCount.getOrDefault(it, 0) + 1 + } + } + } + else -> {} + } + } + + return super.put(value, feedback) + } +} + //@Suppress("UNCHECKED_CAST") //class SingleDistributableValueStorage>( // strategy: SingleValueSelectionStrategy, From 51fd1317937612752cd617d54239aa9be0677513 Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 7 Aug 2023 10:56:07 +0300 Subject: [PATCH 13/25] Minor fixes --- .../org.mockito.plugins.MockMaker | 1 + .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 14 +- .../kotlin/org/utbot/fuzzing/Configuration.kt | 4 +- .../kotlin/org/utbot/fuzzing/Mutations.kt | 6 +- .../kotlin/org/utbot/fuzzing/Statistic.kt | 129 ++++++----- utbot-fuzzing/src/main/resources/loganal.py | 211 +++++++++++------- utbot-fuzzing/src/main/resources/sandbox.py | 26 +++ .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 2 +- 8 files changed, 240 insertions(+), 153 deletions(-) create mode 100644 utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 utbot-fuzzing/src/main/resources/sandbox.py diff --git a/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index e32a70fa7b..9ace95817f 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -62,9 +62,8 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { -// fuzz(description, LastKeepingSingleValueMinsetStatistic(statistics)) -// fuzz(description, BasicSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) - fuzz(description, MutationsCountingSingleValueMinsetStatistic(statistics, seedSelectionStrategy = SingleValueSelectionStrategy.LAST)) + fuzz(description, SingleSeedKeepingStatistics(statistics)) +// fuzz(description, BasicSingleValueMinsetStatistic(statistics)) } /** @@ -187,7 +186,7 @@ class LoggingReporter( out.println("Total runs: ${statistic.totalRuns}") if (statistic is SeedsMaintainingStatistic<*, *, *>) { - out.println("Final minset size: ${statistic.getMinsetSize()}") + out.println("Final minset size: ${statistic.size()}") } val seconds = statistic.elapsedTime / 1_000_000_000 @@ -446,9 +445,10 @@ suspend fun , F : Feedback> Fuzzing>, Float>.randomMutation(random: Random): Mutation>? { +fun Map, Float>.randomMutation(random: Random): Mutation<*>? { val weightsSum = entries.sumOf { it.value.toDouble() } if (weightsSum == 0.0) { @@ -52,8 +52,10 @@ class MutationFactory { mutations.random(random) } +// val mutation = mutations.random(random) + if (mutations.isNotEmpty()) { - resultToMutate.mutate(mutation, random, configuration) + resultToMutate.mutate(mutation as Mutation>, random, configuration) } else { resultToMutate } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 7056eed51c..352dc57eaa 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -25,11 +25,9 @@ interface SeedsMaintainingStatistic) : MinsetEvent fun getRandomSeed(random: Random, configuration: Configuration): Node + fun getMutationsEfficiencies(): Map, Float> { return emptyMap() } fun isNotEmpty() : Boolean - fun getMinsetSize() : Int - fun getMutationsEfficiencies(): Map>, Float> { - return emptyMap() - } + fun size() : Int } open class BaseStatisticImpl>( @@ -85,7 +83,8 @@ open class BaseStatisticImpl>( } override fun isNotEmpty() = seeds.isNotEmpty() - override fun getMinsetSize(): Int { + + override fun size(): Int { return seeds.size } } @@ -131,7 +130,7 @@ open class SingleValueMinsetStatistic>( + +open class SingleSeedKeepingStatistics> ( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), override val random: Random, - override val configuration: Configuration, -) : BasicSingleValueMinsetStatistic( - totalRuns, startTime, missedTypes, random, configuration, SingleValueSelectionStrategy.LAST -) { - private val successCumSums: HashMap, Int> = hashMapOf() - private val overallCumSums: HashMap, Int> = hashMapOf() - private val knownValueMutationsEfficiencies: HashMap>, Float> = hashMapOf() - - private var lastSeed: Node? = null - - constructor(source: Statistic) : this ( + override val configuration: Configuration +): SeedsMaintainingStatistic { + constructor(source: Statistic) : this( totalRuns = source.totalRuns, startTime = source.startTime, missedTypes = source.missedTypes, @@ -199,14 +191,20 @@ class LastKeepingSingleValueMinsetStatistic - ): MinsetEvent { - lastSeed = seed - val event = super.put(random, configuration, feedback, seed) + override val elapsedTime: Long + get() = System.nanoTime() - startTime + + private val storedSeed = SingleValueStorage(configuration.singleValueSelectionStrategy) + private val feedbacksCount: LinkedHashMap = linkedMapOf() + private val successCumSums: HashMap, Int> = hashMapOf() + private val overallCumSums: HashMap, Int> = hashMapOf() + private val mutationsEfficiencies: HashMap, Float> = hashMapOf() + + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { + storedSeed.put(seed, feedback) + feedbacksCount.merge(feedback, 1L, Long::plus) + val event = if (feedbacksCount[feedback] == 1L) MinsetEvent.NEW_FEEDBACK else MinsetEvent.NOTHING_NEW seed.result.forEach { result -> when(result) { @@ -226,7 +224,7 @@ class LastKeepingSingleValueMinsetStatistic>] = + mutationsEfficiencies[it] = successCumSums[it]!!.toFloat() / overallCumSums[it]!!.toFloat() } } @@ -239,18 +237,54 @@ class LastKeepingSingleValueMinsetStatistic>, Float> { - return knownValueMutationsEfficiencies + override fun getRandomSeed(random: Random, configuration: Configuration): Node { + return storedSeed.next() } - override fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (lastSeed == null) { - error("Seed requested but no seed stored") - } - return lastSeed!! + override fun getMutationsEfficiencies(): Map, Float> { + return mutationsEfficiencies } + + override fun isNotEmpty(): Boolean { + return feedbacksCount.isNotEmpty() + } + + override fun size(): Int { + return feedbacksCount.size + } +} + +class FirstSeedKeepingStatistics>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, +) : SingleSeedKeepingStatistics(totalRuns, startTime, missedTypes, random, configuration) { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) } +class LastSeedKeepingStatistics>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, +) : SingleSeedKeepingStatistics(totalRuns, startTime, missedTypes, random, configuration) { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) +} ///endregion ///region Minset @@ -316,12 +350,7 @@ open class SingleValueStorage ( private var storedValue: Node? = null override fun put(value: Node, feedback: Feedback) : Boolean { - - var result = false; - - if(storedValue == null) { - result = true - } + val result = storedValue == null storedValue = when (strategy) { SingleValueSelectionStrategy.FIRST -> storedValue ?: value @@ -366,24 +395,4 @@ class MutationsCountingSingleValueStorage(strategy: SingleValueSel } } -//@Suppress("UNCHECKED_CAST") -//class SingleDistributableValueStorage>( -// strategy: SingleValueSelectionStrategy, -// var distributionTree: DistributionNode -//) : SingleValueStorage(strategy) { -// fun put(value: Node, feedback: BaseWeightedFeedback<*, TYPE, RESULT, *>) { -// super.put(value, feedback) -// value.result.forEach { -//// distributionTree.put(it) -// when(it) { -// is Result.Empty -> {} -// is Result.Simple -> distributionTree.put(it as RESULT) -// is Result.Collection -> distributionTree.put(it as RESULT) -// is Result.Known<*, *, *> -> distributionTree.put(it as RESULT) -// is Result.Recursive -> distributionTree.put(it as RESULT) -// } -// } -// } -//} - ///endregion \ No newline at end of file diff --git a/utbot-fuzzing/src/main/resources/loganal.py b/utbot-fuzzing/src/main/resources/loganal.py index 701b26a273..6d653aeaad 100644 --- a/utbot-fuzzing/src/main/resources/loganal.py +++ b/utbot-fuzzing/src/main/resources/loganal.py @@ -1,109 +1,156 @@ import os import sys import re +from datetime import datetime import argparse import pandas as pd from tqdm import tqdm from matplotlib import pyplot as plt -def build_dataframe(logpath: str): - pattern = r'(?<=: )\w+(?=,|)' - mutation_labels = set() - - file_path = os.path.expanduser(logpath) - with open(file_path, 'r') as log: - lines_counter = 0 - for line in log.readlines(): - try: - if 'class name' in line.split("|")[1]: - continue - mutations = re.findall(pattern, line.split("|")[1]) +plt.rcParams["figure.figsize"] = (16, 9) - for mutation in mutations: - mutation_labels.add(mutation) - lines_counter+= 1 - except: - pass - log.seek(0) +def parse_java_log(file_path: str, operation): + with open(file_path, 'r') as log: + items = log.read().replace('\n', '').split(' | ')[:-1] - data = [] - for line in tqdm(log, total = lines_counter): - try: - (time, value, class_name, method, line_number, trace_hash, trace_count, event, _) = line.split(' | ') - mutations = re.findall(pattern, value) - mutations_mapping = [ 1 if mutation in mutations else 0 for mutation in mutation_labels ] - - data.append( - [ - class_name.split(': ')[1], - method.split(': ', 1), - value - ] + mutations_mapping + [ - line_number.split(': ')[1], - trace_hash.split(': ')[1], - trace_count.split(': ')[1], - event, - time[1:-1] - ] - ) - except: - pass - - df = pd.DataFrame(data, columns = - ['class_name', 'method', 'value'] + list(mutation_labels) + - ['line_number', 'trace_hash', 'trace_count', 'event', 'time']) - df = df.assign(new_feedbacks_count = (df.event == 'NEW_FEEDBACK').cumsum()) - - return (df, list(mutation_labels)) - -def draw_report(df: pd.DataFrame, mutations_labels: list, title: str): + c = 0 + for item in items: + match c: + case 0: time = datetime.strptime(item, '[%H:%M:%S:%f]') + case 1: value = item + case 2: + try: + class_name = item.split(': ')[1] + except: + print(item) + case 3: method = item.split(': ')[1] + case 4: line_number = item.split(': ')[1] + case 5: trace_hash = item.split(': ')[1] + case 6: trace_count = item.split(': ')[1] + case 7: + event = item + operation(time, value, class_name, method, line_number, trace_hash, trace_count, event) + c = -1 + c += 1 + pass + +def build_dataframe(logpath: str): + file_path = os.path.expanduser(logpath) + pattern = r'(? Date: Thu, 10 Aug 2023 11:48:55 +0300 Subject: [PATCH 14/25] Multiple minor fixes, some refactoring and comments --- .gitignore | 1 + .../utbot/framework/plugin/api/CoverageApi.kt | 2 +- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 38 +- .../kotlin/org/utbot/fuzzing/Configuration.kt | 30 +- .../kotlin/org/utbot/fuzzing/Mutations.kt | 41 +- .../kotlin/org/utbot/fuzzing/Statistic.kt | 385 +++++++++--------- .../org/utbot/fuzzing/seeds/StringValue.kt | 2 +- utbot-fuzzing/src/main/resources/loganal.py | 156 ------- utbot-fuzzing/src/main/resources/sandbox.py | 26 -- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 2 +- 10 files changed, 251 insertions(+), 432 deletions(-) delete mode 100644 utbot-fuzzing/src/main/resources/loganal.py delete mode 100644 utbot-fuzzing/src/main/resources/sandbox.py diff --git a/.gitignore b/.gitignore index 5b11d761cb..25ab286e68 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target/ utbot-intellij/src/main/resources/settings.properties __pycache__ .dmypy.json +tbot-fuzzing/src/main/resources/* diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 1c8528043a..22c9d49518 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -19,7 +19,7 @@ data class Instruction( val className: String get() = internalName.replace('/', '.') override fun toString(): String { - return "class name: ${className.split(".").last()} | method: $methodSignature | line number: $lineNumber" + return "class name: ${className.split(".").last()}; method: $methodSignature; line number: $lineNumber" } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 9ace95817f..ee472504e9 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -12,6 +12,8 @@ import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.random.Random +import java.nio.charset.Charset + private val logger by lazy { KotlinLogging.logger {} } @@ -62,8 +64,7 @@ interface Fuzzing, FEEDBAC * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, SingleSeedKeepingStatistics(statistics)) -// fuzz(description, BasicSingleValueMinsetStatistic(statistics)) + fuzz(description, MainStatisticImpl(statistics)) } /** @@ -158,25 +159,23 @@ class LoggingReporter( init { File(actualPath).mkdirs() -// logFile.apply { delete(); createNewFile() } -// overviewFile.apply { delete(); createNewFile() } } override fun update(values: Node, feedback: Feedback, event : MinsetEvent, additionalMessage: String) { - - fun alignString(obj: Any, length: Int) : String { - val aligned = obj.toString().replace("\n", "/") - return if (aligned.length > length) { - aligned.take((length-1)/2) + ".." + aligned.takeLast((length-1)/2) - } else { - aligned + " ".repeat(length - aligned.length) - } - } - val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) FileOutputStream(logFile, true).bufferedWriter().use { - it.write("[$time] | $values | $feedback | $event | $additionalMessage\n") + + val printableValues = buildString { + values.toString() + .toByteArray(Charsets.UTF_8) + .forEach { + byte -> val hexString = byte.toInt().toString(16).padStart(2, '0') + append("\\u").append(hexString) + } + } + + it.write("[$time]\t$printableValues\t$feedback\t$event\t$additionalMessage\n") } } @@ -445,10 +444,7 @@ suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing { class Node( val result: List>, val parameters: List, - val builder: Routine, + val builder: Routine ) { override fun toString() : String { return result.map { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index 6ef1a8fdf1..6d3fd81236 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -7,15 +7,27 @@ import kotlin.math.pow */ data class Configuration( + /** + * Configuration used by minset to accumulate and use statistics. + * + * TODO: This part of configuration tend to be changing through runtime tuning or evolutionary algorithm. + */ + var minsetConfiguration: MinsetConfiguration = MinsetConfiguration(), + + /** + * Number of continuous iterations for each value. + */ + var runsPerValue: Long = 100, + /** * Number of iterations before mutations probabilities correction. */ - var investigationPeriodIterations: Int = 500, + var investigationPeriodPerValue: Int = 50, /** * Choose between already generated values and new generation of values. */ - var probSeedRetrievingInsteadGenerating: Int = 100, + var probSeedRetrievingInsteadGenerating: Int = 75, /** * Choose between generation and mutation. @@ -100,8 +112,18 @@ data class Configuration( * Limits maximum number of recursive seed modifications */ var maxNumberOfRecursiveSeedModifications: Int = 10, +) - val singleValueSelectionStrategy: SingleValueSelectionStrategy = SingleValueSelectionStrategy.LAST +/** + * Configuration used by minset to accumulate and use statistics. + */ +data class MinsetConfiguration ( + var obsolescenceMultiplier: Double = 1.0, + var rewardMultiplier: Double = 1.0, + var rewardWeight: Double = 1.0, + var penaltyMultiplier: Double = 1.0, + var penaltyWeight: Double = 0.0, + val valueStoragingStrategy: ValueStoragingStrategy = ValueStoragingStrategy.LAST ) -enum class SingleValueSelectionStrategy { FIRST, LAST } +enum class ValueStoragingStrategy { FIRST, LAST } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index c91c89e3de..fe8358e0a7 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -7,19 +7,16 @@ import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random -fun Map, Float>.randomMutation(random: Random): Mutation<*>? { - val weightsSum = entries.sumOf { it.value.toDouble() } - - if (weightsSum == 0.0) { - return null - } - - var randomWeight = random.nextDouble( weightsSum ) - - for (entry in entries) { - randomWeight -= entry.value.toDouble() - if (randomWeight <= 0) { - return entry.key +fun Map, Double>.getRandomMutation(random: Random): Mutation<*>? { + val sum = values.sum() + + if (sum > 0) { + var value = random.nextDouble(sum) + for ((mutation, weight) in entries) { + value -= weight + if (value <= 0) { + return mutation + } } } @@ -46,13 +43,13 @@ class MutationFactory { is Result.Known -> { val mutations = resultToMutate.value.mutations() - val mutation = if (configuration.investigationPeriodIterations > 0 && statistic.totalRuns > configuration.investigationPeriodIterations) { - statistic.getMutationsEfficiencies().randomMutation(random) ?: mutations.random(random) - } else { - mutations.random(random) - } - -// val mutation = mutations.random(random) + val mutation = if ( + configuration.investigationPeriodPerValue > 0 && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue + ) { + statistic.getMutationsEfficiencies().getRandomMutation(random) ?: mutations.random(random) + } else { + mutations.random(random) + } if (mutations.isNotEmpty()) { resultToMutate.mutate(mutation as Mutation>, random, configuration) @@ -68,8 +65,7 @@ class MutationFactory { random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> RecursiveMutations.ShuffleAndCutModifications() - else -> - RecursiveMutations.Mutate() + else -> RecursiveMutations.Mutate() }.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { @@ -80,7 +76,6 @@ class MutationFactory { else -> CollectionMutations.Shuffle() }.mutate(resultToMutate, recursive, random, configuration) - } else { resultToMutate } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 352dc57eaa..2915214947 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -1,9 +1,7 @@ package org.utbot.fuzzing -import org.utbot.fuzzing.seeds.KnownValue import org.utbot.fuzzing.utils.MissedSeed import org.utbot.fuzzing.utils.chooseOne -import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random /** @@ -16,167 +14,118 @@ interface Statistic { val missedTypes: MissedSeed val random: Random val configuration: Configuration -// val minsetSize: Long } ///region Statistic Implementations +/** + * Interface of statistic object with seeds maintaining logic (seed selection, value storaging, feedbacks counting, etc.) + */ interface SeedsMaintainingStatistic>: Statistic { override var totalRuns: Long fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent - fun getRandomSeed(random: Random, configuration: Configuration): Node - fun getMutationsEfficiencies(): Map, Float> { return emptyMap() } + fun getSeed(random: Random, configuration: Configuration): Node + fun getMutationsEfficiencies(): Map, Double> { return emptyMap() } fun isNotEmpty() : Boolean fun size() : Int } -open class BaseStatisticImpl>( + +/** + * Statistic implementation used for experiments with single seed. + * Keeps [Minset] with all feedbacks but [getSeed] returns only one seed, saved from last run. + */ +open class SingleSeedKeepingStatistic> ( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), override val random: Random, override val configuration: Configuration -) : SeedsMaintainingStatistic { +): SeedsMaintainingStatistic { constructor(source: Statistic) : this( totalRuns = source.totalRuns, startTime = source.startTime, missedTypes = source.missedTypes, random = source.random, - configuration = source.configuration.copy(), + configuration = source.configuration.copy() ) override val elapsedTime: Long get() = System.nanoTime() - startTime - private val seeds = linkedMapOf>() - private val count = linkedMapOf() - override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent { - var result = MinsetEvent.NOTHING_NEW + private val storedSeed = SingleValueStorage(configuration.minsetConfiguration.valueStoragingStrategy) + private val feedbacksCounts: LinkedHashMap = linkedMapOf() - if (!seeds.containsKey(feedback)) { - result = MinsetEvent.NEW_FEEDBACK - } + private val mutationsCounts = mutableMapOf, Long>() + private val mutationsSuccessCounts = mutableMapOf, Double>() + private val mutationsEfficiencies = mutableMapOf, Double>() - if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { - if (seeds[feedback] != seed) { - result = MinsetEvent.NEW_VALUE - } - seeds[feedback] = seed - } else { - seeds.putIfAbsent(feedback, seed) - } - count[feedback] = count.getOrDefault(feedback, 0L) + 1L - return result - } + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { + storedSeed.put(seed, feedback) + feedbacksCounts.merge(feedback, 1L, Long::plus) - override fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") - val entries = seeds.entries.toList() - val frequencies = DoubleArray(seeds.size).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) - } + val event = when { + feedbacksCounts[feedback] == 1L -> MinsetEvent.NEW_FEEDBACK + configuration.minsetConfiguration.valueStoragingStrategy == ValueStoragingStrategy.LAST -> MinsetEvent.NOTHING_NEW + else -> MinsetEvent.NOTHING_NEW } - val index = random.chooseOne(frequencies) - return entries[index].value - } - override fun isNotEmpty() = seeds.isNotEmpty() + seed.result.forEach { result -> + when(result) { + is Result.Known -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - override fun size(): Int { - return seeds.size - } -} + with(mutationsEfficiencies) { + forEach { (key, value) -> + this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier + } + this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + } -open class SingleValueMinsetStatistic, STORAGE : SingleValueStorage> ( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, - private val generateStorage: () -> STORAGE -) : SeedsMaintainingStatistic { - override val elapsedTime: Long - get() = System.nanoTime() - startTime + mutationsSuccessCounts[mutation] = when (event) { + MinsetEvent.NEW_FEEDBACK -> { + if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight + } else { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 + } + } + else -> mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + } + } + } + else -> {} + } + } - private val minset: Minset = SingleValueMinset( - valueStorageGenerator = generateStorage - ) + return event + } - constructor(source: Statistic, generateStorage: () -> STORAGE) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - generateStorage = generateStorage - ) + override fun getSeed(random: Random, configuration: Configuration): Node { + return storedSeed.next() + } - override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent { - return minset.put(seed, feedback) + override fun getMutationsEfficiencies(): Map, Double> { + return mutationsEfficiencies } - override fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") - val entries = minset.seeds.entries.toList() - val frequencies = DoubleArray(minset.getSize()).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction( minset.count.getOrDefault(key, 0L)) - } - } - val index = random.chooseOne(frequencies) - return entries[index].value.next() + override fun isNotEmpty(): Boolean { + return feedbacksCounts.isNotEmpty() } - override fun isNotEmpty() = minset.isNotEmpty() override fun size(): Int { - return minset.getSize() + return feedbacksCounts.size } } -open class BasicSingleValueMinsetStatistic>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, - private val seedSelectionStrategy: SingleValueSelectionStrategy -) : SingleValueMinsetStatistic>( - totalRuns, startTime, missedTypes, random, configuration, { SingleValueStorage(seedSelectionStrategy) } -) { - constructor(source: Statistic, seedSelectionStrategy: SingleValueSelectionStrategy) : this ( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - seedSelectionStrategy = seedSelectionStrategy - ) -} - -open class MutationsCountingSingleValueMinsetStatistic>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, - private val seedSelectionStrategy: SingleValueSelectionStrategy -) : SingleValueMinsetStatistic>( - totalRuns, startTime, missedTypes, random, configuration, { MutationsCountingSingleValueStorage(seedSelectionStrategy) } -) { - constructor(source: Statistic, seedSelectionStrategy: SingleValueSelectionStrategy) : this ( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - seedSelectionStrategy = seedSelectionStrategy - ) -} - - -open class SingleSeedKeepingStatistics> ( +/** + * Main implementation of [SeedsMaintainingStatistic]. Implements + * mutations efficiencies evaluating algorithm used for mutation probability tuning (see [MainStatisticImpl.put]) + * and seed selection algorithm based on feedbacks counting (see [MainStatisticImpl.getSeed]). + */ +open class MainStatisticImpl> ( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), @@ -194,104 +143,134 @@ open class SingleSeedKeepingStatistics(configuration.singleValueSelectionStrategy) - private val feedbacksCount: LinkedHashMap = linkedMapOf() - private val successCumSums: HashMap, Int> = hashMapOf() - private val overallCumSums: HashMap, Int> = hashMapOf() - private val mutationsEfficiencies: HashMap, Float> = hashMapOf() + private val minset: Minset> = Minset( + { SingleValueStorage(configuration.minsetConfiguration.valueStoragingStrategy) } + ) + + private var currentValue: Node? = null + + private val mutationsCounts = mutableMapOf, Long>() + private val mutationsSuccessCounts = mutableMapOf, Double>() + private val mutationsEfficiencies = mutableMapOf, Double>() override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { - storedSeed.put(seed, feedback) - feedbacksCount.merge(feedback, 1L, Long::plus) - val event = if (feedbacksCount[feedback] == 1L) MinsetEvent.NEW_FEEDBACK else MinsetEvent.NOTHING_NEW + val event = minset.put(seed, feedback) seed.result.forEach { result -> - when(result) { + when (result) { is Result.Known -> { - result.lastMutation?.let { - overallCumSums[it] = overallCumSums.getOrDefault(it, 0) + 1 + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + + with(mutationsEfficiencies) { + forEach { (key, value) -> + this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier + } + this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + } + + mutationsSuccessCounts[mutation] = when (event) { + MinsetEvent.NEW_FEEDBACK -> { + if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight + } else { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 + } + } + + else -> mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + } } } + else -> {} } } - if (event == MinsetEvent.NEW_FEEDBACK) { - seed.result.forEach { result -> - when(result) { - is Result.Known -> { - result.lastMutation.let { - it?.let { - successCumSums[it] = successCumSums.getOrDefault(it, 0) + 1 - mutationsEfficiencies[it] = - successCumSums[it]!!.toFloat() / overallCumSums[it]!!.toFloat() + return event + } + + + override fun getSeed(random: Random, configuration: Configuration): Node { + if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") + + currentValue?.result?.forEach { + when (it) { + is Result.Known -> { + if (this.totalRuns % configuration.runsPerValue == 1L) { + + // the law of Demeter is violated because seed selection and power schedule will be significantly reworked + val entries = minset.seeds.entries.toList() + val frequencies = DoubleArray(minset.size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) } } + + currentValue = entries[random.chooseOne(frequencies)].value.next() + + mutationsCounts.clear() + mutationsSuccessCounts.clear() + mutationsEfficiencies.clear() } - else -> {} } + else -> {} } } - return event - } + // This variable is used for dirty way to turn off sequential running on once chosen value for debugging. + // Breakpoint below evaluates "enabled = false", so currentValue is chosen again independently + val enabled = true - override fun getRandomSeed(random: Random, configuration: Configuration): Node { - return storedSeed.next() - } + if (currentValue == null || !enabled) { + // the law of Demeter is violated because seed selection and power schedule will be significantly reworked + val entries = minset.seeds.entries.toList() + val frequencies = DoubleArray(minset.size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) + } + } - override fun getMutationsEfficiencies(): Map, Float> { - return mutationsEfficiencies + currentValue = entries[random.chooseOne(frequencies)].value.next() + } + + return currentValue!! } override fun isNotEmpty(): Boolean { - return feedbacksCount.isNotEmpty() + return minset.isNotEmpty() } override fun size(): Int { - return feedbacksCount.size + return minset.size() } } - -class FirstSeedKeepingStatistics>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, -) : SingleSeedKeepingStatistics(totalRuns, startTime, missedTypes, random, configuration) { - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy() - ) -} - -class LastSeedKeepingStatistics>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, -) : SingleSeedKeepingStatistics(totalRuns, startTime, missedTypes, random, configuration) { - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy() - ) -} ///endregion ///region Minset +/** + * Possible answers given by [Minset] after putting a value in it with [Minset.put]: + * - [NEW_FEEDBACK]: new unique feedback was found; + * - [NEW_VALUE]: feedback is already in minset, and value stored for it was updated; + * - [NOTHING_NEW]: feedback is already in minset and the value was not updated either. + * + * According to current implementation, [Minset] figures out if feedback is new, + * but decision between [NEW_VALUE] and [NOTHING_NEW] is making by [ValueStorage]. + */ enum class MinsetEvent { NEW_FEEDBACK, NEW_VALUE, NOTHING_NEW } -abstract class Minset, STORAGE : ValueStorage> ( +/** + * Storage for feedbacks mapped on the values that led to them. + */ +open class Minset, STORAGE : ValueStorage> ( open val valueStorageGenerator: () -> STORAGE, val seeds: LinkedHashMap = linkedMapOf(), val count: LinkedHashMap = linkedMapOf() @@ -324,28 +303,36 @@ abstract class Minset, STORAGE : return seeds.isEmpty() } - fun getSize() : Int { + fun size() : Int { return seeds.size } } - -open class SingleValueMinset, STORAGE : SingleValueStorage> ( - override val valueStorageGenerator: () -> STORAGE, -) : Minset(valueStorageGenerator) ///endregion ///region Value storages + +/** + * Interface used for [ValueStorage]. + */ interface InfiniteIterator : Iterator { override operator fun next(): T override operator fun hasNext(): Boolean } +/** + * Interface to storage values mapped to feedbacks in [Minset]. + * Now only [SingleValueStorage] implements it, but there might be more complex logic. + */ interface ValueStorage : InfiniteIterator> { fun put(value: Node, feedback: Feedback) : Boolean } +/** + * Base implementation of [ValueStorage] that keeps only one (first or last, according to [strategy]) + * value for each single feedback. + */ open class SingleValueStorage ( - private val strategy : SingleValueSelectionStrategy + private val strategy : ValueStoragingStrategy ) : ValueStorage { private var storedValue: Node? = null @@ -353,11 +340,11 @@ open class SingleValueStorage ( val result = storedValue == null storedValue = when (strategy) { - SingleValueSelectionStrategy.FIRST -> storedValue ?: value - SingleValueSelectionStrategy.LAST -> value + ValueStoragingStrategy.FIRST -> storedValue ?: value + ValueStoragingStrategy.LAST -> value } - return result || (strategy == SingleValueSelectionStrategy.LAST) + return result || (strategy == ValueStoragingStrategy.LAST) } override fun next(): Node { @@ -373,24 +360,24 @@ open class SingleValueStorage ( } } -class MutationsCountingSingleValueStorage(strategy: SingleValueSelectionStrategy) : +/** + * [SingleValueStorage] implementation that counts mutations for each feedback stored. + * Can be used in the future for trying to reach rare feedbacks. + */ +class MutationsCountingSingleValueStorage(strategy: ValueStoragingStrategy) : SingleValueStorage(strategy) { private val knownValueMutationsCount: HashMap, Int> = hashMapOf() override fun put(value: Node, feedback: Feedback): Boolean { value.result.forEach { result -> - when(result) { + when (result) { is Result.Known<*, *, *> -> { - result.lastMutation.let { - it?.let { - knownValueMutationsCount[it] = knownValueMutationsCount.getOrDefault(it, 0) + 1 - } + result.lastMutation?.let { + knownValueMutationsCount[it] = knownValueMutationsCount.getOrDefault(it, 0) + 1 } } - else -> {} - } + else -> {}} } - return super.put(value, feedback) } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt index c66d96c79a..b264c44c70 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -26,6 +26,6 @@ open class StringValue( } override fun toString(): String { - return "\"" + value + "\"" + return "\"$value\"" } } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/resources/loganal.py b/utbot-fuzzing/src/main/resources/loganal.py deleted file mode 100644 index 6d653aeaad..0000000000 --- a/utbot-fuzzing/src/main/resources/loganal.py +++ /dev/null @@ -1,156 +0,0 @@ -import os -import sys -import re -from datetime import datetime -import argparse -import pandas as pd -from tqdm import tqdm -from matplotlib import pyplot as plt - - -plt.rcParams["figure.figsize"] = (16, 9) - - -def parse_java_log(file_path: str, operation): - with open(file_path, 'r') as log: - items = log.read().replace('\n', '').split(' | ')[:-1] - - c = 0 - for item in items: - match c: - case 0: time = datetime.strptime(item, '[%H:%M:%S:%f]') - case 1: value = item - case 2: - try: - class_name = item.split(': ')[1] - except: - print(item) - case 3: method = item.split(': ')[1] - case 4: line_number = item.split(': ')[1] - case 5: trace_hash = item.split(': ')[1] - case 6: trace_count = item.split(': ')[1] - case 7: - event = item - operation(time, value, class_name, method, line_number, trace_hash, trace_count, event) - c = -1 - c += 1 - pass - -def build_dataframe(logpath: str): - file_path = os.path.expanduser(logpath) - pattern = r'(? Date: Thu, 10 Aug 2023 11:56:05 +0300 Subject: [PATCH 15/25] Add known values mutation strategy based on probability tuning --- .gitignore | 1 + .../org.mockito.plugins.MockMaker | 1 + .../utbot/framework/plugin/api/CoverageApi.kt | 4 + .../org/utbot/engine/UtBotSymbolicEngine.kt | 18 +- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 267 +++++++++---- .../kotlin/org/utbot/fuzzing/Configuration.kt | 35 +- .../kotlin/org/utbot/fuzzing/Mutations.kt | 72 +++- .../kotlin/org/utbot/fuzzing/Providers.kt | 14 +- .../kotlin/org/utbot/fuzzing/Statistic.kt | 371 +++++++++++++++++- .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 11 +- .../org/utbot/fuzzing/demo/ForkFuzzing.kt | 12 +- .../org/utbot/fuzzing/demo/HttpRequest.kt | 6 +- .../org/utbot/fuzzing/demo/JavaFuzzing.kt | 13 +- .../org/utbot/fuzzing/demo/JsonFuzzing.kt | 2 +- .../org/utbot/fuzzing/seeds/StringValue.kt | 4 + .../kotlin/org/utbot/fuzzing/utils/Trie.kt | 10 +- .../org/utbot/fuzzing/FuzzerSmokeTest.kt | 26 +- .../kotlin/org/utbot/fuzzing/ProvidersTest.kt | 18 +- .../kotlin/org/utbot/fuzzer/FuzzedValue.kt | 6 +- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 38 +- .../org/utbot/fuzzing/JavaFuzzingTest.kt | 2 +- .../org/utbot/python/fuzzing/PythonApi.kt | 2 +- 22 files changed, 785 insertions(+), 148 deletions(-) create mode 100644 utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/.gitignore b/.gitignore index 5b11d761cb..25ab286e68 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target/ utbot-intellij/src/main/resources/settings.properties __pycache__ .dmypy.json +tbot-fuzzing/src/main/resources/* diff --git a/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 06c4c9350f..22c9d49518 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -17,6 +17,10 @@ data class Instruction( val id: Long ) { val className: String get() = internalName.replace('/', '.') + + override fun toString(): String { + return "class name: ${className.split(".").last()}; method: $methodSignature; line number: $lineNumber" + } } /** 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 cfc15a9579..c465885ade 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -465,13 +465,13 @@ class UtBotSymbolicEngine( if (controller.job?.isActive == false || diff <= thresholdMillisForFuzzingOperation) { logger.info { "Fuzzing overtime: $methodUnderTest" } logger.info { "Test created by fuzzer: $testEmittedByFuzzer" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } if (thisInstance?.model is UtNullModel) { // We should not try to run concretely any models with null-this. // But fuzzer does generate such values, because it can fail to generate any "good" values. - return@runJavaFuzzing BaseFeedback(Trie.emptyNode(), Control.PASS) + return@runJavaFuzzing JavaFeedback(Trie.emptyNode(), Control.PASS) } val stateBefore = concreteExecutionContext.createStateBefore( @@ -494,17 +494,17 @@ class UtBotSymbolicEngine( } // in case an exception occurred from the concrete execution - concreteExecutionResult ?: return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + concreteExecutionResult ?: return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) // in case of processed failure in the concrete execution concreteExecutionResult.processedFailure()?.let { failure -> logger.debug { "Instrumented process failed with exception ${failure.exception} before concrete execution started" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } if (concreteExecutionResult.violatesUtMockAssumption()) { logger.debug { "Generated test case by fuzzer violates the UtMock assumption: $concreteExecutionResult" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } val result = concreteExecutionResult.result @@ -521,16 +521,16 @@ class UtBotSymbolicEngine( coverageToMinStateBeforeSize[trieNode] = curStateBeforeSize else { if (++attempts >= attemptsLimit) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } - return@runJavaFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) + return@runJavaFuzzing JavaFeedback(result = trieNode, control = Control.CONTINUE) } } else { logger.error { "Coverage is empty for $methodUnderTest with $values" } if (result is UtSandboxFailure) { val stackTraceElements = result.exception.stackTrace.reversed() if (errorStackTraceTracker.add(stackTraceElements).count > 1) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } } } @@ -548,7 +548,7 @@ class UtBotSymbolicEngine( ) testEmittedByFuzzer++ - BaseFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) + JavaFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index a33cba2ea0..ee472504e9 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -5,10 +5,15 @@ import kotlinx.coroutines.* import mu.KotlinLogging import org.utbot.fuzzing.seeds.KnownValue import org.utbot.fuzzing.utils.MissedSeed -import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import org.utbot.fuzzing.utils.transformIfNotEmpty +import java.io.File +import java.io.FileOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.random.Random +import java.nio.charset.Charset + private val logger by lazy { KotlinLogging.logger {} } @@ -19,7 +24,7 @@ private val logger by lazy { KotlinLogging.logger {} } * @see [org.utbot.fuzzing.demo.JavaFuzzing] * @see [org.utbot.fuzzing.demo.JsonFuzzingKt] */ -interface Fuzzing, FEEDBACK : Feedback> { +interface Fuzzing, FEEDBACK : Feedback> { /** * Before producing seeds, this method is called to recognize, @@ -59,7 +64,7 @@ interface Fuzzing, FEEDBACK : Feed * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, StatisticImpl(statistics)) + fuzz(description, MainStatisticImpl(statistics)) } /** @@ -73,19 +78,129 @@ interface Fuzzing, FEEDBACK : Feed suspend fun afterIteration(description: DESCRIPTION, statistics: Statistic) { } } +///region Description /** * Some description of the current fuzzing run. Usually, it contains the name of the target method and its parameter list. */ -open class Description( +open class Description( parameters: List ) { val parameters: List = parameters.toList() - open fun clone(scope: Scope): Description { + open fun clone(scope: Scope): Description { error("Scope was changed for $this, but method clone is not specified") } + + open fun setUp() {} + + open fun updatePerIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} + + open fun finalizeReport(statistic: Statistic) {} +} + +abstract class ReportingDescription ( + parameters: List, + private val reporter: Reporter +) : Description(parameters) { + + override fun setUp() { + reporter.setUp(this) + } + + override fun updatePerIteration(values : Node, feedback : Feedback, event: MinsetEvent) { + reporter.update(values, feedback, event) + } + + override fun finalizeReport(statistic: Statistic) { + reporter.conclude(statistic) + } +} + +open class LoggingDescription ( + parameters: List, + path: String +) : ReportingDescription( + parameters, + LoggingReporter(path = path) +) + + +///region Reporter +abstract class Reporter( + private val reportPath : String, +) { + abstract fun clone() : Reporter + + private lateinit var description: Description + fun setUp(description: Description) { + this.description = description + } + abstract fun update(values : Node, feedback : Feedback, event : MinsetEvent, additionalMessage: String = "") + abstract fun conclude(statistic: Statistic) } + +class LoggingReporter( + val path: String +) : Reporter(path) { + + override fun clone() : LoggingReporter { + return LoggingReporter(path) + } + + private val actualPath = if (path.startsWith("~/")) { + System.getProperty("user.home") + path.drop(1) + } else { + path + } + + private val logFile = File("$actualPath/log") + private val overviewFile = File("$actualPath/overview.txt") + + init { + File(actualPath).mkdirs() + } + + override fun update(values: Node, feedback: Feedback, event : MinsetEvent, additionalMessage: String) { + val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) + + FileOutputStream(logFile, true).bufferedWriter().use { + + val printableValues = buildString { + values.toString() + .toByteArray(Charsets.UTF_8) + .forEach { + byte -> val hexString = byte.toInt().toString(16).padStart(2, '0') + append("\\u").append(hexString) + } + } + + it.write("[$time]\t$printableValues\t$feedback\t$event\t$additionalMessage\n") + } + } + + override fun conclude(statistic: Statistic) { + File(actualPath).mkdirs() + overviewFile.apply{ createNewFile() }.printWriter().use { out -> + out.println("Total runs: ${statistic.totalRuns}") + + if (statistic is SeedsMaintainingStatistic<*, *, *>) { + out.println("Final minset size: ${statistic.size()}") + } + + val seconds = statistic.elapsedTime / 1_000_000_000 + val minutes = (seconds % 3600) / 60 + val remainingSeconds = (seconds % 3600) % 60 + val formattedTime = String.format("%02d min %02d.%03d sec", minutes, remainingSeconds, statistic.elapsedTime % 1_000_000) + out.println("Elapsed time: $formattedTime") + } + } +} +///endregion +///endregion + + + class Scope( val parameterIndex: Int, val recursionDepth: Int, @@ -245,7 +360,29 @@ interface Feedback : AsKey { data class BaseFeedback( val result: VALUE, override val control: Control, -) : Feedback +) : Feedback { + override fun toString(): String { + return "$result" + } +} + +interface WeightedFeedback> : Feedback { + val weight: WEIGHT +} + +data class BaseWeightedFeedback>( + val result: VALUE, + override val weight: WEIGHT, + override val control: Control, +) : WeightedFeedback, Comparable> { + override fun compareTo(other: WeightedFeedback): Int { + return weight.compareTo(other.weight) + } + override fun toString(): String { + return "$result with weight $weight" + } +} + /** * Controls fuzzing execution. @@ -284,6 +421,10 @@ private object EmptyFeedback : Feedback { override fun hashCode(): Int { return 0 } + + override fun toString(): String { + return "EMPTY_FEEDBACK" + } } class NoSeedValueException internal constructor( @@ -298,12 +439,12 @@ class NoSeedValueException internal constructor( get() = "No seed candidates generated for type: $type" } -suspend fun , F : Feedback> Fuzzing.fuzz( +suspend fun , F : Feedback> Fuzzing.fuzz( description: D, random: Random = Random(0), configuration: Configuration = Configuration() ) { - fuzz(description, StatisticImpl(random = random, configuration = configuration)) + fuzz(description, MainStatisticImpl(random = random, configuration = configuration)) } /** @@ -311,9 +452,9 @@ suspend fun , F : Feedback> Fuzzing.f * * This is an entry point for every fuzzing. */ -private suspend fun , F : Feedback> Fuzzing.fuzz( +private suspend fun , F : Feedback> Fuzzing.fuzz( description: D, - statistic: StatisticImpl, + statistic: SeedsMaintainingStatistic, ) { val random = statistic.random val configuration = statistic.configuration @@ -330,18 +471,20 @@ private suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing, R>() val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } val feedback = fuzzing.handle(description, result) + + val minsetResponse = statistic.put(random, configuration, feedback, values) + when (feedback.control) { Control.CONTINUE -> { - statistic.put(random, configuration, feedback, values) + description.updatePerIteration(values, feedback, minsetResponse) + description.finalizeReport(statistic) } Control.STOP -> { + description.finalizeReport(statistic) break } - Control.PASS -> {} + Control.PASS -> { + description.finalizeReport(statistic) + } } } } - - ///region Implementation of the fuzzing and non-public functions. -private fun , FEEDBACK : Feedback> fuzz( +private fun , FEEDBACK : Feedback> fuzz( parameters: List, fuzzing: Fuzzing, description: DESCRIPTION, @@ -399,7 +547,7 @@ private fun , FEEDBACK : Feedback< return Node(result, parameters, builder) } -private fun , FEEDBACK : Feedback> produce( +private fun , FEEDBACK : Feedback> produce( type: TYPE, fuzzing: Fuzzing, description: DESCRIPTION, @@ -436,7 +584,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Collection] type. When `configuration.recursionTreeDepth` limit is reached it creates * an empty collection and doesn't do any modification to it. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Collection, fuzzing: Fuzzing, description: DESCRIPTION, @@ -496,7 +644,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Recursive] type. When `configuration.recursionTreeDepth` limit is reached it calls * `Seed.Recursive#empty` routine to create an empty object. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Recursive, fuzzing: Fuzzing, description: DESCRIPTION, @@ -652,13 +800,18 @@ sealed interface Result { /** * Known value. */ - class Known>(val value: V, val build: (V) -> RESULT) : Result + class Known>(val value: V, val build: (V) -> RESULT, val lastMutation: Mutation? = null) : Result { + override fun toString(): String { + return this.value.toString() + } + } /** * A tree of object that has constructor and some modifications. */ class Recursive( val construct: Node, val modify: List>, + val lastMutation: RecursiveMutations? = null, ) : Result /** @@ -668,6 +821,7 @@ sealed interface Result { val construct: Node, val modify: List>, val iterations: Int, + val lastMutation: CollectionMutations? = null, ) : Result /** @@ -684,52 +838,33 @@ sealed interface Result { class Node( val result: List>, val parameters: List, - val builder: Routine, -) - -private class StatisticImpl>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, -) : Statistic { - - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - ) - - override val elapsedTime: Long - get() = System.nanoTime() - startTime - private val seeds = linkedMapOf>() - private val count = linkedMapOf() - - fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { - if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { - seeds[feedback] = seed - } else { - seeds.putIfAbsent(feedback, seed) - } - count[feedback] = count.getOrDefault(feedback, 0L) + 1L - } - - fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") - val entries = seeds.entries.toList() - val frequencies = DoubleArray(seeds.size).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) + val builder: Routine +) { + override fun toString() : String { + return result.map { + when(it) { + is Result.Empty -> "_" + is Result.Simple -> it.result + is Result.Known<*, *, *> -> { + if (it.lastMutation != null) { + "${it.value} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.value.toString() } + } + is Result.Collection -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "[", "]")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.modify.joinToString(", ", "[", "]") } + } + is Result.Recursive -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "{", "}")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { + it.modify.joinToString(", ", "{", "}") + } + } } - } - val index = random.chooseOne(frequencies) - return entries[index].value + }.joinToString(", ") } - - fun isNotEmpty() = seeds.isNotEmpty() } ///endregion diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index a6c19926c9..6d3fd81236 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -7,10 +7,27 @@ import kotlin.math.pow */ data class Configuration( + /** + * Configuration used by minset to accumulate and use statistics. + * + * TODO: This part of configuration tend to be changing through runtime tuning or evolutionary algorithm. + */ + var minsetConfiguration: MinsetConfiguration = MinsetConfiguration(), + + /** + * Number of continuous iterations for each value. + */ + var runsPerValue: Long = 100, + + /** + * Number of iterations before mutations probabilities correction. + */ + var investigationPeriodPerValue: Int = 50, + /** * Choose between already generated values and new generation of values. */ - var probSeedRetrievingInsteadGenerating: Int = 70, + var probSeedRetrievingInsteadGenerating: Int = 75, /** * Choose between generation and mutation. @@ -95,4 +112,18 @@ data class Configuration( * Limits maximum number of recursive seed modifications */ var maxNumberOfRecursiveSeedModifications: Int = 10, -) \ No newline at end of file +) + +/** + * Configuration used by minset to accumulate and use statistics. + */ +data class MinsetConfiguration ( + var obsolescenceMultiplier: Double = 1.0, + var rewardMultiplier: Double = 1.0, + var rewardWeight: Double = 1.0, + var penaltyMultiplier: Double = 1.0, + var penaltyWeight: Double = 0.0, + val valueStoragingStrategy: ValueStoragingStrategy = ValueStoragingStrategy.LAST +) + +enum class ValueStoragingStrategy { FIRST, LAST } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 9868be39b8..fe8358e0a7 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -7,20 +7,52 @@ import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random -class MutationFactory { +fun Map, Double>.getRandomMutation(random: Random): Mutation<*>? { + val sum = values.sum() + + if (sum > 0) { + var value = random.nextDouble(sum) + for ((mutation, weight) in entries) { + value -= weight + if (value <= 0) { + return mutation + } + } + } + + return null +} - fun mutate(node: Node, random: Random, configuration: Configuration): Node { +class MutationFactory { + fun mutate( + node: Node, + random: Random, + configuration: Configuration, + statistic: SeedsMaintainingStatistic + ): Node { if (node.result.isEmpty()) return node val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) val recursive: NodeMutation = NodeMutation { n, r, c -> - mutate(n, r, c) + mutate(n, r, c, statistic) } val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { - is Result.Simple -> Result.Simple(resultToMutate.mutation(resultToMutate.result, random), resultToMutate.mutation) + is Result.Simple -> Result.Simple( + resultToMutate.mutation(resultToMutate.result, random), + resultToMutate.mutation + ) is Result.Known -> { val mutations = resultToMutate.value.mutations() + + val mutation = if ( + configuration.investigationPeriodPerValue > 0 && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue + ) { + statistic.getMutationsEfficiencies().getRandomMutation(random) ?: mutations.random(random) + } else { + mutations.random(random) + } + if (mutations.isNotEmpty()) { - resultToMutate.mutate(mutations.random(random), random, configuration) + resultToMutate.mutate(mutation as Mutation>, random, configuration) } else { resultToMutate } @@ -29,16 +61,18 @@ class MutationFactory { when { resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> RecursiveMutations.Constructor() + random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> RecursiveMutations.ShuffleAndCutModifications() - else -> - RecursiveMutations.Mutate() + + else -> RecursiveMutations.Mutate() }.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { when { random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> CollectionMutations.Mutate() + else -> CollectionMutations.Shuffle() }.mutate(resultToMutate, recursive, random, configuration) @@ -82,12 +116,17 @@ class MutationFactory { } @Suppress("UNCHECKED_CAST") - private fun > Result.Known.mutate(mutation: Mutation, random: Random, configuration: Configuration): Result.Known { + private fun > Result.Known.mutate( + mutation: Mutation, + random: Random, + configuration: Configuration + ): Result.Known { val source: T = value as T val mutate = mutation.mutate(source, random, configuration) return Result.Known( mutate, - build as (T) -> RESULT + build as (T) -> RESULT, + lastMutation = mutation ) } } @@ -244,7 +283,8 @@ sealed interface CollectionMutations : Mutation : Mutation : Mutation { return Result.Recursive( construct = recursive.mutate(source.construct,random, configuration), - modify = source.modify + modify = source.modify, + lastMutation = this, ) } } @@ -309,7 +351,8 @@ sealed interface RecursiveMutations : Mutation { return Result.Recursive( construct = source.construct, - modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)) + modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)), + lastMutation = this, ) } } @@ -326,7 +369,8 @@ sealed interface RecursiveMutations : Mutation, FEEDBACK : Feedback> runFuzzing( +suspend fun , FEEDBACK : Feedback> runFuzzing( provider: ValueProvider, description: DESCRIPTION, random: Random = Random(0), @@ -24,7 +24,7 @@ suspend fun , FEEDBACK : Feedback< * @param providers is a list of "type to values" generator * @param exec this function is called when fuzzer generates values of type R to run it with target program. */ -class BaseFuzzing, F : Feedback>( +class BaseFuzzing, F : Feedback>( val providers: List>, val exec: suspend (description: D, values: List) -> F ) : Fuzzing { @@ -60,7 +60,7 @@ class BaseFuzzing, F : Feedback>( /** * Value provider generates [Seed] and has other methods to combine providers. */ -fun interface ValueProvider> { +fun interface ValueProvider> { fun enrich(description: D, type: T, scope: Scope) {} @@ -144,7 +144,7 @@ fun interface ValueProvider> { return if (this is Fallback) { provider.unwrapIfFallback() } else { this } } - private class Fallback>( + private class Fallback>( val provider: ValueProvider, val fallback: ValueProvider, ): ValueProvider { @@ -172,7 +172,7 @@ fun interface ValueProvider> { /** * Wrapper class that delegates implementation to the [providers]. */ - private class Combined>(providers: List>): ValueProvider { + private class Combined>(providers: List>): ValueProvider { val providers: List> init { @@ -203,7 +203,7 @@ fun interface ValueProvider> { } companion object { - fun > of(valueProviders: List>): ValueProvider { + fun > of(valueProviders: List>): ValueProvider { return Combined(valueProviders) } } @@ -215,7 +215,7 @@ fun interface ValueProvider> { * @param type that is used as a filter to call this provider * @param generate yields values for the type */ -class TypeProvider>( +class TypeProvider>( val type: T, val generate: suspend SequenceScope>.(description: D, type: T) -> Unit ) : ValueProvider { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 877937cedb..2915214947 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -1,6 +1,7 @@ package org.utbot.fuzzing import org.utbot.fuzzing.utils.MissedSeed +import org.utbot.fuzzing.utils.chooseOne import kotlin.random.Random /** @@ -13,4 +14,372 @@ interface Statistic { val missedTypes: MissedSeed val random: Random val configuration: Configuration -} \ No newline at end of file +} + +///region Statistic Implementations + +/** + * Interface of statistic object with seeds maintaining logic (seed selection, value storaging, feedbacks counting, etc.) + */ +interface SeedsMaintainingStatistic>: Statistic { + override var totalRuns: Long + fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent + fun getSeed(random: Random, configuration: Configuration): Node + fun getMutationsEfficiencies(): Map, Double> { return emptyMap() } + fun isNotEmpty() : Boolean + fun size() : Int +} + + +/** + * Statistic implementation used for experiments with single seed. + * Keeps [Minset] with all feedbacks but [getSeed] returns only one seed, saved from last run. + */ +open class SingleSeedKeepingStatistic> ( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration +): SeedsMaintainingStatistic { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) + + override val elapsedTime: Long + get() = System.nanoTime() - startTime + + private val storedSeed = SingleValueStorage(configuration.minsetConfiguration.valueStoragingStrategy) + private val feedbacksCounts: LinkedHashMap = linkedMapOf() + + private val mutationsCounts = mutableMapOf, Long>() + private val mutationsSuccessCounts = mutableMapOf, Double>() + private val mutationsEfficiencies = mutableMapOf, Double>() + + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { + storedSeed.put(seed, feedback) + feedbacksCounts.merge(feedback, 1L, Long::plus) + + val event = when { + feedbacksCounts[feedback] == 1L -> MinsetEvent.NEW_FEEDBACK + configuration.minsetConfiguration.valueStoragingStrategy == ValueStoragingStrategy.LAST -> MinsetEvent.NOTHING_NEW + else -> MinsetEvent.NOTHING_NEW + } + + seed.result.forEach { result -> + when(result) { + is Result.Known -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + + with(mutationsEfficiencies) { + forEach { (key, value) -> + this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier + } + this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + } + + mutationsSuccessCounts[mutation] = when (event) { + MinsetEvent.NEW_FEEDBACK -> { + if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight + } else { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 + } + } + else -> mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + } + } + } + else -> {} + } + } + + return event + } + + override fun getSeed(random: Random, configuration: Configuration): Node { + return storedSeed.next() + } + + override fun getMutationsEfficiencies(): Map, Double> { + return mutationsEfficiencies + } + + override fun isNotEmpty(): Boolean { + return feedbacksCounts.isNotEmpty() + } + + override fun size(): Int { + return feedbacksCounts.size + } +} + +/** + * Main implementation of [SeedsMaintainingStatistic]. Implements + * mutations efficiencies evaluating algorithm used for mutation probability tuning (see [MainStatisticImpl.put]) + * and seed selection algorithm based on feedbacks counting (see [MainStatisticImpl.getSeed]). + */ +open class MainStatisticImpl> ( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration +): SeedsMaintainingStatistic { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) + + override val elapsedTime: Long + get() = System.nanoTime() - startTime + + private val minset: Minset> = Minset( + { SingleValueStorage(configuration.minsetConfiguration.valueStoragingStrategy) } + ) + + private var currentValue: Node? = null + + private val mutationsCounts = mutableMapOf, Long>() + private val mutationsSuccessCounts = mutableMapOf, Double>() + private val mutationsEfficiencies = mutableMapOf, Double>() + + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { + val event = minset.put(seed, feedback) + + seed.result.forEach { result -> + when (result) { + is Result.Known -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + + with(mutationsEfficiencies) { + forEach { (key, value) -> + this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier + } + this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + } + + mutationsSuccessCounts[mutation] = when (event) { + MinsetEvent.NEW_FEEDBACK -> { + if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight + } else { + mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 + } + } + + else -> mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + } + } + } + + else -> {} + } + } + + return event + } + + + override fun getSeed(random: Random, configuration: Configuration): Node { + if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") + + currentValue?.result?.forEach { + when (it) { + is Result.Known -> { + if (this.totalRuns % configuration.runsPerValue == 1L) { + + // the law of Demeter is violated because seed selection and power schedule will be significantly reworked + val entries = minset.seeds.entries.toList() + val frequencies = DoubleArray(minset.size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) + } + } + + currentValue = entries[random.chooseOne(frequencies)].value.next() + + mutationsCounts.clear() + mutationsSuccessCounts.clear() + mutationsEfficiencies.clear() + } + } + else -> {} + } + } + + // This variable is used for dirty way to turn off sequential running on once chosen value for debugging. + // Breakpoint below evaluates "enabled = false", so currentValue is chosen again independently + val enabled = true + + if (currentValue == null || !enabled) { + // the law of Demeter is violated because seed selection and power schedule will be significantly reworked + val entries = minset.seeds.entries.toList() + val frequencies = DoubleArray(minset.size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) + } + } + + currentValue = entries[random.chooseOne(frequencies)].value.next() + } + + return currentValue!! + } + + override fun isNotEmpty(): Boolean { + return minset.isNotEmpty() + } + + override fun size(): Int { + return minset.size() + } +} +///endregion + +///region Minset + +/** + * Possible answers given by [Minset] after putting a value in it with [Minset.put]: + * - [NEW_FEEDBACK]: new unique feedback was found; + * - [NEW_VALUE]: feedback is already in minset, and value stored for it was updated; + * - [NOTHING_NEW]: feedback is already in minset and the value was not updated either. + * + * According to current implementation, [Minset] figures out if feedback is new, + * but decision between [NEW_VALUE] and [NOTHING_NEW] is making by [ValueStorage]. + */ +enum class MinsetEvent { NEW_FEEDBACK, NEW_VALUE, NOTHING_NEW } + +/** + * Storage for feedbacks mapped on the values that led to them. + */ +open class Minset, STORAGE : ValueStorage> ( + open val valueStorageGenerator: () -> STORAGE, + val seeds: LinkedHashMap = linkedMapOf(), + val count: LinkedHashMap = linkedMapOf() +) { + operator fun get(feedback: FEEDBACK): STORAGE? { + return seeds[feedback] + } + + open fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { + val result: MinsetEvent + + if (seeds.containsKey(feedback)) { + result = if( seeds[feedback]!!.put(value, feedback) ) { MinsetEvent.NEW_VALUE } else { MinsetEvent.NOTHING_NEW } + } else { + result = MinsetEvent.NEW_FEEDBACK + seeds[feedback] = valueStorageGenerator.invoke() + seeds[feedback]!!.put(value, feedback) + } + + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + + return result + } + + fun isNotEmpty(): Boolean { + return seeds.isNotEmpty() + } + + fun isEmpty(): Boolean { + return seeds.isEmpty() + } + + fun size() : Int { + return seeds.size + } +} +///endregion + +///region Value storages + +/** + * Interface used for [ValueStorage]. + */ +interface InfiniteIterator : Iterator { + override operator fun next(): T + override operator fun hasNext(): Boolean +} + +/** + * Interface to storage values mapped to feedbacks in [Minset]. + * Now only [SingleValueStorage] implements it, but there might be more complex logic. + */ +interface ValueStorage : InfiniteIterator> { + fun put(value: Node, feedback: Feedback) : Boolean +} + +/** + * Base implementation of [ValueStorage] that keeps only one (first or last, according to [strategy]) + * value for each single feedback. + */ +open class SingleValueStorage ( + private val strategy : ValueStoragingStrategy +) : ValueStorage { + + private var storedValue: Node? = null + override fun put(value: Node, feedback: Feedback) : Boolean { + val result = storedValue == null + + storedValue = when (strategy) { + ValueStoragingStrategy.FIRST -> storedValue ?: value + ValueStoragingStrategy.LAST -> value + } + + return result || (strategy == ValueStoragingStrategy.LAST) + } + + override fun next(): Node { + if (storedValue == null) { + error("Next value requested but no value stored") + } else { + return storedValue as Node + } + } + + override fun hasNext(): Boolean { + return storedValue != null + } +} + +/** + * [SingleValueStorage] implementation that counts mutations for each feedback stored. + * Can be used in the future for trying to reach rare feedbacks. + */ +class MutationsCountingSingleValueStorage(strategy: ValueStoragingStrategy) : + SingleValueStorage(strategy) { + private val knownValueMutationsCount: HashMap, Int> = hashMapOf() + + override fun put(value: Node, feedback: Feedback): Boolean { + value.result.forEach { result -> + when (result) { + is Result.Known<*, *, *> -> { + result.lastMutation?.let { + knownValueMutationsCount[it] = knownValueMutationsCount.getOrDefault(it, 0) + 1 + } + } + else -> {}} + } + return super.put(value, feedback) + } +} + +///endregion \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index 4969ccf7f4..9ab6d90b0d 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -30,14 +30,14 @@ private const val searchString = suspend fun main() { // Define fuzzing description to start searching. - object : Fuzzing, BaseFeedback> { + object : Fuzzing, BaseWeightedFeedback> { /** * Generate method returns several samples or seeds which are used as a base for fuzzing. * * In this particular case only 1 value is provided which is an empty string. Also, a mutation * is defined for any string value. This mutation adds a random character from ASCII table. */ - override fun generate(description: Description, type: Unit) = sequenceOf>( + override fun generate(description: Description, type: Unit) = sequenceOf>( Seed.Simple("") { s, r -> s + Char(r.nextInt(1, 256)) } ) @@ -47,17 +47,18 @@ suspend fun main() { * This implementation just calls the target function and returns a result. After it returns an empty feedback. * If some returned value equals to the length of the source string then feedback returns 'stop' signal. */ - override suspend fun handle(description: Description, values: List): BaseFeedback { + override suspend fun handle(description: Description, values: List): BaseWeightedFeedback { check(values.size == 1) { "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" } val input = values.first() val result = searchString.findMaxSubstring(input) println("findMaxSubstring(\"$input\") = $result") - return BaseFeedback( + return BaseWeightedFeedback( result = result, + weight = result, control = if (result == searchString.length) Control.STOP else Control.CONTINUE ) } - }.fuzz(Description(listOf(Unit))) + }.fuzz(LoggingDescription(listOf(Unit), "~/.utbot/AbcFuzzing")) } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt index 99be17e156..ead8debe12 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt @@ -11,15 +11,15 @@ private enum class Type { fun main(): Unit = runBlocking { launch { - object : Fuzzing, Feedback> { + object : Fuzzing, Feedback> { private val runs = mutableMapOf() - override fun generate(description: Description, type: Type): Sequence> { + override fun generate(description: Description, type: Type): Sequence> { return sequenceOf(Seed.Simple(type.name)) } - override suspend fun handle(description: Description, values: List): Feedback { + override suspend fun handle(description: Description, values: List): Feedback { description.parameters.forEach { runs[it]!!.incrementAndGet() } @@ -28,7 +28,7 @@ fun main(): Unit = runBlocking { } override suspend fun afterIteration( - description: Description, + description: Description, stats: Statistic, ) { if (stats.totalRuns % 10 == 0L && description.parameters.size == 1) { @@ -38,7 +38,7 @@ fun main(): Unit = runBlocking { Type.MORE_CONCRETE -> listOf() } if (newTypes.isNotEmpty()) { - val d = Description(newTypes) + val d = Description(newTypes) fork(d, stats) // Description can be used as a transfer object, // that collects information about the current running. @@ -47,7 +47,7 @@ fun main(): Unit = runBlocking { } } - override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { + override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { println("info: ${description.parameters} runs ${stats.totalRuns}") return description.parameters.all { runs.computeIfAbsent(it) { AtomicLong(0) }.get() >= 10 } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt index 844336aad1..cfe9af33c5 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt @@ -7,8 +7,8 @@ import java.util.concurrent.TimeUnit fun main() = runBlocking { withTimeout(TimeUnit.SECONDS.toMillis(10)) { - object : Fuzzing, Feedback> { - override fun generate(description: Description, type: String) = sequence> { + object : Fuzzing, Feedback> { + override fun generate(description: Description, type: String) = sequence> { when (type) { "url" -> yield(Seed.Recursive( construct = Routine.Create( @@ -49,7 +49,7 @@ fun main() = runBlocking { } override suspend fun handle( - description: Description, + description: Description, values: List ): Feedback { println(values[0]) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt index 0d3d490dd7..dad1e219c6 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt @@ -10,8 +10,8 @@ import org.utbot.fuzzing.seeds.StringValue * This example implements some basics for Java fuzzing that supports only a few types: * integers, strings, primitive arrays and user class [A]. */ -object JavaFuzzing : Fuzzing, Any?, Description>, Feedback, Any?>> { - override fun generate(description: Description>, type: Class<*>) = sequence, Any?>> { +object JavaFuzzing : Fuzzing, Any?, LoggingDescription, Any?>, Feedback, Any?>> { + override fun generate(description: LoggingDescription, Any?>, type: Class<*>) = sequence, Any?>> { if (type == Boolean::class.javaPrimitiveType) { yield(Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) yield(Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) @@ -71,7 +71,7 @@ object JavaFuzzing : Fuzzing, Any?, Description>, Feedback>, values: List): Feedback, Any?> { + override suspend fun handle(description: LoggingDescription, Any?>, values: List): Feedback, Any?> { println(values.joinToString { when (it) { is BooleanArray -> it.contentToString() @@ -88,7 +88,12 @@ object JavaFuzzing : Fuzzing, Any?, Description>, Feedback() - BaseFuzzing, Feedback>( + BaseFuzzing, Feedback>( TypeProvider(CustomType.INT) { _, _ -> for (b in Signed.values()) { yield(Seed.Known(BitVectorValue(3, b)) { bvv -> diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt index cd054754b8..b264c44c70 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -24,4 +24,8 @@ open class StringValue( StringMutations.ShuffleCharacters, ) } + + override fun toString(): String { + return "\"$value\"" + } } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index 7bc4190f64..4e9928d034 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -168,13 +168,21 @@ open class Trie( val parent: NodeImpl?, override var count: Int = 0, val children: MutableMap> = HashMap(), - ) : Node + ) : Node { + override fun toString(): String { + return "$data" + } + } private object EmptyNode : Node { override val data: Any get() = error("empty node has no data") override val count: Int get() = 0 + + override fun toString(): String { + return "EMPTY" + } } companion object { diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt index 260b824942..bb82022ae9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt @@ -18,7 +18,7 @@ class FuzzerSmokeTest { fun `fuzzing runs with empty parameters`() { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( { _, _ -> sequenceOf() }, Description(emptyList()) ) { _, _ -> @@ -34,7 +34,7 @@ class FuzzerSmokeTest { assertThrows { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( provider = { _, _ -> sequenceOf() }, description = Description(listOf(Unit)), configuration = Configuration( @@ -55,7 +55,7 @@ class FuzzerSmokeTest { var count = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> count += 1 BaseFeedback(Unit, Control.STOP) @@ -71,7 +71,7 @@ class FuzzerSmokeTest { var executions = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> executions++ BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) @@ -91,7 +91,7 @@ class FuzzerSmokeTest { generations++ sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) } @@ -108,7 +108,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> throw SpecialException() } @@ -128,7 +128,7 @@ class FuzzerSmokeTest { var depth = 0 var count = 0 runFuzzing( - ValueProvider, Node?, Description>> { _, _ -> + { _, _ -> sequenceOf(Seed.Recursive( construct = Routine.Create(listOf(Node::class, Node::class)) { v -> Node(v[0], v[1]) @@ -162,7 +162,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10 000 ms") @@ -181,7 +181,7 @@ class FuzzerSmokeTest { val start = System.currentTimeMillis() runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -197,7 +197,7 @@ class FuzzerSmokeTest { @Test fun `fuzzer generate same result when random is seeded`() { data class B(var a: Int) - val provider = ValueProvider> { _, _ -> + val provider = ValueProvider> { _, _ -> sequenceOf( Seed.Simple(B(0)) { p, r -> B(p.a + r.nextInt()) }, Seed.Known(BitVectorValue(32, Signed.POSITIVE)) { B(it.toInt()) }, @@ -245,7 +245,7 @@ class FuzzerSmokeTest { flow { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -273,7 +273,7 @@ class FuzzerSmokeTest { modify = sequenceOf(Routine.Call(emptyList()) { s, _ -> s.append("1") }), empty = Routine.Empty { fail("Empty is called despite construct requiring no args") } )) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, (s) -> if (s.isEmpty()) { seenEmpty = true @@ -299,7 +299,7 @@ class FuzzerSmokeTest { modify = emptySequence(), empty = Routine.Empty { null } )}.asSequence() }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> seenAnything = true BaseFeedback(Unit, Control.STOP) diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt index 74b2d8c204..29965929c9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt @@ -5,23 +5,23 @@ import org.junit.jupiter.api.Test class ProvidersTest { - private fun p(supplier: () -> T) : ValueProvider> { + private fun p(supplier: () -> T) : ValueProvider> { return ValueProvider { _, _ -> sequenceOf(Seed.Simple(supplier())) } } private fun p( accept: () -> Boolean, supplier: () -> T - ) : ValueProvider> { - return object : ValueProvider> { + ) : ValueProvider> { + return object : ValueProvider> { override fun accept(type: Unit) = accept() - override fun generate(description: Description, type: Unit) = sequence> { + override fun generate(description: Description, type: Unit) = sequence> { yield(Seed.Simple(supplier())) } } } - private val description = Description(listOf(Unit)) + private val description = Description(listOf(Unit)) @Test fun `test common provider API`() { @@ -170,9 +170,9 @@ class ProvidersTest { val seq = ValueProvider.of(listOf( p({ false }, { 1 }), p({ false }, { 2 }), p({ false }, { 3 }), )).withFallback( - object : ValueProvider> { + object : ValueProvider> { override fun accept(type: Unit) = false - override fun generate(description: Description, type: Unit) = emptySequence>() + override fun generate(description: Description, type: Unit) = emptySequence>() } ).generate(description, Unit) Assertions.assertEquals(0, seq.count()) @@ -180,12 +180,12 @@ class ProvidersTest { @Test fun `type providers check exactly the type`() { - val seq1 = TypeProvider>('A') { _, _ -> + val seq1 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'A') Assertions.assertEquals(1, seq1.count()) - val seq2 = TypeProvider>('A') { _, _ -> + val seq2 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'B') Assertions.assertEquals(0, seq2.count()) diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt index d5d4a6d7f2..fae534654c 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -19,4 +19,8 @@ import org.utbot.framework.plugin.api.UtModel open class FuzzedValue( val model: UtModel, var summary: String? = null, -) \ No newline at end of file +) { + override fun toString(): String { + return "FuzzedValue: $summary" + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 38565368e9..e8ed3b6550 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -17,16 +17,19 @@ private val logger = KotlinLogging.logger {} typealias JavaValueProvider = ValueProvider + + class FuzzedDescription( val description: FuzzedMethodDescription, val tracer: Trie, val typeCache: MutableMap, val random: Random, - val scope: Scope? = null -) : Description( + val scope: Scope? = null, +) : LoggingDescription( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) - } + }, + "~/.utbot/JavaFuzzing" ) { val constants: Sequence get() = description.concreteValues.asSequence() @@ -36,6 +39,7 @@ class FuzzedDescription( } } + fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( BooleanValueProvider, IntegerValueProvider, @@ -53,13 +57,39 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis NullValueProvider, ) + +class JavaFeedback( + val result: Trie.Node, + override val control: Control, + ) : Feedback { + override fun equals(other: Any?): Boolean { + if (other is JavaFeedback) { + return this.result == other.result && this.control == other.control + } + return false + } + + override fun hashCode(): Int { + return result.hashCode() * 31 + control.hashCode() + } + + override fun toString(): String { + if (result.count == 0) { + return "$result | FAIL | trace hash: ${result.hashCode()} | trace count: ${String.format("%4s", result.count)}" + } + return "$result | trace hash: ${result.hashCode()} | trace count: ${String.format("%4s", result.count)}" + } +} + + + suspend fun runJavaFuzzing( idGenerator: IdentityPreservingIdGenerator, methodUnderTest: ExecutableId, constants: Collection, names: List, providers: List> = defaultValueProviders(idGenerator), - exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> + exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> JavaFeedback ) { val random = Random(0) val classUnderTest = methodUnderTest.classId diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt index 360d706356..5a23bb1ed0 100644 --- a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt @@ -231,7 +231,7 @@ class JavaFuzzingTest { } } -class MarkerValueProvider>( +class MarkerValueProvider>( val name: String ) : ValueProvider { var enrich: Int = 0 diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt index e3b7b1931c..8f67d030ca 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt @@ -29,7 +29,7 @@ class PythonMethodDescription( val pythonTypeStorage: PythonTypeStorage, val tracer: Trie, val random: Random, -) : Description(parameters) +) : Description(parameters) sealed interface FuzzingExecutionFeedback class ValidExecution(val utFuzzedExecution: PythonUtExecution): FuzzingExecutionFeedback From 6c7de7381d334545eab8d3928063c1603bb8b91e Mon Sep 17 00:00:00 2001 From: grigory Date: Fri, 11 Aug 2023 19:06:35 +0300 Subject: [PATCH 16/25] Fix --- .../utbot/framework/plugin/api/CoverageApi.kt | 2 +- .../kotlin/org/utbot/fuzzing/Configuration.kt | 4 +- .../kotlin/org/utbot/fuzzing/Mutations.kt | 33 ++++-------- .../kotlin/org/utbot/fuzzing/Statistic.kt | 54 +++++++++---------- .../org/utbot/fuzzing/seeds/StringValue.kt | 2 +- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 30 ++++------- 6 files changed, 51 insertions(+), 74 deletions(-) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 22c9d49518..20190000bf 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -19,7 +19,7 @@ data class Instruction( val className: String get() = internalName.replace('/', '.') override fun toString(): String { - return "class name: ${className.split(".").last()}; method: $methodSignature; line number: $lineNumber" + return "class: $className; method: $methodSignature; line number: $lineNumber" } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index 6d3fd81236..58a26e9133 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -17,12 +17,12 @@ data class Configuration( /** * Number of continuous iterations for each value. */ - var runsPerValue: Long = 100, + var runsPerValue: Long = 250, /** * Number of iterations before mutations probabilities correction. */ - var investigationPeriodPerValue: Int = 50, + var investigationPeriodPerValue: Int = 100, /** * Choose between already generated values and new generation of values. diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index fe8358e0a7..ac04ff6aef 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -7,21 +7,6 @@ import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random -fun Map, Double>.getRandomMutation(random: Random): Mutation<*>? { - val sum = values.sum() - - if (sum > 0) { - var value = random.nextDouble(sum) - for ((mutation, weight) in entries) { - value -= weight - if (value <= 0) { - return mutation - } - } - } - - return null -} class MutationFactory { fun mutate( @@ -43,16 +28,20 @@ class MutationFactory { is Result.Known -> { val mutations = resultToMutate.value.mutations() + val mutationsEffieciencies = statistic.getMutationsEfficiencies() + val mutation = if ( - configuration.investigationPeriodPerValue > 0 && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue - ) { - statistic.getMutationsEfficiencies().getRandomMutation(random) ?: mutations.random(random) - } else { - mutations.random(random) - } + configuration.investigationPeriodPerValue > 0 && + statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && + mutationsEffieciencies.values.sum() >= 0 + ) { + mutations[random.chooseOne(mutationsEffieciencies.values.toDoubleArray())] + } else { + mutations.random(random) + } if (mutations.isNotEmpty()) { - resultToMutate.mutate(mutation as Mutation>, random, configuration) + resultToMutate.mutate(mutation, random, configuration) } else { resultToMutate } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 2915214947..b8ca067361 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -25,9 +25,9 @@ interface SeedsMaintainingStatistic) : MinsetEvent fun getSeed(random: Random, configuration: Configuration): Node - fun getMutationsEfficiencies(): Map, Double> { return emptyMap() } - fun isNotEmpty() : Boolean + fun getMutationsEfficiencies(): Map, Double> fun size() : Int + fun isNotEmpty() : Boolean { return size() > 0 } } @@ -58,7 +58,6 @@ open class SingleSeedKeepingStatistic, Long>() private val mutationsSuccessCounts = mutableMapOf, Double>() - private val mutationsEfficiencies = mutableMapOf, Double>() override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { @@ -75,24 +74,28 @@ open class SingleSeedKeepingStatistic -> { result.lastMutation?.let { mutation -> - mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - - with(mutationsEfficiencies) { - forEach { (key, value) -> - this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier - } - this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + mutationsSuccessCounts.forEach{ (key, value) -> + mutationsSuccessCounts[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier } + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = when (event) { MinsetEvent.NEW_FEEDBACK -> { if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { - mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight } else { mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 } } - else -> mutationsSuccessCounts.getOrDefault(mutation, 0.0) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + + else -> mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight } } } @@ -108,11 +111,9 @@ open class SingleSeedKeepingStatistic, Double> { - return mutationsEfficiencies - } - - override fun isNotEmpty(): Boolean { - return feedbacksCounts.isNotEmpty() + return mutationsCounts.mapValues { (key, value) -> + (mutationsSuccessCounts[key] ?: 0.0) / value + } } override fun size(): Int { @@ -151,7 +152,6 @@ open class MainStatisticImpl> ( private val mutationsCounts = mutableMapOf, Long>() private val mutationsSuccessCounts = mutableMapOf, Double>() - private val mutationsEfficiencies = mutableMapOf, Double>() override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { @@ -161,15 +161,12 @@ open class MainStatisticImpl> ( when (result) { is Result.Known -> { result.lastMutation?.let { mutation -> - mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - - with(mutationsEfficiencies) { - forEach { (key, value) -> - this[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier - } - this[mutation] = getOrDefault(mutation, 0.0) / mutationsCounts[mutation]!! + mutationsSuccessCounts.forEach{ (key, value) -> + mutationsSuccessCounts[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier } + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = when (event) { MinsetEvent.NEW_FEEDBACK -> { if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { @@ -218,7 +215,6 @@ open class MainStatisticImpl> ( mutationsCounts.clear() mutationsSuccessCounts.clear() - mutationsEfficiencies.clear() } } else -> {} @@ -244,8 +240,10 @@ open class MainStatisticImpl> ( return currentValue!! } - override fun isNotEmpty(): Boolean { - return minset.isNotEmpty() + override fun getMutationsEfficiencies(): Map, Double> { + return mutationsCounts.mapValues { (key, value) -> + (mutationsSuccessCounts[key] ?: 0.0) / value + } } override fun size(): Int { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt index b264c44c70..99cd552c5c 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -26,6 +26,6 @@ open class StringValue( } override fun toString(): String { - return "\"$value\"" + return "\"${valueProvider()}\"" } } \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index e8ed3b6550..1aa83b37d9 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -25,12 +25,17 @@ class FuzzedDescription( val typeCache: MutableMap, val random: Random, val scope: Scope? = null, -) : LoggingDescription( +) : Description( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) - }, - "~/.utbot/JavaFuzzing" + } ) { + // To turn on logging, use this implementation of Description: + //LoggingDescription( + // description.parameters.mapIndexed { index, classId -> + // description.fuzzerType(index) ?: FuzzedType(classId) + //}, "~/.utbot/JavaFuzzing") { ... } + val constants: Sequence get() = description.concreteValues.asSequence() @@ -39,7 +44,6 @@ class FuzzedDescription( } } - fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( BooleanValueProvider, IntegerValueProvider, @@ -58,26 +62,12 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis ) -class JavaFeedback( +data class JavaFeedback( val result: Trie.Node, override val control: Control, ) : Feedback { - override fun equals(other: Any?): Boolean { - if (other is JavaFeedback) { - return this.result == other.result && this.control == other.control - } - return false - } - - override fun hashCode(): Int { - return result.hashCode() * 31 + control.hashCode() - } - override fun toString(): String { - if (result.count == 0) { - return "$result | FAIL | trace hash: ${result.hashCode()} | trace count: ${String.format("%4s", result.count)}" - } - return "$result | trace hash: ${result.hashCode()} | trace count: ${String.format("%4s", result.count)}" + return "$result; trace hash: ${result.hashCode()}; trace count: ${result.count}" } } From 98de6e233066cff2d8bc45b0bf6c3e524e483246 Mon Sep 17 00:00:00 2001 From: grigory Date: Fri, 11 Aug 2023 19:29:29 +0300 Subject: [PATCH 17/25] Fix --- .../src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 6531c2fb05..dd054a318b 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -28,8 +28,7 @@ class FuzzedDescription( ) : Description( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) - }, - "~/.utbot/JavaFuzzing" + } ) { // To turn on logging, use this implementation of Description: //LoggingDescription( From e4612822d96cd626f6878c26d5b285b1b97e3659 Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 21 Aug 2023 13:24:05 +0300 Subject: [PATCH 18/25] Add options for values rotation and global mutation probability tuning, and many other minor fixes --- .../org/utbot/engine/UtBotSymbolicEngine.kt | 3 +- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 57 ++-- .../kotlin/org/utbot/fuzzing/Configuration.kt | 53 +++- .../kotlin/org/utbot/fuzzing/Mutations.kt | 100 +++++-- .../kotlin/org/utbot/fuzzing/Statistic.kt | 251 ++++++++---------- .../kotlin/org/utbot/fuzzing/utils/Trie.kt | 17 ++ .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 4 +- 7 files changed, 292 insertions(+), 193 deletions(-) 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 c465885ade..117208e1d1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -512,7 +512,8 @@ class UtBotSymbolicEngine( var trieNode: Trie.Node? = null if (coveredInstructions.isNotEmpty()) { - trieNode = descr.tracer.add(coveredInstructions) + trieNode = descr.tracer.add( coveredInstructions ) + trieNode.setTraceLen( coveredInstructions.size ) val earlierStateBeforeSize = coverageToMinStateBeforeSize[trieNode] val curStateBeforeSize = stateBefore.calculateSize() diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index ee472504e9..6c85ad53bb 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -12,7 +12,6 @@ import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.random.Random -import java.nio.charset.Charset private val logger by lazy { KotlinLogging.logger {} } @@ -366,16 +365,16 @@ data class BaseFeedback( } } -interface WeightedFeedback> : Feedback { - val weight: WEIGHT +interface WeightedFeedback : Feedback { + val weight: Int } -data class BaseWeightedFeedback>( +data class BaseWeightedFeedback( val result: VALUE, - override val weight: WEIGHT, + override val weight: Int, override val control: Control, -) : WeightedFeedback, Comparable> { - override fun compareTo(other: WeightedFeedback): Int { +) : WeightedFeedback, Comparable> { + override fun compareTo(other: WeightedFeedback): Int { return weight.compareTo(other.weight) } override fun toString(): String { @@ -474,28 +473,45 @@ private suspend fun , F : Feedback> Fuzzing 0) { + statistic.getSeed(random, configuration).let { mutationFactory.mutate(it, random, configuration, statistic) } + } else { - val actualParameters = description.parameters - // fuzz one value, seems to be bad, when have only a few and simple values - fuzzOne(actualParameters).let { - if (random.flipCoin(configuration.probMutationRate)) { + + if (random.flipCoin(50)) { + carrot + } else { + stick + } + + if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { + statistic.getSeed(random, configuration).let { mutationFactory.mutate(it, random, configuration, statistic) - } else { - it + } + } else { + val actualParameters = description.parameters + // fuzz one value, seems to be bad, when have only a few and simple values + fuzzOne(actualParameters).let { + if (random.flipCoin(configuration.probMutationRate)) { + mutationFactory.mutate(it, random, configuration, statistic) + } else { + it + } } } + } + afterIteration(description, statistic) - yield() - statistic.apply { - totalRuns++ - } check(values.parameters.size == values.result.size) { "Cannot create value for ${values.parameters}" } val valuesCache = mutableMapOf, R>() val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } @@ -503,6 +519,11 @@ private suspend fun , F : Feedback> Fuzzing { description.updatePerIteration(values, feedback, minsetResponse) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index 58a26e9133..926040fa9a 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -7,27 +7,46 @@ import kotlin.math.pow */ data class Configuration( + var tuneKnownValueMutations: Boolean = true, + var tuneCollectionMutations: Boolean = true, + var tuneRecursiveMutations: Boolean = false, + + /** + * When true, fuzzer selects one value and runs it for [runsPerValue] times, first [investigationPeriodPerValue] of which + * are investigational. When false, fuzzer investigates mutations efficiencies for [globalInvestigationPeriod] runs and + * tunes it for next [globalExploitationPeriod] runs, then drops statistics and starts over. + */ + var rotateValues: Boolean = false, + + /** + * Number of iterations before mutations probabilities correction when [rotateValues] is false. + */ + var globalInvestigationPeriod: Long = 1000, + + /** + * Number of iterations before dropping statistics when [rotateValues] is false. + */ + var globalExploitationPeriod: Long = 1500, + /** * Configuration used by minset to accumulate and use statistics. - * - * TODO: This part of configuration tend to be changing through runtime tuning or evolutionary algorithm. */ var minsetConfiguration: MinsetConfiguration = MinsetConfiguration(), /** - * Number of continuous iterations for each value. + * Number of continuous iterations for each value when [rotateValues] is true. */ - var runsPerValue: Long = 250, + var runsPerValue: Long = 50, /** - * Number of iterations before mutations probabilities correction. + * Number of iterations before mutations probabilities correction when [rotateValues] is true. */ - var investigationPeriodPerValue: Int = 100, + var investigationPeriodPerValue: Int = 25, /** * Choose between already generated values and new generation of values. */ - var probSeedRetrievingInsteadGenerating: Int = 75, + var probSeedRetrievingInsteadGenerating: Int = 70, /** * Choose between generation and mutation. @@ -118,12 +137,22 @@ data class Configuration( * Configuration used by minset to accumulate and use statistics. */ data class MinsetConfiguration ( - var obsolescenceMultiplier: Double = 1.0, var rewardMultiplier: Double = 1.0, - var rewardWeight: Double = 1.0, + var rewardWeight: Double = 0.0, var penaltyMultiplier: Double = 1.0, - var penaltyWeight: Double = 0.0, - val valueStoragingStrategy: ValueStoragingStrategy = ValueStoragingStrategy.LAST + var penaltyWeight: Double = 0.0 +) + +val carrot = MinsetConfiguration( + rewardMultiplier = 1.1, + rewardWeight = 1.25, + penaltyMultiplier = 1.0, + penaltyWeight = 0.0 ) -enum class ValueStoragingStrategy { FIRST, LAST } +val stick = MinsetConfiguration( + rewardMultiplier = 0.0, + rewardWeight = 0.0, + penaltyMultiplier = 1.1, + penaltyWeight = 1.0 +) \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index ac04ff6aef..558a218ac3 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -28,14 +28,20 @@ class MutationFactory { is Result.Known -> { val mutations = resultToMutate.value.mutations() - val mutationsEffieciencies = statistic.getMutationsEfficiencies() + val mutationsEfficiencies = statistic.getMutationsEfficiencies() + .filter { (k, _) -> mutations.contains(k) } as Map>, Double> val mutation = if ( - configuration.investigationPeriodPerValue > 0 && - statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && - mutationsEffieciencies.values.sum() >= 0 + configuration.tuneKnownValueMutations && + ( + configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || + !configuration.rotateValues && + statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod + ) && + mutationsEfficiencies.isNotEmpty() && + mutationsEfficiencies.values.sum() >= 0 ) { - mutations[random.chooseOne(mutationsEffieciencies.values.toDoubleArray())] + mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { mutations.random(random) } @@ -47,24 +53,62 @@ class MutationFactory { } } is Result.Recursive -> { - when { - resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> - RecursiveMutations.Constructor() + val mutationsEfficiencies = statistic.getMutationsEfficiencies() + .filter { (k, _) -> k is RecursiveMutations<*, *> } as Map, Double> + + val mutation = if ( + configuration.tuneRecursiveMutations && + ( + configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || + !configuration.rotateValues && + statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod + ) && + statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && + mutationsEfficiencies.isNotEmpty() && + mutationsEfficiencies.values.sum() >= 0 + ) { + mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] + } else { + when { + resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> + RecursiveMutations.Constructor() - random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> - RecursiveMutations.ShuffleAndCutModifications() + random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> + RecursiveMutations.ShuffleAndCutModifications() - else -> RecursiveMutations.Mutate() - }.mutate(resultToMutate, recursive, random, configuration) + else -> RecursiveMutations.Mutate() + } + } + + mutation.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { - when { - random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> - CollectionMutations.Mutate() + val mutationsEfficiencies = statistic.getMutationsEfficiencies() + .filter { (k, _) -> k is CollectionMutations<*, *> } as Map, Double> - else -> - CollectionMutations.Shuffle() - }.mutate(resultToMutate, recursive, random, configuration) + val mutation = if ( + configuration.tuneCollectionMutations && + ( + configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || + !configuration.rotateValues && + statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod + ) && + mutationsEfficiencies.isNotEmpty() && + statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && + mutationsEfficiencies.values.sum() >= 0 + ) { + mutationsEfficiencies.keys.toList()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] + } else { + when { + random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> + CollectionMutations.Mutate() + + else -> + CollectionMutations.Shuffle() + } + } + + mutation.mutate(resultToMutate, recursive, random, configuration) } else { resultToMutate } @@ -276,6 +320,16 @@ sealed interface CollectionMutations : Mutation : CollectionMutations { @@ -295,6 +349,16 @@ sealed interface CollectionMutations : Mutation fun getMutationsEfficiencies(): Map, Double> fun size() : Int + fun isEmpty() : Boolean { return size() == 0 } fun isNotEmpty() : Boolean { return size() > 0 } } /** - * Statistic implementation used for experiments with single seed. - * Keeps [Minset] with all feedbacks but [getSeed] returns only one seed, saved from last run. + * Main implementation of [SeedsMaintainingStatistic]. Implements + * mutations efficiencies evaluating algorithm used for mutation probability tuning (see [MainStatisticImpl.put]) + * and seed selection algorithm based on feedbacks counting (see [MainStatisticImpl.getSeed]). */ -open class SingleSeedKeepingStatistic> ( +open class MainStatisticImpl> ( override var totalRuns: Long = 0, override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), @@ -53,136 +55,79 @@ open class SingleSeedKeepingStatistic(configuration.minsetConfiguration.valueStoragingStrategy) - private val feedbacksCounts: LinkedHashMap = linkedMapOf() + private val minset: Minset> = Minset( { SingleValueStorage() } ) + private val minsetConfiguration: MinsetConfiguration = configuration.minsetConfiguration + + private var currentValue: Node? = null private val mutationsCounts = mutableMapOf, Long>() private val mutationsSuccessCounts = mutableMapOf, Double>() override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { - storedSeed.put(seed, feedback) - feedbacksCounts.merge(feedback, 1L, Long::plus) - - val event = when { - feedbacksCounts[feedback] == 1L -> MinsetEvent.NEW_FEEDBACK - configuration.minsetConfiguration.valueStoragingStrategy == ValueStoragingStrategy.LAST -> MinsetEvent.NOTHING_NEW - else -> MinsetEvent.NOTHING_NEW - } + val event = minset.put(seed, feedback) seed.result.forEach { result -> - when(result) { + when (result) { is Result.Known -> { result.lastMutation?.let { mutation -> - mutationsSuccessCounts.forEach{ (key, value) -> - mutationsSuccessCounts[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier - } mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 mutationsSuccessCounts[mutation] = when (event) { MinsetEvent.NEW_FEEDBACK -> { - if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { - mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight - } else { - mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 - } + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight } else -> mutationsSuccessCounts.getOrDefault( mutation, 0.0 - ) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight } } } - else -> {} - } - } - - return event - } - - override fun getSeed(random: Random, configuration: Configuration): Node { - return storedSeed.next() - } - - override fun getMutationsEfficiencies(): Map, Double> { - return mutationsCounts.mapValues { (key, value) -> - (mutationsSuccessCounts[key] ?: 0.0) / value - } - } - - override fun size(): Int { - return feedbacksCounts.size - } -} -/** - * Main implementation of [SeedsMaintainingStatistic]. Implements - * mutations efficiencies evaluating algorithm used for mutation probability tuning (see [MainStatisticImpl.put]) - * and seed selection algorithm based on feedbacks counting (see [MainStatisticImpl.getSeed]). - */ -open class MainStatisticImpl> ( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration -): SeedsMaintainingStatistic { - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy() - ) - - override val elapsedTime: Long - get() = System.nanoTime() - startTime - - private val minset: Minset> = Minset( - { SingleValueStorage(configuration.minsetConfiguration.valueStoragingStrategy) } - ) - - private var currentValue: Node? = null + is Result.Collection -> { + result.lastMutation?.let { mutation -> - private val mutationsCounts = mutableMapOf, Long>() - private val mutationsSuccessCounts = mutableMapOf, Double>() + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = when (event) { + MinsetEvent.NEW_FEEDBACK -> { + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight + } - override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { - val event = minset.put(seed, feedback) + else -> mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight + } + } + } - seed.result.forEach { result -> - when (result) { - is Result.Known -> { + is Result.Recursive -> { result.lastMutation?.let { mutation -> - mutationsSuccessCounts.forEach{ (key, value) -> - mutationsSuccessCounts[key] = value * configuration.minsetConfiguration.obsolescenceMultiplier - } mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 mutationsSuccessCounts[mutation] = when (event) { MinsetEvent.NEW_FEEDBACK -> { - if (this.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue) { - mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * configuration.minsetConfiguration.rewardMultiplier + configuration.minsetConfiguration.rewardWeight - } else { - mutationsSuccessCounts.getOrDefault(mutation, 0.0) + 1 - } + mutationsSuccessCounts.getOrDefault( + mutation, + 0.0 + ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight } else -> mutationsSuccessCounts.getOrDefault( mutation, 0.0 - ) * configuration.minsetConfiguration.penaltyMultiplier + configuration.minsetConfiguration.penaltyWeight + ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight } } } @@ -198,52 +143,37 @@ open class MainStatisticImpl> ( override fun getSeed(random: Random, configuration: Configuration): Node { if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") - currentValue?.result?.forEach { - when (it) { - is Result.Known -> { - if (this.totalRuns % configuration.runsPerValue == 1L) { - - // the law of Demeter is violated because seed selection and power schedule will be significantly reworked - val entries = minset.seeds.entries.toList() - val frequencies = DoubleArray(minset.size()).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) - } - } - - currentValue = entries[random.chooseOne(frequencies)].value.next() - - mutationsCounts.clear() - mutationsSuccessCounts.clear() - } - } - else -> {} - } - } - - // This variable is used for dirty way to turn off sequential running on once chosen value for debugging. - // Breakpoint below evaluates "enabled = false", so currentValue is chosen again independently - val enabled = true - - if (currentValue == null || !enabled) { - // the law of Demeter is violated because seed selection and power schedule will be significantly reworked - val entries = minset.seeds.entries.toList() - val frequencies = DoubleArray(minset.size()).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(minset.count.getOrDefault(key, 0L)) - } + if (configuration.rotateValues && totalRuns % configuration.runsPerValue > 0 && currentValue != null) { + return currentValue!! + } else { + currentValue = minset.getNextSeed(random, configuration.energyFunction) + + if ( + configuration.rotateValues && totalRuns % configuration.runsPerValue == 0L || + !configuration.rotateValues && totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) == 0L + ) { + mutationsCounts.clear() + mutationsSuccessCounts.clear() } - - currentValue = entries[random.chooseOne(frequencies)].value.next() } return currentValue!! } override fun getMutationsEfficiencies(): Map, Double> { - return mutationsCounts.mapValues { (key, value) -> + val efficiencies = mutationsCounts.mapValues { (key, value) -> (mutationsSuccessCounts[key] ?: 0.0) / value } + + if (efficiencies.isNotEmpty()) { + val minValue = efficiencies.minBy { (_, v) -> v }.value + + if (minValue < 0) { + return efficiencies.mapValues { (_, v) -> v + minValue } + } + } + + return efficiencies } override fun size(): Int { @@ -273,6 +203,29 @@ open class Minset, STORAGE : Val val seeds: LinkedHashMap = linkedMapOf(), val count: LinkedHashMap = linkedMapOf() ) { + fun getNextSeed(random: Random, energyFunction: (x: Long) -> Double): Node { + + val entries = seeds.entries.toList() + + var frequencies = DoubleArray(size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = energyFunction(count.getOrDefault(key, 0L)) + } + } + + val currentFeedback = entries[random.chooseOne(frequencies)].key + if (currentFeedback is WeightedFeedback<*, *>) { +// return seeds[seeds.keys.maxBy { (it as WeightedFeedback<*, *>).weight}]!!.next() + frequencies = DoubleArray(size()).also { f -> + seeds.entries.forEachIndexed { index, (key, _) -> + f[index] = (key as WeightedFeedback<*, *>).weight.toDouble() + } + } + } + + return entries[random.chooseOne(frequencies)].value.next() + } + operator fun get(feedback: FEEDBACK): STORAGE? { return seeds[feedback] } @@ -325,24 +278,39 @@ interface ValueStorage : InfiniteIterator> { fun put(value: Node, feedback: Feedback) : Boolean } + /** * Base implementation of [ValueStorage] that keeps only one (first or last, according to [strategy]) * value for each single feedback. */ -open class SingleValueStorage ( - private val strategy : ValueStoragingStrategy -) : ValueStorage { +open class SingleValueStorage : ValueStorage { + + protected var storedValue: Node? = null - private var storedValue: Node? = null + var storedWeight: Int = -1 override fun put(value: Node, feedback: Feedback) : Boolean { - val result = storedValue == null - storedValue = when (strategy) { - ValueStoragingStrategy.FIRST -> storedValue ?: value - ValueStoragingStrategy.LAST -> value + when (feedback) { + is WeightedFeedback -> {} + else -> {} } - return result || (strategy == ValueStoragingStrategy.LAST) + if (feedback is WeightedFeedback<*, *>) { + + val result = storedValue == null || storedWeight <= ((feedback as WeightedFeedback<*, *>).weight) + +// if (storedValue == null || storedWeight <= ((feedback as WeightedFeedback<*, *>).weight)) { + if (storedValue == null || storedWeight > ((feedback as WeightedFeedback<*, *>).weight)) { + storedValue = value + storedWeight = (feedback as WeightedFeedback<*, *>).weight + } + + return result + + } else { + storedValue = value + return true + } } override fun next(): Node { @@ -362,8 +330,7 @@ open class SingleValueStorage ( * [SingleValueStorage] implementation that counts mutations for each feedback stored. * Can be used in the future for trying to reach rare feedbacks. */ -class MutationsCountingSingleValueStorage(strategy: ValueStoragingStrategy) : - SingleValueStorage(strategy) { +class MutationsCountingSingleValueStorage : SingleValueStorage() { private val knownValueMutationsCount: HashMap, Int> = hashMapOf() override fun put(value: Node, feedback: Feedback): Boolean { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index 4e9928d034..022e42c867 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -151,6 +151,9 @@ open class Trie( } interface Node { + fun setTraceLen(len: Int) + fun getTraceLen(): Int + val data: T val count: Int } @@ -168,13 +171,27 @@ open class Trie( val parent: NodeImpl?, override var count: Int = 0, val children: MutableMap> = HashMap(), + private var traceLen: Int = -1 ) : Node { + override fun setTraceLen(len: Int) { + traceLen = len + } + + override fun getTraceLen(): Int { + return traceLen + } + override fun toString(): String { return "$data" } } private object EmptyNode : Node { + override fun setTraceLen(len: Int) {} + override fun getTraceLen(): Int { + return -1 + } + override val data: Any get() = error("empty node has no data") override val count: Int diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index dd054a318b..3f3bcd8aac 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -66,14 +66,14 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis data class JavaFeedback( val result: Trie.Node, override val control: Control, - ) : Feedback { + override val weight: Int = result.getTraceLen(), + ) : WeightedFeedback { override fun toString(): String { return "$result; trace hash: ${result.hashCode()}; trace count: ${result.count}" } } - suspend fun runJavaFuzzing( idGenerator: IdentityPreservingIdGenerator, methodUnderTest: ExecutableId, From f790172d368575c863e6feef64fff7bdc6189b28 Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 21 Aug 2023 15:27:51 +0300 Subject: [PATCH 19/25] Fix AbcFuzzing with reworked WeightedFeedbcak --- .../src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index 9ab6d90b0d..9ca7973e38 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -30,7 +30,7 @@ private const val searchString = suspend fun main() { // Define fuzzing description to start searching. - object : Fuzzing, BaseWeightedFeedback> { + object : Fuzzing, BaseWeightedFeedback> { /** * Generate method returns several samples or seeds which are used as a base for fuzzing. * @@ -47,7 +47,7 @@ suspend fun main() { * This implementation just calls the target function and returns a result. After it returns an empty feedback. * If some returned value equals to the length of the source string then feedback returns 'stop' signal. */ - override suspend fun handle(description: Description, values: List): BaseWeightedFeedback { + override suspend fun handle(description: Description, values: List): BaseWeightedFeedback { check(values.size == 1) { "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" } From 2440bb22d968182809f115e5f6074d4436fca7ef Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 21 Aug 2023 16:48:22 +0300 Subject: [PATCH 20/25] Fix minset configuration choosing --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 6c85ad53bb..87d37c166e 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -479,17 +479,19 @@ private suspend fun , F : Feedback> Fuzzing 0) { - statistic.getSeed(random, configuration).let { mutationFactory.mutate(it, random, configuration, statistic) } } else { - if (random.flipCoin(50)) { - carrot - } else { - stick + if (configuration.rotateValues) { + configuration.minsetConfiguration = + if (random.flipCoin(50)) { + carrot + } else { + stick + } } if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { From 7f7e6dbf9e5788558a0f865275ea9acdfeebd7f0 Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 28 Aug 2023 13:24:15 +0300 Subject: [PATCH 21/25] Add run duration to Feedback, simplify mutations statistics accumulation, change energy function, and do some refactoring --- .../org/utbot/engine/UtBotSymbolicEngine.kt | 1 - .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 56 ++++++-- .../kotlin/org/utbot/fuzzing/Configuration.kt | 47 ++----- .../kotlin/org/utbot/fuzzing/Mutations.kt | 53 +++----- .../kotlin/org/utbot/fuzzing/Statistic.kt | 128 ++++++------------ .../kotlin/org/utbot/fuzzing/utils/Trie.kt | 17 --- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 4 +- .../src/main/resources/classes/guava/list | 20 --- .../org/utbot/python/fuzzing/PythonApi.kt | 4 +- 9 files changed, 120 insertions(+), 210 deletions(-) delete mode 100644 utbot-junit-contest/src/main/resources/classes/guava/list 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 117208e1d1..ca899ad927 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -513,7 +513,6 @@ class UtBotSymbolicEngine( if (coveredInstructions.isNotEmpty()) { trieNode = descr.tracer.add( coveredInstructions ) - trieNode.setTraceLen( coveredInstructions.size ) val earlierStateBeforeSize = coverageToMinStateBeforeSize[trieNode] val curStateBeforeSize = stateBefore.calculateSize() diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 87d37c166e..4cde06db92 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -92,7 +92,11 @@ open class Description( open fun setUp() {} - open fun updatePerIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} + open fun beforeIteration() {} + open fun beforeRun() {} + open fun afterRun() {} + + open fun afterIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} open fun finalizeReport(statistic: Statistic) {} } @@ -106,7 +110,22 @@ abstract class ReportingDescription ( reporter.setUp(this) } - override fun updatePerIteration(values : Node, feedback : Feedback, event: MinsetEvent) { + override fun beforeIteration() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun beforeRun() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun afterRun() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun afterIteration(values : Node, feedback : Feedback, event: MinsetEvent) { reporter.update(values, feedback, event) } @@ -348,6 +367,7 @@ interface Feedback : AsKey { * @see [Control] */ val control: Control + var runDuration: Long? } /** @@ -360,6 +380,8 @@ data class BaseFeedback( val result: VALUE, override val control: Control, ) : Feedback { + override var runDuration: Long? = null + override fun toString(): String { return "$result" } @@ -377,6 +399,9 @@ data class BaseWeightedFeedback( override fun compareTo(other: WeightedFeedback): Int { return weight.compareTo(other.weight) } + + override var runDuration: Long? = null + override fun toString(): String { return "$result with weight $weight" } @@ -412,6 +437,7 @@ fun emptyFeedback(): Feedback = (EmptyFeedback as Feedback) private object EmptyFeedback : Feedback { override val control: Control get() = Control.CONTINUE + override var runDuration: Long? = null override fun equals(other: Any?): Boolean { return true @@ -475,6 +501,12 @@ private suspend fun , F : Feedback> Fuzzing 100) { +// configuration.rotateValues = true +// } val values = @@ -482,18 +514,7 @@ private suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing, R>() val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } + + description.beforeRun() + + val timeBeforeHandle = System.nanoTime() val feedback = fuzzing.handle(description, result) + feedback.runDuration = System.nanoTime() - timeBeforeHandle % 100 + + description.afterRun() val minsetResponse = statistic.put(random, configuration, feedback, values) @@ -528,7 +556,7 @@ private suspend fun , F : Feedback> Fuzzing { - description.updatePerIteration(values, feedback, minsetResponse) + description.afterIteration(values, feedback, minsetResponse) description.finalizeReport(statistic) } Control.STOP -> { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index 926040fa9a..480ffe8812 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -9,7 +9,7 @@ data class Configuration( var tuneKnownValueMutations: Boolean = true, var tuneCollectionMutations: Boolean = true, - var tuneRecursiveMutations: Boolean = false, + var tuneRecursiveMutations: Boolean = true, /** * When true, fuzzer selects one value and runs it for [runsPerValue] times, first [investigationPeriodPerValue] of which @@ -21,27 +21,22 @@ data class Configuration( /** * Number of iterations before mutations probabilities correction when [rotateValues] is false. */ - var globalInvestigationPeriod: Long = 1000, + var globalInvestigationPeriod: Long = 100, /** * Number of iterations before dropping statistics when [rotateValues] is false. */ - var globalExploitationPeriod: Long = 1500, - - /** - * Configuration used by minset to accumulate and use statistics. - */ - var minsetConfiguration: MinsetConfiguration = MinsetConfiguration(), + var globalExploitationPeriod: Long = 900, /** * Number of continuous iterations for each value when [rotateValues] is true. */ - var runsPerValue: Long = 50, + var runsPerValue: Long = 500, /** * Number of iterations before mutations probabilities correction when [rotateValues] is true. */ - var investigationPeriodPerValue: Int = 25, + var investigationPeriodPerValue: Int = 250, /** * Choose between already generated values and new generation of values. @@ -68,7 +63,14 @@ data class Configuration( /** * Energy function that is used to choose seeded value. */ - var energyFunction: (x: Long) -> Double = { x -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) }, + var energyFunction: (feedbackCount: Long, runDuration: Long) -> Double = { + // x, _ -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) + feedbackCount, runDuration -> 1000 / + feedbackCount.coerceAtLeast(1L).toDouble().pow(2) / + runDuration.coerceAtLeast(1L).toDouble().pow(2) + }, + + var mutationRatingFunction: (successProbability: Double) -> Double = { runsPerSuccess -> runsPerSuccess.pow(2) }, /** * Probability to prefer shuffling collection instead of mutation one value from modification @@ -133,26 +135,3 @@ data class Configuration( var maxNumberOfRecursiveSeedModifications: Int = 10, ) -/** - * Configuration used by minset to accumulate and use statistics. - */ -data class MinsetConfiguration ( - var rewardMultiplier: Double = 1.0, - var rewardWeight: Double = 0.0, - var penaltyMultiplier: Double = 1.0, - var penaltyWeight: Double = 0.0 -) - -val carrot = MinsetConfiguration( - rewardMultiplier = 1.1, - rewardWeight = 1.25, - penaltyMultiplier = 1.0, - penaltyWeight = 0.0 -) - -val stick = MinsetConfiguration( - rewardMultiplier = 0.0, - rewardWeight = 0.0, - penaltyMultiplier = 1.1, - penaltyWeight = 1.0 -) \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 558a218ac3..5053522c69 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -20,6 +20,17 @@ class MutationFactory { val recursive: NodeMutation = NodeMutation { n, r, c -> mutate(n, r, c, statistic) } + + // Turn on mutations tuning in two cases: + // - if value rotation is turned on, and it's time according to runsPerValue and investigationPeriodPerValue + // - if value rotation is turned off, and it's time according to globalInvestigationPeriod and globalExploitationPeriod + val tuningMode = + configuration.rotateValues && + statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || + !configuration.rotateValues && + statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod + + val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { is Result.Simple -> Result.Simple( resultToMutate.mutation(resultToMutate.result, random), @@ -28,18 +39,12 @@ class MutationFactory { is Result.Known -> { val mutations = resultToMutate.value.mutations() - val mutationsEfficiencies = statistic.getMutationsEfficiencies() + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) .filter { (k, _) -> mutations.contains(k) } as Map>, Double> - val mutation = if ( + val mutation = if (tuningMode && configuration.tuneKnownValueMutations && - ( - configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || - !configuration.rotateValues && - statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod - ) && - mutationsEfficiencies.isNotEmpty() && - mutationsEfficiencies.values.sum() >= 0 + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 ) { mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -53,19 +58,11 @@ class MutationFactory { } } is Result.Recursive -> { - val mutationsEfficiencies = statistic.getMutationsEfficiencies() + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) .filter { (k, _) -> k is RecursiveMutations<*, *> } as Map, Double> - val mutation = if ( - configuration.tuneRecursiveMutations && - ( - configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || - !configuration.rotateValues && - statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod - ) && - statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && - mutationsEfficiencies.isNotEmpty() && - mutationsEfficiencies.values.sum() >= 0 + val mutation = if (tuningMode && configuration.tuneRecursiveMutations && + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 ) { mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -83,19 +80,11 @@ class MutationFactory { mutation.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { - val mutationsEfficiencies = statistic.getMutationsEfficiencies() + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) .filter { (k, _) -> k is CollectionMutations<*, *> } as Map, Double> - val mutation = if ( - configuration.tuneCollectionMutations && - ( - configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || - !configuration.rotateValues && - statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod - ) && - mutationsEfficiencies.isNotEmpty() && - statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue && - mutationsEfficiencies.values.sum() >= 0 + val mutation = if (tuningMode && configuration.tuneCollectionMutations && + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 ) { mutationsEfficiencies.keys.toList()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -114,6 +103,8 @@ class MutationFactory { } is Result.Empty -> resultToMutate } + + return Node(node.result.toMutableList().apply { set(indexOfMutatedResult, mutated) }, node.parameters, node.builder) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 77deee45d8..1a1dcd2294 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -23,12 +23,14 @@ interface Statistic { */ interface SeedsMaintainingStatistic>: Statistic { override var totalRuns: Long + var lastNewFeedbackIter: Long fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent fun getSeed(random: Random, configuration: Configuration): Node - fun getMutationsEfficiencies(): Map, Double> + fun getMutationsRatings(configuration: Configuration): Map, Double> fun size() : Int fun isEmpty() : Boolean { return size() == 0 } fun isNotEmpty() : Boolean { return size() > 0 } + fun dropMutationsStats() } @@ -42,7 +44,8 @@ open class MainStatisticImpl> ( override val startTime: Long = System.nanoTime(), override var missedTypes: MissedSeed = MissedSeed(), override val random: Random, - override val configuration: Configuration + override val configuration: Configuration, + override var lastNewFeedbackIter: Long = 0 ): SeedsMaintainingStatistic { constructor(source: Statistic) : this( totalRuns = source.totalRuns, @@ -56,79 +59,43 @@ open class MainStatisticImpl> ( get() = System.nanoTime() - startTime private val minset: Minset> = Minset( { SingleValueStorage() } ) - private val minsetConfiguration: MinsetConfiguration = configuration.minsetConfiguration private var currentValue: Node? = null - private val mutationsCounts = mutableMapOf, Long>() - private val mutationsSuccessCounts = mutableMapOf, Double>() + val mutationsCounts = mutableMapOf, Long>() + val mutationsSuccessCounts = mutableMapOf, Double>() override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { val event = minset.put(seed, feedback) + if (event == MinsetEvent.NEW_FEEDBACK) { + lastNewFeedbackIter = totalRuns + } + seed.result.forEach { result -> when (result) { is Result.Known -> { result.lastMutation?.let { mutation -> - mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - - mutationsSuccessCounts[mutation] = when (event) { - MinsetEvent.NEW_FEEDBACK -> { - mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight - } - - else -> mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight - } + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 } } is Result.Collection -> { result.lastMutation?.let { mutation -> - mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - - mutationsSuccessCounts[mutation] = when (event) { - MinsetEvent.NEW_FEEDBACK -> { - mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight - } - - else -> mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight - } + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 } } is Result.Recursive -> { result.lastMutation?.let { mutation -> - mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 - - mutationsSuccessCounts[mutation] = when (event) { - MinsetEvent.NEW_FEEDBACK -> { - mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.rewardMultiplier + minsetConfiguration.rewardWeight - } - - else -> mutationsSuccessCounts.getOrDefault( - mutation, - 0.0 - ) * minsetConfiguration.penaltyMultiplier + minsetConfiguration.penaltyWeight - } + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 } } @@ -152,33 +119,31 @@ open class MainStatisticImpl> ( configuration.rotateValues && totalRuns % configuration.runsPerValue == 0L || !configuration.rotateValues && totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) == 0L ) { - mutationsCounts.clear() - mutationsSuccessCounts.clear() + dropMutationsStats() } } return currentValue!! } - override fun getMutationsEfficiencies(): Map, Double> { - val efficiencies = mutationsCounts.mapValues { (key, value) -> - (mutationsSuccessCounts[key] ?: 0.0) / value + override fun getMutationsRatings(configuration: Configuration): Map, Double> { + val ratings = mutationsCounts.mapValues { (key, _) -> + configuration.mutationRatingFunction( + ( (mutationsSuccessCounts[key] ?: 0.01) / mutationsCounts[key]!! ) + ) } - if (efficiencies.isNotEmpty()) { - val minValue = efficiencies.minBy { (_, v) -> v }.value - - if (minValue < 0) { - return efficiencies.mapValues { (_, v) -> v + minValue } - } - } - - return efficiencies + return ratings } override fun size(): Int { return minset.size() } + + override fun dropMutationsStats() { + mutationsCounts.clear() + mutationsSuccessCounts.clear() + } } ///endregion @@ -203,27 +168,16 @@ open class Minset, STORAGE : Val val seeds: LinkedHashMap = linkedMapOf(), val count: LinkedHashMap = linkedMapOf() ) { - fun getNextSeed(random: Random, energyFunction: (x: Long) -> Double): Node { - + fun getNextSeed(random: Random, energyFunction: (feedbackCount: Long, runDuration: Long) -> Double): Node { val entries = seeds.entries.toList() - var frequencies = DoubleArray(size()).also { f -> + val energy = DoubleArray(size()).also { f -> entries.forEachIndexed { index, (key, _) -> - f[index] = energyFunction(count.getOrDefault(key, 0L)) - } - } - - val currentFeedback = entries[random.chooseOne(frequencies)].key - if (currentFeedback is WeightedFeedback<*, *>) { -// return seeds[seeds.keys.maxBy { (it as WeightedFeedback<*, *>).weight}]!!.next() - frequencies = DoubleArray(size()).also { f -> - seeds.entries.forEachIndexed { index, (key, _) -> - f[index] = (key as WeightedFeedback<*, *>).weight.toDouble() - } + f[index] = energyFunction(count.getOrDefault(key, 0L), key.runDuration ?: 0) } } - return entries[random.chooseOne(frequencies)].value.next() + return entries[random.chooseOne(energy)].value.next() } operator fun get(feedback: FEEDBACK): STORAGE? { @@ -285,31 +239,25 @@ interface ValueStorage : InfiniteIterator> { */ open class SingleValueStorage : ValueStorage { - protected var storedValue: Node? = null + private var storedValue: Node? = null - var storedWeight: Int = -1 + private var storedWeight: Int = -1 override fun put(value: Node, feedback: Feedback) : Boolean { - when (feedback) { - is WeightedFeedback -> {} - else -> {} - } - - if (feedback is WeightedFeedback<*, *>) { + return if (feedback is WeightedFeedback<*, *>) { val result = storedValue == null || storedWeight <= ((feedback as WeightedFeedback<*, *>).weight) -// if (storedValue == null || storedWeight <= ((feedback as WeightedFeedback<*, *>).weight)) { if (storedValue == null || storedWeight > ((feedback as WeightedFeedback<*, *>).weight)) { storedValue = value storedWeight = (feedback as WeightedFeedback<*, *>).weight } - return result + result } else { storedValue = value - return true + true } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index 022e42c867..4e9928d034 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -151,9 +151,6 @@ open class Trie( } interface Node { - fun setTraceLen(len: Int) - fun getTraceLen(): Int - val data: T val count: Int } @@ -171,27 +168,13 @@ open class Trie( val parent: NodeImpl?, override var count: Int = 0, val children: MutableMap> = HashMap(), - private var traceLen: Int = -1 ) : Node { - override fun setTraceLen(len: Int) { - traceLen = len - } - - override fun getTraceLen(): Int { - return traceLen - } - override fun toString(): String { return "$data" } } private object EmptyNode : Node { - override fun setTraceLen(len: Int) {} - override fun getTraceLen(): Int { - return -1 - } - override val data: Any get() = error("empty node has no data") override val count: Int diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 3f3bcd8aac..653b7abb4b 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -66,8 +66,8 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis data class JavaFeedback( val result: Trie.Node, override val control: Control, - override val weight: Int = result.getTraceLen(), - ) : WeightedFeedback { + ) : Feedback { + override var runDuration: Long? = null override fun toString(): String { return "$result; trace hash: ${result.hashCode()}; trace count: ${result.count}" } diff --git a/utbot-junit-contest/src/main/resources/classes/guava/list b/utbot-junit-contest/src/main/resources/classes/guava/list deleted file mode 100644 index 6d57caaecc..0000000000 --- a/utbot-junit-contest/src/main/resources/classes/guava/list +++ /dev/null @@ -1,20 +0,0 @@ -com.google.common.collect.MinMaxPriorityQueue -com.google.common.math.LongMath -com.google.common.primitives.Doubles -com.google.common.graph.Graphs -com.google.common.collect.TreeMultiset -com.google.common.collect.FilteredEntryMultimap -com.google.common.io.FileBackedOutputStream -com.google.common.collect.ComparatorOrdering -com.google.common.collect.LinkedListMultimap -com.google.common.collect.LexicographicalOrdering -com.google.common.base.Throwables -com.google.common.collect.SparseImmutableTable -com.google.common.primitives.ParseRequest -com.google.common.primitives.SignedBytes -com.google.thirdparty.publicsuffix.PublicSuffixType -com.google.common.collect.ImmutableEnumSet -com.google.common.net.MediaType -com.google.common.primitives.UnsignedLongs -com.google.common.collect.FilteredMultimapValues -com.google.common.io.Closeables \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt index 8f67d030ca..b50a9f46ba 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt @@ -47,7 +47,9 @@ data class PythonExecutionResult( data class PythonFeedback( override val control: Control = Control.CONTINUE, val result: Trie.Node = Trie.emptyNode(), -) : Feedback +) : Feedback { + override var runDuration: Long? = null +} class PythonFuzzedValue( val tree: PythonTree.PythonTreeNode, From 2ea4d33d26c4e2484c225dffdaf2b04450a57bbf Mon Sep 17 00:00:00 2001 From: grigory Date: Mon, 28 Aug 2023 14:12:52 +0300 Subject: [PATCH 22/25] Add comment for mutationRatingFunction --- .../src/main/kotlin/org/utbot/fuzzing/Configuration.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index 480ffe8812..8c13105c84 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -70,7 +70,10 @@ data class Configuration( runDuration.coerceAtLeast(1L).toDouble().pow(2) }, - var mutationRatingFunction: (successProbability: Double) -> Double = { runsPerSuccess -> runsPerSuccess.pow(2) }, + /** + * Rating function that is used to manipulate mutations probabilities while tuning. + */ + var mutationRatingFunction: (successProbability: Double) -> Double = { successProbability -> successProbability.pow(2) }, /** * Probability to prefer shuffling collection instead of mutation one value from modification From e565461b5eee571b7692bf2c94c726cce783bc98 Mon Sep 17 00:00:00 2001 From: Perfectrum <38108124+Perfectrum@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:53:09 +0300 Subject: [PATCH 23/25] Add known values mutation strategy based on probability tuning (#2497) * Add result type as the second generic parameter to Description class: Description => Description * Add reporting mechanics with basic logging reporter * Add linear order on feedback with WeightedFeedback * Add ValueStorage, Minset and minset based Statistic implementation * Improve logging and attach it to java fuzzing * Fix java fuzzing logging and add SingleEntryMinsetStatistic * Prettify java fuzzing logging * Add mutations logging * Log only last mutation * Keep last mutation for each result node * Correct mutation probabilities for known values * Move investigations iterations number in Configuration and add MutationsCountingSingleValueStorage * Minor fixes * Multiple minor fixes, some refactoring and comments * Add known values mutation strategy based on probability tuning * Fix * Fix * Add options for values rotation and global mutation probability tuning, and many other minor fixes * Fix AbcFuzzing with reworked WeightedFeedbcak * Fix minset configuration choosing * Add run duration to Feedback, simplify mutations statistics accumulation, change energy function, and do some refactoring * Add comment for mutationRatingFunction --- .gitignore | 1 + .../org.mockito.plugins.MockMaker | 1 + .../utbot/framework/plugin/api/CoverageApi.kt | 4 + .../org/utbot/engine/UtBotSymbolicEngine.kt | 20 +- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 340 ++++++++++++++---- .../kotlin/org/utbot/fuzzing/Configuration.kt | 46 ++- .../kotlin/org/utbot/fuzzing/Mutations.kt | 140 ++++++-- .../kotlin/org/utbot/fuzzing/Providers.kt | 14 +- .../kotlin/org/utbot/fuzzing/Statistic.kt | 284 ++++++++++++++- .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 11 +- .../org/utbot/fuzzing/demo/ForkFuzzing.kt | 12 +- .../org/utbot/fuzzing/demo/HttpRequest.kt | 6 +- .../org/utbot/fuzzing/demo/JavaFuzzing.kt | 13 +- .../org/utbot/fuzzing/demo/JsonFuzzing.kt | 2 +- .../org/utbot/fuzzing/seeds/StringValue.kt | 4 + .../kotlin/org/utbot/fuzzing/utils/Trie.kt | 10 +- .../org/utbot/fuzzing/FuzzerSmokeTest.kt | 26 +- .../kotlin/org/utbot/fuzzing/ProvidersTest.kt | 18 +- .../kotlin/org/utbot/fuzzer/FuzzedValue.kt | 6 +- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 27 +- .../org/utbot/fuzzing/JavaFuzzingTest.kt | 2 +- .../src/main/resources/classes/guava/list | 20 -- .../org/utbot/python/fuzzing/PythonApi.kt | 6 +- 23 files changed, 821 insertions(+), 192 deletions(-) create mode 100644 utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 utbot-junit-contest/src/main/resources/classes/guava/list diff --git a/.gitignore b/.gitignore index 5b11d761cb..25ab286e68 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target/ utbot-intellij/src/main/resources/settings.properties __pycache__ .dmypy.json +tbot-fuzzing/src/main/resources/* diff --git a/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/utbot-analytics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 06c4c9350f..20190000bf 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -17,6 +17,10 @@ data class Instruction( val id: Long ) { val className: String get() = internalName.replace('/', '.') + + override fun toString(): String { + return "class: $className; method: $methodSignature; line number: $lineNumber" + } } /** 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 cfc15a9579..ca899ad927 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -465,13 +465,13 @@ class UtBotSymbolicEngine( if (controller.job?.isActive == false || diff <= thresholdMillisForFuzzingOperation) { logger.info { "Fuzzing overtime: $methodUnderTest" } logger.info { "Test created by fuzzer: $testEmittedByFuzzer" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } if (thisInstance?.model is UtNullModel) { // We should not try to run concretely any models with null-this. // But fuzzer does generate such values, because it can fail to generate any "good" values. - return@runJavaFuzzing BaseFeedback(Trie.emptyNode(), Control.PASS) + return@runJavaFuzzing JavaFeedback(Trie.emptyNode(), Control.PASS) } val stateBefore = concreteExecutionContext.createStateBefore( @@ -494,17 +494,17 @@ class UtBotSymbolicEngine( } // in case an exception occurred from the concrete execution - concreteExecutionResult ?: return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + concreteExecutionResult ?: return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) // in case of processed failure in the concrete execution concreteExecutionResult.processedFailure()?.let { failure -> logger.debug { "Instrumented process failed with exception ${failure.exception} before concrete execution started" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } if (concreteExecutionResult.violatesUtMockAssumption()) { logger.debug { "Generated test case by fuzzer violates the UtMock assumption: $concreteExecutionResult" } - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } val result = concreteExecutionResult.result @@ -512,7 +512,7 @@ class UtBotSymbolicEngine( var trieNode: Trie.Node? = null if (coveredInstructions.isNotEmpty()) { - trieNode = descr.tracer.add(coveredInstructions) + trieNode = descr.tracer.add( coveredInstructions ) val earlierStateBeforeSize = coverageToMinStateBeforeSize[trieNode] val curStateBeforeSize = stateBefore.calculateSize() @@ -521,16 +521,16 @@ class UtBotSymbolicEngine( coverageToMinStateBeforeSize[trieNode] = curStateBeforeSize else { if (++attempts >= attemptsLimit) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.STOP) } - return@runJavaFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) + return@runJavaFuzzing JavaFeedback(result = trieNode, control = Control.CONTINUE) } } else { logger.error { "Coverage is empty for $methodUnderTest with $values" } if (result is UtSandboxFailure) { val stackTraceElements = result.exception.stackTrace.reversed() if (errorStackTraceTracker.add(stackTraceElements).count > 1) { - return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + return@runJavaFuzzing JavaFeedback(result = Trie.emptyNode(), control = Control.PASS) } } } @@ -548,7 +548,7 @@ class UtBotSymbolicEngine( ) testEmittedByFuzzer++ - BaseFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) + JavaFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index a33cba2ea0..4cde06db92 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -5,11 +5,15 @@ import kotlinx.coroutines.* import mu.KotlinLogging import org.utbot.fuzzing.seeds.KnownValue import org.utbot.fuzzing.utils.MissedSeed -import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import org.utbot.fuzzing.utils.transformIfNotEmpty +import java.io.File +import java.io.FileOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.random.Random + private val logger by lazy { KotlinLogging.logger {} } /** @@ -19,7 +23,7 @@ private val logger by lazy { KotlinLogging.logger {} } * @see [org.utbot.fuzzing.demo.JavaFuzzing] * @see [org.utbot.fuzzing.demo.JsonFuzzingKt] */ -interface Fuzzing, FEEDBACK : Feedback> { +interface Fuzzing, FEEDBACK : Feedback> { /** * Before producing seeds, this method is called to recognize, @@ -59,7 +63,7 @@ interface Fuzzing, FEEDBACK : Feed * Starts fuzzing with new description but with copy of [Statistic]. */ suspend fun fork(description: DESCRIPTION, statistics: Statistic) { - fuzz(description, StatisticImpl(statistics)) + fuzz(description, MainStatisticImpl(statistics)) } /** @@ -73,19 +77,148 @@ interface Fuzzing, FEEDBACK : Feed suspend fun afterIteration(description: DESCRIPTION, statistics: Statistic) { } } +///region Description /** * Some description of the current fuzzing run. Usually, it contains the name of the target method and its parameter list. */ -open class Description( +open class Description( parameters: List ) { val parameters: List = parameters.toList() - open fun clone(scope: Scope): Description { + open fun clone(scope: Scope): Description { error("Scope was changed for $this, but method clone is not specified") } + + open fun setUp() {} + + open fun beforeIteration() {} + open fun beforeRun() {} + open fun afterRun() {} + + open fun afterIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} + + open fun finalizeReport(statistic: Statistic) {} +} + +abstract class ReportingDescription ( + parameters: List, + private val reporter: Reporter +) : Description(parameters) { + + override fun setUp() { + reporter.setUp(this) + } + + override fun beforeIteration() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun beforeRun() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun afterRun() { + // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead + super.beforeIteration() + } + + override fun afterIteration(values : Node, feedback : Feedback, event: MinsetEvent) { + reporter.update(values, feedback, event) + } + + override fun finalizeReport(statistic: Statistic) { + reporter.conclude(statistic) + } +} + +open class LoggingDescription ( + parameters: List, + path: String +) : ReportingDescription( + parameters, + LoggingReporter(path = path) +) + + +///region Reporter +abstract class Reporter( + private val reportPath : String, +) { + abstract fun clone() : Reporter + + private lateinit var description: Description + fun setUp(description: Description) { + this.description = description + } + abstract fun update(values : Node, feedback : Feedback, event : MinsetEvent, additionalMessage: String = "") + abstract fun conclude(statistic: Statistic) } + +class LoggingReporter( + val path: String +) : Reporter(path) { + + override fun clone() : LoggingReporter { + return LoggingReporter(path) + } + + private val actualPath = if (path.startsWith("~/")) { + System.getProperty("user.home") + path.drop(1) + } else { + path + } + + private val logFile = File("$actualPath/log") + private val overviewFile = File("$actualPath/overview.txt") + + init { + File(actualPath).mkdirs() + } + + override fun update(values: Node, feedback: Feedback, event : MinsetEvent, additionalMessage: String) { + val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) + + FileOutputStream(logFile, true).bufferedWriter().use { + + val printableValues = buildString { + values.toString() + .toByteArray(Charsets.UTF_8) + .forEach { + byte -> val hexString = byte.toInt().toString(16).padStart(2, '0') + append("\\u").append(hexString) + } + } + + it.write("[$time]\t$printableValues\t$feedback\t$event\t$additionalMessage\n") + } + } + + override fun conclude(statistic: Statistic) { + File(actualPath).mkdirs() + overviewFile.apply{ createNewFile() }.printWriter().use { out -> + out.println("Total runs: ${statistic.totalRuns}") + + if (statistic is SeedsMaintainingStatistic<*, *, *>) { + out.println("Final minset size: ${statistic.size()}") + } + + val seconds = statistic.elapsedTime / 1_000_000_000 + val minutes = (seconds % 3600) / 60 + val remainingSeconds = (seconds % 3600) % 60 + val formattedTime = String.format("%02d min %02d.%03d sec", minutes, remainingSeconds, statistic.elapsedTime % 1_000_000) + out.println("Elapsed time: $formattedTime") + } + } +} +///endregion +///endregion + + + class Scope( val parameterIndex: Int, val recursionDepth: Int, @@ -234,6 +367,7 @@ interface Feedback : AsKey { * @see [Control] */ val control: Control + var runDuration: Long? } /** @@ -245,7 +379,34 @@ interface Feedback : AsKey { data class BaseFeedback( val result: VALUE, override val control: Control, -) : Feedback +) : Feedback { + override var runDuration: Long? = null + + override fun toString(): String { + return "$result" + } +} + +interface WeightedFeedback : Feedback { + val weight: Int +} + +data class BaseWeightedFeedback( + val result: VALUE, + override val weight: Int, + override val control: Control, +) : WeightedFeedback, Comparable> { + override fun compareTo(other: WeightedFeedback): Int { + return weight.compareTo(other.weight) + } + + override var runDuration: Long? = null + + override fun toString(): String { + return "$result with weight $weight" + } +} + /** * Controls fuzzing execution. @@ -276,6 +437,7 @@ fun emptyFeedback(): Feedback = (EmptyFeedback as Feedback) private object EmptyFeedback : Feedback { override val control: Control get() = Control.CONTINUE + override var runDuration: Long? = null override fun equals(other: Any?): Boolean { return true @@ -284,6 +446,10 @@ private object EmptyFeedback : Feedback { override fun hashCode(): Int { return 0 } + + override fun toString(): String { + return "EMPTY_FEEDBACK" + } } class NoSeedValueException internal constructor( @@ -298,12 +464,12 @@ class NoSeedValueException internal constructor( get() = "No seed candidates generated for type: $type" } -suspend fun , F : Feedback> Fuzzing.fuzz( +suspend fun , F : Feedback> Fuzzing.fuzz( description: D, random: Random = Random(0), configuration: Configuration = Configuration() ) { - fuzz(description, StatisticImpl(random = random, configuration = configuration)) + fuzz(description, MainStatisticImpl(random = random, configuration = configuration)) } /** @@ -311,9 +477,9 @@ suspend fun , F : Feedback> Fuzzing.f * * This is an entry point for every fuzzing. */ -private suspend fun , F : Feedback> Fuzzing.fuzz( +private suspend fun , F : Feedback> Fuzzing.fuzz( description: D, - statistic: StatisticImpl, + statistic: SeedsMaintainingStatistic, ) { val random = statistic.random val configuration = statistic.configuration @@ -330,49 +496,82 @@ private suspend fun , F : Feedback> Fuzzing 100) { +// configuration.rotateValues = true +// } + + val values = + + if (configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > 0) { + statistic.getSeed(random, configuration).let { + mutationFactory.mutate(it, random, configuration, statistic) } } else { - val actualParameters = description.parameters - // fuzz one value, seems to be bad, when have only a few and simple values - fuzzOne(actualParameters).let { - if (random.flipCoin(configuration.probMutationRate)) { - mutationFactory.mutate(it, random, configuration) - } else { - it + if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { + statistic.getSeed(random, configuration).let { + mutationFactory.mutate(it, random, configuration, statistic) + } + } else { + val actualParameters = description.parameters + // fuzz one value, seems to be bad, when have only a few and simple values + fuzzOne(actualParameters).let { + if (random.flipCoin(configuration.probMutationRate)) { + mutationFactory.mutate(it, random, configuration, statistic) + } else { + it + } } } + } + afterIteration(description, statistic) - yield() - statistic.apply { - totalRuns++ - } check(values.parameters.size == values.result.size) { "Cannot create value for ${values.parameters}" } val valuesCache = mutableMapOf, R>() val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } + + description.beforeRun() + + val timeBeforeHandle = System.nanoTime() val feedback = fuzzing.handle(description, result) + feedback.runDuration = System.nanoTime() - timeBeforeHandle % 100 + + description.afterRun() + + val minsetResponse = statistic.put(random, configuration, feedback, values) + + yield() + statistic.apply { + totalRuns++ + } + when (feedback.control) { Control.CONTINUE -> { - statistic.put(random, configuration, feedback, values) + description.afterIteration(values, feedback, minsetResponse) + description.finalizeReport(statistic) } Control.STOP -> { + description.finalizeReport(statistic) break } - Control.PASS -> {} + Control.PASS -> { + description.finalizeReport(statistic) + } } } } - - ///region Implementation of the fuzzing and non-public functions. -private fun , FEEDBACK : Feedback> fuzz( +private fun , FEEDBACK : Feedback> fuzz( parameters: List, fuzzing: Fuzzing, description: DESCRIPTION, @@ -399,7 +598,7 @@ private fun , FEEDBACK : Feedback< return Node(result, parameters, builder) } -private fun , FEEDBACK : Feedback> produce( +private fun , FEEDBACK : Feedback> produce( type: TYPE, fuzzing: Fuzzing, description: DESCRIPTION, @@ -436,7 +635,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Collection] type. When `configuration.recursionTreeDepth` limit is reached it creates * an empty collection and doesn't do any modification to it. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Collection, fuzzing: Fuzzing, description: DESCRIPTION, @@ -496,7 +695,7 @@ private fun , FEEDBACK : Feedback< * reduces [Seed.Recursive] type. When `configuration.recursionTreeDepth` limit is reached it calls * `Seed.Recursive#empty` routine to create an empty object. */ -private fun , FEEDBACK : Feedback> reduce( +private fun , FEEDBACK : Feedback> reduce( task: Seed.Recursive, fuzzing: Fuzzing, description: DESCRIPTION, @@ -652,13 +851,18 @@ sealed interface Result { /** * Known value. */ - class Known>(val value: V, val build: (V) -> RESULT) : Result + class Known>(val value: V, val build: (V) -> RESULT, val lastMutation: Mutation? = null) : Result { + override fun toString(): String { + return this.value.toString() + } + } /** * A tree of object that has constructor and some modifications. */ class Recursive( val construct: Node, val modify: List>, + val lastMutation: RecursiveMutations? = null, ) : Result /** @@ -668,6 +872,7 @@ sealed interface Result { val construct: Node, val modify: List>, val iterations: Int, + val lastMutation: CollectionMutations? = null, ) : Result /** @@ -684,52 +889,33 @@ sealed interface Result { class Node( val result: List>, val parameters: List, - val builder: Routine, -) - -private class StatisticImpl>( - override var totalRuns: Long = 0, - override val startTime: Long = System.nanoTime(), - override var missedTypes: MissedSeed = MissedSeed(), - override val random: Random, - override val configuration: Configuration, -) : Statistic { - - constructor(source: Statistic) : this( - totalRuns = source.totalRuns, - startTime = source.startTime, - missedTypes = source.missedTypes, - random = source.random, - configuration = source.configuration.copy(), - ) - - override val elapsedTime: Long - get() = System.nanoTime() - startTime - private val seeds = linkedMapOf>() - private val count = linkedMapOf() - - fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { - if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { - seeds[feedback] = seed - } else { - seeds.putIfAbsent(feedback, seed) - } - count[feedback] = count.getOrDefault(feedback, 0L) + 1L - } - - fun getRandomSeed(random: Random, configuration: Configuration): Node { - if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") - val entries = seeds.entries.toList() - val frequencies = DoubleArray(seeds.size).also { f -> - entries.forEachIndexed { index, (key, _) -> - f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) + val builder: Routine +) { + override fun toString() : String { + return result.map { + when(it) { + is Result.Empty -> "_" + is Result.Simple -> it.result + is Result.Known<*, *, *> -> { + if (it.lastMutation != null) { + "${it.value} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.value.toString() } + } + is Result.Collection -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "[", "]")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { it.modify.joinToString(", ", "[", "]") } + } + is Result.Recursive -> { + if (it.lastMutation != null) { + "${it.modify.joinToString(", ", "{", "}")} : ${it.lastMutation.toString().substringAfter("$").substringBefore('@')}" + } else { + it.modify.joinToString(", ", "{", "}") + } + } } - } - val index = random.chooseOne(frequencies) - return entries[index].value + }.joinToString(", ") } - - fun isNotEmpty() = seeds.isNotEmpty() } ///endregion diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt index a6c19926c9..8c13105c84 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -7,6 +7,37 @@ import kotlin.math.pow */ data class Configuration( + var tuneKnownValueMutations: Boolean = true, + var tuneCollectionMutations: Boolean = true, + var tuneRecursiveMutations: Boolean = true, + + /** + * When true, fuzzer selects one value and runs it for [runsPerValue] times, first [investigationPeriodPerValue] of which + * are investigational. When false, fuzzer investigates mutations efficiencies for [globalInvestigationPeriod] runs and + * tunes it for next [globalExploitationPeriod] runs, then drops statistics and starts over. + */ + var rotateValues: Boolean = false, + + /** + * Number of iterations before mutations probabilities correction when [rotateValues] is false. + */ + var globalInvestigationPeriod: Long = 100, + + /** + * Number of iterations before dropping statistics when [rotateValues] is false. + */ + var globalExploitationPeriod: Long = 900, + + /** + * Number of continuous iterations for each value when [rotateValues] is true. + */ + var runsPerValue: Long = 500, + + /** + * Number of iterations before mutations probabilities correction when [rotateValues] is true. + */ + var investigationPeriodPerValue: Int = 250, + /** * Choose between already generated values and new generation of values. */ @@ -32,7 +63,17 @@ data class Configuration( /** * Energy function that is used to choose seeded value. */ - var energyFunction: (x: Long) -> Double = { x -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) }, + var energyFunction: (feedbackCount: Long, runDuration: Long) -> Double = { + // x, _ -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) + feedbackCount, runDuration -> 1000 / + feedbackCount.coerceAtLeast(1L).toDouble().pow(2) / + runDuration.coerceAtLeast(1L).toDouble().pow(2) + }, + + /** + * Rating function that is used to manipulate mutations probabilities while tuning. + */ + var mutationRatingFunction: (successProbability: Double) -> Double = { successProbability -> successProbability.pow(2) }, /** * Probability to prefer shuffling collection instead of mutation one value from modification @@ -95,4 +136,5 @@ data class Configuration( * Limits maximum number of recursive seed modifications */ var maxNumberOfRecursiveSeedModifications: Int = 10, -) \ No newline at end of file +) + diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 9868be39b8..5053522c69 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -7,46 +7,104 @@ import org.utbot.fuzzing.utils.chooseOne import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random -class MutationFactory { - fun mutate(node: Node, random: Random, configuration: Configuration): Node { +class MutationFactory { + fun mutate( + node: Node, + random: Random, + configuration: Configuration, + statistic: SeedsMaintainingStatistic + ): Node { if (node.result.isEmpty()) return node val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) val recursive: NodeMutation = NodeMutation { n, r, c -> - mutate(n, r, c) + mutate(n, r, c, statistic) } + + // Turn on mutations tuning in two cases: + // - if value rotation is turned on, and it's time according to runsPerValue and investigationPeriodPerValue + // - if value rotation is turned off, and it's time according to globalInvestigationPeriod and globalExploitationPeriod + val tuningMode = + configuration.rotateValues && + statistic.totalRuns % configuration.runsPerValue > configuration.investigationPeriodPerValue || + !configuration.rotateValues && + statistic.totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) > configuration.globalInvestigationPeriod + + val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { - is Result.Simple -> Result.Simple(resultToMutate.mutation(resultToMutate.result, random), resultToMutate.mutation) + is Result.Simple -> Result.Simple( + resultToMutate.mutation(resultToMutate.result, random), + resultToMutate.mutation + ) is Result.Known -> { val mutations = resultToMutate.value.mutations() + + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) + .filter { (k, _) -> mutations.contains(k) } as Map>, Double> + + val mutation = if (tuningMode && + configuration.tuneKnownValueMutations && + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + ) { + mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] + } else { + mutations.random(random) + } + if (mutations.isNotEmpty()) { - resultToMutate.mutate(mutations.random(random), random, configuration) + resultToMutate.mutate(mutation, random, configuration) } else { resultToMutate } } is Result.Recursive -> { - when { - resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> - RecursiveMutations.Constructor() - random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> - RecursiveMutations.ShuffleAndCutModifications() - else -> - RecursiveMutations.Mutate() - }.mutate(resultToMutate, recursive, random, configuration) + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) + .filter { (k, _) -> k is RecursiveMutations<*, *> } as Map, Double> + + val mutation = if (tuningMode && configuration.tuneRecursiveMutations && + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + ) { + mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] + } else { + when { + resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> + RecursiveMutations.Constructor() + + random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> + RecursiveMutations.ShuffleAndCutModifications() + + else -> RecursiveMutations.Mutate() + } + } + + mutation.mutate(resultToMutate, recursive, random, configuration) } is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { - when { - random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> - CollectionMutations.Mutate() - else -> - CollectionMutations.Shuffle() - }.mutate(resultToMutate, recursive, random, configuration) + val mutationsEfficiencies = statistic.getMutationsRatings(configuration) + .filter { (k, _) -> k is CollectionMutations<*, *> } as Map, Double> + + val mutation = if (tuningMode && configuration.tuneCollectionMutations && + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + ) { + mutationsEfficiencies.keys.toList()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] + } else { + when { + random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> + CollectionMutations.Mutate() + + else -> + CollectionMutations.Shuffle() + } + } + + mutation.mutate(resultToMutate, recursive, random, configuration) } else { resultToMutate } is Result.Empty -> resultToMutate } + + return Node(node.result.toMutableList().apply { set(indexOfMutatedResult, mutated) }, node.parameters, node.builder) @@ -82,12 +140,17 @@ class MutationFactory { } @Suppress("UNCHECKED_CAST") - private fun > Result.Known.mutate(mutation: Mutation, random: Random, configuration: Configuration): Result.Known { + private fun > Result.Known.mutate( + mutation: Mutation, + random: Random, + configuration: Configuration + ): Result.Known { val source: T = value as T val mutate = mutation.mutate(source, random, configuration) return Result.Known( mutate, - build as (T) -> RESULT + build as (T) -> RESULT, + lastMutation = mutation ) } } @@ -244,9 +307,20 @@ sealed interface CollectionMutations : Mutation : CollectionMutations { @@ -262,9 +336,20 @@ sealed interface CollectionMutations : Mutation : Mutation { return Result.Recursive( construct = recursive.mutate(source.construct,random, configuration), - modify = source.modify + modify = source.modify, + lastMutation = this, ) } } @@ -309,7 +395,8 @@ sealed interface RecursiveMutations : Mutation { return Result.Recursive( construct = source.construct, - modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)) + modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)), + lastMutation = this, ) } } @@ -326,7 +413,8 @@ sealed interface RecursiveMutations : Mutation, FEEDBACK : Feedback> runFuzzing( +suspend fun , FEEDBACK : Feedback> runFuzzing( provider: ValueProvider, description: DESCRIPTION, random: Random = Random(0), @@ -24,7 +24,7 @@ suspend fun , FEEDBACK : Feedback< * @param providers is a list of "type to values" generator * @param exec this function is called when fuzzer generates values of type R to run it with target program. */ -class BaseFuzzing, F : Feedback>( +class BaseFuzzing, F : Feedback>( val providers: List>, val exec: suspend (description: D, values: List) -> F ) : Fuzzing { @@ -60,7 +60,7 @@ class BaseFuzzing, F : Feedback>( /** * Value provider generates [Seed] and has other methods to combine providers. */ -fun interface ValueProvider> { +fun interface ValueProvider> { fun enrich(description: D, type: T, scope: Scope) {} @@ -144,7 +144,7 @@ fun interface ValueProvider> { return if (this is Fallback) { provider.unwrapIfFallback() } else { this } } - private class Fallback>( + private class Fallback>( val provider: ValueProvider, val fallback: ValueProvider, ): ValueProvider { @@ -172,7 +172,7 @@ fun interface ValueProvider> { /** * Wrapper class that delegates implementation to the [providers]. */ - private class Combined>(providers: List>): ValueProvider { + private class Combined>(providers: List>): ValueProvider { val providers: List> init { @@ -203,7 +203,7 @@ fun interface ValueProvider> { } companion object { - fun > of(valueProviders: List>): ValueProvider { + fun > of(valueProviders: List>): ValueProvider { return Combined(valueProviders) } } @@ -215,7 +215,7 @@ fun interface ValueProvider> { * @param type that is used as a filter to call this provider * @param generate yields values for the type */ -class TypeProvider>( +class TypeProvider>( val type: T, val generate: suspend SequenceScope>.(description: D, type: T) -> Unit ) : ValueProvider { diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 877937cedb..1a1dcd2294 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -1,6 +1,7 @@ package org.utbot.fuzzing import org.utbot.fuzzing.utils.MissedSeed +import org.utbot.fuzzing.utils.chooseOne import kotlin.random.Random /** @@ -13,4 +14,285 @@ interface Statistic { val missedTypes: MissedSeed val random: Random val configuration: Configuration -} \ No newline at end of file +} + +///region Statistic Implementations + +/** + * Interface of statistic object with seeds maintaining logic (seed selection, value storaging, feedbacks counting, etc.) + */ +interface SeedsMaintainingStatistic>: Statistic { + override var totalRuns: Long + var lastNewFeedbackIter: Long + fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) : MinsetEvent + fun getSeed(random: Random, configuration: Configuration): Node + fun getMutationsRatings(configuration: Configuration): Map, Double> + fun size() : Int + fun isEmpty() : Boolean { return size() == 0 } + fun isNotEmpty() : Boolean { return size() > 0 } + fun dropMutationsStats() +} + + +/** + * Main implementation of [SeedsMaintainingStatistic]. Implements + * mutations efficiencies evaluating algorithm used for mutation probability tuning (see [MainStatisticImpl.put]) + * and seed selection algorithm based on feedbacks counting (see [MainStatisticImpl.getSeed]). + */ +open class MainStatisticImpl> ( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, + override var lastNewFeedbackIter: Long = 0 +): SeedsMaintainingStatistic { + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy() + ) + + override val elapsedTime: Long + get() = System.nanoTime() - startTime + + private val minset: Minset> = Minset( { SingleValueStorage() } ) + + private var currentValue: Node? = null + + val mutationsCounts = mutableMapOf, Long>() + val mutationsSuccessCounts = mutableMapOf, Double>() + + + override fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node): MinsetEvent { + val event = minset.put(seed, feedback) + + if (event == MinsetEvent.NEW_FEEDBACK) { + lastNewFeedbackIter = totalRuns + } + + seed.result.forEach { result -> + when (result) { + is Result.Known -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 + } + } + + is Result.Collection -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 + } + } + + is Result.Recursive -> { + result.lastMutation?.let { mutation -> + mutationsCounts[mutation] = mutationsCounts.getOrDefault(mutation, 0) + 1 + mutationsSuccessCounts[mutation] = mutationsSuccessCounts.getOrDefault(mutation, 0.0) + + if (event == MinsetEvent.NEW_FEEDBACK) 1.0 else 0.0 + } + } + + else -> {} + } + } + + return event + } + + + override fun getSeed(random: Random, configuration: Configuration): Node { + if (minset.isEmpty()) error("Call `isNotEmpty` before getting the seed") + + if (configuration.rotateValues && totalRuns % configuration.runsPerValue > 0 && currentValue != null) { + return currentValue!! + } else { + currentValue = minset.getNextSeed(random, configuration.energyFunction) + + if ( + configuration.rotateValues && totalRuns % configuration.runsPerValue == 0L || + !configuration.rotateValues && totalRuns % (configuration.globalInvestigationPeriod + configuration.globalExploitationPeriod) == 0L + ) { + dropMutationsStats() + } + } + + return currentValue!! + } + + override fun getMutationsRatings(configuration: Configuration): Map, Double> { + val ratings = mutationsCounts.mapValues { (key, _) -> + configuration.mutationRatingFunction( + ( (mutationsSuccessCounts[key] ?: 0.01) / mutationsCounts[key]!! ) + ) + } + + return ratings + } + + override fun size(): Int { + return minset.size() + } + + override fun dropMutationsStats() { + mutationsCounts.clear() + mutationsSuccessCounts.clear() + } +} +///endregion + +///region Minset + +/** + * Possible answers given by [Minset] after putting a value in it with [Minset.put]: + * - [NEW_FEEDBACK]: new unique feedback was found; + * - [NEW_VALUE]: feedback is already in minset, and value stored for it was updated; + * - [NOTHING_NEW]: feedback is already in minset and the value was not updated either. + * + * According to current implementation, [Minset] figures out if feedback is new, + * but decision between [NEW_VALUE] and [NOTHING_NEW] is making by [ValueStorage]. + */ +enum class MinsetEvent { NEW_FEEDBACK, NEW_VALUE, NOTHING_NEW } + +/** + * Storage for feedbacks mapped on the values that led to them. + */ +open class Minset, STORAGE : ValueStorage> ( + open val valueStorageGenerator: () -> STORAGE, + val seeds: LinkedHashMap = linkedMapOf(), + val count: LinkedHashMap = linkedMapOf() +) { + fun getNextSeed(random: Random, energyFunction: (feedbackCount: Long, runDuration: Long) -> Double): Node { + val entries = seeds.entries.toList() + + val energy = DoubleArray(size()).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = energyFunction(count.getOrDefault(key, 0L), key.runDuration ?: 0) + } + } + + return entries[random.chooseOne(energy)].value.next() + } + + operator fun get(feedback: FEEDBACK): STORAGE? { + return seeds[feedback] + } + + open fun put(value: Node, feedback: FEEDBACK) : MinsetEvent { + val result: MinsetEvent + + if (seeds.containsKey(feedback)) { + result = if( seeds[feedback]!!.put(value, feedback) ) { MinsetEvent.NEW_VALUE } else { MinsetEvent.NOTHING_NEW } + } else { + result = MinsetEvent.NEW_FEEDBACK + seeds[feedback] = valueStorageGenerator.invoke() + seeds[feedback]!!.put(value, feedback) + } + + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + + return result + } + + fun isNotEmpty(): Boolean { + return seeds.isNotEmpty() + } + + fun isEmpty(): Boolean { + return seeds.isEmpty() + } + + fun size() : Int { + return seeds.size + } +} +///endregion + +///region Value storages + +/** + * Interface used for [ValueStorage]. + */ +interface InfiniteIterator : Iterator { + override operator fun next(): T + override operator fun hasNext(): Boolean +} + +/** + * Interface to storage values mapped to feedbacks in [Minset]. + * Now only [SingleValueStorage] implements it, but there might be more complex logic. + */ +interface ValueStorage : InfiniteIterator> { + fun put(value: Node, feedback: Feedback) : Boolean +} + + +/** + * Base implementation of [ValueStorage] that keeps only one (first or last, according to [strategy]) + * value for each single feedback. + */ +open class SingleValueStorage : ValueStorage { + + private var storedValue: Node? = null + + private var storedWeight: Int = -1 + override fun put(value: Node, feedback: Feedback) : Boolean { + + return if (feedback is WeightedFeedback<*, *>) { + + val result = storedValue == null || storedWeight <= ((feedback as WeightedFeedback<*, *>).weight) + + if (storedValue == null || storedWeight > ((feedback as WeightedFeedback<*, *>).weight)) { + storedValue = value + storedWeight = (feedback as WeightedFeedback<*, *>).weight + } + + result + + } else { + storedValue = value + true + } + } + + override fun next(): Node { + if (storedValue == null) { + error("Next value requested but no value stored") + } else { + return storedValue as Node + } + } + + override fun hasNext(): Boolean { + return storedValue != null + } +} + +/** + * [SingleValueStorage] implementation that counts mutations for each feedback stored. + * Can be used in the future for trying to reach rare feedbacks. + */ +class MutationsCountingSingleValueStorage : SingleValueStorage() { + private val knownValueMutationsCount: HashMap, Int> = hashMapOf() + + override fun put(value: Node, feedback: Feedback): Boolean { + value.result.forEach { result -> + when (result) { + is Result.Known<*, *, *> -> { + result.lastMutation?.let { + knownValueMutationsCount[it] = knownValueMutationsCount.getOrDefault(it, 0) + 1 + } + } + else -> {}} + } + return super.put(value, feedback) + } +} + +///endregion \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt index 4969ccf7f4..9ca7973e38 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -30,14 +30,14 @@ private const val searchString = suspend fun main() { // Define fuzzing description to start searching. - object : Fuzzing, BaseFeedback> { + object : Fuzzing, BaseWeightedFeedback> { /** * Generate method returns several samples or seeds which are used as a base for fuzzing. * * In this particular case only 1 value is provided which is an empty string. Also, a mutation * is defined for any string value. This mutation adds a random character from ASCII table. */ - override fun generate(description: Description, type: Unit) = sequenceOf>( + override fun generate(description: Description, type: Unit) = sequenceOf>( Seed.Simple("") { s, r -> s + Char(r.nextInt(1, 256)) } ) @@ -47,17 +47,18 @@ suspend fun main() { * This implementation just calls the target function and returns a result. After it returns an empty feedback. * If some returned value equals to the length of the source string then feedback returns 'stop' signal. */ - override suspend fun handle(description: Description, values: List): BaseFeedback { + override suspend fun handle(description: Description, values: List): BaseWeightedFeedback { check(values.size == 1) { "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" } val input = values.first() val result = searchString.findMaxSubstring(input) println("findMaxSubstring(\"$input\") = $result") - return BaseFeedback( + return BaseWeightedFeedback( result = result, + weight = result, control = if (result == searchString.length) Control.STOP else Control.CONTINUE ) } - }.fuzz(Description(listOf(Unit))) + }.fuzz(LoggingDescription(listOf(Unit), "~/.utbot/AbcFuzzing")) } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt index 99be17e156..ead8debe12 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt @@ -11,15 +11,15 @@ private enum class Type { fun main(): Unit = runBlocking { launch { - object : Fuzzing, Feedback> { + object : Fuzzing, Feedback> { private val runs = mutableMapOf() - override fun generate(description: Description, type: Type): Sequence> { + override fun generate(description: Description, type: Type): Sequence> { return sequenceOf(Seed.Simple(type.name)) } - override suspend fun handle(description: Description, values: List): Feedback { + override suspend fun handle(description: Description, values: List): Feedback { description.parameters.forEach { runs[it]!!.incrementAndGet() } @@ -28,7 +28,7 @@ fun main(): Unit = runBlocking { } override suspend fun afterIteration( - description: Description, + description: Description, stats: Statistic, ) { if (stats.totalRuns % 10 == 0L && description.parameters.size == 1) { @@ -38,7 +38,7 @@ fun main(): Unit = runBlocking { Type.MORE_CONCRETE -> listOf() } if (newTypes.isNotEmpty()) { - val d = Description(newTypes) + val d = Description(newTypes) fork(d, stats) // Description can be used as a transfer object, // that collects information about the current running. @@ -47,7 +47,7 @@ fun main(): Unit = runBlocking { } } - override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { + override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { println("info: ${description.parameters} runs ${stats.totalRuns}") return description.parameters.all { runs.computeIfAbsent(it) { AtomicLong(0) }.get() >= 10 } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt index 844336aad1..cfe9af33c5 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt @@ -7,8 +7,8 @@ import java.util.concurrent.TimeUnit fun main() = runBlocking { withTimeout(TimeUnit.SECONDS.toMillis(10)) { - object : Fuzzing, Feedback> { - override fun generate(description: Description, type: String) = sequence> { + object : Fuzzing, Feedback> { + override fun generate(description: Description, type: String) = sequence> { when (type) { "url" -> yield(Seed.Recursive( construct = Routine.Create( @@ -49,7 +49,7 @@ fun main() = runBlocking { } override suspend fun handle( - description: Description, + description: Description, values: List ): Feedback { println(values[0]) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt index 0d3d490dd7..dad1e219c6 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt @@ -10,8 +10,8 @@ import org.utbot.fuzzing.seeds.StringValue * This example implements some basics for Java fuzzing that supports only a few types: * integers, strings, primitive arrays and user class [A]. */ -object JavaFuzzing : Fuzzing, Any?, Description>, Feedback, Any?>> { - override fun generate(description: Description>, type: Class<*>) = sequence, Any?>> { +object JavaFuzzing : Fuzzing, Any?, LoggingDescription, Any?>, Feedback, Any?>> { + override fun generate(description: LoggingDescription, Any?>, type: Class<*>) = sequence, Any?>> { if (type == Boolean::class.javaPrimitiveType) { yield(Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) yield(Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) @@ -71,7 +71,7 @@ object JavaFuzzing : Fuzzing, Any?, Description>, Feedback>, values: List): Feedback, Any?> { + override suspend fun handle(description: LoggingDescription, Any?>, values: List): Feedback, Any?> { println(values.joinToString { when (it) { is BooleanArray -> it.contentToString() @@ -88,7 +88,12 @@ object JavaFuzzing : Fuzzing, Any?, Description>, Feedback() - BaseFuzzing, Feedback>( + BaseFuzzing, Feedback>( TypeProvider(CustomType.INT) { _, _ -> for (b in Signed.values()) { yield(Seed.Known(BitVectorValue(3, b)) { bvv -> diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt index cd054754b8..99cd552c5c 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -24,4 +24,8 @@ open class StringValue( StringMutations.ShuffleCharacters, ) } + + override fun toString(): String { + return "\"${valueProvider()}\"" + } } \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index 7bc4190f64..4e9928d034 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -168,13 +168,21 @@ open class Trie( val parent: NodeImpl?, override var count: Int = 0, val children: MutableMap> = HashMap(), - ) : Node + ) : Node { + override fun toString(): String { + return "$data" + } + } private object EmptyNode : Node { override val data: Any get() = error("empty node has no data") override val count: Int get() = 0 + + override fun toString(): String { + return "EMPTY" + } } companion object { diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt index 260b824942..bb82022ae9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt @@ -18,7 +18,7 @@ class FuzzerSmokeTest { fun `fuzzing runs with empty parameters`() { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( { _, _ -> sequenceOf() }, Description(emptyList()) ) { _, _ -> @@ -34,7 +34,7 @@ class FuzzerSmokeTest { assertThrows { runBlocking { var count = 0 - runFuzzing, BaseFeedback>( + runFuzzing, BaseFeedback>( provider = { _, _ -> sequenceOf() }, description = Description(listOf(Unit)), configuration = Configuration( @@ -55,7 +55,7 @@ class FuzzerSmokeTest { var count = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> count += 1 BaseFeedback(Unit, Control.STOP) @@ -71,7 +71,7 @@ class FuzzerSmokeTest { var executions = 0 runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> executions++ BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) @@ -91,7 +91,7 @@ class FuzzerSmokeTest { generations++ sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) } @@ -108,7 +108,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> throw SpecialException() } @@ -128,7 +128,7 @@ class FuzzerSmokeTest { var depth = 0 var count = 0 runFuzzing( - ValueProvider, Node?, Description>> { _, _ -> + { _, _ -> sequenceOf(Seed.Recursive( construct = Routine.Create(listOf(Node::class, Node::class)) { v -> Node(v[0], v[1]) @@ -162,7 +162,7 @@ class FuzzerSmokeTest { withTimeout(1000) { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10 000 ms") @@ -181,7 +181,7 @@ class FuzzerSmokeTest { val start = System.currentTimeMillis() runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -197,7 +197,7 @@ class FuzzerSmokeTest { @Test fun `fuzzer generate same result when random is seeded`() { data class B(var a: Int) - val provider = ValueProvider> { _, _ -> + val provider = ValueProvider> { _, _ -> sequenceOf( Seed.Simple(B(0)) { p, r -> B(p.a + r.nextInt()) }, Seed.Known(BitVectorValue(32, Signed.POSITIVE)) { B(it.toInt()) }, @@ -245,7 +245,7 @@ class FuzzerSmokeTest { flow { runFuzzing( { _, _ -> sequenceOf(Seed.Simple(Unit)) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> if (System.currentTimeMillis() - start > 10_000) { error("Fuzzer didn't stopped in 10_000 ms") @@ -273,7 +273,7 @@ class FuzzerSmokeTest { modify = sequenceOf(Routine.Call(emptyList()) { s, _ -> s.append("1") }), empty = Routine.Empty { fail("Empty is called despite construct requiring no args") } )) }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, (s) -> if (s.isEmpty()) { seenEmpty = true @@ -299,7 +299,7 @@ class FuzzerSmokeTest { modify = emptySequence(), empty = Routine.Empty { null } )}.asSequence() }, - Description(listOf(Unit)) + Description(listOf(Unit)) ) { _, _ -> seenAnything = true BaseFeedback(Unit, Control.STOP) diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt index 74b2d8c204..29965929c9 100644 --- a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt @@ -5,23 +5,23 @@ import org.junit.jupiter.api.Test class ProvidersTest { - private fun p(supplier: () -> T) : ValueProvider> { + private fun p(supplier: () -> T) : ValueProvider> { return ValueProvider { _, _ -> sequenceOf(Seed.Simple(supplier())) } } private fun p( accept: () -> Boolean, supplier: () -> T - ) : ValueProvider> { - return object : ValueProvider> { + ) : ValueProvider> { + return object : ValueProvider> { override fun accept(type: Unit) = accept() - override fun generate(description: Description, type: Unit) = sequence> { + override fun generate(description: Description, type: Unit) = sequence> { yield(Seed.Simple(supplier())) } } } - private val description = Description(listOf(Unit)) + private val description = Description(listOf(Unit)) @Test fun `test common provider API`() { @@ -170,9 +170,9 @@ class ProvidersTest { val seq = ValueProvider.of(listOf( p({ false }, { 1 }), p({ false }, { 2 }), p({ false }, { 3 }), )).withFallback( - object : ValueProvider> { + object : ValueProvider> { override fun accept(type: Unit) = false - override fun generate(description: Description, type: Unit) = emptySequence>() + override fun generate(description: Description, type: Unit) = emptySequence>() } ).generate(description, Unit) Assertions.assertEquals(0, seq.count()) @@ -180,12 +180,12 @@ class ProvidersTest { @Test fun `type providers check exactly the type`() { - val seq1 = TypeProvider>('A') { _, _ -> + val seq1 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'A') Assertions.assertEquals(1, seq1.count()) - val seq2 = TypeProvider>('A') { _, _ -> + val seq2 = TypeProvider>('A') { _, _ -> yield(Seed.Simple(2)) }.generate(Description(listOf('A')), 'B') Assertions.assertEquals(0, seq2.count()) diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt index d5d4a6d7f2..fae534654c 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -19,4 +19,8 @@ import org.utbot.framework.plugin.api.UtModel open class FuzzedValue( val model: UtModel, var summary: String? = null, -) \ No newline at end of file +) { + override fun toString(): String { + return "FuzzedValue: $summary" + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 38565368e9..653b7abb4b 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -17,17 +17,25 @@ private val logger = KotlinLogging.logger {} typealias JavaValueProvider = ValueProvider + + class FuzzedDescription( val description: FuzzedMethodDescription, val tracer: Trie, val typeCache: MutableMap, val random: Random, - val scope: Scope? = null -) : Description( + val scope: Scope? = null, +) : Description( description.parameters.mapIndexed { index, classId -> description.fuzzerType(index) ?: FuzzedType(classId) } ) { + // To turn on logging, use this implementation of Description: + //LoggingDescription( + // description.parameters.mapIndexed { index, classId -> + // description.fuzzerType(index) ?: FuzzedType(classId) + //}, "~/.utbot/JavaFuzzing") { ... } + val constants: Sequence get() = description.concreteValues.asSequence() @@ -36,6 +44,7 @@ class FuzzedDescription( } } + fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( BooleanValueProvider, IntegerValueProvider, @@ -53,13 +62,25 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis NullValueProvider, ) + +data class JavaFeedback( + val result: Trie.Node, + override val control: Control, + ) : Feedback { + override var runDuration: Long? = null + override fun toString(): String { + return "$result; trace hash: ${result.hashCode()}; trace count: ${result.count}" + } +} + + suspend fun runJavaFuzzing( idGenerator: IdentityPreservingIdGenerator, methodUnderTest: ExecutableId, constants: Collection, names: List, providers: List> = defaultValueProviders(idGenerator), - exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> + exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> JavaFeedback ) { val random = Random(0) val classUnderTest = methodUnderTest.classId diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt index 360d706356..5a23bb1ed0 100644 --- a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt @@ -231,7 +231,7 @@ class JavaFuzzingTest { } } -class MarkerValueProvider>( +class MarkerValueProvider>( val name: String ) : ValueProvider { var enrich: Int = 0 diff --git a/utbot-junit-contest/src/main/resources/classes/guava/list b/utbot-junit-contest/src/main/resources/classes/guava/list deleted file mode 100644 index 6d57caaecc..0000000000 --- a/utbot-junit-contest/src/main/resources/classes/guava/list +++ /dev/null @@ -1,20 +0,0 @@ -com.google.common.collect.MinMaxPriorityQueue -com.google.common.math.LongMath -com.google.common.primitives.Doubles -com.google.common.graph.Graphs -com.google.common.collect.TreeMultiset -com.google.common.collect.FilteredEntryMultimap -com.google.common.io.FileBackedOutputStream -com.google.common.collect.ComparatorOrdering -com.google.common.collect.LinkedListMultimap -com.google.common.collect.LexicographicalOrdering -com.google.common.base.Throwables -com.google.common.collect.SparseImmutableTable -com.google.common.primitives.ParseRequest -com.google.common.primitives.SignedBytes -com.google.thirdparty.publicsuffix.PublicSuffixType -com.google.common.collect.ImmutableEnumSet -com.google.common.net.MediaType -com.google.common.primitives.UnsignedLongs -com.google.common.collect.FilteredMultimapValues -com.google.common.io.Closeables \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt index 9d32164be0..f60cc6c838 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt @@ -29,7 +29,7 @@ class PythonMethodDescription( val pythonTypeStorage: PythonTypeStorage, val tracer: Trie, val random: Random, -) : Description(parameters) +) : Description(parameters) sealed interface FuzzingExecutionFeedback class ValidExecution(val utFuzzedExecution: PythonUtExecution): FuzzingExecutionFeedback @@ -47,7 +47,9 @@ data class PythonExecutionResult( data class PythonFeedback( override val control: Control = Control.CONTINUE, val result: Trie.Node = Trie.emptyNode(), -) : Feedback +) : Feedback { + override var runDuration: Long? = null +} class PythonFuzzedValue( val tree: PythonTree.PythonTreeNode, From ade6bb0f51beb78f9bde31bb099c8c391792c449 Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 6 Sep 2023 16:52:58 +0300 Subject: [PATCH 24/25] Fix IO --- .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 140 ++++++++---------- .../kotlin/org/utbot/fuzzing/Statistic.kt | 2 +- 2 files changed, 63 insertions(+), 79 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt index 4cde06db92..a10c17b44d 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -11,6 +11,7 @@ import java.io.File import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import kotlin.properties.Delegates import kotlin.random.Random @@ -96,9 +97,7 @@ open class Description( open fun beforeRun() {} open fun afterRun() {} - open fun afterIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} - - open fun finalizeReport(statistic: Statistic) {} + open suspend fun afterIteration(values: Node, feedback: Feedback, event: MinsetEvent) {} } abstract class ReportingDescription ( @@ -111,26 +110,19 @@ abstract class ReportingDescription ( } override fun beforeIteration() { - // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead - super.beforeIteration() + reporter.beforeIteration() } override fun beforeRun() { - // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead - super.beforeIteration() + reporter.beforeRun() } override fun afterRun() { - // TODO: Use beforeIteration, beforeRun, afterRun and afterIteration to analyze fuzzer's overhead - super.beforeIteration() - } - - override fun afterIteration(values : Node, feedback : Feedback, event: MinsetEvent) { - reporter.update(values, feedback, event) + reporter.afterRun() } - override fun finalizeReport(statistic: Statistic) { - reporter.conclude(statistic) + override suspend fun afterIteration(values : Node, feedback : Feedback, event: MinsetEvent) { + reporter.afterIteration(values, feedback, event) } } @@ -153,8 +145,10 @@ abstract class Reporter( fun setUp(description: Description) { this.description = description } - abstract fun update(values : Node, feedback : Feedback, event : MinsetEvent, additionalMessage: String = "") - abstract fun conclude(statistic: Statistic) + open fun beforeIteration() {} + open fun beforeRun() {} + open fun afterRun() {} + open fun afterIteration(values: Node, feedback: Feedback, event : MinsetEvent) {} } @@ -162,6 +156,10 @@ class LoggingReporter( val path: String ) : Reporter(path) { + private var iterationStartTime by Delegates.notNull() + private var executionStartTime by Delegates.notNull() + private var executionFinishTime by Delegates.notNull() + override fun clone() : LoggingReporter { return LoggingReporter(path) } @@ -172,46 +170,46 @@ class LoggingReporter( path } - private val logFile = File("$actualPath/log") - private val overviewFile = File("$actualPath/overview.txt") + private val logWriter = FileOutputStream(File("$actualPath/log"), true).bufferedWriter() init { File(actualPath).mkdirs() } - override fun update(values: Node, feedback: Feedback, event : MinsetEvent, additionalMessage: String) { - val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) - - FileOutputStream(logFile, true).bufferedWriter().use { - - val printableValues = buildString { - values.toString() - .toByteArray(Charsets.UTF_8) - .forEach { - byte -> val hexString = byte.toInt().toString(16).padStart(2, '0') - append("\\u").append(hexString) - } - } + override fun beforeIteration() { + iterationStartTime = System.nanoTime() + } - it.write("[$time]\t$printableValues\t$feedback\t$event\t$additionalMessage\n") - } + override fun beforeRun() { + executionStartTime = System.nanoTime() } - override fun conclude(statistic: Statistic) { - File(actualPath).mkdirs() - overviewFile.apply{ createNewFile() }.printWriter().use { out -> - out.println("Total runs: ${statistic.totalRuns}") + override fun afterRun() { + executionFinishTime = System.nanoTime() + } - if (statistic is SeedsMaintainingStatistic<*, *, *>) { - out.println("Final minset size: ${statistic.size()}") - } + override fun afterIteration(values: Node, feedback: Feedback, event : MinsetEvent) { + val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss:SSS")) - val seconds = statistic.elapsedTime / 1_000_000_000 - val minutes = (seconds % 3600) / 60 - val remainingSeconds = (seconds % 3600) % 60 - val formattedTime = String.format("%02d min %02d.%03d sec", minutes, remainingSeconds, statistic.elapsedTime % 1_000_000) - out.println("Elapsed time: $formattedTime") + val printableValues = buildString { + values.toString() + .toByteArray(Charsets.UTF_8) + .forEach { byte -> + val char = byte.toInt().toChar() + if (char.isDefined() && char != '\t' && char != '\n' && char != '"') { + append(char) + } else { + val hexString = byte.toInt().toString(16).padStart(2, '0') + append("\\u").append(hexString) + } + } } + + val iterationDuration = (System.nanoTime() - iterationStartTime).toFloat() / 1_000_000 + val executionDuration = (executionFinishTime - executionStartTime).toFloat() / 1_000_000 + + logWriter.write("$time\t${"%.3f".format(iterationDuration)}\t${"%.3f".format(executionDuration)}\t$feedback\t$event\t$printableValues\n") + logWriter.flush() } } ///endregion @@ -503,33 +501,27 @@ private suspend fun , F : Feedback> Fuzzing 100) { -// configuration.rotateValues = true -// } - val values = - - if (configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > 0) { - statistic.getSeed(random, configuration).let { - mutationFactory.mutate(it, random, configuration, statistic) - } - } else { - if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { + if (configuration.rotateValues && statistic.totalRuns % configuration.runsPerValue > 0) { statistic.getSeed(random, configuration).let { mutationFactory.mutate(it, random, configuration, statistic) } } else { - val actualParameters = description.parameters - // fuzz one value, seems to be bad, when have only a few and simple values - fuzzOne(actualParameters).let { - if (random.flipCoin(configuration.probMutationRate)) { + if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { + statistic.getSeed(random, configuration).let { mutationFactory.mutate(it, random, configuration, statistic) - } else { - it + } + } else { + val actualParameters = description.parameters + // fuzz one value, seems to be bad, when have only a few and simple values + fuzzOne(actualParameters).let { + if (random.flipCoin(configuration.probMutationRate)) { + mutationFactory.mutate(it, random, configuration, statistic) + } else { + it + } } } - } } @@ -543,7 +535,7 @@ private suspend fun , F : Feedback> Fuzzing, F : Feedback> Fuzzing { - description.afterIteration(values, feedback, minsetResponse) - description.finalizeReport(statistic) - } - Control.STOP -> { - description.finalizeReport(statistic) - break - } - Control.PASS -> { - description.finalizeReport(statistic) - } + description.afterIteration(values, feedback, minsetResponse) + + if (feedback.control == Control.STOP) { + break } } } diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt index 1a1dcd2294..5f382cd714 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -129,7 +129,7 @@ open class MainStatisticImpl> ( override fun getMutationsRatings(configuration: Configuration): Map, Double> { val ratings = mutationsCounts.mapValues { (key, _) -> configuration.mutationRatingFunction( - ( (mutationsSuccessCounts[key] ?: 0.01) / mutationsCounts[key]!! ) + ((mutationsSuccessCounts[key] ?: 0.01) / mutationsSuccessCounts.values.sum()) ) } From 89b5f9dbb7da1f8054f7797d365e55fb59a2958c Mon Sep 17 00:00:00 2001 From: grigory Date: Wed, 6 Sep 2023 17:22:31 +0300 Subject: [PATCH 25/25] Fix FuzzerSmokeTest --- .../kotlin/org/utbot/fuzzing/Mutations.kt | 49 +++++++++++++++---- .../org/utbot/fuzzing/FuzzerSmokeTest.kt | 2 +- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt index 5053522c69..a92e885fb8 100644 --- a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -44,7 +44,7 @@ class MutationFactory { val mutation = if (tuningMode && configuration.tuneKnownValueMutations && - mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() > 0 ) { mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -62,7 +62,7 @@ class MutationFactory { .filter { (k, _) -> k is RecursiveMutations<*, *> } as Map, Double> val mutation = if (tuningMode && configuration.tuneRecursiveMutations && - mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() > 0 ) { mutationsEfficiencies.keys.toTypedArray()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -84,7 +84,7 @@ class MutationFactory { .filter { (k, _) -> k is CollectionMutations<*, *> } as Map, Double> val mutation = if (tuningMode && configuration.tuneCollectionMutations && - mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() >= 0 + mutationsEfficiencies.isNotEmpty() && mutationsEfficiencies.values.sum() > 0 ) { mutationsEfficiencies.keys.toList()[random.chooseOne(mutationsEfficiencies.values.toDoubleArray())] } else { @@ -252,6 +252,10 @@ sealed interface StringMutations : Mutation { } return StringValue(newString, lastMutation = this, mutatedFrom = source) } + + override fun equals(other: Any?): Boolean { + return this.javaClass == other?.javaClass + } } object RemoveCharacter : StringMutations { @@ -266,6 +270,10 @@ sealed interface StringMutations : Mutation { } return StringValue(newString, this) } + + override fun equals(other: Any?): Boolean { + return this.javaClass == other?.javaClass + } } object ShuffleCharacters : StringMutations { @@ -275,6 +283,10 @@ sealed interface StringMutations : Mutation { lastMutation = this ) } + + override fun equals(other: Any?): Boolean { + return this.javaClass == other?.javaClass + } } } @@ -313,9 +325,7 @@ sealed interface CollectionMutations : Mutation : Mutation : Mutation : RecursiveMutations { @@ -399,6 +415,14 @@ sealed interface RecursiveMutations : Mutation : RecursiveMutations { @@ -418,5 +442,12 @@ sealed interface RecursiveMutations : Mutation B(size) }, - modify = Routine.ForEach(listOf(Unit)) { self, ind, v -> self.a = ind * self.a * v.first().a } + modify = Routine.ForEach(listOf(Unit)) { self, ind, v -> self.a *= ind * v.first().a } ) ) }