Skip to content

Commit 9b81489

Browse files
authored
[TS] Test factory DSL (#320)
1 parent 419eb50 commit 9b81489

File tree

10 files changed

+540
-299
lines changed

10 files changed

+540
-299
lines changed

buildSrc/src/main/kotlin/Dependencies.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ object Versions {
66
const val clikt = "5.0.0"
77
const val detekt = "1.23.7"
88
const val ini4j = "0.5.4"
9-
const val jacodb = "5acbadfed0"
9+
const val jacodb = "213f9a1aee"
1010
const val juliet = "1.3.2"
1111
const val junit = "5.9.3"
1212
const val kotlin = "2.1.0"

buildSrc/src/main/kotlin/usvm.kotlin-conventions.gradle.kts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,25 @@ tasks {
4444
}
4545
}
4646

47-
tasks.named<Test>("test") {
48-
// Use JUnit Platform for unit tests.
49-
useJUnitPlatform()
50-
47+
tasks.withType<Test> {
5148
maxHeapSize = "4G"
52-
5349
testLogging {
5450
events("passed")
5551
}
5652
}
5753

54+
tasks.test {
55+
useJUnitPlatform {
56+
excludeTags("manual")
57+
}
58+
}
59+
60+
tasks.create("manualTest", Test::class) {
61+
useJUnitPlatform {
62+
includeTags("manual")
63+
}
64+
}
65+
5866
publishing {
5967
repositories {
6068
maven {

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ findProject(":usvm-python:usvm-python-runner")?.name = "usvm-python-runner"
5252
include("usvm-python:usvm-python-commons")
5353
findProject(":usvm-python:usvm-python-commons")?.name = "usvm-python-commons"
5454

55-
// Actually, `includeBuild("../jacodb")` is enough, but there is a bug in IDEA when path is a symlink.
55+
// Actually, relative path is enough, but there is a bug in IDEA when the path is a symlink.
5656
// As a workaround, we convert it to a real absolute path.
5757
// See IDEA bug: https://youtrack.jetbrains.com/issue/IDEA-329756
5858
// val jacodbPath = file("jacodb").takeIf { it.exists() }

usvm-ts-dataflow/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies {
3030

3131
testFixturesImplementation(Libs.kotlin_logging)
3232
testFixturesImplementation(Libs.junit_jupiter_api)
33+
testFixturesImplementation(Libs.kotlinx_coroutines_core)
3334
}
3435

3536
tasks.withType<Test> {

usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt

Lines changed: 198 additions & 203 deletions
Large diffs are not rendered by default.
Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,77 @@
11
package org.usvm.dataflow.ts
22

3+
import kotlinx.coroutines.ExperimentalCoroutinesApi
4+
import kotlinx.coroutines.channels.Channel
35
import org.junit.jupiter.api.DynamicContainer
46
import org.junit.jupiter.api.DynamicNode
57
import org.junit.jupiter.api.DynamicTest
6-
import org.junit.jupiter.api.function.Executable
7-
import java.util.stream.Stream
88

9-
private interface TestProvider {
10-
fun test(name: String, test: () -> Unit)
11-
}
12-
13-
private interface ContainerProvider {
14-
fun container(name: String, init: TestContainerBuilder.() -> Unit)
15-
}
9+
@DslMarker
10+
annotation class TestFactoryDsl
1611

17-
class TestContainerBuilder(var name: String) : TestProvider, ContainerProvider {
18-
private val nodes: MutableList<DynamicNode> = mutableListOf()
12+
@TestFactoryDsl
13+
abstract class TestNodeBuilder {
14+
private val nodeChannel = Channel<() -> DynamicNode>(Channel.UNLIMITED)
1915

20-
override fun test(name: String, test: () -> Unit) {
21-
nodes += dynamicTest(name, test)
16+
fun test(name: String, test: () -> Unit) {
17+
nodeChannel.trySend { dynamicTest(name, test) }
2218
}
2319

24-
override fun container(name: String, init: TestContainerBuilder.() -> Unit) {
25-
nodes += containerBuilder(name, init)
20+
fun container(name: String, init: TestContainerBuilder.() -> Unit) {
21+
nodeChannel.trySend { dynamicContainer(name, init) }
2622
}
2723

28-
fun build(): DynamicContainer = DynamicContainer.dynamicContainer(name, nodes)
29-
}
24+
protected fun createNodes(): Iterable<DynamicNode> =
25+
Iterable { DynamicNodeIterator() }
3026

31-
private fun containerBuilder(name: String, init: TestContainerBuilder.() -> Unit): DynamicContainer =
32-
TestContainerBuilder(name).apply(init).build()
27+
private inner class DynamicNodeIterator : Iterator<DynamicNode> {
28+
@OptIn(ExperimentalCoroutinesApi::class)
29+
override fun hasNext(): Boolean = !nodeChannel.isEmpty
3330

34-
class TestFactoryBuilder : TestProvider, ContainerProvider {
35-
private val nodes: MutableList<DynamicNode> = mutableListOf()
36-
37-
override fun test(name: String, test: () -> Unit) {
38-
nodes += dynamicTest(name, test)
31+
override fun next(): DynamicNode {
32+
val node = nodeChannel.tryReceive().getOrThrow()
33+
return node()
34+
}
3935
}
36+
}
4037

41-
override fun container(name: String, init: TestContainerBuilder.() -> Unit) {
42-
nodes += containerBuilder(name, init)
38+
class TestContainerBuilder(var name: String) : TestNodeBuilder() {
39+
fun build(): DynamicContainer {
40+
return DynamicContainer.dynamicContainer(name, createNodes())
4341
}
42+
}
4443

45-
fun build(): Stream<out DynamicNode> = nodes.stream()
44+
class TestFactoryBuilder : TestNodeBuilder() {
45+
fun build(): Iterable<DynamicNode> {
46+
return createNodes()
47+
}
4648
}
4749

48-
fun testFactory(init: TestFactoryBuilder.() -> Unit): Stream<out DynamicNode> =
50+
inline fun testFactory(init: TestFactoryBuilder.() -> Unit): Iterable<DynamicNode> =
4951
TestFactoryBuilder().apply(init).build()
5052

5153
private fun dynamicTest(name: String, test: () -> Unit): DynamicTest =
52-
DynamicTest.dynamicTest(name, Executable(test))
54+
DynamicTest.dynamicTest(name, test)
55+
56+
private fun dynamicContainer(name: String, init: TestContainerBuilder.() -> Unit): DynamicContainer =
57+
TestContainerBuilder(name).apply(init).build()
58+
59+
inline fun <reified T> TestNodeBuilder.testForEach(
60+
data: Iterable<T>,
61+
crossinline nameProvider: (T) -> String = { it.toString() },
62+
crossinline test: (T) -> Unit,
63+
) {
64+
data.forEach { item ->
65+
test(nameProvider(item)) { test(item) }
66+
}
67+
}
68+
69+
inline fun <reified T> TestNodeBuilder.containerForEach(
70+
data: Iterable<T>,
71+
crossinline nameProvider: (T) -> String = { it.toString() },
72+
crossinline init: TestContainerBuilder.(T) -> Unit,
73+
) {
74+
data.forEach { item ->
75+
container(nameProvider(item)) { init(item) }
76+
}
77+
}

usvm-ts/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies {
2525
testImplementation(Libs.mockk)
2626
testImplementation(Libs.junit_jupiter_params)
2727
testImplementation(Libs.logback)
28+
testImplementation(testFixtures(project(":usvm-ts-dataflow")))
2829

2930
// https://mvnrepository.com/artifact/org.burningwave/core
3031
// Use it to export all modules to all

usvm-ts/src/test/kotlin/org/usvm/project/DemoCalc.kt

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,75 @@
11
package org.usvm.project
22

3+
import mu.KotlinLogging
34
import org.jacodb.ets.model.EtsScene
45
import org.jacodb.ets.utils.ANONYMOUS_CLASS_PREFIX
56
import org.jacodb.ets.utils.ANONYMOUS_METHOD_PREFIX
67
import org.jacodb.ets.utils.CONSTRUCTOR_NAME
78
import org.jacodb.ets.utils.INSTANCE_INIT_METHOD_NAME
89
import org.jacodb.ets.utils.STATIC_INIT_METHOD_NAME
9-
import org.jacodb.ets.utils.loadEtsProjectFromIR
10-
import org.junit.jupiter.api.condition.EnabledIf
10+
import org.jacodb.ets.utils.loadEtsProjectAutoConvert
11+
import org.junit.jupiter.api.Tag
1112
import org.usvm.machine.TsMachine
1213
import org.usvm.machine.TsOptions
1314
import org.usvm.util.TsMethodTestRunner
1415
import org.usvm.util.getResourcePath
15-
import org.usvm.util.getResourcePathOrNull
1616
import kotlin.test.Test
1717
import kotlin.time.Duration.Companion.seconds
1818

19-
@EnabledIf("projectAvailable")
19+
private val logger = KotlinLogging.logger {}
20+
21+
@Tag("manual")
2022
class RunOnDemoCalcProject : TsMethodTestRunner() {
2123

2224
companion object {
23-
private const val PROJECT_PATH = "/projects/Demo_Calc/etsir/entry"
24-
private const val SDK_PATH = "/sdk/ohos/etsir"
25-
26-
@JvmStatic
27-
private fun projectAvailable(): Boolean {
28-
val isProjectPresent = getResourcePathOrNull(PROJECT_PATH) != null
29-
val isSdkPreset = getResourcePathOrNull(SDK_PATH) != null
30-
return isProjectPresent && isSdkPreset
31-
}
25+
private const val PROJECT_PATH = "/projects/Demo_Calc/source/entry"
26+
private const val SDK_OHOS_PATH = "/sdk/ohos/5.0.1.111/ets"
3227
}
3328

3429
override val scene: EtsScene = run {
35-
val projectPath = getResourcePath(PROJECT_PATH)
36-
val sdkPath = getResourcePathOrNull(SDK_PATH)
37-
?: error(
38-
"Could not load SDK from resources '$SDK_PATH'. " +
39-
"Try running './gradlew generateSdkIR' to generate it."
40-
)
41-
loadEtsProjectFromIR(projectPath, sdkPath)
30+
val project = loadEtsProjectAutoConvert(getResourcePath(PROJECT_PATH))
31+
val sdkFiles = listOf(SDK_OHOS_PATH).flatMap { sdk ->
32+
val sdkPath = getResourcePath(sdk)
33+
val sdkProject = loadEtsProjectAutoConvert(sdkPath, useArkAnalyzerTypeInference = null)
34+
sdkProject.projectFiles
35+
}
36+
EtsScene(project.projectFiles, sdkFiles, projectName = project.projectName)
4237
}
4338

4439
@Test
45-
fun `test run on each method`() {
40+
fun `test run on each class`() {
4641
val exceptions = mutableListOf<Throwable>()
47-
val classes = scene.projectClasses.filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) }
42+
val classes = scene.projectClasses
43+
.filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) }
4844

4945
println("Total classes: ${classes.size}")
5046

51-
classes
52-
.forEach { cls ->
53-
val methods = cls.methods
54-
.filterNot { it.cfg.stmts.isEmpty() }
55-
.filterNot { it.isStatic }
56-
.filterNot { it.name.startsWith(ANONYMOUS_METHOD_PREFIX) }
57-
.filterNot { it.name == "build" }
58-
.filterNot { it.name == INSTANCE_INIT_METHOD_NAME }
59-
.filterNot { it.name == STATIC_INIT_METHOD_NAME }
60-
.filterNot { it.name == CONSTRUCTOR_NAME }
61-
62-
if (methods.isEmpty()) return@forEach
63-
64-
runCatching {
65-
val tsOptions = TsOptions()
66-
TsMachine(scene, options, tsOptions).use { machine ->
67-
val states = machine.analyze(methods)
68-
states.let {}
69-
}
70-
}.onFailure {
71-
exceptions += it
47+
for (cls in classes) {
48+
logger.info {
49+
"Analyzing class ${cls.name} with ${cls.methods.size} methods"
50+
}
51+
52+
val methods = cls.methods
53+
.filterNot { it.cfg.stmts.isEmpty() }
54+
.filterNot { it.isStatic }
55+
.filterNot { it.name.startsWith(ANONYMOUS_METHOD_PREFIX) }
56+
.filterNot { it.name == "build" }
57+
.filterNot { it.name == INSTANCE_INIT_METHOD_NAME }
58+
.filterNot { it.name == STATIC_INIT_METHOD_NAME }
59+
.filterNot { it.name == CONSTRUCTOR_NAME }
60+
61+
if (methods.isEmpty()) continue
62+
63+
runCatching {
64+
val tsOptions = TsOptions()
65+
TsMachine(scene, options, tsOptions).use { machine ->
66+
val states = machine.analyze(methods)
67+
states.let {}
7268
}
69+
}.onFailure {
70+
exceptions += it
7371
}
72+
}
7473

7574
val exc = exceptions.groupBy { it }
7675
println("Total exceptions: ${exc.size}")

usvm-ts/src/test/kotlin/org/usvm/project/DemoPhotos.kt

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.jacodb.ets.utils.CONSTRUCTOR_NAME
88
import org.jacodb.ets.utils.INSTANCE_INIT_METHOD_NAME
99
import org.jacodb.ets.utils.STATIC_INIT_METHOD_NAME
1010
import org.jacodb.ets.utils.loadEtsProjectAutoConvert
11+
import org.junit.jupiter.api.Tag
1112
import org.junit.jupiter.api.condition.EnabledIf
1213
import org.usvm.machine.TsMachine
1314
import org.usvm.machine.TsOptions
@@ -18,29 +19,22 @@ import kotlin.test.Test
1819

1920
private val logger = KotlinLogging.logger {}
2021

21-
@EnabledIf("projectAvailable")
22+
@Tag("manual")
2223
class RunOnDemoPhotosProject : TsMethodTestRunner() {
2324

2425
companion object {
2526
private const val PROJECT_PATH = "/projects/Demo_Photos/source/entry"
26-
private const val SDK_PATH = "/sdk/ohos/etsir"
27-
28-
@JvmStatic
29-
private fun projectAvailable(): Boolean {
30-
val isProjectPresent = getResourcePathOrNull(PROJECT_PATH) != null
31-
val isSdkPreset = getResourcePathOrNull(SDK_PATH) != null
32-
return isProjectPresent && isSdkPreset
33-
}
27+
private const val SDK_OHOS_PATH = "/sdk/ohos/5.0.1.111/ets"
3428
}
3529

3630
override val scene: EtsScene = run {
37-
val projectPath = getResourcePath(PROJECT_PATH)
38-
val sdkPath = getResourcePathOrNull(SDK_PATH)
39-
?: error(
40-
"Could not load SDK from resources '$SDK_PATH'. " +
41-
"Try running './gradlew generateSdkIR' to generate it."
42-
)
43-
loadEtsProjectAutoConvert(projectPath, sdkPath)
31+
val project = loadEtsProjectAutoConvert(getResourcePath(PROJECT_PATH))
32+
val sdkFiles = listOf(SDK_OHOS_PATH).flatMap { sdk ->
33+
val sdkPath = getResourcePath(sdk)
34+
val sdkProject = loadEtsProjectAutoConvert(sdkPath, useArkAnalyzerTypeInference = null)
35+
sdkProject.projectFiles
36+
}
37+
EtsScene(project.projectFiles, sdkFiles, projectName = project.projectName)
4438
}
4539

4640
@Test
@@ -112,8 +106,9 @@ class RunOnDemoPhotosProject : TsMethodTestRunner() {
112106
@Test
113107
fun `test on particular method`() {
114108
val method = scene.projectClasses
109+
.filter { it.toString() == "@entry/utils/ResourceUtils: %dflt" }
115110
.flatMap { it.methods }
116-
.single { it.name == "onCreate" && it.enclosingClass?.name == "EntryAbility" }
111+
.single { it.name == "getResourceString" && it.enclosingClass?.name == "%dflt" }
117112

118113
val tsOptions = TsOptions()
119114
TsMachine(scene, options, tsOptions).use { machine ->

0 commit comments

Comments
 (0)