diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6a283978..192ed4be 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -210,4 +210,17 @@ dependencies { debugImplementation(libs.compose.ui.test.manifest) } -tasks.withType { useJUnitPlatform() } +tasks.withType { + useJUnitPlatform() + // Print the assertion message + full stack trace for failures. The + // default console output gives only "AssertionError at Foo.kt:12", and + // for a failure inside a `runTest { }` lambda even that line collapses + // to the test function's own line (the assertion frames live in the + // suspend-lambda class, which Gradle filters out) — leaving nothing to + // debug from when the HTML report isn't reachable, as in CI. + testLogging { + events("failed") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showStackTraces = true + } +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/UpdateVeilController.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/UpdateVeilController.kt index 83166e54..2d1fabd8 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/shared/UpdateVeilController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/UpdateVeilController.kt @@ -3,7 +3,6 @@ package com.fabledsword.minstrel.shared import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -14,7 +13,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull // Once raised, the veil stays up at least this long. Without a floor a @@ -166,15 +164,15 @@ class UpdateVeilController( // here before the raiser has been dispatched, tear the session // down, and leave the churn uncovered. withTimeoutOrNull(timings.maxHoldMs) { awaitSettled() } + // Honour the no-flash minimum before lowering. Deliberately in + // the try and not the finally: on cancellation the scope is + // going away and nothing will render the veil, so the floor is + // pointless there — and a finally that suspends is a finally + // that can resist teardown. + if (raised.isCompleted) floor.join() } finally { raiser.cancel() ceiling.cancel() - if (raised.isCompleted) { - // NonCancellable so the floor is honoured (and the veil - // always cleared) even while the scope is torn down; the - // floor job dies with the scope, so this cannot hang. - withContext(NonCancellable) { floor.join() } - } floor.cancel() visibleInternal.value = false }