Fix 25 latent test failures surfaced by enabling JUnit Platform on KMP jvmTest - #147
Fix 25 latent test failures surfaced by enabling JUnit Platform on KMP jvmTest#147alexander-yevsyukov wants to merge 8 commits into
jvmTest#147Conversation
…P jvmTest
The kmp-module convention never enabled the JUnit Platform on the KMP
jvmTest task, so :logging:jvmTest silently discovered zero tests while
behavior drifted from the Flogger contract during the Kotlin migration.
Enable the platform (mirroring module-testing, without an engine filter
since Kotest provides its own engine) and fix the failures it surfaced.
Production fixes:
- MetadataKey.cast() throws ClassCastException on type mismatch again
instead of silently returning null.
- checkCannotRepeat() throws IllegalArgumentException per the documented
contract of findValue()/getSingleValue(), with a corrected message.
- MetadataHandler.Builder.addRepeatedHandler() requires a repeatable key
(the check was inverted).
- SimpleProcessor guards repeated values with an unmodifiable iterator
(the unmodifiable-list wrapping was lost in the Java-to-Kotlin port).
- LogContext evaluates the lazy message lambda inside
AbstractLogger.write(), so a throwing or reentrant toString() is
handled by the recursion guard and robust error handling instead of
propagating (or overflowing the stack). A null message now reaches
the backend unmodified.
- AbstractLogger.atConfig() maps to Level.CONFIG, not Level.INFO.
- Logging-error timestamps include milliseconds and the UTC offset.
- LogPerBucketingStrategy.byClass()/byClassName() use the Java class,
restoring identity semantics and fixing an NPE for local and
anonymous classes.
- ScopedLoggingContext.Builder.run(Runnable) is restored; without it,
`.run { }` silently bound to the Kotlin stdlib extension and never
installed the context.
Test fixes: eager invocation counters outside lazy log lambdas, the
spread operator for vararg arrays, Kotlin qualified names in
LogLevelMapSpec, and message-format alignments.
The module-local jvmTest workaround in otel-backend is superseded by
the convention change and removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Do not invoke a user-supplied toString() when composing the ClassCastException message in MetadataKey.cast(); report the value class instead. - Document the IllegalArgumentException thrown by addRepeatedHandler(). - Document the null pass-through of MetadataKey.cast(). - Clarify comments around the deferred message evaluation in LogContext. - Restore the Flogger attribution in the MetadataKey.kt header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR restores effective JVM test execution for the logging KMP module by enabling JUnit Platform for jvmTest, then fixes the production/test regressions that were previously masked by “0 tests discovered”. It also realigns several logging behaviors with the Flogger contract and updates documentation/version artifacts accordingly.
Changes:
- Enable JUnit Platform on KMP
jvmTest(and remove the module-localotel-backendworkaround). - Fix Flogger-contract drift in core logging (lazy message evaluation inside
write(), correctatConfig()level, repeated-metadata immutability, stricterMetadataKey.cast(), safer bucketing strategy keys, restoredScopedLoggingContext.Builder.run(Runnable)). - Update tests and generated docs/version references to match the corrected behavior.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| version.gradle.kts | Bumps versionToPublish to 2.0.0-SNAPSHOT.422 using the delegated extra property shape. |
| logging/src/jvmTest/kotlin/io/spine/logging/test/LoggingCompatibilityTest.kt | Fixes Kotlin vararg array passing via spread operator in compatibility test. |
| logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt | Updates expectations for null literal message handling and formatting behavior. |
| logging/src/jvmTest/kotlin/io/spine/logging/JvmLoggerSpec.kt | Aligns log-site/method-name expectation and moves counters outside lazy message lambdas. |
| logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt | Updates expectation to match backtick diagnostic style for toString() returning null. |
| logging/src/commonTest/kotlin/io/spine/logging/context/LogLevelMapSpec.kt | Adjusts mapped logger-name expectations to Kotlin qualified names (e.g., kotlin.String). |
| logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt | Makes recursion error assertion case-insensitive. |
| logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt | Restores Flogger-like cast() behavior (throw on mismatch) and corrects checkCannotRepeat() exception type/message. |
| logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt | Uses JVM class identity/name for bucketing to avoid KClass identity/NPE issues. |
| logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt | Defers lazy message evaluation into AbstractLogger.write() recursion guard and preserves null literal behavior to backend. |
| logging/src/commonMain/kotlin/io/spine/logging/context/ScopedLoggingContext.kt | Restores Builder.run(Runnable) to avoid stdlib run shadowing and ensure context installation. |
| logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt | Ensures repeated metadata iteration is non-modifiable via an iterator wrapper that throws on remove(). |
| logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt | Fixes repeated-handler registration to require key.canRepeat() (and documents the thrown IllegalArgumentException). |
| logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt | Fixes atConfig() level, moves message evaluation into write(data, prepare), and improves logging-error timestamp formatting (millis + UTC offset). |
| docs/dependencies/pom.xml | Updates docs dependency POM version to 2.0.0-SNAPSHOT.422. |
| docs/dependencies/dependencies.md | Updates generated dependency report version headers/timestamps to match the bumped snapshot. |
| buildSrc/src/main/kotlin/kmp-module.gradle.kts | Configures KMP jvmTest to use JUnit Platform and test logging (config-distributed file). |
| backends/otel-backend/build.gradle.kts | Removes now-superseded local jvmTest JUnit Platform/test logging workaround. |
| .agents/tasks/logging-jvmtest-triage.md | Adds a triage note capturing failure analysis and rationale (config-managed path). |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## claude/angry-hugle-d62cfe #147 +/- ##
===============================================================
+ Coverage 67.80% 67.87% +0.06%
Complexity 464 464
===============================================================
Files 109 109
Lines 2578 2574 -4
Branches 403 402 -1
===============================================================
- Hits 1748 1747 -1
+ Misses 696 694 -2
+ Partials 134 133 -1 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 210c1e869d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On the JVM, `LoggingFactory.loggerFor()` names loggers by the Java class name (`Platform.getBackend(cls.java.name)`), while `LogLevelMap.Builder` registered classes under their Kotlin qualified names. For classes whose Kotlin and Java names differ (mapped types such as `String`, and nested classes), a map entry created via `add(level, SomeClass::class)` could never affect the logger created for that very class. `KClass.toLoggerName()` now returns the Java class name, and the builder uses it, so map entries always match the loggers they target. This also removes the `IllegalArgumentException` for local and anonymous classes, which have no Kotlin qualified name but always have a Java name. Reported by Codex review on PR #147. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both branches fixed the same 25 latent jvmTest failures independently. The merge deduplicates them, generally preferring the base branch and keeping from this branch: - The `LogLevelMap`/`toLoggerName` alignment with JVM logger names (Codex review finding on PR #147). - The canonical `val versionToPublish: String by extra(...)` shape, advanced to `2.0.0-SNAPSHOT.423` above the base version. - The `The Flogger Authors; ` copyright attribution in ported files. New tests from the base branch (level-mapping coverage, repeated-handler precondition) are retained: 234 tests, all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…claude/inspiring-morse-a006d4
|
Closing as incorrect. We want to use fully qualified Kotlin class names in |
What changed
The
kmp-moduleconvention never enabled the JUnit Platform on the KMPjvmTesttask, so:logging:jvmTesthas been silently discovering zero tests while behavior drifted from the Flogger contract during the Java-to-Kotlin migration. This PR enables the platform in the convention (mirroringmodule-testing, without an engine filter sincekmp-moduleadds the Kotest runner — a JUnit Platform engine of its own), removes the now-superseded module-local workaround inotel-backend, and fixes the 25 latent failures this surfaced::logging:jvmTestnow runs 232/232 green.Production fixes (Flogger-contract drift)
MetadataKey.cast()throwsClassCastExceptionon a type mismatch again instead of silently returningnull(the message reports the value class, never invoking a usertoString()).checkCannotRepeat()throwsIllegalArgumentExceptionper the documented contract offindValue()/getSingleValue(); its message was also inverted.MetadataHandler.Builder.addRepeatedHandler()required a key that cannot repeat — the exact opposite of Flogger'scheckArgument(key.canRepeat()).SimpleProcessorlostCollections.unmodifiableListin translation (e.setValue(e.value as List<*>)was a no-op cast); repeated values now dispatch through an iterator whoseremove()throws.LogContextevaluated the lazy message lambda beforeAbstractLogger.write(), outside the recursion guard and robust error handling — a throwingtoString()propagated to callers and reentrant logging hitStackOverflowError. Evaluation now happens insidewrite(data, prepare)(@JvmOverloadspreserves the old JVM signature). Anullmessage also reaches the backend asnullinstead of the string"null".AbstractLogger.atConfig()mapped toLevel.INFO(copy-paste); nowLevel.CONFIG.yyyy-MM-dd'T'HH:mm:ss.SSSZequivalent).LogPerBucketingStrategy.byClass()/byClassName()usedkey::class/qualifiedName!!— no identity guarantee and an NPE for local/anonymous classes; nowkey.javaClass/.name.ScopedLoggingContext.Builder.run(Runnable)was missing, so.run { }silently bound to Kotlin's stdlibrunand never installed the context; the member is restored.Test fixes (stale or Java-specific expectations)
Invocation counters moved out of lazy log lambdas (they only run for statements that actually log), the
*spread operator for vararg arrays, Kotlin qualified names inLogLevelMapSpec, and message-format alignments. Full per-failure triage rationale:.agents/tasks/logging-jvmtest-triage.md.Reviewer notes
buildSrc/src/main/kotlin/kmp-module.gradle.ktsis config-distributed — the samejvmTestwiring must land inSpineEventEngine/config, or the next./config/pullreverts it andjvmTestsilently drops back to zero tests (a follow-up task is queued for this).val versionToPublish: String by extra("…")shape; the previous bump commit introducedextra.set(…), which theversion-bumpedguard script cannot parse.AbstractLogger.write()catchesRuntimeExceptionaround the message evaluation, which would also contain aCancellationExceptionraised from user code reachable viatoString(). This matches Flogger's "log statements never throw" contract and the lambdas are non-suspending, so it is left as-is — flagging for awareness.The Flogger Authors;attribution (also restored inMetadataKey.kt, where it had been lost on master).Pre-PR checklist:
./gradlew build dokkaGenerategreen (all modules, detekt, Kover); reviewerskotlin-engineer,spine-code-review,review-docsall APPROVE WITH CHANGES, applied or tracked as above.🤖 Generated with Claude Code