Skip to content

Commit 8a87578

Browse files
Update rendering, fix JcSpringMvcTestTransformer (UnitTestBot#60)
* fix: lint * fix: inherit test method annotations from fakeMethod * feat: introduce reflection utils renderer, remove spring import manager * Fix class expression render for primitive and array types (UnitTestBot#144) * Fix empty unbound wildcard type bound (UnitTestBot#147) * feat: introduce reflection utils inline strategy Signed-off-by: Andrew Petrukhin <dartmol2300@gmail.com> * refactor: update reflection utils inline strategy ownership * fix: update JcSpringMvcTestTransformer.kt to match actual context init * fix: style * feat: remove unsafe import manager * fix: pass isAccessibleFromTestClass separately from inline strategy * fix: remove file renderer from unsafe utils renderer * fix: style --------- Signed-off-by: Andrew Petrukhin <dartmol2300@gmail.com>
1 parent bc1a731 commit 8a87578

34 files changed

+1035
-561
lines changed

usvm-jvm-rendering/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ dependencies {
1010
implementation("com.github.javaparser:javaparser-symbol-solver-core:3.26.3")
1111
}
1212

13+
tasks.withType<ProcessResources> {
14+
val reflectionUtils = project.sourceSets.main.get().java.find { file ->
15+
file.name == "ReflectionUtils.java"
16+
}
17+
18+
from(reflectionUtils)
19+
}
20+
1321
publishing {
1422
publications {
1523
create<MavenPublication>("maven") {

usvm-jvm-rendering/src/main/kotlin/org/usvm/jvm/rendering/JcFileRendererFactory.kt

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,30 @@ object JcTestFileRendererFactory {
8787
cu: CompilationUnit,
8888
cp: JcClasspath,
8989
testClassInfo: JcTestClassInfo,
90-
shouldInlineUsvmUtils: Boolean
90+
reflectionUtilsInlineStrategy: ReflectionUtilsInlineStrategy,
91+
isAccessibleFromTestClass: ((JcClassOrInterface) -> Boolean)? = null
9192
): JcTestFileRenderer {
93+
check(testClassInfo !is JcTestClassInfo.SpringUnit && testClassInfo !is JcTestClassInfo.SpringMvc || isAccessibleFromTestClass != null) {
94+
"isAccessible predicate should not be null for spring renderer"
95+
}
96+
9297
return when (testClassInfo) {
93-
is JcTestClassInfo.SpringMvc -> JcSpringMvcTestFileRenderer(testClassInfo.clazz.toType(), cu, cp, shouldInlineUsvmUtils)
94-
is JcTestClassInfo.SpringUnit -> JcSpringUnitTestFileRenderer(cu, cp, shouldInlineUsvmUtils)
95-
is JcTestClassInfo.Unsafe -> JcUnsafeTestFileRenderer(cu, cp, shouldInlineUsvmUtils)
98+
is JcTestClassInfo.SpringMvc -> JcSpringMvcTestFileRenderer(
99+
testClassInfo.clazz.toType(),
100+
cu,
101+
cp,
102+
reflectionUtilsInlineStrategy,
103+
isAccessibleFromTestClass!!
104+
)
105+
106+
is JcTestClassInfo.SpringUnit -> JcSpringUnitTestFileRenderer(
107+
cu,
108+
cp,
109+
reflectionUtilsInlineStrategy,
110+
isAccessibleFromTestClass!!
111+
)
112+
113+
is JcTestClassInfo.Unsafe -> JcUnsafeTestFileRenderer(cu, cp, reflectionUtilsInlineStrategy)
96114
is JcTestClassInfo.Base -> JcTestFileRenderer(cu, cp)
97115
}
98116
}
@@ -101,13 +119,31 @@ object JcTestFileRendererFactory {
101119
packageName: String?,
102120
cp: JcClasspath,
103121
testClassInfo: JcTestClassInfo,
104-
shouldInlineUsvmUtils: Boolean
122+
reflectionUtilsInlineStrategy: ReflectionUtilsInlineStrategy,
123+
isAccessibleFromTestClass: ((JcClassOrInterface) -> Boolean)? = null
105124
): JcTestFileRenderer {
125+
check(testClassInfo !is JcTestClassInfo.SpringUnit && testClassInfo !is JcTestClassInfo.SpringMvc || isAccessibleFromTestClass != null) {
126+
"isAccessible predicate should not be null for spring renderer"
127+
}
128+
106129
return when (testClassInfo) {
107-
is JcTestClassInfo.SpringMvc -> JcSpringMvcTestFileRenderer(testClassInfo.clazz.toType(), packageName, cp, shouldInlineUsvmUtils)
108-
is JcTestClassInfo.SpringUnit -> JcSpringUnitTestFileRenderer(packageName, cp, shouldInlineUsvmUtils)
109-
is JcTestClassInfo.Unsafe -> JcUnsafeTestFileRenderer(packageName, cp, shouldInlineUsvmUtils)
130+
is JcTestClassInfo.SpringMvc -> JcSpringMvcTestFileRenderer(
131+
testClassInfo.clazz.toType(),
132+
packageName,
133+
cp,
134+
reflectionUtilsInlineStrategy,
135+
isAccessibleFromTestClass!!
136+
)
137+
138+
is JcTestClassInfo.SpringUnit -> JcSpringUnitTestFileRenderer(
139+
packageName,
140+
cp,
141+
reflectionUtilsInlineStrategy,
142+
isAccessibleFromTestClass!!
143+
)
144+
145+
is JcTestClassInfo.Unsafe -> JcUnsafeTestFileRenderer(packageName, cp, reflectionUtilsInlineStrategy)
110146
is JcTestClassInfo.Base -> JcTestFileRenderer(packageName, cp)
111147
}
112148
}
113-
}
149+
}

usvm-jvm-rendering/src/main/kotlin/org/usvm/jvm/rendering/JcTestsRenderer.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.usvm.jvm.rendering
22

33
import com.github.javaparser.StaticJavaParser
44
import com.github.javaparser.printer.DefaultPrettyPrinter
5+
import org.jacodb.api.jvm.JcClassOrInterface
56
import org.jacodb.api.jvm.JcClasspath
67
import org.usvm.jvm.rendering.testRenderer.JcTestInfo
78
import org.usvm.jvm.rendering.testTransformers.JcCallCtorTransformer
@@ -19,7 +20,12 @@ class JcTestsRenderer {
1920
JcDeadCodeTransformer()
2021
)
2122

22-
fun renderTests(cp: JcClasspath, tests: List<Pair<UTest, JcTestInfo>>, shouldInlineUsvmUtils: Boolean): Map<JcTestClassInfo, String> {
23+
fun renderTests(
24+
cp: JcClasspath,
25+
tests: List<Pair<UTest, JcTestInfo>>,
26+
reflectionUtilsInlineStrategy: ReflectionUtilsInlineStrategy = ReflectionUtilsInlineStrategy.NoInline(),
27+
isAccessibleFromTestClass: ((JcClassOrInterface) -> Boolean)? = null
28+
): Map<JcTestClassInfo, String> {
2329
val renderedFiles = mutableMapOf<JcTestClassInfo, String>()
2430
val testClasses = tests.groupBy { (_, info) -> JcTestClassInfo.from(info) }
2531
val printer = DefaultPrettyPrinter()
@@ -33,15 +39,17 @@ class JcTestsRenderer {
3339
StaticJavaParser.parse(testFile),
3440
cp,
3541
testClassInfo,
36-
shouldInlineUsvmUtils
42+
reflectionUtilsInlineStrategy,
43+
isAccessibleFromTestClass
3744
)
3845
}
3946
else -> {
4047
JcTestFileRendererFactory.testFileRendererFor(
4148
testClassInfo.testPackageName,
4249
cp,
4350
testClassInfo,
44-
shouldInlineUsvmUtils
51+
reflectionUtilsInlineStrategy,
52+
isAccessibleFromTestClass
4553
)
4654
}
4755
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package org.usvm.jvm.rendering
2+
3+
import com.github.javaparser.StaticJavaParser
4+
import com.github.javaparser.ast.CompilationUnit
5+
import com.github.javaparser.ast.Modifier
6+
import com.github.javaparser.ast.NodeList
7+
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
8+
import com.github.javaparser.ast.expr.SimpleName
9+
import kotlin.jvm.optionals.getOrNull
10+
import org.usvm.jvm.rendering.baseRenderer.JcImportManager
11+
12+
sealed class ReflectionUtilsInlineStrategy(val inTestClassFile: Boolean) {
13+
14+
abstract fun addReflectionUtils(importManager: JcImportManager, cu: CompilationUnit): CompilationUnit
15+
16+
class NoInline : ReflectionUtilsInlineStrategy(inTestClassFile = false) {
17+
override fun addReflectionUtils(
18+
importManager: JcImportManager,
19+
cu: CompilationUnit
20+
): CompilationUnit {
21+
return cu
22+
}
23+
}
24+
25+
class Inline : ReflectionUtilsInlineStrategy(inTestClassFile = true) {
26+
27+
override fun addReflectionUtils(
28+
importManager: JcImportManager,
29+
cu: CompilationUnit
30+
): CompilationUnit {
31+
val testClass = cu.types.singleOrNull()
32+
33+
check(testClass != null) {
34+
"exactly one test class expected"
35+
}
36+
37+
check(!testClass.getFieldByName("UNSAFE").isPresent) {
38+
"field and init blocks merge not yet supported"
39+
}
40+
41+
val filteredUtilCu = filterReflectionUtilCu() ?: return cu
42+
43+
val requiredUtilMembers = filteredUtilCu.getClassByName("ReflectionUtils").get().members
44+
45+
for (member in requiredUtilMembers) {
46+
if (member.isMethodDeclaration) {
47+
member.asMethodDeclaration().setModifiers(Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC)
48+
}
49+
}
50+
51+
testClass.members.addAll(requiredUtilMembers)
52+
53+
cu.imports.addAll(filteredUtilCu.imports)
54+
55+
return cu
56+
}
57+
}
58+
59+
class NestedClass : ReflectionUtilsInlineStrategy(inTestClassFile = true) {
60+
61+
override fun addReflectionUtils(
62+
importManager: JcImportManager,
63+
cu: CompilationUnit
64+
): CompilationUnit {
65+
val testClass = cu.types.singleOrNull()
66+
67+
check(testClass != null) {
68+
"exactly one test class expected"
69+
}
70+
71+
val filteredUtilCu = filterReflectionUtilCu() ?: return cu
72+
73+
var utilsClass =
74+
testClass.members.firstOrNull {
75+
it is ClassOrInterfaceDeclaration && it.isNestedType && it.name == SimpleName("ReflectionUtils")
76+
} as? ClassOrInterfaceDeclaration
77+
78+
val requiredUtils = filteredUtilCu.getClassByName("ReflectionUtils").get()
79+
80+
if (utilsClass != null) {
81+
mergeUtilClass(utilsClass, requiredUtils)
82+
} else {
83+
utilsClass = requiredUtils
84+
testClass.addMember(utilsClass)
85+
}
86+
87+
cu.imports.addAll(filteredUtilCu.imports)
88+
89+
utilsClass.setModifiers(Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC)
90+
91+
return cu
92+
}
93+
}
94+
95+
class OuterClass : ReflectionUtilsInlineStrategy(inTestClassFile = true) {
96+
97+
override fun addReflectionUtils(
98+
importManager: JcImportManager,
99+
cu: CompilationUnit
100+
): CompilationUnit {
101+
val filteredUtilCu = filterReflectionUtilCu()
102+
if (filteredUtilCu == null) return cu
103+
104+
var currentUtilsClass = cu.getClassByName("ReflectionUtils").getOrNull()
105+
val requiredUtilsClass = filteredUtilCu.getClassByName("ReflectionUtils").get()
106+
107+
if (currentUtilsClass != null) {
108+
mergeUtilClass(currentUtilsClass, requiredUtilsClass)
109+
} else {
110+
currentUtilsClass = requiredUtilsClass
111+
cu.addType(currentUtilsClass)
112+
}
113+
114+
cu.imports.addAll(filteredUtilCu.imports)
115+
116+
currentUtilsClass.modifiers = NodeList()
117+
118+
return cu
119+
}
120+
}
121+
122+
private val utilsCu: CompilationUnit by lazy {
123+
this::class.java.classLoader.getResourceAsStream("ReflectionUtils.java").use { stream ->
124+
StaticJavaParser.parse(stream)
125+
}
126+
}
127+
128+
fun useUsvmReflectionMethod(name: String) {
129+
usvmUtilMethodCollector.add(name)
130+
}
131+
132+
protected fun filterReflectionUtilCu(): CompilationUnit? {
133+
val usedMethods = extractUsedUsvmUtilMethods()
134+
if (usedMethods.isEmpty()) return null
135+
136+
val cu = utilsCu.clone()
137+
val utilsClass = cu.getClassByName("ReflectionUtils").get()
138+
139+
utilsClass.members.removeIf { it.isMethodDeclaration && (it.asMethodDeclaration().name.asString() !in usedMethods) }
140+
cu.allContainedComments.forEach { it.remove() }
141+
142+
return cu
143+
}
144+
145+
protected fun mergeUtilClass(prev: ClassOrInterfaceDeclaration, extra: ClassOrInterfaceDeclaration) {
146+
val declaredMembersNames =
147+
prev.members.mapNotNull { if (it.isMethodDeclaration) it.asMethodDeclaration().name else null }
148+
149+
extra.members.filter { it.isMethodDeclaration }.forEach { declaration ->
150+
if (declaration.asMethodDeclaration().name !in declaredMembersNames) {
151+
prev.addMember(declaration)
152+
}
153+
}
154+
}
155+
156+
157+
private val usvmUtilMethodCollector: MutableSet<String> = mutableSetOf()
158+
159+
private val usvmUtilRequiredMethodsMapping = mapOf(
160+
"callConstructor" to listOf("getConstructor", "methodSignature", "parameterTypesSignature"),
161+
"callMethod" to listOf("getMethod", "getInstanceMethods", "methodSignature", "parameterTypesSignature"),
162+
"callStaticMethod" to listOf(
163+
"callMethod",
164+
"getMethod",
165+
"getStaticMethod",
166+
"getStaticMethods",
167+
"getInstanceMethods",
168+
"methodSignature",
169+
"parameterTypesSignature"
170+
),
171+
"getStaticFieldValue" to listOf(
172+
"getStaticField",
173+
"getFieldValue",
174+
"getOffsetOf",
175+
"isStatic",
176+
"getStaticFields"
177+
),
178+
"getFieldValue" to listOf("getOffsetOf", "isStatic"),
179+
"setStaticFieldValue" to listOf(
180+
"getStaticField",
181+
"getStaticFields",
182+
"setFieldValue",
183+
"getOffsetOf",
184+
"isStatic"
185+
),
186+
"setFieldValue" to listOf("getField", "getInstanceFields", "getOffsetOf", "isStatic"),
187+
"allocateInstance" to listOf()
188+
)
189+
190+
private fun extractUsedUsvmUtilMethods(): Set<String> {
191+
val usedMethodsTransitive = usvmUtilMethodCollector.flatMap { method ->
192+
usvmUtilRequiredMethodsMapping[method]!! + method
193+
}
194+
195+
return usedMethodsTransitive.toSet()
196+
}
197+
}

usvm-jvm-rendering/src/main/kotlin/org/usvm/jvm/rendering/baseRenderer/JcBlockRenderer.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@ open class JcBlockRenderer private constructor(
128128
addExpression(renderSetStaticField(field, value))
129129
}
130130

131-
protected fun addThrownExceptions(method: JcMethod) {
132-
thrownExceptions.addAll(
133-
method.exceptions.map {
134-
renderClass(it.typeName)
135-
}
136-
)
131+
fun addThrownExceptions(method: JcMethod) {
132+
addThrownExceptions(method.exceptions.map { it.typeName })
133+
}
134+
135+
fun addThrownExceptions(exceptionsTypeNames: List<String>) {
136+
exceptionsTypeNames.forEach { addThrownException(it) }
137137
}
138138

139-
protected fun addThrownException(typeName: String, cp: JcClasspath) {
139+
fun addThrownException(typeName: String) {
140140
val thrown = renderClass(typeName)
141141
thrownExceptions.add(thrown)
142142
}

usvm-jvm-rendering/src/main/kotlin/org/usvm/jvm/rendering/baseRenderer/JcCodeRenderer.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ abstract class JcCodeRenderer<T: Node>(
188188
val renderedType = StaticJavaParser.parseClassOrInterfaceType(qualifiedName(renderName))
189189
val argTypes = type.typeArguments.zip(type.typeParameters).map { (a, p) ->
190190
when (a) {
191-
is JcUnboundWildcard -> p.bounds.first()
191+
is JcUnboundWildcard -> p.bounds.firstOrNull() ?: type.classpath.objectType
192192
is JcBoundedWildcard -> (a.lowerBounds + a.upperBounds).first()
193193
else -> a
194194
}
@@ -211,8 +211,8 @@ abstract class JcCodeRenderer<T: Node>(
211211
fun renderClassExpression(type: JcClassOrInterface): Expression =
212212
ClassExpr(renderClass(type, false))
213213

214-
fun renderClassExpression(type: JcClassType): Expression =
215-
ClassExpr(renderClass(type, false))
214+
fun renderClassExpression(type: JcType): Expression =
215+
ClassExpr(renderType(type, false))
216216

217217
//endregion
218218

0 commit comments

Comments
 (0)