ci(android): print full assertion messages for failing tests
android / Build + lint + test (push) Failing after 3m11s

CI run 3161 reported seven failures as bare "java.lang.AssertionError at
UpdateVeilControllerTest.kt:87" — and line 87 is the test's own `fun ... =
runTest {` line, not the assertion. Gradle picks the first stack frame
belonging to the test class, and assertions inside a `runTest { }` lambda
live in a generated suspend-lambda class that gets filtered out, so every
failure in a coroutine test collapses to the function declaration. With the
HTML report unreachable from CI, that leaves nothing to debug from.

testLogging with exceptionFormat = FULL prints the assertion message and the
whole stack trace for failures, which is what makes a coroutine-test failure
diagnosable at all here.

Also drop the NonCancellable floor-join from UpdateVeilController's finally.
Honouring the minimum hold while the scope is being torn down is pointless —
nothing is left to render the veil — and a finally that suspends is a finally
that can resist cancellation. The floor is now awaited in the try instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 20:39:54 -04:00
co-authored by Claude Opus 5
parent 5044e7a055
commit 4f99b42844
2 changed files with 20 additions and 9 deletions
+14 -1
View File
@@ -210,4 +210,17 @@ dependencies {
debugImplementation(libs.compose.ui.test.manifest)
}
tasks.withType<Test> { useJUnitPlatform() }
tasks.withType<Test> {
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
}
}
@@ -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
}