Skip to content

test: 100% Kotlin test coverage of the Android layer; fix regressions from #388 - #477

Merged
tony19 merged 4 commits into
mainfrom
ccr-909c13ae-b56q1t
Sep 29, 2026
Merged

tony19 merged 4 commits into
mainfrom
ccr-909c13ae-b56q1t

Conversation

@tony19

@tony19 tony19 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds Kotlin unit tests that cover every line and branch of the Android-specific layer (ch.qos.logback.{classic,core}.android), and a CI gate that keeps coverage there.

The tests were first written against the Java code that #388 replaced, then run against the Kotlin conversion to check it. They found regressions, which this PR also fixes.

Tests (test:)

  • Ported to Kotlin: LogcatAppenderTest, SQLiteAppenderTest and AndroidContextUtilTest, keeping every original case.
  • New: BasicLogcatConfiguratorTest, SystemClockTest and SystemPropertiesProxyTest.
  • API check: JavaApiSignaturesTest pins the JVM-level API that Java callers and Joran rely on, so a binary-incompatible change fails a test.
  • What the tests assert: observable behavior, such as logcat output, the rows written to SQLite, status messages, properties and exceptions. They don't just execute lines; review passes checked that mutated code fails them.
  • Coverage: 178 tests. In a local Robolectric run with JaCoCo 0.8.15, coverage of the layer is 100%: 1810/1810 instructions, 246/246 branches, 344/344 lines.

Regressions from #388 fixed (fix:)

  • Return type: SystemPropertiesProxy.getBoolean returned a primitive boolean instead of java.lang.Boolean, which is binary-incompatible for Java callers (NoSuchMethodError).
  • Null keys: SystemPropertiesProxy.get and getBoolean threw on a null key instead of returning the default.
  • Final members: the public members of AndroidContextUtil, LogcatAppender and SQLiteAppender had become final, which breaks subclasses that override them. They are open again.
  • Dropped events: LogcatAppender dropped events without a logger name; they are logged with a null tag again.
  • Pre-Lollipop check: AndroidContextUtil.getNoBackupFilesDirectoryPath lost its SDK check; it's restored.
  • Unreachable null checks: removed so that every branch can be tested.

Kept as #388 changed them, since they're harmless:

  • SQLiteAppender.finalize() is removed;
  • blank filenames use isNullOrBlank;
  • AndroidContextUtil.getContext() is public;
  • BasicLogcatConfigurator is an object;
  • SystemPropertiesProxy is final (its constructor is private);
  • LogcatAppender falls back to the logger name when a tag layout produces no tag.

Build (build:)

  • Coverage recording: JaCoCo 0.8.15 unit-test coverage via AGP's enableUnitTestCoverage, with includeNoLocationClasses so classes loaded by Robolectric are recorded.
  • Gate: verifyAndroidLayerCoverage fails unless every class in the layer has 100% line and branch coverage.
    • CI runs it after the unit tests.
    • CI uploads the jdk11Debug report as the coverage-report artifact.
  • Test dependencies: kotlin-test and mockito-kotlin 6.4.0.
  • Kover was tried first and dropped: with AGP 9's variants it never attached the unit tests' coverage data to its reports, so it always reported 0%.

Architecture: no packages, runtime dependencies or published outputs change; docs/architecture/ is untouched.

Deferred work considered: the architecture-docs check failed on main after #388, which made Static Analysis fail here too. #480 fixed it on main, and this branch is rebased onto that fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AN8SJNGETCegfBbPy4iV5K

@tony19 tony19 added the run-ci-on-drafts label Sep 29, 2026 — with Claude
@tony19
tony19 force-pushed the ccr-909c13ae-b56q1t branch from 394d4af to 2d293ad Compare September 29, 2026 05:36
@tony19 tony19 changed the title test: Kotlin tests with 100% coverage of the Android layer, to validate #388 test: Kotlin tests with 100% coverage of the Android layer Sep 29, 2026

tony19 commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Static Analysis (Checkstyle + PMD) fails at the ./scripts/check-architecture-docs.sh step, and the failure does not come from this PR. main has failed this check since #388 merged; see the Static Analysis runs for ffe4cf3 (#388) and e6aa6dd (#479).

The failing step reports:

docs/architecture/components.md: entries for packages that no longer exist under logback-android/src/main/java:
  ch/qos/logback/classic/android
  ch/qos/logback/core/android

Cause: the script, added in #478, only looks for packages under src/main/java, and #388 moved those two packages to src/main/kotlin. The catalogue entries themselves are still correct. The script's two dependency rules have the same gap: core must not import classic, and only allowed packages may import android.*. Kotlin sources are never checked against either rule.

No fix is open yet. This patch makes the script scan both source roots. I checked it locally against current main, where it prints "52 packages catalogued, dependency rules hold". I also added a temporary Kotlin file with a core→classic import and an android.* import; the patched script flags both.

--- a/scripts/check-architecture-docs.sh
+++ b/scripts/check-architecture-docs.sh
@@ -18,7 +18,9 @@ set -euo pipefail
 cd "$(dirname "$0")/.."
 
-SRC=logback-android/src/main/java
+# Library sources: the upstream-derived port is Java, the Android-specific
+# layer is Kotlin (#388). Package paths are relative to either root.
+SRC_ROOTS="logback-android/src/main/java logback-android/src/main/kotlin"
 CATALOGUE=docs/architecture/components.md
@@ -30,7 +32,15 @@
-packages=$(find "$SRC" -name '*.java' -exec dirname {} \; | sed "s|^$SRC/||" | sort -u)
+# Every Java/Kotlin source file, as a path relative to its source root.
+sources() {
+  for root in $SRC_ROOTS; do
+    [ -d "$root" ] || continue
+    find "$root" \( -name '*.java' -o -name '*.kt' \) | sed "s|^$root/||"
+  done
+}
+
+packages=$(sources | xargs -n1 dirname | sort -u)
@@ -43,19 +53,24 @@
-  echo "$CATALOGUE: entries for packages that no longer exist under $SRC:"
+  echo "$CATALOGUE: entries for packages that no longer exist under $SRC_ROOTS:"
@@
-core_to_classic=$(grep -rl 'import ch\.qos\.logback\.classic' "$SRC/ch/qos/logback/core" || true)
+core_to_classic=$(for root in $SRC_ROOTS; do
+  [ -d "$root/ch/qos/logback/core" ] && grep -rl 'import ch\.qos\.logback\.classic' "$root/ch/qos/logback/core"
+done || true)
@@
-android_users=$({ grep -rlE '^import (static )?android\.' "$SRC" || true; } | xargs -r -n1 dirname | sed "s|^$SRC/||" | sort -u)
+android_users=$(for root in $SRC_ROOTS; do
+  [ -d "$root" ] || continue
+  { grep -rlE '^import (static )?android\.' "$root" || true; } | xargs -r -n1 dirname | sed "s|^$root/||"
+done | sort -u)

The failure is deterministic, so re-running the job won't help. I've left the fix out of this PR to keep it scoped to tests and the coverage gate. It can go in as its own fix: PR, or be folded in here if you prefer.


Generated by Claude Code

@tony19
tony19 force-pushed the ccr-909c13ae-b56q1t branch from 95f082b to 7c264e2 Compare September 29, 2026 05:46
@tony19 tony19 changed the title test: Kotlin tests with 100% coverage of the Android layer test: 100% Kotlin test coverage of the Android layer; fix regressions from #388 Sep 29, 2026
Adds what the Kotlin unit tests of the Android-specific layer need, and a
coverage gate so those tests keep pinning every path of that layer:

- JaCoCo 0.8.15 coverage of the debug unit tests (AGP's
  enableUnitTestCoverage), recording classes that Robolectric's sandbox
  classloader defines (includeNoLocationClasses).
- androidLayerCoverageVerification<Variant> / verifyAndroidLayerCoverage:
  fail unless the unit tests cover every line and branch of each class in
  ch.qos.logback.{classic,core}.android; run in CI after the unit tests,
  with the jdk11Debug HTML/XML report uploaded.
- kotlin-test and mockito-kotlin 6.4.0 for the tests, which the existing
  Kotlin plugin setup compiles from src/test/kotlin.

Kover was tried first, but with AGP 9's variants it never attached the unit
test tasks' coverage data to its reports (always 0%), although JaCoCo
recorded it correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AN8SJNGETCegfBbPy4iV5K
…nged

The Kotlin tests of the Android-specific layer, written against the Java
code #388 replaced, found these regressions in the conversion:

- SystemPropertiesProxy.getBoolean returned a primitive boolean instead of
  java.lang.Boolean, which breaks Java callers compiled against the old
  signature (NoSuchMethodError). It again returns Boolean? and passes the
  SystemProperties result through.
- SystemPropertiesProxy.get/getBoolean threw on a null key (Kotlin's
  parameter check) instead of returning the default; they accept null again.
  They also invoke SystemProperties with its class as the receiver again, so
  an IllegalArgumentException from the reflective call propagates as before.
- The public members of AndroidContextUtil, LogcatAppender and
  SQLiteAppender had become final, breaking subclasses that override them
  (the classes are open, and these were overridable in the published Java
  API). They are open again; AndroidContextUtil.setupProperties once more
  tolerates null paths from such overrides.
- LogcatAppender dropped (with an error status) events without a logger
  name; getTag returns String? again, so they're logged with a null tag.
- AndroidContextUtil.getNoBackupFilesDirectoryPath lost its pre-Lollipop
  check.

Also removes null checks that could never fail (the database handle once
started, the history duration passed to the log cleaner), so that every
branch of the layer is reachable by its tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AN8SJNGETCegfBbPy4iV5K
Ports LogcatAppenderTest, SQLiteAppenderTest and AndroidContextUtilTest to
Kotlin (keeping every case) and adds tests until every line and branch of
ch.qos.logback.{classic,core}.android is covered, asserting behavior rather
than just executing it:

- LogcatAppenderTest: level-to-priority mapping, isLoggable() checks and tag
  truncation, start() validation, %nopex handling of tag layouts, and XML
  configuration through Joran.
- BasicLogcatConfiguratorTest, SystemClockTest.
- SQLiteAppenderTest: the rows actually written (arguments, caller data,
  properties, exceptions with causes), log cleanup timing, and every failure
  path (database open/create/insert failures, rolled-back transactions).
- AndroidContextUtilTest: every path/property getter across SDK levels,
  storage states, package info, and the application-context lookup.
- SystemPropertiesProxyTest (new).
- JavaApiSignaturesTest: pins the JVM-level API that Java callers and Joran
  rely on, so a binary-incompatible change fails a test.

Uses kotlin-test assertions, mockito-kotlin and Robolectric.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AN8SJNGETCegfBbPy4iV5K
AGP compiles unit tests against android.jar's java.lang.ClassLoader, which
has no getClassLoadingLock(); lock on the loader itself instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AN8SJNGETCegfBbPy4iV5K
@tony19
tony19 force-pushed the ccr-909c13ae-b56q1t branch from 0fadec9 to f242957 Compare September 29, 2026 06:26

tony19 commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

The architecture-docs failure is fixed on main: #480 made check-architecture-docs.sh scan src/main/kotlin as well, so the patch from my earlier comment isn't needed. I've rebased this branch onto current main, where the check passes: "52 packages catalogued, dependency rules hold".


Generated by Claude Code

@tony19
tony19 marked this pull request as ready for review September 29, 2026 06:33
@tony19 tony19 removed the run-ci-on-drafts label Sep 29, 2026 — with Claude
@tony19
tony19 merged commit 1c46ff2 into main Sep 29, 2026
13 checks passed
@tony19
tony19 deleted the ccr-909c13ae-b56q1t branch September 29, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants