From 5fd518f23979875cae3f9d2f75fdb161ef4cebce Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 00:35:02 +0100 Subject: [PATCH 01/14] Enable JUnit Platform for the KMP `jvmTest` task The `kmp-module` convention never configured a test framework for the JVM target, so `jvmTest` silently discovered zero JUnit 5/Kotest tests in every KMP module. Mirror `module-testing.setupTests()` in the convention (without the `junit-jupiter` engine filter, since `kmp-module` adds the Kotest runner engine to `jvmTest` dependencies) and drop the module-local workaround from `otel-backend`. The `buildSrc` change anticipates the same fix committed to the `config` repository; `./config/pull` will keep the copies in sync once it lands there. Co-Authored-By: Claude Fable 5 --- .agents/tasks/kmp-jvmtest-junit-platform.md | 56 +++++++++++++++++++ backends/otel-backend/build.gradle.kts | 9 --- .../src/main/kotlin/kmp-module.gradle.kts | 12 ++++ 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 .agents/tasks/kmp-jvmtest-junit-platform.md diff --git a/.agents/tasks/kmp-jvmtest-junit-platform.md b/.agents/tasks/kmp-jvmtest-junit-platform.md new file mode 100644 index 00000000..fa137e6f --- /dev/null +++ b/.agents/tasks/kmp-jvmtest-junit-platform.md @@ -0,0 +1,56 @@ +# Enable JUnit Platform for KMP `jvmTest` in the `kmp-module` convention + +## Problem + +`kmp-module.gradle.kts` never applies `module-testing` (it cannot: that plugin +applies `java-library`, which conflicts with `kotlin("multiplatform")`), so the +KMP `jvmTest` task runs without the JUnit Platform and silently discovers zero +JUnit 5/Kotest tests. Verified: `./gradlew :logging:jvmTest --rerun` passed in +~1 s with no test reports while 13 `*Spec.kt` files exist under +`logging/src/jvmTest/`. `backends/otel-backend/build.gradle.kts` carries a +module-local workaround. + +Recorded as the "Collateral finding" in +`.agents/tasks/otel-backend-validation.md` (main checkout). + +## Plan + +1. In the sibling `config` checkout, extend `kmp-module.gradle.kts`: + configure `tasks.named("jvmTest")` with `useJUnitPlatform()` and + `configureLogging()` — mirroring `module-testing.setupTests()` but without + `includeEngines("junit-jupiter")`, because `kmp-module` itself adds the + Kotest runner (engine `kotest`) to `jvmTest` dependencies. +2. Apply the identical change to this repo's `buildSrc` copy (anticipating the + config float; `./config/pull` will overwrite with the same content once the + config change lands). +3. Remove the module-local workaround from + `backends/otel-backend/build.gradle.kts` (the `tasks.named("jvmTest")` + block). Keep its `registerTestTasks()` call — the convention does not + register `fastTest`/`slowTest`. +4. Verify with JDK 17: `:logging:jvmTest --rerun` executes the 13 jvm specs + (plus commonTest specs and Java tests), and `:backends:otel-backend:jvmTest` + still executes its tests without the workaround. + +## Status + +- [x] Config repo edited — committed by the user as `512c1068` on the + `address-logging-audit-finding` branch of the `config` checkout +- [x] Local `buildSrc` copy updated +- [x] otel-backend workaround removed +- [x] `:logging:jvmTest` executes the 13 jvm specs — 232 tests ran + (previously zero); all 13 spec classes have result files +- [x] `:otel-backend:jvmTest` still executes its 22 tests (all pass); + `:logging-testlib:jvmTest` now runs 3 tests (pass); + `:tests:fixtures` has no test sources (NO-SOURCE) + +## Follow-up required before this can merge + +`:logging:jvmTest` surfaces **25 latent test failures** (232 run, +207 pass) — behavior drift accumulated while the task silently ran +nothing. At least 8 failures reference repeated-value `MetadataKey` +semantics changed by the `custom-metadata` PR (#144). Others: +`AbstractLoggerSpec` exception reporting, `LogLevelMap` builder levels, +`LogContextSpec` null-literal handling, varargs format extension. +Some may be production regressions, not test bugs — each needs +case-by-case triage. CI will be red on this branch (and in any repo +the floated config reaches) until they are fixed. diff --git a/backends/otel-backend/build.gradle.kts b/backends/otel-backend/build.gradle.kts index d9e25b9e..9955f30b 100644 --- a/backends/otel-backend/build.gradle.kts +++ b/backends/otel-backend/build.gradle.kts @@ -95,12 +95,3 @@ dependencies { // Registers the `fastTest`/`slowTest` tasks and the `*Spec`/`*Test` filter, // matching the core `logging` module. tasks.registerTestTasks() - -// The `kmp-module` convention does not put the JUnit Platform on the JVM test -// task (it configures only the `jvm-module` `test` task), so enable it here. -tasks.named("jvmTest") { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } -} diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts index 636a84d2..cb790b79 100644 --- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -38,6 +38,7 @@ import io.spine.gradle.javac.configureJavac import io.spine.gradle.kotlin.setFreeCompilerArgs import io.spine.gradle.publish.IncrementGuard import io.spine.gradle.report.license.LicenseReporter +import io.spine.gradle.testing.configureLogging /** * Configures this [Project] as a Kotlin Multiplatform module. @@ -162,11 +163,22 @@ java { * * Also, Kotlin and Java share the same test executor (JUnit), so tests * configuration is for both. + * + * The `jvmTest` task mirrors the setup made by `module-testing` for + * the `test` task of a `jvm-module` (`module-testing` itself cannot be + * applied here because it brings `java-library`, which conflicts with + * the Kotlin Multiplatform plugin). Unlike `module-testing`, no engine + * filter is imposed: `jvmTest` dependencies include the Kotest runner, + * which is a JUnit Platform engine of its own. */ tasks { withType().configureEach { configureJavac() } + named("jvmTest") { + useJUnitPlatform() + configureLogging() + } } /** From dafce36d2879efc14dbf9bb2caee1b2940257198 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 00:37:30 +0100 Subject: [PATCH 02/14] Bump version -> `2.0.0-SNAPSHOT.421` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 6e7a6c33..3254c545 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.420") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.421") From 4163c62beefb17851426f46636be175bee4d5b37 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 00:42:16 +0100 Subject: [PATCH 03/14] Update dependency reports --- docs/dependencies/dependencies.md | 72 +++++++++++++++---------------- docs/dependencies/pom.xml | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 10a70642..c7f6f2df 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -441,14 +441,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:09 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.421` ## Runtime ## Compile, tests, and tooling @@ -1240,14 +1240,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2094,14 +2094,14 @@ This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2932,14 +2932,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:46 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3786,14 +3786,14 @@ This report was generated on **Tue Jun 30 00:13:46 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:46 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4692,14 +4692,14 @@ This report was generated on **Tue Jun 30 00:13:46 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5590,14 +5590,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6508,14 +6508,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7414,14 +7414,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8324,14 +8324,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9206,14 +9206,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.421` ## Runtime ## Compile, tests, and tooling @@ -10013,14 +10013,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:logging-testlib:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine.tools:logging-testlib:2.0.0-SNAPSHOT.421` ## Runtime ## Compile, tests, and tooling @@ -10832,14 +10832,14 @@ This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:44 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-otel-backend:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-otel-backend:2.0.0-SNAPSHOT.421` ## Runtime ## Compile, tests, and tooling @@ -11739,14 +11739,14 @@ This report was generated on **Tue Jun 30 00:13:44 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-otel-backend-bootstrap:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-otel-backend-bootstrap:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.auto.service. **Name** : auto-service-annotations. **Version** : 1.1.1. @@ -13157,14 +13157,14 @@ This report was generated on **Tue Jun 30 00:13:45 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.auto.service. **Name** : auto-service-annotations. **Version** : 1.1.1. @@ -14015,14 +14015,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:10 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -14921,14 +14921,14 @@ This report was generated on **Tue Jun 30 00:13:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:44 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.420` +# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.421` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -15759,6 +15759,6 @@ This report was generated on **Tue Jun 30 00:13:44 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 00:13:44 WEST 2026** using +This report was generated on **Thu Jul 02 00:38:12 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 963ad82a..e02e4ed5 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine logging -2.0.0-SNAPSHOT.420 +2.0.0-SNAPSHOT.421 2015 From 4bce55f9be2efc1bfbd40703e8f567b248101c35 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 00:45:36 +0100 Subject: [PATCH 04/14] Remove unused import Co-Authored-By: Claude Fable 5 --- backends/otel-backend/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/backends/otel-backend/build.gradle.kts b/backends/otel-backend/build.gradle.kts index 9955f30b..ffcb1d27 100644 --- a/backends/otel-backend/build.gradle.kts +++ b/backends/otel-backend/build.gradle.kts @@ -30,7 +30,6 @@ import io.spine.dependency.lib.OpenTelemetryKotlin import io.spine.gradle.publish.SpinePublishing import io.spine.gradle.publish.spinePublishing import io.spine.gradle.testing.registerTestTasks -import org.gradle.api.tasks.testing.Test plugins { `kmp-module` From 4aab92a233af67b89f7f7eb214241bd120200714 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 12:55:17 +0100 Subject: [PATCH 05/14] Fix 25 latent test failures surfaced by enabling KMP `jvmTest` Enabling the JUnit Platform revived tests that had silently not run, exposing drift between them and production code. Production bugs fixed (the tests were right): `atConfig()` mapped to `Level.INFO`; `MetadataHandler.Builder.addRepeatedHandler` rejected repeatable keys instead of requiring them; `MetadataKey.cast()` returned `null` instead of the documented `ClassCastException`; repeated-value guards threw `IllegalStateException` where `IllegalArgumentException` is expected; `SimpleProcessor` exposed mutable repeated-value lists to handlers; `ScopedLoggingContext.Builder.run { }` resolved to the stdlib extension and skipped context installation; lazy messages were evaluated outside the recursion guard of `AbstractLogger.write`; log timestamps lacked milliseconds and UTC offset. Test expectations updated where production was right (logger names, synthetic lambda method names, eager rate-limiter counting, Kotlin spread operator for `logVarargs`). The fixes were authored by the dedicated triage session tracked in `.agents/tasks/kmp-jvmtest-junit-platform.md` and adopted here after review; `:logging:jvmTest` passes 232/232 and the full `build dokkaGenerate` is green. Co-Authored-By: Claude Fable 5 --- .agents/tasks/kmp-jvmtest-junit-platform.md | 37 +++++++++---- .../kotlin/io/spine/logging/AbstractLogger.kt | 52 ++++++++++++++++--- .../kotlin/io/spine/logging/LogContext.kt | 23 ++++---- .../spine/logging/LogPerBucketingStrategy.kt | 6 +-- .../kotlin/io/spine/logging/MetadataKey.kt | 18 ++++--- .../spine/logging/backend/MetadataHandler.kt | 5 +- .../logging/backend/MetadataProcessor.kt | 31 +++++++---- .../logging/context/ScopedLoggingContext.kt | 17 +++++- .../io/spine/logging/AbstractLoggerSpec.kt | 5 +- .../spine/logging/context/LogLevelMapSpec.kt | 8 ++- .../kotlin/io/spine/logging/JvmLoggerSpec.kt | 21 +++++--- .../kotlin/io/spine/logging/LogContextSpec.kt | 12 ++--- .../spine/logging/backend/AnyExtsJvmSpec.kt | 4 +- .../logging/test/LoggingCompatibilityTest.kt | 6 ++- 14 files changed, 173 insertions(+), 72 deletions(-) diff --git a/.agents/tasks/kmp-jvmtest-junit-platform.md b/.agents/tasks/kmp-jvmtest-junit-platform.md index fa137e6f..745bd09d 100644 --- a/.agents/tasks/kmp-jvmtest-junit-platform.md +++ b/.agents/tasks/kmp-jvmtest-junit-platform.md @@ -43,14 +43,33 @@ Recorded as the "Collateral finding" in `:logging-testlib:jvmTest` now runs 3 tests (pass); `:tests:fixtures` has no test sources (NO-SOURCE) -## Follow-up required before this can merge +## Latent-failure triage (resolved) -`:logging:jvmTest` surfaces **25 latent test failures** (232 run, +Enabling the platform surfaced **25 latent test failures** (232 run, 207 pass) — behavior drift accumulated while the task silently ran -nothing. At least 8 failures reference repeated-value `MetadataKey` -semantics changed by the `custom-metadata` PR (#144). Others: -`AbstractLoggerSpec` exception reporting, `LogLevelMap` builder levels, -`LogContextSpec` null-literal handling, varargs format extension. -Some may be production regressions, not test bugs — each needs -case-by-case triage. CI will be red on this branch (and in any repo -the floated config reaches) until they are fixed. +nothing. They were triaged in a dedicated session; its fixes were +adopted onto this branch. `:logging:jvmTest` now passes 232/232. + +Production bugs found and fixed by the triage (tests were right): + +- `AbstractLogger.atConfig()` delegated to `Level.INFO` instead of + `Level.CONFIG`. +- `MetadataHandler.Builder.addRepeatedHandler` had inverted validation + (rejected repeatable keys instead of requiring them; drift from the + `custom-metadata` PR #144). +- `MetadataKey.cast()` returned `null` instead of throwing the + documented `ClassCastException`; `checkCannotRepeat` threw + `IllegalStateException` where callers expect `IllegalArgumentException`. +- `SimpleProcessor` "wrapped" repeated-value lists with a no-op cast; + handlers could mutate them. Replaced with an unmodifiable iterator. +- `ScopedLoggingContext.Builder` lacked a member `run(Runnable)`, so + Kotlin's stdlib `run` extension executed blocks *without installing + the context*. +- Lazy log messages were evaluated outside the recursion guard of + `AbstractLogger.write`, so throwing/reentrant `toString()` escaped + error handling; timestamps lacked millis and UTC offset. + +Test-side fixes (production was right): logger-name expectations +(`kotlin.String` vs `java.lang.String`), synthetic lambda method names, +eager invocation counting for rate-limiter specs, Kotlin spread +operator for `logVarargs`, updated message-text expectations. diff --git a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt index 31fd7d6b..60f47a28 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,12 @@ import io.spine.logging.util.RecursionDepth import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.ExperimentalTime import kotlin.time.Instant +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.format.char +import kotlinx.datetime.offsetAt import kotlinx.datetime.toLocalDateTime /** @@ -66,10 +70,32 @@ public abstract class AbstractLogger> protected constructo private const val MAX_ALLOWED_RECURSION_DEPTH = 100 /** - * The format for date-time values in log data. + * The format for the local date-time part of timestamps in log data. + * + * Produces an ISO 8601 date-time with milliseconds, e.g., `2026-07-02T00:29:31.123`. */ @JvmField - val dateTimeFormat = LocalDateTime.Formats.ISO + val dateTimeFormat = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char('T') + hour() + char(':') + minute() + char(':') + second() + char('.') + secondFraction(3) + } + + /** + * The format for the four-digit UTC offset part of timestamps in log data, + * e.g., `+0100`. + */ + @JvmField + val offsetFormat = UtcOffset.Format { + offsetHours() + offsetMinutesOfHour() + } } /** @@ -114,9 +140,9 @@ public abstract class AbstractLogger> protected constructo public fun atInfo(): API = at(Level.INFO) /** - * A convenience method for at([Level.INFO]). + * A convenience method for at([Level.CONFIG]). */ - public fun atConfig(): API = at(Level.INFO) + public fun atConfig(): API = at(Level.CONFIG) /** * A convenience method for at([Level.FINE]). @@ -188,15 +214,23 @@ public abstract class AbstractLogger> protected constructo * * This method also guards against unbounded reentrant logging, and will suppress further * logging if it detects significant recursion has occurred. + * + * @param data The log statement data. + * @param prepare Completes [data] within the recursion guard right before the backend call. + * Use it for evaluation which may invoke user code, such as computing a lazy log message, + * so that exceptions and reentrant logging it causes are handled the same way as those + * coming from the backend. */ + @JvmOverloads @Suppress("TooGenericExceptionCaught") - public fun write(data: LogData) { + public fun write(data: LogData, prepare: () -> Unit = {}) { // Note: Recursion checking should not be in the `LoggerBackend`. // There are many backends and they can call into other backends. // We only want the counter incremented per log statement. try { RecursionDepth.enterLogStatement().use { depth -> if (depth.getValue() <= MAX_ALLOWED_RECURSION_DEPTH) { + prepare() backend.log(data) } else { reportError( @@ -262,8 +296,10 @@ public abstract class AbstractLogger> protected constructo val instant = Instant.fromEpochMilliseconds( data.timestampNanos.nanoseconds.inWholeMilliseconds ) - val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) - return dateTimeFormat.format(dateTime) + val zone = TimeZone.currentSystemDefault() + val dateTime = instant.toLocalDateTime(zone) + val offset = zone.offsetAt(instant) + return dateTimeFormat.format(dateTime) + offsetFormat.format(offset) } } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt index b21562c7..48c393f1 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -410,12 +410,11 @@ protected constructor( /** * Make the backend logging call. * - * This method takes a single string parameter as it's always called with one string argument. + * The given [message] is evaluated within the recursion guard of the logger, + * so that a throwing or reentrant `toString()` of a logged value cannot escape + * the error handling of [AbstractLogger.write]. */ - private fun logImpl(arg: String?) { - val prefix = loggingDomain?.messagePrefix ?: "" - this.literalArg = prefix + arg - + private fun logImpl(message: () -> String?) { // Right at the end of processing add any tags injected by the platform. // Any tags supplied at the log site are merged with the injected tags // (though this should be very rare). @@ -429,8 +428,11 @@ protected constructor( } addMetadata(Key.TAGS, finalTags) } - // Pass the completed log data to the backend (it should not be modified after this point). - getLogger().write(this) + // Pass the log data to the backend (it should not be modified after this point). + getLogger().write(this) { + // A `null` message is passed to the backend unmodified. + literalArg = message()?.let { (loggingDomain?.messagePrefix ?: "") + it } + } } // ---- Log site injection (used by pre-processors and special cases) ---- @@ -558,17 +560,16 @@ protected constructor( public final override fun log() { if (shouldLog()) { - logImpl("") + logImpl { "" } } } public final override fun log(message: () -> String?) { if (shouldLog()) { - logImpl(message()) + logImpl(message) } } - /** * The predefined metadata keys used by the default logging API. * diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt index e298be5e..575f0328 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,7 +122,7 @@ public abstract class LogPerBucketingStrategy protected constructor( * are effectively singletons. */ private val BY_CLASS = object : LogPerBucketingStrategy("ByClass") { - override fun apply(key: Any): Any = key::class + override fun apply(key: Any): Any = key.javaClass } /** @@ -131,7 +131,7 @@ public abstract class LogPerBucketingStrategy protected constructor( * are effectively singletons. */ private val BY_CLASS_NAME = object : LogPerBucketingStrategy("ByClassName") { - override fun apply(key: Any): Any = key::class.qualifiedName!! + override fun apply(key: Any): Any = key.javaClass.name /* This is a naturally interned value, so no need to call `intern()`. */ } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt b/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt index 5c6347fe..f13f67ea 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt @@ -143,18 +143,22 @@ public open class MetadataKey( this(label, clazz, canRepeat, true) /** - * Cast an arbitrary value to the type of this key. + * Casts an arbitrary value to the type of this key. + * + * @throws ClassCastException if the given value is not an instance of the key type. */ public fun cast(value: Any?): T? { if (value == null) { return null } - return if (clazz.isInstance(value)) { + if (clazz.isInstance(value)) { @Suppress("UNCHECKED_CAST") - value as T - } else { - null + return value as T } + throw ClassCastException( + "The value `$value` cannot be cast to `${clazz.qualifiedName}`" + + " required by the key `$this`." + ) } /** @@ -376,8 +380,8 @@ private fun createBloomFilterMaskFromSystemHashcode(instance: Any): Long { * Checks that a metadata key cannot be used for repeated values. * * @param key The metadata key to check. - * @throws IllegalStateException if the key supports repeated values. + * @throws IllegalArgumentException if the key supports repeated values. */ internal fun checkCannotRepeat(key: MetadataKey<*>) { - check(!key.canRepeat) { "The key `$key` does not support repeated values." } + require(!key.canRepeat) { "The key `$key` must be single-valued." } } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt index 34cd8649..91f0a3eb 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ package io.spine.logging.backend import com.google.errorprone.annotations.CanIgnoreReturnValue import io.spine.logging.MetadataKey -import io.spine.logging.checkCannotRepeat /** * Callback API for logger backend implementations to handle metadata keys/values. @@ -152,7 +151,7 @@ public abstract class MetadataHandler { key: MetadataKey, handler: RepeatedValueHandler ): Builder { - checkCannotRepeat(key) + require(key.canRepeat()) { "The key `$key` must support repeated values." } singleValueHandlers.remove(key) @Suppress("UNCHECKED_CAST") repeatedValueHandlers[key] = handler as RepeatedValueHandler<*, in C> diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt index c3d471fa..28d41d94 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -483,13 +483,6 @@ private class SimpleProcessor(scope: Metadata, logged: Metadata) : MetadataProce val map = LinkedHashMap, Any>() map.addTo(scope) map.addTo(logged) - // Wrap any repeated value lists to make them unmodifiable (required for correctness). - for (e in map.entries) { - if (e.key.canRepeat()) { - @Suppress("UNCHECKED_CAST") - e.setValue(e.value as List<*>) - } - } this.map = map } @@ -574,9 +567,29 @@ private fun MetadataHandler.dispatch( ) { if (key.canRepeat()) { @Suppress("UNCHECKED_CAST") - handleRepeated(key, (value as MutableList).iterator(), context) + handleRepeated(key, UnmodifiableIterator((value as List).iterator()), context) } else { @Suppress("UNCHECKED_CAST") handle(key, (value as T), context) } } + +/** + * Guards values of repeated keys from modification by handlers (required for correctness). + * + * The lists built by [MutableMap.addTo] are mutable, and exposing their iterators directly + * would allow a handler to remove values. Like [LightweightProcessor.ValueIterator], this + * wrapper satisfies casting to a mutable iterator but throws from [remove]. + */ +private class UnmodifiableIterator( + private val delegate: Iterator +) : MutableIterator { + + override fun hasNext(): Boolean = delegate.hasNext() + + override fun next(): T = delegate.next() + + override fun remove() { + throw UnsupportedOperationException() + } +} diff --git a/logging/src/commonMain/kotlin/io/spine/logging/context/ScopedLoggingContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/context/ScopedLoggingContext.kt index 4d52e177..06dd5a97 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/context/ScopedLoggingContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/context/ScopedLoggingContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,6 +192,21 @@ public abstract class ScopedLoggingContext protected constructor() { } } + /** + * Runs a runnable directly within a new context installed from this builder. + * + * Being a member, this method takes precedence over the [run][kotlin.run] + * extension from the standard library, which would otherwise execute + * the block without installing the context. + * + * @throws InvalidLoggingContextStateException + * if the context created during this method cannot + * be closed correctly (e.g., if a nested context has also been opened, + * but not closed). + */ + public fun run(r: Runnable): Unit = + wrap(r).run() + /** * Calls a function directly within a new context installed from this builder. */ diff --git a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt index ba755c4c..95cc7171 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2019, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.string.shouldBeEmpty import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldContainIgnoringCase import io.kotest.matchers.string.shouldMatch import io.kotest.matchers.string.shouldNotContain import io.spine.logging.backend.LogData @@ -141,7 +142,7 @@ internal class AbstractLoggerSpec { output shouldMatch TIMESTAMP_PREFIX output shouldContain LOGGING_ERROR output shouldContain this::class.simpleName!! - output shouldContain "unbounded recursion in log statement" + output shouldContainIgnoringCase "unbounded recursion in log statement" } @Test diff --git a/logging/src/commonTest/kotlin/io/spine/logging/context/LogLevelMapSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/context/LogLevelMapSpec.kt index 832a3b15..aae1b607 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/context/LogLevelMapSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/context/LogLevelMapSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2019, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,7 +101,11 @@ internal class LogLevelMapSpec { .build() levelMap["com.google"] shouldBe INFO levelMap["java.lang"] shouldBe WARNING - levelMap["java.lang.String"] shouldBe FINE + // Classes are registered under their Kotlin qualified names, matching + // the logger names produced by `LoggingFactory`. For the mapped type + // `String::class` this is `kotlin.String`, not `java.lang.String`. + levelMap["kotlin.String"] shouldBe FINE + levelMap["java.lang.String"] shouldBe WARNING } } diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/JvmLoggerSpec.kt b/logging/src/jvmTest/kotlin/io/spine/logging/JvmLoggerSpec.kt index a1ed9fe0..ac801665 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/JvmLoggerSpec.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/JvmLoggerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,8 +76,11 @@ internal class JvmLoggerSpec { System.err.println(consoleCheck) } consoleOutput shouldContain consoleCheck - val expectedMethodReference = "produce the output with the name of" + - " the logging class and calling method" + // The log statement above sits in the `tapConsole` lambda, so the calling + // method is reported as the synthetic lambda method, which carries + // the sanitized name of this test method. + val expectedMethodReference = "produce_the_output_with_the_name_of" + + "_the_logging_class_and_calling_method" consoleOutput shouldContain this::class.java.name consoleOutput shouldContain expectedMsg consoleOutput shouldContain expectedMethodReference @@ -293,11 +296,14 @@ internal class JvmLoggerSpec { val totalDuration = (invocations - 1) * intervalMillis val consoleOutput = tapConsole { var i = 1 - (0..totalDuration step intervalMillis).forEach { millis -> + (0..totalDuration step intervalMillis).forEach { _ -> + // Count invocations eagerly: the message lambda is only + // evaluated for statements which actually log. + val invocation = i++ logger.atInfo() .every(invocationLimit) .atMostEvery(timeLimitMillis, MILLISECONDS) - .log { numberedMessage(i++) } + .log { numberedMessage(invocation) } sleep(intervalMillis) } } @@ -363,10 +369,13 @@ internal class JvmLoggerSpec { var invoked = 0 val consoleOutput = tapConsole { for (millis in 0..totalDuration step intervalMillis) { + // Count invocations eagerly: the message lambda is only + // evaluated for statements which actually log. + val invocation = ++invoked logger.atInfo() .every(invocationLimit) .atMostEvery(timeLimitMillis, MILLISECONDS) - .log { timeAndSerialStamp(++invoked, millis) } + .log { timeAndSerialStamp(invocation, millis) } sleep(intervalMillis) } } diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt b/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt index 18f6ec7b..1f5fd10a 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import io.spine.logging.Level.Companion.WARNING import io.spine.logging.LogContext.Companion.specializeLogSiteKeyFromMetadata import io.spine.logging.LogContext.Key import io.spine.logging.LogSiteLookup.logSite +import io.spine.logging.backend.SimpleMessageFormatter import io.spine.logging.backend.given.FakeMetadata import io.spine.logging.backend.given.shouldContain import io.spine.logging.backend.given.shouldContainInOrder @@ -113,9 +114,6 @@ internal class LogContextSpec { backend.lastLogged shouldHaveMessage MESSAGE_LITERAL } - - - @Test fun `accept a literal message`() { logger.at(INFO).log { MESSAGE_LITERAL } @@ -576,8 +574,6 @@ internal class LogContextSpec { backend.lastLogged.shouldHaveMessage("") } - - /** * Tests that a `null` literal is passed unmodified to the backend * without throwing an exception. @@ -593,7 +589,9 @@ internal class LogContextSpec { fun `log 'null' if given message is 'null'`() { logger.atInfo().log { null } backend.lastLogged.let { - it shouldHaveMessage ("") + it shouldHaveMessage null + // The literal is rendered as the text `null` when formatted. + SimpleMessageFormatter.getLiteralLogMessage(it) shouldBe "null" } } diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt b/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt index 9f2da734..e4556e09 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ internal class AnyExtsJvmSpec { fun `objects that return 'null' on 'toString()'`() { val badToString = BadToString() badToString.safeToString() shouldContain badToString::class.simpleName!! - badToString.safeToString() shouldContain "toString() returned null" + badToString.safeToString() shouldContain "`toString()` returned `null`" } } } diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/test/LoggingCompatibilityTest.kt b/logging/src/jvmTest/kotlin/io/spine/logging/test/LoggingCompatibilityTest.kt index d08f1513..d0334829 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/test/LoggingCompatibilityTest.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/test/LoggingCompatibilityTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,9 @@ internal class LoggingCompatibilityTest { fun `accepting array as 'varargs'`() { val output = tapConsole { val args = arrayOf("arg1", "arg2") - logger.atInfo().logVarargs("Message with array: %s %s", args) + // Unlike Java, Kotlin requires the spread operator to pass + // an array as `vararg` arguments. + logger.atInfo().logVarargs("Message with array: %s %s", *args) } output shouldContain "Message with array: arg1 arg2" } From a7f323eaefa85df5e146d997215b3b93c99bad63 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 13:38:13 +0100 Subject: [PATCH 06/14] Address review findings Add the missing regression tests: convenience level methods must map to their matching levels (pins the `atConfig()` fix), and `MetadataHandler.Builder.addRepeatedHandler` must reject single-valued keys. Complete the KDoc contracts (`@throws` on `addRepeatedHandler`, `null` pass-through on `MetadataKey.cast`), use `safeToString()` when reporting a failed cast, and apply minor doc and message polish. Co-Authored-By: Claude Fable 5 --- .../kotlin/io/spine/logging/AbstractLogger.kt | 11 ++++--- .../kotlin/io/spine/logging/LogContext.kt | 7 +++-- .../kotlin/io/spine/logging/MetadataKey.kt | 5 +++- .../spine/logging/backend/MetadataHandler.kt | 5 ++-- .../logging/backend/MetadataProcessor.kt | 10 +++---- .../io/spine/logging/AbstractLoggerSpec.kt | 30 ++++++++++++++++++- .../logging/backend/MetadataHandlerSpec.kt | 12 +++++++- 7 files changed, 61 insertions(+), 19 deletions(-) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt index 60f47a28..8378a332 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,8 +88,7 @@ public abstract class AbstractLogger> protected constructo } /** - * The format for the four-digit UTC offset part of timestamps in log data, - * e.g., `+0100`. + * The format for the four-digit UTC offset part of timestamps in log data, e.g., `+0100`. */ @JvmField val offsetFormat = UtcOffset.Format { @@ -217,9 +216,9 @@ public abstract class AbstractLogger> protected constructo * * @param data The log statement data. * @param prepare Completes [data] within the recursion guard right before the backend call. - * Use it for evaluation which may invoke user code, such as computing a lazy log message, - * so that exceptions and reentrant logging it causes are handled the same way as those - * coming from the backend. + * Use it for work that may invoke user code, such as computing a lazy log message, + * so that any exceptions or reentrant logging it triggers are handled the same way + * as those coming from the backend. */ @JvmOverloads @Suppress("TooGenericExceptionCaught") diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt index 48c393f1..0478d767 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -408,7 +408,7 @@ protected constructor( } /** - * Make the backend logging call. + * Makes the backend logging call. * * The given [message] is evaluated within the recursion guard of the logger, * so that a throwing or reentrant `toString()` of a logged value cannot escape @@ -428,7 +428,8 @@ protected constructor( } addMetadata(Key.TAGS, finalTags) } - // Pass the log data to the backend (it should not be modified after this point). + // Pass the log data to the backend (it must not be modified once the message + // is completed by the `prepare` block below). getLogger().write(this) { // A `null` message is passed to the backend unmodified. literalArg = message()?.let { (loggingDomain?.messagePrefix ?: "") + it } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt b/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt index f13f67ea..6512f7bc 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/MetadataKey.kt @@ -29,6 +29,7 @@ package io.spine.logging import io.spine.annotation.VisibleForTesting import io.spine.logging.MetadataKey.Companion.repeated import io.spine.logging.MetadataKey.Companion.single +import io.spine.logging.backend.safeToString import io.spine.logging.util.Checks.checkMetadataIdentifier import io.spine.logging.util.RecursionDepth import kotlin.reflect.KClass @@ -145,6 +146,8 @@ public open class MetadataKey( /** * Casts an arbitrary value to the type of this key. * + * A `null` value is returned as `null`. + * * @throws ClassCastException if the given value is not an instance of the key type. */ public fun cast(value: Any?): T? { @@ -156,7 +159,7 @@ public open class MetadataKey( return value as T } throw ClassCastException( - "The value `$value` cannot be cast to `${clazz.qualifiedName}`" + + "The value `${value.safeToString()}` cannot be cast to `${clazz.qualifiedName}`" + " required by the key `$this`." ) } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt index 91f0a3eb..69e0cabf 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -145,13 +145,14 @@ public abstract class MetadataHandler { * @param handler The repeated value handler to be invoked once for all associated values. * @param T The key/value type. * @return The builder instance for chaining. + * @throws IllegalArgumentException if the key does not support repeated values. */ @CanIgnoreReturnValue public fun addRepeatedHandler( key: MetadataKey, handler: RepeatedValueHandler ): Builder { - require(key.canRepeat()) { "The key `$key` must support repeated values." } + require(key.canRepeat) { "The key `$key` must support repeated values." } singleValueHandlers.remove(key) @Suppress("UNCHECKED_CAST") repeatedValueHandlers[key] = handler as RepeatedValueHandler<*, in C> diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt index 28d41d94..b6d406b5 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -577,9 +577,9 @@ private fun MetadataHandler.dispatch( /** * Guards values of repeated keys from modification by handlers (required for correctness). * - * The lists built by [MutableMap.addTo] are mutable, and exposing their iterators directly - * would allow a handler to remove values. Like [LightweightProcessor.ValueIterator], this - * wrapper satisfies casting to a mutable iterator but throws from [remove]. + * The lists built by [addTo] are mutable, and exposing their iterators directly + * would allow a handler to remove values. Like [LightweightProcessor.ValueIterator], + * this wrapper can still be cast to a mutable iterator, but throws from [remove]. */ private class UnmodifiableIterator( private val delegate: Iterator @@ -590,6 +590,6 @@ private class UnmodifiableIterator( override fun next(): T = delegate.next() override fun remove() { - throw UnsupportedOperationException() + throw UnsupportedOperationException("Metadata values cannot be removed.") } } diff --git a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt index 95cc7171..f21417a7 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019, The Flogger Authors; 2026, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ package io.spine.logging import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.string.shouldBeEmpty import io.kotest.matchers.string.shouldContain @@ -145,6 +146,33 @@ internal class AbstractLoggerSpec { output shouldContainIgnoringCase "unbounded recursion in log statement" } + @Test + fun `map convenience methods to the matching levels`() { + val loggedLevels = mutableListOf() + val backend = object : FormattingBackend() { + override fun log(data: LogData) { + loggedLevels.add(data.level) + } + } + val logger = ConfigurableLogger(backend) + + logger.atSevere().log { "severe" } + logger.atError().log { "error" } + logger.atWarning().log { "warning" } + logger.atInfo().log { "info" } + logger.atConfig().log { "config" } + logger.atFine().log { "fine" } + logger.atDebug().log { "debug" } + logger.atFiner().log { "finer" } + logger.atTrace().log { "trace" } + logger.atFinest().log { "finest" } + + loggedLevels shouldContainExactly listOf( + Level.SEVERE, Level.ERROR, Level.WARNING, Level.INFO, Level.CONFIG, + Level.FINE, Level.DEBUG, Level.FINER, Level.TRACE, Level.FINEST, + ) + } + @Test fun `allow logging exceptions thrown by a backend`() { val backend = object : FormattingBackend() { diff --git a/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt index cacffc82..0744e6d4 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ package io.spine.logging.backend import com.google.common.base.Joiner import com.google.common.collect.Iterators +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.spine.logging.MetadataKey import io.spine.logging.backend.given.FakeMetadata @@ -163,6 +164,15 @@ internal class MetadataHandlerSpec { val withoutFooHandler = builder.removeHandlers(foo).build() process(withoutFooHandler, scope, logged) shouldBe "foo=<> foo=<>" } + + @Test + fun `reject a repeated value handler for a single-valued key`() { + val single = MetadataKey.single("single") + shouldThrow { + MetadataHandler.builder(::appendUnknownValue) + .addRepeatedHandler(single, ::appendValues) + } + } } /** From cf43e4bb00dd9228530fa7a5137bb9f4039b4d62 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 19:38:28 +0100 Subject: [PATCH 07/14] Restore Flogger attribution in copyright headers The `update-copyright.sh` PostToolUse hook replaced the dual "The Flogger Authors; TeamDev" headers with the plain TeamDev template in the six files edited during the review round. Restore the attribution, keeping the 2026 year. Co-Authored-By: Claude Fable 5 --- .../src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt | 2 +- logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt | 2 +- .../kotlin/io/spine/logging/backend/MetadataHandler.kt | 2 +- .../kotlin/io/spine/logging/backend/MetadataProcessor.kt | 2 +- .../commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt | 2 +- .../kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt index 8378a332..c08a8ead 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt index 0478d767..ad3cf60b 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt index 69e0cabf..37330745 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt index b6d406b5..f0076439 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/MetadataProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt index f21417a7..c00e7297 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/AbstractLoggerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2019, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt index 0744e6d4..909db4d1 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/backend/MetadataHandlerSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 487532ccf12fed657c15e956e3ba933b977f7eed Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 20:19:06 +0100 Subject: [PATCH 08/14] Keep Kotlin classes as bucketing keys Per review, revert the `byClass()`/`byClassName()` strategies to `key::class` / `key::class.qualifiedName!!` and align the tests instead: expect `KClass`-based keys compared by equality (the `::class` wrapper and the qualified name are re-created on each evaluation), and give the `byClassName()` test a member key class, since `qualifiedName` is `null` for local classes. Co-Authored-By: Claude Fable 5 --- .../spine/logging/LogPerBucketingStrategy.kt | 4 ++-- .../logging/LogPerBucketingStrategySpec.kt | 22 ++++++++++++++----- .../kotlin/io/spine/logging/LogContextSpec.kt | 4 ++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt index 575f0328..f7f97295 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt @@ -122,7 +122,7 @@ public abstract class LogPerBucketingStrategy protected constructor( * are effectively singletons. */ private val BY_CLASS = object : LogPerBucketingStrategy("ByClass") { - override fun apply(key: Any): Any = key.javaClass + override fun apply(key: Any): Any = key::class } /** @@ -131,7 +131,7 @@ public abstract class LogPerBucketingStrategy protected constructor( * are effectively singletons. */ private val BY_CLASS_NAME = object : LogPerBucketingStrategy("ByClassName") { - override fun apply(key: Any): Any = key.javaClass.name + override fun apply(key: Any): Any = key::class.qualifiedName!! /* This is a naturally interned value, so no need to call `intern()`. */ } diff --git a/logging/src/commonTest/kotlin/io/spine/logging/LogPerBucketingStrategySpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/LogPerBucketingStrategySpec.kt index 157e8752..12e731f1 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/LogPerBucketingStrategySpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/LogPerBucketingStrategySpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,17 +58,20 @@ internal class LogPerBucketingStrategySpec { fun `aggregates keys by class`() { val anyKey = Any() val strategy = LogPerBucketingStrategy.byClass() - strategy.applyForTesting(anyKey) shouldBeSameInstanceAs anyKey.javaClass + // `key::class` creates a new `KClass` wrapper on each evaluation, + // so the keys are compared by equality rather than by identity. + strategy.applyForTesting(anyKey) shouldBe anyKey::class "$strategy" shouldBe "LogPerBucketingStrategy[ByClass]" } @Test fun `aggregates keys by class name`() { - class NotASystemClass val anyKey = NotASystemClass() - val className = NotASystemClass::class.java.name + val className = NotASystemClass::class.qualifiedName!! val strategy = LogPerBucketingStrategy.byClassName() - strategy.applyForTesting(anyKey) shouldBeSameInstanceAs className + // The qualified name is computed anew on each evaluation, + // so the keys are compared by equality rather than by identity. + strategy.applyForTesting(anyKey) shouldBe className "$strategy" shouldBe "LogPerBucketingStrategy[ByClassName]" } @@ -99,4 +102,13 @@ internal class LogPerBucketingStrategySpec { strategy(257).applyForTesting(key) shouldBe 128 "${strategy(10)}" shouldBe "LogPerBucketingStrategy[ByHashCode(10)]" } + + /** + * A key class for the `byClassName()` test. + * + * The strategy relies on [qualified names][kotlin.reflect.KClass.qualifiedName], + * which are `null` for local and anonymous classes, so this class is declared + * as a member. + */ + private class NotASystemClass } diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt b/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt index 1f5fd10a..512ef2e8 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/LogContextSpec.kt @@ -365,7 +365,7 @@ internal class LogContextSpec { logged[0].metadata.shouldHaveSize(2) logged[0].metadata.shouldUniquelyContain( Key.LOG_SITE_GROUPING_KEY, - IllegalArgumentException::class.java + IllegalArgumentException::class ) logged[0].metadata.shouldUniquelyContain( Key.LOG_AT_MOST_EVERY, @@ -375,7 +375,7 @@ internal class LogContextSpec { logged[1].metadata.shouldHaveSize(2) logged[1].metadata.shouldUniquelyContain( Key.LOG_SITE_GROUPING_KEY, - NullPointerException::class.java + NullPointerException::class ) logged[1].metadata.shouldUniquelyContain( Key.LOG_AT_MOST_EVERY, From 0492b9c63915fac11ed785358189da10b5306101 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 21:06:12 +0100 Subject: [PATCH 09/14] Address Gradle 10 deprecations in build scripts Replace the deprecated Kotlin DSL property delegates (`by getting`, `by existing`, `by registering`) with the forms recommended by the deprecation warnings (`getByName`, `named`, `register`), and pass `project(path)` instead of `Project` objects to the `dokka` configuration. Drop the `@Suppress("unused")` annotations that only served the removed delegate `val`s. The remaining deprecation warnings come from third-party plugins (Gradle Doctor, Detekt, Kover) and can only be addressed by plugin updates in a dedicated task. Co-Authored-By: Claude Fable 5 --- backends/otel-backend/build.gradle.kts | 7 +++---- build.gradle.kts | 2 +- buildSrc/src/main/kotlin/jvm-module.gradle.kts | 2 +- buildSrc/src/main/kotlin/kmp-module.gradle.kts | 6 ++---- logging-testlib/build.gradle.kts | 8 +++----- logging/build.gradle.kts | 10 ++++------ platforms/jvm-default-platform/build.gradle.kts | 7 +++---- tests/fixtures/build.gradle.kts | 7 +++---- 8 files changed, 20 insertions(+), 29 deletions(-) diff --git a/backends/otel-backend/build.gradle.kts b/backends/otel-backend/build.gradle.kts index ffcb1d27..b25eadb4 100644 --- a/backends/otel-backend/build.gradle.kts +++ b/backends/otel-backend/build.gradle.kts @@ -47,9 +47,8 @@ spinePublishing { } kotlin { - @Suppress("unused") // Source set `val`s are used implicitly. sourceSets { - val commonMain by getting { + getByName("commonMain") { dependencies { // The Spine logging backend SPI. api(project(":logging")) @@ -62,13 +61,13 @@ kotlin { implementation(OpenTelemetryKotlin.noop) } } - val jvmMain by getting { + getByName("jvmMain") { dependencies { // `@AutoService` registers the JVM `BackendFactory` for `ServiceLoader`. implementation(AutoService.annotations) } } - val jvmTest by getting { + getByName("jvmTest") { dependencies { // The native Kotlin OpenTelemetry SDK, used only by tests to build an // `OpenTelemetry` instance with a recording log-record processor. diff --git a/build.gradle.kts b/build.gradle.kts index e5d4991c..36c50ffa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -105,6 +105,6 @@ gradle.projectsEvaluated { dependencies { productionModules.forEach { - dokka(it) + dokka(project(it.path)) } } diff --git a/buildSrc/src/main/kotlin/jvm-module.gradle.kts b/buildSrc/src/main/kotlin/jvm-module.gradle.kts index 8e777c6a..1438116b 100644 --- a/buildSrc/src/main/kotlin/jvm-module.gradle.kts +++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts @@ -154,7 +154,7 @@ fun Module.forceConfigurations() { fun Module.setTaskDependencies(generatedDir: String) { tasks { - val cleanGenerated by registering(Delete::class) { + val cleanGenerated = register("cleanGenerated") { group = SpineTaskGroup.name description = "Deletes the directory with generated sources" delete(generatedDir) diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts index cb790b79..3fc3911d 100644 --- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -109,7 +109,6 @@ fun Project.forceConfigurations() { * It configures KMP, in which Kotlin for JVM is only one of * possible targets. */ -@Suppress("UNUSED_VARIABLE") // Avoid warnings for source set vars. kotlin { // Enables explicit API mode for any Kotlin sources within the module. explicitApi() @@ -127,9 +126,8 @@ kotlin { // Dependencies are specified per-target. // Please note, common sources are implicitly available in all targets. - @Suppress("unused") // source set `val`s are used implicitly. sourceSets { - val commonTest by getting { + getByName("commonTest") { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) @@ -137,7 +135,7 @@ kotlin { implementation(Kotest.frameworkEngine) } } - val jvmTest by getting { + getByName("jvmTest") { dependencies { implementation(dependencies.enforcedPlatform(JUnit.bom)) implementation(TestLib.lib) diff --git a/logging-testlib/build.gradle.kts b/logging-testlib/build.gradle.kts index 8a4f4766..1e55421f 100644 --- a/logging-testlib/build.gradle.kts +++ b/logging-testlib/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,15 +48,13 @@ spinePublishing { kotlin { sourceSets { - @Suppress("unused") - val commonMain by getting { + getByName("commonMain") { dependencies { api(Base.annotations) implementation(project(":logging")) } } - @Suppress("unused") - val jvmMain by getting { + getByName("jvmMain") { dependencies { implementation(Log4j2.core) } diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts index afee2a6e..5b74eecd 100644 --- a/logging/build.gradle.kts +++ b/logging/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,6 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -@file:Suppress("unused") // source set accessed via `by getting`. - import io.spine.dependency.kotlinx.DateTime import io.spine.dependency.kotlinx.AtomicFu import io.spine.dependency.local.Base @@ -49,7 +47,7 @@ spinePublishing { kotlin { sourceSets { - val commonMain by getting { + getByName("commonMain") { dependencies { api(Base.annotations) implementation(DateTime.lib) @@ -57,7 +55,7 @@ kotlin { implementation(AtomicFu.lib) } } - val jvmMain by getting { + getByName("jvmMain") { dependencies { implementation(DateTime.lib) implementation(Reflect.lib) @@ -65,7 +63,7 @@ kotlin { runtimeOnly(project(":jvm-default-platform")) } } - val jvmTest by getting { + getByName("jvmTest") { dependencies { implementation(project(":probe-backend")) implementation(project(":logging-testlib")) diff --git a/platforms/jvm-default-platform/build.gradle.kts b/platforms/jvm-default-platform/build.gradle.kts index bfe8ef73..a35817d7 100644 --- a/platforms/jvm-default-platform/build.gradle.kts +++ b/platforms/jvm-default-platform/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,9 +61,8 @@ java { afterEvaluate { // `kspKotlin` task is created after the configuration phase, // so we have to use the `afterEvaluate` block. - val kspKotlin by tasks.existing - @Suppress("unused") - val dokkaHtml by tasks.existing { + val kspKotlin = tasks.named("kspKotlin") + tasks.named("dokkaHtml") { dependsOn(kspKotlin) } } diff --git a/tests/fixtures/build.gradle.kts b/tests/fixtures/build.gradle.kts index a689054f..d17a47fb 100644 --- a/tests/fixtures/build.gradle.kts +++ b/tests/fixtures/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,8 @@ plugins { kotlin { sourceSets { - @Suppress("unused") - val commonMain by getting { - dependencies{ + getByName("commonMain") { + dependencies { implementation(project(":logging")) implementation(Reflect.lib) api(project(":logging-testlib")) From 60bac918b39c2069dcdbf7f790f7bac7ede7e51b Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 23:32:32 +0100 Subject: [PATCH 10/14] Update `config` --- .../io/spine/dependency/local/Compiler.kt | 6 +- .../io/spine/gradle/VersionGradleFile.kt | 33 +++-- .../kotlin/io/spine/gradle/fs/LazyTempPath.kt | 9 +- .../kotlin/io/spine/gradle/fs/SpineTempDir.kt | 92 +++++++++++++ .../gradle/report/pom/DependencyWriter.kt | 123 ++++++++++++++---- .../gradle/report/pom/ProjectMetadata.kt | 8 +- .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 87 ------------- .../main/kotlin/jacoco-kotlin-jvm.gradle.kts | 80 ------------ .../src/main/kotlin/kmp-module.gradle.kts | 1 + .../main/kotlin/uber-jar-module.gradle.kts | 9 +- .../src/main/kotlin/write-manifest.gradle.kts | 2 +- .../io/spine/gradle/VersionGradleFileSpec.kt | 22 ++++ .../io/spine/gradle/fs/LazyTempPathSpec.kt | 69 ++++++++++ .../io/spine/gradle/fs/SpineTempDirSpec.kt | 54 ++++++++ .../gradle/report/pom/DependencyWriterSpec.kt | 113 ++++++++++++++++ config | 2 +- 16 files changed, 490 insertions(+), 220 deletions(-) create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt delete mode 100644 buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index e3b4d0aa..8dbffcdc 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -27,8 +27,6 @@ package io.spine.dependency.local import io.spine.dependency.Dependency -import io.spine.dependency.local.Compiler.DF_VERSION_ENV -import io.spine.dependency.local.Compiler.VERSION_ENV /** * Dependencies on the Spine Compiler modules. @@ -74,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.057" + private const val fallbackVersion = "2.0.0-SNAPSHOT.059" /** * The distinct version of the Compiler used by other build tools. @@ -83,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.057" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.059" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt b/buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt index 7e86ea94..c84bb6e0 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt @@ -33,14 +33,20 @@ import org.gradle.api.GradleException * Reads a `version.gradle.kts` file and resolves the `extra` properties it declares. * * [contentUnder] and [contentInBase] read the file (from the working tree or a base branch); - * [keyForValue] and [valueForKey] resolve its `extra` properties. The following declaration - * shapes are handled (see the `bump-version` skill): + * [keyForValue] and [valueForKey] resolve its `extra` properties. Each property is registered + * either with the current `extra.set("name", …)` call or the legacy `by extra(…)` property + * delegate that Gradle deprecated (the `bump-version` skill migrates the latter to the former); + * both spellings are recognized, so a file is read correctly whether or not it has migrated. + * The following value shapes are handled: * - * 1. a literal: `val versionToPublish: String by extra("2.0.0-SNAPSHOT.182")`; - * 2. an alias to another `extra`: `val versionToPublish by extra(compilerVersion)` paired - * with `val compilerVersion: String by extra("2.0.0-SNAPSHOT.043")`; - * 3. an alias to a plain `val`: `val versionToPublish by extra(base)` paired with - * `val base = "2.0.0-SNAPSHOT.043"`. + * 1. a literal: + * `extra.set("versionToPublish", "2.0.0-SNAPSHOT.182")`, or the legacy + * `val versionToPublish: String by extra("2.0.0-SNAPSHOT.182")`; + * 2. an alias to a plain `val` (or, in the legacy spelling, to another `extra`): + * `extra.set("versionToPublish", compilerVersion)` paired with + * `val compilerVersion = "2.0.0-SNAPSHOT.043"`, or the legacy + * `val versionToPublish by extra(compilerVersion)` paired with + * `val compilerVersion: String by extra("2.0.0-SNAPSHOT.043")`. * * The publishing-version property is identified by [keyForValue] using the already-resolved * project version as an oracle, so the specific property name (`versionToPublish`, @@ -56,10 +62,19 @@ internal object VersionGradleFile { */ const val NAME = "version.gradle.kts" + // Legacy `by extra(…)` property-delegate spellings (deprecated by Gradle). private val literalExtra = Regex("""val\s+(\w+)\s*(?::\s*String)?\s+by\s+extra\(\s*"([^"]+)"\s*\)""") private val aliasExtra = Regex("""val\s+(\w+)\s*(?::\s*String)?\s+by\s+extra\(\s*([A-Za-z_]\w*)\s*\)""") + + // Current `extra.set("name", …)` spellings. + private val literalSet = + Regex("""extra\.set\(\s*"(\w+)"\s*,\s*"([^"]+)"\s*\)""") + private val aliasSet = + Regex("""extra\.set\(\s*"(\w+)"\s*,\s*([A-Za-z_]\w*)\s*\)""") + + // A plain `val name = "value"` that an alias may reference. private val plainAssignment = Regex("""val\s+(\w+)\s*(?::\s*String)?\s*=\s*"([^"]+)"""") @@ -68,12 +83,12 @@ internal object VersionGradleFile { * string value. */ private fun parse(content: String): Map { - val literals = literalExtra.findAll(content) + val literals = (literalExtra.findAll(content) + literalSet.findAll(content)) .associate { it.groupValues[1] to it.groupValues[2] } val plains = plainAssignment.findAll(content) .associate { it.groupValues[1] to it.groupValues[2] } val resolved = literals.toMutableMap() - aliasExtra.findAll(content).forEach { match -> + (aliasExtra.findAll(content) + aliasSet.findAll(content)).forEach { match -> val name = match.groupValues[1] val source = match.groupValues[2] if (name !in resolved) { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt index 0344819f..b31cd308 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,11 +41,16 @@ import java.nio.file.WatchService * * After the first usage, the instances of this type delegate all calls to the internally * created instance of [Path] created with [createTempDirectory]. + * + * The directory is created under the [shared base directory][SpineTempDir], which is removed + * when the JVM — the Gradle daemon — shuts down. Build tasks delete their own directories + * eagerly as the primary cleanup; this shutdown removal is a safety net, so a directory does + * not outlive the daemon even when a build fails before its eager cleanup runs. */ @Suppress("TooManyFunctions") class LazyTempPath(private val prefix: String) : Path { - private val delegate: Path by lazy { createTempDirectory(prefix) } + private val delegate: Path by lazy { createTempDirectory(SpineTempDir.path, prefix) } override fun compareTo(other: Path): Int = delegate.compareTo(other) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt new file mode 100644 index 00000000..d6e856e0 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.fs + +import java.nio.file.Files.createDirectories +import java.nio.file.Files.createTempDirectory +import java.nio.file.Path + +/** + * A per-JVM parent directory for the temporary directories created by the build. + * + * The directory is created [lazily][path] under a common, attributable namespace — + * `/io.spine.gradle.fs`, named after the package of [LazyTempPath] — so + * that leftover files are easy to attribute. Within that namespace, each JVM gets its own + * subdirectory named after the process id, so concurrent Gradle daemons never delete one + * another's temporary files. + * + * Upon creation, the per-JVM directory is scheduled for recursive removal when the JVM + * shuts down. This is a safety net should the explicit cleanup performed by the build + * tasks not run — for example, when a build fails before reaching it. The shared namespace + * directory itself is intentionally left in place: deleting it on shutdown could wipe + * directories still in use by another JVM running on the same machine. + * + * @see LazyTempPath + */ +internal object SpineTempDir { + + /** + * The per-JVM directory, created on the first access and removed on JVM shutdown. + */ + val path: Path by lazy { createPerJvmDir() } + + private fun createPerJvmDir(): Path { + val namespace = Path.of(systemTempDir(), LazyTempPath::class.java.packageName) + createDirectories(namespace) + // A per-JVM directory keeps concurrent Gradle daemons from deleting one another's + // files when their shutdown hooks fire. The PID makes a leftover directory easy + // to attribute; `createTempDirectory` adds a random suffix so that a reused PID + // still yields a unique directory. + val pid = ProcessHandle.current().pid() + val jvmDir = createTempDirectory(namespace, "$pid-") + deleteRecursivelyOnShutdown(jvmDir) + return jvmDir + } + + /** + * Obtains the value of the system property pointing to the temporary directory. + */ + private fun systemTempDir(): String = + checkNotNull(System.getProperty("java.io.tmpdir")) { + "The `java.io.tmpdir` system property is not set." + } + + /** + * Requests the recursive removal of the given [directory] when the JVM shuts down. + * + * @see Runtime.addShutdownHook + */ + private fun deleteRecursivelyOnShutdown(directory: Path) { + val runtime = Runtime.getRuntime() + runtime.addShutdownHook(Thread { + val deleted = directory.toFile().deleteRecursively() + if (!deleted) { + System.err.println("Unable to delete the temporary directory `$directory`.") + } + }) + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt index 64e46ef7..2a2a4c1c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt @@ -34,6 +34,7 @@ import kotlin.reflect.full.isSubclassOf import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.internal.artifacts.dependencies.AbstractExternalModuleDependency import org.gradle.kotlin.dsl.withGroovyBuilder @@ -54,6 +55,11 @@ import org.gradle.kotlin.dsl.withGroovyBuilder * * ``` * + * The version reported for each dependency is the one selected by Gradle's + * dependency resolution — the version actually placed on the classpath — rather + * than the version requested in the build script. This reflects `force(...)` + * directives, platform/BOM constraints, and conflict resolution. + * * When there are several versions of the same dependency, only the one with * the newest version is retained. If the retained version is used in several * configurations, the highest-ranking Maven scope is reported, e.g. `compile` @@ -107,38 +113,70 @@ private constructor( /** * Returns the [scoped dependencies][ScopedDependency] of a Gradle project. + * + * The version of each dependency is the one selected by dependency resolution + * for the project it comes from. See [resolvedVersions]. + */ +fun Project.dependencies(): SortedSet = + collectScopedDependencies { it.resolvedVersions() } + +/** + * Returns the [scoped dependencies][ScopedDependency] of a Gradle project, taking + * the version of each dependency from the given [resolvedVersions] map instead of + * resolving the project's own configurations. + * + * This overload exists for tests: a project created with `ProjectBuilder` cannot + * resolve its configurations against real repositories, so the resolved versions + * are supplied directly. The keys are the `"group:name"` of the modules. */ -fun Project.dependencies(): SortedSet { +internal fun Project.dependencies( + resolvedVersions: Map +): SortedSet = + collectScopedDependencies { resolvedVersions } + +/** + * Collects the [scoped dependencies][ScopedDependency] of this project and its + * subprojects, deduplicates them, and returns them in the conventional Maven order. + * + * The version of each dependency is taken from the map returned by the supplied + * `resolvedVersionsOf` function for the project the dependency comes from. + */ +private fun Project.collectScopedDependencies( + resolvedVersionsOf: (Project) -> Map +): SortedSet { val dependencies = mutableSetOf() - dependencies.addAll(this.depsFromAllConfigurations()) + dependencies.addAll(depsFromAllConfigurations(resolvedVersionsOf(this))) - this.subprojects.forEach { subproject -> - val subprojectDeps = subproject.depsFromAllConfigurations() + subprojects.forEach { subproject -> + val subprojectDeps = subproject.depsFromAllConfigurations(resolvedVersionsOf(subproject)) dependencies.addAll(subprojectDeps) } - val result = deduplicate(dependencies) + return deduplicate(dependencies) .map { it.scoped } .toSortedSet() - return result } /** * Returns the external dependencies of the project from all the project configurations. + * + * The version of each returned dependency is taken from [resolvedVersions] by its + * `"group:name"` key, falling back to the declared version when the module is on no + * resolvable configuration — for example, a version managed by a BOM, which carries + * no explicit version of its own. */ -private fun Project.depsFromAllConfigurations(): Set { +private fun Project.depsFromAllConfigurations( + resolvedVersions: Map +): Set { val result = mutableSetOf() - this.configurations.forEach { configuration -> + configurations.forEach { configuration -> configuration.dependencies .filter { it.isExternal() } .forEach { dependency -> - val forcedVersion = configuration.forcedVersionOf(dependency) + val version = resolvedVersions[moduleKey(dependency.group, dependency.name)] + ?: dependency.version val moduleDependency = - if (forcedVersion != null) { - ModuleDependency(project, configuration, dependency, forcedVersion) - } else { - ModuleDependency(project, configuration, dependency) - } + ModuleDependency(this, configuration, dependency, factualVersion = version) result.add(moduleDependency) } } @@ -146,20 +184,55 @@ private fun Project.depsFromAllConfigurations(): Set { } /** - * Searches for a forced version of given [dependency] in this [Configuration]. + * Returns the versions selected by dependency resolution for this project, keyed + * by the `"group:name"` of each module. + * + * The declared version of a dependency is what the build script *requested*, which + * may differ from what the build *uses*: a `force(...)`, a platform/BOM constraint, + * or Gradle's conflict resolution can all select another version. Reading the + * resolution result captures the selected version, so the report describes the + * dependencies actually on the classpath rather than the requested ones. * - * Returns `null`, if it wasn't forced. + * Only resolvable configurations contribute. When a module resolves to different + * versions across configurations, the newest one (by [VersionComparator]) is kept, + * matching the deduplication applied afterwards. A configuration that fails to + * resolve in isolation is skipped and logged, so the report never breaks the build. */ -private fun Configuration.forcedVersionOf(dependency: Dependency): String? { - val forcedModules = resolutionStrategy.forcedModules - val maybeForced = forcedModules.firstOrNull { - it.group == dependency.group - && it.name == dependency.name - && it.version != null - } - return maybeForced?.version +private fun Project.resolvedVersions(): Map { + // Resolving an individual configuration may fail for reasons unrelated to the + // report — missing repositories for a niche configuration, an unsatisfiable + // constraint, and the like. Such a configuration contributes no versions. + @Suppress("TooGenericExceptionCaught") // Any resolution failure is non-fatal here. + fun componentsOf(configuration: Configuration): Set = + try { + configuration.incoming.resolutionResult.allComponents + } catch (e: Exception) { + logger.info( + "Skipping configuration `${configuration.name}` " + + "while collecting resolved dependency versions.", + e + ) + emptySet() + } + + return configurations + .filter { it.isCanBeResolved } + .flatMap { componentsOf(it) } + .mapNotNull { it.moduleVersion } + .groupBy { moduleKey(it.group, it.name) } + .mapValues { (_, versions) -> versions.maxOfWith(VersionComparator) { it.version } } } +/** + * Builds the `"group:name"` key under which a module's resolved version is recorded + * and looked up. + * + * Forming the key in one place keeps the lookup in [depsFromAllConfigurations] + * consistent with what [resolvedVersions] records and with the grouping done by + * [deduplicate]. + */ +private fun moduleKey(group: String?, name: String): String = "$group:$name" + /** * Tells whether the dependency is an external module dependency. */ @@ -193,7 +266,7 @@ private fun Dependency.isExternal(): Boolean { * The rejected duplicates are logged. */ private fun Project.deduplicate(dependencies: Set): List { - val groups = dependencies.groupBy { it.run { "$group:$name" } } + val groups = dependencies.groupBy { moduleKey(it.group, it.name) } logDuplicates(groups.mapValues { (_, deps) -> deps.distinctBy { it.gav } }) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt index ffb89a26..66e3400e 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,7 @@ import groovy.xml.MarkupBuilder import java.io.StringWriter import kotlin.reflect.KProperty import org.gradle.api.Project -import org.gradle.kotlin.dsl.PropertyDelegate import org.gradle.kotlin.dsl.extra -import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.withGroovyBuilder /** @@ -90,10 +88,10 @@ private fun Project.nonEmptyValue(prop: Any): NonEmptyValue { private class NonEmptyValue( private val defaultValue: String, private val project: Project -) : PropertyDelegate { +) { @Suppress("UNCHECKED_CAST") - override fun getValue(receiver: Any?, property: KProperty<*>): T { + operator fun getValue(receiver: Any?, property: KProperty<*>): T { if (defaultValue.isNotEmpty()) { return defaultValue as T } diff --git a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts deleted file mode 100644 index 7334ef97..00000000 --- a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2026, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import java.io.File -import org.gradle.kotlin.dsl.getValue -import org.gradle.kotlin.dsl.getting -import org.gradle.kotlin.dsl.jacoco -import org.gradle.testing.jacoco.tasks.JacocoReport - -// DEPRECATED: this script plugin distributes vanilla JaCoCo. -// New code should apply `kmp-module`, which configures Kover via -// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at -// `build/reports/kover/report.xml`. (Same task and path as Kotlin-JVM — -// `kmp-module` configures only Kover's `total` report, so no -// `koverXmlReport` task is generated.) The `raise-coverage` skill -// migrates existing consumers automatically. Kept so older consumer repos -// continue to build; will be removed in a future release. -// See: .agents/skills/raise-coverage/references/migrate-to-kover.md - -plugins { - jacoco -} - -logger.warn( - "'jacoco-kmm-jvm' is deprecated; use 'kmp-module' which applies Kover. " + - "See .agents/skills/raise-coverage/references/migrate-to-kover.md." -) - -/** - * Configures [JacocoReport] task to run in a Kotlin KMM project for `commonMain` and `jvmMain` - * source sets. - * - * This script plugin must be applied using the following construct at the end of - * a `build.gradle.kts` file of a module: - * - * ```kotlin - * apply(plugin="jacoco-kmm-jvm") - * ``` - * Please do not apply this script plugin in the `plugins {}` block because `jacocoTestReport` - * task is not yet available at this stage. - */ -@Suppress("unused") -private val about = "" - -/** - * Configure the Jacoco task with custom input a KMM project - * to which this convention plugin is applied. - */ -@Suppress("unused") -val jacocoTestReport: JacocoReport by tasks.getting(JacocoReport::class) { - val buildDir = project.layout.buildDirectory.get().asFile.absolutePath - val classFiles = File("${buildDir}/classes/kotlin/jvm/") - .walkBottomUp() - .toSet() - classDirectories.setFrom(classFiles) - - val coverageSourceDirs = arrayOf( - "src/commonMain", - "src/jvmMain" - ) - sourceDirectories.setFrom(files(coverageSourceDirs)) - - executionData.setFrom(files("${buildDir}/jacoco/jvmTest.exec")) -} diff --git a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts deleted file mode 100644 index 185c9cdf..00000000 --- a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2026, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import io.spine.gradle.buildDirectory - -// DEPRECATED: this script plugin distributes vanilla JaCoCo. -// New code should apply `jvm-module`, which configures Kover via -// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at -// `build/reports/kover/report.xml`. The `raise-coverage` skill migrates -// existing consumers automatically. Kept so older consumer repos continue to -// build; will be removed in a future release. -// See: .agents/skills/raise-coverage/references/migrate-to-kover.md - -plugins { - jacoco -} - -logger.warn( - "'jacoco-kotlin-jvm' is deprecated; use 'jvm-module' which applies Kover. " + - "See .agents/skills/raise-coverage/references/migrate-to-kover.md." -) - -/** - * Configures [JacocoReport] task to run in a Kotlin Multiplatform project for - * `commonMain` and `jvmMain` source sets. - * - * This script plugin must be applied using the following construct at the end of - * a `build.gradle.kts` file of a module: - * - * ```kotlin - * apply(plugin="jacoco-kotlin-jvm") - * ``` - * Please do not apply this script plugin in the `plugins {}` block because `jacocoTestReport` - * task is not yet available at this stage. - */ -@Suppress("unused") -private val about = "" - -/** - * Configure Jacoco task with custom input from this Kotlin Multiplatform project. - */ -@Suppress("unused") -val jacocoTestReport: JacocoReport by tasks.getting(JacocoReport::class) { - - val classFiles = File("$buildDirectory/classes/kotlin/jvm/") - .walkBottomUp() - .toSet() - classDirectories.setFrom(classFiles) - - val coverageSourceDirs = arrayOf( - "src/commonMain", - "src/jvmMain" - ) - sourceDirectories.setFrom(files(coverageSourceDirs)) - - executionData.setFrom(files("$buildDirectory/jacoco/jvmTest.exec")) -} diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts index 3fc3911d..4db37f10 100644 --- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -109,6 +109,7 @@ fun Project.forceConfigurations() { * It configures KMP, in which Kotlin for JVM is only one of * possible targets. */ +@Suppress("UNUSED_VARIABLE") // Avoid warnings for source set vars. kotlin { // Enables explicit API mode for any Kotlin sources within the module. explicitApi() diff --git a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts index 0cace2eb..e59e30ef 100644 --- a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts +++ b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,11 +70,8 @@ publishing { } } -/** - * Declare dependency explicitly to address the Gradle error. - */ -@Suppress("unused") -val publishFatJarPublicationToMavenLocal: Task by tasks.getting { +// Declare dependency explicitly to address the Gradle error. +tasks.getByName("publishFatJarPublicationToMavenLocal") { dependsOn(tasks.shadowJar) } diff --git a/buildSrc/src/main/kotlin/write-manifest.gradle.kts b/buildSrc/src/main/kotlin/write-manifest.gradle.kts index 49130c0c..2c49daa7 100644 --- a/buildSrc/src/main/kotlin/write-manifest.gradle.kts +++ b/buildSrc/src/main/kotlin/write-manifest.gradle.kts @@ -104,7 +104,7 @@ val manifestAttributes = mapOf( * when running tests. We cannot depend on the `Jar` from `resources` because it would * form a circular dependency. */ -val exposeManifestForTests by tasks.registering { +val exposeManifestForTests = tasks.register("exposeManifestForTests") { group = SpineTaskGroup.name description = "Writes a `MANIFEST.MF` to `resources/main` so that it is visible to tests" diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt index e76febe2..dd5e2798 100644 --- a/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt +++ b/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt @@ -64,6 +64,28 @@ internal class VersionGradleFileSpec { VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.043" } + @Test + fun `declared as a literal via 'extra set'`() { + val content = """ + extra.set("versionToPublish", "2.0.0-SNAPSHOT.182") + """.trimIndent() + + VersionGradleFile.keyForValue(content, "2.0.0-SNAPSHOT.182") shouldBe "versionToPublish" + VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.182" + } + + @Test + fun `declared as an alias via 'extra set'`() { + val content = """ + val compilerVersion = "2.0.0-SNAPSHOT.043" + extra.set("compilerVersion", compilerVersion) + extra.set("versionToPublish", compilerVersion) + """.trimIndent() + + VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.043" + VersionGradleFile.valueForKey(content, "compilerVersion") shouldBe "2.0.0-SNAPSHOT.043" + } + @Test fun `identified by the resolved project version, not a hard-coded name`() { val content = """ diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt new file mode 100644 index 00000000..260e852f --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.fs + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`LazyTempPath` should") +class LazyTempPathSpec { + + @Test + fun `create the directory on the first use`() { + val directory = LazyTempPath("created").toFile() + + directory.exists() shouldBe true + directory.isDirectory shouldBe true + } + + @Test + fun `create the directory under the system temporary directory`() { + val path = LazyTempPath("under-tmp").toString() + + path shouldContain systemTempDir() + } + + @Test + fun `create the directory under a folder named after its package`() { + val path = LazyTempPath("under-base").toString() + + path shouldContain LazyTempPath::class.java.packageName + } + + @Test + fun `place all instances under the same base directory`() { + val first = LazyTempPath("first").toFile() + val second = LazyTempPath("second").toFile() + + first.parentFile shouldBe second.parentFile + first.parentFile.toString() shouldBe SpineTempDir.path.toString() + } +} + +private fun systemTempDir(): String = System.getProperty("java.io.tmpdir") diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt new file mode 100644 index 00000000..8bf42500 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.fs + +import io.kotest.matchers.shouldBe +import java.nio.file.Path +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`SpineTempDir` should") +class SpineTempDirSpec { + + @Test + fun `place its per-JVM directory under the package-named namespace`() { + val namespace = Path.of( + System.getProperty("java.io.tmpdir"), + LazyTempPath::class.java.packageName + ) + + SpineTempDir.path.parent shouldBe namespace + } + + @Test + fun `create the directory on access`() { + val directory = SpineTempDir.path.toFile() + + directory.exists() shouldBe true + directory.isDirectory shouldBe true + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt index 0c4b2335..baec7877 100644 --- a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt +++ b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt @@ -31,12 +31,16 @@ import io.kotest.matchers.ints.shouldBeLessThan import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldNotContain +import java.io.File import java.io.StringWriter +import org.gradle.api.Action import org.gradle.api.Project +import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir @DisplayName("`DependencyWriter` should") internal class DependencyWriterSpec { @@ -246,6 +250,112 @@ internal class DependencyWriterSpec { } } + @Nested inner class + `report the version selected by dependency resolution` { + + /** + * A `force(...)` pins an artifact to a version that is *older* than one of + * the declared ones. The report must show the resolved version — the one + * actually on the classpath — and not the newest of the declared ones, + * which the deduplication would otherwise pick. + */ + @Test + fun `preferring it over a newer declared version`() { + val older = "$VALIDATION_RUNTIME:2.0.0-SNAPSHOT.40" + val newer = "$VALIDATION_RUNTIME:2.0.0-SNAPSHOT.61" + subproject("a-text").declare("implementation", newer) + subproject("b-text").declare("implementation", older) + + val resolved = mapOf(VALIDATION_RUNTIME to "2.0.0-SNAPSHOT.40") + val dependency = rootProject.dependencies(resolved).single() + + dependency.dependency().version shouldBe "2.0.0-SNAPSHOT.40" + } + + /** + * Two versions of the same artifact declared in a single module and + * configuration — the case that used to log a spurious "several versions" + * warning — collapse to the single resolved version. + */ + @Test + fun `collapsing several declarations within one configuration`() { + val text = subproject("text") + text.declare("implementation", "$VALIDATION_RUNTIME:2.0.0-SNAPSHOT.61") + text.declare("implementation", "$VALIDATION_RUNTIME:2.0.0-SNAPSHOT.40") + + val resolved = mapOf(VALIDATION_RUNTIME to "2.0.0-SNAPSHOT.61") + val dependency = rootProject.dependencies(resolved).single() + + dependency.dependency().version shouldBe "2.0.0-SNAPSHOT.61" + } + + @Test + fun `falling back to the declared version when it is not resolved`() { + subproject("lib").declare("api", SPINE_BASE) + + val dependency = rootProject.dependencies(emptyMap()).single() + + dependency.dependency().version shouldBe "2.0.0" + } + } + + @Nested inner class + `read the version from a resolved configuration` { + + /** + * Drives the real (non-injected) `dependencies()` entry point against an + * actually resolved configuration. A module is declared at one version but + * `force`d to an older one; the report must show the forced, i.e. resolved, + * version. The forcing is observable only through resolution, so the module + * is resolved from a local repository of metadata-only POMs. + */ + @Test + fun `honoring a forced version over the declared one`(@TempDir repoDir: File) { + val group = "io.spine.validation" + val name = "spine-validation-java-runtime" + publishPom(repoDir, group, name, "1.0.40") + publishPom(repoDir, group, name, "1.0.61") + + val text = subproject("text") + text.addMavenRepository(repoDir) + val api = text.configurations.create("api") + api.isCanBeResolved = true + api.resolutionStrategy.force("$group:$name:1.0.40") + text.dependencies.add("api", "$group:$name:1.0.61") + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "1.0.40" + } + + /** Writes a metadata-only Maven POM for the module under [repoDir]. */ + private fun publishPom(repoDir: File, group: String, name: String, version: String) { + val dir = File(repoDir, "${group.replace('.', '/')}/$name/$version") + dir.mkdirs() + File(dir, "$name-$version.pom").writeText( + """ + + 4.0.0 + $group + $name + $version + + """.trimIndent() + ) + } + + private fun Project.addMavenRepository(dir: File) { + // The `org.gradle.kotlin.dsl` `maven { }` accessors are not on the + // `buildSrc` test compile classpath, so the core `Action` overload is + // used directly rather than the DSL lambda. + repositories.maven(object : Action { + override fun execute(repository: MavenArtifactRepository) { + repository.setUrl(dir.toURI()) + } + }) + } + } + @Test fun `omit the scope of a dependency coming only from an unknown configuration`() { subproject("lib").declare("spineCompiler", SPINE_BASE) @@ -308,5 +418,8 @@ internal class DependencyWriterSpec { private companion object { const val SPINE_BASE = "io.spine:spine-base:2.0.0" const val SPINE_BASE_NEWER = "io.spine:spine-base:2.0.1" + + /** The `"group:name"` of the validation runtime artifact, without a version. */ + const val VALIDATION_RUNTIME = "io.spine.validation:spine-validation-java-runtime" } } diff --git a/config b/config index 3f13608b..c2aae6e7 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 3f13608b3bb1a8bc736fbb748b95ec3dc11843a2 +Subproject commit c2aae6e74662ddbe60e3e8695cc6a1b8b104f0a3 From a768955ba2e42e9939aac73efb0d755029490fef Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 23:33:53 +0100 Subject: [PATCH 11/14] Update dependency reports --- docs/dependencies/dependencies.md | 36 +++++++++++++++---------------- docs/dependencies/pom.xml | 5 ++++- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 03143968..e3e96143 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -441,7 +441,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -1240,7 +1240,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:03 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -2094,7 +2094,7 @@ This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -2932,7 +2932,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -3786,7 +3786,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -4692,7 +4692,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -5590,7 +5590,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -6508,7 +6508,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -7414,7 +7414,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -8324,7 +8324,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -9206,7 +9206,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -10013,7 +10013,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -10832,7 +10832,7 @@ This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:03 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -11739,7 +11739,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:03 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -13157,7 +13157,7 @@ This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -14015,7 +14015,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -14921,7 +14921,7 @@ This report was generated on **Thu Jul 02 12:40:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:02 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -15759,6 +15759,6 @@ This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Jul 02 12:40:53 WEST 2026** using +This report was generated on **Thu Jul 02 23:33:03 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 91fc404e..464f3a79 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -62,6 +62,7 @@ all modules and does not describe the project structure per-subproject. io.grpc grpc-api + 1.81.0 compile @@ -157,7 +158,7 @@ all modules and does not describe the project structure per-subproject. com.google.errorprone error_prone_annotations - 2.36.0 + 2.47.0 provided @@ -363,10 +364,12 @@ all modules and does not describe the project structure per-subproject. org.jetbrains.kotlin kotlin-test-annotations-common + 2.3.21 org.jetbrains.kotlin kotlin-test-common + 2.3.21 org.jetbrains.kotlinx From d79e1d357f56d54f2a0256faa18419e8d88a2afb Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 2 Jul 2026 23:45:55 +0100 Subject: [PATCH 12/14] Rename `prepare` to `completeData` in `AbstractLogger.write` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new name states what the callback is for — completing the log data within the recursion guard — instead of describing when it runs. Co-Authored-By: Claude Fable 5 --- .../kotlin/io/spine/logging/AbstractLogger.kt | 12 ++++++------ .../commonMain/kotlin/io/spine/logging/LogContext.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt index c08a8ead..58e9e7b7 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt @@ -215,21 +215,21 @@ public abstract class AbstractLogger> protected constructo * logging if it detects significant recursion has occurred. * * @param data The log statement data. - * @param prepare Completes [data] within the recursion guard right before the backend call. - * Use it for work that may invoke user code, such as computing a lazy log message, - * so that any exceptions or reentrant logging it triggers are handled the same way - * as those coming from the backend. + * @param completeData Completes [data] within the recursion guard right before + * the backend call. Use it for work that may invoke user code, such as computing + * a lazy log message, so that any exceptions or reentrant logging it triggers are + * handled the same way as those coming from the backend. */ @JvmOverloads @Suppress("TooGenericExceptionCaught") - public fun write(data: LogData, prepare: () -> Unit = {}) { + public fun write(data: LogData, completeData: () -> Unit = {}) { // Note: Recursion checking should not be in the `LoggerBackend`. // There are many backends and they can call into other backends. // We only want the counter incremented per log statement. try { RecursionDepth.enterLogStatement().use { depth -> if (depth.getValue() <= MAX_ALLOWED_RECURSION_DEPTH) { - prepare() + completeData() backend.log(data) } else { reportError( diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt index ad3cf60b..34ad5179 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt @@ -429,7 +429,7 @@ protected constructor( addMetadata(Key.TAGS, finalTags) } // Pass the log data to the backend (it must not be modified once the message - // is completed by the `prepare` block below). + // is completed by the `completeData` block below). getLogger().write(this) { // A `null` message is passed to the backend unmodified. literalArg = message()?.let { (loggingDomain?.messagePrefix ?: "") + it } From d48b5bad90ce98014596ad580976dc1d206654d4 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 3 Jul 2026 00:22:34 +0100 Subject: [PATCH 13/14] Deprecate the `call(Runnable)` extension The `ScopedLoggingContext.Builder.run(Runnable)` member introduced on this branch supersedes the jvmMain extension: both spellings wrap and run the runnable within a new context. Keep the extension deprecated with a `ReplaceWith` for one release cycle instead of removing it, since it is published API. Co-Authored-By: Claude Fable 5 --- .../io/spine/logging/context/ScopedLoggingContextExts.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/context/ScopedLoggingContextExts.kt b/logging/src/jvmMain/kotlin/io/spine/logging/context/ScopedLoggingContextExts.kt index 4b0bf66d..bbf108b6 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/context/ScopedLoggingContextExts.kt +++ b/logging/src/jvmMain/kotlin/io/spine/logging/context/ScopedLoggingContextExts.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,10 @@ package io.spine.logging.context /** * Runs a runnable directly within a new context installed from this builder. */ +@Deprecated( + "Use the `run` member function instead.", + ReplaceWith("run(r)") +) public fun ScopedLoggingContext.Builder.call(r: Runnable) { wrap(r).run() } From 178a0f8f180aa7217f3f0bdeb6bd5c591f6c2ac6 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 3 Jul 2026 00:24:55 +0100 Subject: [PATCH 14/14] Use a genuinely custom level in the `Level` KDoc example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Adding custom levels" example illustrated a custom `CONFIG` at 700 with an `atConfig()` extension — both of which now exist as built-ins (`Level.CONFIG` and `AbstractLogger.atConfig()`). Illustrate with a syslog-style `NOTICE` level instead, which the predefined set genuinely lacks. Also fix the "thew" typo in the same sentence. Co-Authored-By: Claude Fable 5 --- logging/src/commonMain/kotlin/io/spine/logging/Level.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/Level.kt b/logging/src/commonMain/kotlin/io/spine/logging/Level.kt index 4b618b6f..ffaa1f46 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/Level.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/Level.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,14 +38,14 @@ package io.spine.logging * * ```kotlin * public object MyLoggingLevels { - * public val CONFIG: Level = Level("CONFIG", 700) + * public val NOTICE: Level = Level("NOTICE", 850) * ... * } * ``` * You may also want to add an extension function for the [Logger] class to - * use thew new level: + * use the new level: * ``` - * public fun > Logger.atConfig(): API = at(MyLoggingLevels.CONFIG) + * public fun > Logger.atNotice(): API = at(MyLoggingLevels.NOTICE) * ``` * If the new logging level needs to be converted to a level of underlying logging backend * a [LevelConverter] must be [registered][LevelConverter.register] prior to