Skip to content

[TS] Test factory DSL #320

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
merged 7 commits into from
Aug 7, 2025
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
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ object Versions {
const val clikt = "5.0.0"
const val detekt = "1.23.7"
const val ini4j = "0.5.4"
const val jacodb = "5acbadfed0"
const val jacodb = "213f9a1aee"
const val juliet = "1.3.2"
const val junit = "5.9.3"
const val kotlin = "2.1.0"
Expand Down
18 changes: 13 additions & 5 deletions buildSrc/src/main/kotlin/usvm.kotlin-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,25 @@ tasks {
}
}

tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()

tasks.withType<Test> {
maxHeapSize = "4G"

testLogging {
events("passed")
}
}

tasks.test {
useJUnitPlatform {
excludeTags("manual")
}
}

tasks.create("manualTest", Test::class) {
useJUnitPlatform {
includeTags("manual")
}
}

publishing {
repositories {
maven {
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ findProject(":usvm-python:usvm-python-runner")?.name = "usvm-python-runner"
include("usvm-python:usvm-python-commons")
findProject(":usvm-python:usvm-python-commons")?.name = "usvm-python-commons"

// Actually, `includeBuild("../jacodb")` is enough, but there is a bug in IDEA when path is a symlink.
// Actually, relative path is enough, but there is a bug in IDEA when the path is a symlink.
// As a workaround, we convert it to a real absolute path.
// See IDEA bug: https://youtrack.jetbrains.com/issue/IDEA-329756
// val jacodbPath = file("jacodb").takeIf { it.exists() }
Expand Down
1 change: 1 addition & 0 deletions usvm-ts-dataflow/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {

testFixturesImplementation(Libs.kotlin_logging)
testFixturesImplementation(Libs.junit_jupiter_api)
testFixturesImplementation(Libs.kotlinx_coroutines_core)
}

tasks.withType<Test> {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,52 +1,77 @@
package org.usvm.dataflow.ts

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import org.junit.jupiter.api.DynamicContainer
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.function.Executable
import java.util.stream.Stream

private interface TestProvider {
fun test(name: String, test: () -> Unit)
}

private interface ContainerProvider {
fun container(name: String, init: TestContainerBuilder.() -> Unit)
}
@DslMarker
annotation class TestFactoryDsl

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

override fun test(name: String, test: () -> Unit) {
nodes += dynamicTest(name, test)
fun test(name: String, test: () -> Unit) {
nodeChannel.trySend { dynamicTest(name, test) }
}

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

fun build(): DynamicContainer = DynamicContainer.dynamicContainer(name, nodes)
}
protected fun createNodes(): Iterable<DynamicNode> =
Iterable { DynamicNodeIterator() }

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

class TestFactoryBuilder : TestProvider, ContainerProvider {
private val nodes: MutableList<DynamicNode> = mutableListOf()

override fun test(name: String, test: () -> Unit) {
nodes += dynamicTest(name, test)
override fun next(): DynamicNode {
val node = nodeChannel.tryReceive().getOrThrow()
return node()
}
}
}

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

fun build(): Stream<out DynamicNode> = nodes.stream()
class TestFactoryBuilder : TestNodeBuilder() {
fun build(): Iterable<DynamicNode> {
return createNodes()
}
}

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

private fun dynamicTest(name: String, test: () -> Unit): DynamicTest =
DynamicTest.dynamicTest(name, Executable(test))
DynamicTest.dynamicTest(name, test)

private fun dynamicContainer(name: String, init: TestContainerBuilder.() -> Unit): DynamicContainer =
TestContainerBuilder(name).apply(init).build()

inline fun <reified T> TestNodeBuilder.testForEach(
data: Iterable<T>,
crossinline nameProvider: (T) -> String = { it.toString() },
crossinline test: (T) -> Unit,
) {
data.forEach { item ->
test(nameProvider(item)) { test(item) }
}
}

inline fun <reified T> TestNodeBuilder.containerForEach(
data: Iterable<T>,
crossinline nameProvider: (T) -> String = { it.toString() },
crossinline init: TestContainerBuilder.(T) -> Unit,
) {
data.forEach { item ->
container(nameProvider(item)) { init(item) }
}
}
1 change: 1 addition & 0 deletions usvm-ts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
testImplementation(Libs.mockk)
testImplementation(Libs.junit_jupiter_params)
testImplementation(Libs.logback)
testImplementation(testFixtures(project(":usvm-ts-dataflow")))

// https://mvnrepository.com/artifact/org.burningwave/core
// Use it to export all modules to all
Expand Down
85 changes: 42 additions & 43 deletions usvm-ts/src/test/kotlin/org/usvm/project/DemoCalc.kt
Original file line number Diff line number Diff line change
@@ -1,75 +1,74 @@
package org.usvm.project

import mu.KotlinLogging
import org.jacodb.ets.model.EtsScene
import org.jacodb.ets.utils.ANONYMOUS_CLASS_PREFIX
import org.jacodb.ets.utils.ANONYMOUS_METHOD_PREFIX
import org.jacodb.ets.utils.CONSTRUCTOR_NAME
import org.jacodb.ets.utils.INSTANCE_INIT_METHOD_NAME
import org.jacodb.ets.utils.STATIC_INIT_METHOD_NAME
import org.jacodb.ets.utils.loadEtsProjectFromIR
import org.junit.jupiter.api.condition.EnabledIf
import org.jacodb.ets.utils.loadEtsProjectAutoConvert
import org.junit.jupiter.api.Tag
import org.usvm.machine.TsMachine
import org.usvm.machine.TsOptions
import org.usvm.util.TsMethodTestRunner
import org.usvm.util.getResourcePath
import org.usvm.util.getResourcePathOrNull
import kotlin.test.Test

@EnabledIf("projectAvailable")
private val logger = KotlinLogging.logger {}

@Tag("manual")
class RunOnDemoCalcProject : TsMethodTestRunner() {

companion object {
private const val PROJECT_PATH = "/projects/Demo_Calc/etsir/entry"
private const val SDK_PATH = "/sdk/ohos/etsir"

@JvmStatic
private fun projectAvailable(): Boolean {
val isProjectPresent = getResourcePathOrNull(PROJECT_PATH) != null
val isSdkPreset = getResourcePathOrNull(SDK_PATH) != null
return isProjectPresent && isSdkPreset
}
private const val PROJECT_PATH = "/projects/Demo_Calc/source/entry"
private const val SDK_OHOS_PATH = "/sdk/ohos/5.0.1.111/ets"
}

override val scene: EtsScene = run {
val projectPath = getResourcePath(PROJECT_PATH)
val sdkPath = getResourcePathOrNull(SDK_PATH)
?: error(
"Could not load SDK from resources '$SDK_PATH'. " +
"Try running './gradlew generateSdkIR' to generate it."
)
loadEtsProjectFromIR(projectPath, sdkPath)
val project = loadEtsProjectAutoConvert(getResourcePath(PROJECT_PATH))
val sdkFiles = listOf(SDK_OHOS_PATH).flatMap { sdk ->
val sdkPath = getResourcePath(sdk)
val sdkProject = loadEtsProjectAutoConvert(sdkPath, useArkAnalyzerTypeInference = null)
sdkProject.projectFiles
}
EtsScene(project.projectFiles, sdkFiles, projectName = project.projectName)
}

@Test
fun `test run on each method`() {
fun `test run on each class`() {
val exceptions = mutableListOf<Throwable>()
val classes = scene.projectClasses.filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) }
val classes = scene.projectClasses
.filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) }

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

classes
.forEach { cls ->
val methods = cls.methods
.filterNot { it.cfg.stmts.isEmpty() }
.filterNot { it.isStatic }
.filterNot { it.name.startsWith(ANONYMOUS_METHOD_PREFIX) }
.filterNot { it.name == "build" }
.filterNot { it.name == INSTANCE_INIT_METHOD_NAME }
.filterNot { it.name == STATIC_INIT_METHOD_NAME }
.filterNot { it.name == CONSTRUCTOR_NAME }

if (methods.isEmpty()) return@forEach

runCatching {
val tsOptions = TsOptions()
TsMachine(scene, options, tsOptions).use { machine ->
val states = machine.analyze(methods)
states.let {}
}
}.onFailure {
exceptions += it
for (cls in classes) {
logger.info {
"Analyzing class ${cls.name} with ${cls.methods.size} methods"
}

val methods = cls.methods
.filterNot { it.cfg.stmts.isEmpty() }
.filterNot { it.isStatic }
.filterNot { it.name.startsWith(ANONYMOUS_METHOD_PREFIX) }
.filterNot { it.name == "build" }
.filterNot { it.name == INSTANCE_INIT_METHOD_NAME }
.filterNot { it.name == STATIC_INIT_METHOD_NAME }
.filterNot { it.name == CONSTRUCTOR_NAME }

if (methods.isEmpty()) continue

runCatching {
val tsOptions = TsOptions()
TsMachine(scene, options, tsOptions).use { machine ->
val states = machine.analyze(methods)
states.let {}
}
}.onFailure {
exceptions += it
}
}

val exc = exceptions.groupBy { it }
println("Total exceptions: ${exc.size}")
Expand Down
29 changes: 12 additions & 17 deletions usvm-ts/src/test/kotlin/org/usvm/project/DemoPhotos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.jacodb.ets.utils.CONSTRUCTOR_NAME
import org.jacodb.ets.utils.INSTANCE_INIT_METHOD_NAME
import org.jacodb.ets.utils.STATIC_INIT_METHOD_NAME
import org.jacodb.ets.utils.loadEtsProjectAutoConvert
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.condition.EnabledIf
import org.usvm.machine.TsMachine
import org.usvm.machine.TsOptions
Expand All @@ -18,29 +19,22 @@ import kotlin.test.Test

private val logger = KotlinLogging.logger {}

@EnabledIf("projectAvailable")
@Tag("manual")
class RunOnDemoPhotosProject : TsMethodTestRunner() {

companion object {
private const val PROJECT_PATH = "/projects/Demo_Photos/source/entry"
private const val SDK_PATH = "/sdk/ohos/etsir"

@JvmStatic
private fun projectAvailable(): Boolean {
val isProjectPresent = getResourcePathOrNull(PROJECT_PATH) != null
val isSdkPreset = getResourcePathOrNull(SDK_PATH) != null
return isProjectPresent && isSdkPreset
}
private const val SDK_OHOS_PATH = "/sdk/ohos/5.0.1.111/ets"
}

override val scene: EtsScene = run {
val projectPath = getResourcePath(PROJECT_PATH)
val sdkPath = getResourcePathOrNull(SDK_PATH)
?: error(
"Could not load SDK from resources '$SDK_PATH'. " +
"Try running './gradlew generateSdkIR' to generate it."
)
loadEtsProjectAutoConvert(projectPath, sdkPath)
val project = loadEtsProjectAutoConvert(getResourcePath(PROJECT_PATH))
val sdkFiles = listOf(SDK_OHOS_PATH).flatMap { sdk ->
val sdkPath = getResourcePath(sdk)
val sdkProject = loadEtsProjectAutoConvert(sdkPath, useArkAnalyzerTypeInference = null)
sdkProject.projectFiles
}
EtsScene(project.projectFiles, sdkFiles, projectName = project.projectName)
}

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

val tsOptions = TsOptions()
TsMachine(scene, options, tsOptions).use { machine ->
Expand Down
Loading