Skip to content

Commit 47331d5

Browse files
qwwdfsadpablobaxter
authored andcommitted
Coverage: improve test coverage, disable deprecations, add missing test… (Kotlin#3161)
Addresses Kotlin#3156
1 parent ac276a1 commit 47331d5

File tree

10 files changed

+325
-137
lines changed

10 files changed

+325
-137
lines changed

buildSrc/src/main/kotlin/kover-conventions.gradle.kts

+7-7
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ val notCovered = sourceless + internal + unpublished
1010

1111
val expectedCoverage = mutableMapOf(
1212
// These have lower coverage in general, it can be eventually fixed
13-
"kotlinx-coroutines-swing" to 70,
14-
"kotlinx-coroutines-android" to 50,
13+
"kotlinx-coroutines-swing" to 70, // awaitFrame is not tested
1514
"kotlinx-coroutines-javafx" to 39, // JavaFx is not tested on TC because its graphic subsystem cannot be initialized in headless mode
1615

17-
// TODO figure it out, these probably should be fixed
18-
"kotlinx-coroutines-debug" to 84,
19-
"kotlinx-coroutines-reactive" to 65,
16+
// Re-evaluate this along with Kover update where deprecated with error+ functions are not considered as uncovered: IDEA-287459
2017
"kotlinx-coroutines-reactor" to 65,
21-
"kotlinx-coroutines-rx2" to 78,
22-
"kotlinx-coroutines-slf4j" to 81
18+
"kotlinx-coroutines-rx2" to 78
2319
)
2420

2521
extensions.configure<KoverExtension> {
@@ -51,4 +47,8 @@ subprojects {
5147
}
5248
}
5349
}
50+
51+
tasks.withType<KoverHtmlReportTask> {
52+
htmlReportDir.set(file(rootProject.buildDir.toString() + "/kover/" + project.name + "/html"))
53+
}
5454
}

integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ class MDCContextTest : TestBase() {
102102
val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!!
103103
withContext(Dispatchers.Default + MDCContext()) {
104104
assertEquals("myValue", MDC.get("myKey"))
105+
assertEquals("myValue", coroutineContext[MDCContext]?.contextMap?.get("myKey"))
105106
withContext(mainDispatcher) {
106107
assertEquals("myValue", MDC.get("myKey"))
107108
}
108109
}
109110
}
110-
}
111+
}

kotlinx-coroutines-debug/build.gradle

+12
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,15 @@ shadowJar {
5151
configurations = [project.configurations.shadowDeps]
5252
relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy')
5353
}
54+
55+
def commonKoverExcludes =
56+
// Never used, safety mechanism
57+
["kotlinx.coroutines.debug.internal.NoOpProbesKt"]
58+
59+
tasks.koverHtmlReport {
60+
excludes = commonKoverExcludes
61+
}
62+
63+
tasks.koverVerify {
64+
excludes = commonKoverExcludes
65+
}

kotlinx-coroutines-debug/test/ToStringTest.kt

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
88
import kotlinx.coroutines.channels.*
99
import org.junit.*
1010
import org.junit.Test
11+
import java.io.*
1112
import kotlin.coroutines.*
1213
import kotlin.test.*
1314

@@ -105,6 +106,8 @@ class ToStringTest : TestBase() {
105106
expect(6)
106107
assertEquals(expected, DebugProbes.jobToString(root).trimEnd().trimStackTrace().trimPackage())
107108
assertEquals(expected, DebugProbes.scopeToString(CoroutineScope(root)).trimEnd().trimStackTrace().trimPackage())
109+
assertEquals(expected, printToString { DebugProbes.printScope(CoroutineScope(root), it) }.trimEnd().trimStackTrace().trimPackage())
110+
assertEquals(expected, printToString { DebugProbes.printJob(root, it) }.trimEnd().trimStackTrace().trimPackage())
108111

109112
root.cancelAndJoin()
110113
finish(7)
@@ -145,4 +148,12 @@ class ToStringTest : TestBase() {
145148
}
146149
}
147150
}
151+
152+
private inline fun printToString(block: (PrintStream) -> Unit): String {
153+
val baos = ByteArrayOutputStream()
154+
val ps = PrintStream(baos)
155+
block(ps)
156+
ps.close()
157+
return baos.toString()
158+
}
148159
}

reactive/kotlinx-coroutines-reactive/build.gradle.kts

+14
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,17 @@ tasks.check {
3434
externalDocumentationLink(
3535
url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/"
3636
)
37+
38+
val commonKoverExcludes = listOf(
39+
"kotlinx.coroutines.reactive.FlowKt", // Deprecated
40+
"kotlinx.coroutines.reactive.FlowKt__MigrationKt", // Deprecated
41+
"kotlinx.coroutines.reactive.ConvertKt" // Deprecated
42+
)
43+
44+
tasks.koverHtmlReport {
45+
excludes = commonKoverExcludes
46+
}
47+
48+
tasks.koverVerify {
49+
excludes = commonKoverExcludes
50+
}

reactive/kotlinx-coroutines-reactor/build.gradle.kts

+12
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,15 @@ tasks {
2727
externalDocumentationLink(
2828
url = "https://projectreactor.io/docs/core/$reactorVersion/api/"
2929
)
30+
31+
val commonKoverExcludes = listOf(
32+
"kotlinx.coroutines.reactor.FlowKt" // Deprecated
33+
)
34+
35+
tasks.koverHtmlReport {
36+
excludes = commonKoverExcludes
37+
}
38+
39+
tasks.koverVerify {
40+
excludes = commonKoverExcludes
41+
}

ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public suspend fun awaitFrame(): Long {
190190
postFrameCallback(choreographer, cont)
191191
}
192192
}
193-
// post into looper thread thread to figure it out
193+
// post into looper thread to figure it out
194194
return suspendCancellableCoroutine { cont ->
195195
Dispatchers.Main.dispatch(EmptyCoroutineContext, Runnable {
196196
updateChoreographerAndPostFrameCallback(cont)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.android
6+
7+
import kotlinx.coroutines.*
8+
import org.junit.Test
9+
import org.junit.runner.*
10+
import org.robolectric.*
11+
import org.robolectric.annotation.*
12+
import kotlin.test.*
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
@Config(manifest = Config.NONE, sdk = [27])
16+
class AndroidExceptionPreHandlerTest : TestBase() {
17+
@Test
18+
fun testUnhandledException() = runTest {
19+
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
20+
try {
21+
Thread.setDefaultUncaughtExceptionHandler { _, e ->
22+
expect(3)
23+
assertIs<TestException>(e)
24+
}
25+
expect(1)
26+
GlobalScope.launch(Dispatchers.Main) {
27+
expect(2)
28+
throw TestException()
29+
}.join()
30+
finish(4)
31+
} finally {
32+
Thread.setDefaultUncaughtExceptionHandler(previousHandler)
33+
}
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.android
6+
7+
import android.os.*
8+
import kotlinx.coroutines.*
9+
import org.junit.Test
10+
import org.junit.runner.*
11+
import org.robolectric.*
12+
import org.robolectric.Shadows.*
13+
import org.robolectric.annotation.*
14+
import org.robolectric.shadows.*
15+
import org.robolectric.util.*
16+
import java.util.concurrent.*
17+
import kotlin.test.*
18+
19+
@RunWith(RobolectricTestRunner::class)
20+
@Config(manifest = Config.NONE, sdk = [28])
21+
class HandlerDispatcherAsyncTest : TestBase() {
22+
23+
/**
24+
* Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a
25+
* result we only test its behavior on the newest API level and assert that it uses async
26+
* messages. We rely on the other tests to exercise the variance of the mechanism that the main
27+
* dispatcher uses to ensure it has correct behavior on all API levels.
28+
*/
29+
@Test
30+
fun mainIsAsync() = runTest {
31+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
32+
33+
val mainLooper = shadowOf(Looper.getMainLooper())
34+
mainLooper.pause()
35+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
36+
37+
val job = launch(Dispatchers.Main) {
38+
expect(2)
39+
}
40+
41+
val message = mainMessageQueue.head
42+
assertTrue(message.isAsynchronous)
43+
job.join(mainLooper)
44+
}
45+
46+
@Test
47+
fun asyncMessagesApi14() = runTest {
48+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14)
49+
50+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
51+
52+
val mainLooper = shadowOf(Looper.getMainLooper())
53+
mainLooper.pause()
54+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
55+
56+
val job = launch(main) {
57+
expect(2)
58+
}
59+
60+
val message = mainMessageQueue.head
61+
assertFalse(message.isAsynchronous)
62+
job.join(mainLooper)
63+
}
64+
65+
@Test
66+
fun asyncMessagesApi16() = runTest {
67+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16)
68+
69+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
70+
71+
val mainLooper = shadowOf(Looper.getMainLooper())
72+
mainLooper.pause()
73+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
74+
75+
val job = launch(main) {
76+
expect(2)
77+
}
78+
79+
val message = mainMessageQueue.head
80+
assertTrue(message.isAsynchronous)
81+
job.join(mainLooper)
82+
}
83+
84+
@Test
85+
fun asyncMessagesApi28() = runTest {
86+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
87+
88+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
89+
90+
val mainLooper = shadowOf(Looper.getMainLooper())
91+
mainLooper.pause()
92+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
93+
94+
val job = launch(main) {
95+
expect(2)
96+
}
97+
98+
val message = mainMessageQueue.head
99+
assertTrue(message.isAsynchronous)
100+
job.join(mainLooper)
101+
}
102+
103+
@Test
104+
fun noAsyncMessagesIfNotRequested() = runTest {
105+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
106+
107+
val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher()
108+
109+
val mainLooper = shadowOf(Looper.getMainLooper())
110+
mainLooper.pause()
111+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
112+
113+
val job = launch(main) {
114+
expect(2)
115+
}
116+
117+
val message = mainMessageQueue.head
118+
assertFalse(message.isAsynchronous)
119+
job.join(mainLooper)
120+
}
121+
122+
@Test
123+
fun testToString() {
124+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
125+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName")
126+
assertEquals("testName", main.toString())
127+
assertEquals("testName.immediate", main.immediate.toString())
128+
assertEquals("testName.immediate", main.immediate.immediate.toString())
129+
}
130+
131+
private suspend fun Job.join(mainLooper: ShadowLooper) {
132+
expect(1)
133+
mainLooper.unPause()
134+
join()
135+
finish(3)
136+
}
137+
138+
// TODO compile against API 23+ so this can be invoked without reflection.
139+
private val Looper.queue: MessageQueue
140+
get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue
141+
142+
// TODO compile against API 22+ so this can be invoked without reflection.
143+
private val Message.isAsynchronous: Boolean
144+
get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean
145+
}

0 commit comments

Comments
 (0)