build(jacoco): scope coverage report to the JVM-testable surface #292

Closed
JMR-dev wants to merge 9 commits from build-290-jacoco-scope into main
JMR-dev commented 2026-07-04 06:08:18 +00:00 (Migrated from github.com)

Closes #290

Result

Line coverage: 50% → 78% (4,519 of 5,787 lines). Verified via a clean :app:jacocoTestReport run (deleted app/build/reports/jacoco first, reran from scratch) — see the "78% vs. the ~85-90% ballpark" note below for why this lands under the issue's estimate, on purpose.

ViewModels, repos, mappers, DAOs, utils, policies, richtext, mail, and reporting-logic are all still counted — spot-checked the per-package HTML (AccountSettingsViewModel, SettingsViewModel, ReaderViewModel, MailboxViewModel, AppLockViewModel, MailRepositoryImpl, AccountRepositoryImpl, ReportStore, ReportSubmitter, DiagnosticsCollector, etc. all present) and confirmed org.libremail.di / org.libremail.data.local.coldopen no longer appear as packages in the report at all.

Exclusion globs added

val nonJvmTestableSurface = listOf(
    // --- Compose UI render code: one glob per screen/component file (see the exceptions below) ---
    "**/LibreMailApp*", "**/AccountPickerScreen*", "**/AppPasswordSetupScreen*",
    "**/ManualSetupScreen*", "**/ComposeScreen*", "**/ColorSwatch*", "**/FontPicker*",
    "**/FontSizePicker*", "**/ParagraphAlignmentControl*", "**/DraftsScreen*", "**/LockScreen*",
    "**/AppLockGateHost*", "**/FolderDrawer*", "**/MailboxScreen*", "**/AddAnotherAccountScreen*",
    "**/BatteryOptimizationScreen*", "**/ContactsAccessScreen*", "**/LicenseScreen*",
    "**/OnboardingWelcomeScreen*", "**/OutboxScreen*", "**/ReaderScreen*",
    "**/ProblemReportsScreen*", "**/AccountSettingsScreen*", "**/SettingsScreen*",
    "**/SettingsComponents*", "**/SignatureEditScreen*", "**/SignaturesScreen*",
    // --- Android framework entry points ---
    "**/*Activity*", "**/*Service*", "**/*Worker*", "**/LibreMailApplication*", "**/*BackupAgent*",
    // --- Hilt DI wiring ---
    "**/di/**",
    // --- src/debug cold-open probe (issue #221) ---
    "**/data/local/coldopen/**",
)

Two things worth your review

1. Four Screen/component files were deliberately left OUT of the exclusion list, even though they live in packages the issue calls out (ui/compose, ui/settings, ui/reader, ui/reporting):

  • ui/compose/RichTextEditor.kt — RichTextEditorTest (17 tests) covers the AnnotatedString<->RichTextContent editor-op functions (applyStyle/applyBlock/applyLink/toRichContent/toAnnotatedString/...).
  • ui/settings/AccountReorderList.kt — AccountReorderListTest covers commitDrag's reorder maths ("Pure so the index maths is unit-testable without a gesture" per its own kdoc).
  • ui/reader/HtmlBody.kt — HtmlBodyTest + InlineImageResolverTest cover cidKey/resolveInlineImage/wrapHtml/toCssHex.
  • ui/reporting/ReportReviewScreen.kt — ReportReviewClipboardTest covers copyReportPayloadToClipboard ("Kept as a plain suspend function... unit-testable... without a Compose UI test or emulator" per its own kdoc).

JaCoCo excludes at class-file granularity, and Kotlin compiles every top-level function in a .kt file — @Composable or not — into the same facade class. Excluding these four files would have zeroed out those five dedicated test files' coverage along with the render code. I verified this concretely: temporarily adding these four to the exclusion list brought line coverage to 87% (matching the issue's ~85-90% estimate almost exactly), which is strong evidence the original audit's estimate assumed a blanket **/ui/**-style exclusion that didn't catch this mixed-file wrinkle. I kept them in scope and am reporting the more conservative 78% instead — happy to go the other way if you'd rather hit the estimate and accept losing that coverage signal (or if you'd prefer a third option: split each file so the pure functions live outside the render file, letting both goals be satisfied at once — that's more invasive than this ticket's scope, so I didn't do it here).

2. **/*Worker* removes coverage that is currently real, not just glue. All six Workers it excludes — SyncWorker, BackfillWorker, PruneWorker, SendWorker, ReportPurgeWorker, ReportUploadWorker — are directly unit-tested today (construct the worker with mocked collaborators, call doWork(), assert the Result): SyncWorkerTest, BackfillWorkerTest, PruneWorkerTest, SendWorkerTest (272 lines testing a 182-line class), ReportPurgeWorkerTest, ReportUploadWorkerTest/ReportUploadWorkerHttpTest. I implemented the exclusion exactly as specified in the issue (framework entry points, by category, regardless of current test investment), but flagging it since it's a real, measurable tradeoff rather than a clear-cut "untestable glue" case like MainActivity/IdleService/LibreMailBackupAgent (all zero JVM tests, no ambiguity there).

Validation

  • :app:jacocoTestReport — clean, HTML/XML regenerated from scratch.
  • :app:ktlintCheck :app:detekt — green.
  • Build-script-only change (no source/test files touched), so per the ticket's own "no emulator" validation scope I did not run the full preflight/emulator gate.

🤖 Generated with Claude Code

Closes #290 ## Result Line coverage: **50% → 78%** (4,519 of 5,787 lines). Verified via a clean `:app:jacocoTestReport` run (deleted `app/build/reports/jacoco` first, reran from scratch) — see the "78% vs. the ~85-90% ballpark" note below for why this lands under the issue's estimate, on purpose. ViewModels, repos, mappers, DAOs, utils, policies, richtext, mail, and reporting-logic are all still counted — spot-checked the per-package HTML (`AccountSettingsViewModel`, `SettingsViewModel`, `ReaderViewModel`, `MailboxViewModel`, `AppLockViewModel`, `MailRepositoryImpl`, `AccountRepositoryImpl`, `ReportStore`, `ReportSubmitter`, `DiagnosticsCollector`, etc. all present) and confirmed `org.libremail.di` / `org.libremail.data.local.coldopen` no longer appear as packages in the report at all. ## Exclusion globs added ```kotlin val nonJvmTestableSurface = listOf( // --- Compose UI render code: one glob per screen/component file (see the exceptions below) --- "**/LibreMailApp*", "**/AccountPickerScreen*", "**/AppPasswordSetupScreen*", "**/ManualSetupScreen*", "**/ComposeScreen*", "**/ColorSwatch*", "**/FontPicker*", "**/FontSizePicker*", "**/ParagraphAlignmentControl*", "**/DraftsScreen*", "**/LockScreen*", "**/AppLockGateHost*", "**/FolderDrawer*", "**/MailboxScreen*", "**/AddAnotherAccountScreen*", "**/BatteryOptimizationScreen*", "**/ContactsAccessScreen*", "**/LicenseScreen*", "**/OnboardingWelcomeScreen*", "**/OutboxScreen*", "**/ReaderScreen*", "**/ProblemReportsScreen*", "**/AccountSettingsScreen*", "**/SettingsScreen*", "**/SettingsComponents*", "**/SignatureEditScreen*", "**/SignaturesScreen*", // --- Android framework entry points --- "**/*Activity*", "**/*Service*", "**/*Worker*", "**/LibreMailApplication*", "**/*BackupAgent*", // --- Hilt DI wiring --- "**/di/**", // --- src/debug cold-open probe (issue #221) --- "**/data/local/coldopen/**", ) ``` ## Two things worth your review **1. Four Screen/component files were deliberately left OUT of the exclusion list**, even though they live in packages the issue calls out (`ui/compose`, `ui/settings`, `ui/reader`, `ui/reporting`): - `ui/compose/RichTextEditor.kt` — `RichTextEditorTest` (17 tests) covers the `AnnotatedString`<->`RichTextContent` editor-op functions (`applyStyle`/`applyBlock`/`applyLink`/`toRichContent`/`toAnnotatedString`/...). - `ui/settings/AccountReorderList.kt` — `AccountReorderListTest` covers `commitDrag`'s reorder maths ("Pure so the index maths is unit-testable without a gesture" per its own kdoc). - `ui/reader/HtmlBody.kt` — `HtmlBodyTest` + `InlineImageResolverTest` cover `cidKey`/`resolveInlineImage`/`wrapHtml`/`toCssHex`. - `ui/reporting/ReportReviewScreen.kt` — `ReportReviewClipboardTest` covers `copyReportPayloadToClipboard` ("Kept as a plain suspend function... unit-testable... without a Compose UI test or emulator" per its own kdoc). JaCoCo excludes at class-file granularity, and Kotlin compiles every top-level function in a `.kt` file — `@Composable` or not — into the same facade class. Excluding these four files would have zeroed out those five dedicated test files' coverage along with the render code. I verified this concretely: temporarily adding these four to the exclusion list brought line coverage to **87%** (matching the issue's ~85-90% estimate almost exactly), which is strong evidence the original audit's estimate assumed a blanket `**/ui/**`-style exclusion that didn't catch this mixed-file wrinkle. I kept them in scope and am reporting the more conservative 78% instead — happy to go the other way if you'd rather hit the estimate and accept losing that coverage signal (or if you'd prefer a third option: split each file so the pure functions live outside the render file, letting both goals be satisfied at once — that's more invasive than this ticket's scope, so I didn't do it here). **2. `**/*Worker*` removes coverage that is currently real, not just glue.** All six Workers it excludes — `SyncWorker`, `BackfillWorker`, `PruneWorker`, `SendWorker`, `ReportPurgeWorker`, `ReportUploadWorker` — are directly unit-tested today (construct the worker with mocked collaborators, call `doWork()`, assert the `Result`): `SyncWorkerTest`, `BackfillWorkerTest`, `PruneWorkerTest`, `SendWorkerTest` (272 lines testing a 182-line class), `ReportPurgeWorkerTest`, `ReportUploadWorkerTest`/`ReportUploadWorkerHttpTest`. I implemented the exclusion exactly as specified in the issue (framework entry points, by category, regardless of current test investment), but flagging it since it's a real, measurable tradeoff rather than a clear-cut "untestable glue" case like `MainActivity`/`IdleService`/`LibreMailBackupAgent` (all zero JVM tests, no ambiguity there). ## Validation - `:app:jacocoTestReport` — clean, HTML/XML regenerated from scratch. - `:app:ktlintCheck :app:detekt` — green. - Build-script-only change (no source/test files touched), so per the ticket's own "no emulator" validation scope I did not run the full preflight/emulator gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JMR-dev commented 2026-07-04 16:29:51 +00:00 (Migrated from github.com)

Superseded by #323, which incorporates this scoping, keeps the unit-tested Workers in-scope (fixing this PR's over-broad Worker exclusion), and adds the no-regression CI gate (#251).

Superseded by #323, which incorporates this scoping, keeps the unit-tested Workers in-scope (fixing this PR's over-broad Worker exclusion), and adds the no-regression CI gate (#251).

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.