From 3542afb7360fdea2c4503307c38d424ef3cbf78d Mon Sep 17 00:00:00 2001 From: Maksim Pelevin Date: Mon, 12 Dec 2022 09:22:39 +0300 Subject: [PATCH] New fuzzing platform (#1457) --- docs/Fuzzing Platform.md | 237 +++++++ settings.gradle.kts | 1 + .../org/utbot/engine/UtBotSymbolicEngine.kt | 208 ++---- .../org/utbot/external/api/UtBotJavaApi.kt | 16 +- .../org/utbot/fuzzer/FuzzerFunctions.kt | 30 +- utbot-fuzzers/build.gradle.kts | 1 + .../kotlin/org/utbot/fuzzer/FuzzedType.kt | 4 +- .../kotlin/org/utbot/fuzzer/FuzzedValue.kt | 1 - .../main/kotlin/org/utbot/fuzzer/Fuzzer.kt | 3 +- .../org/utbot/fuzzer/FuzzerStatistics.kt | 2 + .../kotlin/org/utbot/fuzzer/ModelMutator.kt | 2 +- .../kotlin/org/utbot/fuzzer/ModelProvider.kt | 2 +- .../fuzzer/mutators/NumberRandomMutator.kt | 2 +- .../fuzzer/mutators/StringRandomMutator.kt | 2 +- .../fuzzer/objects/AssembleModelUtils.kt | 2 +- .../providers/DateConstantModelProvider.kt | 3 +- .../fuzzer/providers/ObjectModelProvider.kt | 69 +- .../fuzzer/providers/RegexModelProvider.kt | 2 +- .../kotlin/org/utbot/fuzzing/JavaLanguage.kt | 122 ++++ .../org/utbot/fuzzing/providers/Arrays.kt | 43 ++ .../utbot/fuzzing/providers/Collections.kt | 208 ++++++ .../org/utbot/fuzzing/providers/Enums.kt | 30 + .../org/utbot/fuzzing/providers/Objects.kt | 198 ++++++ .../org/utbot/fuzzing/providers/Others.kt | 140 ++++ .../org/utbot/fuzzing/providers/Primitives.kt | 245 +++++++ .../framework/plugin/api/ModelMutatorTest.kt | 2 +- utbot-fuzzing/build.gradle.kts | 7 + .../main/java/org/utbot/fuzzing/demo/A.java | 33 + .../src/main/kotlin/org/utbot/fuzzing/Api.kt | 662 ++++++++++++++++++ .../kotlin/org/utbot/fuzzing/Configuration.kt | 75 ++ .../kotlin/org/utbot/fuzzing/Mutations.kt | 74 ++ .../kotlin/org/utbot/fuzzing/Providers.kt | 191 +++++ .../org/utbot/fuzzing/demo/AbcFuzzing.kt | 63 ++ .../org/utbot/fuzzing/demo/JavaFuzzing.kt | 94 +++ .../org/utbot/fuzzing/demo/JsonFuzzing.kt | 88 +++ .../org/utbot/fuzzing/seeds/BitVectorValue.kt | 254 +++++++ .../org/utbot/fuzzing/seeds/IEEE754Value.kt | 278 ++++++++ .../org/utbot/fuzzing/seeds/KnownValue.kt | 13 + .../org/utbot/fuzzing/seeds/RegexValue.kt | 24 + .../org/utbot/fuzzing/seeds/StringValue.kt | 53 ++ .../utbot/fuzzing/utils}/CartesianProduct.kt | 2 +- .../org/utbot/fuzzing/utils}/Combinations.kt | 2 +- .../org/utbot/fuzzing/utils/Functions.kt | 96 +++ .../utils}/PseudoShuffledIntProgression.kt | 2 +- .../utbot/fuzzing/utils}/RandomExtensions.kt | 2 +- .../kotlin/org/utbot/fuzzing/utils}/Trie.kt | 20 +- .../providers/known/BitVectorValueTest.kt | 136 ++++ .../org/utbot/fuzzing/samples/Arrays.java | 110 +++ .../utbot/fuzzing/samples/Collections.java | 177 +++++ .../org/utbot/fuzzing/samples/Dates.java | 21 + .../org/utbot/fuzzing/samples/Enums.java | 61 ++ .../org/utbot/fuzzing/samples/Floats.java | 44 ++ .../org/utbot/fuzzing/samples/Integers.java | 66 ++ .../org/utbot/fuzzing/samples/Objects.java | 36 + .../org/utbot/fuzzing/samples/Strings.java | 67 ++ .../utbot/fuzzing/samples/data/Recursive.java | 19 + .../samples/data/UserDefinedClass.java | 44 ++ .../utbot/fuzzing/utils}/CombinationsTest.kt | 5 +- .../PseudoShuffledIntProgressionTest.kt | 3 +- .../fuzzing/utils}/RandomExtensionsTest.kt | 4 +- .../org/utbot/fuzzing/utils}/TrieTest.kt | 5 +- 61 files changed, 4107 insertions(+), 299 deletions(-) create mode 100644 docs/Fuzzing Platform.md create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt create mode 100644 utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt create mode 100644 utbot-fuzzing/build.gradle.kts create mode 100644 utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt rename {utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer => utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils}/CartesianProduct.kt (98%) rename {utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer => utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils}/Combinations.kt (99%) create mode 100644 utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt rename {utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer => utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils}/PseudoShuffledIntProgression.kt (99%) rename {utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer => utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils}/RandomExtensions.kt (96%) rename {utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer => utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils}/Trie.kt (91%) create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java create mode 100644 utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java rename {utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api => utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils}/CombinationsTest.kt (98%) rename {utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api => utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils}/PseudoShuffledIntProgressionTest.kt (98%) rename {utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api => utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils}/RandomExtensionsTest.kt (96%) rename {utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api => utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils}/TrieTest.kt (96%) diff --git a/docs/Fuzzing Platform.md b/docs/Fuzzing Platform.md new file mode 100644 index 0000000000..686ff78ec8 --- /dev/null +++ b/docs/Fuzzing Platform.md @@ -0,0 +1,237 @@ +# Fuzzing Platform (FP) Design + +**Problem:** fuzzing is a versatile technique for generating values to be used as method arguments. Normally, +to generate values, one needs information on a method signature, or rather on the parameter types (if a fuzzer is +able to "understand" them). _White-box_ approach also requires AST, and _grey-box_ approach needs coverage +information. To generate values that may serve as method arguments, the fuzzer uses generators, mutators, and +predefined values. + +* _Generators_ yield concrete objects created by descriptions. The basic description for creating objects is _type_. + Constants, regular expressions, and other structured object specifications (e.g. in HTML) may be also used as + descriptions. + +* _Mutators_ modify the object in accordance with some logic that usually means random changes. To get better + results, mutators obtain feedback (information on coverage and the inner state of the + program) during method call. + +* _Predefined values_ work well for known problems, e.g. incorrect symbol sequences. To discover potential problems one can analyze parameter names as well as the specific constructs or method calls inside the method body. + +General API for using fuzzer looks like this: + +``` +fuzz( + params = "number", "string", "object: number, string", + seedGenerator = (type: Type) -> seeds + details: (constants, providers, etc) +).forEveryGeneratedValues { values: List -> + feedback = exec(values); + return feedback +} +``` + +Fuzzer accepts list of types which can be provided in different formats: string, object or Class<*> in Java. Then seed +generator accepts these types and produces seeds which are used as base objects for value generation and mutations. +Fuzzing logic about how to choose, combine and mutate values from seed set is only fuzzing responsibility. API should not provide such abilities except general fuzzing configuring. + +## Parameters + +The general fuzzing process gets the list of parameter descriptions as input and returns the corresponding list of values. The simplest description is the specific object type, for example: + +```kotlin +[Int, Bool] +``` + +In this particular case, the fuzzing process can generate the set of all the pairs having integer as the first value +and `true` or `false` as the second one. If values `-3, 0, 10` are generated to be the `Int` values, the set of all the possible combinations has six items: `(-3, false), (0, false), (10, false), (-3, true), (0, true), (10, true)`. Depending on the programming language, one may use interface descriptions or annotations (type hints) instead of defining the specific type. Fuzzing platform (FP) is not able to create the concrete objects as it does not deal with the specific languages. It still can convert the descriptions to the known constructs it can work with. + +Say, in most of the programming languages, any integer may be represented as a bit array, and fuzzer can construct and +modify bit arrays. So, in general case, the boundary values for the integer are these bit arrays: + +* [0, 0, 0, ..., 0] - null +* [1, 0, 0, ..., 0] - minimum value +* [0, 1, 1, ..., 1] - maximum value +* [0, 0, ..., 0, 1] - plus 1 +* [1, 1, 1, ..., 1] - minus 1 + +One can correctly use this representation for unsigned integers as well: + +* [0, 0, 0, ..., 0] - null (minimum value) +* [1, 0, 0, ..., 0] - maximum value / 2 +* [0, 1, 1, ..., 1] - maximum value / 2 + 1 +* [0, 0, ..., 0, 1] - plus 1 +* [1, 1, 1, ..., 1] - maximum value + +Thus, FP interprets the _Byte_ and _Unsigned Byte_ descriptions in different ways: in the former case, the maximum value is [0, 1, 1, 1, 1, 1, 1, 1], while in the latter case it is [1, 1, 1, 1, 1, 1, 1, 1]. FP types are described in details further. + +## Refined parameter description + +During the fuzzing process, some parameters get the refined description, for example: + +``` +public boolean isNaN(Number n) { + if (!(n instanceof Double)) { + return false; + } + return Double.isNaN((Double) n); +} +``` + +In the above example, let the parameter be `Integer`. Considering the feedback, the fuzzer suggests that nothing but `Double` might increase coverage, so the type may be downcasted to `Double`. This allows for filtering out a priori unfitting values. + +## Statically and dynamically generated values +Predefined, or _statically_ generated, values help to define the initial range of values, which could be used as method arguments. These values allow us to: + +* check if it is possible to call the given method with at least some set of values as arguments, +* gather statistics on executing the program, +* refine the parameter description. + +_Dynamic_ values are generated in two ways: + +* internally — via mutating the existing values, successfully performed as method arguments (i.e. seeds); +* externally — via obtaining feedback that can return not only the statistics on the execution (the paths explored, + the time spent, etc.) but also the set of new values to be blended with the values already in use. + +Dynamic values should have the higher priority for a sample, that's why they should be chosen either first or at least more likely than the statically generated ones. In general, the algorithm that guides the fuzzing process looks like this: + +``` +# dynamic values are stored with respect to their return priority +dynamic_values = empty_priority_queue() +# static values are generated beforehand +static_values = generate() +# "good" values +seeded_values = [] +# +filters = [] + +# the loop runs until coverage reaches 100% +while app.should_fuzz(seeded_values.feedbacks): + # first we choose all dynamic values + # if there are no dynamic values, choose the static ones + value = dynamic_values.take() ?: static_values.take_random() + # if there is no value or it was filtered out (static values are generated in advance — they can be random and unfitting), try to generate new values via mutating the seeds + if value is null or filters.is_filtered(value): + value = mutate(seeded_values.random_value()) + # if there is still no value at this point, it means that there are no appropriate values at all, and the process stops + if value is null: break + + # run with the given values and obtain feedback + feedback = yield_run(value) + # feedback says if it is reasonable to add the current value to the set of seeds + if feedback is good: + seeded_values[feedback] += value + # feedback may also provide fuzzer with the new values + if feedback has suggested_value: + dynamic_values += feedback.suggested_values() with high_priority + + # mutate the static value thus allowing fuzzer to alternate static and dynamic values + if value.is_static_generated: + dynamic_values += mutate(seeded_values.random_value()) with low_priority +``` + +## Helping fuzzer via code modification + +Sometimes it is reasonable to modify the source code so that it makes applying fuzzer to it easier. This is one of possible approaches: to split the complex _if_-statement into the sequence of simpler _if_-statements. See [Circumventing Fuzzing Roadblocks with Compiler Transformations](https://lafintel.wordpress.com/2016/08/15/circumventing-fuzzing-roadblocks-with-compiler-transformations/) for details. + +## Generators + +There are two types of generators: + +* yielding values of primitive data types: integers, strings, booleans +* yielding values of recursive data types: objects, lists + +Sometimes it is necessary not only to create an object but to modify it as well. We can apply fuzzing to +the fuzzer-generated values that should be modified. For example, you have the `HashMap.java` class, and you need to +generate +three +modifications for it using `put(key, value)`. For this purpose, you may request for applying the fuzzer to six +parameters `(key, value, key, value, key, value)` and get the necessary modified values. + +Primitive type generators allow for yielding + +1. Signed integers of a given size (8, 16, 32, and 64 bits, usually) +2. Unsigned integers of a given size +3. Floating-point numbers with a given size of significand and exponent according to IEEE 754 +4. Booleans: _True_ and _False_ +5. Characters (in UTF-16 format) +6. Strings (consisting of UTF-16 characters) + +Fuzzer should be able to provide out-of-the-box support for these types — be able to create, modify, and process +them. To work with multiple languages it is enough to specify the possible type size and to describe and create the +concrete objects based on the FP-generated values. + +The recursive types include two categories: + +* Collections (arrays and lists) +* Objects + +Collections may be nested and have _n_ dimensions (one, two, three, or more). + +Collections may be: + +* of a fixed size (e.g., arrays) +* of a variable size (e.g., lists and dictionaries) + +Objects may have: + +1. Constructors with parameters + +2. Modifiable inner fields + +3. Modifiable global values (the static ones) + +4. Calls for modifying methods + +FP should be able to create and describe such objects in the form of a tree. The semantics of actual modifications is under the responsibility of a programming language. + + +## Typing + +FP does not use the concept of _type_ for creating objects. Instead, FP introduces the _task_ concept — it +encapsulates the description of a type, which should be used to create an object. Generally, this task consists of two +blocks: the task for initializing values and the list of tasks for modifying the initialized value. + +``` +Task = [ + Initialization: [T1, T2, T3, ..., TN] + Modification(Initialization): [ + М1: [T1, T2, ..., TK], + М2: [T1, T2, ..., TJ], + МH: [T1, T2, ..., TI], + ] +] +``` + +Thus, we can group the tasks as follows: + +``` +1. Trivial task = [ + Initialization: [INT|UNSIGNED.INT|FLOAT.POINT.NUMBER|BOOLEAN|CHAR|STRING] + Modification(Initialization): [] +] + + +2. Task for creating an array = [ + Initialization: [UNSIGNED.INT] + Modification(UNSIGNED.INT) = [T] * UNSIGNED.INT +] + +or + +2. Task for creating an array = [ + Initialization: [UNSIGNED.INT] + Modification(UNSIGNED.INT) = [[T * UNSIGNED.INT]] +] + +where "*" means repeating the type the specified number of times + +3. Task for creating an object = [ + Initialization: [Т1, Т2, ... ТN], + Modification(UNSIGNED.INT) = [ + ... + ] +] + +``` + +Therefore, each programming language defines how to interpret a certain type and how to infer it. This allows fuzzer +to store and mutate complex objects without any additional support from the language. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1ecbc36e4d..3480b63ceb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include("utbot-framework-api") include("utbot-intellij") include("utbot-sample") include("utbot-fuzzers") +include("utbot-fuzzing") include("utbot-junit-contest") include("utbot-analytics") include("utbot-analytics-torch") 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 fb0ce9855f..c85b7e84f8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -1,5 +1,8 @@ package org.utbot.engine +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import mu.KotlinLogging import org.utbot.analytics.EngineAnalyticsContext import org.utbot.analytics.FeatureProcessor @@ -7,29 +10,17 @@ import org.utbot.analytics.Predictors import org.utbot.common.bracket import org.utbot.common.debug import org.utbot.engine.MockStrategy.NO_MOCKS -import org.utbot.engine.pc.UtArraySelectExpression -import org.utbot.engine.pc.UtBoolExpression -import org.utbot.engine.pc.UtContextInitializer -import org.utbot.engine.pc.UtSolver -import org.utbot.engine.pc.UtSolverStatusSAT -import org.utbot.engine.pc.findTheMostNestedAddr -import org.utbot.engine.pc.mkEq -import org.utbot.engine.pc.mkInt -import org.utbot.engine.selectors.PathSelector -import org.utbot.engine.selectors.StrategyOption -import org.utbot.engine.selectors.coveredNewSelector -import org.utbot.engine.selectors.cpInstSelector -import org.utbot.engine.selectors.forkDepthSelector -import org.utbot.engine.selectors.inheritorsSelector -import org.utbot.engine.selectors.mlSelector +import org.utbot.engine.pc.* +import org.utbot.engine.selectors.* import org.utbot.engine.selectors.nurs.NonUniformRandomSearch -import org.utbot.engine.selectors.pollUntilFastSAT -import org.utbot.engine.selectors.randomPathSelector -import org.utbot.engine.selectors.randomSelector import org.utbot.engine.selectors.strategies.GraphViz -import org.utbot.engine.selectors.subpathGuidedSelector +import org.utbot.engine.state.ExecutionStackElement +import org.utbot.engine.state.ExecutionState +import org.utbot.engine.state.StateLabel import org.utbot.engine.symbolic.SymbolicState import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.TypeRegistry +import org.utbot.engine.types.TypeResolver import org.utbot.engine.util.mockListeners.MockListener import org.utbot.engine.util.mockListeners.MockListenerController import org.utbot.framework.PathSelectorType @@ -43,75 +34,19 @@ import org.utbot.framework.UtSettings.useDebugVisualization import org.utbot.framework.concrete.UtConcreteExecutionData import org.utbot.framework.concrete.UtConcreteExecutionResult import org.utbot.framework.concrete.UtExecutionInstrumentation -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.ConcreteExecutionFailureException -import org.utbot.framework.plugin.api.EnvironmentModels -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.Step -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -import org.utbot.framework.plugin.api.UtError -import org.utbot.framework.plugin.api.UtFailedExecution -import org.utbot.framework.plugin.api.UtInstrumentation -import org.utbot.framework.plugin.api.UtNullModel -import org.utbot.framework.plugin.api.UtOverflowFailure -import org.utbot.framework.plugin.api.UtResult -import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.util.* import org.utbot.framework.util.graph -import org.utbot.framework.plugin.api.util.isStatic -import org.utbot.framework.plugin.api.util.description -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isConstructor -import org.utbot.framework.plugin.api.util.isEnum -import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.framework.util.sootMethod -import org.utbot.fuzzer.FallbackModelProvider -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ReferencePreservingIntIdGenerator -import org.utbot.fuzzer.Trie -import org.utbot.fuzzer.TrieBasedFuzzerStatistics -import org.utbot.fuzzer.UtFuzzedExecution -import org.utbot.fuzzer.collectConstantsForFuzzer -import org.utbot.fuzzer.defaultModelMutators -import org.utbot.fuzzer.defaultModelProviders -import org.utbot.fuzzer.flipCoin -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.providers.ObjectModelProvider -import org.utbot.fuzzer.withMutations +import org.utbot.fuzzer.* +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.Trie import org.utbot.instrumentation.ConcreteExecutor import soot.jimple.Stmt import soot.tagkit.ParamNamesTag import java.lang.reflect.Method -import kotlin.random.Random import kotlin.system.measureTimeMillis -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.isActive -import kotlinx.coroutines.job -import kotlinx.coroutines.yield -import org.utbot.engine.state.ExecutionStackElement -import org.utbot.engine.state.ExecutionState -import org.utbot.engine.state.StateLabel -import org.utbot.engine.types.TypeRegistry -import org.utbot.engine.types.TypeResolver -import org.utbot.framework.plugin.api.UtExecutionSuccess -import org.utbot.framework.plugin.api.UtLambdaModel -import org.utbot.framework.plugin.api.UtSandboxFailure -import org.utbot.framework.plugin.api.util.executable -import org.utbot.framework.plugin.api.util.isAbstract -import org.utbot.fuzzer.toFuzzerType val logger = KotlinLogging.logger {} val pathLogger = KotlinLogging.logger(logger.name + ".path") @@ -387,91 +322,37 @@ class UtBotSymbolicEngine( * Run fuzzing flow. * * @param until is used by fuzzer to cancel all tasks if the current time is over this value - * @param modelProvider provides model values for a method + * @param transform provides model values for a method */ - fun fuzzing(until: Long = Long.MAX_VALUE, modelProvider: (ModelProvider) -> ModelProvider = { it }) = flow { + fun fuzzing(until: Long = Long.MAX_VALUE, transform: (JavaValueProvider) -> JavaValueProvider = { it }) = flow { val isFuzzable = methodUnderTest.parameters.all { classId -> classId != Method::class.java.id && // causes the instrumented process crash at invocation classId != Class::class.java.id // causes java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(Native Method) } - if (!isFuzzable) { + val hasMethodUnderTestParametersToFuzz = methodUnderTest.parameters.isNotEmpty() + if (!isFuzzable || !hasMethodUnderTestParametersToFuzz && methodUnderTest.isStatic) { + // Currently, fuzzer doesn't work with static methods with empty parameters return@flow } - - val fallbackModelProvider = FallbackModelProvider(defaultIdGenerator) - val constantValues = collectConstantsForFuzzer(graph) - - val syntheticMethodForFuzzingThisInstanceDescription = - FuzzedMethodDescription("thisInstance", voidClassId, listOf(classUnderTest), constantValues).apply { - className = classUnderTest.simpleName - packageName = classUnderTest.packageName - } - - val random = Random(0) - val thisInstance = when { - methodUnderTest.isStatic -> null - methodUnderTest.isConstructor -> if ( - classUnderTest.isAbstract || // can't instantiate abstract class - classUnderTest.isEnum // can't reflectively create enum objects - ) { - return@flow - } else { - null - } - else -> { - ObjectModelProvider(defaultIdGenerator).withFallback(fallbackModelProvider).generate( - syntheticMethodForFuzzingThisInstanceDescription - ).take(10).shuffled(random).map { it.value.model }.first().apply { - if (this is UtNullModel) { // it will definitely fail because of NPE, - return@flow - } - } - } - } - - val methodUnderTestDescription = FuzzedMethodDescription(methodUnderTest, collectConstantsForFuzzer(graph)).apply { - compilableName = if (!methodUnderTest.isConstructor) methodUnderTest.name else null - className = classUnderTest.simpleName - canonicalName = classUnderTest.canonicalName - isNested = classUnderTest.isNested - packageName = classUnderTest.packageName - val names = graph.body.method.tags.filterIsInstance().firstOrNull()?.names - parameterNameMap = { index -> names?.getOrNull(index) } - fuzzerType = { try { toFuzzerType(methodUnderTest.executable.genericParameterTypes[it]) } catch (_: Throwable) { null } } - shouldMock = { mockStrategy.eligibleToMock(it, classUnderTest) } - } val errorStackTraceTracker = Trie(StackTraceElement::toString) - val coveredInstructionTracker = Trie(Instruction::id) - val coveredInstructionValues = linkedMapOf, List>() var attempts = 0 val attemptsLimit = UtSettings.fuzzingMaxAttempts - val hasMethodUnderTestParametersToFuzz = methodUnderTest.parameters.isNotEmpty() - if (!hasMethodUnderTestParametersToFuzz && methodUnderTest.isStatic) { - // Currently, fuzzer doesn't work with static methods with empty parameters - return@flow - } - val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) { - fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders(defaultIdGenerator))) - } else { - // in case a method with no parameters is passed fuzzing tries to fuzz this instance with different constructors, setters and field mutators - fuzz(syntheticMethodForFuzzingThisInstanceDescription, ObjectModelProvider(defaultIdGenerator).apply { - totalLimit = 500 - }) - }.withMutations( - TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray() - ) - fuzzedValues.forEach { values -> + val names = graph.body.method.tags.filterIsInstance().firstOrNull()?.names ?: emptyList() + + runJavaFuzzing( + defaultIdGenerator, + methodUnderTest, + collectConstantsForFuzzer(graph), + names, + { mockStrategy.eligibleToMock(it, classUnderTest) }, + listOf(transform(ValueProvider.of(defaultValueProviders(defaultIdGenerator)))) + ) { thisInstance, descr, values -> if (controller.job?.isActive == false || System.currentTimeMillis() >= until) { logger.info { "Fuzzing overtime: $methodUnderTest" } - return@flow + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) } - val initialEnvironmentModels = if (hasMethodUnderTestParametersToFuzz) { - EnvironmentModels(thisInstance, values.map { it.model }, mapOf()) - } else { - check(values.size == 1 && values.first().model is UtAssembleModel) - EnvironmentModels(values.first().model, emptyList(), mapOf()) - } + val initialEnvironmentModels = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf()) val concreteExecutionResult: UtConcreteExecutionResult? = try { concreteExecutor.executeConcretely(methodUnderTest, initialEnvironmentModels, listOf()) @@ -484,30 +365,25 @@ class UtBotSymbolicEngine( } // in case an exception occurred from the concrete execution - concreteExecutionResult ?: return@forEach + concreteExecutionResult ?: return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions + var trieNode: Trie.Node? = null if (coveredInstructions.isNotEmpty()) { - val coverageKey = coveredInstructionTracker.add(coveredInstructions) - if (coverageKey.count > 1) { + trieNode = descr.tracer.add(coveredInstructions) + if (trieNode.count > 1) { if (++attempts >= attemptsLimit) { - return@flow + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) } - // Update the seeded values sometimes - // This is necessary because some values cannot do a good values in mutation in any case - if (random.flipCoin(probability = 50)) { - coveredInstructionValues[coverageKey] = values - } - return@forEach + return@runJavaFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) } - coveredInstructionValues[coverageKey] = values } else { - logger.error { "Coverage is empty for $methodUnderTest with ${values.map { it.model }}" } + logger.error { "Coverage is empty for $methodUnderTest with $values" } val result = concreteExecutionResult.result if (result is UtSandboxFailure) { val stackTraceElements = result.exception.stackTrace.reversed() if (errorStackTraceTracker.add(stackTraceElements).count > 1) { - return@forEach + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) } } } @@ -519,9 +395,11 @@ class UtBotSymbolicEngine( result = concreteExecutionResult.result, coverage = concreteExecutionResult.coverage, fuzzingValues = values, - fuzzedMethodDescription = methodUnderTestDescription + fuzzedMethodDescription = descr.description ) ) + + BaseFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt index ddbf6593bc..0ba2272692 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt @@ -31,9 +31,11 @@ import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.framework.plugin.api.util.wrapperByPrimitive import org.utbot.framework.plugin.services.JdkInfoDefaultProvider +import org.utbot.fuzzer.FuzzedType import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.execute import kotlin.reflect.jvm.kotlinFunction @@ -160,14 +162,10 @@ object UtBotJavaApi { } ?.map { UtPrimitiveModel(it) } ?: emptySequence() - val customModelProvider = ModelProvider { description -> + val customModelProvider = ValueProvider { _, type -> sequence { - description.parametersMap.forEach { (classId, indices) -> - createPrimitiveModels(primitiveValuesSupplier, classId).forEach { model -> - indices.forEach { index -> - yieldValue(index, FuzzedValue(model)) - } - } + createPrimitiveModels(primitiveValuesSupplier, type.classId).forEach { model -> + yield(Seed.Simple(FuzzedValue(model))) } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt index 656b0c1655..8bae40fd8a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt @@ -1,17 +1,14 @@ package org.utbot.fuzzer import mu.KotlinLogging -import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.byteClassId import org.utbot.framework.plugin.api.util.charClassId import org.utbot.framework.plugin.api.util.doubleClassId import org.utbot.framework.plugin.api.util.floatClassId -import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.intClassId import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.framework.util.executableId @@ -49,11 +46,6 @@ import soot.jimple.internal.JStaticInvokeExpr import soot.jimple.internal.JTableSwitchStmt import soot.jimple.internal.JVirtualInvokeExpr import soot.toolkits.graph.ExceptionalUnitGraph -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.TypeVariable -import java.lang.reflect.WildcardType private val logger = KotlinLogging.logger {} @@ -299,24 +291,4 @@ private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) { else -> FuzzedContext.Unknown } -private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() - -fun toFuzzerType(type: Type): FuzzedType { - return when (type) { - is WildcardType -> type.upperBounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) - is TypeVariable<*> -> type.bounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) - is ParameterizedType -> FuzzedType((type.rawType as Class<*>).id, type.actualTypeArguments.map { toFuzzerType(it) }) - is GenericArrayType -> { - val genericComponentType = type.genericComponentType - val fuzzerType = toFuzzerType(genericComponentType) - val classId = if (genericComponentType !is GenericArrayType) { - ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId) - } else { - ClassId("[" + fuzzerType.classId.name, fuzzerType.classId) - } - FuzzedType(classId) - } - is Class<*> -> FuzzedType(type.id) - else -> error("Unknown type: $type") - } -} \ No newline at end of file +private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() \ No newline at end of file diff --git a/utbot-fuzzers/build.gradle.kts b/utbot-fuzzers/build.gradle.kts index ee77190275..8486cf8279 100644 --- a/utbot-fuzzers/build.gradle.kts +++ b/utbot-fuzzers/build.gradle.kts @@ -17,6 +17,7 @@ tasks { dependencies { implementation(project(":utbot-framework-api")) + api(project(":utbot-fuzzing")) implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { exclude(group="com.google.guava", module="guava") diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt index a46080cba2..b70c714b40 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.ClassId * * @see ClassId.typeParameters */ -class FuzzedType( +data class FuzzedType( val classId: ClassId, - val generics: List = emptyList() + val generics: List = emptyList(), ) \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt index 1da79b63e8..d5d4a6d7f2 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -18,6 +18,5 @@ import org.utbot.framework.plugin.api.UtModel */ open class FuzzedValue( val model: UtModel, - val createdBy: ModelProvider? = null, var summary: String? = null, ) \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt index e9ebfca2e9..fd61469f8a 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -27,6 +27,7 @@ import kotlin.random.Random import org.utbot.fuzzer.providers.DateConstantModelProvider import org.utbot.fuzzer.providers.PrimitiveRandomModelProvider import org.utbot.fuzzer.providers.RecursiveModelProvider +import org.utbot.fuzzing.utils.CartesianProduct private val logger by lazy { KotlinLogging.logger {} } @@ -119,7 +120,7 @@ fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvi modelProviders.forEach { fuzzingProvider -> fuzzingProvider.generate(description).forEach { (index, model) -> val mock = replaceWithMock(model.model, description.shouldMock) - values[index].add(FuzzedValue(mock, model.createdBy).apply { + values[index].add(FuzzedValue(mock).apply { summary = model.summary }) } diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt index 521fa7457d..2328362ce3 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt @@ -1,5 +1,7 @@ package org.utbot.fuzzer +import org.utbot.fuzzing.utils.Trie +import org.utbot.fuzzing.utils.chooseOne import kotlin.math.pow import kotlin.random.Random diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt index 486a583ad9..dc80cf4997 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt @@ -39,6 +39,6 @@ interface ModelMutator { ) : FuzzedValue? fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue { - return FuzzedValue(this, template.createdBy).apply(block) + return FuzzedValue(this).apply(block) } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt index 4b28dc0dcf..540f73e57a 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt @@ -156,7 +156,7 @@ fun interface ModelProvider { } } - fun UtModel.fuzzed(block: FuzzedValue.() -> Unit = {}): FuzzedValue = FuzzedValue(this, this@ModelProvider).apply(block) + fun UtModel.fuzzed(block: FuzzedValue.() -> Unit = {}): FuzzedValue = FuzzedValue(this).apply(block) } inline fun ModelProvider.exceptIsInstance(): ModelProvider { diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt index 9634d46112..f00401bd4c 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt @@ -4,7 +4,7 @@ import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelMutator -import org.utbot.fuzzer.invertBit +import org.utbot.fuzzing.utils.invertBit import kotlin.random.Random /** diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt index 3274fb0254..8bf71f8afa 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt @@ -5,7 +5,7 @@ import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelMutator -import org.utbot.fuzzer.flipCoin +import org.utbot.fuzzing.utils.flipCoin import kotlin.random.Random /** diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt index d17292fe55..d90f6bc1be 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt @@ -13,7 +13,7 @@ import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.hex +import org.utbot.fuzzing.utils.hex fun ModelProvider.assembleModel(id: Int, constructorId: ConstructorId, params: List): FuzzedValue { diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt index f5f403b610..9a7326cd9f 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt @@ -5,7 +5,6 @@ import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.util.dateClassId import org.utbot.framework.plugin.api.util.executableId import org.utbot.framework.plugin.api.util.id @@ -23,7 +22,7 @@ import org.utbot.fuzzer.ModelProvider import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues import org.utbot.fuzzer.defaultModelProviders import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.hex +import org.utbot.fuzzing.utils.hex import org.utbot.fuzzer.objects.assembleModel class DateConstantModelProvider( diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt index 6bdbf2891f..745c055b69 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt @@ -1,14 +1,6 @@ package org.utbot.fuzzer.providers import java.lang.reflect.Constructor -import java.lang.reflect.Field -import java.lang.reflect.Member -import java.lang.reflect.Method -import java.lang.reflect.Modifier.isFinal -import java.lang.reflect.Modifier.isPrivate -import java.lang.reflect.Modifier.isProtected -import java.lang.reflect.Modifier.isPublic -import java.lang.reflect.Modifier.isStatic import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.FieldId @@ -31,6 +23,9 @@ import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.IdentityPreservingIdGenerator import org.utbot.fuzzer.objects.FuzzerMockableMethodId import org.utbot.fuzzer.objects.assembleModel +import org.utbot.fuzzing.providers.FieldDescription +import org.utbot.fuzzing.providers.findAccessibleModifableFields +import org.utbot.fuzzing.providers.isAccessible /** * Creates [UtAssembleModel] for objects which have public constructors @@ -69,7 +64,7 @@ class ObjectModelProvider( // and mutate some fields. Only if there's no option next block // with empty constructor should be used. if (constructorId.parameters.isEmpty()) { - val fields = findSuitableFields(constructorId.classId, description) + val fields = findAccessibleModifableFields(constructorId.classId, description.packageName) if (fields.isNotEmpty()) { yield( ModelConstructor(fields.map { FuzzedType(it.classId) }) { @@ -143,54 +138,6 @@ class ObjectModelProvider( } } - private fun isAccessible(member: Member, packageName: String?): Boolean { - return isPublic(member.modifiers) || - (packageName != null && isPackagePrivate(member.modifiers) && member.declaringClass.`package`?.name == packageName) - } - - private fun isPackagePrivate(modifiers: Int): Boolean { - val hasAnyAccessModifier = isPrivate(modifiers) - || isProtected(modifiers) - || isProtected(modifiers) - return !hasAnyAccessModifier - } - - private fun findSuitableFields(classId: ClassId, description: FuzzedMethodDescription): List { - val jClass = classId.jClass - return jClass.declaredFields.map { field -> - val setterAndGetter = jClass.findPublicSetterGetterIfHasPublicGetter(field, description) - FieldDescription( - name = field.name, - classId = field.type.id, - canBeSetDirectly = isAccessible(field, description.packageName) && !isFinal(field.modifiers) && !isStatic(field.modifiers), - setter = setterAndGetter?.setter, - getter = setterAndGetter?.getter, - ) - } - } - - private class PublicSetterGetter( - val setter: Method, - val getter: Method, - ) - - private fun Class<*>.findPublicSetterGetterIfHasPublicGetter(field: Field, description: FuzzedMethodDescription): PublicSetterGetter? { - val postfixName = field.name.capitalize() - val setterName = "set$postfixName" - val getterName = "get$postfixName" - val getter = try { getDeclaredMethod(getterName) } catch (_: NoSuchMethodException) { return null } - return if (isAccessible(getter, description.packageName) && getter.returnType == field.type) { - declaredMethods.find { - isAccessible(it, description.packageName) && - it.name == setterName && - it.parameterCount == 1 && - it.parameterTypes[0] == field.type - }?.let { PublicSetterGetter(it, getter) } - } else { - null - } - } - private val primitiveParameterizedConstructorsFirstAndThenByParameterCount = compareByDescending { constructorId -> constructorId.parameters.all { classId -> @@ -199,13 +146,5 @@ class ObjectModelProvider( }.thenComparingInt { constructorId -> constructorId.parameters.size } - - private class FieldDescription( - val name: String, - val classId: ClassId, - val canBeSetDirectly: Boolean, - val setter: Method?, - val getter: Method? - ) } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt index b2b56440b4..acabdff57c 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt @@ -65,4 +65,4 @@ object RegexModelProvider : ModelProvider { } } -class RegexFuzzedValue(value: FuzzedValue, val regex: String) : FuzzedValue(value.model, value.createdBy) \ No newline at end of file +class RegexFuzzedValue(value: FuzzedValue, val regex: String) : FuzzedValue(value.model) \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt new file mode 100644 index 0000000000..4bc4cd4349 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -0,0 +1,122 @@ +package org.utbot.fuzzing + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.* +import org.utbot.fuzzing.providers.* +import org.utbot.fuzzing.utils.Trie +import java.lang.reflect.* + +typealias JavaValueProvider = ValueProvider + +class FuzzedDescription( + val description: FuzzedMethodDescription, + val tracer: Trie, +) : Description( + description.parameters.mapIndexed { index, classId -> + description.fuzzerType(index) ?: FuzzedType(classId) + } +) { + val constants: Sequence + get() = description.concreteValues.asSequence() +} + +fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( + BooleanValueProvider, + IntegerValueProvider, + FloatValueProvider, + StringValueProvider, + NumberValueProvider, + ObjectValueProvider(idGenerator), + ArrayValueProvider(idGenerator), + EnumValueProvider(idGenerator), + ListSetValueProvider(idGenerator), + MapValueProvider(idGenerator), + EmptyCollectionValueProvider(idGenerator), + DateValueProvider(idGenerator), +// NullValueProvider, +) + +suspend fun runJavaFuzzing( + idGenerator: IdentityPreservingIdGenerator, + methodUnderTest: ExecutableId, + constants: Collection, + names: List, + mock: (ClassId) -> Boolean = { false }, + providers: List> = defaultValueProviders(idGenerator), + exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> +) { + val classUnderTest = methodUnderTest.classId + val name = methodUnderTest.classId.simpleName + "." + methodUnderTest.name + val returnType = methodUnderTest.returnType + val parameters = methodUnderTest.parameters + + /** + * To fuzz this instance the class of it is added into head of parameters list. + * Done for compatibility with old fuzzer logic and should be reworked more robust way. + */ + fun createFuzzedMethodDescription(self: ClassId?) = FuzzedMethodDescription( + name, returnType, listOfNotNull(self) + parameters, constants + ).apply { + compilableName = if (!methodUnderTest.isConstructor) methodUnderTest.name else null + className = classUnderTest.simpleName + canonicalName = classUnderTest.canonicalName + isNested = classUnderTest.isNested + packageName = classUnderTest.packageName + parameterNameMap = { index -> + when { + self != null && index == 0 -> "this" + self != null -> names.getOrNull(index - 1) + else -> names.getOrNull(index) + } + } + fuzzerType = { + try { + when { + self != null && it == 0 -> toFuzzerType(methodUnderTest.executable.declaringClass) + self != null -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it - 1]) + else -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it]) + } + } catch (_: Throwable) { + null + } + } + shouldMock = mock + } + + val thisInstance = with(methodUnderTest) { + if (!isStatic && !isConstructor) { classUnderTest } else { null } + } + val tracer = Trie(Instruction::id) + val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer) + val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer) + runFuzzing(ValueProvider.of(providers), descriptionWithOptionalThisInstance) { _, t -> + if (thisInstance == null) { + exec(null, descriptionWithOnlyParameters, t) + } else { + exec(t.first(), descriptionWithOnlyParameters, t.drop(1)) + } + } +} + +private fun toFuzzerType(type: Type): FuzzedType { + return when (type) { + is WildcardType -> type.upperBounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) + is TypeVariable<*> -> type.bounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) + is ParameterizedType -> FuzzedType((type.rawType as Class<*>).id, type.actualTypeArguments.map { toFuzzerType(it) }) + is GenericArrayType -> { + val genericComponentType = type.genericComponentType + val fuzzerType = toFuzzerType(genericComponentType) + val classId = if (genericComponentType !is GenericArrayType) { + ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId) + } else { + ClassId("[" + fuzzerType.classId.name, fuzzerType.classId) + } + FuzzedType(classId) + } + is Class<*> -> FuzzedType(type.id, type.typeParameters.map { toFuzzerType(it) }) + else -> error("Unknown type: $type") + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt new file mode 100644 index 0000000000..8af39a4ce8 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt @@ -0,0 +1,43 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdGenerator +import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +class ArrayValueProvider( + val idGenerator: IdGenerator, +) : ValueProvider { + + override fun accept(type: FuzzedType) = type.classId.isArray + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtArrayModel( + id = idGenerator.createId(), + classId = type.classId, + length = it, + constModel = type.classId.elementClassId!!.defaultValueModel(), + stores = hashMapOf(), + ).fuzzed { + summary = "%var% = ${type.classId.elementClassId!!.simpleName}[$it]" + } + }, + modify = Routine.ForEach(listOf(FuzzedType(type.classId.elementClassId!!))) { self, i, values -> + (self.model as UtArrayModel).stores[i] = values.first().model + } + )) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt new file mode 100644 index 0000000000..b77d04850f --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt @@ -0,0 +1,208 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdGenerator +import org.utbot.fuzzer.providers.PrimitivesModelProvider.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import kotlin.reflect.KClass + +class EmptyCollectionValueProvider( + val idGenerator: IdGenerator +) : ValueProvider { + private class Info(val classId: ClassId, val methodName: String, val returnType: ClassId = classId) + + private val unmodifiableCollections = listOf( + Info(java.util.List::class.id, "emptyList"), + Info(java.util.Set::class.id, "emptySet"), + Info(java.util.Map::class.id, "emptyMap"), + Info(java.util.Collection::class.id, "emptyList", returnType = java.util.List::class.id), + Info(java.lang.Iterable::class.id, "emptyList", returnType = java.util.List::class.id), + Info(java.util.Iterator::class.id, "emptyIterator"), + ) + + private val emptyCollections = listOf( + Info(java.util.NavigableSet::class.id, "constructor", java.util.TreeSet::class.id), + Info(java.util.SortedSet::class.id, "constructor", java.util.TreeSet::class.id), + Info(java.util.NavigableMap::class.id, "constructor", java.util.TreeMap::class.id), + Info(java.util.SortedMap::class.id, "constructor", java.util.TreeMap::class.id), + ) + + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence { + unmodifiableCollections + .asSequence() + .filter { it.classId == type.classId } + .forEach { info -> + yieldWith(info.classId, MethodId(java.util.Collections::class.id, info.methodName, info.returnType, emptyList())) + } + emptyCollections + .asSequence() + .filter { it.classId == type.classId } + .forEach { info -> + yieldWith(info.classId, ConstructorId(info.returnType, emptyList())) + } + } + + private suspend fun SequenceScope>.yieldWith(classId: ClassId, executableId: ExecutableId) { + yield(Seed.Recursive( + construct = Routine.Create(executableId.parameters.map { FuzzedType(it) }) { value -> + UtAssembleModel( + id = idGenerator.createId(), + classId = classId, + modelName = "", + instantiationCall = UtExecutableCallModel(null, executableId, value.map { it.model }) + + ).fuzzed() + }, + empty = Routine.Empty { UtNullModel(classId).fuzzed { summary = "%var% = null" } } + )) + } +} + +class MapValueProvider( + idGenerator: IdGenerator +) : CollectionValueProvider(idGenerator, java.util.Map::class.id) { + override fun resolveType(description: FuzzedDescription, type: FuzzedType) = sequence { + val keyGeneric = type.generics.getOrNull(0) ?: FuzzedType(objectClassId) + val valueGeneric = type.generics.getOrNull(1) ?: FuzzedType(objectClassId) + when (type.classId) { + java.util.Map::class.id -> { + if (keyGeneric.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeMap::class.id, listOf(keyGeneric, valueGeneric))) + } + yield(FuzzedType(java.util.HashMap::class.id, listOf(keyGeneric, valueGeneric))) + } + java.util.NavigableMap::class.id, + java.util.SortedMap::class.id -> { + if (keyGeneric.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeMap::class.id, listOf(keyGeneric, valueGeneric))) + } + } + else -> yieldConcreteClass(FuzzedType(type.classId, listOf(keyGeneric, valueGeneric))) + } + } + + override fun findMethod(resolvedType: FuzzedType, values: List): MethodId { + return MethodId(resolvedType.classId, "put", objectClassId, listOf(objectClassId, objectClassId)) + } +} + +class ListSetValueProvider( + idGenerator: IdGenerator +) : CollectionValueProvider(idGenerator, java.lang.Iterable::class.id) { + override fun resolveType(description: FuzzedDescription, type: FuzzedType) = sequence { + val generic = type.generics.firstOrNull() ?: FuzzedType(objectClassId) + when (type.classId) { + java.lang.Iterable::class.id -> { + yield(FuzzedType(java.util.ArrayList::class.id, listOf(generic))) + yield(FuzzedType(java.util.HashSet::class.id, listOf(generic))) + } + java.util.Queue::class.id, + java.util.Deque::class.id-> { + yield(FuzzedType(java.util.ArrayDeque::class.id, listOf(generic))) + } + java.util.List::class.id -> { + yield(FuzzedType(java.util.ArrayList::class.id, listOf(generic))) + yield(FuzzedType(java.util.LinkedList::class.id, listOf(generic))) + } + java.util.Collection::class.id -> { + yield(FuzzedType(java.util.ArrayList::class.id, listOf(generic))) + yield(FuzzedType(java.util.HashSet::class.id, listOf(generic))) + } + java.util.Set::class.id -> { + if (generic.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeSet::class.id, listOf(generic))) + } + yield(FuzzedType(java.util.HashSet::class.id, listOf(generic))) + } + java.util.NavigableSet::class.id, + java.util.SortedSet::class.id -> { + if (generic.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeSet::class.id, listOf(generic))) + } + } + else -> yieldConcreteClass(FuzzedType(type.classId, listOf(generic))) + } + } + + override fun findMethod(resolvedType: FuzzedType, values: List): MethodId { + return MethodId(resolvedType.classId, "add", booleanClassId, listOf(objectClassId)) + } +} + +/** + * Accepts only instances of Collection or Map + */ +abstract class CollectionValueProvider( + private val idGenerator: IdGenerator, + vararg acceptableCollectionTypes: ClassId +) : ValueProvider { + + private val acceptableCollectionTypes = acceptableCollectionTypes.toList() + + override fun accept(type: FuzzedType): Boolean { + return with (type.classId) { + acceptableCollectionTypes.any { acceptableCollectionType -> + isSubtypeOf(acceptableCollectionType.kClass) + } + } + } + + protected suspend fun SequenceScope.yieldConcreteClass(type: FuzzedType) { + if (with (type.classId) { !isAbstract && isPublic && (!isInner || isStatic) }) { + val emptyConstructor = type.classId.allConstructors.find { it.parameters.isEmpty() } + if (emptyConstructor != null && emptyConstructor.isPublic) { + yield(type) + } + } + } + + /** + * Types should be resolved with type parameters + */ + abstract fun resolveType(description: FuzzedDescription, type: FuzzedType) : Sequence + + abstract fun findMethod(resolvedType: FuzzedType, values: List) : MethodId + + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + resolveType(description, type).forEach { resolvedType -> + val typeParameter = resolvedType.generics + yield(Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = idGenerator.createId(), + classId = resolvedType.classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(resolvedType.classId, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ).fuzzed { + summary = "%var% = collection" + } + }, + modify = Routine.ForEach(typeParameter) { self, _, values -> + val model = self.model as UtAssembleModel + (model.modificationsChain as MutableList) += UtExecutableCallModel( + model, + findMethod(resolvedType, values), + values.map { it.model } + ) + } + )) + } + } + + protected infix fun ClassId.isSubtypeOf(klass: KClass<*>): Boolean { + // commented code above doesn't work this case: SomeList extends LinkedList {} and Collection +// return isSubtypeOf(another) + return klass.java.isAssignableFrom(this.jClass) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt new file mode 100644 index 0000000000..e7467e6b88 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt @@ -0,0 +1,30 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.util.isEnum +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdentityPreservingIdGenerator +import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +class EnumValueProvider( + val idGenerator: IdentityPreservingIdGenerator, +) : ValueProvider { + override fun accept(type: FuzzedType) = type.classId.isEnum + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence> { + type.classId.jClass.enumConstants.filterIsInstance>().forEach { enum -> + val id = idGenerator.getOrCreateIdForValue(enum) + yield(Seed.Simple(UtEnumConstantModel(id, type.classId, enum).fuzzed { + summary = "%var% = $enum" + })) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt new file mode 100644 index 0000000000..0e1ca84ab7 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt @@ -0,0 +1,198 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdGenerator +import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.hex +import java.lang.reflect.Field +import java.lang.reflect.Member +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +class ObjectValueProvider( + val idGenerator: IdGenerator, +) : ValueProvider { + + private val unwantedConstructorsClasses = listOf( + stringClassId, + dateClassId, + NumberValueProvider.classId + ) + + override fun accept(type: FuzzedType) = !isIgnored(type.classId) + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + val classId = type.classId + val constructors = findTypesOfNonRecursiveConstructor(type, description.description.packageName) + .takeIf { it.isNotEmpty() } + ?.asSequence() + ?: classId.allConstructors.filter { + isAccessible(it.constructor, description.description.packageName) + } + constructors.forEach { constructorId -> + yield(createValue(classId, constructorId, description)) + } + } + + private fun createValue(classId: ClassId, constructorId: ConstructorId, description: FuzzedDescription): Seed.Recursive { + return Seed.Recursive( + construct = Routine.Create(constructorId.parameters.map { FuzzedType(it) }) { values -> + val id = idGenerator.createId() + UtAssembleModel( + id = id, + classId = classId, + modelName = "${constructorId.classId.name}${constructorId.parameters}#" + id.hex(), + instantiationCall = UtExecutableCallModel( + null, + constructorId, + values.map { it.model }), + modificationsChainProvider = { mutableListOf() } + ).fuzzed { + summary = "%var% = ${classId.simpleName}(${constructorId.parameters.joinToString { it.simpleName }})" + } + }, + modify = sequence { + findAccessibleModifableFields(classId, description.description.packageName).forEach { fd -> + when { + fd.canBeSetDirectly -> { + yield(Routine.Call(listOf(FuzzedType(fd.classId))) { self, values -> + val model = self.model as UtAssembleModel + model.modificationsChain as MutableList += UtDirectSetFieldModel( + model, + FieldId(classId, fd.name), + values.first().model + ) + }) + } + + fd.setter != null && fd.getter != null -> { + yield(Routine.Call(listOf(FuzzedType(fd.classId))) { self, values -> + val model = self.model as UtAssembleModel + model.modificationsChain as MutableList += UtExecutableCallModel( + model, + fd.setter.executableId, + values.map { it.model }) + }) + } + } + } + }, + empty = Routine.Empty { + UtNullModel(classId).fuzzed { + summary = "%var% = null" + } + } + ) + } + + private fun isIgnored(type: ClassId): Boolean { + return unwantedConstructorsClasses.contains(type) + || type.isIterableOrMap + || type.isPrimitiveWrapper + || type.isEnum + || type.isAbstract + || (type.isInner && !type.isStatic) + } + + private fun isAccessible(member: Member, packageName: String?): Boolean { + return Modifier.isPublic(member.modifiers) || + (packageName != null && isPackagePrivate(member.modifiers) && member.declaringClass.`package`?.name == packageName) + } + + private fun isPackagePrivate(modifiers: Int): Boolean { + val hasAnyAccessModifier = Modifier.isPrivate(modifiers) + || Modifier.isProtected(modifiers) + || Modifier.isProtected(modifiers) + return !hasAnyAccessModifier + } + + private fun findTypesOfNonRecursiveConstructor(type: FuzzedType, packageName: String?): List { + return type.classId.allConstructors + .filter { isAccessible(it.constructor, packageName) } + .filter { c -> + c.parameters.all { it.isPrimitive || it == stringClassId || it.isArray } + }.toList() + } +} + +@Suppress("unused") +object NullValueProvider : ValueProvider { + override fun accept(type: FuzzedType) = type.classId.isRefType + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequenceOf>( + Seed.Simple(UtNullModel(type.classId).fuzzed { + summary = "%var% = null" + }) + ) +} + +internal class PublicSetterGetter( + val setter: Method, + val getter: Method, +) + +internal class FieldDescription( + val name: String, + val classId: ClassId, + val canBeSetDirectly: Boolean, + val setter: Method?, + val getter: Method? +) + +internal fun findAccessibleModifableFields(classId: ClassId, packageName: String?): List { + val jClass = classId.jClass + return jClass.declaredFields.map { field -> + val setterAndGetter = jClass.findPublicSetterGetterIfHasPublicGetter(field, packageName) + FieldDescription( + name = field.name, + classId = field.type.id, + canBeSetDirectly = isAccessible( + field, + packageName + ) && !Modifier.isFinal(field.modifiers) && !Modifier.isStatic(field.modifiers), + setter = setterAndGetter?.setter, + getter = setterAndGetter?.getter, + ) + } +} + +internal fun Class<*>.findPublicSetterGetterIfHasPublicGetter(field: Field, packageName: String?): PublicSetterGetter? { + @Suppress("DEPRECATION") val postfixName = field.name.capitalize() + val setterName = "set$postfixName" + val getterName = "get$postfixName" + val getter = try { getDeclaredMethod(getterName) } catch (_: NoSuchMethodException) { return null } + return if (isAccessible(getter, packageName) && getter.returnType == field.type) { + declaredMethods.find { + isAccessible(it, packageName) && + it.name == setterName && + it.parameterCount == 1 && + it.parameterTypes[0] == field.type + }?.let { PublicSetterGetter(it, getter) } + } else { + null + } +} + + + +internal fun isAccessible(member: Member, packageName: String?): Boolean { + return Modifier.isPublic(member.modifiers) || + (packageName != null && isPackagePrivate(member.modifiers) && member.declaringClass.`package`?.name == packageName) +} + +internal fun isPackagePrivate(modifiers: Int): Boolean { + val hasAnyAccessModifier = Modifier.isPrivate(modifiers) + || Modifier.isProtected(modifiers) + || Modifier.isProtected(modifiers) + return !hasAnyAccessModifier +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt new file mode 100644 index 0000000000..19a37137ec --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt @@ -0,0 +1,140 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.IdGenerator +import org.utbot.fuzzer.providers.PrimitivesModelProvider.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.hex +import java.text.SimpleDateFormat +import java.util.* + +abstract class ClassValueProvider( + val classId: ClassId +) : ValueProvider { + override fun accept(type: FuzzedType) = type.classId == classId +} + +object NumberValueProvider : ClassValueProvider(Number::class.id) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + listOf( + byteClassId, shortClassId, intClassId, longClassId, floatClassId, doubleClassId + ).forEach { numberPrimitiveType -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(FuzzedType(numberPrimitiveType))) { v -> v.first() }, + empty = Routine.Empty { UtNullModel(type.classId).fuzzed() } + )) + } + } +} + +class DateValueProvider( + private val idGenerator: IdGenerator +) : ClassValueProvider(Date::class.id) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + // now date + val nowDateModel = UtAssembleModel( + id = idGenerator.createId(), + classId = type.classId, + modelName = "Date::now", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = type.classId.allConstructors.firstOrNull { it.parameters.isEmpty() } + ?: error("Cannot find default constructor of ${type.classId}"), + params = emptyList()) + ).fuzzed { } + yield(Seed.Simple(nowDateModel)) + + // from string dates + val strings = description.constants + .filter { + it.classId == stringClassId + } + .map { it.value } + .filterIsInstance() + .distinct() + val dateFormats = strings + .filter { it.isDateFormat() } + defaultDateFormat + val formatToDates = dateFormats.associateWith { format -> strings.filter { it.isDate(format) } } + formatToDates.forEach { (format, dates) -> + dates.forEach { date -> + yield(Seed.Simple( + assembleDateFromString(idGenerator.createId(), format, date) + )) + } + } + + // from numbers + type.classId.allConstructors + .filter { + it.parameters.isNotEmpty() && it.parameters.all { p -> p == intClassId || p == longClassId } + } + .forEach { constructor -> + yield(Seed.Recursive( + construct = Routine.Create(constructor.parameters.map { FuzzedType(it) }) { values -> + UtAssembleModel( + id = idGenerator.createId(), + classId = type.classId, + modelName = "Date(${values.map { it.model.classId }})", + instantiationCall = UtExecutableCallModel(null, constructor, values.map { it.model }) + ).fuzzed { } + }, + empty = Routine.Empty { nowDateModel } + )) + } + } + + companion object { + private const val defaultDateFormat = "dd-MM-yyyy HH:mm:ss:ms" + } + + private fun String.isDate(format: String): Boolean { + val formatter = SimpleDateFormat(format).apply { + isLenient = false + } + return runCatching { formatter.parse(trim()) }.isSuccess + } + + private fun String.isDateFormat(): Boolean { + return none { it.isDigit() } && // fixes concrete date values + runCatching { SimpleDateFormat(this) }.isSuccess + } + + private fun assembleDateFromString(id: Int, formatString: String, dateString: String): FuzzedValue { + val simpleDateFormatModel = assembleSimpleDateFormat(idGenerator.createId(), formatString) + val dateFormatParse = simpleDateFormatModel.classId.jClass + .getMethod("parse", String::class.java).executableId + val instantiationCall = UtExecutableCallModel( + simpleDateFormatModel, dateFormatParse, listOf(UtPrimitiveModel(dateString)) + ) + return UtAssembleModel( + id, + dateClassId, + "$dateFormatParse#" + id.hex(), + instantiationCall + ).fuzzed { + summary = "%var% = $dateFormatParse($stringClassId)" + } + } + + private fun assembleSimpleDateFormat(id: Int, formatString: String): UtAssembleModel { + val simpleDateFormatId = SimpleDateFormat::class.java.id + val formatStringConstructor = simpleDateFormatId.allConstructors.first { + it.parameters.singleOrNull() == stringClassId + } + val formatSetLenient = SimpleDateFormat::setLenient.executableId + val formatModel = UtPrimitiveModel(formatString) + + val instantiationCall = UtExecutableCallModel(instance = null, formatStringConstructor, listOf(formatModel)) + return UtAssembleModel( + id, + simpleDateFormatId, + "$simpleDateFormatId[$stringClassId]#" + id.hex(), + instantiationCall + ) { + listOf(UtExecutableCallModel(instance = this, formatSetLenient, listOf(UtPrimitiveModel(false)))) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt new file mode 100644 index 0000000000..f08736b073 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt @@ -0,0 +1,245 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedContext.Comparison.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.* +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException +import kotlin.random.Random + +abstract class PrimitiveValueProvider( + vararg acceptableTypes: ClassId +) : ValueProvider { + protected val acceptableTypes = acceptableTypes.toSet() + + final override fun accept(type: FuzzedType) = type.classId in acceptableTypes + + protected suspend fun SequenceScope>.yieldKnown( + value: T, + toValue: T.() -> Any + ) { + yield(Seed.Known(value) { known -> + UtPrimitiveModel(toValue(known)).fuzzed { + summary = buildString { + append("%var% = ${known.valueToString()}") + if (known.mutatedFrom != null) { + append(" (mutated from ${known.mutatedFrom?.valueToString()})") + } + } + } + }) + } + + private fun T.valueToString(): String { + when (this) { + is BitVectorValue -> { + for (defaultBound in Signed.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase() + } + } + return when (size) { + 8 -> toByte().toString() + 16 -> toShort().toString() + 32 -> toInt().toString() + 64 -> toLong().toString() + else -> toString(10) + } + } + is IEEE754Value -> { + for (defaultBound in DefaultFloatBound.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase().replace("_", " ") + } + } + return when { + is32Float() -> toFloat().toString() + is64Float() -> toDouble().toString() + else -> toString() + } + } + is StringValue -> { + return "'${value.substringToLength(10, "...")}'" + } + is RegexValue -> { + return "'${value.substringToLength(10, "...")}' from $pattern" + } + else -> return toString() + } + } + + private fun String.substringToLength(size: Int, postfix: String): String { + return when { + length <= size -> this + else -> substring(0, size) + postfix + } + } +} + +object BooleanValueProvider : PrimitiveValueProvider(booleanClassId, booleanWrapperClassId) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence { + yieldKnown(Bool.TRUE()) { toBoolean() } + yieldKnown(Bool.FALSE()) { toBoolean() } + } +} + +object IntegerValueProvider : PrimitiveValueProvider( + charClassId, charWrapperClassId, + byteClassId, byteWrapperClassId, + shortClassId, shortWrapperClassId, + intClassId, intWrapperClassId, + longClassId, longWrapperClassId, +) { + + private val ClassId.typeSize: Int + get() = when (this) { + charClassId, charWrapperClassId -> 7 + byteClassId, byteWrapperClassId -> 8 + shortClassId, shortWrapperClassId -> 16 + intClassId, intWrapperClassId -> 32 + longClassId, longWrapperClassId -> 64 + else -> error("unknown type $this") + } + + /** + * Returns null when values cannot be cast. + */ + private fun ClassId.tryCast(value: BitVectorValue): Any? = when (this) { + charClassId, charWrapperClassId -> value.takeIf { typeSize <= charClassId.typeSize }?.toCharacter() + byteClassId, byteWrapperClassId -> value.takeIf { typeSize <= byteClassId.typeSize }?.toByte() + shortClassId, shortWrapperClassId -> value.takeIf { typeSize <= shortClassId.typeSize }?.toShort() + intClassId, intWrapperClassId -> value.takeIf { typeSize <= intClassId.typeSize }?.toInt() + longClassId, longWrapperClassId -> value.takeIf { typeSize <= longClassId.typeSize }?.toLong() + else -> error("unknown type $this") + } + + private fun ClassId.cast(value: BitVectorValue): Any = tryCast(value)!! + + private val randomStubWithNoUsage = Random(0) + private val configurationStubWithNoUsage = Configuration() + + private fun BitVectorValue.change(func: BitVectorValue.() -> Unit): BitVectorValue { + return Mutation { _, _, _ -> + BitVectorValue(this).apply { func() } + }.mutate(this, randomStubWithNoUsage, configurationStubWithNoUsage) as BitVectorValue + } + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + description.constants.forEach { (t, v, c) -> + if (t in acceptableTypes) { + val value = BitVectorValue.fromValue(v) + val values = listOfNotNull( + value, + when (c) { + EQ, NE, LE, GT -> value.change { inc() } + LT, GE -> value.change { dec() } + else -> null + } + ) + values.forEach { + if (type.classId.tryCast(it) != null) { + yieldKnown(it) { + type.classId.cast(this) + } + } + } + + } + } + Signed.values().forEach { bound -> + val s = type.classId.typeSize + val value = bound(s) + if (type.classId.tryCast(value) != null) { + yieldKnown(value) { + type.classId.cast(this) + } + } + } + } +} + +object FloatValueProvider : PrimitiveValueProvider( + floatClassId, floatWrapperClassId, + doubleClassId, doubleWrapperClassId, +) { + private val ClassId.typeSize: Pair + get() = when (this) { + floatClassId, floatWrapperClassId -> 23 to 8 + doubleClassId, doubleWrapperClassId -> 52 to 11 + else -> error("unknown type $this") + } + + /** + * Returns null when values cannot be cast. + */ + private fun ClassId.cast(value: IEEE754Value): Any = when (this) { + floatClassId, floatWrapperClassId -> value.toFloat() + doubleClassId, doubleWrapperClassId -> value.toDouble() + else -> error("unknown type $this") + } + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + description.constants.forEach { (t, v, _) -> + if (t in acceptableTypes) { + yieldKnown(IEEE754Value.fromValue(v)) { type.classId.cast(this) } + } + } + DefaultFloatBound.values().forEach { bound -> + val (m, e) = type.classId.typeSize + yieldKnown(bound(m ,e)) { + type.classId.cast(this) + } + } + } +} + +object StringValueProvider : PrimitiveValueProvider(stringClassId) { + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + val constants = description.constants + .filter { it.classId == stringClassId } + val values = constants + .mapNotNull { it.value as? String } + + sequenceOf("", "abc", "\n\t\r") + values.forEach { yieldKnown(StringValue(it)) { value } } + constants + .filter { it.fuzzedContext.isPatterMatchingContext() } + .map { it.value as String } + .distinct() + .filter { it.isNotBlank() } + .filter { + try { + Pattern.compile(it); true + } catch (_: PatternSyntaxException) { + false + } + }.forEach { + yieldKnown(RegexValue(it, Random(0))) { value } + } + } + + private fun FuzzedContext.isPatterMatchingContext(): Boolean { + if (this !is FuzzedContext.Call) return false + val stringMethodWithRegexArguments = setOf("matches", "split") + return when { + method.classId == Pattern::class.java.id -> true + method.classId == String::class.java.id && stringMethodWithRegexArguments.contains(method.name) -> true + else -> false + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt index 35bc58d284..45750f1a2b 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt @@ -2,7 +2,7 @@ package org.utbot.framework.plugin.api import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -import org.utbot.fuzzer.invertBit +import org.utbot.fuzzing.utils.invertBit import kotlin.random.Random class ModelMutatorTest { diff --git a/utbot-fuzzing/build.gradle.kts b/utbot-fuzzing/build.gradle.kts new file mode 100644 index 0000000000..627ea222a2 --- /dev/null +++ b/utbot-fuzzing/build.gradle.kts @@ -0,0 +1,7 @@ +val kotlinLoggingVersion: String by rootProject +val rgxgenVersion: String by rootProject + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "com.github.curious-odd-man", name = "rgxgen", version = rgxgenVersion) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java b/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java new file mode 100644 index 0000000000..d073a0f0a6 --- /dev/null +++ b/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java @@ -0,0 +1,33 @@ +package org.utbot.fuzzing.demo; + +/** + * Example class that is used in {@link JavaFuzzingKt} + */ +@SuppressWarnings("unused") +final class A { + + public String name; + public int age; + public A copy; + + public A() { + } + + public A(String name) { + this.name = name; + } + + public A(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "A{" + + "name='" + name + '\'' + + ", age=" + age + + ", copy=" + copy + + '}'; + } +} diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt new file mode 100644 index 0000000000..a22f1b9592 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -0,0 +1,662 @@ +@file:JvmName("FuzzingApi") +package org.utbot.fuzzing + +import mu.KotlinLogging +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.fuzzing.utils.chooseOne +import org.utbot.fuzzing.utils.flipCoin +import org.utbot.fuzzing.utils.transformIfNotEmpty +import kotlin.random.Random + +private val logger by lazy { KotlinLogging.logger {} } + +/** + * Describes some data to start fuzzing: initial seeds and how to run target program using generated values. + * + * @see [org.utbot.fuzzing.demo.AbcFuzzingKt] + * @see [org.utbot.fuzzing.demo.JavaFuzzing] + * @see [org.utbot.fuzzing.demo.JsonFuzzingKt] + */ +interface Fuzzing, FEEDBACK : Feedback> { + /** + * Generates seeds for a concrete type. + * + * If any information except type is required, like parameter index or another, + * [description] parameter can be used. + * + * NB: Fuzzing implementation caches seeds for concrete types to improve performance because + * usually all seeds are statically defined. In case some dynamic behavior is required use + * [Feedback.control] to reset caches. + */ + fun generate(description: DESCRIPTION, type: TYPE): Sequence> + + /** + * This method is called on every value list generated by fuzzer. + * + * Fuzzing combines, randomize and mutates values using the seeds. + * Then it generates values and runs them with this method. This method should provide some feedback, + * which is the most important part for good fuzzing result. [emptyFeedback] can be provided only for test + * or infinite loops. Consider to implement own implementation of [Feedback] to provide more correct data or + * use [BaseFeedback] to generate key based feedback. In this case, the key is used to analyse what value should be next. + * + * @param description contains user-defined information about current run. Can be used as a state of the run. + * @param values current values to process. + */ + suspend fun handle(description: DESCRIPTION, values: List): FEEDBACK +} + +/** + * Some description of current fuzzing run. Usually, contains name of the target method and its parameter list. + */ +open class Description( + val parameters: List +) + +/** + * Input value that fuzzing knows how to build and use them. + */ +sealed interface Seed { + /** + * Simple value is just a concrete value that should be used as is. + * + * Any mutation can be provided if it is applicable to this value. + */ + class Simple(val value: RESULT, val mutation: (RESULT, random: Random) -> RESULT = { f, _ -> f }): Seed + + /** + * Known value is a typical value that can be manipulated by fuzzing without knowledge about object structure + * in concrete language. For example, integer can be represented as a bit vector of n-bits. + * + * List of the known to fuzzing values are: + * + * 1. BitVectorValue represents a vector of bits. + * 2. ... + */ + class Known(val value: V, val build: (V) -> RESULT): Seed + + /** + * Recursive value defines an object with typically has a constructor and list of modifications. + * + * This task creates a tree of object values. + */ + class Recursive( + val construct: Routine.Create, + val modify: Sequence> = emptySequence(), + val empty: Routine.Empty + ) : Seed + + /** + * Collection is a task, that has 2 main options: + * + * 1. Construction the collection + * 2. Modification of the collections that depends on some number of iterations. + */ + class Collection( + val construct: Routine.Collection, + val modify: Routine.ForEach + ) : Seed +} + +/** + * Routine is a task that is used to build a value. + * + * There are several types of a routine, which all are generally only functions. + * These function accepts some data and generates target value. + */ +sealed class Routine(val types: List) : Iterable by types { + + /** + * Creates an empty recursive object. + */ + class Create( + types: List, + val builder: (arguments: List) -> R, + ) : Routine(types), Map by hashMapOf() { + operator fun invoke(arguments: List): R = builder(arguments) + } + + /** + * Calls routine for a given object. + */ + class Call( + types: List, + val callable: (instance: R, arguments: List) -> Unit + ) : Routine(types) { + operator fun invoke(instance: R, arguments: List) { + callable(instance, arguments) + } + } + + /** + * Creates a collection of concrete size. + */ + class Collection( + val builder: (size: Int) -> R, + ) : Routine(emptyList()) { + operator fun invoke(size: Int): R = builder(size) + } + + /** + * Is called for collection with index of iterations. + */ + class ForEach( + types: List, + val callable: (instance: R, index: Int, arguments: List) -> Unit + ) : Routine(types) { + operator fun invoke(instance: R, index: Int, arguments: List) = callable(instance, index, arguments) + } + + /** + * Empty routine that generates a concrete value. + */ + class Empty( + val builder: () -> R, + ) : Routine(emptyList()) { + operator fun invoke(): R = builder() + } +} + +/** + * Interface to force [Any.hashCode] and [Any.equals] implementation for [Feedback], + * because it is used in the map. + */ +interface AsKey { + override fun equals(other: Any?): Boolean + override fun hashCode(): Int +} + +/** + * Language feedback from a concrete execution of the target code. + */ +interface Feedback : AsKey { + /** + * Controls what fuzzing should do. + * + * @see [Control] + */ + val control: Control +} + +/** + * Base implementation of [Feedback]. + * + * NB! [VALUE] type must implement [equals] and [hashCode] due to the fact it uses as a key in map. + * If it doesn't implement those method, [OutOfMemoryError] is possible. + */ +data class BaseFeedback( + val result: VALUE, + override val control: Control, +) : Feedback + +/** + * Controls fuzzing execution. + */ +enum class Control { + /** + * Analyse feedback and continue. + */ + CONTINUE, + + /** + * Reset type cache and continue. + * + * Current seed and result will be analysed and cached. + */ + RESET_TYPE_CACHE_AND_CONTINUE, + + /** + * Do not process this feedback and just start next value generation. + */ + PASS, + + /** + * Stop fuzzing. + */ + STOP, +} + +/** + * Returns empty feedback which is equals to any another empty feedback. + */ +@Suppress("UNCHECKED_CAST") +fun emptyFeedback(): Feedback = (EmptyFeedback as Feedback) + +private object EmptyFeedback : Feedback { + override val control: Control + get() = Control.CONTINUE + + override fun equals(other: Any?): Boolean { + return true + } + + override fun hashCode(): Int { + return 0 + } +} + +/** + * Starts fuzzing for this [Fuzzing] object. + * + * This is an entry point for every fuzzing. + */ +suspend fun , F : Feedback> Fuzzing.fuzz( + description: D, + random: Random = Random(0), + configuration: Configuration = Configuration() +) { + val fuzzing = this + val typeCache = hashMapOf>>() + fun fuzzOne(): Node = fuzz( + parameters = description.parameters, + fuzzing = fuzzing, + description = description, + random = random, + configuration = configuration, + builder = PassRoutine("Main Routine"), + state = State(1, typeCache), + ) + val dynamicallyGenerated = mutableListOf>() + val seeds = Statistics() + run breaking@ { + sequence { + while (true) { + if (dynamicallyGenerated.isNotEmpty()) { + yield(dynamicallyGenerated.removeFirst()) + } else { + val fuzzOne = fuzzOne() + // fuzz one value, seems to be bad, when have only a few and simple values + yield(fuzzOne) + + val randomSeed = seeds.getRandomSeed(random, configuration) + if (randomSeed != null) { + dynamicallyGenerated += mutate( + randomSeed, + fuzzing, + random, + configuration, + State(1, typeCache) + ) + } + } + } + }.forEach execution@ { values -> + 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) } } + val feedback = try { + fuzzing.handle(description, result) + } catch (t: Throwable) { + logger.error(t) { "Error when running fuzzing with $values" } + return@execution + } + when (feedback.control) { + Control.CONTINUE -> { + seeds.put(random, configuration, feedback, values) + } + Control.RESET_TYPE_CACHE_AND_CONTINUE -> { + dynamicallyGenerated.clear() + typeCache.clear() + seeds.put(random, configuration, feedback, values) + } + Control.STOP -> { return@breaking } + Control.PASS -> {} + } + } + } +} + + +///region Implementation of the fuzzing and non-public functions. + +private fun , FEEDBACK : Feedback> fuzz( + parameters: List, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + builder: Routine, + state: State, +): Node { + val typeCache = mutableMapOf>>() + val result = parameters.map { type -> + val results = typeCache.computeIfAbsent(type) { mutableListOf() } + if (results.isNotEmpty() && random.flipCoin(configuration.probReuseGeneratedValueForSameType)) { + results.random(random) + } else { + produce(type, fuzzing, description, random, configuration, state).also { + results += it + } + } + } + // is not inlined to debug values generated for a concrete type + return Node(result, parameters, builder) +} + +private fun , FEEDBACK : Feedback> produce( + type: TYPE, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + val candidates = state.cache.computeIfAbsent(type) { fuzzing.generate(description, type).toList() }.map { + @Suppress("UNCHECKED_CAST") + when (it) { + is Seed.Simple -> Result.Simple(it.value, it.mutation) + is Seed.Known -> Result.Known(it.value, it.build as KnownValue.() -> RESULT) + is Seed.Recursive -> reduce(it, fuzzing, description, random, configuration, state) + is Seed.Collection -> reduce(it, fuzzing, description, random, configuration, state) + } + } + if (candidates.isEmpty()) { + error("Unknown type: $type") + } + return candidates.random(random) +} + +/** + * 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( + task: Seed.Collection, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + return if (state.recursionTreeDepth > configuration.recursionTreeDepth) { + Result.Empty { task.construct.builder(0) } + } else { + val iterations = if (state.iterations >= 0 && random.flipCoin(configuration.probCreateRectangleCollectionInsteadSawLike)) { + state.iterations + } else { + random.nextInt(0, configuration.collectionIterations + 1) + } + Result.Collection( + construct = fuzz( + task.construct.types, + fuzzing, + description, + random, + configuration, + task.construct, + State(state.recursionTreeDepth + 1, state.cache, iterations) + ), + modify = if (random.flipCoin(configuration.probCollectionMutationInsteadCreateNew)) { + val result = fuzz(task.modify.types, fuzzing, description, random, configuration, task.modify, State(state.recursionTreeDepth + 1, state.cache, iterations)) + arrayListOf(result).apply { + (1 until iterations).forEach { _ -> + add(mutate(result, fuzzing, random, configuration, state)) + } + } + } else { + (0 until iterations).map { + fuzz(task.modify.types, fuzzing, description, random, configuration, task.modify, State(state.recursionTreeDepth + 1, state.cache, iterations)) + } + }, + iterations = iterations + ) + } +} + +/** + * 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( + task: Seed.Recursive, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + return if (state.recursionTreeDepth > configuration.recursionTreeDepth) { + Result.Empty { task.empty.builder() } + } else { + Result.Recursive( + construct = fuzz( + task.construct.types, + fuzzing, + description, + random, + configuration, + task.construct, + State(state.recursionTreeDepth + 1, state.cache) + ), + modify = task.modify + .shuffled(random) + .mapTo(arrayListOf()) { routine -> + fuzz( + routine.types, + fuzzing, + description, + random, + configuration, + routine, + State(state.recursionTreeDepth + 1, state.cache) + ) + }.transformIfNotEmpty { + take(random.nextInt(size + 1).coerceAtLeast(1)) + } + ) + } +} + +/** + * Starts mutations of some seeds from the object tree. + */ +@Suppress("UNCHECKED_CAST") +private fun , FEEDBACK : Feedback> mutate( + node: Node, + fuzzing: Fuzzing, + random: Random, + configuration: Configuration, + state: State, +): Node { + if (node.result.isEmpty()) return node + val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) + val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { + is Result.Simple -> Result.Simple(resultToMutate.mutation(resultToMutate.result, random), resultToMutate.mutation) + is Result.Known -> { + val mutations = resultToMutate.value.mutations() + if (mutations.isNotEmpty()) { + Result.Known( + mutations.random(random).mutate(resultToMutate.value, random, configuration), + resultToMutate.build as KnownValue.() -> RESULT + ) + } else { + resultToMutate + } + } + is Result.Recursive -> { + if (resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation)) { + Result.Recursive( + construct = mutate(resultToMutate.construct, fuzzing, random, configuration, State(state.recursionTreeDepth + 1, state.cache)), + modify = resultToMutate.modify + ) + } else if (random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation)) { + Result.Recursive( + construct = resultToMutate.construct, + modify = resultToMutate.modify.shuffled(random).take(random.nextInt(resultToMutate.modify.size + 1).coerceAtLeast(1)) + ) + } else { + Result.Recursive( + construct = resultToMutate.construct, + modify = resultToMutate.modify.toMutableList().apply { + val i = random.nextInt(0, resultToMutate.modify.size) + set(i, mutate(resultToMutate.modify[i], fuzzing, random, configuration, State(state.recursionTreeDepth + 1, state.cache))) + } + ) + } + } + is Result.Collection -> Result.Collection( + construct = resultToMutate.construct, + modify = resultToMutate.modify.toMutableList().apply { + if (isNotEmpty()) { + if (random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation)) { + val i = random.nextInt(0, resultToMutate.modify.size) + set(i, mutate(resultToMutate.modify[i], fuzzing, random, configuration, State(state.recursionTreeDepth + 1, state.cache))) + } else { + shuffle(random) + } + } + }, + iterations = resultToMutate.iterations + ) + is Result.Empty -> resultToMutate + } + return Node(node.result.toMutableList().apply { + set(indexOfMutatedResult, mutated) + }, node.parameters, node.builder) +} + +/** + * Rates somehow the result. + * + * For example, fuzzing should not try to mutate some empty structures, like empty collections or objects. + */ +private fun rate(result: Result): Double { + return when (result) { + is Result.Recursive -> if (result.construct.parameters.isEmpty() and result.modify.isEmpty()) 1E-7 else 0.5 + is Result.Collection -> if (result.iterations == 0) return 0.01 else 0.7 + else -> 1.0 + } +} + +/** + * Creates a real result. + * + * Fuzzing doesn't use real object because it mutates values by itself. + */ +@Suppress("UNCHECKED_CAST") +private fun create(result: Result): R = when(result) { + is Result.Simple -> result.result + is Result.Known -> (result.build as KnownValue.() -> R)(result.value) + is Result.Recursive -> with(result) { + val obj: R = when (val c = construct.builder) { + is Routine.Create -> c(construct.result.map { create(it) }) + is Routine.Empty -> c() + else -> error("Undefined create method") + } + modify.forEach { func -> + when (val builder = func.builder) { + is Routine.Call -> builder(obj, func.result.map { create(it) }) + is PassRoutine -> logger.warn { "Routine pass: ${builder.description}" } + else -> error("Undefined object call method ${func.builder}") + } + } + obj + } + is Result.Collection -> with(result) { + val collection: R = when (val c = construct.builder) { + is Routine.Create -> c(construct.result.map { create(it) }) + is Routine.Empty -> c() + is Routine.Collection -> c(modify.size) + else -> error("Undefined create method") + } + modify.forEachIndexed { index, func -> + when (val builder = func.builder) { + is Routine.ForEach -> builder(collection, index, func.result.map { create(it) }) + is PassRoutine -> logger.warn { "Routine pass: ${builder.description}" } + else -> error("Undefined collection call method ${func.builder}") + } + } + collection + } + is Result.Empty -> result.build() +} + +/** + * Empty routine to start a recursion within [fuzz]. + */ +private data class PassRoutine(val description: String) : Routine(emptyList()) + +/** + * Internal state for one fuzzing run. + */ +private class State( + val recursionTreeDepth: Int = 1, + val cache: MutableMap>>, + val iterations: Int = -1 +) + +/** + * The result of producing real values for the language. + */ +private sealed interface Result { + + /** + * Simple result as is. + */ + class Simple(val result: RESULT, val mutation: (RESULT, random: Random) -> RESULT = { f, _ -> f }) : Result + + /** + * Known value. + */ + class Known(val value: V, val build: (V) -> RESULT) : Result + /** + * A tree of object that has constructor and some modifications. + */ + class Recursive( + val construct: Node, + val modify: List>, + ) : Result + + /** + * A tree of collection-like structures and their modification. + */ + class Collection( + val construct: Node, + val modify: List>, + val iterations: Int, + ) : Result + + /** + * Empty result which just returns a value. + */ + class Empty( + val build: () -> RESULT + ) : Result +} + +/** + * Temporary object to storage information about partly calculated values tree. + */ +private class Node( + val result: List>, + val parameters: List, + val builder: Routine, +) + + +private class Statistics> { + private val seeds = hashMapOf>() + private val count = hashMapOf() + + 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()) return null + 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 + } +} + +///endregion \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt new file mode 100644 index 0000000000..20821e836f --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -0,0 +1,75 @@ +package org.utbot.fuzzing + +import kotlin.math.pow + +/** + * Configures fuzzing behaviour. Usually, it is not required to tune anything. + */ +class Configuration( + /** + * Fuzzer creates a tree of object for generating values. At some point this recursion should be stopped. + * + * To stop recursion [Seed.Recursive.empty] is called to create new values. + */ + var recursionTreeDepth: Int = 3, + + /** + * The limit of collection size to create. + */ + var collectionIterations: Int = 5, + + /** + * Energy function that is used to choose seeded value. + */ + var energyFunction: (x: Long) -> Double = { x -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) }, + + /** + * Probability to prefer shuffling collection instead of mutation one value from modification + */ + var probCollectionShuffleInsteadResultMutation: Int = 75, + + /** + * Probability of creating shifted array values instead of generating new values for modification. + */ + var probCollectionMutationInsteadCreateNew: Int = 50, + + /** + * Probability to prefer change constructor instead of modification. + */ + var probConstructorMutationInsteadModificationMutation: Int = 90, + + /** + * Probability to shuffle modification list of the recursive object + */ + var probShuffleAndCutRecursiveObjectModificationMutation: Int = 10, + + /** + * Probability to prefer create rectangle collections instead of creating saw-like one. + */ + var probCreateRectangleCollectionInsteadSawLike: Int = 80, + + /** + * Probability of updating old seed instead of leaving to the new one when [Feedback] has same key. + */ + var probUpdateSeedInsteadOfKeepOld: Int = 70, + + /** + * When mutating StringValue a new string will not exceed this value. + */ + var maxStringLengthWhenMutated: Int = 64, + + /** + * Probability of adding a new character when mutating StringValue + */ + var probStringAddCharacter: Int = 50, + + /** + * Probability of removing an old character from StringValue when mutating + */ + var probStringRemoveCharacter: Int = 50, + + /** + * Probability of reusing same generated value when 2 or more parameters have the same type. + */ + var probReuseGeneratedValueForSameType: Int = 1 +) \ 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 new file mode 100644 index 0000000000..77fae48428 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -0,0 +1,74 @@ +@file:Suppress("ReplaceRangeStartEndInclusiveWithFirstLast") + +package org.utbot.fuzzing + +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.IEEE754Value +import kotlin.random.Random + +/** + * Mutations is an object which applies some changes to the source object + * and then returns a new object (or old one without changes). + */ +fun interface Mutation { + fun mutate(source: T, random: Random, configuration: Configuration): T +} + +inline fun Mutation.adapt(): Mutation { + return Mutation { s, r, c -> + if (s is F) return@Mutation mutate(s, r, c) else s + } +} + +sealed class BitVectorMutations : Mutation { + + abstract fun rangeOfMutation(source: BitVectorValue): IntRange + + override fun mutate(source: BitVectorValue, random: Random, configuration: Configuration): BitVectorValue { + with (rangeOfMutation(source)) { + val firstBits = random.nextInt(start, endInclusive.coerceAtLeast(1)) + return BitVectorValue(source).apply { this[firstBits] = !this[firstBits] } + } + } + + object SlightDifferent : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = 0 .. source.size / 4 + } + + object DifferentWithSameSign : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = source.size / 4 .. source.size + } + + object ChangeSign : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = source.size - 1 .. source.size + } +} + +sealed class IEEE754Mutations : Mutation { + + object ChangeSign : IEEE754Mutations() { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + return IEEE754Value(source).apply { + setRaw(0, !getRaw(0)) + } + } + } + + object Mantissa : IEEE754Mutations() { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + val i = random.nextInt(0, source.mantissaSize) + return IEEE754Value(source).apply { + setRaw(1 + exponentSize + i, !getRaw(1 + exponentSize + i)) + } + } + } + + object Exponent : IEEE754Mutations() { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + val i = random.nextInt(0, source.exponentSize) + return IEEE754Value(source).apply { + setRaw(1 + i, !getRaw(1 + i)) + } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt new file mode 100644 index 0000000000..c6aac53178 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt @@ -0,0 +1,191 @@ +package org.utbot.fuzzing + +import mu.KotlinLogging +import kotlin.random.Random + +private val logger by lazy { KotlinLogging.logger {} } + +/** + * Entry point to run fuzzing. + */ +suspend fun , FEEDBACK : Feedback> runFuzzing( + provider: ValueProvider, + description: DESCRIPTION, + random: Random = Random(0), + configuration: Configuration = Configuration(), + handle: suspend (description: DESCRIPTION, values: List) -> FEEDBACK +) { + BaseFuzzing(listOf(provider), handle).fuzz(description, random, configuration) +} + +/** + * Implements base concepts that use providers to generate values for some types. + * + * @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>( + val providers: List>, + val exec: suspend (description: D, values: List) -> F +) : Fuzzing { + + constructor(vararg providers: ValueProvider, exec: suspend (description: D, values: List) -> F) : this(providers.toList(), exec) + + override fun generate(description: D, type: T): Sequence> { + return providers.asSequence().flatMap { provider -> + try { + if (provider.accept(type)) { + provider.generate(description, type) + } else { + emptySequence() + } + } catch (t: Throwable) { + logger.error(t) { "Error occurs in value provider: $provider" } + emptySequence() + } + } + } + + override suspend fun handle(description: D, values: List): F { + return exec(description, values) + } +} + +/** + * Value provider generates [Seed] and has other methods to combine providers. + */ +fun interface ValueProvider> { + /** + * Generate a sequence of [Seed] that is merged with values generated by other provider. + */ + fun generate(description: D, type: T): Sequence> + + /** + * Validates if this provider is applicable to some type. + */ + fun accept(type: T): Boolean = true + + /** + * Combines this model provider with `anotherValueProviders` into one instance. + * + * This model provider is called before `anotherValueProviders`. + */ + fun with(anotherValueProvider: ValueProvider): ValueProvider { + fun toList(m: ValueProvider) = if (m is Combined) m.providers else listOf(m) + return Combined(toList(this) + toList(anotherValueProvider)) + } + + /** + * Removes `anotherValueProviders): ValueProvider { + return except { it == anotherValueProvider } + } + + /** + * Removes `anotherValueProviders) -> Boolean): ValueProvider { + return if (this is Combined) { + Combined(providers.filterNot(filter)) + } else { + Combined(if (filter(this)) emptyList() else listOf(this)) + } + } + + /** + * Applies [transform] for current provider + */ + fun map(transform: (ValueProvider) -> ValueProvider): ValueProvider { + return if (this is Combined) { + Combined(providers.map(transform)) + } else { + transform(this) + } + } + + fun withFallback(fallback: ValueProvider) : ValueProvider { + val thisProvider = this + return ValueProvider { description, type -> + val default = if (accept(type)) thisProvider.generate(description, type) else emptySequence() + if (default.iterator().hasNext()) { + default + } else if (fallback.accept(type)) { + fallback.generate(description, type) + } else { + emptySequence() + } + } + } + + /** + * Creates new value provider that creates default value if no values are generated by this provider. + */ + fun withFallback(fallbackSupplier: (T) -> Seed) : ValueProvider { + val thisProvider = this + return ValueProvider { description, type -> + if (accept(type)) { + thisProvider.generate(description, type) + .takeIf { it.iterator().hasNext() } + ?: sequenceOf(fallbackSupplier(type)) + } else { + emptySequence() + } + } + } + + /** + * Wrapper class that delegates implementation to the [providers]. + */ + private class Combined>(providers: List>): ValueProvider { + val providers: List> + + init { + // Flattening to avoid Combined inside Combined (for correct work of except, map, etc.) + this.providers = providers.flatMap { + if (it is Combined) + it.providers + else + listOf(it) + } + } + + override fun accept(type: T): Boolean { + return providers.any { it.accept(type) } + } + + override fun generate(description: D, type: T): Sequence> = sequence { + providers.asSequence().filter { it.accept(type) }.forEach { provider -> + provider.generate(description, type).forEach { + yield(it) + } + } + } + } + + companion object { + fun > of(valueProviders: List>): ValueProvider { + return Combined(valueProviders) + } + } +} + +fun> List>.withFallback(fallbackSupplier: (T) -> Seed) : List> { + return listOf(ValueProvider.of(this).withFallback(fallbackSupplier)) +} + +/** + * Simple value provider for a concrete type. + * + * @param type that is used as a filter to call this provider + * @param generate yields values for the type + */ +class TypeProvider>( + val type: T, + val generate: suspend SequenceScope>.(description: D, type: T) -> Unit +) : ValueProvider { + override fun accept(type: T) = this.type == type + override fun generate(description: D, type: T) = sequence { + this.generate(description, type) + } +} 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 new file mode 100644 index 0000000000..4969ccf7f4 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -0,0 +1,63 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* + +/** + * This example shows the minimal required implementation to start fuzzing any function. + * + * Assume, there's a function that returns some positive integer if one string is a substring. + * The value of this integer is maximum number of same characters, therefore, bigger is better. + * + * Lets fuzzing values for some given string to find out does the fuzzing can find the whole string or not. + */ +fun String.findMaxSubstring(s: String) : Int { + if (s.isEmpty()) return -1 + for (i in s.indices) { + if (s[i] != this[i]) return i - 1 + } + return s.length +} + +// the given string +private const val searchString = + "fun String.findMaxSubstring(s: String) : Int {\n" + + " if (s.isEmpty()) return -1\n" + + " for (i in s.indices) {\n" + + " if (s[i] != this[i]) return i - 1\n" + + " }\n" + + " return s.length\n" + + "}" + +suspend fun main() { + // Define fuzzing description to start searching. + 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>( + Seed.Simple("") { s, r -> s + Char(r.nextInt(1, 256)) } + ) + + /** + * After the fuzzing generates a new value it calls this method to execute target program and waits for feedback. + * + * 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 { + 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( + result = result, + control = if (result == searchString.length) Control.STOP else Control.CONTINUE + ) + } + }.fuzz(Description(listOf(Unit))) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..0d3d490dd7 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt @@ -0,0 +1,94 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.Bool +import org.utbot.fuzzing.seeds.Signed +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?>> { + 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() }) + } + if (type == String::class.java) { + yield(Seed.Known(StringValue(""), StringValue::value)) + yield(Seed.Known(StringValue("hello"), StringValue::value)) + } + for (signed in Signed.values()) { + if (type == Char::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(8)) { obj: BitVectorValue -> obj.toCharacter() }) + } + if (type == Byte::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(8)) { obj: BitVectorValue -> obj.toByte() }) + } + if (type == Short::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(16)) { obj: BitVectorValue -> obj.toShort() }) + } + if (type == Int::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(32)) { obj: BitVectorValue -> obj.toInt() }) + } + if (type == Long::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(64)) { obj: BitVectorValue -> obj.toLong() }) + } + } + if (type == A::class.java) { + for (constructor in A::class.java.constructors) { + yield( + Seed.Recursive( + construct = Routine.Create(constructor.parameters.map { it.type }) { objects -> + constructor.newInstance(*objects.toTypedArray()) + }, + modify = type.fields.asSequence().map { field -> + Routine.Call(listOf(field.type)) { self: Any?, objects: List<*> -> + try { + field[self] = objects[0] + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } + } + }, + empty = Routine.Empty { null } + ) + ) + } + } + if (type.isArray) { + yield( + Seed.Collection( + construct = Routine.Collection { length: Int -> + java.lang.reflect.Array.newInstance(type.componentType, length) + }, + modify = Routine.ForEach(listOf(type.componentType)) { self: Any?, index: Int, objects: List<*> -> + java.lang.reflect.Array.set(self, index, objects[0]) + } + )) + } + } + + override suspend fun handle(description: Description>, values: List): Feedback, Any?> { + println(values.joinToString { + when (it) { + is BooleanArray -> it.contentToString() + is CharArray -> it.contentToString() + is ByteArray -> it.contentToString() + is ShortArray -> it.contentToString() + is IntArray -> it.contentToString() + is LongArray -> it.contentToString() + else -> it.toString() + } + }) + return emptyFeedback() + } +} + +suspend fun main() { + JavaFuzzing.fuzz( + Description(listOf(Int::class.javaPrimitiveType!!, CharArray::class.java, A::class.java)), + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..fff760d395 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt @@ -0,0 +1,88 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.StringValue +import kotlin.random.Random + +private enum class CustomType { + INT, STR, OBJ, LST +} + +private class JsonBuilder( + var before: String, + val children: MutableList = mutableListOf(), + var after: String = "", +) { + override fun toString(): String { + return buildString { + append(before) + append(children.joinToString(", ")) + append(after) + } + } +} + +/** + * This example shows how json based output can be built by fuzzer for some types. + * + * Also, some utility class such as [BaseFuzzing] and [TypeProvider] are used to start fuzzing without class inheritance. + */ +@Suppress("RemoveExplicitTypeArguments") +suspend fun main() { + BaseFuzzing, Feedback>( + TypeProvider(CustomType.INT) { _, _ -> + for (b in Signed.values()) { + yield(Seed.Known(BitVectorValue(3, b)) { bvv -> + JsonBuilder((0 until bvv.size).joinToString(separator = "", prefix = "\"0b", postfix = "\"") { i -> + if (bvv[i]) "1" else "0" + }) + }) + } + }, + TypeProvider(CustomType.STR) { _, _ -> + listOf("Ted", "Mike", "John").forEach { n -> + yield(Seed.Known(StringValue(n)) { sv -> JsonBuilder("\"${sv.value}\"") }) + } + }, + TypeProvider(CustomType.OBJ) { _, _ -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(CustomType.OBJ)) { + JsonBuilder(before = "{", after = "}") + }, + modify = sequence { + yield(Routine.Call(listOf(CustomType.INT)) { self, values -> + self.children += JsonBuilder("\"value\": ${values.joinToString(", ")}") + }) + yield(Routine.Call(listOf(CustomType.STR)) { self, values -> + self.children += JsonBuilder("\"name\": ${values.joinToString(", ")}") + }) + yield(Routine.Call(listOf(CustomType.OBJ)) { self, values -> + self.children += JsonBuilder("\"child\": ${values.joinToString(", ")}") + }) + }, + empty = Routine.Empty { + JsonBuilder("null") + } + )) + }, + TypeProvider(CustomType.LST) { _, _ -> + for (type in listOf(CustomType.INT, CustomType.STR)) { + yield(Seed.Collection( + construct = Routine.Collection { JsonBuilder(before = "[", after = "]") }, + modify = Routine.ForEach(listOf(type)) { self, _, values -> + self.children += JsonBuilder(values.joinToString(", ")) + } + )) + } + }, + ) { _, values -> + println(values) + emptyFeedback() + }.fuzz( + Description(listOf(CustomType.LST, CustomType.OBJ)), + Random(0), + Configuration(recursionTreeDepth = 2, collectionIterations = 2) + ) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt new file mode 100644 index 0000000000..91bfd369a7 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt @@ -0,0 +1,254 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.Endian +import java.math.BigInteger +import java.util.BitSet + +class BitVectorValue : KnownValue { + + /** + * Vector of value bits. + * + * Here, the base is 2 and the exponent for each member is equal to index of this member. + * Therefore, the values is stored using LE system. + */ + private val vector: BitSet + val size: Int + override val lastMutation: Mutation? + override val mutatedFrom: KnownValue? + + constructor(bits: Int, bound: Bound) { + vector = BitSet(bits).also { + for (i in 0 until bits) { + it[i] = bound.initializer(i, bits) + } + } + size = bits + lastMutation = null + mutatedFrom = null + } + + constructor(other: BitVectorValue, mutation: Mutation? = null) { + vector = other.vector.clone() as BitSet + size = other.size + lastMutation = mutation + mutatedFrom = other + } + + private constructor(size: Int, value: BitSet) : this(size, { i, _ -> value[i] }) + + operator fun get(index: Int): Boolean = vector[index] + + operator fun set(index: Int, value: Boolean) { + vector[index] = value + } + + /** + * Increase value by 1. + * + * @return true if integer overflow is occurred in sign values + */ + fun inc(): Boolean { + var shift = 0 + var carry = true + while (carry and (shift < size)) { + if (!vector[shift]) { + carry = false + } + vector[shift] = !vector[shift] + shift++ + } + return !carry && shift == size + } + + /** + * Decrease value by 1. + * + * @return true if integer underflow is occurred + */ + fun dec(): Boolean { + var shift = 0 + var carry = true + while (carry and (shift < size)) { + if (vector[shift]) { + carry = false + } + vector[shift] = !vector[shift] + shift++ + } + return !carry && shift == size + } + + override fun mutations() = listOf>( + BitVectorMutations.SlightDifferent.adapt(), + BitVectorMutations.DifferentWithSameSign.adapt(), + BitVectorMutations.ChangeSign.adapt() + ) + + override fun equals(other: Any?): Boolean { + if (other !is BitVectorValue) return false + if (size != other.size) return false + for (i in 0 until size) { + if (vector[i] != other.vector[i]) return false + } + return true + } + + override fun hashCode(): Int { + return vector.hashCode() + } + + @Suppress("MemberVisibilityCanBePrivate") + fun toString(radix: Int, isUnsigned: Boolean = false): String { + val size = if (isUnsigned) size + 1 else size + val array = ByteArray(size / 8 + if (size % 8 != 0) 1 else 0) { index -> + toLong(bits = 8, shift = index * 8).toByte() + } + array.reverse() + return BigInteger(array).toString(radix) + } + + override fun toString() = toString(10) + + internal fun toBinaryString(endian: Endian) = buildString { + for (i in endian.range(0, size - 1)) { + append(if (this@BitVectorValue[i]) '1' else '0') + } + } + + private fun toLong(bits: Int, shift: Int = 0): Long { + assert(bits <= 64) { "Cannot convert to long vector with more than 64 bits, but $bits is requested" } + var result = 0L + for (i in shift until minOf(bits + shift, size)) { + result = result or ((if (vector[i]) 1L else 0L) shl (i - shift)) + } + return result + } + + fun toBoolean() = vector[0] + + fun toByte() = toLong(8).toByte() + + fun toUByte() = toLong(8).toUByte() + + fun toShort() = toLong(16).toShort() + + fun toUShort() = toLong(16).toUShort() + + fun toInt() = toLong(32).toInt() + + fun toUInt() = toLong(32).toUInt() + + fun toLong() = toLong(64) + + fun toULong() = toLong(64).toULong() + + fun toCharacter() = Char(toUShort()) + + companion object { + fun fromValue(value: Any): BitVectorValue { + return when (value) { + is Char -> fromChar(value) + is Boolean -> fromBoolean(value) + is Byte -> fromByte(value) + is Short -> fromShort(value) + is Int -> fromInt(value) + is Long -> fromLong(value) + else -> error("unknown type of value $value (${value::class})") + } + } + + fun fromBoolean(value: Boolean): BitVectorValue { + return BitVectorValue(1, if (value) Bool.TRUE else Bool.FALSE) + } + + fun fromByte(value: Byte): BitVectorValue { + return fromLong(8, value.toLong()) + } + + fun fromShort(value: Short): BitVectorValue { + return fromLong(16, value.toLong()) + } + + fun fromChar(value: Char): BitVectorValue { + return fromLong(16, value.code.toLong()) + } + + fun fromInt(value: Int): BitVectorValue { + return fromLong(32, value.toLong()) + } + + fun fromLong(value: Long): BitVectorValue { + return fromLong(64, value) + } + + private fun fromLong(size: Int, value: Long): BitVectorValue { + val vector = BitSet(size) + for (i in 0 until size) { + vector[i] = value and (1L shl i) != 0L + } + return BitVectorValue(size, vector) + } + } +} + +fun interface Bound { + fun initializer(index: Int, size: Int): Boolean +} + +class DefaultBound private constructor(private val value: Long) : Bound { + + override fun initializer(index: Int, size: Int): Boolean { + return value and (1L shl index) != 0L + } + + @Suppress("unused") + companion object { + fun ofByte(value: Byte) = DefaultBound(value.toLong()) + + fun ofUByte(value: UByte) = DefaultBound(value.toLong()) + + fun ofShort(value: Short) = DefaultBound(value.toLong()) + + fun ofUShort(value: UShort) = DefaultBound(value.toLong()) + + fun ofInt(value: Int) = DefaultBound(value.toLong()) + + fun ofUInt(value: UInt) = DefaultBound(value.toLong()) + + fun ofLong(value: Long) = DefaultBound(value) + + fun ofULong(value: ULong) = DefaultBound(value.toLong()) + } +} + +enum class Signed : Bound { + ZERO { override fun initializer(index: Int, size: Int) = false }, + MIN { override fun initializer(index: Int, size: Int) = index == size - 1 }, + NEGATIVE { override fun initializer(index: Int, size: Int) = true }, + POSITIVE { override fun initializer(index: Int, size: Int) = index == 0 }, + MAX { override fun initializer(index: Int, size: Int) = index < size - 1 }, + ; + + operator fun invoke(size: Int) = BitVectorValue(size, this) + + fun test(value: BitVectorValue) = (0..value.size).all { value[it] == initializer(it, value.size) } +} + +enum class Unsigned : Bound { + ZERO { override fun initializer(index: Int, size: Int) = false }, + POSITIVE { override fun initializer(index: Int, size: Int) = index == 0 }, + MAX { override fun initializer(index: Int, size: Int) = true }, + ; + + operator fun invoke(size: Int) = BitVectorValue(size, this) +} + +enum class Bool : Bound { + FALSE { override fun initializer(index: Int, size: Int) = false }, + TRUE { override fun initializer(index: Int, size: Int) = true }, + ; + + operator fun invoke() = BitVectorValue(1, this) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt new file mode 100644 index 0000000000..128341f0d7 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt @@ -0,0 +1,278 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.* +import kotlin.math.pow + +//val FLOAT_ZERO = DefaultFloatBound.ZERO(23, 8) +//val FLOAT_NAN = DefaultFloatBound.NAN(23, 8) +//val FLOAT_POSITIVE_INFINITY = DefaultFloatBound.POSITIVE_INFINITY(23, 8) +//val FLOAT_NEGATIVE_INFINITY = DefaultFloatBound.NEGATIVE_INFINITY(23, 8) +//val DOUBLE_ZERO = DefaultFloatBound.ZERO(52, 11) +//val DOUBLE_NAN = DefaultFloatBound.NAN(52, 11) +//val DOUBLE_POSITIVE_INFINITY = DefaultFloatBound.POSITIVE_INFINITY(52, 11) +//val DOUBLE_NEGATIVE_INFINITY = DefaultFloatBound.NEGATIVE_INFINITY(52, 11) + +class IEEE754Value : KnownValue { + + val isPositive: Boolean + get() = !vector[0] + + val bias: Int + get() = (2 shl (exponentSize - 2)) - 1 + + val exponent: Long + get() { + check(exponentSize <= 64) { "Exponent cannot be represented as long" } + var result = 0L + for (i in 0 until exponentSize) { + if (vector[exponentSize - i]) { + result += 1L shl i + } + } + return result - bias + } + + val mantissaSize: Int + val exponentSize: Int + override val lastMutation: Mutation? + override val mutatedFrom: KnownValue? + + private val vector: BitVectorValue + + constructor(mantissaSize: Int, exponentSize: Int, bound: FloatBound ) { + this.mantissaSize = mantissaSize + this.exponentSize = exponentSize + this.vector = BitVectorValue(1 + mantissaSize + exponentSize) { index, size -> + check(1 + exponentSize + mantissaSize == size) { "size exceeds" } + when { + index >= 1 + exponentSize + mantissaSize -> error("out of range") + index >= 1 + exponentSize -> bound.mantissa(index - 1 - exponentSize, mantissaSize) + index >= 1 -> bound.exponent(index - 1, exponentSize) + index == 0 -> bound.sign() + else -> error("out of range") + } + } + lastMutation = null + mutatedFrom = null + } + + constructor(value: IEEE754Value, mutation: Mutation? = null) { + this.vector = BitVectorValue(value.vector) + this.mantissaSize = value.mantissaSize + this.exponentSize = value.exponentSize + this.lastMutation = mutation + this.mutatedFrom = value + } + + fun getRaw(index: Int) = vector[index] + + fun setRaw(index: Int, value: Boolean) { + vector[index] = value + } + + fun toFloat(): Float { + DefaultFloatBound.values().forEach { + if (it.test(this)) when (it) { + DefaultFloatBound.ZERO -> return 0.0f + DefaultFloatBound.NAN -> return Float.NaN + DefaultFloatBound.POSITIVE_INFINITY -> return Float.POSITIVE_INFINITY + DefaultFloatBound.NEGATIVE_INFINITY -> return Float.NEGATIVE_INFINITY + else -> {} + } + } + var result = 0.0f + val e = exponent.toFloat() + result += 2.0f.pow(e) + for (i in 0 until mantissaSize) { + if (vector[1 + exponentSize + i]) { + result += 2.0f.pow(e - 1 - i) + } + } + return result * if (isPositive) 1.0f else -1.0f + } + + fun toDouble(): Double { + DefaultFloatBound.values().forEach { + if (it.test(this)) when (it) { + DefaultFloatBound.ZERO -> return 0.0 + DefaultFloatBound.NAN -> return Double.NaN + DefaultFloatBound.POSITIVE_INFINITY -> return Double.POSITIVE_INFINITY + DefaultFloatBound.NEGATIVE_INFINITY -> return Double.NEGATIVE_INFINITY + else -> {} + } + } + var result = 0.0 + val e = exponent.toDouble() + result += 2.0.pow(e) + for (i in 0 until mantissaSize) { + if (vector[1 + exponentSize + i]) { + result += 2.0.pow(e - 1 - i) + } + } + return result * if (isPositive) 1.0 else -1.0 + } + + fun is32Float(): Boolean { + return vector.size == 32 && mantissaSize == 23 && exponentSize == 8 + } + + fun is64Float(): Boolean { + return vector.size == 64 && mantissaSize == 52 && exponentSize == 11 + } + + override fun toString() = buildString { + for (i in 0 until vector.size) { + if (i == 1 || i == 1 + exponentSize) append(" ") + append(if (getRaw(i)) '1' else '0') + } + } + + override fun mutations() = listOf>( + IEEE754Mutations.ChangeSign.adapt(), + IEEE754Mutations.Mantissa.adapt(), + IEEE754Mutations.Exponent.adapt() + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IEEE754Value + + if (mantissaSize != other.mantissaSize) return false + if (exponentSize != other.exponentSize) return false + if (vector != other.vector) return false + + return true + } + + override fun hashCode(): Int { + var result = mantissaSize + result = 31 * result + exponentSize + result = 31 * result + vector.hashCode() + return result + } + + companion object { + fun fromValue(value: Any): IEEE754Value { + return when (value) { + is Float -> fromFloat(value) + is Double -> fromDouble(value) + else -> error("unknown type of value $value (${value::class})") + } + } + + fun fromFloat(value: Float): IEEE754Value { + return IEEE754Value(23, 8, object : FloatBound { + + val rawInt = value.toRawBits() + + override fun sign(): Boolean { + return rawInt and (1 shl 31) != 0 + } + + override fun mantissa(index: Int, size: Int): Boolean { + return rawInt and (1 shl (size - 1 - index)) != 0 + } + + override fun exponent(index: Int, size: Int): Boolean { + return rawInt and (1 shl (30 - index)) != 0 + } + + }) + } + + fun fromDouble(value: Double): IEEE754Value { + return IEEE754Value(52, 11, object : FloatBound { + + val rawLong = value.toRawBits() + + override fun sign(): Boolean { + return rawLong and (1L shl 63) != 0L + } + + override fun mantissa(index: Int, size: Int): Boolean { + return rawLong and (1L shl (size - 1 - index)) != 0L + } + + override fun exponent(index: Int, size: Int): Boolean { + return rawLong and (1L shl (62 - index)) != 0L + } + + }) + } + } +} + +interface FloatBound { + fun sign(): Boolean + fun mantissa(index: Int, size: Int): Boolean + fun exponent(index: Int, size: Int): Boolean +} + +private class CopyBound(val vector: IEEE754Value) : FloatBound { + override fun sign(): Boolean = vector.getRaw(0) + override fun exponent(index: Int, size: Int): Boolean = vector.getRaw(1 + index) + override fun mantissa(index: Int, size: Int): Boolean = vector.getRaw(1 + vector.exponentSize + index) +} + +enum class DefaultFloatBound : FloatBound { + ZERO { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = false + }, + NAN { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = index == size - 1 + override fun exponent(index: Int, size: Int) = true + }, + POSITIVE { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = index != 0 + }, + NEGATIVE { + override fun sign() = true + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = index != 0 + }, + POSITIVE_INFINITY { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = true + }, + NEGATIVE_INFINITY { + override fun sign() = true + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = true + }, + ; + + operator fun invoke(mantissaSize: Int, exponentSize: Int): IEEE754Value { + return IEEE754Value(mantissaSize, exponentSize, this) + } + + fun test(value: IEEE754Value): Boolean { + for (i in 0 until 1 + value.exponentSize + value.mantissaSize) { + @Suppress("KotlinConstantConditions") + val res = when { + i >= 1 + value.exponentSize -> mantissa(i - 1 - value.exponentSize, value.mantissaSize) + i >= 1 -> exponent(i - 1, value.exponentSize) + i == 0 -> sign() + else -> error("bad index $i") + } + if (value.getRaw(i) != res) return false + } + return true + } +} + +//fun main() { +// println(IEEE754Value.fromDouble(8.75).toFloat()) +// println(IEEE754Value.fromFloat(28.7f).toDouble()) +// println(28.7f.toDouble()) +// DefaultFloatBound.values().forEach { +// println(IEEE754Value(3, 3, it).toFloat()) +// } +//} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt new file mode 100644 index 0000000000..6a550480ee --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt @@ -0,0 +1,13 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.Mutation + +interface KnownValue { + val lastMutation: Mutation? + get() = null + + val mutatedFrom: KnownValue? + get() = null + + fun mutations(): List> +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt new file mode 100644 index 0000000000..9091f54b9b --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt @@ -0,0 +1,24 @@ +package org.utbot.fuzzing.seeds + +import com.github.curiousoddman.rgxgen.RgxGen +import com.github.curiousoddman.rgxgen.config.RgxGenOption +import com.github.curiousoddman.rgxgen.config.RgxGenProperties +import org.utbot.fuzzing.Mutation +import kotlin.random.Random +import kotlin.random.asJavaRandom + +class RegexValue(val pattern: String, val random: Random) : KnownValue { + + val value: String by lazy { RgxGen(pattern).apply { + setProperties(rgxGenProperties) + }.generate(random.asJavaRandom()) } + + override fun mutations() = listOf(Mutation { source, random, _ -> + require(this === source) + RegexValue(pattern, random) + }) +} + +private val rgxGenProperties = RgxGenProperties().apply { + setProperty(RgxGenOption.INFINITE_PATTERN_REPETITION.key, "5") +} \ No newline at end of file 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 new file mode 100644 index 0000000000..37bb8f9eed --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -0,0 +1,53 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.Mutation +import org.utbot.fuzzing.utils.flipCoin +import kotlin.random.Random + +class StringValue(val value: String) : KnownValue { + + override fun mutations(): List> { + return listOf(Mutation { source, random, configuration -> + require(source == this) + // we can miss some mutation for a purpose + val position = random.nextInt(value.length + 1) + var result: String = value + if (random.flipCoin(configuration.probStringRemoveCharacter)) { + result = tryRemoveChar(random, result, position) ?: value + } + if (result.length < configuration.maxStringLengthWhenMutated && random.flipCoin(configuration.probStringAddCharacter)) { + result = tryAddChar(random, result, position) + } + StringValue(result) + }) + } + + private fun tryAddChar(random: Random, value: String, position: Int): String { + val charToMutate = if (value.isNotEmpty()) { + value.random(random) + } else { + // use any meaningful character from the ascii table + random.nextInt(33, 127).toChar() + } + return buildString { + append(value.substring(0, position)) + // try to change char to some that is close enough to origin char + val charTableSpread = 64 + if (random.nextBoolean()) { + append(charToMutate - random.nextInt(1, charTableSpread)) + } else { + append(charToMutate + random.nextInt(1, charTableSpread)) + } + append(value.substring(position, value.length)) + } + } + + private fun tryRemoveChar(random: Random, value: String, position: Int): String? { + if (position >= value.length) return null + val toRemove = random.nextInt(value.length) + return buildString { + append(value.substring(0, toRemove)) + append(value.substring(toRemove + 1, value.length)) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt similarity index 98% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt index 0dd190f1d1..138076fcbc 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils import kotlin.jvm.Throws import kotlin.random.Random diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt similarity index 99% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt index c724022ae6..33908563a8 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils /** * Enumerates all possible combinations for a given list of maximum numbers of elements for every position. diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt new file mode 100644 index 0000000000..b32d13b45f --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt @@ -0,0 +1,96 @@ +@file:Suppress("unused") + +package org.utbot.fuzzing.utils + +import org.utbot.fuzzing.seeds.BitVectorValue + +internal fun T.toBinaryString( + size: Int, + endian: Endian = Endian.BE, + separator: String = " ", + format: (index: Int) -> Boolean = BinaryFormat.BYTE, + bit: (v: T, i: Int) -> Boolean +): String = buildString { + (endian.range(0, size - 1)).forEachIndexed { index, i -> + val b = if (bit(this@toBinaryString, i)) 1 else 0 + if (format(index)) append(separator) + append(b) + } + appendLine() +} + +internal fun UByte.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(8, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun Byte.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(8, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun UShort.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(16, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun Short.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(16, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun UInt.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(32, endian, separator, format) { v, i -> v and (1u shl i) != 0u } +} + +internal fun Int.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(32, endian, separator, format) { v, i -> v and (1 shl i) != 0 } +} + +internal fun ULong.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(64, endian, separator, format) { v, i -> v and (1uL shl i) != 0uL } +} + +internal fun Long.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(64, endian, separator, format) { v, i -> v and (1L shl i) != 0L } +} + +internal fun Float.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = { + when (endian) { + Endian.BE -> it == 1 || it == 9 + Endian.LE -> it == 31 || it == 23 + } +}): String { + return toRawBits().toBinaryString(endian, separator, format) +} + +internal fun Double.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = { + when (endian) { + Endian.BE -> it == 1 || it == 12 + Endian.LE -> it == 63 || it == 52 + } +}): String { + return toRawBits().toBinaryString(endian, separator, format) +} + +internal enum class Endian { + BE { override fun range(fromInclusive: Int, toInclusive: Int) = (toInclusive downTo fromInclusive) }, + LE { override fun range(fromInclusive: Int, toInclusive: Int) = (fromInclusive .. toInclusive) }; + abstract fun range(fromInclusive: Int, toInclusive: Int): IntProgression +} + +internal enum class BinaryFormat : (Int) -> Boolean { + HALF { override fun invoke(index: Int) = index % 4 == 0 && index != 0 }, + BYTE { override fun invoke(index: Int) = index % 8 == 0 && index != 0 }, + DOUBLE { override fun invoke(index: Int) = index % 16 == 0 && index != 0 }, +} + +internal fun List.transformIfNotEmpty(transform: List.() -> List): List { + return if (isNotEmpty()) transform() else this +} + +// todo move to tests +//fun main() { +// val endian = Endian.BE +// println(255.toUByte().toBinaryString(endian)) +// println(2.toBinaryString(endian)) +// println(BitVectorValue.fromInt(2).toBinaryString(endian)) +// print(8.75f.toBinaryString(endian)) +// print(8.75.toBinaryString(endian)) +//} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt similarity index 99% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt index 4ed08dd7a4..5d6ebdbce1 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils import kotlin.math.sqrt import kotlin.random.Random diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt similarity index 96% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt index c665f0bbf6..c92a7fecda 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils import kotlin.random.Random diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt similarity index 91% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index fb3c8d6345..16ba6b0c1e 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils fun trieOf(vararg values: Iterable): Trie = IdentityTrie().apply { values.forEach(this::add) @@ -169,4 +169,22 @@ open class Trie( override var count: Int = 0, val children: MutableMap> = HashMap(), ) : Node + + private object EmptyNode : Node { + override val data: Any + get() = error("empty node has no data") + override val count: Int + get() = 0 + override fun equals(other: Any?): Boolean { + return false + } + override fun hashCode(): Int { + return 0 + } + } + + companion object { + @Suppress("UNCHECKED_CAST") + fun emptyNode() = EmptyNode as Node + } } \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt new file mode 100644 index 0000000000..dc447931a7 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt @@ -0,0 +1,136 @@ +package org.utbot.fuzzing.providers.known + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.DefaultBound +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.Unsigned +import kotlin.random.Random + +class BitVectorValueTest { + + @Test + fun `convert default kotlin literals to vector`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + assertEquals(i.toByte(), BitVectorValue(Byte.SIZE_BITS, DefaultBound.ofByte(i.toByte())).toByte()) + } + + for (i in Short.MIN_VALUE..Short.MAX_VALUE) { + assertEquals(i.toShort(), BitVectorValue(Short.SIZE_BITS, DefaultBound.ofShort(i.toShort())).toShort()) + } + + val randomIntSequence = with(Random(0)) { sequence { while (true) yield(nextInt()) } } + randomIntSequence.take(100_000).forEach { + assertEquals(it, BitVectorValue(Int.SIZE_BITS, DefaultBound.ofInt(it)).toInt()) + } + + val randomLongSequence = with(Random(0)) { sequence { while (true) yield(nextLong()) } } + randomLongSequence.take(100_000).forEach { + assertEquals(it, BitVectorValue(Long.SIZE_BITS, DefaultBound.ofLong(it)).toLong()) + } + } + + @Test + fun `test default bit vectors (byte)`() { + assertEquals(0, BitVectorValue(8, Signed.ZERO).toByte()) + assertEquals(-128, BitVectorValue(8, Signed.MIN).toByte()) + assertEquals(-1, BitVectorValue(8, Signed.NEGATIVE).toByte()) + assertEquals(1, BitVectorValue(8, Signed.POSITIVE).toByte()) + assertEquals(127, BitVectorValue(8, Signed.MAX).toByte()) + } + + @Test + fun `test default bit vectors (unsigned byte)`() { + assertEquals(0u.toUByte(), BitVectorValue(8, Unsigned.ZERO).toUByte()) + assertEquals(1u.toUByte(), BitVectorValue(8, Unsigned.POSITIVE).toUByte()) + assertEquals(255u.toUByte(), BitVectorValue(8, Unsigned.MAX).toUByte()) + } + + @Test + fun `test default bit vectors (short)`() { + assertEquals(0, BitVectorValue(16, Signed.ZERO).toShort()) + assertEquals(Short.MIN_VALUE, BitVectorValue(16, Signed.MIN).toShort()) + assertEquals(-1, BitVectorValue(16, Signed.NEGATIVE).toShort()) + assertEquals(1, BitVectorValue(16, Signed.POSITIVE).toShort()) + assertEquals(Short.MAX_VALUE, BitVectorValue(16, Signed.MAX).toShort()) + } + + @Test + fun `test default bit vectors (unsigned short)`() { + assertEquals(UShort.MIN_VALUE, BitVectorValue(16, Unsigned.ZERO).toUShort()) + assertEquals(1u.toUShort(), BitVectorValue(16, Unsigned.POSITIVE).toUShort()) + assertEquals(UShort.MAX_VALUE, BitVectorValue(16, Unsigned.MAX).toUShort()) + } + + @Test + fun `test default bit vectors (int)`() { + assertEquals(0, BitVectorValue(32, Signed.ZERO).toInt()) + assertEquals(Int.MIN_VALUE, BitVectorValue(32, Signed.MIN).toInt()) + assertEquals(-1, BitVectorValue(32, Signed.NEGATIVE).toInt()) + assertEquals(1, BitVectorValue(32, Signed.POSITIVE).toInt()) + assertEquals(Int.MAX_VALUE, BitVectorValue(32, Signed.MAX).toInt()) + } + + @Test + fun `test default bit vectors (unsigned int)`() { + assertEquals(UInt.MIN_VALUE, BitVectorValue(32, Unsigned.ZERO).toUInt()) + assertEquals(1u, BitVectorValue(32, Unsigned.POSITIVE).toUInt()) + assertEquals(UInt.MAX_VALUE, BitVectorValue(32, Unsigned.MAX).toUInt()) + } + + @Test + fun `test default bit vectors (long)`() { + assertEquals(0, BitVectorValue(64, Signed.ZERO).toLong()) + assertEquals(Long.MIN_VALUE, BitVectorValue(64, Signed.MIN).toLong()) + assertEquals(-1, BitVectorValue(64, Signed.NEGATIVE).toLong()) + assertEquals(1, BitVectorValue(64, Signed.POSITIVE).toLong()) + assertEquals(Long.MAX_VALUE, BitVectorValue(64, Signed.MAX).toLong()) + } + + @Test + fun `test default bit vectors (unsigned long)`() { + assertEquals(ULong.MIN_VALUE, BitVectorValue(64, Unsigned.ZERO).toULong()) + assertEquals(1uL, BitVectorValue(64, Unsigned.POSITIVE).toULong()) + assertEquals(ULong.MAX_VALUE, BitVectorValue(64, Unsigned.MAX).toULong()) + } + + @Test + fun `convert byte from and to`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + assertEquals(i.toByte(), BitVectorValue.fromByte(i.toByte()).toByte()) + } + } + + @Test + fun `inc and dec byte`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + val v = BitVectorValue.fromByte(i.toByte()) + assertEquals(i.toByte() == Byte.MAX_VALUE, v.inc()) { "$v" } + assertEquals((i + 1).toByte(), v.toByte()) + } + + for (i in Byte.MAX_VALUE downTo Byte.MIN_VALUE) { + val v = BitVectorValue.fromByte(i.toByte()) + assertEquals(i.toByte() == Byte.MIN_VALUE, v.dec()) { "$v" } + assertEquals((i - 1).toByte(), v.toByte()) + } + } + + @Test + fun `inc and dec long`() { + val r = with(Random(0)) { LongArray(1024) { nextLong() } } + r[0] = Long.MIN_VALUE + r[1] = Long.MAX_VALUE + r.forEach { l -> + val v = BitVectorValue.fromLong(l) + assertEquals(l == Long.MAX_VALUE, v.inc()) { "$v" } + assertEquals(l + 1, v.toLong()) + } + r.forEach { l -> + val v = BitVectorValue.fromLong(l) + assertEquals(l == Long.MIN_VALUE, v.dec()) { "$v" } + assertEquals(l - 1, v.toLong()) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java new file mode 100644 index 0000000000..02520ef345 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java @@ -0,0 +1,110 @@ +package org.utbot.fuzzing.samples; + +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings({"unused", "ForLoopReplaceableByForEach"}) +public class Arrays { + + // should find identity matrix + public boolean isIdentityMatrix(int[][] matrix) { + if (matrix.length < 3) { + throw new IllegalArgumentException("matrix.length < 3"); + } + + for (int i = 0; i < matrix.length; i++) { + if (matrix[i].length != matrix.length) { + return false; + } + for (int j = 0; j < matrix[i].length; j++) { + if (i == j && matrix[i][j] != 1) { + return false; + } + + if (i != j && matrix[i][j] != 0) { + return false; + } + } + } + + return true; + } + + // should fail with OOME and should reveal some branches + public boolean isIdentityMatrix(int[][][] matrix) { + if (matrix.length < 3) { + throw new IllegalArgumentException("matrix.length < 3"); + } + + for (int i = 0; i < matrix.length; i++) { + if (matrix[i].length != matrix.length) { + return false; + } else { + for (int j = 0; j < matrix.length; j++) { + if (matrix[i][j].length != matrix[i].length) { + return false; + } + } + } + for (int j = 0; j < matrix[i].length; j++) { + for (int k = 0; k < matrix[i][j].length; k++) { + if (i == j && j == k && matrix[i][j][k] != 1) { + return false; + } + + if ((i != j || j != k) && matrix[i][j][k] != 0) { + return false; + } + } + } + } + + return true; + } + + public static class Point { + int x, y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + boolean equals(Point other) { + return (other.x == this.x && other.y == this.y); + } + } + + boolean checkAllSame(Integer[] a) { // Fuzzer should find parameters giving true as well as parameters giving false + if (a.length < 1) return true; + Set s = new HashSet<>(java.util.Arrays.asList(a)); + return (s.size() <= 1); + } + + public boolean checkAllSamePoints(Point[] a) { // Also works for classes + if (a.length == 4) { + return false; // Test with array of size 4 should be generated by fuzzer + } + for (int i = 1; i < a.length; i++) { + if (!a[i].equals(a[i - 1])) + return false; + } + return true; + } + + public boolean checkRowsWithAllSame2D(int[][] a, int y) { + int cntSame = 0; + for (int i = 0; i < a.length; i++) { + boolean same = true; + for (int j = 1; j < a[i].length; j++) { + if (a[i][j] != a[i][j - 1]) { + same = false; + break; + } + } + if (same) + cntSame++; + } + return (cntSame == y && y > 0 && y < a.length); + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java new file mode 100644 index 0000000000..1d9f8f19f8 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java @@ -0,0 +1,177 @@ +package org.utbot.fuzzing.samples; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Collections { + /** + * Should create unsorted list that will be sorted as a result. + */ + public static Collection sorted(Collection source) { + if (source.size() < 2) throw new IllegalArgumentException(); + return source.stream().sorted().collect(Collectors.toList()); + } + + /** + * Should create at least both answers: one that finds the key, and another returns null. + */ + public static String getKeyForValue(Map map, Number value) { + for (Map.Entry entry : map.entrySet()) { + if (java.util.Objects.equals(entry.getValue(), value)) { + return entry.getKey(); + } + } + return null; + } + + /** + * Should find a branch that returns true and size of array is greater than 1 (non-trivial). + */ + public static boolean isDiagonal(Collection> matrix) { + int cols = matrix.size(); + if (cols <= 1) { + return false; + } + int i = 0; + for (Collection col : matrix) { + if (col.size() != cols) { + return false; + } + int j = 0; + for (Double value : col) { + if (i == j && value == 0.0) return false; + if (i != j && value != 0.0) return false; + j++; + } + i++; + } + return true; + } + + /** + * Checks that different collections can be created. Part 1 + */ + public boolean allCollectionAreSameSize1( + Collection c, + List l, + Set s, + SortedSet ss, + Deque d, + Iterable i + ) { + if (c.size() != l.size()) { + return false; + } + if (l.size() != s.size()) { + return false; + } + if (s.size() != ss.size()) { + return false; + } + if (ss.size() != d.size()) { + return false; + } + if (d.size() != StreamSupport.stream(i.spliterator(), false).count()) { + return false; + } + return true; + } + + /** + * Checks that different collections can be created. Part 2 + */ + public boolean allCollectionAreSameSize2( + Iterable i, + Stack st, + NavigableSet ns, + Map m, + SortedMap sm, + NavigableMap nm + ) { + if (StreamSupport.stream(i.spliterator(), false).count() != st.size()) { + return false; + } + if (st.size() != ns.size()) { + return false; + } + if (ns.size() != m.size()) { + return false; + } + if (m.size() != sm.size()) { + return false; + } + if (sm.size() != nm.size()) { + return false; + } + return true; + } + + /** + * Should create TreeSet without any modifications as T extends Number is not Comparable + */ + public boolean testTreeSetWithoutComparable(NavigableSet set) { + if (set.size() > 3) { + return true; + } + return false; + } + + /** + * Should create TreeSet with modifications as Integer is Comparable + */ + public boolean testTreeSetWithComparable(NavigableSet set) { + if (set.size() > 3) { + return true; + } + return false; + } + + public static class ConcreteList extends LinkedList { + public boolean equals(Collection collection) { + if (collection.size() != size()) { + return false; + } + int i = 0; + for (T t : collection) { + if (!java.util.Objects.equals(get(i), t)) { + return false; + } + } + return true; + } + } + + /** + * Should create concrete class + */ + public boolean testConcreteCollectionIsCreated(ConcreteList list) { + if (list.size() > 3) { + return true; + } + return false; + } + + public static class ConcreteMap extends HashMap { } + + /** + * Should create concrete class + */ + public boolean testConcreteMapIsCreated(ConcreteMap map) { + if (map.size() > 3) { + return true; + } + return false; + } + + /** + * Should create test with no error + */ + public boolean testNoErrorWithHashMap(HashMap map) { + if (map.size() > 5) { + return true; + } + return false; + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java new file mode 100644 index 0000000000..3e0bb16aae --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java @@ -0,0 +1,21 @@ +package org.utbot.fuzzing.samples; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Dates { + public boolean getTime(Date date) { + return date.getTime() == 100; + } + + public boolean getTimeFormatted(Date date) throws ParseException { + boolean after = new SimpleDateFormat("dd-MM-yyyy").parse("10-06-2012").after(date); + if (after) { + return true; + } else { + return false; + } + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java new file mode 100644 index 0000000000..0a216f4af1 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java @@ -0,0 +1,61 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class Enums { + + public int test(int x) { + switch(x) { + case 1: + return 1; + case 2: + return 2; + case 3: + return 3; + } + return 0; + } + + public int test3(int x) { + switch(x) { + case 11: + return 1; + case 25: + return 2; + case 32: + return 3; + } + return 0; + } + + public int test4(S x) { + switch(x) { + case A: + return 1; + case B: + return 2; + case C: + return 3; + } + return 0; + } + + public enum S { + A, B, C + } + + public int test2(int x) { + int a = 0; + switch(x) { + case 1: + a = 1; + break; + case 2: + a = 2; + break; + case 3: + a = 3; + } + return a; + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java new file mode 100644 index 0000000000..0f0917ffe9 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java @@ -0,0 +1,44 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class Floats { + + // Should find the value between 0 and -1. + public int floatToInt(float x) { + if (x < 0) { + if ((int) x < 0) { + return 1; + } + return 2; // smth small to int zero + } + return 3; + } + + // should find all branches that return -2, -1, 0, 1, 2. + public int numberOfRootsInSquareFunction(double a, double b, double c) { + if (!Double.isFinite(a) || !Double.isFinite(b) || !Double.isFinite(c)) return -1; + if (a == 0.0 || b == 0.0 || c == 0.0) return -2; + double result = b * b - 4 * a * c; + if (result > 0) { + return 2; + } else if (result == 0) { + return 1; + } + return 0; + } + + // will never be succeeded to find the value because of floating precision + public void floatEq(float v) { + if (v == 28.7) { + throw new IllegalArgumentException(); + } + } + + // should generate double for constant float that equals to 28.700000762939453 + public void floatEq(double v) { + if (v == 28.7f) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java new file mode 100644 index 0000000000..d65dcbdfa9 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java @@ -0,0 +1,66 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Integers { + + public byte biting(byte a, byte b, boolean s) { + if (s) { + return a; + } else { + return b; + } + } + + // should cover 100% + public static void diff(int a) { + a = Math.abs(a); + while (a > 0) { + a = a % 2; + } + if (a < 0) { + throw new IllegalArgumentException(); + } + throw new RuntimeException(); + } + + // should cover 100% and better when values are close to constants, + // also should generate "this" empty object + public String extent(int a) { + if (a < -2.0) { + return "-1"; + } + if (a > 5) { + return "-2"; + } + if (a == 3) { + return "-3"; + } + if (4L < a) { + return "-4"; + } + return "0"; + } + + // should cover 100% with 3 tests + public static boolean isGreater(long a, short b, int c) { + if (b > a && a < c) { + return true; + } + return false; + } + + // should find a bad value with integer overflow + public boolean unreachable(int x) { + int y = x * x - 2 * x + 1; + if (y < 0) throw new IllegalArgumentException(); + return true; + } + + public boolean chars(char a) { + if (a >= 'a' && a <= 'z') { + return true; + } + return false; + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java new file mode 100644 index 0000000000..a6d640cc48 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java @@ -0,0 +1,36 @@ +package org.utbot.fuzzing.samples; + +import org.utbot.fuzzing.samples.data.Recursive; +import org.utbot.fuzzing.samples.data.UserDefinedClass; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Objects { + + public int test(UserDefinedClass userDefinedClass) { + if (userDefinedClass.getX() > 5 && userDefinedClass.getY() < 10) { + return 1; + } else if (userDefinedClass.getZ() < 20) { + return 2; + } else { + return 3; + } + } + + public boolean testMe(Recursive r) { + if (r.val() > 10) { + return true; + } + return false; + } + + private int data; + + public static void foo(Objects a, Objects b) { + a.data = 1; + b.data = 2; + //noinspection ConstantValue + if (a.data == b.data) { + throw new IllegalArgumentException(); + } + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java new file mode 100644 index 0000000000..49ef688ac5 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java @@ -0,0 +1,67 @@ +package org.utbot.fuzzing.samples; + +import java.util.regex.Pattern; + +@SuppressWarnings({"unused", "SimplifiableConditionalExpression"}) +public class Strings { + + // should find a string that starts with "bad!" prefix + public static void test(String s) { + if (s.charAt(0) == "b".charAt(0)) { + if (s.charAt(1) == "a".charAt(0)) { + if (s.charAt(2) == "d".charAt(0)) { + if (s.charAt(3) == "!".charAt(0)) { + throw new IllegalArgumentException(); + } + } + } + } + } + + // should try to find the string with size 6 and with "!" in the end + public static void testStrRem(String str) { + if (!"world???".equals(str) && str.charAt(5) == '!' && str.length() == 6) { + throw new RuntimeException(); + } + } + + public boolean isValidUuid(String uuid) { + return isNotBlank(uuid) && uuid + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + public boolean isValidUuidShortVersion(String uuid) { + return uuid != null && uuid.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + public boolean isValidDouble(String value) { + return value.matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?([fFdD]?)") && value.contains("E") && value.contains("-"); + } + + static final String pattern = "\\d+"; + + public boolean isNumber(String s) { + return Pattern.matches(pattern, s) ? true : false; + } + + private static boolean isNotBlank(CharSequence cs) { + return !isBlank(cs); + } + + private static boolean isBlank(CharSequence cs) { + int strLen = length(cs); + if (strLen != 0) { + for (int i = 0; i < strLen; ++i) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + + } + return true; + } + + private static int length(CharSequence cs) { + return cs == null ? 0 : cs.length(); + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java new file mode 100644 index 0000000000..9f3ab45d71 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java @@ -0,0 +1,19 @@ +package org.utbot.fuzzing.samples.data; + +@SuppressWarnings("unused") +public class Recursive { + + private final int a; + + public Recursive(int a) { + this.a = a; + } + + public Recursive(Recursive r) { + this.a = r.a; + } + + public int val() { + return a; + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java new file mode 100644 index 0000000000..5733a78c55 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java @@ -0,0 +1,44 @@ +package org.utbot.fuzzing.samples.data; + +@SuppressWarnings("unused") +public class UserDefinedClass { + public int x; + private int y; + public int z; + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public UserDefinedClass setY(int y) { + this.y = y; + return this; + } + + public void setZ(int z) { + this.z = z; + } + + public int getZ() { + return z; + } + + public UserDefinedClass() { + } + + public UserDefinedClass(int x, int y) { + this.x = x; + this.y = y; + this.z = 0; + } + + public UserDefinedClass(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt similarity index 98% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt index bddde87d50..fbd702b59e 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt @@ -1,7 +1,5 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils -import org.utbot.fuzzer.CartesianProduct -import org.utbot.fuzzer.Combinations import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -11,7 +9,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.utbot.fuzzer.TooManyCombinationsException import java.util.BitSet import kotlin.math.pow import kotlin.random.Random diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt similarity index 98% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt index 6f1f6fd64c..af48867f06 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt @@ -1,10 +1,9 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.utbot.fuzzer.PseudoShuffledIntProgression import kotlin.random.Random class PseudoShuffledIntProgressionTest { diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt similarity index 96% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt index 764d13219d..cfc4ada3dc 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt @@ -1,11 +1,9 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.utbot.fuzzer.chooseOne -import org.utbot.fuzzer.flipCoin import kotlin.math.abs import kotlin.random.Random diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt similarity index 96% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt index a5cd8f62ab..cc3a6ba360 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt @@ -1,10 +1,7 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -import org.utbot.fuzzer.Trie -import org.utbot.fuzzer.stringTrieOf -import org.utbot.fuzzer.trieOf class TrieTest {