Skip to content

Commit a5eb0a6

Browse files
authored
Add bytecode transformation for if statement with call of String methods (#2446)
* Add bytecode transformation for if statement with call of String.equals method * Add bytecode transformation for String.startsWith and String.endsWith methods * Fix bug and add tests * Fix bug with computing maximum stack size and the maximum number of local variables of the method * Add flag to enable bytecode transformation * Fix bug
1 parent 1554a97 commit a5eb0a6

File tree

13 files changed

+593
-15
lines changed

13 files changed

+593
-15
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,15 @@ object UtSettings : AbstractSettings(logger, defaultKeyForSettingsPath, defaultS
466466
*/
467467
var useSandbox by getBooleanProperty(true)
468468

469+
/**
470+
* Transform bytecode in the instrumented process.
471+
*
472+
* If true, bytecode transformation will help fuzzing to find interesting input data, but the size of bytecode can increase.
473+
*
474+
* If false, bytecode won`t be changed.
475+
*/
476+
var useBytecodeTransformation by getBooleanProperty(false)
477+
469478
/**
470479
* Limit for number of generated tests per method (in each region)
471480
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.utbot.examples.samples.transformation;
2+
3+
public class StringMethodsCalls {
4+
public static boolean equalsWithEmptyString(String strToCompare) {
5+
if (strToCompare.equals("")) {
6+
return true;
7+
}
8+
return false;
9+
}
10+
11+
public static boolean equalsWithNotEmptyString(String strToCompare) {
12+
if (strToCompare.equals("abc")) {
13+
return true;
14+
}
15+
return false;
16+
}
17+
18+
public static boolean startsWithWithEmptyString(String strToCompare) {
19+
if (strToCompare.startsWith("")) {
20+
return true;
21+
}
22+
return false;
23+
}
24+
25+
public static boolean startsWithWithNotEmptyString(String strToCompare) {
26+
if (strToCompare.startsWith("abc")) {
27+
return true;
28+
}
29+
return false;
30+
}
31+
32+
public static boolean endsWithWithEmptyString(String strToCompare) {
33+
if (strToCompare.endsWith("")) {
34+
return true;
35+
}
36+
return false;
37+
}
38+
39+
public static boolean endsWithWithNotEmptyString(String strToCompare) {
40+
if (strToCompare.endsWith("abc")) {
41+
return true;
42+
}
43+
return false;
44+
}
45+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.utbot.examples
2+
3+
import org.junit.jupiter.api.Assertions.assertFalse
4+
import org.junit.jupiter.api.Assertions.assertTrue
5+
import org.junit.jupiter.api.Test
6+
import org.utbot.examples.samples.transformation.StringMethodsCalls
7+
import org.utbot.instrumentation.execute
8+
import org.utbot.instrumentation.instrumentation.transformation.BytecodeTransformation
9+
import org.utbot.instrumentation.withInstrumentation
10+
11+
class TestBytecodeTransformation {
12+
lateinit var utContext: AutoCloseable
13+
14+
@Test
15+
fun testStringEqualsWithEmptyStringCall() {
16+
withInstrumentation(
17+
BytecodeTransformation.Factory,
18+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
19+
) { executor ->
20+
val res1 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf(""))
21+
assertTrue(res1.getOrNull() as Boolean)
22+
23+
val res2 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf("abc"))
24+
assertFalse(res2.getOrNull() as Boolean)
25+
26+
val res3 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf(null))
27+
assertTrue(res3.exceptionOrNull() is NullPointerException)
28+
}
29+
}
30+
31+
@Test
32+
fun testStringEqualsWithNotEmptyStringCall() {
33+
withInstrumentation(
34+
BytecodeTransformation.Factory,
35+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
36+
) { executor ->
37+
val res1 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf(""))
38+
assertFalse(res1.getOrNull() as Boolean)
39+
40+
val res2 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf("abc"))
41+
assertTrue(res2.getOrNull() as Boolean)
42+
43+
val res3 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf("abcd"))
44+
assertFalse(res3.getOrNull() as Boolean)
45+
46+
val res4 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf(null))
47+
assertTrue(res4.exceptionOrNull() is NullPointerException)
48+
}
49+
}
50+
51+
@Test
52+
fun testStringStartsWithWithEmptyStringCall() {
53+
withInstrumentation(
54+
BytecodeTransformation.Factory,
55+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
56+
) { executor ->
57+
val res1 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf(""))
58+
assertTrue(res1.getOrNull() as Boolean)
59+
60+
val res2 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf("abc"))
61+
assertTrue(res2.getOrNull() as Boolean)
62+
63+
val res3 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf(null))
64+
assertTrue(res3.exceptionOrNull() is NullPointerException)
65+
}
66+
}
67+
68+
@Test
69+
fun testStringStartsWithWithNotEmptyStringCall() {
70+
withInstrumentation(
71+
BytecodeTransformation.Factory,
72+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
73+
) { executor ->
74+
val res1 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf(""))
75+
assertFalse(res1.getOrNull() as Boolean)
76+
77+
val res2 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("abc"))
78+
assertTrue(res2.getOrNull() as Boolean)
79+
80+
val res3 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("abcd"))
81+
assertTrue(res3.getOrNull() as Boolean)
82+
83+
val res4 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("aabc"))
84+
assertFalse(res4.getOrNull() as Boolean)
85+
86+
val res5 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf(null))
87+
assertTrue(res5.exceptionOrNull() is NullPointerException)
88+
}
89+
}
90+
91+
@Test
92+
fun testStringEndsWithWithEmptyString() {
93+
withInstrumentation(
94+
BytecodeTransformation.Factory,
95+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
96+
) { executor ->
97+
val res1 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf(""))
98+
assertTrue(res1.getOrNull() as Boolean)
99+
100+
val res2 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf("abc"))
101+
assertTrue(res2.getOrNull() as Boolean)
102+
103+
val res3 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf(null))
104+
assertTrue(res3.exceptionOrNull() is NullPointerException)
105+
}
106+
}
107+
108+
@Test
109+
fun testStringEndsWithWithNotEmptyString() {
110+
withInstrumentation(
111+
BytecodeTransformation.Factory,
112+
StringMethodsCalls::class.java.protectionDomain.codeSource.location.path
113+
) { executor ->
114+
val res1 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf(""))
115+
assertFalse(res1.getOrNull() as Boolean)
116+
117+
val res2 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("abc"))
118+
assertTrue(res2.getOrNull() as Boolean)
119+
120+
val res3 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("aabc"))
121+
assertTrue(res3.getOrNull() as Boolean)
122+
123+
val res4 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("abcd"))
124+
assertFalse(res4.getOrNull() as Boolean)
125+
126+
val res5 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf(null))
127+
assertTrue(res5.exceptionOrNull() is NullPointerException)
128+
}
129+
}
130+
}

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.lang.instrument.ClassFileTransformer
99
import java.nio.file.Paths
1010
import java.security.ProtectionDomain
1111
import kotlin.io.path.absolutePathString
12+
import kotlin.properties.Delegates
1213

1314

1415
private val logger = getLogger<DynamicClassTransformer>()
@@ -19,6 +20,7 @@ private val logger = getLogger<DynamicClassTransformer>()
1920
class DynamicClassTransformer : ClassFileTransformer {
2021
lateinit var transformer: ClassFileTransformer
2122

23+
var useBytecodeTransformation by Delegates.notNull<Boolean>()
2224
private val pathsToUserClasses = mutableSetOf<String>()
2325

2426
fun addUserPaths(paths: Iterable<String>) {

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SimpleUtExecutionInstrumentation.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.utbot.framework.plugin.api.UtModel
66
import org.utbot.framework.plugin.api.util.executable
77
import org.utbot.framework.plugin.api.util.signature
88
import org.utbot.framework.plugin.api.util.singleExecutableId
9+
import org.utbot.instrumentation.agent.Agent
910
import org.utbot.instrumentation.instrumentation.ArgumentList
1011
import org.utbot.instrumentation.instrumentation.InvokeInstrumentation
1112
import org.utbot.instrumentation.instrumentation.et.TraceHandler
@@ -18,6 +19,7 @@ import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicD
1819
import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController
1920
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
2021
import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor
22+
import org.utbot.instrumentation.instrumentation.transformation.BytecodeTransformer
2123
import java.security.ProtectionDomain
2224
import kotlin.reflect.jvm.javaMethod
2325

@@ -144,6 +146,12 @@ class SimpleUtExecutionInstrumentation(
144146
): ByteArray {
145147
val instrumenter = Instrumenter(classfileBuffer, loader)
146148

149+
if (Agent.dynamicClassTransformer.useBytecodeTransformation) {
150+
instrumenter.visitClass { writer ->
151+
BytecodeTransformer(writer)
152+
}
153+
}
154+
147155
traceHandler.registerClass(className)
148156
instrumenter.visitInstructions(traceHandler.computeInstructionVisitor(className))
149157

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.utbot.instrumentation.instrumentation.transformation
2+
3+
import org.utbot.framework.plugin.api.FieldId
4+
import org.utbot.instrumentation.instrumentation.ArgumentList
5+
import org.utbot.instrumentation.instrumentation.Instrumentation
6+
import org.utbot.instrumentation.instrumentation.InvokeInstrumentation
7+
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
8+
import java.security.ProtectionDomain
9+
10+
/**
11+
* This instrumentation transforms bytecode and delegates invoking a given function to [InvokeInstrumentation].
12+
*/
13+
class BytecodeTransformation : Instrumentation<Result<*>> {
14+
private val invokeInstrumentation = InvokeInstrumentation()
15+
16+
override fun invoke(
17+
clazz: Class<*>,
18+
methodSignature: String,
19+
arguments: ArgumentList,
20+
parameters: Any?
21+
): Result<*> = invokeInstrumentation.invoke(clazz, methodSignature, arguments, parameters)
22+
23+
override fun getStaticField(fieldId: FieldId): Result<*> = invokeInstrumentation.getStaticField(fieldId)
24+
25+
override fun transform(
26+
loader: ClassLoader?,
27+
className: String?,
28+
classBeingRedefined: Class<*>?,
29+
protectionDomain: ProtectionDomain?,
30+
classfileBuffer: ByteArray
31+
): ByteArray {
32+
val instrumenter = Instrumenter(classfileBuffer, loader)
33+
34+
instrumenter.visitClass { writer ->
35+
BytecodeTransformer(writer)
36+
}
37+
38+
return instrumenter.classByteCode
39+
}
40+
41+
object Factory : Instrumentation.Factory<Result<*>, BytecodeTransformation> {
42+
override fun create(): BytecodeTransformation = BytecodeTransformation()
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.utbot.instrumentation.instrumentation.transformation
2+
3+
import org.objectweb.asm.ClassVisitor
4+
import org.objectweb.asm.MethodVisitor
5+
import org.utbot.instrumentation.Settings
6+
import org.utbot.instrumentation.instrumentation.transformation.adapters.StringMethodsAdapter
7+
8+
/**
9+
* Main class for the transformation.
10+
* Bytecode transformations will be combined in this class.
11+
*/
12+
class BytecodeTransformer(classVisitor: ClassVisitor) : ClassVisitor(Settings.ASM_API, classVisitor) {
13+
override fun visitMethod(
14+
access: Int,
15+
name: String?,
16+
descriptor: String?,
17+
signature: String?,
18+
exceptions: Array<out String>?
19+
): MethodVisitor {
20+
val methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
21+
return StringMethodsAdapter(api, access, descriptor, methodVisitor)
22+
}
23+
}

0 commit comments

Comments
 (0)