Skip to content

Use regex for generating string when regex pattern is found #778 #793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.util.longClassId
import org.utbot.framework.plugin.api.util.shortClassId
import org.utbot.framework.plugin.api.util.stringClassId
import mu.KotlinLogging
import org.utbot.framework.util.executableId
import soot.BooleanType
import soot.ByteType
import soot.CharType
Expand Down Expand Up @@ -39,6 +40,7 @@ import soot.jimple.internal.JLeExpr
import soot.jimple.internal.JLookupSwitchStmt
import soot.jimple.internal.JLtExpr
import soot.jimple.internal.JNeExpr
import soot.jimple.internal.JStaticInvokeExpr
import soot.jimple.internal.JTableSwitchStmt
import soot.jimple.internal.JVirtualInvokeExpr
import soot.toolkits.graph.ExceptionalUnitGraph
Expand All @@ -64,6 +66,7 @@ fun collectConstantsForFuzzer(graph: ExceptionalUnitGraph): Set<FuzzedConcreteVa
ConstantsFromSwitchCase,
BoundValuesForDoubleChecks,
StringConstant,
RegexByVarStringConstant,
).flatMap { finder ->
try {
finder.find(graph, unit, value)
Expand Down Expand Up @@ -113,8 +116,8 @@ private object ConstantsFromIfStatement: ConstantsFinder {
val exactValue = value.plainValue
val local = useBoxes[(valueIndex + 1) % 2]
var op = sootIfToFuzzedOp(ifStatement)
if (valueIndex == 0) {
op = op.reverseOrElse { it }
if (valueIndex == 0 && op is FuzzedContext.Comparison) {
op = op.reverse()
}
// Soot loads any integer type as an Int,
// therefore we try to guess target type using second value
Expand Down Expand Up @@ -146,7 +149,7 @@ private object ConstantsFromCast: ConstantsFinder {
if (next is JAssignStmt) {
val const = next.useBoxes.findFirstInstanceOf<Constant>()
if (const != null) {
val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedOp.NONE
val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedContext.Unknown
val exactValue = const.plainValue as Number
return listOfNotNull(
when (value.op.type) {
Expand All @@ -170,12 +173,12 @@ private object ConstantsFromSwitchCase: ConstantsFinder {
val result = mutableListOf<FuzzedConcreteValue>()
if (unit is JTableSwitchStmt) {
for (i in unit.lowIndex..unit.highIndex) {
result.add(FuzzedConcreteValue(intClassId, i, FuzzedOp.EQ))
result.add(FuzzedConcreteValue(intClassId, i, FuzzedContext.Comparison.EQ))
}
}
if (unit is JLookupSwitchStmt) {
unit.lookupValues.asSequence().filterIsInstance<IntConstant>().forEach {
result.add(FuzzedConcreteValue(intClassId, it.value, FuzzedOp.EQ))
result.add(FuzzedConcreteValue(intClassId, it.value, FuzzedContext.Comparison.EQ))
}
}
return result
Expand Down Expand Up @@ -205,7 +208,7 @@ private object StringConstant: ConstantsFinder {
if (value.method.declaringClass.name == "java.lang.String") {
val stringConstantWasPassedAsArg = unit.useBoxes.findFirstInstanceOf<Constant>()?.plainValue
if (stringConstantWasPassedAsArg != null && stringConstantWasPassedAsArg is String) {
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedOp.CH))
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedContext.Call(value.method.executableId)))
}
val stringConstantWasPassedAsThis = graph.getPredsOf(unit)
?.filterIsInstance<JAssignStmt>()
Expand All @@ -214,12 +217,29 @@ private object StringConstant: ConstantsFinder {
?.findFirstInstanceOf<Constant>()
?.plainValue
if (stringConstantWasPassedAsThis != null && stringConstantWasPassedAsThis is String) {
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedOp.CH))
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedContext.Call(value.method.executableId)))
}
}
return emptyList()
}
}

/**
* Finds strings that are used inside Pattern's methods.
*
* Due to compiler optimizations it should work when a string is assigned to a variable or static final field.
*/
private object RegexByVarStringConstant: ConstantsFinder {
override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List<FuzzedConcreteValue> {
if (unit !is JAssignStmt || value !is JStaticInvokeExpr) return emptyList()
if (value.method.declaringClass.name == "java.util.regex.Pattern") {
val stringConstantWasPassedAsArg = unit.useBoxes.findFirstInstanceOf<Constant>()?.plainValue
if (stringConstantWasPassedAsArg != null && stringConstantWasPassedAsArg is String) {
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedContext.Call(value.method.executableId)))
}
}
return emptyList()
}
}

private object ConstantsAsIs: ConstantsFinder {
Expand All @@ -241,13 +261,13 @@ private val Constant.plainValue
get() = javaClass.getField("value")[this]

private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) {
is JEqExpr -> FuzzedOp.NE
is JNeExpr -> FuzzedOp.EQ
is JGtExpr -> FuzzedOp.LE
is JGeExpr -> FuzzedOp.LT
is JLtExpr -> FuzzedOp.GE
is JLeExpr -> FuzzedOp.GT
else -> FuzzedOp.NONE
is JEqExpr -> FuzzedContext.Comparison.NE
is JNeExpr -> FuzzedContext.Comparison.EQ
is JGtExpr -> FuzzedContext.Comparison.LE
is JGeExpr -> FuzzedContext.Comparison.LT
is JLtExpr -> FuzzedContext.Comparison.GE
is JLeExpr -> FuzzedContext.Comparison.GT
else -> FuzzedContext.Unknown
}

private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()
1 change: 1 addition & 0 deletions utbot-fuzzers/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
api project(':utbot-framework-api')
implementation "com.github.UnitTestBot:soot:${soot_commit_hash}"
implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version
implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgen_version
}

compileJava {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.utbot.fuzzer

import org.utbot.framework.plugin.api.ClassId

/**
* Object to pass concrete values to fuzzer
*/
data class FuzzedConcreteValue(
val classId: ClassId,
val value: Any,
val fuzzedContext: FuzzedContext = FuzzedContext.Unknown,
)
45 changes: 45 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.utbot.fuzzer

import org.utbot.framework.plugin.api.ExecutableId

/**
* Context is a bit of information about [FuzzedConcreteValue]'s conditions.
*
* For example, it can be:
*
* 1. Comparison operations: `a > 2`
* 2. Method call: `Double.isNaN(2.0)`
*/
sealed interface FuzzedContext {

object Unknown : FuzzedContext

class Call(
val method: ExecutableId
) : FuzzedContext {
override fun toString(): String {
return method.toString()
}
}

enum class Comparison(
val sign: String
) : FuzzedContext {
EQ("=="),
NE("!="),
GT(">"),
GE(">="),
LT("<"),
LE("<="),
;

fun reverse(): Comparison = when (this) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java Comparator<T> interface (and its counterpart in the Kotlin stdlib) has reversed method that returns a comparator with reverse ordering. What do you think about using reversed() instead of reverse() here? I think it could be a bit more readable (at least it would be for me -- I used to read 'reverse()` as an in-place operation on collections).

EQ -> NE
NE -> EQ
GT -> LE
LT -> GE
LE -> GT
GE -> LT
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,40 +57,4 @@ class FuzzedMethodDescription(
executableId.parameters,
concreteValues
)
}

/**
* Object to pass concrete values to fuzzer
*/
data class FuzzedConcreteValue(
val classId: ClassId,
val value: Any,
val relativeOp: FuzzedOp = FuzzedOp.NONE,
)

enum class FuzzedOp(val sign: String?) {
NONE(null),
EQ("=="),
NE("!="),
GT(">"),
GE(">="),
LT("<"),
LE("<="),
CH(null), // changed or called
;

fun isComparisonOp() = this == EQ || this == NE || this == GT || this == GE || this == LT || this == LE

fun reverseOrNull() : FuzzedOp? = when(this) {
EQ -> NE
NE -> EQ
GT -> LE
LT -> GE
LE -> GT
GE -> LT
else -> null
}

fun reverseOrElse(another: (FuzzedOp) -> FuzzedOp): FuzzedOp =
reverseOrNull() ?: another(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.utbot.framework.plugin.api.UtModel
* Fuzzed Value stores information about concrete UtModel, reference to [ModelProvider]
* and reasons about why this value was generated.
*/
class FuzzedValue(
open class FuzzedValue(
val model: UtModel,
val createdBy: ModelProvider? = null,
) {
Expand Down
10 changes: 9 additions & 1 deletion utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.utbot.fuzzer

import mu.KotlinLogging
import org.utbot.fuzzer.mutators.NumberRandomMutator
import org.utbot.fuzzer.mutators.RegexStringModelMutator
import org.utbot.fuzzer.mutators.StringRandomMutator
import org.utbot.fuzzer.providers.ArrayModelProvider
import org.utbot.fuzzer.providers.CharToStringModelProvider
Expand All @@ -12,6 +13,7 @@ import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
import org.utbot.fuzzer.providers.PrimitivesModelProvider
import org.utbot.fuzzer.providers.RegexModelProvider
import org.utbot.fuzzer.providers.StringConstantModelProvider
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -154,6 +156,7 @@ fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Mode
EnumModelProvider(idGenerator),
ConstantsModelProvider,
StringConstantModelProvider,
RegexModelProvider,
CharToStringModelProvider,
PrimitivesModelProvider,
PrimitiveWrapperModelProvider,
Expand All @@ -169,14 +172,19 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Model
ArrayModelProvider(idGenerator),
EnumModelProvider(idGenerator),
StringConstantModelProvider,
RegexModelProvider,
CharToStringModelProvider,
ConstantsModelProvider,
PrimitiveDefaultsModelProvider,
PrimitiveWrapperModelProvider,
)
}

fun defaultModelMutators(): List<ModelMutator> = listOf(StringRandomMutator, NumberRandomMutator)
fun defaultModelMutators(): List<ModelMutator> = listOf(
StringRandomMutator,
RegexStringModelMutator,
NumberRandomMutator,
)

/**
* Tries to mutate a random value from the seed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ interface ModelMutator {
index: Int,
value: FuzzedValue,
random: Random
) : FuzzedValue? {
return null
}
) : FuzzedValue?

fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue {
return FuzzedValue(this, template.createdBy).apply(block)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.utbot.fuzzer.mutators

import com.github.curiousoddman.rgxgen.RgxGen
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.providers.RegexFuzzedValue
import org.utbot.fuzzer.providers.RegexModelProvider
import kotlin.random.Random
import kotlin.random.asJavaRandom

/**
* Provides different regex value for a concrete regex pattern
*/
object RegexStringModelMutator : ModelMutator {

override fun mutate(
description: FuzzedMethodDescription,
index: Int,
value: FuzzedValue,
random: Random
): FuzzedValue? {
if (value is RegexFuzzedValue) {
val string = RgxGen(value.regex).apply {
setProperties(RegexModelProvider.rgxGenProperties)
}.generate(random.asJavaRandom())
return RegexFuzzedValue(UtPrimitiveModel(string).mutatedFrom(value) {
summary = "%var% = mutated regex ${value.regex}"
}, value.regex)
}
return null
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ object StringRandomMutator : ModelMutator {
if (random.flipCoin(probability = 50)) {
result = tryRemoveChar(random, result, position) ?: string
}
if (random.flipCoin(probability = 50)) {
if (random.flipCoin(probability = 50) && result.length < 1000) {
result = tryAddChar(random, result, position)
}
return result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package org.utbot.fuzzer.providers

import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.util.isPrimitive
import org.utbot.fuzzer.FuzzedContext
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedOp
import org.utbot.fuzzer.FuzzedParameter
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
Expand Down Expand Up @@ -32,9 +32,9 @@ object ConstantsModelProvider : ModelProvider {
}
}

private fun modifyValue(value: Any, op: FuzzedOp): FuzzedValue? {
if (!op.isComparisonOp()) return null
val multiplier = if (op == FuzzedOp.LT || op == FuzzedOp.GE) -1 else 1
private fun modifyValue(value: Any, op: FuzzedContext): FuzzedValue? {
if (op !is FuzzedContext.Comparison) return null
val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.GE) -1 else 1
return when(value) {
is Boolean -> value.not()
is Byte -> value + multiplier.toByte()
Expand All @@ -46,8 +46,8 @@ object ConstantsModelProvider : ModelProvider {
is Double -> value + multiplier.toDouble()
else -> null
}?.let { UtPrimitiveModel(it).fuzzed { summary = "%var% ${
(if (op == FuzzedOp.EQ || op == FuzzedOp.LE || op == FuzzedOp.GE) {
op.reverseOrNull() ?: error("cannot find reverse operation for $op")
(if (op == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) {
op.reverse()
} else op).sign
} $value" } }
}
Expand Down
Loading