summaryrefslogtreecommitdiff
path: root/app/build.gradle.kts
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-11-03 08:29:32 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-11-03 08:29:32 +0000
commit913ba0e829ca20ccbb1a8db213541760dfc42a4b (patch)
treeb56b62b4739e0f4c8ed5b36cae5404936048dc22 /app/build.gradle.kts
parent87b8cfa942b995d1c013827ec7014cf6ef4138ca (diff)
Measure and test the view models the ui exclusion was hiding
The coverage gate excluded `**/ui/**` wholesale, justified as "Compose screens are impractical to unit-test". It also hid six view models -- plain Kotlin classes holding real decisions, every one at 0%, not one of them a screen -- behind a justification that only ever applied to screens. PlayerViewModel and ListenViewModel were 150 lines of untested logic the gate could not see. Done by a subagent; its coverage figure, test count and detekt result were re-derived here from the XML rather than taken on trust, and its two findings checked against the source. The exclusion now names each screen file instead of the package. All six view models go from 0% to 92-99%, the measured `ui` package to 96.6%, and the project to **95.17% against a 94.44% baseline** -- the gate passes with the exclusion narrowed and no COVERAGE_EXCEPTION. 96 new tests, suite 951 -> 1048. The tests drive the real SubscriptionRepository, PlaybackAdvancer, QueueRepository, DripReleaser and FeedResolver against the existing FakeDaos and MockWebServer, so the banners asserted are the app's own wording rather than a stub agreeing with itself. Four production seams, all behaviour-preserving: `UiGraph` and `ViewPreferences` over the graph, `PlaybackControls` over PlayerConnection (the same shape as the `Playhead` precedent), and a second constructor per view model with the `(Application)` one kept as a delegate. That last is load-bearing in a way nothing would catch: `by viewModels()` finds a constructor reflectively, so losing it throws on screen open with a green build and no failing test. ViewModelConstructionTest is the guard. Its first version checked for the literal ": AndroidViewModel" and so missed TranscriptViewModel, which wraps its supertype onto the next line -- a check that would have passed while leaving a seventh view model unguarded in silence. It now cross-checks the two ways of finding them against each other. The action and state objects are excluded on a different ground from the screens, and the comment says so explicitly so the list does not grow by analogy: they construct fine off a device, but every line jacoco counts in them is one Kotlin generated -- constructor, componentN, copy, equals, hashCode, toString. Measuring them measures the compiler. A data class that gains a method or an init block comes back off the list. Also, found while reviewing: detekt's `UnusedImports` rule is off by default, which is how 71 dead imports accumulated across nine files. EpisodeActions.kt was eleven lines of data class under forty of them -- Scaffold, SnackbarHost, LazyColumn and the rest, left behind when the screens were split per file, which made it read like a screen. The rule is on and the 71 are gone. Two in that file needed removing by hand: detekt whitelists `getValue`/`setValue` unconditionally because it cannot tell a `by` delegation from none, and that file has no delegation at all. 1048 tests pass, detekt 0 findings on a fresh report, lint clean, coverage 95.17%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'app/build.gradle.kts')
-rw-r--r--app/build.gradle.kts89
1 files changed, 88 insertions, 1 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 36d6038..a7b74cb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -254,6 +254,29 @@ jacoco {
* Compose screens are excluded for the same reason as FeeDreader: they are impractical to
* unit-test and including them makes the metric meaningless.
*
+ * They are named one screen file at a time rather than by excluding the whole `ui` package,
+ * which is what this used to do. That blanket pattern also hid six view models -- plain Kotlin
+ * classes with real decisions in them, every one measuring 0% and not one of them a Compose
+ * screen -- behind a justification that only ever applied to the screens. The list below is
+ * longer, and that is its honest length: each glob names the classes one screen file compiles
+ * to (its `...Kt` class, its `ComposableSingletons` holder, and the state and action objects
+ * only that screen constructs). Nothing else under `ui` is excluded.
+ *
+ * The action and state objects are excluded on a different ground from the screens, and it is
+ * worth being explicit so the list does not grow by analogy. They are not hard to execute off a
+ * device -- `EpisodeActions(play = {}, ...)` constructs fine. They are excluded because every
+ * line jacoco counts in them is one Kotlin generated: the constructor, `componentN`, `copy`,
+ * `equals`, `hashCode`, `toString`. There is no statement of this app's own in any of them, so
+ * measuring them measures the compiler. **That is the whole of the reason**, and it does not
+ * extend to a data class that gains a method, an `init` block or a default computed from
+ * another field -- the moment one does, it comes off this list.
+ *
+ * Two entries need saying out loud. The BreakJudgingKt glob excludes only the composables in
+ * BreakJudging.kt; the `BreakJudging` and `BreakJudgingSource` classes beside them stay
+ * measured. And the EpisodeScreenKt glob takes the tested `stripHtml` out of the measurement
+ * along with the screen it happens to live in -- a cost of excluding by file, not a reason to
+ * stop testing it.
+ *
* The rest are framework adapters -- classes whose whole body is a call into ExoPlayer, Media3,
* Room, WorkManager or Chaquopy, none of which can execute on the JVM. They were overridden
* eight times in a row with `COVERAGE_EXCEPTION`, and the last few were perverse: extracting
@@ -267,7 +290,71 @@ jacoco {
* logic runs fine against fakes.
*/
val coverageExclusions = listOf(
- "**/ui/**",
+ // Compose screens, one file at a time. See the note above: `**/ui/**` also hid the view
+ // models, which are ordinary classes and are measured.
+ //
+ // Every Compose file compiles a `ComposableSingletons` holder for its constant lambdas.
+ // It is generated, there is nothing in it to test, and it belongs to the screen it is
+ // named after.
+ "**/ui/ComposableSingletons\$*",
+
+ "**/ui/MainActivity*",
+ "**/ui/ShareReceiverActivity*",
+ "**/ui/BreakJudgingKt*",
+ "**/ui/DripSettingsKt*",
+ "**/ui/EpisodeDescriptionKt*",
+ "**/ui/EpisodeOverflowKt*",
+ "**/ui/EpisodeRowKt*",
+ "**/ui/EpisodeScreenKt*",
+ "**/ui/EpisodeTimelineKt*",
+ "**/ui/EpisodeTransportKt*",
+ "**/ui/FurnitureSkipSettingsStateKt*",
+ "**/ui/LibraryScreenKt*",
+ "**/ui/NowPlayingBarKt*",
+ "**/ui/PlayerBarKt*",
+ "**/ui/PlayerMessageKt*",
+ "**/ui/QueueScreenKt*",
+ "**/ui/RuleConditionFieldsKt*",
+ "**/ui/RuleEditScreenKt*",
+ "**/ui/RulesScreenKt*",
+ "**/ui/SettingRowsKt*",
+ "**/ui/SettingsScreenKt*",
+ "**/ui/ShowSettingsSheetKt*",
+ "**/ui/SkipServerSettingsStateKt*",
+ "**/ui/SkipSettingsKt*",
+ "**/ui/SubscriptionRowKt*",
+ "**/ui/SubscriptionScreenKt*",
+ "**/ui/TierSettingKt*",
+ "**/ui/TranscriptScreenKt*",
+
+ // A screen's own parameter and state objects: data classes of callbacks and already-decided
+ // values, handed from one composable to another. They decide nothing, they are constructed
+ // only inside the files above, and the only test that could reach one would call its
+ // constructor and assert nothing.
+ //
+ // Excluded by what they are rather than by which file they sit in. Most are declared inside
+ // their screen and were already covered by the globs above; these five happen to have files
+ // of their own, and a line that excluded one and measured the other would be describing the
+ // author's filing rather than the code.
+ "**/ui/EpisodeActions*",
+ "**/ui/EpisodeDetailActions*",
+ "**/ui/EpisodeDetailState*",
+ "**/ui/EpisodeHooks*",
+ "**/ui/EpisodePlayback*",
+ "**/ui/FurnitureSkipSettings*",
+ "**/ui/LibraryContent*",
+ "**/ui/LibraryNav*",
+ "**/ui/LibraryTopBarActions*",
+ "**/ui/NowPlayingActions*",
+ "**/ui/OverflowContent*",
+ "**/ui/QueueNav*",
+ "**/ui/QueueRowActions*",
+ "**/ui/ScreenSource*",
+ "**/ui/SkipServerSettings*",
+ "**/ui/SkipSettings*",
+ "**/ui/SubscriptionNav*",
+ "**/ui/SubscriptionTopBarActions*",
+
"**/R.class", "**/R\$*.class", "**/BuildConfig.*", "**/*_Impl*",
// ExoPlayer, MediaSession and the browse tree: no JVM equivalent to run against.