Skip to content

Commit 78df295

Browse files
authored
Use regex for generating string when regex pattern is found #778 (#793)
1 parent 73b6e89 commit 78df295

File tree

14 files changed

+252
-85
lines changed

14 files changed

+252
-85
lines changed

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.util.longClassId
1111
import org.utbot.framework.plugin.api.util.shortClassId
1212
import org.utbot.framework.plugin.api.util.stringClassId
1313
import mu.KotlinLogging
14+
import org.utbot.framework.util.executableId
1415
import soot.BooleanType
1516
import soot.ByteType
1617
import soot.CharType
@@ -39,6 +40,7 @@ import soot.jimple.internal.JLeExpr
3940
import soot.jimple.internal.JLookupSwitchStmt
4041
import soot.jimple.internal.JLtExpr
4142
import soot.jimple.internal.JNeExpr
43+
import soot.jimple.internal.JStaticInvokeExpr
4244
import soot.jimple.internal.JTableSwitchStmt
4345
import soot.jimple.internal.JVirtualInvokeExpr
4446
import soot.toolkits.graph.ExceptionalUnitGraph
@@ -64,6 +66,7 @@ fun collectConstantsForFuzzer(graph: ExceptionalUnitGraph): Set<FuzzedConcreteVa
6466
ConstantsFromSwitchCase,
6567
BoundValuesForDoubleChecks,
6668
StringConstant,
69+
RegexByVarStringConstant,
6770
).flatMap { finder ->
6871
try {
6972
finder.find(graph, unit, value)
@@ -113,8 +116,8 @@ private object ConstantsFromIfStatement: ConstantsFinder {
113116
val exactValue = value.plainValue
114117
val local = useBoxes[(valueIndex + 1) % 2]
115118
var op = sootIfToFuzzedOp(ifStatement)
116-
if (valueIndex == 0) {
117-
op = op.reverseOrElse { it }
119+
if (valueIndex == 0 && op is FuzzedContext.Comparison) {
120+
op = op.reverse()
118121
}
119122
// Soot loads any integer type as an Int,
120123
// therefore we try to guess target type using second value
@@ -146,7 +149,7 @@ private object ConstantsFromCast: ConstantsFinder {
146149
if (next is JAssignStmt) {
147150
val const = next.useBoxes.findFirstInstanceOf<Constant>()
148151
if (const != null) {
149-
val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedOp.NONE
152+
val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedContext.Unknown
150153
val exactValue = const.plainValue as Number
151154
return listOfNotNull(
152155
when (value.op.type) {
@@ -170,12 +173,12 @@ private object ConstantsFromSwitchCase: ConstantsFinder {
170173
val result = mutableListOf<FuzzedConcreteValue>()
171174
if (unit is JTableSwitchStmt) {
172175
for (i in unit.lowIndex..unit.highIndex) {
173-
result.add(FuzzedConcreteValue(intClassId, i, FuzzedOp.EQ))
176+
result.add(FuzzedConcreteValue(intClassId, i, FuzzedContext.Comparison.EQ))
174177
}
175178
}
176179
if (unit is JLookupSwitchStmt) {
177180
unit.lookupValues.asSequence().filterIsInstance<IntConstant>().forEach {
178-
result.add(FuzzedConcreteValue(intClassId, it.value, FuzzedOp.EQ))
181+
result.add(FuzzedConcreteValue(intClassId, it.value, FuzzedContext.Comparison.EQ))
179182
}
180183
}
181184
return result
@@ -205,7 +208,7 @@ private object StringConstant: ConstantsFinder {
205208
if (value.method.declaringClass.name == "java.lang.String") {
206209
val stringConstantWasPassedAsArg = unit.useBoxes.findFirstInstanceOf<Constant>()?.plainValue
207210
if (stringConstantWasPassedAsArg != null && stringConstantWasPassedAsArg is String) {
208-
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedOp.CH))
211+
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedContext.Call(value.method.executableId)))
209212
}
210213
val stringConstantWasPassedAsThis = graph.getPredsOf(unit)
211214
?.filterIsInstance<JAssignStmt>()
@@ -214,12 +217,29 @@ private object StringConstant: ConstantsFinder {
214217
?.findFirstInstanceOf<Constant>()
215218
?.plainValue
216219
if (stringConstantWasPassedAsThis != null && stringConstantWasPassedAsThis is String) {
217-
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedOp.CH))
220+
return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedContext.Call(value.method.executableId)))
218221
}
219222
}
220223
return emptyList()
221224
}
225+
}
222226

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

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

243263
private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) {
244-
is JEqExpr -> FuzzedOp.NE
245-
is JNeExpr -> FuzzedOp.EQ
246-
is JGtExpr -> FuzzedOp.LE
247-
is JGeExpr -> FuzzedOp.LT
248-
is JLtExpr -> FuzzedOp.GE
249-
is JLeExpr -> FuzzedOp.GT
250-
else -> FuzzedOp.NONE
264+
is JEqExpr -> FuzzedContext.Comparison.NE
265+
is JNeExpr -> FuzzedContext.Comparison.EQ
266+
is JGtExpr -> FuzzedContext.Comparison.LE
267+
is JGeExpr -> FuzzedContext.Comparison.LT
268+
is JLtExpr -> FuzzedContext.Comparison.GE
269+
is JLeExpr -> FuzzedContext.Comparison.GT
270+
else -> FuzzedContext.Unknown
251271
}
252272

253273
private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first()

utbot-fuzzers/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
api project(':utbot-framework-api')
99
implementation "com.github.UnitTestBot:soot:${soot_commit_hash}"
1010
implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version
11+
implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgen_version
1112
}
1213

1314
compileJava {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.utbot.fuzzer
2+
3+
import org.utbot.framework.plugin.api.ClassId
4+
5+
/**
6+
* Object to pass concrete values to fuzzer
7+
*/
8+
data class FuzzedConcreteValue(
9+
val classId: ClassId,
10+
val value: Any,
11+
val fuzzedContext: FuzzedContext = FuzzedContext.Unknown,
12+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.utbot.fuzzer
2+
3+
import org.utbot.framework.plugin.api.ExecutableId
4+
5+
/**
6+
* Context is a bit of information about [FuzzedConcreteValue]'s conditions.
7+
*
8+
* For example, it can be:
9+
*
10+
* 1. Comparison operations: `a > 2`
11+
* 2. Method call: `Double.isNaN(2.0)`
12+
*/
13+
sealed interface FuzzedContext {
14+
15+
object Unknown : FuzzedContext
16+
17+
class Call(
18+
val method: ExecutableId
19+
) : FuzzedContext {
20+
override fun toString(): String {
21+
return method.toString()
22+
}
23+
}
24+
25+
enum class Comparison(
26+
val sign: String
27+
) : FuzzedContext {
28+
EQ("=="),
29+
NE("!="),
30+
GT(">"),
31+
GE(">="),
32+
LT("<"),
33+
LE("<="),
34+
;
35+
36+
fun reverse(): Comparison = when (this) {
37+
EQ -> NE
38+
NE -> EQ
39+
GT -> LE
40+
LT -> GE
41+
LE -> GT
42+
GE -> LT
43+
}
44+
}
45+
}

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

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -57,40 +57,4 @@ class FuzzedMethodDescription(
5757
executableId.parameters,
5858
concreteValues
5959
)
60-
}
61-
62-
/**
63-
* Object to pass concrete values to fuzzer
64-
*/
65-
data class FuzzedConcreteValue(
66-
val classId: ClassId,
67-
val value: Any,
68-
val relativeOp: FuzzedOp = FuzzedOp.NONE,
69-
)
70-
71-
enum class FuzzedOp(val sign: String?) {
72-
NONE(null),
73-
EQ("=="),
74-
NE("!="),
75-
GT(">"),
76-
GE(">="),
77-
LT("<"),
78-
LE("<="),
79-
CH(null), // changed or called
80-
;
81-
82-
fun isComparisonOp() = this == EQ || this == NE || this == GT || this == GE || this == LT || this == LE
83-
84-
fun reverseOrNull() : FuzzedOp? = when(this) {
85-
EQ -> NE
86-
NE -> EQ
87-
GT -> LE
88-
LT -> GE
89-
LE -> GT
90-
GE -> LT
91-
else -> null
92-
}
93-
94-
fun reverseOrElse(another: (FuzzedOp) -> FuzzedOp): FuzzedOp =
95-
reverseOrNull() ?: another(this)
9660
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import org.utbot.framework.plugin.api.UtModel
66
* Fuzzed Value stores information about concrete UtModel, reference to [ModelProvider]
77
* and reasons about why this value was generated.
88
*/
9-
class FuzzedValue(
9+
open class FuzzedValue(
1010
val model: UtModel,
1111
val createdBy: ModelProvider? = null,
1212
) {

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.utbot.fuzzer
22

33
import mu.KotlinLogging
44
import org.utbot.fuzzer.mutators.NumberRandomMutator
5+
import org.utbot.fuzzer.mutators.RegexStringModelMutator
56
import org.utbot.fuzzer.mutators.StringRandomMutator
67
import org.utbot.fuzzer.providers.ArrayModelProvider
78
import org.utbot.fuzzer.providers.CharToStringModelProvider
@@ -12,6 +13,7 @@ import org.utbot.fuzzer.providers.ObjectModelProvider
1213
import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
1314
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
1415
import org.utbot.fuzzer.providers.PrimitivesModelProvider
16+
import org.utbot.fuzzer.providers.RegexModelProvider
1517
import org.utbot.fuzzer.providers.StringConstantModelProvider
1618
import java.util.*
1719
import java.util.concurrent.atomic.AtomicInteger
@@ -154,6 +156,7 @@ fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Mode
154156
EnumModelProvider(idGenerator),
155157
ConstantsModelProvider,
156158
StringConstantModelProvider,
159+
RegexModelProvider,
157160
CharToStringModelProvider,
158161
PrimitivesModelProvider,
159162
PrimitiveWrapperModelProvider,
@@ -169,14 +172,19 @@ fun objectModelProviders(idGenerator: IdentityPreservingIdGenerator<Int>): Model
169172
ArrayModelProvider(idGenerator),
170173
EnumModelProvider(idGenerator),
171174
StringConstantModelProvider,
175+
RegexModelProvider,
172176
CharToStringModelProvider,
173177
ConstantsModelProvider,
174178
PrimitiveDefaultsModelProvider,
175179
PrimitiveWrapperModelProvider,
176180
)
177181
}
178182

179-
fun defaultModelMutators(): List<ModelMutator> = listOf(StringRandomMutator, NumberRandomMutator)
183+
fun defaultModelMutators(): List<ModelMutator> = listOf(
184+
StringRandomMutator,
185+
RegexStringModelMutator,
186+
NumberRandomMutator,
187+
)
180188

181189
/**
182190
* Tries to mutate a random value from the seed.

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ interface ModelMutator {
3636
index: Int,
3737
value: FuzzedValue,
3838
random: Random
39-
) : FuzzedValue? {
40-
return null
41-
}
39+
) : FuzzedValue?
4240

4341
fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue {
4442
return FuzzedValue(this, template.createdBy).apply(block)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.utbot.fuzzer.mutators
2+
3+
import com.github.curiousoddman.rgxgen.RgxGen
4+
import org.utbot.framework.plugin.api.UtPrimitiveModel
5+
import org.utbot.fuzzer.FuzzedMethodDescription
6+
import org.utbot.fuzzer.FuzzedValue
7+
import org.utbot.fuzzer.ModelMutator
8+
import org.utbot.fuzzer.providers.RegexFuzzedValue
9+
import org.utbot.fuzzer.providers.RegexModelProvider
10+
import kotlin.random.Random
11+
import kotlin.random.asJavaRandom
12+
13+
/**
14+
* Provides different regex value for a concrete regex pattern
15+
*/
16+
object RegexStringModelMutator : ModelMutator {
17+
18+
override fun mutate(
19+
description: FuzzedMethodDescription,
20+
index: Int,
21+
value: FuzzedValue,
22+
random: Random
23+
): FuzzedValue? {
24+
if (value is RegexFuzzedValue) {
25+
val string = RgxGen(value.regex).apply {
26+
setProperties(RegexModelProvider.rgxGenProperties)
27+
}.generate(random.asJavaRandom())
28+
return RegexFuzzedValue(UtPrimitiveModel(string).mutatedFrom(value) {
29+
summary = "%var% = mutated regex ${value.regex}"
30+
}, value.regex)
31+
}
32+
return null
33+
}
34+
35+
}

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ object StringRandomMutator : ModelMutator {
3737
if (random.flipCoin(probability = 50)) {
3838
result = tryRemoveChar(random, result, position) ?: string
3939
}
40-
if (random.flipCoin(probability = 50)) {
40+
if (random.flipCoin(probability = 50) && result.length < 1000) {
4141
result = tryAddChar(random, result, position)
4242
}
4343
return result

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package org.utbot.fuzzer.providers
22

33
import org.utbot.framework.plugin.api.UtPrimitiveModel
44
import org.utbot.framework.plugin.api.util.isPrimitive
5+
import org.utbot.fuzzer.FuzzedContext
56
import org.utbot.fuzzer.FuzzedMethodDescription
6-
import org.utbot.fuzzer.FuzzedOp
77
import org.utbot.fuzzer.FuzzedParameter
88
import org.utbot.fuzzer.FuzzedValue
99
import org.utbot.fuzzer.ModelProvider
@@ -32,9 +32,9 @@ object ConstantsModelProvider : ModelProvider {
3232
}
3333
}
3434

35-
private fun modifyValue(value: Any, op: FuzzedOp): FuzzedValue? {
36-
if (!op.isComparisonOp()) return null
37-
val multiplier = if (op == FuzzedOp.LT || op == FuzzedOp.GE) -1 else 1
35+
private fun modifyValue(value: Any, op: FuzzedContext): FuzzedValue? {
36+
if (op !is FuzzedContext.Comparison) return null
37+
val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.GE) -1 else 1
3838
return when(value) {
3939
is Boolean -> value.not()
4040
is Byte -> value + multiplier.toByte()
@@ -46,8 +46,8 @@ object ConstantsModelProvider : ModelProvider {
4646
is Double -> value + multiplier.toDouble()
4747
else -> null
4848
}?.let { UtPrimitiveModel(it).fuzzed { summary = "%var% ${
49-
(if (op == FuzzedOp.EQ || op == FuzzedOp.LE || op == FuzzedOp.GE) {
50-
op.reverseOrNull() ?: error("cannot find reverse operation for $op")
49+
(if (op == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) {
50+
op.reverse()
5151
} else op).sign
5252
} $value" } }
5353
}

0 commit comments

Comments
 (0)