Skip to content

Commit

Permalink
Coverage: improve test coverage, disable deprecations, add missing te…
Browse files Browse the repository at this point in the history
…st… (#3161)

Addresses #3156
  • Loading branch information
qwwdfsad committed Feb 1, 2022
1 parent 649d03e commit 76989f7
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 137 deletions.
14 changes: 7 additions & 7 deletions buildSrc/src/main/kotlin/kover-conventions.gradle.kts
Expand Up @@ -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<KoverExtension> {
Expand Down Expand Up @@ -51,4 +47,8 @@ subprojects {
}
}
}

tasks.withType<KoverHtmlReportTask> {
htmlReportDir.set(file(rootProject.buildDir.toString() + "/kover/" + project.name + "/html"))
}
}
3 changes: 2 additions & 1 deletion integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
Expand Up @@ -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"))
}
}
}
}
}
12 changes: 12 additions & 0 deletions kotlinx-coroutines-debug/build.gradle
Expand Up @@ -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
}
11 changes: 11 additions & 0 deletions kotlinx-coroutines-debug/test/ToStringTest.kt
Expand Up @@ -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.*

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
}
14 changes: 14 additions & 0 deletions reactive/kotlinx-coroutines-reactive/build.gradle.kts
Expand Up @@ -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
}
12 changes: 12 additions & 0 deletions reactive/kotlinx-coroutines-reactor/build.gradle.kts
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
Expand Up @@ -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)
Expand Down
@@ -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<TestException>(e)
}
expect(1)
GlobalScope.launch(Dispatchers.Main) {
expect(2)
throw TestException()
}.join()
finish(4)
} finally {
Thread.setDefaultUncaughtExceptionHandler(previousHandler)
}
}
}
145 changes: 145 additions & 0 deletions 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
}

0 comments on commit 76989f7

Please sign in to comment.