Skip to content

coroutines-test as an MPP #2965

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
Oct 8, 2021
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
54 changes: 35 additions & 19 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.konan.target.HostManager
import org.gradle.util.VersionNumber
import org.jetbrains.dokka.gradle.DokkaTaskPartial
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask

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

def coreModule = "kotlinx-coroutines-core"
def testModule = "kotlinx-coroutines-test"
def multiplatform = [coreModule, testModule]
// Not applicable for Kotlin plugin
def sourceless = ['kotlinx.coroutines', 'kotlinx-coroutines-bom', 'integration-testing']
def internal = ['kotlinx.coroutines', 'benchmarks', 'integration-testing']
Expand Down Expand Up @@ -112,7 +111,7 @@ apiValidation {
ignoredProjects += unpublished + ["kotlinx-coroutines-bom"]
if (build_snapshot_train) {
ignoredProjects.remove("example-frontend-js")
ignoredProjects.add("kotlinx-coroutines-core")
ignoredProjects.add(coreModule)
}
ignoredPackages += "kotlinx.coroutines.internal"
}
Expand All @@ -133,13 +132,31 @@ allprojects {
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
evaluationDependsOn(":$coreModule")
def platform = PlatformKt.platformOf(it)
apply plugin: "kotlin-${platform}-conventions"
dependencies {
// See comment below for rationale, it will be replaced with "project" dependency
api project(":$coreModule")
// the only way IDEA can resolve test classes
testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
if (it.name in multiplatform) {
apply plugin: "kotlin-multiplatform"
apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle")
apply from: rootProject.file("gradle/compile-common.gradle")

if (rootProject.ext["native_targets_enabled"] as Boolean) {
apply from: rootProject.file("gradle/compile-native-multiplatform.gradle")
}

apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
apply from: rootProject.file("gradle/publish-npm-js.gradle")
kotlin.sourceSets.commonMain.dependencies {
api project(":$coreModule")
}
kotlin.sourceSets.jvmTest.dependencies {
implementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
}
} else {
def platform = PlatformKt.platformOf(it)
apply plugin: "kotlin-${platform}-conventions"
dependencies {
api project(":$coreModule")
// the only way IDEA can resolve test classes
testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
}
}
}

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

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

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

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

List<String> jarTasks
if (it.name == "kotlinx-coroutines-core") {
if (it.name in multiplatform) {
jarTasks = ["jvmJar", "metadataJar"]
} else if (it.name == "kotlinx-coroutines-debug") {
// We shadow debug module instead of just packaging it
Expand Down Expand Up @@ -324,12 +340,12 @@ allprojects { subProject ->
.matching {
// Excluding substituted project itself because of circular dependencies, but still do it
// for "*Test*" configurations
subProject.name != "kotlinx-coroutines-core" || it.name.contains("Test")
subProject.name != coreModule || it.name.contains("Test")
}
.configureEach { conf ->
conf.resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core"))
.using(project(":kotlinx-coroutines-core"))
substitute(module("org.jetbrains.kotlinx:$coreModule"))
.using(project(":$coreModule"))
.because("Because Kotlin compiler embeddable leaks coroutines into the runtime classpath, " +
"triggering all sort of incompatible class changes errors")
}
Expand Down
13 changes: 13 additions & 0 deletions buildSrc/src/main/kotlin/OptInPreset.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

val optInAnnotations = listOf(
"kotlin.RequiresOptIn",
"kotlin.experimental.ExperimentalTypeInference",
"kotlin.ExperimentalMultiplatform",
"kotlinx.coroutines.DelicateCoroutinesApi",
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.coroutines.ObsoleteCoroutinesApi",
"kotlinx.coroutines.InternalCoroutinesApi",
"kotlinx.coroutines.FlowPreview")
19 changes: 19 additions & 0 deletions buildSrc/src/main/kotlin/SourceSets.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import org.jetbrains.kotlin.gradle.plugin.*

fun KotlinSourceSet.configureMultiplatform() {
val srcDir = if (name.endsWith("Main")) "src" else "test"
val platform = name.dropLast(4)
kotlin.srcDir("$platform/$srcDir")
if (name == "jvmMain") {
resources.srcDir("$platform/resources")
} else if (name == "jvmTest") {
resources.srcDir("$platform/test-resources")
}
languageSettings {
optInAnnotations.forEach { optIn(it) }
progressiveMode = true
}
}
2 changes: 1 addition & 1 deletion gradle/dokka.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ tasks.withType(DokkaTaskPartial::class).configureEach {
packageListUrl.set(rootProject.projectDir.toPath().resolve("site/stdlib.package.list").toUri().toURL())
}

if (project.name != "kotlinx-coroutines-core") {
if (project.name != "kotlinx-coroutines-core" && project.name != "kotlinx-coroutines-test") {
dependsOn(project.configurations["compileClasspath"])
doFirst {
// resolve classpath only during execution
Expand Down
13 changes: 0 additions & 13 deletions gradle/opt-in.gradle

This file was deleted.

2 changes: 1 addition & 1 deletion gradle/publish.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ apply plugin: 'signing'

// ------------- tasks

def isMultiplatform = project.name == "kotlinx-coroutines-core"
def isMultiplatform = project.name == "kotlinx-coroutines-core" || project.name == "kotlinx-coroutines-test"
def isBom = project.name == "kotlinx-coroutines-bom"

if (!isBom) {
Expand Down
15 changes: 2 additions & 13 deletions kotlinx-coroutines-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,8 @@ if (rootProject.ext.native_targets_enabled) {
* because JMV-only projects depend on core, thus core should always be initialized before configuration.
*/
kotlin {
configure(sourceSets) {
def srcDir = name.endsWith('Main') ? 'src' : 'test'
def platform = name[0..-5]
kotlin.srcDirs = ["$platform/$srcDir"]
if (name == "jvmMain") {
resources.srcDirs = ["$platform/resources"]
} else if (name == "jvmTest") {
resources.srcDirs = ["$platform/test-resources"]
}
languageSettings {
progressiveMode = true
optInAnnotations.each { useExperimentalAnnotation(it) }
}
sourceSets.forEach {
SourceSetsKt.configureMultiplatform(it)
}

configure(targets) {
Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/src/Unconfined.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal object Unconfined : CoroutineDispatcher() {
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

override fun dispatch(context: CoroutineContext, block: Runnable) {
// It can only be called by the "yield" function. See also code of "yield" function.
/** It can only be called by the [yield] function. See also code of [yield] function. */
val yieldContext = context[YieldContext]
if (yieldContext != null) {
// report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/src/flow/Migration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public fun <T> Flow<T>.skip(count: Int): Flow<T> = noImpl()
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'forEach' is 'collect'",
replaceWith = ReplaceWith("collect(block)")
replaceWith = ReplaceWith("collect(action)")
)
public fun <T> Flow<T>.forEach(action: suspend (value: T) -> Unit): Unit = noImpl()

Expand Down
7 changes: 3 additions & 4 deletions kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor

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

public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
public abstract fun cleanupTestCoroutines ()V
public abstract fun cleanupTestCoroutinesCaptor ()V
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this rename, JS IR compiler just fails, and the native compiler complains

Member inherits different @Throws filters from UncaughtExceptionCaptor, DelayController

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth reporting, because JS IR should ignore @Throws

public abstract fun getUncaughtExceptions ()Ljava/util/List;
}

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

10 changes: 8 additions & 2 deletions kotlinx-coroutines-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

dependencies {
implementation(project(":kotlinx-coroutines-debug"))
val experimentalAnnotations = listOf(
"kotlin.Experimental",
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.coroutines.InternalCoroutinesApi"
)

kotlin {
sourceSets.all { configureMultiplatform() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ public interface DelayController {
*/
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)
public class UncompletedCoroutinesError(message: String): AssertionError(message)
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,16 @@ public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineS
runBlockingTest(this, block)

private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
// TODO optimize it
val dispatcher = get(ContinuationInterceptor).run {
this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } }
this ?: TestCoroutineDispatcher()
val dispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
is DelayController -> dispatcher
null -> TestCoroutineDispatcher()
else -> throw IllegalArgumentException("Dispatcher must implement DelayController: $dispatcher")
}

val exceptionHandler = get(CoroutineExceptionHandler).run {
this?.let {
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" }
}
this ?: TestCoroutineExceptionHandler()
val exceptionHandler = when (val handler = get(CoroutineExceptionHandler)) {
is UncaughtExceptionCaptor -> handler
null -> TestCoroutineExceptionHandler()
else -> throw IllegalArgumentException("coroutineExceptionHandler must implement UncaughtExceptionCaptor: $handler")
}

val job = get(Job) ?: SupervisorJob()
return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController)
return Pair(this + dispatcher + exceptionHandler + job, dispatcher)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.jvm.*
import kotlin.math.*

/**
Expand Down Expand Up @@ -67,11 +68,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
/** @suppress */
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
val node = postDelayed(block, timeMillis)
return object : DisposableHandle {
override fun dispose() {
queue.remove(node)
}
}
return DisposableHandle { queue.remove(node) }
}

/** @suppress */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*

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

Expand All @@ -28,7 +29,7 @@ public interface UncaughtExceptionCaptor {
*
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
*/
public fun cleanupTestCoroutines()
public fun cleanupTestCoroutinesCaptor()
}

/**
Expand All @@ -39,21 +40,22 @@ public class TestCoroutineExceptionHandler :
AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
{
private val _exceptions = mutableListOf<Throwable>()
private val _lock = SynchronizedObject()

/** @suppress **/
override fun handleException(context: CoroutineContext, exception: Throwable) {
synchronized(_exceptions) {
synchronized(_lock) {
_exceptions += exception
}
}

/** @suppress **/
override val uncaughtExceptions: List<Throwable>
get() = synchronized(_exceptions) { _exceptions.toList() }
get() = synchronized(_lock) { _exceptions.toList() }

/** @suppress **/
override fun cleanupTestCoroutines() {
synchronized(_exceptions) {
override fun cleanupTestCoroutinesCaptor() {
synchronized(_lock) {
val exception = _exceptions.firstOrNull() ?: return
// log the rest
_exceptions.drop(1).forEach { it.printStackTrace() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import kotlin.coroutines.*
public interface TestCoroutineScope: CoroutineScope, UncaughtExceptionCaptor, DelayController {
/**
* Call after the test completes.
* Calls [UncaughtExceptionCaptor.cleanupTestCoroutines] and [DelayController.cleanupTestCoroutines].
* Calls [UncaughtExceptionCaptor.cleanupTestCoroutinesCaptor] and [DelayController.cleanupTestCoroutines].
*
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
Expand All @@ -31,7 +31,7 @@ private class TestCoroutineScopeImpl (
DelayController by coroutineContext.delayController
{
override fun cleanupTestCoroutines() {
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutines()
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
coroutineContext.delayController.cleanupTestCoroutines()
}
}
Expand Down
Loading