Skip to content

Commit 35a1511

Browse files
committed
Introduce timeouts in tests for waitForIdle and createComposeWindow
1 parent 412620a commit 35a1511

File tree

2 files changed

+42
-8
lines changed

2 files changed

+42
-8
lines changed

compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import kotlin.coroutines.EmptyCoroutineContext
4949
import kotlin.coroutines.cancellation.CancellationException
5050
import kotlin.math.roundToInt
5151
import kotlin.time.Duration
52+
import kotlin.time.DurationUnit
53+
import kotlin.time.toDuration
5254
import kotlinx.coroutines.CoroutineDispatcher
5355
import kotlinx.coroutines.CoroutineScope
5456
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -366,7 +368,8 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
366368
}
367369

368370
override fun waitForIdle() {
369-
// TODO: consider adding a timeout to avoid an infinite loop?
371+
val startedAt = currentNanoTime().toDuration(DurationUnit.NANOSECONDS)
372+
var lastReportedElapsedSeconds = 0L
370373
// always check even if we are idle
371374
uncaughtExceptionHandler.throwUncaught()
372375
while (!isIdle()) {
@@ -375,6 +378,12 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
375378
if (!areAllResourcesIdle()) {
376379
sleep(IDLING_RESOURCES_CHECK_INTERVAL_MS)
377380
}
381+
val currentTime = currentNanoTime().toDuration(DurationUnit.NANOSECONDS)
382+
val elapsedSeconds = (currentTime - startedAt).inWholeSeconds
383+
if (elapsedSeconds > lastReportedElapsedSeconds) {
384+
println("Suspicious! waitForIdle has not finished after $elapsedSeconds seconds.")
385+
lastReportedElapsedSeconds = elapsedSeconds
386+
}
378387
}
379388
}
380389

compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/OnCanvasTests.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ import kotlinx.coroutines.CoroutineScope
3434
import kotlinx.coroutines.Dispatchers
3535
import kotlinx.coroutines.Job
3636
import kotlinx.coroutines.MainScope
37+
import kotlinx.coroutines.TimeoutCancellationException
3738
import kotlinx.coroutines.channels.Channel
3839
import kotlinx.coroutines.flow.collect
3940
import kotlinx.coroutines.flow.takeWhile
4041
import kotlinx.coroutines.launch
4142
import kotlinx.coroutines.test.TestResult
4243
import kotlinx.coroutines.test.runTest
44+
import kotlinx.coroutines.withContext
45+
import kotlinx.coroutines.withTimeout
4346
import kotlinx.coroutines.yield
4447
import org.w3c.dom.Element
4548
import org.w3c.dom.HTMLCanvasElement
@@ -103,13 +106,23 @@ internal interface OnCanvasTests {
103106
) {
104107
ComposeViewport(viewportContainerId = containerId, configure = configure, content = content)
105108

106-
suspendCoroutine { continuation ->
107-
// This helps reduce the flakiness.
108-
// A potential cause of flakiness: the default Coroutine Dispatcher regularly postpones
109-
// the resumption of the tasks in its queue to the next frame.
110-
// (it does so to let the event loop run / release the single thread)
111-
// I don't expect any issue from doing this, since a test will suspend and won't do anything.
112-
window.requestAnimationFrame { continuation.resumeWith(Result.success(it)) }
109+
withContext(Dispatchers.Default) {
110+
val timeoutDuration = 1.seconds
111+
try {
112+
// Using withTimeout here for diagnostic. Some flaky tests fail due to timeout. But there is nothing else except this suspend call.
113+
withTimeout(timeoutDuration) {
114+
suspendCoroutine<Unit> { continuation ->
115+
// This helps reduce the flakiness.
116+
// A potential cause of flakiness: the default Coroutine Dispatcher regularly postpones
117+
// the resumption of the tasks in its queue to the next frame.
118+
// (it does so to let the event loop run / release the single thread)
119+
// I don't expect any issue from doing this, since a test will suspend and won't do anything.
120+
window.requestAnimationFrame { continuation.resumeWith(Result.success(Unit)) }
121+
}
122+
}
123+
} catch (e: TimeoutCancellationException) {
124+
throw AssertionError("Timed out ($timeoutDuration) waiting for AnimationFrame", e)
125+
}
113126
}
114127
}
115128

@@ -158,6 +171,18 @@ internal interface OnCanvasTests {
158171
WebApplicationScope(this).body()
159172
}
160173
}
174+
175+
suspend fun <T> Channel<T>.receiveWithTimeout(timeout: Duration = 2.seconds): T {
176+
return withContext(Dispatchers.Default) {
177+
try {
178+
withTimeout(timeout) {
179+
this@receiveWithTimeout.receive()
180+
}
181+
} catch (e: TimeoutCancellationException) {
182+
throw AssertionError("Timed out ($timeout) waiting for value on channel", e)
183+
}
184+
}
185+
}
161186
}
162187

163188
internal fun <T> Channel<T>.sendFromScope(value: T, scope: CoroutineScope = MainScope()) {

0 commit comments

Comments
 (0)