Files
LibreMediaConverter/gradle/libs.versions.toml
T
JMR-devandClaude Opus 5 64c1a60a97 Drain the escaped coroutine error before the next test starts
`ConversionViewModelProbeFailureTest.an OutOfMemoryError is not swallowed`
deliberately lets a real error escape `viewModelScope.launch`, which has no
exception handler by design -- the ViewModel's KDoc says an OOM raised in the
probe should reach the thread's handler and take the process down.

On the JVM something else takes it. kotlinx-coroutines-test installs a
process-wide collector for uncaught coroutine errors, keeps whatever it catches,
and hands the backlog to the next `runTest` that starts, which throws
UncaughtExceptionsBeforeTest. Every Compose test is a `runTest`: that is how
`createComposeRule` runs a composition. So the error lands on an unrelated test
in an unrelated file, and the message names neither the test that caused it nor
the error's origin.

Nothing has hit it yet only because the sole Compose test in the repo happens to
run before the ViewModel one. R38 adds six more Compose classes in exactly the
two packages that surround it, and the first two of them made the suite fail in
two different files on two consecutive runs of identical, green code -- the
throw is on a real Dispatchers.IO thread, delivered after the state assertion
that ends the test responsible, so which class catches it is a race.

Drain it where a Compose rule is built. A @Before cannot: the rule's `runTest`
wraps the statement that calls it, so it has already thrown. @BeforeClass cannot
either, because Robolectric runs it outside the sandbox classloader, where the
collector is a different object. Constructing the rule is early enough, since
JUnit builds a fresh test-class instance -- and every @get:Rule field on it --
before evaluating any rule.

This is containment, not the cure. The cure is a seam: give the probe hop an
injectable dispatcher the way the constructor already does for
cleanupDispatcher, so the error has somewhere to land. That is a production
change and deserves its own commit.

kotlinx-coroutines-test was already on the unit-test classpath through
compose-ui-test-junit4; it is declared now because a file imports it. Pinned,
like robolectric and the linters: org.jetbrains.kotlinx is not one of the groups
the prerelease guard covers, so a float here would be free to take a milestone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:30:32 -05:00

163 lines
9.8 KiB
TOML

[versions]
# Build tooling. Pinned, never floating. agp and kotlin are coupled (below); ksp is
# staged for a later phase and unapplied today -- KSP2 versions independently of Kotlin.
#
# AGP 9 has BUILT-IN Kotlin: it compiles Kotlin itself rather than delegating to the
# org.jetbrains.kotlin.android plugin, which is incompatible with its DSL and fails the
# build if applied. DO NOT add it.
#
# By default AGP compiles with the KGP it bundles -- 2.2.10 for AGP 9.3.1, per its POM --
# and that version caps jvmTarget at 24, which would cap the app's bytecode below the JDK
# everything else runs on. The root build.gradle.kts therefore puts KGP on the buildscript
# classpath explicitly, which AGP's built-in Kotlin picks up instead. That is the only
# reason `kotlin` here can be ahead of what AGP ships.
#
# The Compose compiler plugin is versioned in lockstep with Kotlin and is read from this
# same `kotlin` entry, so the two cannot drift. Raising `kotlin` means checking that a
# matching compose-compiler-gradle-plugin exists.
agp = "9.3.1"
kotlin = "2.4.10"
ksp = "2.3.11"
# AndroidX / Compose -- floating on minor + patch. The prerelease guard in
# app/build.gradle.kts is what keeps `+` from selecting an alpha: several of these
# (lifecycle, navigation, work, datastore, annotation) publish alphas and RCs with
# version numbers ABOVE their newest stable, and Gradle's `+` would take them.
# Bare `+`, not "2026.+": the year is the major in this scheme (YYYY.MM.PP), so a
# 2026-prefixed float would quietly stop finding releases on 1 January and keep
# building green against a frozen BOM.
composeBom = "+"
coreKtx = "1.+"
activityCompose = "1.+"
lifecycle = "2.+"
navigation = "2.+"
work = "2.+"
datastore = "1.+"
media3 = "1.+"
room = "2.+"
documentfile = "1.+"
annotation = "1.+"
# Test -- floating, same rules as above.
junit = "4.+"
androidxJunit = "1.+"
espressoCore = "3.+"
# PINNED, unlike its neighbours. Under semver a 0.x minor is allowed to break, and
# this library is load-bearing exactly where breakage is hardest to see: the wrapper
# reaches for smartexception.java.Exceptions only when an FFmpeg call FAILS, so a
# moved class surfaces as an R8 missing-class error at release time, or as a crash on
# the error path -- the least-exercised code in the app. Bump it deliberately.
smartException = "0.2.1"
# Lint/format. PINNED, deliberately, while the libraries above float.
#
# A library bump that misbehaves usually still compiles. An analysis-tool bump does
# something worse: a new rule in ktlint or detekt makes files nobody touched stop
# passing, so CI goes red on a PR whose diff cannot explain it. Upgrading these is
# therefore an act with its own commit -- run the tool, read the new findings, fix or
# relax them -- which is exactly the reviewable step floating is meant to skip.
#
# ktlint owns formatting; detekt owns static analysis (its formatting ruleset stays
# off, so the two can never disagree about the same line).
# detekt 2.0 is the only line with Gradle 9 support -- stable 1.23.x tops out at
# Gradle 8.12, and this project is on 9.7.1.
ktlint = "14.2.0"
detekt = "2.0.0-alpha.6"
# JaCoCo coverage agent. Pinned rather than inheriting whatever Gradle 9.7.1 bundles,
# so the agent version that reads Kotlin 2.2.10 bytecode is stated, not implied.
jacoco = "0.8.15"
# Robolectric. PINNED, and it belongs with ktlint/detekt/jacoco above rather than with
# the floating libraries, for two reasons that both point the same way.
#
# First, the prerelease guard in app/build.gradle.kts only covers the groups this project
# floats -- "androidx.", "junit", "com.arthenica" -- so org.robolectric is unguarded, and
# a "4.+" here would resolve straight to 4.17-beta-3, which is the newest thing published.
# Second, Robolectric is not a library the app ships: it is the JVM's Android runtime, and
# a version bump changes which android-all jar the tests execute against. That is the same
# "a tool moved under a diff that cannot explain it" failure the linters are pinned for.
#
# 4.16.1 is the newest RELEASED version; the 4.17 line is beta-only at the time of writing.
robolectric = "4.16.1"
# kotlinx-coroutines-test. Already on the unit-test classpath transitively, through
# compose-ui-test-junit4 -- declared here because a source file now imports it, and a direct
# import of a transitive is a dependency nobody chose.
#
# PINNED, for the same reason as robolectric above: org.jetbrains.kotlinx is not one of the
# groups in the prerelease guard's `floatedGroupPrefixes`, so a "1.+" here would be free to
# resolve to a milestone build. This value is what the Compose BOM already resolves it to, so
# stating it changes nothing in the graph today; if the BOM moves ahead, Gradle takes the
# higher version and this stays a floor rather than a conflict.
coroutinesTest = "1.9.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
# Compose — versions come from the BOM, so no version.ref here.
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-windowsize = { group = "androidx.compose.material3", name = "material3-window-size-class" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
# Media3 — the hardware fast path (Apache-2.0).
media3-transformer = { group = "androidx.media3", name = "media3-transformer", version.ref = "media3" }
media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3" }
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" }
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
media3-muxer = { group = "androidx.media3", name = "media3-muxer", version.ref = "media3" }
# Room — job history. Added in a later phase; KSP plugin stays unapplied until then.
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
# Required at runtime by the ffmpeg-kit-next wrapper: AbstractSession.fail()
# references smartexception.java.Exceptions. Debug builds tolerate its absence through
# lazy class loading, so this only surfaces as a crash on the first FFmpeg error --
# or, as it did here, as an R8 missing-class error.
smart-exception-java = { group = "com.arthenica", name = "smart-exception-java", version.ref = "smartException" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
# Robolectric — an Android runtime for the JVM test source set, so file-lifecycle behaviour
# that needs a real Context can be verified without a device. The instrumented suite cannot
# run on the development host at all (see CLAUDE.md), so an androidTest-only red test is not
# a TDD loop anyone here can execute.
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
# Only for its `runTest`, and only to drain the collector kotlinx-coroutines-test installs
# process-wide. See EscapedCoroutineErrors.kt in the JVM test source set.
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" }
[plugins]
# com.android.application and org.jetbrains.kotlin.plugin.compose are deliberately absent.
# They come from the root buildscript classpath (see build.gradle.kts) so that a newer KGP
# can override AGP's bundled one, and the module applies them by id(). An alias here would
# be dead weight that reads like the source of truth.
#
# ktlint and detekt are ordinary plugin-portal resolutions and stay aliases.
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
detekt = { id = "dev.detekt", version.ref = "detekt" }
# DO NOT add org.jetbrains.kotlin.android — AGP 9 built-in Kotlin makes it a build failure.