Skip to content

Commit 2706a76

Browse files
authored
Make kotlinx-coroutines-test an MPP (Kotlin#2965)
Change the build scripts and the file layout so that kotlinx-coroutines-test is built on all platforms.
1 parent 1ba202d commit 2706a76

37 files changed

+577
-370
lines changed

build.gradle

+35-19
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44

55
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
66
import org.jetbrains.kotlin.konan.target.HostManager
7-
import org.gradle.util.VersionNumber
87
import org.jetbrains.dokka.gradle.DokkaTaskPartial
9-
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
108

119
apply plugin: 'jdk-convention'
12-
apply from: rootProject.file("gradle/opt-in.gradle")
1310

1411
def coreModule = "kotlinx-coroutines-core"
12+
def testModule = "kotlinx-coroutines-test"
13+
def multiplatform = [coreModule, testModule]
1514
// Not applicable for Kotlin plugin
1615
def sourceless = ['kotlinx.coroutines', 'kotlinx-coroutines-bom', 'integration-testing']
1716
def internal = ['kotlinx.coroutines', 'benchmarks', 'integration-testing']
@@ -112,7 +111,7 @@ apiValidation {
112111
ignoredProjects += unpublished + ["kotlinx-coroutines-bom"]
113112
if (build_snapshot_train) {
114113
ignoredProjects.remove("example-frontend-js")
115-
ignoredProjects.add("kotlinx-coroutines-core")
114+
ignoredProjects.add(coreModule)
116115
}
117116
ignoredPackages += "kotlinx.coroutines.internal"
118117
}
@@ -133,13 +132,31 @@ allprojects {
133132
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
134133
configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
135134
evaluationDependsOn(":$coreModule")
136-
def platform = PlatformKt.platformOf(it)
137-
apply plugin: "kotlin-${platform}-conventions"
138-
dependencies {
139-
// See comment below for rationale, it will be replaced with "project" dependency
140-
api project(":$coreModule")
141-
// the only way IDEA can resolve test classes
142-
testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
135+
if (it.name in multiplatform) {
136+
apply plugin: "kotlin-multiplatform"
137+
apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle")
138+
apply from: rootProject.file("gradle/compile-common.gradle")
139+
140+
if (rootProject.ext["native_targets_enabled"] as Boolean) {
141+
apply from: rootProject.file("gradle/compile-native-multiplatform.gradle")
142+
}
143+
144+
apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
145+
apply from: rootProject.file("gradle/publish-npm-js.gradle")
146+
kotlin.sourceSets.commonMain.dependencies {
147+
api project(":$coreModule")
148+
}
149+
kotlin.sourceSets.jvmTest.dependencies {
150+
implementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
151+
}
152+
} else {
153+
def platform = PlatformKt.platformOf(it)
154+
apply plugin: "kotlin-${platform}-conventions"
155+
dependencies {
156+
api project(":$coreModule")
157+
// the only way IDEA can resolve test classes
158+
testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
159+
}
143160
}
144161
}
145162

@@ -150,7 +167,7 @@ configure(subprojects.findAll { !sourceless.contains(it.name) }) {
150167

151168
// Configure options for all Kotlin compilation tasks
152169
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
153-
kotlinOptions.freeCompilerArgs += optInAnnotations.collect { "-Xopt-in=" + it }
170+
kotlinOptions.freeCompilerArgs += OptInPresetKt.optInAnnotations.collect { "-Xopt-in=" + it }
154171
kotlinOptions.freeCompilerArgs += "-progressive"
155172
// Disable KT-36770 for RxJava2 integration
156173
kotlinOptions.freeCompilerArgs += "-XXLanguage:-ProhibitUsingNullableTypeParameterAgainstNotNullAnnotated"
@@ -177,7 +194,7 @@ if (build_snapshot_train) {
177194
}
178195

179196
println "Manifest of kotlin-compiler-embeddable.jar for coroutines"
180-
configure(subprojects.findAll { it.name == "kotlinx-coroutines-core" }) {
197+
configure(subprojects.findAll { it.name == coreModule }) {
181198
configurations.matching { it.name == "kotlinCompilerClasspath" }.all {
182199
resolvedConfiguration.getFiles().findAll { it.name.contains("kotlin-compiler-embeddable") }.each {
183200
def manifest = zipTree(it).matching {
@@ -194,9 +211,8 @@ if (build_snapshot_train) {
194211

195212
// Redefine source sets because we are not using 'kotlin/main/fqn' folder convention
196213
configure(subprojects.findAll {
197-
!sourceless.contains(it.name) &&
214+
!sourceless.contains(it.name) && !multiplatform.contains(it.name) &&
198215
it.name != "benchmarks" &&
199-
it.name != coreModule &&
200216
it.name != "example-frontend-js"
201217
}) {
202218
// Pure JS and pure MPP doesn't have this notion and are configured separately
@@ -250,7 +266,7 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) {
250266
}
251267

252268
List<String> jarTasks
253-
if (it.name == "kotlinx-coroutines-core") {
269+
if (it.name in multiplatform) {
254270
jarTasks = ["jvmJar", "metadataJar"]
255271
} else if (it.name == "kotlinx-coroutines-debug") {
256272
// We shadow debug module instead of just packaging it
@@ -324,12 +340,12 @@ allprojects { subProject ->
324340
.matching {
325341
// Excluding substituted project itself because of circular dependencies, but still do it
326342
// for "*Test*" configurations
327-
subProject.name != "kotlinx-coroutines-core" || it.name.contains("Test")
343+
subProject.name != coreModule || it.name.contains("Test")
328344
}
329345
.configureEach { conf ->
330346
conf.resolutionStrategy.dependencySubstitution {
331-
substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core"))
332-
.using(project(":kotlinx-coroutines-core"))
347+
substitute(module("org.jetbrains.kotlinx:$coreModule"))
348+
.using(project(":$coreModule"))
333349
.because("Because Kotlin compiler embeddable leaks coroutines into the runtime classpath, " +
334350
"triggering all sort of incompatible class changes errors")
335351
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
val optInAnnotations = listOf(
6+
"kotlin.RequiresOptIn",
7+
"kotlin.experimental.ExperimentalTypeInference",
8+
"kotlin.ExperimentalMultiplatform",
9+
"kotlinx.coroutines.DelicateCoroutinesApi",
10+
"kotlinx.coroutines.ExperimentalCoroutinesApi",
11+
"kotlinx.coroutines.ObsoleteCoroutinesApi",
12+
"kotlinx.coroutines.InternalCoroutinesApi",
13+
"kotlinx.coroutines.FlowPreview")
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
import org.jetbrains.kotlin.gradle.plugin.*
5+
6+
fun KotlinSourceSet.configureMultiplatform() {
7+
val srcDir = if (name.endsWith("Main")) "src" else "test"
8+
val platform = name.dropLast(4)
9+
kotlin.srcDir("$platform/$srcDir")
10+
if (name == "jvmMain") {
11+
resources.srcDir("$platform/resources")
12+
} else if (name == "jvmTest") {
13+
resources.srcDir("$platform/test-resources")
14+
}
15+
languageSettings {
16+
optInAnnotations.forEach { optIn(it) }
17+
progressiveMode = true
18+
}
19+
}

gradle/dokka.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ tasks.withType(DokkaTaskPartial::class).configureEach {
3737
packageListUrl.set(rootProject.projectDir.toPath().resolve("site/stdlib.package.list").toUri().toURL())
3838
}
3939

40-
if (project.name != "kotlinx-coroutines-core") {
40+
if (project.name != "kotlinx-coroutines-core" && project.name != "kotlinx-coroutines-test") {
4141
dependsOn(project.configurations["compileClasspath"])
4242
doFirst {
4343
// resolve classpath only during execution

gradle/opt-in.gradle

-13
This file was deleted.

gradle/publish.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ apply plugin: 'signing'
1212

1313
// ------------- tasks
1414

15-
def isMultiplatform = project.name == "kotlinx-coroutines-core"
15+
def isMultiplatform = project.name == "kotlinx-coroutines-core" || project.name == "kotlinx-coroutines-test"
1616
def isBom = project.name == "kotlinx-coroutines-bom"
1717

1818
if (!isBom) {

kotlinx-coroutines-core/build.gradle

+2-13
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,8 @@ if (rootProject.ext.native_targets_enabled) {
7070
* because JMV-only projects depend on core, thus core should always be initialized before configuration.
7171
*/
7272
kotlin {
73-
configure(sourceSets) {
74-
def srcDir = name.endsWith('Main') ? 'src' : 'test'
75-
def platform = name[0..-5]
76-
kotlin.srcDirs = ["$platform/$srcDir"]
77-
if (name == "jvmMain") {
78-
resources.srcDirs = ["$platform/resources"]
79-
} else if (name == "jvmTest") {
80-
resources.srcDirs = ["$platform/test-resources"]
81-
}
82-
languageSettings {
83-
progressiveMode = true
84-
optInAnnotations.each { useExperimentalAnnotation(it) }
85-
}
73+
sourceSets.forEach {
74+
SourceSetsKt.configureMultiplatform(it)
8675
}
8776

8877
configure(targets) {

kotlinx-coroutines-core/common/src/Unconfined.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal object Unconfined : CoroutineDispatcher() {
1414
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
1515

1616
override fun dispatch(context: CoroutineContext, block: Runnable) {
17-
// It can only be called by the "yield" function. See also code of "yield" function.
17+
/** It can only be called by the [yield] function. See also code of [yield] function. */
1818
val yieldContext = context[YieldContext]
1919
if (yieldContext != null) {
2020
// report to "yield" that it is an unconfined dispatcher and don't call "block.run()"

kotlinx-coroutines-core/common/src/flow/Migration.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public fun <T> Flow<T>.skip(count: Int): Flow<T> = noImpl()
260260
@Deprecated(
261261
level = DeprecationLevel.ERROR,
262262
message = "Flow analogue of 'forEach' is 'collect'",
263-
replaceWith = ReplaceWith("collect(block)")
263+
replaceWith = ReplaceWith("collect(action)")
264264
)
265265
public fun <T> Flow<T>.forEach(action: suspend (value: T) -> Unit): Unit = noImpl()
266266

kotlinx-coroutines-test/api/kotlinx-coroutines-test.api

+3-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor
3636

3737
public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
3838
public fun <init> ()V
39-
public fun cleanupTestCoroutines ()V
39+
public fun cleanupTestCoroutinesCaptor ()V
4040
public fun getUncaughtExceptions ()Ljava/util/List;
4141
public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V
4242
}
@@ -56,12 +56,11 @@ public final class kotlinx/coroutines/test/TestDispatchers {
5656
}
5757

5858
public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
59-
public abstract fun cleanupTestCoroutines ()V
59+
public abstract fun cleanupTestCoroutinesCaptor ()V
6060
public abstract fun getUncaughtExceptions ()Ljava/util/List;
6161
}
6262

6363
public final class kotlinx/coroutines/test/UncompletedCoroutinesError : java/lang/AssertionError {
64-
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
65-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
64+
public fun <init> (Ljava/lang/String;)V
6665
}
6766

kotlinx-coroutines-test/build.gradle.kts

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
dependencies {
6-
implementation(project(":kotlinx-coroutines-debug"))
5+
val experimentalAnnotations = listOf(
6+
"kotlin.Experimental",
7+
"kotlinx.coroutines.ExperimentalCoroutinesApi",
8+
"kotlinx.coroutines.InternalCoroutinesApi"
9+
)
10+
11+
kotlin {
12+
sourceSets.all { configureMultiplatform() }
713
}

kotlinx-coroutines-test/src/DelayController.kt renamed to kotlinx-coroutines-test/common/src/DelayController.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,4 @@ public interface DelayController {
126126
*/
127127
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
128128
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
129-
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)
129+
public class UncompletedCoroutinesError(message: String): AssertionError(message)

kotlinx-coroutines-test/src/TestBuilders.kt renamed to kotlinx-coroutines-test/common/src/TestBuilders.kt

+9-12
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,16 @@ public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineS
8080
runBlockingTest(this, block)
8181

8282
private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
83-
// TODO optimize it
84-
val dispatcher = get(ContinuationInterceptor).run {
85-
this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } }
86-
this ?: TestCoroutineDispatcher()
83+
val dispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
84+
is DelayController -> dispatcher
85+
null -> TestCoroutineDispatcher()
86+
else -> throw IllegalArgumentException("Dispatcher must implement DelayController: $dispatcher")
8787
}
88-
89-
val exceptionHandler = get(CoroutineExceptionHandler).run {
90-
this?.let {
91-
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" }
92-
}
93-
this ?: TestCoroutineExceptionHandler()
88+
val exceptionHandler = when (val handler = get(CoroutineExceptionHandler)) {
89+
is UncaughtExceptionCaptor -> handler
90+
null -> TestCoroutineExceptionHandler()
91+
else -> throw IllegalArgumentException("coroutineExceptionHandler must implement UncaughtExceptionCaptor: $handler")
9492
}
95-
9693
val job = get(Job) ?: SupervisorJob()
97-
return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController)
94+
return Pair(this + dispatcher + exceptionHandler + job, dispatcher)
9895
}

kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt renamed to kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlinx.atomicfu.*
88
import kotlinx.coroutines.*
99
import kotlinx.coroutines.internal.*
1010
import kotlin.coroutines.*
11+
import kotlin.jvm.*
1112
import kotlin.math.*
1213

1314
/**
@@ -67,11 +68,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
6768
/** @suppress */
6869
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
6970
val node = postDelayed(block, timeMillis)
70-
return object : DisposableHandle {
71-
override fun dispose() {
72-
queue.remove(node)
73-
}
74-
}
71+
return DisposableHandle { queue.remove(node) }
7572
}
7673

7774
/** @suppress */

kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt renamed to kotlinx-coroutines-test/common/src/TestCoroutineExceptionHandler.kt

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.coroutines.test
66

77
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.internal.*
89
import kotlin.coroutines.*
910

1011
/**
@@ -16,7 +17,7 @@ public interface UncaughtExceptionCaptor {
1617
* List of uncaught coroutine exceptions.
1718
*
1819
* The returned list is a copy of the currently caught exceptions.
19-
* During [cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
20+
* During [cleanupTestCoroutinesCaptor] the first element of this list is rethrown if it is not empty.
2021
*/
2122
public val uncaughtExceptions: List<Throwable>
2223

@@ -28,7 +29,7 @@ public interface UncaughtExceptionCaptor {
2829
*
2930
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
3031
*/
31-
public fun cleanupTestCoroutines()
32+
public fun cleanupTestCoroutinesCaptor()
3233
}
3334

3435
/**
@@ -39,21 +40,22 @@ public class TestCoroutineExceptionHandler :
3940
AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
4041
{
4142
private val _exceptions = mutableListOf<Throwable>()
43+
private val _lock = SynchronizedObject()
4244

4345
/** @suppress **/
4446
override fun handleException(context: CoroutineContext, exception: Throwable) {
45-
synchronized(_exceptions) {
47+
synchronized(_lock) {
4648
_exceptions += exception
4749
}
4850
}
4951

5052
/** @suppress **/
5153
override val uncaughtExceptions: List<Throwable>
52-
get() = synchronized(_exceptions) { _exceptions.toList() }
54+
get() = synchronized(_lock) { _exceptions.toList() }
5355

5456
/** @suppress **/
55-
override fun cleanupTestCoroutines() {
56-
synchronized(_exceptions) {
57+
override fun cleanupTestCoroutinesCaptor() {
58+
synchronized(_lock) {
5759
val exception = _exceptions.firstOrNull() ?: return
5860
// log the rest
5961
_exceptions.drop(1).forEach { it.printStackTrace() }

kotlinx-coroutines-test/src/TestCoroutineScope.kt renamed to kotlinx-coroutines-test/common/src/TestCoroutineScope.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.coroutines.*
1414
public interface TestCoroutineScope: CoroutineScope, UncaughtExceptionCaptor, DelayController {
1515
/**
1616
* Call after the test completes.
17-
* Calls [UncaughtExceptionCaptor.cleanupTestCoroutines] and [DelayController.cleanupTestCoroutines].
17+
* Calls [UncaughtExceptionCaptor.cleanupTestCoroutinesCaptor] and [DelayController.cleanupTestCoroutines].
1818
*
1919
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
2020
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
@@ -31,7 +31,7 @@ private class TestCoroutineScopeImpl (
3131
DelayController by coroutineContext.delayController
3232
{
3333
override fun cleanupTestCoroutines() {
34-
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutines()
34+
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
3535
coroutineContext.delayController.cleanupTestCoroutines()
3636
}
3737
}

0 commit comments

Comments
 (0)