Skip to content

Commit 219eec0

Browse files
authored
Introduce timeouts in tests for waitForIdle and createComposeWindow (#2718)
## Testing N/A ## Release Notes N/A
1 parent 02f343b commit 219eec0

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
@@ -104,13 +107,23 @@ internal interface OnCanvasTests {
104107
) {
105108
ComposeViewport(viewportContainerId = containerId, configure = configure, content = content)
106109

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

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

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

0 commit comments

Comments
 (0)