Skip to content

Commit 61c211c

Browse files
swankjessesquarejesseJakeWharton
authored
New feature, CoroutineTestInterceptor (#183)
* New feature, CoroutineTestInterceptor This is in a new optional module, burst-coroutines, that must be added as a standalone dependency. With this update TestInterceptor can no longer be used to intercept tests that use runTest() from kotlinx-coroutines-test. That used to only partially work; it didn't work properly on Kotlin/JS. * Update burst-coroutines/build.gradle.kts Co-authored-by: Jake Wharton <github@jakewharton.com> --------- Co-authored-by: Jesse Wilson <jwilson@squareup.com> Co-authored-by: Jake Wharton <github@jakewharton.com>
1 parent 819ff91 commit 61c211c

File tree

21 files changed

+1205
-155
lines changed

21 files changed

+1205
-155
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
public abstract class app/cash/burst/coroutines/CoroutineTestFunction {
2+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
3+
public final fun getClassName ()Ljava/lang/String;
4+
public final fun getFunctionName ()Ljava/lang/String;
5+
public final fun getPackageName ()Ljava/lang/String;
6+
public abstract fun invoke (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
7+
public fun toString ()Ljava/lang/String;
8+
}
9+
10+
public abstract interface class app/cash/burst/coroutines/CoroutineTestInterceptor {
11+
public abstract fun intercept (Lapp/cash/burst/coroutines/CoroutineTestFunction;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
12+
}
13+

burst-coroutines/build.gradle.kts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import com.vanniktech.maven.publish.JavadocJar
2+
import com.vanniktech.maven.publish.KotlinMultiplatform
3+
import com.vanniktech.maven.publish.MavenPublishBaseExtension
4+
import org.jetbrains.kotlin.gradle.plugin.NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME
5+
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
6+
7+
plugins {
8+
kotlin("multiplatform")
9+
id("org.jetbrains.dokka")
10+
id("com.vanniktech.maven.publish.base")
11+
id("binary-compatibility-validator")
12+
}
13+
14+
dependencies {
15+
add(PLUGIN_CLASSPATH_CONFIGURATION_NAME, projects.burstKotlinPlugin)
16+
add(NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME, projects.burstKotlinPlugin)
17+
}
18+
19+
kotlin {
20+
androidNativeArm32()
21+
androidNativeArm64()
22+
androidNativeX64()
23+
androidNativeX86()
24+
25+
iosArm64()
26+
iosSimulatorArm64()
27+
iosX64()
28+
29+
js().nodejs()
30+
31+
jvm()
32+
33+
linuxArm64()
34+
linuxX64()
35+
36+
macosArm64()
37+
macosX64()
38+
39+
mingwX64()
40+
41+
tvosArm64()
42+
tvosSimulatorArm64()
43+
tvosX64()
44+
45+
wasmJs().nodejs()
46+
wasmWasi().nodejs()
47+
48+
watchosArm32()
49+
watchosArm64()
50+
watchosDeviceArm64()
51+
watchosSimulatorArm64()
52+
watchosX64()
53+
54+
applyDefaultHierarchyTemplate()
55+
56+
sourceSets {
57+
val commonMain by getting {
58+
dependencies {
59+
api(projects.burst)
60+
api(libs.kotlinx.coroutines.core)
61+
api(libs.kotlinx.coroutines.test)
62+
}
63+
}
64+
val commonTest by getting {
65+
dependencies {
66+
implementation(libs.assertk)
67+
implementation(kotlin("test"))
68+
}
69+
}
70+
}
71+
}
72+
73+
configure<MavenPublishBaseExtension> {
74+
configure(
75+
KotlinMultiplatform(javadocJar = JavadocJar.Empty())
76+
)
77+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (C) 2025 Cash App
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.burst.coroutines
17+
18+
interface CoroutineTestInterceptor {
19+
suspend fun intercept(
20+
testFunction: CoroutineTestFunction,
21+
)
22+
}
23+
24+
abstract class CoroutineTestFunction(
25+
val packageName: String,
26+
27+
val className: String,
28+
29+
val functionName: String,
30+
31+
// TODO(jwilson): add TestScope parameter?
32+
) {
33+
abstract suspend operator fun invoke()
34+
35+
override fun toString(): String {
36+
return buildString {
37+
if (packageName.isNotEmpty()) {
38+
append(packageName)
39+
append(".")
40+
}
41+
append(className)
42+
append(".")
43+
append(functionName)
44+
}
45+
}
46+
}

burst-gradle-plugin/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ tasks {
6060
dependsOn(":burst:publishKotlinMultiplatformPublicationToTestMavenRepository")
6161
dependsOn(":burst:publishLinuxX64PublicationToTestMavenRepository")
6262
dependsOn(":burst:publishMacosArm64PublicationToTestMavenRepository")
63+
dependsOn(":burst-coroutines:publishJsPublicationToTestMavenRepository")
64+
dependsOn(":burst-coroutines:publishJvmPublicationToTestMavenRepository")
65+
dependsOn(":burst-coroutines:publishKotlinMultiplatformPublicationToTestMavenRepository")
66+
dependsOn(":burst-coroutines:publishLinuxX64PublicationToTestMavenRepository")
67+
dependsOn(":burst-coroutines:publishMacosArm64PublicationToTestMavenRepository")
6368
}
6469
}
6570

burst-gradle-plugin/src/test/kotlin/app/cash/burst/gradle/TestInterceptorGradlePluginTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import assertk.assertions.contains
2121
import assertk.assertions.isEqualTo
2222
import assertk.assertions.isNotNull
2323
import assertk.assertions.isNull
24+
import org.jetbrains.kotlin.konan.target.HostManager
25+
import org.jetbrains.kotlin.konan.target.presetName
2426
import org.junit.Test
2527

2628
class TestInterceptorGradlePluginTest {
@@ -272,4 +274,52 @@ class TestInterceptorGradlePluginTest {
272274
)
273275
}
274276
}
277+
278+
@Test
279+
fun multiplatformJvm() {
280+
multiplatform(testTaskName = "jvmTest")
281+
}
282+
283+
@Test
284+
fun multiplatformJs() {
285+
multiplatform(testTaskName = "jsNodeTest")
286+
}
287+
288+
@Test
289+
fun multiplatformNative() {
290+
// Like 'linuxX64' or 'macosArm64'.
291+
val platformName = HostManager.host.presetName
292+
multiplatform(testTaskName = "${platformName}Test")
293+
}
294+
295+
private fun multiplatform(testTaskName: String) {
296+
val tester = GradleTester("multiplatformInterceptor")
297+
tester.cleanAndBuild(":lib:$testTaskName")
298+
299+
with(tester.readTestSuite("app.cash.burst.tests.BasicTest", testTaskName)) {
300+
assertThat(systemOut).isEqualTo(
301+
"""
302+
|intercepting app.cash.burst.tests.BasicTest.passingTest
303+
|set up
304+
|running
305+
|tear down
306+
|intercepted
307+
|
308+
""".trimMargin(),
309+
)
310+
}
311+
312+
with(tester.readTestSuite("app.cash.burst.tests.CoroutinesTest", testTaskName)) {
313+
assertThat(systemOut).isEqualTo(
314+
"""
315+
|intercepting app.cash.burst.tests.CoroutinesTest.passingTest in passingCoroutine
316+
|set up
317+
|running in passingCoroutine
318+
|tear down
319+
|intercepted
320+
|
321+
""".trimMargin(),
322+
)
323+
}
324+
}
275325
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
3+
4+
buildscript {
5+
repositories {
6+
maven {
7+
url = file("$rootDir/../../../../../build/testMaven").toURI()
8+
}
9+
mavenCentral()
10+
google()
11+
}
12+
dependencies {
13+
classpath("app.cash.burst:burst-gradle-plugin:${project.property("burstVersion")}")
14+
classpath(libs.kotlin.gradlePlugin)
15+
}
16+
}
17+
18+
allprojects {
19+
repositories {
20+
maven {
21+
url = file("$rootDir/../../../../../build/testMaven").toURI()
22+
}
23+
mavenCentral()
24+
google()
25+
}
26+
27+
tasks.withType(JavaCompile::class.java).configureEach {
28+
sourceCompatibility = "1.8"
29+
targetCompatibility = "1.8"
30+
}
31+
32+
tasks.withType(KotlinJvmCompile::class.java).configureEach {
33+
compilerOptions {
34+
jvmTarget.set(JvmTarget.JVM_1_8)
35+
}
36+
}
37+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
kotlin("multiplatform")
3+
id("app.cash.burst")
4+
}
5+
6+
kotlin {
7+
jvm()
8+
js {
9+
nodejs()
10+
}
11+
12+
// Cover the host platforms where we run Burst tests.
13+
linuxX64()
14+
macosArm64()
15+
16+
sourceSets {
17+
commonTest {
18+
dependencies {
19+
implementation("app.cash.burst:burst-coroutines:${project.property("burstVersion")}")
20+
implementation(kotlin("test"))
21+
implementation(libs.kotlinx.coroutines.core)
22+
implementation(libs.kotlinx.coroutines.test)
23+
}
24+
}
25+
}
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.TestFunction
5+
import app.cash.burst.TestInterceptor
6+
import kotlin.test.AfterTest
7+
import kotlin.test.BeforeTest
8+
import kotlin.test.Test
9+
10+
class BasicTest {
11+
@InterceptTest
12+
val interceptor = BasicInterceptor()
13+
14+
@BeforeTest
15+
fun setUp() {
16+
println("set up")
17+
}
18+
19+
@AfterTest
20+
fun tearDown() {
21+
println("tear down")
22+
}
23+
24+
@Test
25+
fun passingTest() {
26+
println("running")
27+
}
28+
29+
class BasicInterceptor : TestInterceptor {
30+
override fun intercept(testFunction: TestFunction) {
31+
println("intercepting $testFunction")
32+
testFunction()
33+
println("intercepted")
34+
}
35+
}
36+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.coroutines.CoroutineTestFunction
5+
import app.cash.burst.coroutines.CoroutineTestInterceptor
6+
import kotlin.coroutines.coroutineContext
7+
import kotlin.test.AfterTest
8+
import kotlin.test.BeforeTest
9+
import kotlin.test.Test
10+
import kotlin.time.Duration.Companion.milliseconds
11+
import kotlinx.coroutines.CoroutineName
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.coroutineScope
14+
import kotlinx.coroutines.delay
15+
import kotlinx.coroutines.test.runTest
16+
17+
/**
18+
* Note that although this test includes 4 seconds of delays, it takes advantage of the
19+
* `TestCoroutineScheduler` to skip these delays.
20+
*/
21+
class CoroutinesTest {
22+
@InterceptTest
23+
val interceptor = DelayInterceptor()
24+
25+
@BeforeTest
26+
fun setUp() {
27+
println("set up")
28+
}
29+
30+
@AfterTest
31+
fun tearDown() {
32+
println("tear down")
33+
}
34+
35+
@Test
36+
fun passingTest() = runTest(CoroutineName("passingCoroutine")) {
37+
val deferred = async {
38+
println("running in ${coroutineContext[CoroutineName]?.name}")
39+
}
40+
delay(1000.milliseconds)
41+
deferred.await()
42+
}
43+
44+
class DelayInterceptor : CoroutineTestInterceptor {
45+
override suspend fun intercept(testFunction: CoroutineTestFunction) {
46+
coroutineScope {
47+
val before = async {
48+
println("intercepting $testFunction in ${coroutineContext[CoroutineName]?.name}")
49+
}
50+
delay(1000.milliseconds)
51+
before.await()
52+
53+
val execute = async {
54+
testFunction()
55+
}
56+
delay(1000.milliseconds)
57+
execute.await()
58+
59+
val after = async {
60+
println("intercepted")
61+
}
62+
delay(1000.milliseconds)
63+
before.await()
64+
}
65+
}
66+
}
67+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
dependencyResolutionManagement {
2+
versionCatalogs {
3+
create("libs") {
4+
from(files("../../../../../gradle/libs.versions.toml"))
5+
}
6+
}
7+
}
8+
9+
include(":lib")

0 commit comments

Comments
 (0)