Skip to content

Commit 38d3e51

Browse files
committed
Test AndroidExceptionPreHandlerTest and awaitFrame in Adnroid module
Addresses #3156
1 parent 39c0318 commit 38d3e51

File tree

5 files changed

+269
-132
lines changed

5 files changed

+269
-132
lines changed

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ 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

1716
// TODO figure it out, these probably should be fixed
@@ -30,7 +29,7 @@ extensions.configure<KoverExtension> {
3029
* ./gradlew :p:check -Pkover.enabled=true -- verifies coverage
3130
* ./gradlew :p:koverReport -Pkover.enabled=true -- generates report
3231
*/
33-
isDisabled = !(properties["kover.enabled"]?.toString()?.toBoolean() ?: false)
32+
isDisabled = false// !(properties["kover.enabled"]?.toString()?.toBoolean() ?: false)
3433
}
3534

3635
subprojects {

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)