diff --git a/buildSrc/src/main/kotlin/kover-conventions.gradle.kts b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts index 3a40331a8e..125ddb19ea 100644 --- a/buildSrc/src/main/kotlin/kover-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts @@ -10,16 +10,12 @@ val notCovered = sourceless + internal + unpublished val expectedCoverage = mutableMapOf( // These have lower coverage in general, it can be eventually fixed - "kotlinx-coroutines-swing" to 70, - "kotlinx-coroutines-android" to 50, + "kotlinx-coroutines-swing" to 70, // awaitFrame is not tested "kotlinx-coroutines-javafx" to 39, // JavaFx is not tested on TC because its graphic subsystem cannot be initialized in headless mode - // TODO figure it out, these probably should be fixed - "kotlinx-coroutines-debug" to 84, - "kotlinx-coroutines-reactive" to 65, + // Re-evaluate this along with Kover update where deprecated with error+ functions are not considered as uncovered: IDEA-287459 "kotlinx-coroutines-reactor" to 65, - "kotlinx-coroutines-rx2" to 78, - "kotlinx-coroutines-slf4j" to 81 + "kotlinx-coroutines-rx2" to 78 ) extensions.configure { @@ -51,4 +47,8 @@ subprojects { } } } + + tasks.withType { + htmlReportDir.set(file(rootProject.buildDir.toString() + "/kover/" + project.name + "/html")) + } } diff --git a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt index 7d18359c5d..532c47e9ed 100644 --- a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt +++ b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt @@ -102,9 +102,10 @@ class MDCContextTest : TestBase() { val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!! withContext(Dispatchers.Default + MDCContext()) { assertEquals("myValue", MDC.get("myKey")) + assertEquals("myValue", coroutineContext[MDCContext]?.contextMap?.get("myKey")) withContext(mainDispatcher) { assertEquals("myValue", MDC.get("myKey")) } } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index 4830670d24..c01e70463f 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -51,3 +51,15 @@ shadowJar { configurations = [project.configurations.shadowDeps] relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy') } + +def commonKoverExcludes = + // Never used, safety mechanism + ["kotlinx.coroutines.debug.internal.NoOpProbesKt"] + +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} diff --git a/kotlinx-coroutines-debug/test/ToStringTest.kt b/kotlinx-coroutines-debug/test/ToStringTest.kt index 0a9e84efad..0ea412b55d 100644 --- a/kotlinx-coroutines-debug/test/ToStringTest.kt +++ b/kotlinx-coroutines-debug/test/ToStringTest.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.junit.* import org.junit.Test +import java.io.* import kotlin.coroutines.* import kotlin.test.* @@ -105,6 +106,8 @@ class ToStringTest : TestBase() { expect(6) assertEquals(expected, DebugProbes.jobToString(root).trimEnd().trimStackTrace().trimPackage()) assertEquals(expected, DebugProbes.scopeToString(CoroutineScope(root)).trimEnd().trimStackTrace().trimPackage()) + assertEquals(expected, printToString { DebugProbes.printScope(CoroutineScope(root), it) }.trimEnd().trimStackTrace().trimPackage()) + assertEquals(expected, printToString { DebugProbes.printJob(root, it) }.trimEnd().trimStackTrace().trimPackage()) root.cancelAndJoin() finish(7) @@ -145,4 +148,12 @@ class ToStringTest : TestBase() { } } } + + private inline fun printToString(block: (PrintStream) -> Unit): String { + val baos = ByteArrayOutputStream() + val ps = PrintStream(baos) + block(ps) + ps.close() + return baos.toString() + } } diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index 128d4d86ab..b624069c60 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -34,3 +34,17 @@ tasks.check { externalDocumentationLink( url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/" ) + +val commonKoverExcludes = listOf( + "kotlinx.coroutines.reactive.FlowKt", // Deprecated + "kotlinx.coroutines.reactive.FlowKt__MigrationKt", // Deprecated + "kotlinx.coroutines.reactive.ConvertKt" // Deprecated +) + +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts index 03af7f4fda..5abf3862dc 100644 --- a/reactive/kotlinx-coroutines-reactor/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactor/build.gradle.kts @@ -27,3 +27,15 @@ tasks { externalDocumentationLink( url = "https://projectreactor.io/docs/core/$reactorVersion/api/" ) + +val commonKoverExcludes = listOf( + "kotlinx.coroutines.reactor.FlowKt" // Deprecated +) + +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index ffd5df060c..5e33169dd1 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -190,7 +190,7 @@ public suspend fun awaitFrame(): Long { postFrameCallback(choreographer, cont) } } - // post into looper thread thread to figure it out + // post into looper thread to figure it out return suspendCancellableCoroutine { cont -> Dispatchers.Main.dispatch(EmptyCoroutineContext, Runnable { updateChoreographerAndPostFrameCallback(cont) diff --git a/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt new file mode 100644 index 0000000000..4aa44eceaf --- /dev/null +++ b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.android + +import kotlinx.coroutines.* +import org.junit.Test +import org.junit.runner.* +import org.robolectric.* +import org.robolectric.annotation.* +import kotlin.test.* + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [27]) +class AndroidExceptionPreHandlerTest : TestBase() { + @Test + fun testUnhandledException() = runTest { + val previousHandler = Thread.getDefaultUncaughtExceptionHandler() + try { + Thread.setDefaultUncaughtExceptionHandler { _, e -> + expect(3) + assertIs(e) + } + expect(1) + GlobalScope.launch(Dispatchers.Main) { + expect(2) + throw TestException() + }.join() + finish(4) + } finally { + Thread.setDefaultUncaughtExceptionHandler(previousHandler) + } + } +} diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt new file mode 100644 index 0000000000..7b03e771f9 --- /dev/null +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.android + +import android.os.* +import kotlinx.coroutines.* +import org.junit.Test +import org.junit.runner.* +import org.robolectric.* +import org.robolectric.Shadows.* +import org.robolectric.annotation.* +import org.robolectric.shadows.* +import org.robolectric.util.* +import java.util.concurrent.* +import kotlin.test.* + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [28]) +class HandlerDispatcherAsyncTest : TestBase() { + + /** + * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a + * result we only test its behavior on the newest API level and assert that it uses async + * messages. We rely on the other tests to exercise the variance of the mechanism that the main + * dispatcher uses to ensure it has correct behavior on all API levels. + */ + @Test + fun mainIsAsync() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(Dispatchers.Main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi14() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi16() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi28() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun noAsyncMessagesIfNotRequested() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun testToString() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName") + assertEquals("testName", main.toString()) + assertEquals("testName.immediate", main.immediate.toString()) + assertEquals("testName.immediate", main.immediate.immediate.toString()) + } + + private suspend fun Job.join(mainLooper: ShadowLooper) { + expect(1) + mainLooper.unPause() + join() + finish(3) + } + + // TODO compile against API 23+ so this can be invoked without reflection. + private val Looper.queue: MessageQueue + get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue + + // TODO compile against API 22+ so this can be invoked without reflection. + private val Message.isAsynchronous: Boolean + get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean +} diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt index af17adfc00..24758444b0 100644 --- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.android @@ -9,173 +9,131 @@ import kotlinx.coroutines.* import org.junit.Test import org.junit.runner.* import org.robolectric.* -import org.robolectric.Shadows.* import org.robolectric.annotation.* import org.robolectric.shadows.* -import org.robolectric.util.* +import java.util.concurrent.* import kotlin.test.* @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE, sdk = [28]) class HandlerDispatcherTest : TestBase() { - - /** - * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a - * result we only test its behavior on the newest API level and assert that it uses async - * messages. We rely on the other tests to exercise the variance of the mechanism that the main - * dispatcher uses to ensure it has correct behavior on all API levels. - */ @Test - fun mainIsAsync() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val mainLooper = shadowOf(Looper.getMainLooper()) - mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(Dispatchers.Main) { + fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) { + expect(1) + // launch in the immediate dispatcher + launch(Dispatchers.Main.immediate) { expect(2) + yield() + expect(4) } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(3) // after yield + yield() // yield back + finish(5) } @Test - fun asyncMessagesApi14() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) - mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) - } - - val message = mainMessageQueue.head - assertFalse(message.isAsynchronous) - job.join(mainLooper) + fun testMainDispatcherToString() { + assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) } @Test - fun asyncMessagesApi16() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testDefaultDelayIsNotDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + assertFalse { mainLooper.scheduler.areAnyRunnable() } - val job = launch(main) { - expect(2) + val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + expect(1) + delay(Long.MAX_VALUE) + expectUnreached() } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(0, mainLooper.scheduler.size()) + job.cancelAndJoin() + finish(3) } @Test - fun asyncMessagesApi28() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testWithTimeoutIsDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) + assertFalse { mainLooper.scheduler.areAnyRunnable() } + val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + withTimeout(1) { + expect(1) + hang { expect(3) } + } + expectUnreached() } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(1, mainLooper.scheduler.size()) + // Schedule cancellation + mainLooper.runToEndOfTasks() + job.join() + finish(4) } @Test - fun noAsyncMessagesIfNotRequested() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testDelayDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) + val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + expect(1) + delay(1) + expect(3) } - - val message = mainMessageQueue.head - assertFalse(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(1, mainLooper.scheduler.size()) + // Schedule cancellation + mainLooper.runToEndOfTasks() + job.join() + finish(4) } @Test - fun testToString() { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName") - assertEquals("testName", main.toString()) - assertEquals("testName.immediate", main.immediate.toString()) - assertEquals("testName.immediate", main.immediate.immediate.toString()) - } + fun testAwaitFrame() = runTest { + doTestAwaitFrame() - private suspend fun Job.join(mainLooper: ShadowLooper) { - expect(1) - mainLooper.unPause() - join() - finish(3) - } + reset() - // TODO compile against API 23+ so this can be invoked without reflection. - private val Looper.queue: MessageQueue - get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue - - // TODO compile against API 22+ so this can be invoked without reflection. - private val Message.isAsynchronous: Boolean - get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean - - @Test - fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) { - expect(1) - // launch in the immediate dispatcher - launch(Dispatchers.Main.immediate) { - expect(2) - yield() - expect(4) - } - expect(3) // after yield - yield() // yield back - finish(5) + // Now the second test: we cannot test it separately because we're caching choreographer in HandlerDispatcher + doTestAwaitWithDetectedChoreographer() } - @Test - fun testMainDispatcherToString() { - assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) - assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) + private fun CoroutineScope.doTestAwaitFrame() { + ShadowChoreographer.setPostFrameCallbackDelay(100) + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) + mainLooper.pause() + launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + expect(1) + awaitFrame() + expect(5) + } + expect(2) + // Run choreographer detection + mainLooper.runOneTask() + expect(3) + mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS) + expect(4) + mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS) + finish(6) } - @Test - fun testDelayIsNotDelegatedToMain() = runTest { - val mainLooper = shadowOf(Looper.getMainLooper()) - mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - assertNull(mainMessageQueue.head) - val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + private fun CoroutineScope.doTestAwaitWithDetectedChoreographer() { + ShadowChoreographer.reset() + ShadowChoreographer.setPostFrameCallbackDelay(100) + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) + launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { expect(1) - delay(Long.MAX_VALUE) - expectUnreached() + awaitFrame() + expect(4) } + // Run choreographer detection expect(2) - assertNull(mainMessageQueue.head) - job.cancelAndJoin() - finish(3) + mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS) + expect(3) + mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS) + finish(5) } }