From c8e5afe16707bd955fbc80c6aa0859be14d1cec8 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 19:12:05 +0100 Subject: [PATCH 01/10] Bump version -> `2.0.0-SNAPSHOT.466` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 4149a1db12..25631b4e07 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of the Validation library to publish. */ -extra.set("validationVersion", "2.0.0-SNAPSHOT.465") +extra.set("validationVersion", "2.0.0-SNAPSHOT.466") From 84ffee9371ebbd11d43a404eba1e92214a38eb3f Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 20:18:31 +0100 Subject: [PATCH 02/10] Write Validation settings before `LaunchSpineCompiler` runs The renderer settings were written in a `doFirst` action of every `LaunchSpineCompiler`, after Gradle had fingerprinted the settings directory. A fresh daemon then saw the file as added and reran the task, a cache hit skipped the action, and a changed DSL value left the task up-to-date with stale settings. A new `writeValidationSettings` task writes the file. It takes the DSL value as its input and the file as its only output, runs after `clean`, and every `LaunchSpineCompiler` depends on it. Co-Authored-By: Claude Opus 5.5 --- .agents/tasks/up-to-date-compiler-tasks.md | 127 ++++++++++++++++ .../gradle/ValidationGradlePlugin.kt | 78 ++++------ .../gradle/WriteValidationSettings.kt | 131 ++++++++++++++++ .../gradle/ValidationGradlePluginSpec.kt | 142 +++++++++++++++++- .../gradle/WriteValidationSettingsSpec.kt | 107 +++++++++++++ 5 files changed, 532 insertions(+), 53 deletions(-) create mode 100644 .agents/tasks/up-to-date-compiler-tasks.md create mode 100644 gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt create mode 100644 gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt diff --git a/.agents/tasks/up-to-date-compiler-tasks.md b/.agents/tasks/up-to-date-compiler-tasks.md new file mode 100644 index 0000000000..c4aa9af1c0 --- /dev/null +++ b/.agents/tasks/up-to-date-compiler-tasks.md @@ -0,0 +1,127 @@ +--- +slug: up-to-date-compiler-tasks +branch: write-settings-before-launch (Change 1) +owner: claude +status: in-progress +started: 2026-10-19 +--- + +## Goal + +`LaunchSpineCompiler` stays `UP-TO-DATE` when nothing it depends on has changed, +also in a fresh Gradle daemon and after `clean`. It reruns when a settings DSL +value changes. Two runs over the same inputs produce byte-identical sources. + +## Context + +Follow-ups from SpineEventEngine/compiler#94 ("Stop the integration-test build +from recompiling the root modules"). + +Findings (verified 2026-10-19 on Gradle 9.6.1): + +- `LaunchSpineCompiler.settingsDirectory` (`build/spine/compiler/settings`) is an + `@InputFiles` property shared by all source sets of a project. The `compiler` + Gradle API has no way for other plugins to contribute settings; they write + files there directly. +- Validation writes `JavaValidationRenderer.pb.json` in a `doFirst` of every + `LaunchSpineCompiler`, i.e. after Gradle has fingerprinted the inputs. + In `core-jvm-compiler :annotation`: + - same daemon: next run `UP-TO-DATE` (the daemon's VFS keeps the snapshot + taken before `doFirst`, which hides the problem); + - fresh daemon: reruns with "file …JavaValidationRenderer.pb.json has been added"; + - after `clean`: the cache key holds the 7 CoreJvm files only; `FROM-CACHE` + skips `doFirst`, so the renderer settings never affect the cache key. + - Changing the DSL without `clean` leaves the task `UP-TO-DATE` with stale + settings (minimal repro). +- CoreJvm's `writeSpineCompilerPluginsSettings` declares the whole settings + directory as `@OutputDirectory` and its DSL options as `@Internal`. With no + inputs it stays `UP-TO-DATE` when the DSL changes (minimal repro). +- Generated sources are non-deterministic for two reasons: + 1. `mangled()` appends `SecureRandomString` suffixes + (`PatternGenerator`, `RequireOptionGenerator`). + 2. Generators iterate `select().all()`. `core-jvm`'s `TenantRecords` + stores records in a `HashMap` keyed by proto IDs, whose `hashCode()` mixes + in the descriptor's identity hash. In `compiler :api`, 35 of 39 differing + files differ only by the order of constraint blocks. +- `Pipeline.generateId()` (compiler) also uses `SecureRandomString`, but the ID + never reaches the generated output. Out of scope. + +## Plan + +### Change 1 — `validation/gradle-plugin`: write settings before fingerprinting +- [x] Add a `WriteValidationSettings` task (`writeValidationSettings`): + - `@Input Property` lazily wired to + `validation.java.suppressWarnings.unsignedFields`; + - `@OutputFile` = `/settings/io.spine.tools.validation.java.JavaValidationRenderer.pb.json`; + - builds `JavaValidationRendererSettings` and serializes it in `@TaskAction` + (the JSON printer needs the plugin context class loader); + - `@DisableCachingByDefault`; `mustRunAfter(clean)`. +- [x] Replace the `doFirst` with `dependsOn(writer)` on every `LaunchSpineCompiler` + (still only when `validation.enabled`). +- [x] Update the KDoc that explains the `doFirst` choice, and the + `ValidationGradlePluginSpec` class KDoc. +- [x] `ProjectBuilder` specs: the task is registered, wired to the launch tasks, + and its input reflects the DSL value. +- [x] Version bump. + +### Change 2 — `validation/java`: deterministic generated code +- [ ] `mangled()`: derive the suffix from SHA-256 of the identifier (JDK + `MessageDigest`, base64url, Java-identifier chars only, same length). Keep the + signature; KDoc states that the same identifier yields the same name. +- [ ] Sort the views each generator selects by declaration order + (`Field.orderOfDeclaration`; oneof order for `(choice)`), including + `SetOnceRenderer`. +- [ ] Specs for `mangled()` (deterministic, valid identifier, distinct inputs → + distinct names) and for the declaration order of generated constraints. +- [ ] Drop the `SecureRandomString` import from Validation. +- [ ] Version bump (or share the bump if Change 1 and 2 land in one branch). + +### Change 3 — `core-jvm-compiler/gradle-plugin`: settings writer picks up DSL changes +(after Validation with Changes 1–2 is published) +- [ ] `WriteCompilerPluginsSettings`: declare the exact `@OutputFiles` instead of + the whole directory, so it no longer overlaps Validation's file. +- [ ] Make the task always run (`outputs.upToDateWhen { false }`). Writing is + cheap, and `LaunchSpineCompiler` fingerprints content, so it still stays + `UP-TO-DATE` when the content is unchanged (verified in a minimal repro). + Modelling every DSL option as `@Input` was rejected: proto-valued options would + need a deterministic serialization at fingerprinting time. +- [ ] `mustRunAfter(clean)`. +- [ ] Bump the Validation dependency. + - Released CoreJvm (`.097`) cannot run with Validation `.466` yet: the Compiler + fat CLI it uses bundles the Protobuf `4.36.0` runtime, while Validation's + gencode is `4.36.2` (`ProtobufRuntimeVersionException` on loading + `JavaValidationRendererSettings`). Rebuilding CoreJvm against `.466` fixes it. +- [ ] TestKit regression in `CoreJvmPluginIgTest`: run `launchSpineCompiler` + twice with `--no-watch-fs` (so the VFS cannot hide the problem) and expect + `UP-TO-DATE`; change a CoreJvm and a Validation DSL value and expect a rerun + with the new settings; `clean` + run + run → `UP-TO-DATE`. +- [ ] Version bump. + +### Verification (each change) +- [ ] `./gradlew build` (and `dokkaGenerate` for code changes). +- [ ] In a consumer: `launchSpineCompiler`, `./gradlew --stop`, + `launchSpineCompiler --info` → `UP-TO-DATE`. +- [ ] Determinism: `:api:launchSpineCompiler --rerun` twice in `compiler`, + `diff -r` of `api/generated` → no differences. + +### Follow-ups (not in this plan) +- A settings API in `compiler` (one writer, `@Input` map consumer ID → content), + replacing per-plugin writers. +- Deterministic `select().all()` ordering in `compiler`/`core-jvm` for all + Compiler plugins. + +## Log +- 2026-10-19 — analysis done; scope chosen: Validation + CoreJvm writer; plan drafted, awaiting approval. +- 2026-10-19 — approved; Change 1 started on `write-settings-before-launch` (based on `origin/master`, version `.465`). +- 2026-10-19 — Change 1 implemented (uncommitted, except the `.466` bump commit). `./gradlew build` and `:gradle-plugin:dokkaGenerate` pass. A standalone consumer of the published `.466` plugin stays `UP-TO-DATE` across `--no-daemon` runs, reruns when the DSL value changes, and works after `clean`; on `.465` the second run reruns with "…JavaValidationRenderer.pb.json has been added". `:docs:embedCode` can't run here (x86-64 binary, no Rosetta); `build-and-release.md` updated by hand. +- 2026-10-19 — reviews (Kotlin, Spine rules, Gradle, docs) addressed: the task owns the + consumer ID and file name, takes an `@Internal` settings directory (set lazily) and + derives its `@OutputFile`; properties are `internal`; a spec pins the `@Input`/`@OutputFile` + declarations. With CoreJvm `.097` + Validation `.466`, both writers coexist with + `processResources`/`sourcesJar` (no overlap or implicit-dependency warnings, fresh rerun + `UP-TO-DATE`, `clean` OK); the compiler run itself is blocked by the Protobuf mismatch above. + The automated end-to-end (TestKit) check stays in Change 3. +- 2026-10-19 — session resumed; `./gradlew build` re-run green on the final sources. Change 1 + committed (code + specs, then reports + docs). The `config`, `docs/_time`, and `docs/_examples` + submodule pointers drifted during the session and stay uncommitted: `docs/_examples` was + rewound to its `master` tip, behind the recorded `align-protobuf-runtime` commit. diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt index 8324b8cc51..ed5c637743 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt @@ -27,23 +27,21 @@ package io.spine.tools.validation.gradle import com.google.protobuf.gradle.ProtobufPlugin -import io.spine.format.Format import io.spine.tools.compiler.gradle.api.addUserClasspathDependency import io.spine.tools.compiler.gradle.api.compilerSettings import io.spine.tools.compiler.gradle.api.compilerWorkingDir import io.spine.tools.compiler.gradle.plugin.Extension import io.spine.tools.compiler.gradle.plugin.LaunchSpineCompiler -import io.spine.tools.compiler.params.WorkingDirectory +import io.spine.tools.compiler.params.Directories.SETTINGS_SUBDIR import io.spine.tools.gradle.DslSpec import io.spine.tools.gradle.lib.LibraryPlugin import io.spine.tools.gradle.lib.spineExtension +import io.spine.tools.gradle.task.SpineTaskGroup import io.spine.tools.meta.MavenArtifact -import io.spine.tools.validation.settings.JavaValidationRendererSettings -import io.spine.tools.validation.settings.javaValidationRendererSettings -import io.spine.tools.validation.settings.suppressWarnings -import io.spine.type.toJson import org.gradle.api.Project import org.gradle.api.artifacts.Dependency +import org.gradle.api.plugins.BasePlugin.CLEAN_TASK_NAME +import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.apply import io.spine.tools.compiler.gradle.plugin.Plugin as CompilerGradlePlugin @@ -79,16 +77,16 @@ private fun Project.configureValidation() { val ordered = listOf(ValidationSdk.javaCompilerPlugin) + plugins.get() plugins.set(ordered) } - // Register the settings write as a `doFirst` action on every - // `LaunchSpineCompiler` task rather than writing during `afterEvaluate`. - // Writing at configuration time is not enough: a preceding `clean` - // task (which runs during execution) deletes `build/` — including - // the settings file — before the compiler task can read it. - val theProject = this + // Write the settings in a dedicated task rather than in an action of + // `LaunchSpineCompiler`. Gradle fingerprints the inputs of a task before + // running any of its actions, so a file written by such an action is + // missing from the fingerprint, and `LaunchSpineCompiler` is out of date + // on the next build. + // The dependency is explicit because the settings directory input of + // `LaunchSpineCompiler` does not carry the tasks that write to it. + val writeSettings = registerWriteSettingsTask() tasks.withType(LaunchSpineCompiler::class.java).configureEach { task -> - task.doFirst { - theProject.writeJavaValidationRendererSettings() - } + task.dependsOn(writeSettings) } } } @@ -104,40 +102,26 @@ private fun Project.configureValidation() { } /** - * Writes the [JavaValidationRendererSettings] built from the - * `validation { java { ... } }` DSL block as a settings file for the - * `io.spine.tools.validation.java.JavaValidationRenderer` consumer. - * - * Called as a `doFirst` action on every [LaunchSpineCompiler] task so that - * the file is written during task execution — after any preceding `clean` task - * has finished deleting the build directory — and is guaranteed to be present - * when the Spine Compiler subprocess reads the settings directory. + * Registers the [WriteValidationSettings] task, which writes the settings file + * for the `io.spine.tools.validation.java.JavaValidationRenderer` consumer + * into the settings directory of the Spine Compiler. */ -private fun Project.writeJavaValidationRendererSettings() { - val suppress = validationExtension.java.suppressWarnings - val message = javaValidationRendererSettings { - suppressWarnings = suppressWarnings { - unsignedFields = suppress.unsignedFields.get() - } +private fun Project.registerWriteSettingsTask(): TaskProvider = + tasks.register(WriteValidationSettings.NAME, WriteValidationSettings::class.java) { task -> + task.group = SpineTaskGroup.name + task.description = "Writes the settings of the Validation Compiler" + + " for the Spine Compiler" + task.suppressUnsignedFieldsWarnings.set( + validationExtension.java.suppressWarnings.unsignedFields + ) + // Obtain the directory lazily so that it reflects the final build directory. + task.settingsDirectory.fileProvider(provider { + compilerWorkingDir.dir(SETTINGS_SUBDIR).asFile + }) + // `clean` deletes the build directory, which holds the settings file, + // so the file must be written after `clean`. + task.mustRunAfter(CLEAN_TASK_NAME) } - val workingDir = WorkingDirectory(compilerWorkingDir.asFile.toPath()) - workingDir.settingsDirectory.write( - VALIDATION_RENDERER_CONSUMER_ID, - Format.ProtoJson, - message.toJson() - ) -} - -/** - * Canonical class name of the Spine Compiler renderer that reads - * [JavaValidationRendererSettings] via `LoadsSettings`. - * - * Kept as a string literal because the Validation Gradle plugin does not - * depend on the `:java` module that hosts the renderer class — only on the - * `:java-settings` module that hosts the proto type. - */ -private const val VALIDATION_RENDERER_CONSUMER_ID: String = - "io.spine.tools.validation.java.JavaValidationRenderer" /** * The extension added by [ValidationGradlePlugin]. diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt new file mode 100644 index 0000000000..1f28fa4e7b --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt @@ -0,0 +1,131 @@ +/* + * 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.tools.validation.gradle + +import io.spine.format.Format +import io.spine.tools.validation.settings.JavaValidationRendererSettings +import io.spine.tools.validation.settings.javaValidationRendererSettings +import io.spine.tools.validation.settings.suppressWarnings +import io.spine.type.toJson +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** + * Writes the [JavaValidationRendererSettings] built from the + * `validation { java { ... } }` DSL block as a settings file for the + * `io.spine.tools.validation.java.JavaValidationRenderer` consumer. + * + * The Validation Gradle plugin registers this task when validation is enabled + * in the project, and makes every `LaunchSpineCompiler` task depend on it. + * As a result, the settings file is in place when Gradle fingerprints + * the settings directory, which is an input of `LaunchSpineCompiler`. + * This keeps `LaunchSpineCompiler` up-to-date until the settings or its other + * inputs change, and makes the settings a part of its build cache key. + * + * The DSL values are the inputs of this task. Changing a value makes the task + * rewrite the file, which, in turn, makes `LaunchSpineCompiler` out of date. + * When the file is deleted (for example, by `clean`), the task writes it again. + * + * The task declares only the file it writes as its output. Other plugins write + * their settings files into the same directory. For example, the CoreJvm Compiler + * declares the whole directory as an output of its own settings task. + */ +@DisableCachingByDefault(because = "Writing a single small file is as fast as loading it.") +public abstract class WriteValidationSettings : DefaultTask() { + + /** + * Tells if the "unsigned integer types are not supported in Java" warning + * is suppressed. + * + * @see ValidationExtension.Java.SuppressWarnings.unsignedFields + */ + @get:Input + internal abstract val suppressUnsignedFieldsWarnings: Property + + /** + * The settings directory of the Spine Compiler. + * + * The property is [Internal] because the task does not read the directory. + * The task output is the [settingsFile] in this directory. + */ + @get:Internal + internal abstract val settingsDirectory: DirectoryProperty + + /** + * The settings file to write. + */ + @get:OutputFile + internal val settingsFile: Provider = settingsDirectory.file(SETTINGS_FILE) + + /** + * Writes the settings file in the [ProtoJson][Format.ProtoJson] format. + */ + @TaskAction + public fun write() { + val settings = javaValidationRendererSettings { + suppressWarnings = suppressWarnings { + unsignedFields = suppressUnsignedFieldsWarnings.get() + } + } + settingsFile.get().asFile.writeText(settings.toJson()) + } + + public companion object { + + /** + * The name of the task. + */ + public const val NAME: String = "writeValidationSettings" + + /** + * Canonical class name of the Spine Compiler renderer that reads + * [JavaValidationRendererSettings] via `LoadsSettings`. + * + * Kept as a string literal because the Validation Gradle plugin does not + * depend on the `:java` module that hosts the renderer class — only on the + * `:java-settings` module that hosts the proto type. + */ + internal const val CONSUMER_ID: String = + "io.spine.tools.validation.java.JavaValidationRenderer" + + /** + * The name of the settings file. + * + * Follows the naming of `SettingsDirectory.write()`: the [ID of the consumer][CONSUMER_ID], + * a dot, and the primary extension of the [ProtoJson][Format.ProtoJson] format. + */ + internal val SETTINGS_FILE: String = "$CONSUMER_ID.${Format.ProtoJson.extension}" + } +} diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt index 913e298463..5cb19e240a 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt @@ -27,24 +27,45 @@ package io.spine.tools.validation.gradle import com.google.protobuf.gradle.ProtobufPlugin +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.spine.format.Format +import io.spine.tools.compiler.gradle.api.compilerWorkingDir +import io.spine.tools.compiler.gradle.plugin.LaunchSpineCompiler +import io.spine.tools.compiler.params.WorkingDirectory import io.spine.tools.gradle.lib.spineExtension import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.api.plugins.BasePlugin.CLEAN_TASK_NAME +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.getByName +import org.gradle.kotlin.dsl.withType import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import io.spine.tools.compiler.gradle.plugin.Plugin as CompilerGradlePlugin /** - * Verifies the eager configuration performed by [ValidationGradlePlugin] when - * it is applied to a project: registering the `validation` extension, applying - * the Protobuf and Spine Compiler Gradle plugins on the user's behalf, and - * adding the validation runtime as an `implementation` dependency. + * Verifies the configuration performed by [ValidationGradlePlugin]. * - * The plugin's `afterEvaluate` and per-task `doFirst` actions run only during a - * real build and are covered by the integration projects under `:tests`. + * When the plugin is applied to a project, it registers the `validation` extension, + * applies the Protobuf and Spine Compiler Gradle plugins on the user's behalf, and + * adds the validation runtime as an `implementation` dependency. + * + * When the project is evaluated with validation enabled, the plugin registers + * the [WriteValidationSettings] task and makes the [LaunchSpineCompiler] tasks + * depend on it. The tests evaluate the project explicitly because [ProjectBuilder] + * does not run the `afterEvaluate` actions on its own. The tests do not run + * the Spine Compiler. */ @DisplayName("`ValidationGradlePlugin` should") internal class ValidationGradlePluginSpec { @@ -83,4 +104,113 @@ internal class ValidationGradlePluginSpec { } hasRuntime shouldBe true } + + @Nested inner class + `when the project is evaluated` { + + @Test + fun `register the task writing the settings`() { + evaluate() + + project.tasks.findByName(WriteValidationSettings.NAME) + .shouldNotBeNull() + } + + @Test + fun `make every 'LaunchSpineCompiler' task depend on writing the settings`() { + evaluate() + + val launchTasks = project.tasks.withType() + launchTasks.shouldNotBeEmpty() + launchTasks.forEach { task -> + task.dependencyNames() shouldContain WriteValidationSettings.NAME + } + } + + @Test + fun `pass the DSL value to the task as input`() { + evaluate() + + writeSettingsTask().suppressUnsignedFieldsWarnings.get() shouldBe false + project.spineExtension() + .java.suppressWarnings.unsignedFields.set(true) + writeSettingsTask().suppressUnsignedFieldsWarnings.get() shouldBe true + } + + @Test + fun `declare the DSL value as input and the settings file as output`() { + evaluate() + + val task = writeSettingsTask() + val inputs = task.inputs.properties.filterKeys { + it.startsWith("suppressUnsignedFieldsWarnings") + } + inputs.values.shouldContainExactly(false) + task.inputs.files.files.shouldBeEmpty() + task.outputs.files.singleFile shouldBe task.settingsFile.get().asFile + } + + @Test + fun `point the settings file at the path read by the Spine Compiler`() { + evaluate() + val settingsDirectory = + WorkingDirectory(project.compilerWorkingDir.asFile.toPath()).settingsDirectory + settingsDirectory.write(RENDERER_CONSUMER_ID, Format.ProtoJson, "{}") + + writeSettingsTask().settingsFile.get().asFile.exists() shouldBe true + } + + @Test + fun `run after 'clean'`() { + evaluate() + + val task = writeSettingsTask() + val clean = project.tasks.getByName(CLEAN_TASK_NAME) + task.mustRunAfter.getDependencies(task) shouldContain clean + } + + @Test + fun `not write the settings when validation is disabled`() { + project.spineExtension().enabled.set(false) + evaluate() + + project.tasks.findByName(WriteValidationSettings.NAME).shouldBeNull() + val launchTasks = project.tasks.withType() + launchTasks.shouldNotBeEmpty() + launchTasks.forEach { task -> + task.dependencyNames() shouldNotContain WriteValidationSettings.NAME + } + } + + private fun evaluate() { + // The `DescriptorSetFilePlugin` applied by the Spine Compiler plugin + // after evaluation requires a non-empty `group`. + project.group = "io.spine.validation.test" + (project as ProjectInternal).evaluate() + } + + private fun writeSettingsTask(): WriteValidationSettings = + project.tasks.getByName(WriteValidationSettings.NAME) + + /** + * Obtains the names of the tasks that this task depends on + * via [TaskProvider]s. + * + * Inspects [Task.dependsOn] rather than resolving the task dependencies, + * which would resolve the configurations of the Spine Compiler. + */ + private fun Task.dependencyNames(): List = + dependsOn.filterIsInstance>().map { it.name } + } + + private companion object { + + /** + * The ID of the consumer of the settings written by [WriteValidationSettings]. + * + * Repeats the canonical name of `JavaValidationRenderer`, which loads + * the settings, to lock the name of the settings file. + */ + const val RENDERER_CONSUMER_ID = "io.spine.tools.validation.java.JavaValidationRenderer" + } } diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt new file mode 100644 index 0000000000..7840c8bb1f --- /dev/null +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt @@ -0,0 +1,107 @@ +/* + * 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.tools.validation.gradle + +import io.kotest.matchers.shouldBe +import io.spine.tools.validation.settings.javaValidationRendererSettings +import io.spine.tools.validation.settings.suppressWarnings +import io.spine.type.toJson +import java.io.File +import org.gradle.api.Project +import org.gradle.kotlin.dsl.register +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * Verifies the action of [WriteValidationSettings]. + * + * The registration of the task by [ValidationGradlePlugin] is verified by + * [ValidationGradlePluginSpec]. + */ +@DisplayName("`WriteValidationSettings` should") +internal class WriteValidationSettingsSpec { + + private lateinit var project: Project + private lateinit var outputFile: File + + @BeforeEach + fun setUp(@TempDir projectDir: File) { + project = ProjectBuilder.builder() + .withProjectDir(projectDir) + .build() + outputFile = projectDir.resolve("settings").resolve(WriteValidationSettings.SETTINGS_FILE) + } + + @Test + fun `write the suppressed warnings`() { + write(suppressUnsignedFieldsWarnings = true) + + outputFile.readText() shouldBe expectedJson(unsignedFields = true) + } + + @Test + fun `write the warnings that are not suppressed`() { + write(suppressUnsignedFieldsWarnings = false) + + outputFile.readText() shouldBe expectedJson(unsignedFields = false) + } + + @Test + fun `overwrite the file written before`() { + write(suppressUnsignedFieldsWarnings = true) + write(suppressUnsignedFieldsWarnings = false) + + outputFile.readText() shouldBe expectedJson(unsignedFields = false) + } + + /** + * Runs the action of a newly registered task with the given input. + * + * Gradle creates the parent directories of a task's `@OutputFile` before + * running the task. This function does the same because it calls the task + * action directly. + */ + private fun write(suppressUnsignedFieldsWarnings: Boolean) { + val name = "write${project.tasks.size}" + val task = project.tasks.register(name) { + this.suppressUnsignedFieldsWarnings.set(suppressUnsignedFieldsWarnings) + settingsDirectory.set(outputFile.parentFile) + }.get() + outputFile.parentFile.mkdirs() + task.write() + } + + private fun expectedJson(unsignedFields: Boolean): String = + javaValidationRendererSettings { + suppressWarnings = suppressWarnings { + this.unsignedFields = unsignedFields + } + }.toJson() +} From a8f3d43107388e7300ebbde95a8ef707588d8c86 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 20:18:36 +0100 Subject: [PATCH 03/10] Update dependency reports and docs Co-Authored-By: Claude Opus 5.5 --- .../validation/developer/build-and-release.md | 2 +- docs/dependencies/dependencies.md | 68 +++++++++---------- docs/dependencies/pom.xml | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/content/docs/validation/developer/build-and-release.md b/docs/content/docs/validation/developer/build-and-release.md index 80d8405417..badf3aa778 100644 --- a/docs/content/docs/validation/developer/build-and-release.md +++ b/docs/content/docs/validation/developer/build-and-release.md @@ -47,7 +47,7 @@ through Gradle's `extra` properties: end="validationVersion"> ```kotlin -extra.set("validationVersion", "2.0.0-SNAPSHOT.462") +extra.set("validationVersion", "2.0.0-SNAPSHOT.466") ``` The root build script applies this file under `allprojects { … }` and assigns diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index ebbfda9257..3c4d2d9bf3 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -1785,14 +1785,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-context-tests:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -3217,14 +3217,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-docs:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.466` ## Runtime ## Compile, tests, and tooling @@ -3234,14 +3234,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Oct 16 22:49:59 WEST 2026** using +This report was generated on **Mon Oct 19 19:13: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:validation-gradle-plugin:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -4934,14 +4934,14 @@ This report was generated on **Fri Oct 16 22:49:59 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-java:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -6726,14 +6726,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-java-bundle:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 13.0. @@ -6785,14 +6785,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:20 WEST 2026** using +This report was generated on **Mon Oct 19 19:13: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:validation-java-settings:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-java-settings:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7577,14 +7577,14 @@ This report was generated on **Sat Oct 17 02:32:20 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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-validation-jvm-runtime:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8389,14 +8389,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-consumer:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -9743,14 +9743,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-consumer-dependency:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10658,14 +10658,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-extensions:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -12100,14 +12100,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-runtime:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -13080,14 +13080,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-time:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-time:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -13838,14 +13838,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-validating:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -14956,14 +14956,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-validator:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -16405,14 +16405,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:14 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:validation-validator-dependency:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -16679,14 +16679,14 @@ This report was generated on **Sat Oct 17 02:32:22 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:21 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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:validation-vanilla:2.0.0-SNAPSHOT.465` +# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.466` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -17377,6 +17377,6 @@ This report was generated on **Sat Oct 17 02:32:21 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Oct 17 02:32:21 WEST 2026** using +This report was generated on **Mon Oct 19 19:13:13 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 e3a6a88ec8..4d3fd8fec8 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.tools validation -2.0.0-SNAPSHOT.465 +2.0.0-SNAPSHOT.466 2015 From 58dea9b841b915c180c19de5f66caf827914c4b9 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 20:43:35 +0100 Subject: [PATCH 04/10] Generate the same Java code from build to build Two runs of the Validation compiler over the same protos produced different sources, so `LaunchSpineCompiler` outputs and everything compiled from them changed on every rerun. `mangled()` now derives its suffix from the SHA-256 digest of the identifier instead of a random string. The suffix keeps its length and alphabet. Callers must pass identifiers unique within the target class, since equal identifiers now yield equal names. The option generators and `SetOnceRenderer` sort the views they query by the order of declaration, because the views come in hash order that changes between JVM runs. The `(choice)` generator takes the order of the groups from the declaring message type, as the group in the view carries no fields. Co-Authored-By: Claude Opus 5.5 --- .agents/tasks/up-to-date-compiler-tasks.md | 30 ++++-- .../java/generate/JavaIdentifiers.kt | 38 +++++-- .../java/generate/OptionGenerator.kt | 8 +- .../java/generate/option/ChoiceGenerator.kt | 19 +++- .../java/generate/option/DistinctGenerator.kt | 5 +- .../java/generate/option/GoesGenerator.kt | 5 +- .../java/generate/option/PatternGenerator.kt | 5 +- .../java/generate/option/RequiredGenerator.kt | 3 + .../java/generate/option/ValidateGenerator.kt | 5 +- .../generate/option/bound/MaxGenerator.kt | 5 +- .../generate/option/bound/MinGenerator.kt | 5 +- .../generate/option/bound/RangeGenerator.kt | 5 +- .../java/setonce/SetOnceRenderer.kt | 6 +- .../java/generate/JavaIdentifiersSpec.kt | 92 +++++++++++++++++ .../test/options/DeclarationOrderITest.kt | 84 ++++++++++++++++ .../tools/validate/declaration_order.proto | 98 +++++++++++++++++++ 16 files changed, 385 insertions(+), 28 deletions(-) create mode 100644 java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt create mode 100644 tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt create mode 100644 tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto diff --git a/.agents/tasks/up-to-date-compiler-tasks.md b/.agents/tasks/up-to-date-compiler-tasks.md index c4aa9af1c0..4d94af5dd9 100644 --- a/.agents/tasks/up-to-date-compiler-tasks.md +++ b/.agents/tasks/up-to-date-compiler-tasks.md @@ -1,6 +1,6 @@ --- slug: up-to-date-compiler-tasks -branch: write-settings-before-launch (Change 1) +branch: write-settings-before-launch (Changes 1–2) owner: claude status: in-progress started: 2026-10-19 @@ -65,16 +65,21 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): - [x] Version bump. ### Change 2 — `validation/java`: deterministic generated code -- [ ] `mangled()`: derive the suffix from SHA-256 of the identifier (JDK - `MessageDigest`, base64url, Java-identifier chars only, same length). Keep the +- [x] `mangled()`: derive the suffix from SHA-256 of the identifier (JDK + `MessageDigest`, base64url, Java-identifier chars only, same length: `length` + counts digest bytes, as it counted random bytes before). Keep the signature; KDoc states that the same identifier yields the same name. -- [ ] Sort the views each generator selects by declaration order +- [x] Sort the views each generator selects by declaration order (`Field.orderOfDeclaration`; oneof order for `(choice)`), including `SetOnceRenderer`. -- [ ] Specs for `mangled()` (deterministic, valid identifier, distinct inputs → - distinct names) and for the declaration order of generated constraints. -- [ ] Drop the `SecureRandomString` import from Validation. -- [ ] Version bump (or share the bump if Change 1 and 2 land in one branch). + - `(choice)`: the `OneofGroup` in `ChoiceOneof` has no fields (the + `OneofOptionDiscovered` event carries only the group header), so the order + comes from `MessageType.oneof_group` of the declaring type. +- [x] Specs for `mangled()` (deterministic, valid identifier, distinct inputs → + distinct names) and for the declaration order of generated constraints + (`JavaIdentifiersSpec`; `DeclarationOrderITest` in `tests/validating`). +- [x] Drop the `SecureRandomString` import from Validation. +- [x] Version bump: shares `.466` with Change 1 (same branch). ### Change 3 — `core-jvm-compiler/gradle-plugin`: settings writer picks up DSL changes (after Validation with Changes 1–2 is published) @@ -125,3 +130,12 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): committed (code + specs, then reports + docs). The `config`, `docs/_time`, and `docs/_examples` submodule pointers drifted during the session and stay uncommitted: `docs/_examples` was rewound to its `master` tip, behind the recorded `align-protobuf-runtime` commit. +- 2026-10-19 — Change 2 implemented on the same branch (uncommitted). Determinism check over + the 9 test-module Compiler tasks that have protos (`--rerun`, `diff -r` of + `tests/*/generated`): 0 differing files (52 with the `java/src/main` changes reverted). + `DeclarationOrderITest` fails on the old code and passes on the new one. +- 2026-10-19 — reviews (Kotlin, Spine rules, docs) addressed: a `(min)` case added to + `DeclarationOrderITest`, `(choice)` positions looked up with `getValue`, `length` checked + before hashing, KDoc wording. `clean build` and `dokkaGenerate` green; determinism + re-check on the final code: 0 differing files. Kept the module's `ITest` suffix over + `IgTest` for consistency with its 38 neighbours. diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt index 30fc298ce7..cdc99d201a 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.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. @@ -26,22 +26,46 @@ package io.spine.tools.validation.java.generate -import io.spine.tools.compiler.backend.SecureRandomString +import java.security.MessageDigest +import java.util.Base64 /** * Returns a Java identifier with an appended hash in the format `_`. * - * The random hash is filtered to include only valid Java identifier chars. - * Appending this hash ensures that the generated field or method name does - * not conflict with existing declarations in the target class. + * The hash is the URL-safe Base64 encoding of the first [length] bytes of + * the SHA-256 digest of the [javaIdentifier], filtered to include only valid + * Java identifier chars. Appending this hash ensures that the generated field + * or method name does not conflict with existing declarations in the target class. + * + * For example, `mangled("fooPattern")` returns `fooPattern_JfU6lQD8acWPQ`. + * + * The same identifier always yields the same name, so the generated code + * stays the same from build to build. Since equal identifiers yield equal names, + * the identifiers passed to this function must be unique within the target class. + * + * @param javaIdentifier The identifier to append the hash to. + * @param length The number of digest bytes to encode, from 1 to 32. + * @throws IllegalArgumentException if the [length] is not in the range from 1 to 32. */ public fun mangled(javaIdentifier: String, length: Int = HASH_LENGTH): String { - val hash = SecureRandomString.generate(length) + require(length in 1..DIGEST_SIZE) { + "The hash length must be in the range [1, $DIGEST_SIZE], but was $length." + } + val digest = MessageDigest.getInstance("SHA-256") + .digest(javaIdentifier.toByteArray()) + val hash = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(digest.copyOf(length)) .filter(Char::isJavaIdentifierPart) return "${javaIdentifier}_$hash" } /** - * The default length of the generated hash. + * The default number of digest bytes encoded in the hash. */ private const val HASH_LENGTH = 10 + +/** + * The number of bytes in a SHA-256 digest. + */ +private const val DIGEST_SIZE = 32 diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt index d4fdaa6215..bc19b95234 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.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. @@ -57,6 +57,12 @@ public abstract class OptionGenerator { * Generates validation code for all option applications within the provided * message [type]. * + * The order of the returned code defines the order of this option's constraints + * in the generated validation method. Return the code in a stable order, such as + * the order of declaration of the fields, so that the generated code stays + * the same from build to build. Note that the view states selected via [querying] + * come in no particular order. + * * @param type The message to generate code for. */ public abstract fun codeFor(type: TypeName): List diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt index 1e0caaa859..ef1729c882 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.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,6 +30,7 @@ import io.spine.base.FieldPath import io.spine.server.query.select import io.spine.string.lowerCamelCase import io.spine.tools.compiler.ast.TypeName +import io.spine.tools.compiler.ast.toMessageType import io.spine.tools.compiler.jvm.CodeBlock import io.spine.tools.compiler.jvm.Expression import io.spine.tools.compiler.jvm.ReadVar @@ -64,10 +65,20 @@ internal class ChoiceGenerator : OptionGenerator() { .all() } - override fun codeFor(type: TypeName): List = - allChoiceOneofs - .filter { it.id.type == type } + override fun codeFor(type: TypeName): List { + val choiceOneofs = allChoiceOneofs.filter { it.id.type == type } + if (choiceOneofs.isEmpty()) { + return emptyList() + } + // The views do not tell the position of a group in the message, + // so the order of declaration comes from the message type. + val positions = type.toMessageType(typeSystem).oneofGroupList + .withIndex() + .associate { (index, group) -> group.name to index } + return choiceOneofs + .sortedBy { positions.getValue(it.id.name) } .map { GenerateChoice(it).code() } + } } /** diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt index 0107572201..9fd1e149ff 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.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. @@ -75,10 +75,13 @@ internal class DistinctGenerator : OptionGenerator() { /** * All `(distinct)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allDistinctFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt index ffb046d3ac..226d573fa3 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.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. @@ -67,10 +67,13 @@ internal class GoesGenerator : OptionGeneratorWithConverter() { /** * All `(goes)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allGoesFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt index c247eeafb8..836d2481de 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.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. @@ -84,10 +84,13 @@ internal class PatternGenerator : OptionGenerator() { /** * All `(pattern)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allPatternFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt index 3045cb4c39..5930a4aa2d 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt @@ -70,10 +70,13 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { /** * All `(required)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allRequiredFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt index e80c6d86d9..700df96627 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.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. @@ -65,10 +65,13 @@ internal class ValidateGenerator : OptionGeneratorWithConverter() { /** * All `(validate)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allValidateFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt index de7f80b43c..8d47d53464 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.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. @@ -59,10 +59,13 @@ internal class MaxGenerator : OptionGenerator() { /** * All `(max)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allMaxFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt index 069b558e59..b478b73687 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.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. @@ -59,10 +59,13 @@ internal class MinGenerator : OptionGenerator() { /** * All `(min)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allMinFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt index 96daca17e3..bb48cc2f6b 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.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. @@ -59,10 +59,13 @@ internal class RangeGenerator : OptionGenerator() { /** * All `(range)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allRangeFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt index f134a7d2a8..eeccf7eb1a 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.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. @@ -61,7 +61,11 @@ internal class SetOnceRenderer : JavaRenderer() { } val allCompilationMessages = findMessageTypes().associateBy { it.name } + // Process the fields of each message in the order of declaration, so that + // the code added to the shared builder methods, such as `mergeFrom()`, + // goes in the same order from build to build. (this as Querying).select().all() + .sortedBy { it.subject.orderOfDeclaration } .associateWith { allCompilationMessages[it.id.type]!! } .forEach { (protoField, declaredIn) -> val javaConstraints = javaConstraints(protoField.subject, protoField.errorMessage) diff --git a/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt new file mode 100644 index 0000000000..3826e22df5 --- /dev/null +++ b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.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.tools.validation.java.generate + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldStartWith +import javax.lang.model.SourceVersion +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`mangled()` should") +internal class JavaIdentifiersSpec { + + @Test + fun `append the hash of the identifier`() { + // The expected hash is computed independently of the implementation. + // The URL-safe Base64 encoding of the first 10 bytes of the SHA-256 digest + // of `fooPattern` is `JfU6l-QD8acWPQ`, and `-` is not a Java identifier char. + mangled("fooPattern") shouldBe "fooPattern_JfU6lQD8acWPQ" + } + + @Test + fun `yield the same name for the same identifier`() { + mangled("validateNames") shouldBe mangled("validateNames") + } + + @Test + fun `yield different hashes for different identifiers`() { + val fooHash = mangled("foo").removePrefix("foo_") + val barHash = mangled("bar").removePrefix("bar_") + + fooHash shouldNotBe barHash + } + + @Test + fun `yield valid Java identifiers`() { + // Some of these identifiers, such as `field2`, have `-` in the encoded digest. + // That char must be filtered out. + val identifiers = (1..100).map { "field$it" } + + identifiers.forEach { identifier -> + val name = mangled(identifier) + name shouldStartWith "${identifier}_" + SourceVersion.isIdentifier(name) shouldBe true + } + } + + @Test + fun `encode the given number of digest bytes`() { + // Base64 encodes every 3 bytes with 4 chars. So, the hash of the first + // 3 bytes is the beginning of the hash of the first 10 bytes. + val hash = mangled("validateNames", length = 3).removePrefix("validateNames_") + + hash shouldBe mangled("validateNames").removePrefix("validateNames_").take(4) + } + + @Test + fun `reject a length outside the range from 1 to the digest size`() { + shouldThrow { + mangled("foo", length = 0) + } + shouldThrow { + mangled("foo", length = 33) + } + } +} diff --git a/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt new file mode 100644 index 0000000000..e2b143ae34 --- /dev/null +++ b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt @@ -0,0 +1,84 @@ +/* + * 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.test.options + +import com.google.protobuf.Message +import io.kotest.matchers.collections.shouldContainExactly +import io.spine.test.tools.validate.ChoiceInDeclarationOrder +import io.spine.test.tools.validate.MinInDeclarationOrder +import io.spine.test.tools.validate.RequiredInDeclarationOrder +import io.spine.validation.Validate.violationsOf +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Verifies that the generated code checks the constraints of an option + * in the order of declaration. + * + * A message reports the violations in the order of its constraints, so the tests + * check the order of the violations. The test messages declare the constrained + * fields and groups in an order that differs from both the order of the field numbers + * and the alphabetical order of the names, so the tests are unlikely to pass when + * the constraints go in another order. + */ +@DisplayName("Generated constraints should follow the order of declaration, so that") +internal class DeclarationOrderITest { + + @Test + fun `violations of '(required)' fields go in the order of the fields`() { + val message = RequiredInDeclarationOrder.getDefaultInstance() + + violatedPaths(message).shouldContainExactly( + "echo", "bravo", "hotel", "alpha", "golf", "charlie", "foxtrot", "delta" + ) + } + + @Test + fun `violations of '(min)' fields go in the order of the fields`() { + val message = MinInDeclarationOrder.getDefaultInstance() + + violatedPaths(message).shouldContainExactly( + "delta", "alpha", "echo", "charlie", "bravo" + ) + } + + @Test + fun `violations of '(choice)' groups go in the order of the groups`() { + val message = ChoiceInDeclarationOrder.getDefaultInstance() + + violatedPaths(message).shouldContainExactly( + "charlie", "alpha", "echo", "bravo", "delta" + ) + } +} + +/** + * Returns the field paths of the violations of the given [message] + * in the order of the violations. + */ +private fun violatedPaths(message: Message): List = + violationsOf(message).map { it.fieldPath.fieldNameList.joinToString(".") } diff --git a/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto new file mode 100644 index 0000000000..195f32adb9 --- /dev/null +++ b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto @@ -0,0 +1,98 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.test.tools.validate; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.test.tools.validate"; +option java_outer_classname = "DeclarationOrderProto"; +option java_multiple_files = true; + +// Declares `(required)` fields in an order that differs from both the order of +// the field numbers and the alphabetical order of the field names. +// +// The violations of the constraints must follow the order of declaration. +// +message RequiredInDeclarationOrder { + string echo = 5 [(required) = true]; + string bravo = 2 [(required) = true]; + string hotel = 8 [(required) = true]; + string alpha = 1 [(required) = true]; + string golf = 7 [(required) = true]; + string charlie = 3 [(required) = true]; + string foxtrot = 6 [(required) = true]; + string delta = 4 [(required) = true]; +} + +// Declares `(min)` fields in an order that differs from both the order of +// the field numbers and the alphabetical order of the field names. +// +// The violations of the constraints must follow the order of declaration. +// +message MinInDeclarationOrder { + int32 delta = 4 [(min).value = "1"]; + int32 alpha = 1 [(min).value = "1"]; + int32 echo = 5 [(min).value = "1"]; + int32 charlie = 3 [(min).value = "1"]; + int32 bravo = 2 [(min).value = "1"]; +} + +// Declares `oneof` groups with `(choice).required = true` in an order that differs from both the order of +// the field numbers and the alphabetical order of the group names. +// +// The violations of the constraints must follow the order of declaration. +// +message ChoiceInDeclarationOrder { + + oneof charlie { + option (.choice).required = true; + string charlie_value = 3; + } + + oneof alpha { + option (.choice).required = true; + string alpha_value = 1; + } + + oneof echo { + option (.choice).required = true; + string echo_value = 5; + } + + oneof bravo { + option (.choice).required = true; + string bravo_value = 2; + } + + oneof delta { + option (.choice).required = true; + string delta_value = 4; + } +} From 83ed0dc4c538ab487580d3a738830e5e6a8bf101 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 20:48:45 +0100 Subject: [PATCH 05/10] Rename `DeclarationOrderITest` to `DeclarationOrderIgTest` Follow the `XIgTest` naming of integration tests from the testing guideline. The other `*ITest` suites are to be reviewed separately. Co-Authored-By: Claude Opus 5.5 --- .agents/tasks/up-to-date-compiler-tasks.md | 7 ++++++- ...{DeclarationOrderITest.kt => DeclarationOrderIgTest.kt} | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) rename tests/validating/src/test/kotlin/io/spine/test/options/{DeclarationOrderITest.kt => DeclarationOrderIgTest.kt} (98%) diff --git a/.agents/tasks/up-to-date-compiler-tasks.md b/.agents/tasks/up-to-date-compiler-tasks.md index 4d94af5dd9..a85fe2d82e 100644 --- a/.agents/tasks/up-to-date-compiler-tasks.md +++ b/.agents/tasks/up-to-date-compiler-tasks.md @@ -77,7 +77,7 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): comes from `MessageType.oneof_group` of the declaring type. - [x] Specs for `mangled()` (deterministic, valid identifier, distinct inputs → distinct names) and for the declaration order of generated constraints - (`JavaIdentifiersSpec`; `DeclarationOrderITest` in `tests/validating`). + (`JavaIdentifiersSpec`; `DeclarationOrderIgTest` in `tests/validating`). - [x] Drop the `SecureRandomString` import from Validation. - [x] Version bump: shares `.466` with Change 1 (same branch). @@ -114,6 +114,8 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): replacing per-plugin writers. - Deterministic `select().all()` ordering in `compiler`/`core-jvm` for all Compiler plugins. +- Review the names of the integration tests under `tests/` (`*ITest`) against + the `XIgTest` convention in `.agents/guidelines/testing.md`. ## Log - 2026-10-19 — analysis done; scope chosen: Validation + CoreJvm writer; plan drafted, awaiting approval. @@ -139,3 +141,6 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): before hashing, KDoc wording. `clean build` and `dokkaGenerate` green; determinism re-check on the final code: 0 differing files. Kept the module's `ITest` suffix over `IgTest` for consistency with its 38 neighbours. +- 2026-10-19 — Change 2 committed (`58dea9b841`). The new suite renamed to + `DeclarationOrderIgTest`: the testing guideline takes precedence over the legacy + `ITest` names around it, which are to be reviewed separately (see Follow-ups). diff --git a/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt similarity index 98% rename from tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt rename to tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt index e2b143ae34..96cd1f69b6 100644 --- a/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderITest.kt +++ b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt @@ -46,7 +46,7 @@ import org.junit.jupiter.api.Test * the constraints go in another order. */ @DisplayName("Generated constraints should follow the order of declaration, so that") -internal class DeclarationOrderITest { +internal class DeclarationOrderIgTest { @Test fun `violations of '(required)' fields go in the order of the fields`() { From 3871900a3d643dcb9f194cda4339b73cbde83ef1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 21:00:24 +0100 Subject: [PATCH 06/10] Address pre-PR review findings Refresh the `RequiredGenerator` snippets embedded in the developer docs, and ask new built-in generators to sort the views they select. Describe the output of `WriteValidationSettings` without relying on what the CoreJvm Compiler declares. Wrap the long lines of the proto fixture and the task log. Co-Authored-By: Claude Opus 5.5 --- .agents/tasks/up-to-date-compiler-tasks.md | 13 ++++++++++--- .../developer/adding-a-built-in-option.md | 5 +++++ .../validation/developer/java-code-generation.md | 3 +++ .../validation/gradle/WriteValidationSettings.kt | 5 ++--- .../test/tools/validate/declaration_order.proto | 5 +++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.agents/tasks/up-to-date-compiler-tasks.md b/.agents/tasks/up-to-date-compiler-tasks.md index a85fe2d82e..b6f5618b7f 100644 --- a/.agents/tasks/up-to-date-compiler-tasks.md +++ b/.agents/tasks/up-to-date-compiler-tasks.md @@ -118,9 +118,16 @@ Findings (verified 2026-10-19 on Gradle 9.6.1): the `XIgTest` convention in `.agents/guidelines/testing.md`. ## Log -- 2026-10-19 — analysis done; scope chosen: Validation + CoreJvm writer; plan drafted, awaiting approval. -- 2026-10-19 — approved; Change 1 started on `write-settings-before-launch` (based on `origin/master`, version `.465`). -- 2026-10-19 — Change 1 implemented (uncommitted, except the `.466` bump commit). `./gradlew build` and `:gradle-plugin:dokkaGenerate` pass. A standalone consumer of the published `.466` plugin stays `UP-TO-DATE` across `--no-daemon` runs, reruns when the DSL value changes, and works after `clean`; on `.465` the second run reruns with "…JavaValidationRenderer.pb.json has been added". `:docs:embedCode` can't run here (x86-64 binary, no Rosetta); `build-and-release.md` updated by hand. +- 2026-10-19 — analysis done; scope chosen: Validation + CoreJvm writer; plan drafted, awaiting + approval. +- 2026-10-19 — approved; Change 1 started on `write-settings-before-launch` (based on + `origin/master`, version `.465`). +- 2026-10-19 — Change 1 implemented (uncommitted, except the `.466` bump commit). `./gradlew build` + and `:gradle-plugin:dokkaGenerate` pass. A standalone consumer of the published `.466` plugin + stays `UP-TO-DATE` across `--no-daemon` runs, reruns when the DSL value changes, and works after + `clean`; on `.465` the second run reruns with "…JavaValidationRenderer.pb.json has been added". + `:docs:embedCode` can't run here (x86-64 binary, no Rosetta); `build-and-release.md` updated by + hand. - 2026-10-19 — reviews (Kotlin, Spine rules, Gradle, docs) addressed: the task owns the consumer ID and file name, takes an `@Internal` settings directory (set lazily) and derives its `@OutputFile`; properties are `internal`; a spec pins the `@Input`/`@OutputFile` diff --git a/docs/content/docs/validation/developer/adding-a-built-in-option.md b/docs/content/docs/validation/developer/adding-a-built-in-option.md index 188116c36c..836bf4cab0 100644 --- a/docs/content/docs/validation/developer/adding-a-built-in-option.md +++ b/docs/content/docs/validation/developer/adding-a-built-in-option.md @@ -326,10 +326,13 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { /** * All `(required)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allRequiredFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = @@ -345,6 +348,8 @@ The pattern is uniform across the built-ins: - Query the projection lazily — `querying` is not available until `inject()` returns, see “[The render lifecycle](java-code-generation.md#the-render-lifecycle)”. - Filter views by the message type currently being processed. +- Sort the selected views by `orderOfDeclaration`, so that the generated code is the same + from build to build. The views come in no particular order. - Delegate per-application code construction to a small helper class (`GenerateRequired` here). The helper produces a `CodeBlock` that runs inside the validate scope described in “[The validate scope](java-code-generation.md#the-validate-scope)” — diff --git a/docs/content/docs/validation/developer/java-code-generation.md b/docs/content/docs/validation/developer/java-code-generation.md index fa2011b076..c1c20d5c9d 100644 --- a/docs/content/docs/validation/developer/java-code-generation.md +++ b/docs/content/docs/validation/developer/java-code-generation.md @@ -131,10 +131,13 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { /** * All `(required)` fields in the current compilation process. + * + * The fields of each message go in the order of declaration. */ private val allRequiredFields by lazy { querying.select() .all() + .sortedBy { it.subject.orderOfDeclaration } } override fun codeFor(type: TypeName): List = diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt index 1f28fa4e7b..d3d84c64ef 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt @@ -58,9 +58,8 @@ import org.gradle.work.DisableCachingByDefault * rewrite the file, which, in turn, makes `LaunchSpineCompiler` out of date. * When the file is deleted (for example, by `clean`), the task writes it again. * - * The task declares only the file it writes as its output. Other plugins write - * their settings files into the same directory. For example, the CoreJvm Compiler - * declares the whole directory as an output of its own settings task. + * The task declares only the file it writes as its output, not the whole settings + * directory, because other plugins write their settings files into the same directory. */ @DisableCachingByDefault(because = "Writing a single small file is as fast as loading it.") public abstract class WriteValidationSettings : DefaultTask() { diff --git a/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto index 195f32adb9..5420df4338 100644 --- a/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto +++ b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto @@ -64,8 +64,9 @@ message MinInDeclarationOrder { int32 bravo = 2 [(min).value = "1"]; } -// Declares `oneof` groups with `(choice).required = true` in an order that differs from both the order of -// the field numbers and the alphabetical order of the group names. +// Declares `oneof` groups with `(choice).required = true` in an order that +// differs from both the order of the field numbers and the alphabetical order +// of the group names. // // The violations of the constraints must follow the order of declaration. // From 9bcc3df4c3a8fdf1762cd78d34f1a4f253d52e50 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 21:25:06 +0100 Subject: [PATCH 07/10] Embed the current `publish.yml` in the release docs `publish.yml` became a thin caller of the reusable `publishing.yml` from `config`, so the embedded "Publish artifacts to Maven" step no longer exists and `:docs:checkSamples` fails. Embed the trigger and the job instead, leaving out the `uses:` line whose commit changes on every `./config/pull`, and describe what the reusable workflow does. Co-Authored-By: Claude Opus 5.5 --- .../validation/developer/build-and-release.md | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/content/docs/validation/developer/build-and-release.md b/docs/content/docs/validation/developer/build-and-release.md index badf3aa778..1a3835cd66 100644 --- a/docs/content/docs/validation/developer/build-and-release.md +++ b/docs/content/docs/validation/developer/build-and-release.md @@ -233,25 +233,31 @@ Portal][gradle-plugin-portal] — configured by the `java-gradle-plugin` and That is the destination consumers reach when they write `id("io.spine.validation")` in their `plugins { … }` block. -Publication is automated by the -[`publish.yml`][publish-workflow] GitHub Actions workflow, which runs on every -push to `master` after PR checks have already verified the build: +Publication is automated by the [`publish.yml`][publish-workflow] GitHub Actions +workflow, which runs on every push to `master` after PR checks have already +verified the build: + start="^on:" + end="name: Publish to Maven repositories"> ```yaml -- name: Publish artifacts to Maven - # Since we're in the `master` branch already, this means that tests of a PR passed. - # So, no need to run the tests again when publishing. - run: ./gradlew publish -x test --stacktrace +on: + push: + branches: [master] + +jobs: + publish: + name: Publish to Maven repositories ``` -`-x test` relies on the per-PR build to keep publication fast. Every merge to -`master` emits a new version of the artifacts for downstream consumers to -refresh against. +The workflow is a thin caller. The job itself — checkout, credentials, the Gradle +publication, and the attestation — is the reusable `publishing.yml` workflow +hosted by the `config` repository, which `publish.yml` references at the commit +of `config` this repository pins. The job runs `./gradlew publish` with `-x test`, +relying on the per-PR build to keep publication fast. Every merge to `master` +emits a new version of the artifacts for downstream consumers to refresh against. ## Downstream consumers From d6e299cdb9bdbf24dc24e2f16f91984e431e12e8 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 21:27:37 +0100 Subject: [PATCH 08/10] Update shared agent tooling and config (`./config/pull`) Keep `Compiler.fallbackVersion` at `2.0.0-SNAPSHOT.074`. The pull resets it to the `.069` that `config` carries, while this repository builds with `.074`. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 2 +- AGENTS.md | 34 +- buildSrc/src/main/kotlin/DokkaExts.kt | 32 +- .../src/main/kotlin/config-tester.gradle.kts | 10 +- .../dependency/lib/OpenTelemetryKotlin.kt | 87 +++ .../kotlin/io/spine/dependency/local/Base.kt | 7 +- .../io/spine/dependency/local/BaseTypes.kt | 4 +- .../io/spine/dependency/local/Change.kt | 4 +- .../io/spine/dependency/local/CoreJvm.kt | 7 +- .../spine/dependency/local/CoreJvmCompiler.kt | 2 +- .../io/spine/dependency/local/DokkaTools.kt | 1 - .../io/spine/dependency/local/Logging.kt | 5 +- .../io/spine/dependency/local/Reflect.kt | 4 +- .../kotlin/io/spine/dependency/local/Spine.kt | 4 +- .../io/spine/dependency/local/TestLib.kt | 4 +- .../kotlin/io/spine/dependency/local/Time.kt | 2 +- .../io/spine/dependency/local/ToolBase.kt | 7 +- .../io/spine/dependency/local/Validation.kt | 5 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 203 ++++++- .../spine/gradle/publish/ProtobufRuntime.kt | 173 ++++++ .../gradle/publish/PublicationHandler.kt | 27 + .../kotlin/io/spine/gradle/ClonedRepoSpec.kt | 158 ++++++ .../io/spine/gradle/GitRepositorySpec.kt | 158 ++++++ .../test/kotlin/io/spine/gradle/PathExts.kt | 29 + .../gradle/publish/ProtobufRuntimeIgTest.kt | 519 ++++++++++++++++++ .../publish/PublicationChecksumsIgTest.kt | 40 +- .../publish/PublicationHandlerIgTest.kt | 226 ++++++++ config | 2 +- init-submodules | 168 +++++- 29 files changed, 1836 insertions(+), 88 deletions(-) create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/lib/OpenTelemetryKotlin.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtobufRuntime.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/ClonedRepoSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/GitRepositorySpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/PathExts.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/ProtobufRuntimeIgTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationHandlerIgTest.kt diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3bad11f20f..5d1ca00cdc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ on: jobs: publish: name: Publish to Maven repositories - uses: SpineEventEngine/config/.github/workflows/publishing.yml@a0cbbbfe66c12475224563439fb6deb3da71ba58 + uses: SpineEventEngine/config/.github/workflows/publishing.yml@3516efc5dee84e88bc541944ff743f435eec6a1d permissions: contents: read packages: write diff --git a/AGENTS.md b/AGENTS.md index 8c5f6198d0..dc6927fd2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,17 +20,29 @@ Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (and some shallow clones / cloud checkouts) start with NO submodules checked out, so those symlinks dangle and no skills are found. Bootstrap such a tree with **`./init-submodules`** — a root script that materializes the missing -*config-managed* submodules at their pinned commits: `config` itself, plus every -submodule that declares a tracked `branch` in `.gitmodules` (`.agents/shared`, and -any shared submodule added later) — the same rule `./config/pull` uses to decide -what it floats. Submodules the consumer owns (a Hugo theme, a vendored library, -doc-example submodules, …) declare no tracked branch and are left untouched, so the -automatic `SessionStart` run never tries to clone — or fail on credentials for — a -submodule this project does not manage. It depends on no pre-existing `config` -submodule, so it works before `./config/pull` (which lives inside the `config` -submodule) can. Claude Code runs it automatically via a `SessionStart` hook; other -agents and humans run it by hand, then `./config/pull` to float the shared submodules -to their branch tips. +*config-managed* submodules at the tip of their branch (`master` for `config`, the +tracked `branch` for the rest): `config` itself, plus every submodule that declares +a tracked `branch` in `.gitmodules` (`.agents/shared`, and any shared submodule +added later) — the same rule `./config/pull` uses to decide what it floats. It +deliberately does not stop at the commit the superproject pins for such a +submodule: nothing keeps that pin current — `ignore = all` hides the submodule from +`git status`, and `./config/pull` floats the on-disk copy without committing the +moved gitlink — so materializing the pin as-is means months-old hook scripts, +skills, and guidelines. If the float fails — a branch the remote does not have, +say — a warning is printed and the submodule is left wherever the steps that +did succeed put it, at worst the pin, so a session still starts with a usable +tree; it is not retried on a later run, which is why that warning points at +`./config/pull`. With no network at all the clone itself fails and the +submodule is left uninitialized, as it was before. Submodules the consumer owns +(a Hugo theme, a vendored library, doc-example submodules, …) declare no tracked +branch and are left untouched, so the automatic `SessionStart` run never tries +to clone — or fail on credentials for — a submodule this project does not +manage. A submodule that is already checked out is left exactly as it is, which +keeps repeated runs cheap and offline-safe and never rewinds a tree someone +parked on a particular commit; run `./config/pull` to float those. It depends on +no pre-existing `config` submodule, so it works before `./config/pull` (which +lives inside the `config` submodule) can. Claude Code runs it automatically via +a `SessionStart` hook; other agents and humans run it by hand. ## Commit and history safety diff --git a/buildSrc/src/main/kotlin/DokkaExts.kt b/buildSrc/src/main/kotlin/DokkaExts.kt index d1483b615e..5aeba8d5f0 100644 --- a/buildSrc/src/main/kotlin/DokkaExts.kt +++ b/buildSrc/src/main/kotlin/DokkaExts.kt @@ -334,19 +334,45 @@ private fun abbreviationPattern(abbreviation: String): Regex { private val wordStart = Regex("(?=\\p{javaUpperCase})") /** - * Disables Dokka and Javadoc tasks in this `Project`. + * Disables the Dokka and Javadoc tasks generating the documentation of this `Project`. * * This function could be useful to improve build speed when building subprojects containing * test environments or integration test projects. + * + * The JARs packing the documentation stay enabled, as they do under + * [skipDokkaWhenPublishingToMavenLocal]. A publication declares one artifact + * file per JAR, and a JAR that a disabled task never writes would be missing + * both when publishing and when the published files are attested. With nothing + * generated to pack, such a JAR costs almost nothing: it contains only + * a manifest, or the output of an earlier Dokka run. */ @Suppress("unused") fun Project.disableDocumentationTasks() { gradle.taskGraph.whenReady { tasks.forEach { task -> - val lowercaseName = task.name.lowercase() - if (lowercaseName.contains("dokka") || lowercaseName.contains("javadoc")) { + if (task.generatesDocumentation()) { task.enabled = false } } } } + +/** + * Tells if this task generates documentation, rather than packing already generated + * documentation into a JAR. + * + * A [Jar] task is recognized by its type rather than by its name, so that + * `javadocJar` and `htmlDocsJar` are treated alike — as is any documentation JAR + * added later, whatever it is called. Any other task generates documentation if + * `dokka` or `javadoc` occurs in its name, ignoring case. + * + * [selectsDocsTask] keeps matching the same JARs by name, because the names it + * is given come from the command line rather than from the task graph. + */ +private fun Task.generatesDocumentation(): Boolean { + if (this is Jar) { + return false + } + return name.contains("dokka", ignoreCase = true) + || name.contains("javadoc", ignoreCase = true) +} diff --git a/buildSrc/src/main/kotlin/config-tester.gradle.kts b/buildSrc/src/main/kotlin/config-tester.gradle.kts index 22b2d906f7..d7f3af92f8 100644 --- a/buildSrc/src/main/kotlin/config-tester.gradle.kts +++ b/buildSrc/src/main/kotlin/config-tester.gradle.kts @@ -17,17 +17,19 @@ import io.spine.gradle.SpineRepos import io.spine.gradle.SpineTaskGroup import io.spine.gradle.cleanFolder import java.nio.file.Path -import java.nio.file.Paths // A reference to `config` to use along with the `ConfigTester`. -val config: Path = Paths.get("./") +// +// Both paths below are resolved against the project directory, because a relative path +// would be resolved against the working directory of the Gradle daemon instead. +val config: Path = layout.projectDirectory.asFile.toPath() // A temp folder to use to check out the sources of other repositories with the `ConfigTester`. -val tempFolder = File("./tmp") +val tempFolder = layout.projectDirectory.dir("tmp").asFile // Creates a Gradle task that checks out and builds the selected Spine repositories // with the local version of `config` and `config/buildSrc`. -ConfigTester(config, tasks, tempFolder) +ConfigTester(config = config, tasks = tasks, tempFolder = tempFolder) .addRepo(SpineRepos.baseTypes) // Builds `base-types` at `master`. .addRepo(SpineRepos.base) // Builds `base` at `master`. .addRepo(SpineRepos.coreJvm) // Builds `core-jvm` at `master`. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/OpenTelemetryKotlin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/OpenTelemetryKotlin.kt new file mode 100644 index 0000000000..b473fdcfa8 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/OpenTelemetryKotlin.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.dependency.lib + +import io.spine.dependency.Dependency + +/** + * The Kotlin Multiplatform API and SDK for OpenTelemetry. + * + * The project is currently in the alpha stage, and its API is marked + * with `@io.opentelemetry.kotlin.ExperimentalApi`. Modules using these artifacts + * must opt in to that marker. + * + * The project does not publish a BOM, so the version is embedded in each + * artifact coordinate below. + * + * @see GitHub project + * @see Kotlin documentation + */ +@Suppress("unused") +object OpenTelemetryKotlin : Dependency() { + + // https://central.sonatype.com/artifact/io.opentelemetry.kotlin/api + // + // The project is in the alpha stage, so bump this version only together + // with the code that consumes it. + override val version = "0.4.0" + override val group = "io.opentelemetry.kotlin" + + /** + * The Kotlin Multiplatform API surface (traces, metrics, logs). + * + * This is the artifact production code compiles against; the SDK artifacts + * below are for applications and tests. + */ + val api = "$group:api:$version" + + /** + * A no-op implementation of the [api] artifact. + */ + val noop = "$group:noop:$version" + + /** + * The Kotlin SDK API: span processors, exporters, samplers, and the + * configuration DSL. + * + * This artifact exposes the SDK types on the compile classpath, while the + * `createOpenTelemetry { }` entry point that instantiates them lives in + * [implementation]. Applications and tests configure an `OpenTelemetry` + * instance using these two artifacts; production code needs only + * the [api] artifact. + */ + val core = "$group:core:$version" + + /** + * The Kotlin SDK implementation, including the `createOpenTelemetry { }` + * entry point. + * + * Use it together with [core]. + */ + val implementation = "$group:implementation:$version" + + /** + * The compatibility bridge to the OpenTelemetry Java SDK. + */ + val compat = "$group:compat:$version" + + override val modules: List = listOf( + "$group:api", + "$group:noop", + "$group:core", + "$group:implementation", + "$group:compat", + ) +} diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt index c310c7d67c..45c1d2f023 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt @@ -19,10 +19,13 @@ package io.spine.dependency.local * * @see spine-base-libraries */ -@Suppress("ConstPropertyName", "unused") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, + "unused" /* Some subprojects do not use all Base artifacts. */, +) object Base { const val version = "2.0.0-SNAPSHOT.461" - const val versionForBuildScript = "2.0.0-SNAPSHOT.460" + const val versionForBuildScript = "2.0.0-SNAPSHOT.461" const val group = Spine.group private const val prefix = "spine" const val libModule = "$prefix-base" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/BaseTypes.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/BaseTypes.kt index 70fe77b687..e0a828061a 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/BaseTypes.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/BaseTypes.kt @@ -19,7 +19,9 @@ package io.spine.dependency.local * * @see spine-base-types */ -@Suppress("ConstPropertyName") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, +) object BaseTypes { const val version = "2.0.0-SNAPSHOT.225" const val group = Spine.group diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Change.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Change.kt index 228f96e3fd..1f6ebe1c62 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Change.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Change.kt @@ -19,7 +19,9 @@ package io.spine.dependency.local * * @see spine-change */ -@Suppress("ConstPropertyName") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, +) object Change { const val version = "2.0.0-SNAPSHOT.208" const val group = Spine.group diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt index bef7362aa0..35246a7793 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt @@ -24,10 +24,13 @@ typealias CoreJava = CoreJvm * * See [`SpineEventEngine/core-jvm`](https://github.com/SpineEventEngine/core-jvm/). */ -@Suppress("ConstPropertyName", "unused") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, + "unused" /* Some subprojects do not use all CoreJvm artifacts. */, +) object CoreJvm { const val group = Spine.group - const val version = "2.0.0-SNAPSHOT.567" + const val version = "2.0.0-SNAPSHOT.571" const val coreArtifact = "spine-core" const val clientArtifact = "spine-client" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index 5db1a81060..1af2694bb1 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -22,7 +22,7 @@ package io.spine.dependency.local @Suppress( "MemberVisibilityCanBePrivate" /* The properties are used directly by other subprojects. */, "ConstPropertyName" /* We use a custom convention for artifact properties. */, - "unused" + "unused" /* Some subprojects do not use the CoreJvm Compiler directly. */, ) object CoreJvmCompiler { diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/DokkaTools.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/DokkaTools.kt index 5a64607c17..6026d49f57 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/DokkaTools.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/DokkaTools.kt @@ -21,7 +21,6 @@ package io.spine.dependency.local * See [`SpineEventEngine/dokka-tools`](https://github.com/SpineEventEngine/dokka-tools/). */ @Suppress( - "unused" /* Some subprojects do not use the Dokka tools directly. */, "ConstPropertyName" /* We use a custom convention for artifact properties. */, ) object DokkaTools { diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Logging.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Logging.kt index eabc0a1e9c..6d5f5b798c 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Logging.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Logging.kt @@ -19,7 +19,10 @@ package io.spine.dependency.local * * @see spine-logging */ -@Suppress("ConstPropertyName", "unused") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, + "unused" /* Some subprojects do not use all Logging artifacts. */, +) object Logging { const val version = "2.0.0-SNAPSHOT.426" const val group = Spine.group diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt index 72091278b6..c07fd2f716 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt @@ -19,7 +19,9 @@ package io.spine.dependency.local * * @see spine-reflect */ -@Suppress("ConstPropertyName") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, +) object Reflect { const val version = "2.0.0-SNAPSHOT.202" const val group = Spine.group diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Spine.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Spine.kt index 6efbf78e26..c4e6c2d996 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Spine.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Spine.kt @@ -17,7 +17,9 @@ package io.spine.dependency.local /** * Defines Maven groups for the Spine SDK artifacts. */ -@Suppress("unused", "ConstPropertyName") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, +) object Spine { const val group = "io.spine" const val toolsGroup = "io.spine.tools" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt index b4609db8cc..324a493653 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt @@ -19,7 +19,9 @@ package io.spine.dependency.local * * @see spine-testlib */ -@Suppress("ConstPropertyName") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, +) object TestLib { const val version = "2.0.0-SNAPSHOT.215" const val group = Spine.toolsGroup diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt index 942333f673..3e8e39938d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt @@ -23,7 +23,7 @@ import io.spine.dependency.Dependency */ @Suppress( "unused" /* Some subprojects do not use all Time artifacts. */, - "ConstPropertyName" /* We use custom convention for artifact properties. */, + "ConstPropertyName" /* We use a custom convention for artifact properties. */, "MemberVisibilityCanBePrivate" /* The properties are used directly by other subprojects. */, ) object Time : Dependency() { diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index ce3b11ee28..2a8a255ac6 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt @@ -26,11 +26,14 @@ package io.spine.dependency.local * * @see tool-base */ -@Suppress("ConstPropertyName", "unused") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, + "unused" /* Some subprojects do not use all ToolBase artifacts. */, +) object ToolBase { const val group = Spine.toolsGroup const val version = "2.0.0-SNAPSHOT.426" - const val dogfoodingVersion = "2.0.0-SNAPSHOT.425" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.426" const val archive = "$group:archive:$version" const val code = "$group:code:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index b1069ce3a8..155cbbc602 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -19,7 +19,10 @@ package io.spine.dependency.local * * See [`SpineEventEngine/validation`](https://github.com/SpineEventEngine/validation/). */ -@Suppress("ConstPropertyName", "unused") +@Suppress( + "ConstPropertyName" /* We use a custom convention for artifact properties. */, + "unused" /* Some subprojects do not use all Validation artifacts. */, +) object Validation { /** * The version of the Validation library artifacts. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt index f364ccc1ba..de954af0ae 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt @@ -18,17 +18,36 @@ package io.spine.gradle import java.io.File import java.net.URI +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitResult.CONTINUE +import java.nio.file.FileVisitResult.SKIP_SUBTREE import java.nio.file.Files import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.name +import kotlin.io.path.notExists import org.ajoberstar.grgit.Grgit import org.gradle.api.tasks.TaskContainer +/** + * The name of the folder with the Gradle build logic of a project. + */ +private const val BUILD_SRC = "buildSrc" + /** * A tool to execute the Gradle `build` task in selected Git repositories * with the local version of [config] contents. * * Checks out the content of selected repositories into the specified [tempFolder]. - * The folder is created if it does not exist. By default, uses `./tmp` as a temp folder. + * The folder is created if it does not exist. + * + * Both [config] and [tempFolder] must be absolute paths, e.g. obtained from + * `layout.projectDirectory`. A relative path is resolved against the working directory + * of the Gradle daemon, which is unrelated to the project directory. * * Replaces the `config` and `buildSrc` folders in the checked-out repository by the local versions * of code. If the repository-under-test already contains its own `buildSrc` or `config` folders, @@ -46,7 +65,7 @@ import org.gradle.api.tasks.TaskContainer class ConfigTester( private val config: Path, private val tasks: TaskContainer, - private val tempFolder: File = File("./tmp") + private val tempFolder: File ) { companion object { @@ -57,7 +76,16 @@ class ConfigTester( private const val BUILD_TIMEOUT_MINUTES = 30L } - private val buildSrc: Path = config.resolve("buildSrc") + init { + require(config.isAbsolute) { + "The path to `config` must be absolute. Encountered: `$config`." + } + require(tempFolder.isAbsolute) { + "The path to the temp folder must be absolute. Encountered: `$tempFolder`." + } + } + + private val buildSrc: Path = config.resolve(BUILD_SRC) /** * Git repositories to test. @@ -75,7 +103,8 @@ class ConfigTester( } /** - * Adds a test. + * Adds a Git [repo] into the test build by its URI, to be checked out + * at the given [branch]. */ fun addRepo(repo: URI, branch: Branch): ConfigTester { repos.add(GitRepository(repo, branch)) @@ -159,7 +188,9 @@ class GitRepository( private val uri: URI, /** - * A branch to checkout. + * A branch to check out. + * + * May be any branch of the repository, not only the default one. * * By default, points to `master`. */ @@ -193,6 +224,9 @@ class GitRepository( * the `destinationFolder/foobar` folder. * * If the supplied folder does not exist, it is created. + * + * The [branch] is checked out as a local branch tracking its remote counterpart. + * A [branch] the remote does not advertise fails the checkout. */ fun checkout(destinationFolder: File): ClonedRepo { val preparedFolder = prepareCheckout(destinationFolder).toPath() @@ -201,16 +235,21 @@ class GitRepository( "to `${preparedFolder.toAbsolutePath()}`." ) + // The branch is checked out by the `clone` itself, rather than by a subsequent + // `checkout`. A fresh clone has only the default branch among its local refs, + // so `checkout` cannot resolve any other branch by its plain name, while + // `refToCheckout` is resolved against the refs advertised by the remote, + // where every branch of the repository is present. + // + // Only the checked-out files are of interest further on, so the Git repository + // the clone opens is closed right away. Grgit.clone( mapOf( "dir" to preparedFolder, - "uri" to uri + "uri" to uri, + "refToCheckout" to branch.name ) - ).checkout( - mapOf( - "branch" to branch.name - ) - ) + ).close() return ClonedRepo(this, preparedFolder) } @@ -260,14 +299,37 @@ class ClonedRepo( * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths * when copying. * + * Build output and Git metadata of the [source] are not copied. If the original + * `buildSrc` has its own `module.gradle.kts`, the script is preserved. * * Returns this instance of `ClonedRepo`, for call chaining. */ fun replaceBuildSrc(source: Path, ignoredFolder: Path?): ClonedRepo { - replaceFolder("buildSrc", source, ignoredFolder) + replaceFolder(BUILD_SRC, source, ignoredFolder)?.let(::restoreModuleScript) return this } + /** + * Restores the `module.gradle.kts` script of this repository, if it has one. + * + * `config` ships this script as an empty template that each project replaces + * by its own. The `config/migrate` script preserves the script of a project + * when installing `buildSrc` into it, and so must the test build. + * + * @param originalBuildSrc + * the folder into which the original `buildSrc` of this repository was renamed. + */ + private fun restoreModuleScript(originalBuildSrc: Path) { + val ownScript = originalBuildSrc.resolve(MODULE_SCRIPT_PATH) + if (!ownScript.exists()) { + return + } + println("Preserving the `$MODULE_SCRIPT_PATH` of `${repo.name}`.") + val destination = location.resolve(BUILD_SRC).resolve(MODULE_SCRIPT_PATH) + destination.parent.createDirectories() + ownScript.copyTo(destination, overwrite = true) + } + /** * Replaces the `config` folder in this cloned repository by the contents * of the folder defined by the [source]. @@ -280,6 +342,8 @@ class ClonedRepo( * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths * when copying. * + * Build output and Git metadata of the [source] are not copied. + * * Returns this instance of `ClonedRepo`, for call chaining. */ fun replaceConfig(source: Path, ignoredFolder: Path?): ClonedRepo { @@ -287,19 +351,30 @@ class ClonedRepo( return this } - private fun replaceFolder(folderName: String, source: Path, ignoredFolder: Path?) { + /** + * Replaces the [folderName] folder of this repository by the contents of the [source]. + * + * The original folder, if any, is renamed into `-original`. + * + * Returns the path to which the original folder was renamed, or `null` + * if this repository has no such folder. + */ + private fun replaceFolder(folderName: String, source: Path, ignoredFolder: Path?): Path? { val folder = location.resolve(folderName) val rawFolder = folder.toFile() + var renamedInto: Path? = null if (rawFolder.exists() && rawFolder.isDirectory) { - val toRenameInto = location.resolve(folderName + "-original") + val toRenameInto = location.resolve("$folderName-original") println("Renaming ${folder.toAbsolutePath()} into ${toRenameInto.toAbsolutePath()}.") - rawFolder.renameTo(toRenameInto.toFile()) + Files.move(folder, toRenameInto) + renamedInto = toRenameInto } println( "Copying the files from ${source.toAbsolutePath()} " + "into ${folder.toAbsolutePath()}." ) copyFolder(source, ignoredFolder, folder) + return renamedInto } @Suppress("TooGenericExceptionCaught") @@ -313,22 +388,93 @@ class ClonedRepo( } } + /** + * Copies the [sourceFolder] into the [destinationFolder], skipping the [ignoredFolder] + * along with the build output and the Git metadata. + * + * A symbolic link to a folder is copied as an empty folder, as the tree is walked + * without following the links. A link that does not resolve is skipped: `config` + * has such links when its `.agents/shared` submodule is not checked out. + */ private fun copyRecursively(sourceFolder: Path, ignoredFolder: Path?, destinationFolder: Path) { - fun Path.isIgnored(): Boolean = ignoredFolder - ?.let { toAbsolutePath().startsWith(it.toAbsolutePath()) } - ?: false - - val flattenedTree = Files.walk(sourceFolder).filter { it.isIgnored().not() } - val filesToDestinations = flattenedTree.map { file -> - val destination = destinationFolder.resolve(sourceFolder.relativize(file)) - file to destination + val ignored = ignoredFolder?.toAbsolutePath() + + fun Path.isIgnored(): Boolean = + ignored?.let { toAbsolutePath().startsWith(it) } ?: false + + fun Path.destination(): Path = destinationFolder.resolve(sourceFolder.relativize(this)) + + // The tree is walked rather than streamed, so that an entry which is not copied + // is not descended into either. During the test build the `tmp` folder alone holds + // the full clones of all the tested repositories. + Files.walkFileTree(sourceFolder, object : SimpleFileVisitor() { + + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + if (dir != sourceFolder && (dir.isIgnored() || dir.isNonSource())) { + return SKIP_SUBTREE + } + dir.destination().createDirectories() + return CONTINUE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val danglingLink = attrs.isSymbolicLink && file.notExists() + if (!danglingLink && !file.isIgnored() && !file.isNonSource()) { + file.copyTo(file.destination()) + } + return CONTINUE + } + }) + } + + /** + * Tells if this entry holds the output of a build or the Git metadata, + * and hence must not be copied. + * + * The build output of the local `config` is unrelated to the repository under test. + * Git metadata additionally holds the IPC sockets of the Git file system monitor; + * these cannot be copied at all. + * + * A `build` folder is recognized only next to a Gradle build script, so that + * the source packages named `build` are copied as usual. + */ + private fun Path.isNonSource(): Boolean { + if (name in NON_SOURCE_ENTRIES) { + return true } + return name == BUILD_OUTPUT && parent?.holdsGradleBuild() == true + } + + private fun Path.holdsGradleBuild(): Boolean = BUILD_SCRIPTS.any { resolve(it).exists() } + + private companion object { + + /** + * Names of the entries that never hold sources, at any depth of a project. + * + * @see ClonedRepo.isNonSource + */ + val NON_SOURCE_ENTRIES = setOf(".git", ".gradle", ".kotlin") + + /** + * The name of the folder that holds the output of a Gradle build. + * + * @see ClonedRepo.isNonSource + */ + const val BUILD_OUTPUT = "build" - val directories = filesToDestinations.filter { Files.isDirectory(it.first) } - directories.forEach { Files.createDirectories(it.second) } + /** + * The names of the scripts by which a folder is recognized as a Gradle build. + */ + val BUILD_SCRIPTS = setOf("build.gradle.kts", "settings.gradle.kts") - val files = filesToDestinations.filter { Files.isDirectory(it.first).not() } - files.forEach { Files.copy(it.first, it.second) } + /** + * The path, relative to `buildSrc`, to the script that each project + * defines for itself. + * + * @see ClonedRepo.restoreModuleScript + */ + const val MODULE_SCRIPT_PATH = "src/main/kotlin/module.gradle.kts" } } @@ -353,5 +499,8 @@ object SpineRepos { /** * A name of a Git branch. + * + * The [name] is a plain branch name, such as `master` or `grpc-concurrency-fixes`. + * Neither the `refs/heads/` prefix nor the name of a remote is expected. */ data class Branch(val name: String) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtobufRuntime.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtobufRuntime.kt new file mode 100644 index 0000000000..715a19943e --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtobufRuntime.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME +import groovy.util.Node +import io.spine.dependency.lib.Protobuf +import org.gradle.api.Project +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.add +import org.gradle.kotlin.dsl.named + +/** + * Leaves the Protobuf runtime ([Protobuf.libs]) out of the fat JAR built by + * the `shadowJar` task of this project, declaring it as a `runtime` dependency of + * the published artifacts instead. + * + * A bundled copy of the runtime cannot be relocated, as the runtime depends on + * the names of its classes. Left unrelocated, it shadows the Protobuf runtime that + * a consumer resolves for its own classpath, and no `force(...)` can replace it: + * the copy is bytecode inside another artifact, not a dependency. Should the copy + * be older than the code generated for the consumer, loading that code fails with + * `ProtobufRuntimeVersionException`. Declared as a dependency, the runtime takes part + * in the dependency resolution of the consumer, so the version it resolves wins. + * + * The libraries are declared: + * 1. in the `protobufRuntime` configuration, which the `shadowRuntimeElements` + * configuration of the Shadow plugin extends, so that the Gradle module metadata + * of a publication of the shadowed variant, such as the publication of a Gradle + * plugin, lists them; + * 2. in the POM of the given [publication], which publishes the fat JAR with + * `artifact(...)` and so receives no dependencies from Gradle. + * + * All the libraries of [Protobuf.libs] are declared, whichever of them the fat JAR + * used to bundle. They are declared in the `runtime` scope, so they no longer reach + * the compile classpath of the consumers of the fat JAR, which suits the tools and + * plugins published as fat JARs. + * + * The `shadow` configuration is not used for this purpose: Shadow writes the files of + * that configuration into the `Class-Path` attribute of the JAR manifest. Such entries + * are relative to the JAR and carry the Protobuf version the build resolved, so in + * a directory holding the JAR along with other Protobuf JARs, they would put that + * version first again. + * + * Each library is declared non-transitively, so only the libraries themselves are + * declared, `protobuf-java` among them. The rest of their dependencies stays where + * it was: bundled into the fat JAR (Guava, Gson), or provided where the JAR runs, + * as Gradle provides the Kotlin standard library to plugins. So consumers get + * the same classes as before, except that those of the Protobuf runtime come from + * genuine artifacts. + * + * The libraries are excluded by the dependency filter of the `shadowJar` task rather + * than by the paths of their entries, so that everything they contribute, classes and + * resources alike, is left out, and the SBOM published with the fat JAR does not + * describe them as [bundled][SbomContent.bundled]. + * + * Call this function after applying the Shadow and `maven-publish` plugins and + * creating the publication of the fat JAR, and after any code writing dependencies + * into the POM of the publication, so that the POM keeps a single `dependencies` + * element: + * ```kotlin + * unbundleProtobufRuntime() // The `fatJar` of `uber-jar-module`. + * unbundleProtobufRuntime(publication = "cliFatJar") // A publication of its own. + * ``` + * A project publishing its fat JAR in several publications calls the function + * once for each of them. + * + * @param publication The name of the publication of the fat JAR. Defaults to + * `fatJar`, the publication created by `uber-jar-module`. + */ +fun Project.unbundleProtobufRuntime(publication: String = "fatJar") { + if (configurations.findByName(PROTOBUF_RUNTIME) == null) { + declareProtobufRuntime() + } + publications.named(publication) { + declareRuntimeDependencies(Protobuf.libs) + } +} + +/** + * Declares the Protobuf runtime in the `protobufRuntime` configuration, which + * the `shadowRuntimeElements` configuration extends, and leaves it out of + * the fat JAR built by the `shadowJar` task. + */ +private fun Project.declareProtobufRuntime() { + val protobufRuntime = configurations.create(PROTOBUF_RUNTIME) { + description = "The Protobuf runtime, which the published artifacts declare" + + " rather than bundle." + isCanBeConsumed = false + isCanBeResolved = false + } + Protobuf.libs.forEach { + dependencies.add(protobufRuntime.name, it) { + isTransitive = false + } + } + configurations.named(ShadowJavaPlugin.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { + extendsFrom(protobufRuntime) + } + tasks.named(SHADOW_JAR_TASK_NAME) { + dependencies { + exclude { "${it.moduleGroup}:${it.moduleName}" in protobufRuntimeModules } + } + } +} + +/** + * The name of the configuration holding the Protobuf runtime, which the published + * artifacts declare rather than bundle. + */ +private const val PROTOBUF_RUNTIME = "protobufRuntime" + +/** + * The `group:name` coordinates of the Protobuf runtime libraries. + */ +private val protobufRuntimeModules: Set = + Protobuf.libs.mapTo(mutableSetOf()) { it.substringBeforeLast(':') } + +/** + * Declares the dependencies on the given `group:name:version` [coordinates] + * in the POM of this publication, in the `runtime` scope and without + * transitive dependencies. + * + * The dependencies are added to the `dependencies` element of the POM, + * which is created if the POM has none. + */ +private fun MavenPublication.declareRuntimeDependencies(coordinates: List) { + pom.withXml { + val root = asNode() + val dependencies = root.children() + .filterIsInstance() + .firstOrNull { it.localName == "dependencies" } + ?: root.appendNode("dependencies") + coordinates.forEach { + // Prefixed with `lib`, as plain `name` and `version` would hide those + // of the publication. + val (libGroup, libName, libVersion) = it.split(':') + with(dependencies.appendNode("dependency")) { + appendNode("groupId", libGroup) + appendNode("artifactId", libName) + appendNode("version", libVersion) + appendNode("scope", "runtime") + with(appendNode("exclusions").appendNode("exclusion")) { + appendNode("groupId", "*") + appendNode("artifactId", "*") + } + } + } + } +} + +/** + * The name of this node without its namespace. + * + * The name is a `QName` for a node parsed with namespaces, as the nodes of a POM are. + * It renders as `{namespace}localName`. + */ +private val Node.localName: String + get() = name().toString().substringAfterLast('}') diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt index 1d4bb96798..22d7d73651 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt @@ -38,6 +38,11 @@ import org.gradle.kotlin.dsl.withType */ private const val MAVEN_PUBLISH = "maven-publish" +/** + * The name of the Gradle plugin that adds test fixtures to a project. + */ +private const val JAVA_TEST_FIXTURES = "java-test-fixtures" + /** * Abstract base for handlers of publications in a project * with [spinePublishing] settings declared. @@ -95,6 +100,7 @@ sealed class PublicationHandler( // is applied. pluginManager.withPlugin(MAVEN_PUBLISH) { handlePublications() + suppressTestFixturesPomWarnings() registerDestinations() configurePublishTask(destinations) applied = true @@ -109,6 +115,27 @@ sealed class PublicationHandler( */ abstract fun handlePublications() + /** + * Silences the warnings that Gradle prints about the test fixtures variants + * when it generates the POM of a Maven publication of the [project]. + * + * The `java-test-fixtures` plugin adds the test fixtures to the `java` component + * as variants with a capability of their own. Gradle Module Metadata (`.module`) + * keeps them, while a POM cannot describe a capability, so Gradle leaves them + * out of it by design. Hence, the warnings carry no information. + * + * The publications added after this handler is applied, such as `pluginMaven` + * of `java-gradle-plugin`, are silenced as well. + */ + private fun suppressTestFixturesPomWarnings() { + project.pluginManager.withPlugin(JAVA_TEST_FIXTURES) { + project.publications.withType().configureEach { + suppressPomMetadataWarningsFor("testFixturesApiElements") + suppressPomMetadataWarningsFor("testFixturesRuntimeElements") + } + } + } + /** * Goes through the [destinations] and registers each as a repository for publishing * in the given Gradle project. diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/ClonedRepoSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/ClonedRepoSpec.kt new file mode 100644 index 0000000000..c9f102c5ac --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/ClonedRepoSpec.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle + +import io.kotest.matchers.shouldBe +import java.net.URI +import java.nio.file.Path +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.exists +import kotlin.io.path.readText +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`ClonedRepo` should") +internal class ClonedRepoSpec { + + @TempDir + lateinit var tempDir: Path + + /** The `buildSrc` of the local `config` to be copied. */ + private lateinit var source: Path + + /** The root of the repository under test. */ + private lateinit var location: Path + + @BeforeEach + fun prepareFolders() { + source = tempDir.resolve("config").resolve(BUILD_SRC) + location = tempDir.resolve("repo") + } + + @Test + fun `copy the files, and not only the folders`() { + source.createFile("quality/detekt-config.yml", "The quality settings.") + source.createFile("src/main/kotlin/io/spine/dependency/build/Ksp.kt", "object Ksp") + + replaceBuildSrc() + + val copied = location.resolve(BUILD_SRC) + copied.resolve("quality/detekt-config.yml").readText() shouldBe "The quality settings." + copied.resolve("src/main/kotlin/io/spine/dependency/build/Ksp.kt") + .readText() shouldBe "object Ksp" + } + + @Test + fun `skip the build output and the Git metadata`() { + source.createFile(BUILD_SCRIPT, "The build script.") + source.createFile("src/main/kotlin/Deps.kt", "object Deps") + source.createFile("build/classes/Deps.class", "The compiled code.") + source.createFile(".gradle/file-system.probe", "The probe.") + source.createFile(".kotlin/sessions/session.salive", "The session.") + source.createFile(".git/HEAD", "ref: refs/heads/master") + + replaceBuildSrc() + + val copied = location.resolve(BUILD_SRC) + copied.resolve("src/main/kotlin/Deps.kt").exists() shouldBe true + copied.resolve("build").exists() shouldBe false + copied.resolve(".gradle").exists() shouldBe false + copied.resolve(".kotlin").exists() shouldBe false + copied.resolve(".git").exists() shouldBe false + } + + @Test + fun `skip the build output of a nested Gradle project`() { + val config = tempDir.resolve("config") + config.createFile(BUILD_SCRIPT, "The build script.") + config.createFile("$BUILD_SRC/settings.gradle.kts", "The settings of `buildSrc`.") + config.createFile("$BUILD_SRC/build/classes/Deps.class", "The compiled code.") + config.createFile("$BUILD_SRC/.gradle/file-system.probe", "The probe.") + config.createFile("$BUILD_SRC/src/main/kotlin/io/spine/dependency/build/Ksp.kt", "object Ksp") + + val repo = GitRepository(URI("https://github.com/SpineEventEngine/base")) + ClonedRepo(repo, location).replaceConfig(config, null) + + val copied = location.resolve("config") + copied.resolve("$BUILD_SRC/build").exists() shouldBe false + copied.resolve("$BUILD_SRC/.gradle").exists() shouldBe false + copied.resolve("$BUILD_SRC/src/main/kotlin/io/spine/dependency/build/Ksp.kt") + .readText() shouldBe "object Ksp" + } + + @Test + fun `preserve the 'module gradle kts' of the repository under test`() { + source.createFile(MODULE_SCRIPT, TEMPLATE_MODULE_SCRIPT) + location.resolve(BUILD_SRC).createFile(MODULE_SCRIPT, OWN_MODULE_SCRIPT) + + replaceBuildSrc() + + location.resolve(BUILD_SRC).resolve(MODULE_SCRIPT) + .readText() shouldBe OWN_MODULE_SCRIPT + location.resolve("$BUILD_SRC-original").resolve(MODULE_SCRIPT) + .readText() shouldBe OWN_MODULE_SCRIPT + } + + @Test + fun `take the template 'module gradle kts' when the repository has none`() { + source.createFile(MODULE_SCRIPT, TEMPLATE_MODULE_SCRIPT) + + replaceBuildSrc() + + location.resolve(BUILD_SRC).resolve(MODULE_SCRIPT) + .readText() shouldBe TEMPLATE_MODULE_SCRIPT + } + + @Test + fun `skip a link that does not resolve`() { + source.createFile("src/main/kotlin/Deps.kt", "object Deps") + val link = source.resolve("guidelines") + link.createSymbolicLinkPointingTo(source.resolve("shared/guidelines")) + + replaceBuildSrc() + + val copied = location.resolve(BUILD_SRC) + copied.resolve("src/main/kotlin/Deps.kt").exists() shouldBe true + copied.resolve("guidelines").exists() shouldBe false + } + + @Test + fun `skip the ignored folder`() { + source.createFile("src/main/kotlin/Deps.kt", "object Deps") + source.createFile("tmp/base/README.md", "The checked out sources.") + + replaceBuildSrc(ignoredFolder = source.resolve("tmp")) + + val copied = location.resolve(BUILD_SRC) + copied.resolve("src/main/kotlin/Deps.kt").exists() shouldBe true + copied.resolve("tmp").exists() shouldBe false + } + + private fun replaceBuildSrc(ignoredFolder: Path? = null) { + val repo = GitRepository(URI("https://github.com/SpineEventEngine/base")) + ClonedRepo(repo, location).replaceBuildSrc(source, ignoredFolder) + } + + private companion object { + + const val BUILD_SRC = "buildSrc" + const val BUILD_SCRIPT = "build.gradle.kts" + const val MODULE_SCRIPT = "src/main/kotlin/module.gradle.kts" + const val TEMPLATE_MODULE_SCRIPT = "// A template to be defined by a project." + const val OWN_MODULE_SCRIPT = "plugins { `java-library` }" + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/GitRepositorySpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/GitRepositorySpec.kt new file mode 100644 index 0000000000..791a9ca7c1 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/GitRepositorySpec.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.paths.shouldNotExist +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import java.io.File +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.readText +import org.ajoberstar.grgit.Grgit +import org.eclipse.jgit.api.errors.GitAPIException +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`GitRepository` should") +internal class GitRepositorySpec { + + @TempDir + lateinit var tempDir: Path + + /** The repository to be cloned, playing the role of a remote. */ + private lateinit var origin: Path + + /** The folder the repository is checked out into. */ + private lateinit var destination: File + + /** The root of the checked-out repository. */ + private val clone: Path + get() = destination.toPath().resolve(REPO_NAME) + + /** + * Creates the repository to be cloned, with two branches. + * + * Both branches carry [SHARED_FILE]. Only [FEATURE] carries [FEATURE_FILE], + * which thus tells the branches apart in the clone. + * + * The created repository is left at [MASTER], so that this is the branch + * a clone checks out by default. + */ + @BeforeEach + fun createOrigin() { + origin = tempDir.resolve(REPO_NAME).createDirectories() + destination = tempDir.resolve("checkout").toFile() + Grgit.init(mapOf("dir" to origin)).use { git -> + origin.createFile(SHARED_FILE, SHARED_TEXT) + git.commitAll("Add the file shared by all branches.") + + // Git does not mandate the name of the branch a repository starts with, + // and JGit honors `init.defaultBranch`, so make sure `master` exists. + if (git.branch.current().name != MASTER) { + git.checkout(mapOf("branch" to MASTER, "createBranch" to true)) + } + + git.checkout(mapOf("branch" to FEATURE, "createBranch" to true)) + origin.createFile(FEATURE_FILE, FEATURE_TEXT) + git.commitAll("Add the file of the feature branch.") + + git.checkout(mapOf("branch" to MASTER)) + } + } + + @Test + fun `check out the default branch`() { + GitRepository(origin.toUri()).checkout(destination) + + clone.resolve(SHARED_FILE).readText() shouldBe SHARED_TEXT + clone.resolve(FEATURE_FILE).shouldNotExist() + headOfClone() shouldBe "$MASTER -> origin/$MASTER" + } + + /** + * Verifies that a branch is resolved by its plain name and is checked out + * as a local branch rather than as a detached `HEAD`. + * + * A fresh clone has only the default branch among its local refs, so the branch + * has to be resolved against the refs advertised by the remote. `HEAD` detached + * at `origin/` would hold the right files just as well, which is why + * the `HEAD` of the clone is asserted too. + */ + @Test + fun `check out a branch other than the default one`() { + GitRepository(origin.toUri(), Branch(FEATURE)).checkout(destination) + + clone.resolve(FEATURE_FILE).readText() shouldBe FEATURE_TEXT + headOfClone() shouldBe "$FEATURE -> origin/$FEATURE" + } + + /** + * Verifies that a missing branch fails the checkout right away. + * + * Were the clone to succeed and leave an empty working tree behind, the Gradle + * build of the repository would fail later on with a far less obvious message. + */ + @Test + fun `fail to check out a branch that does not exist`() { + val repo = GitRepository(origin.toUri(), Branch(MISSING)) + + val error = shouldThrow { repo.checkout(destination) } + + // `GitAPIException` roots the whole JGit error hierarchy, so the branch + // has to be named for this to pin the branch resolution rather than, + // say, an unreadable `origin`. + error.message shouldContain MISSING + } + + /** + * Returns the current branch of the clone, along with the branch it tracks, + * as ` -> `. + * + * A detached `HEAD` reads as `HEAD -> null`. + */ + private fun headOfClone(): String = + Grgit.open(mapOf("dir" to clone)).use { + val head = it.branch.current() + "${head.name} -> ${head.trackingBranch?.name}" + } + + private companion object { + + /** The name of the folder the repository is cloned into. */ + const val REPO_NAME = "acme-lib" + + const val MASTER = "master" + const val FEATURE = "grpc-concurrency-fixes" + const val MISSING = "no-such-branch" + + const val SHARED_FILE = "README.md" + const val SHARED_TEXT = "The file shared by all branches." + + const val FEATURE_FILE = "src/main/kotlin/Concurrency.kt" + const val FEATURE_TEXT = "object Concurrency" + + /** + * Commits all the files of this repository, including the new ones. + */ + fun Grgit.commitAll(message: String) { + add(mapOf("patterns" to setOf("."))) + commit(mapOf("message" to message)) + } + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/PathExts.kt b/buildSrc/src/test/kotlin/io/spine/gradle/PathExts.kt new file mode 100644 index 0000000000..c0ae947923 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/PathExts.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle + +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +/** + * Creates a file under this folder, along with the folders leading to it. + */ +internal fun Path.createFile(relativePath: String, content: String): Path { + val file = resolve(relativePath) + file.parent.createDirectories() + file.writeText(content) + return file +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/ProtobufRuntimeIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/ProtobufRuntimeIgTest.kt new file mode 100644 index 0000000000..f6a7d204b5 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/ProtobufRuntimeIgTest.kt @@ -0,0 +1,519 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.kotest.inspectors.forAll +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.spine.dependency.lib.Protobuf +import java.io.File +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import java.util.zip.ZipEntry +import javax.xml.parsers.DocumentBuilderFactory +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.io.TempDir +import org.w3c.dom.Element + +/** + * Verifies [unbundleProtobufRuntime] in a real build of fat JARs, run via Gradle TestKit. + * + * The fixture is built once for the whole class, and each case inspects what + * the build left behind. The build runs with `--offline`, resolving from a local + * `file:` repository, which holds stand-ins for the Protobuf runtime libraries. + * `protobuf-java-util` depends on `com.example:gson`, as the genuine one depends + * on Gson. The build also runs with the configuration cache. + * + * All the modules of the fixture apply Shadow and depend on `com.example:lib`. + * The fixture has: + * - `fat` — a module depending on the Protobuf runtime. It publishes its fat JAR + * in the `cliFatJar` publication, with `artifact(...)`, as the Compiler CLI does, + * and its Java component, which carries the shadowed variant, in the `library` + * publication; + * - `component` — a module whose `library` publication of its Java component is + * given to the function, so that the POM the function adds to already has + * the `dependencies` element Gradle writes; + * - `twin` — a module depending on the Protobuf runtime. It publishes its fat JAR + * in two publications: `fatJar`, as `uber-jar-module` does, and `extra`, whose POM + * gets a `dependencies` element written by hand before the function adds to it. + * The function is called once for each publication. + */ +@DisplayName("`unbundleProtobufRuntime` should") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ProtobufRuntimeIgTest { + + private lateinit var projectDir: File + + @BeforeAll + fun buildFixture(@TempDir dir: File) { + projectDir = dir + writeRepository() + writeBuild() + GradleRunner.create() + .withProjectDir(projectDir) + .withArguments( + ":fat:shadowJar", + ":fat:${pomTaskOf(cliFatJar)}", + ":fat:generateMetadataFileForLibraryPublication", + ":fat:$listIncludedTask", + ":component:${pomTaskOf(library)}", + ":twin:${pomTaskOf(defaultFatJar)}", + ":twin:${pomTaskOf(extra)}", + "--offline", + "--configuration-cache", + "--stacktrace", + ) + .build() + } + + @Test + fun `leave the Protobuf runtime out of the fat JAR`() { + val entries = fatJarEntries() + + entries.filter { it.startsWith("com/google/protobuf/") }.shouldBeEmpty() + entries shouldNotContain "META-INF/protobuf-kotlin.kotlin_module" + } + + @Test + fun `keep bundling the other dependencies, including those of the runtime`() { + fatJarEntries() shouldContainAll listOf( + "com/example/Lib.class", + "com/example/gson/Gson.class", + ) + } + + @Test + fun `exclude the runtime by the dependency filter of the fat JAR`() { + val included = file("fat/build/included.txt").readLines() + + included shouldContainExactlyInAnyOrder listOf("lib-1.0.jar", "gson-1.0.jar") + } + + @Test + fun `not name the runtime in the manifest of the fat JAR`() { + JarFile(fatJarFile()).use { jar -> + val manifest = jar.manifest.shouldNotBeNull() + manifest.mainAttributes.getValue("Class-Path").shouldBeNull() + } + } + + @Test + fun `declare the runtime in the POM of the fat JAR`() { + val dependencies = pomDependencies("fat", cliFatJar) + val artifactIds = dependencies.map { it.childText("artifactId") } + + artifactIds shouldContainExactlyInAnyOrder runtimeLibraries + dependencies.forAll { it.shouldBeRuntimeLibrary() } + } + + @Test + fun `declare the runtime in the shadowed variant`() { + val metadata = ObjectMapper().readTree(file("fat/build/publications/library/module.json")) + val variants = metadata["variants"].shouldNotBeNull() + .filter { it["name"]?.asText() == shadowedVariant } + variants.shouldBeSingleton() + val dependencies = variants.single()["dependencies"].shouldNotBeNull().toList() + val modules = dependencies.map { it["module"]?.asText() } + + modules shouldContainExactlyInAnyOrder runtimeLibraries + dependencies.forAll { + it["group"]?.asText() shouldBe Protobuf.group + it["version"]?.get("requires")?.asText() shouldBe Protobuf.version + it.excludes() shouldContainExactly listOf("*" to "*") + } + } + + @Test + fun `add to the dependencies which Gradle writes into the POM`() { + // The shadowed variant of the Java component adds the runtime as optional. + val dependencies = pomDependencies("component", library) + .filter { it.childTextOrNull("optional") == null } + val artifactIds = dependencies.map { it.childText("artifactId") } + + artifactIds shouldContainExactly listOf("lib") + runtimeLibraries + dependencies.drop(1).forAll { it.shouldBeRuntimeLibrary() } + } + + @Test + fun `add to the dependencies written into the POM by hand`() { + val dependencies = pomDependencies("twin", extra) + val artifactIds = dependencies.map { it.childText("artifactId") } + + artifactIds shouldContainExactly listOf("lib") + runtimeLibraries + dependencies.drop(1).forAll { it.shouldBeRuntimeLibrary() } + } + + @Test + fun `declare the runtime in each publication it is called for`() { + val dependencies = pomDependencies("twin", defaultFatJar) + val artifactIds = dependencies.map { it.childText("artifactId") } + + artifactIds shouldContainExactlyInAnyOrder runtimeLibraries + dependencies.forAll { it.shouldBeRuntimeLibrary() } + } + + private fun fatJarFile(): File = file("fat/build/libs/fat-$moduleVersion-all.jar") + + private fun fatJarEntries(): List = + JarFile(fatJarFile()).use { jar -> jar.entries().toList().map { it.name } } + + /** + * Returns the `dependency` elements of the POM of the given [publication] of + * the given [module], ensuring the POM has a single `dependencies` element. + */ + private fun pomDependencies(module: String, publication: String): List { + val pom = file("$module/build/publications/$publication/pom-default.xml") + val project = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(pom) + .documentElement + val dependencies = project.childElements("dependencies") + dependencies.shouldBeSingleton() + return dependencies.single().childElements("dependency") + } + + /** + * Writes the local repository with the stand-ins for the Protobuf runtime + * and the other libraries of the fixture. + */ + private fun writeRepository() { + library( + Protobuf.javaLib, + entries = listOf("com/google/protobuf/RuntimeVersion.class"), + ) + library( + Protobuf.javaUtil, + entries = listOf("com/google/protobuf/util/JsonFormat.class"), + dependencies = listOf(Protobuf.javaLib, gson), + ) + library( + Protobuf.kotlin, + entries = listOf( + "com/google/protobuf/kotlin/DslList.class", + "META-INF/protobuf-kotlin.kotlin_module", + ), + dependencies = listOf(Protobuf.javaLib), + ) + library(lib, entries = listOf("com/example/Lib.class")) + library(gson, entries = listOf("com/example/gson/Gson.class")) + } + + /** + * Writes the POM and the JAR of the library with the given `group:name:version` + * [coordinates] into the local repository. + * + * @param entries The paths of the empty entries of the JAR. + * @param dependencies The `group:name:version` coordinates of the dependencies + * the POM declares. + */ + private fun library( + coordinates: String, + entries: List, + dependencies: List = emptyList(), + ) { + val (groupId, artifactId, version) = coordinates.split(':') + val path = "repository/${groupId.replace('.', '/')}/$artifactId/$version" + val declared = dependencies.joinToString("\n") { dependency -> + val (depGroup, depArtifact, depVersion) = dependency.split(':') + """ + | + | $depGroup + | $depArtifact + | $depVersion + | + """.trimMargin() + } + val dependenciesElement = if (dependencies.isEmpty()) "" else + " \n$declared\n " + write( + "$path/$artifactId-$version.pom", + """ + | + | + | 4.0.0 + | $groupId + | $artifactId + | $version + |$dependenciesElement + | + |""".trimMargin() + ) + val jar = file("$path/$artifactId-$version.jar") + JarOutputStream(jar.outputStream(), Manifest()).use { out -> + entries.forEach { + out.putNextEntry(ZipEntry(it)) + out.closeEntry() + } + } + } + + private fun writeBuild() { + write( + "settings.gradle.kts", + """ + rootProject.name = "protobuf-runtime-sample" + include("fat", "component", "twin") + """.trimIndent() + ) + write( + "build.gradle.kts", + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + + allprojects { + group = "io.spine.test" + version = "$moduleVersion" + repositories { + maven { url = uri("${'$'}{rootDir}/repository") } + } + } + """.trimIndent() + ) + writeFatModule() + writeComponentModule() + writeTwinModule() + } + + private fun writeFatModule() { + val body = """ + |publishing { + | publications { + | create("$cliFatJar") { + | artifactId = "fat-all" + | artifact(tasks.shadowJar) + | } + | create("$library") { + | from(components["java"]) + | } + | } + |} + | + |unbundleProtobufRuntime(publication = "$cliFatJar") + | + |// Lists the files of the dependencies the fat JAR bundles, which the SBOM + |// of a fat JAR describes as bundled. + |tasks.register("$listIncludedTask") { + | val included = files(tasks.shadowJar.map { it.includedDependencies }) + | val listing = layout.buildDirectory.file("included.txt") + | inputs.files(included) + | outputs.file(listing) + | doLast { + | listing.get().asFile.writeText(included.files.joinToString("\n") { it.name }) + | } + |} + |""".trimMargin() + writeModule("fat", body) + } + + private fun writeComponentModule() { + val body = """ + |publishing { + | publications { + | create("$library") { + | from(components["java"]) + | } + | } + |} + | + |unbundleProtobufRuntime(publication = "$library") + |""".trimMargin() + writeModule("component", body, dependOnRuntime = false) + } + + private fun writeTwinModule() { + val body = """ + |publishing { + | publications { + | create("$defaultFatJar") { + | artifactId = "twin-all" + | artifact(tasks.shadowJar) + | } + | create("$extra") { + | artifactId = "twin-extra" + | artifact(tasks.shadowJar) + | pom.withXml { + | val dependency = asNode() + | .appendNode("dependencies") + | .appendNode("dependency") + | dependency.appendNode("groupId", "com.example") + | dependency.appendNode("artifactId", "lib") + | dependency.appendNode("version", "1.0") + | } + | } + | } + |} + | + |unbundleProtobufRuntime() + |unbundleProtobufRuntime(publication = "$extra") + |""".trimMargin() + writeModule("twin", body) + } + + /** + * Writes the build script of the given [module], which applies Shadow, + * depends on `com.example:lib` and, unless told otherwise, on the Protobuf runtime, + * and ends with the given [body]. + */ + private fun writeModule(module: String, body: String, dependOnRuntime: Boolean = true) { + val libs = if (dependOnRuntime) Protobuf.libs else emptyList() + val runtime = libs.joinToString("\n") { "| implementation(\"$it\")" } + val head = """ + |import io.spine.gradle.publish.unbundleProtobufRuntime + | + |plugins { + | `java-library` + | `maven-publish` + | id("com.gradleup.shadow") + |} + | + |dependencies { + | implementation("$lib") + $runtime + |} + |""".trimMargin() + write("$module/build.gradle.kts", "$head\n$body") + } + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + private fun write(relativePath: String, text: String) { + file(relativePath).apply { + parentFile.mkdirs() + writeText(text) + } + } + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + * + * Each entry after the first starts a line of the generated script, so each + * is prefixed with the indentation of the template it is inserted into, which + * `trimIndent()` then removes along with the rest. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } + + private companion object { + + /** The version of the modules of the fixture. */ + const val moduleVersion = "1.0.0" + + /** The name of a publication of a fat JAR, other than the default one. */ + const val cliFatJar = "cliFatJar" + + /** The name of the publication of a fat JAR, which the function uses by default. */ + const val defaultFatJar = "fatJar" + + /** The name of the publication of a fat JAR whose POM declares a dependency. */ + const val extra = "extra" + + /** The name of a publication of a Java component. */ + const val library = "library" + + /** The name of the variant of the Java component published by the Shadow plugin. */ + const val shadowedVariant = "shadowRuntimeElements" + + /** The name of the task listing the dependencies the fat JAR bundles. */ + const val listIncludedTask = "listIncluded" + + /** The library the modules of the fixture bundle. */ + const val lib = "com.example:lib:1.0" + + /** The library `protobuf-java-util` depends on, as the genuine one depends on Gson. */ + const val gson = "com.example:gson:1.0" + + /** The artifact IDs of the Protobuf runtime libraries. */ + val runtimeLibraries: List = + Protobuf.libs.map { it.substringBeforeLast(':').substringAfter(':') } + + /** Returns the name of the task writing the POM of the given [publication]. */ + fun pomTaskOf(publication: String): String = + "generatePomFileFor${publication.replaceFirstChar { it.uppercase() }}Publication" + } +} + +/** + * Asserts that this `dependency` element of a POM declares a Protobuf runtime + * library in the `runtime` scope and without transitive dependencies. + */ +private fun Element.shouldBeRuntimeLibrary() { + childText("groupId") shouldBe Protobuf.group + childText("version") shouldBe Protobuf.version + childText("scope") shouldBe "runtime" + val exclusions = childElements("exclusions") + exclusions.shouldBeSingleton() + val exclusion = exclusions.single().childElements("exclusion") + exclusion.shouldBeSingleton() + exclusion.single().childText("groupId") shouldBe "*" + exclusion.single().childText("artifactId") shouldBe "*" +} + +/** Returns the child elements of this one with the given [tag]. */ +private fun Element.childElements(tag: String): List { + val children = childNodes + return (0 until children.length) + .map { children.item(it) } + .filterIsInstance() + .filter { it.tagName == tag } +} + +/** Returns the text of the child element of this one with the given [tag]. */ +private fun Element.childText(tag: String): String = + childTextOrNull(tag).shouldNotBeNull() + +/** + * Returns the text of the child element of this one with the given [tag], + * or `null` if there is none. + */ +private fun Element.childTextOrNull(tag: String): String? = + childElements(tag).firstOrNull()?.textContent + +/** + * Returns the `group` and `module` of each exclusion of this dependency of + * the Gradle module metadata. + */ +private fun JsonNode.excludes(): List> = + this["excludes"]?.map { it["group"]?.asText() to it["module"]?.asText() }.orEmpty() diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationChecksumsIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationChecksumsIgTest.kt index b27f6b57f3..48a1b6686e 100644 --- a/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationChecksumsIgTest.kt +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationChecksumsIgTest.kt @@ -79,7 +79,7 @@ internal class PublicationChecksumsIgTest { // `javadocJar` of `spinePublishing` is wired to Dokka, which // this fixture does not apply: the manifest is what is under // test, not the documentation toolchain. - tasks.register("dokkaGeneratePublicationJavadoc") + $generatorStub } spinePublishing { @@ -166,6 +166,34 @@ internal class PublicationChecksumsIgTest { names shouldContain "spine-api-$version-sources.jar" } + /** + * A module that generates no documentation still publishes the JARs that + * would hold it: `disableDocumentationTasks()` stops the generators, not + * the JARs, so each is published containing only a manifest. + * + * Were a JAR stopped along with its generator, the publication would go on + * declaring a file that nothing writes. Publishing to Maven Local tolerates + * that today, so `collectPublicationChecksums` — the first task to read + * every declared file — is where such a module fails the build. + */ + @Test + fun `cover a module that generates no documentation`() { + val script = file("build.gradle.kts") + script.writeText( + script.readText().replace( + generatorStub, + "$generatorStub\n disableDocumentationTasks()" + ) + ) + + runGradle(aggregator, "publishAllPublicationsToStageRepository") + + val names = manifestEntries().map { it.name }.sorted() + names shouldContain "spine-api-$version-javadoc.jar" + names shouldContain "spine-api-$version-html-docs.jar" + names shouldContainExactly publishedFileNames() + } + /** * Maven deploys a version ending in `-SNAPSHOT` under a timestamped name * assigned at upload time, so the names derived from the publication belong @@ -272,6 +300,16 @@ internal class PublicationChecksumsIgTest { const val version = "1.0.0" + /** + * The statement of the fixture that registers a stub of the Dokka task + * feeding `javadocJar`. + * + * It anchors the per-test additions to the configuration shared by + * the modules. The fixture is written from this constant too, so that + * an edit of one cannot leave the other matching nothing. + */ + const val generatorStub = """tasks.register("dokkaGeneratePublicationJavadoc")""" + val sidecars = listOf(".md5", ".sha1", ".sha256", ".sha512") } } diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationHandlerIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationHandlerIgTest.kt new file mode 100644 index 0000000000..67f55727f3 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublicationHandlerIgTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import java.io.File +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.io.TempDir + +/** + * Verifies that [PublicationHandler] silences the warnings that Gradle prints when + * it generates the POMs of modules applying `java-test-fixtures`, in a real + * multi-module build run via Gradle TestKit. + * + * The fixture is built once for the whole class, and each case inspects the output + * of the build. All the modules of the fixture apply `java-test-fixtures`. + * The fixture has: + * - `library` — a module published in the standard way. + * - `plugin` — a module applying `java-gradle-plugin`, listed by the root project + * in `modulesWithCustomPublishing`. Its `pluginMaven` publication is created + * after the handler is applied. + * - `unhandled` — a module that `spinePublishing` does not publish. It declares + * a publication of the `java` component itself, so that the fixture shows + * the warnings the other modules would print. + */ +@DisplayName("`PublicationHandler` should") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class PublicationHandlerIgTest { + + private lateinit var projectDir: File + private lateinit var output: String + + @BeforeAll + fun buildFixture(@TempDir dir: File) { + projectDir = dir + writeBuild() + output = GradleRunner.create() + .withProjectDir(projectDir) + .withArguments( + pomTask(library, standardPublication), + pomTask(pluginModule, pluginPublication), + pomTask(unhandled, unhandledPublication), + "--stacktrace", + ) + .build() + .output + } + + @Test + fun `silence the warnings about test fixtures for a standard publication`() { + output shouldNotContain warningsHeader(standardPublication) + } + + @Test + fun `silence the warnings about test fixtures for a publication added by a plugin`() { + output shouldNotContain warningsHeader(pluginPublication) + } + + @Test + fun `leave the warnings for a publication of a module it does not handle`() { + val header = warningsHeader(unhandledPublication) + output shouldContain header + val warnings = output.substringAfter(header) + warnings shouldContain "Variant testFixturesApiElements" + warnings shouldContain "Variant testFixturesRuntimeElements" + } + + private fun writeBuild() { + write( + "settings.gradle.kts", + """ + rootProject.name = "test-fixtures-sample" + include("$library", "$pluginModule", "$unhandled") + """.trimIndent() + ) + write( + "build.gradle.kts", + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + + import io.spine.gradle.publish.spinePublishing + + allprojects { + group = "io.spine.test" + version = "1.0.0" + } + + spinePublishing { + modules = setOf("$library") + modulesWithCustomPublishing = setOf("$pluginModule") + destinations = emptySet() + } + """.trimIndent() + ) + write( + "$library/build.gradle.kts", + """ + plugins { + `java-library` + `java-test-fixtures` + } + + // `javadocJar` of `spinePublishing` is wired to Dokka, which + // this fixture does not apply: only the POMs are under test. + tasks.register("dokkaGeneratePublicationJavadoc") + """.trimIndent() + ) + write( + "$pluginModule/build.gradle.kts", + """ + plugins { + `java-gradle-plugin` + `maven-publish` + `java-test-fixtures` + } + + gradlePlugin { + plugins { + create("sample") { + id = "io.spine.sample" + implementationClass = "io.spine.sample.SamplePlugin" + } + } + } + """.trimIndent() + ) + write( + "$unhandled/build.gradle.kts", + """ + plugins { + `java-library` + `maven-publish` + `java-test-fixtures` + } + + publishing { + publications { + create("$unhandledPublication") { + from(components["java"]) + } + } + } + """.trimIndent() + ) + } + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + private fun write(relativePath: String, text: String) { + file(relativePath).apply { + parentFile.mkdirs() + writeText(text) + } + } + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } + + private companion object { + + /** The module published in the standard way. */ + const val library = "library" + + /** The module applying `java-gradle-plugin`, published in a custom way. */ + const val pluginModule = "plugin" + + /** The module not published by `spinePublishing`. */ + const val unhandled = "unhandled" + + /* + * The names of the publications, as given by the code that creates them. + */ + const val standardPublication = StandardJavaPublicationHandler.PUBLICATION_NAME + const val pluginPublication = "pluginMaven" + const val unhandledPublication = "unhandled" + + fun pomTask(module: String, publication: String): String { + val name = publication.replaceFirstChar { it.uppercase() } + return ":$module:generatePomFileFor${name}Publication" + } + + /** + * Returns the line with which Gradle starts the POM warnings + * for the given [publication]. + */ + fun warningsHeader(publication: String): String = + "Maven publication '$publication' pom metadata warnings" + } +} diff --git a/config b/config index a0cbbbfe66..3516efc5de 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit a0cbbbfe66c12475224563439fb6deb3da71ba58 +Subproject commit 3516efc5dee84e88bc541944ff743f435eec6a1d diff --git a/init-submodules b/init-submodules index 95b916b83a..64bb0857ff 100755 --- a/init-submodules +++ b/init-submodules @@ -17,10 +17,10 @@ # (distributed by `config`), so `git worktree add` always checks it out — it can # therefore bring `config` itself into existence. # -# It initializes ONLY submodules that are BOTH: +# It acts ONLY on submodules that are BOTH: # -# * not yet checked out — those `git submodule status` marks with a leading `-`, -# at the commit the branch pins; and +# * not yet checked out — those `git submodule status` marks with a leading +# `-`; and # # * config-managed — `config` itself (the bootstrap target `pull` lives inside, # which carries no tracked `branch` in a consumer's `.gitmodules`), plus every @@ -28,6 +28,33 @@ # the rule `./config/pull` uses to decide what it floats, so the two scripts # can never disagree about what is shared. # +# Such a submodule is initialized and then advanced to the tip of its tracked +# branch, the way `./config/pull` floats it — NOT left at the commit the +# superproject pins. That pin is not a version anyone maintains: `.gitmodules` +# declares `ignore = all` for `.agents/shared`, which hides the submodule from +# `git status` and `git diff`, and `./config/pull` floats the on-disk copy without +# ever committing the moved gitlink. The pin therefore still holds the value it +# had on the day the submodule was adopted; an audit of all 19 Spine consumer +# repositories on 2026-10-16 found every one of them months stale. Materializing +# it hands a fresh worktree, CI checkout, or cloud session hook scripts, skills, +# and guidelines that are months out of date. +# +# A submodule may declare `branch = .`, which gitmodules(5) defines as the branch +# the superproject is on rather than a branch name. It is resolved before it +# reaches a refspec, because `git fetch origin .` is a `fatal: invalid refspec`. +# Nothing in Spine declares it today — `.agents/shared` tracks `agents`' `master` +# whatever branch the consumer is on — but it is valid in any `.gitmodules`. +# +# Neither the clone nor the float can fail the session start. With no network at +# all the clone is what fails: the submodule is left uninitialized and the +# symlinks keep dangling until a run that has a network, exactly as before this +# script floated anything. A float that fails after a successful clone — a branch +# the remote does not have, a fetch that dies midway, a local branch that is not +# a fast-forward — warns and leaves the submodule wherever the steps that did +# succeed put it: at worst the pin, a usable, if older, tree. The submodule is +# checked out by then, so later runs no longer see a `-` line for it and never +# retry the float — which is why the warning points at `./config/pull`. +# # Consumer-owned submodules (a Hugo theme, a vendored library, documentation # examples, ...) declare no tracked branch and are deliberately left untouched. # Because a `SessionStart` hook runs this script automatically on every session, @@ -35,15 +62,14 @@ # a submodule this project does not manage, on every single start. They are # skipped (noted on stderr). # -# Submodules already present are left exactly as they are, so a tree that floated -# `config` / `.agents/shared` to a branch tip via `./config/pull` is never -# silently rewound to the pin. That makes the script idempotent and safe to run on -# every session start. +# Submodules already present are left exactly as they are, neither rewound to the +# pin nor advanced. That keeps the script idempotent, keeps the runs that find +# nothing missing free of network access, and never disturbs a tree whose +# submodules someone deliberately parked on a particular commit. Run +# `./config/pull` to float those. # -# It does NOT float submodules to their branch tips — run `./config/pull` -# afterwards for that. Unlike `pull`, it depends on no pre-existing `config` -# submodule, so it can bootstrap a bare worktree where `./config/pull` does not -# yet exist. +# Unlike `pull`, this script depends on no pre-existing `config` submodule, so it +# can bootstrap a bare worktree where `./config/pull` does not yet exist. # ################################################################################ @@ -133,19 +159,29 @@ cd "$root" || exit 0 # Nothing to do in a repo without submodules. [ -f .gitmodules ] || exit 0 -# The set of config-managed submodule paths: `config` itself (handled specially — -# it carries no tracked branch, exactly as in `./config/pull`), plus every -# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule. -config_managed_paths() { - printf '%s\n' 'config' +# The config-managed submodules, as `pathbranch` lines: every submodule that +# declares a tracked `branch` in `.gitmodules`, plus `config` itself — which +# carries no tracked branch in a consumer's `.gitmodules`, and which +# `./config/pull` floats to `master` in its step 1. Mirrors `pull`'s rule. The +# declared entries come first, so a `.gitmodules` that does give `config` a +# tracked branch wins over that default. +config_managed() { git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \ - | while read -r key _branch; do + | while read -r key branch; do name=${key#submodule.}; name=${name%.branch} - git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null + path=$(git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null) || continue + printf '%s\t%s\n' "$path" "$branch" done + printf '%s\t%s\n' 'config' 'master' } -managed=$(config_managed_paths | sort -u) +managed=$(config_managed) + +# The branch the submodule at `$1` tracks; empty when that path is not +# config-managed at all. The first match wins, as noted above. +tracked_branch() { + printf '%s\n' "$managed" | awk -F'\t' -v path="$1" '$1 == path { print $2; exit }' +} # `git submodule status` prefixes each uninitialized submodule with `-`; an # initialized one starts with a space (at the pinned commit) or `+` (ahead of it). @@ -153,13 +189,97 @@ managed=$(config_managed_paths | sort -u) # that path is config-managed. git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do [ -n "$path" ] || continue - if printf '%s\n' "$managed" | grep -qxF -- "$path"; then - echo "init-submodules: initializing '$path'" - git submodule update --init --recursive -- "$path" \ - || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2 - else + branch=$(tracked_branch "$path") + if [ -z "$branch" ]; then echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2 + continue + fi + echo "init-submodules: initializing '$path'" + git submodule update --init --recursive -- "$path" &2 + continue + } + # `.gitmodules` may declare `branch = .`, which gitmodules(5) defines as the + # branch the superproject itself is on. That is a sentinel, not a ref: passed + # through, it dies with `fatal: invalid refspec '.'`, and because the clone + # has already succeeded by then the submodule would keep the pin and never be + # retried. Resolve it to the current branch name. A detached superproject has + # no name to inherit, so the float is skipped and the pin kept — the one case + # where `./config/pull` is the only way forward, once on a branch. + if [ "$branch" = "." ]; then + # `git branch --show-current` prints the name exactly, and prints nothing + # when `HEAD` is detached. `git rev-parse --abbrev-ref HEAD` cannot stand + # in: it shortens to a *non-ambiguous* name, so a superproject carrying + # both a branch and a tag called `foo` yields `heads/foo`; the fetch + # then succeeds but the checkout fails, leaving this freshly + # initialized submodule stale and never retried. `./config/pull` + # resolves it the same way, for the same reason. + branch=$(git branch --show-current 2>/dev/null) + if [ -z "$branch" ]; then + echo "init-submodules: WARNING — '$path' tracks the superproject's" \ + "branch ('.'), which is detached here; left at the pin. Run" \ + "'./config/pull' from a branch to float it." >&2 + continue + fi fi + # What that just checked out is the superproject's pin, which nobody + # maintains (see the header). Advance the fresh checkout to the tip of its + # tracked branch, leaving it ON the branch rather than in a detached `HEAD` — + # the same three steps `./config/pull` takes in its step 2, both scripts + # naming the fetch destination and the checkout start point, for the two + # reasons below. A failure leaves the submodule wherever the steps that did + # succeed put it, so the warning promises no particular commit, as `pull`'s + # does not. + # + # The fetch names its destination. A bare `git fetch origin ` relies + # on the refspec configured for the remote to update + # `refs/remotes/origin/` opportunistically as well, which a full + # clone's does. But a submodule declaring `shallow = true` in `.gitmodules` + # is cloned single-branch (a shallow clone implies `--single-branch`), as + # `+refs/heads/:refs/remotes/origin/`, so any other tracked + # branch lands in `FETCH_HEAD` alone, `refs/remotes/origin/` never + # appears, and the checkout below fails. Spelling the refspec out makes the + # float work whatever refspec the clone carries, and the leading `+` matches + # what a default clone configures. + # + # The checkout asks whether the branch exists locally rather than letting Git + # work it out, because both answers are otherwise wrong in a reachable case. + # A fresh clone has a local branch only for the default branch, so `-b` alone + # would fail with `already exists` on the common path. And plain + # `git checkout ` is not a reliable substitute: with no local branch + # of that name it guesses one from a remote-tracking ref, considering only + # refs the remote's fetch refspec covers — so in the single-branch clone + # above it fails even though the explicit fetch did create + # `refs/remotes/origin/` — and if the submodule carries a *tag* of + # that name it quietly resolves the tag and detaches `HEAD` instead, which + # the `--ff-only` below then fast-forwards to the right commit on no branch + # at all. Naming the start point avoids both. + # + # `merge --ff-only` is kept after either path: a local branch that has + # diverged from `origin/` is reported rather than overwritten. + # Nothing here force-moves a ref except the remote-tracking ref the fetch + # owns, which a default clone force-updates anyway. + # + # It merges `refs/remotes/origin/`, not the `origin/` + # shorthand, for the reason the checkout above spells a ref out too. + # gitrevisions(7) resolves a bare name through `refs/tags/` BEFORE + # `refs/remotes/`, so a tag literally named `origin/` wins the + # shorthand — and then this merge would fast-forward the branch the + # checkout just placed exactly onto whatever that tag points at, reporting + # success. Every ref in the chain is therefore fully qualified. + echo "init-submodules: floating '$path' -> origin/$branch" + ( cd "$path" \ + && git fetch --quiet origin \ + "+refs/heads/$branch:refs/remotes/origin/$branch" \ + && if git show-ref --verify --quiet "refs/heads/$branch"; then \ + git checkout --quiet "$branch"; \ + else \ + git checkout --quiet -b "$branch" \ + "refs/remotes/origin/$branch"; \ + fi \ + && git merge --quiet --ff-only "refs/remotes/origin/$branch" ) &2 done # Route Git hooks to the shared hooks directory so the secret-scan `pre-commit` From 77f1819f07efc412e6ef389373431f88301d5ffd Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 21:28:27 +0100 Subject: [PATCH 09/10] Sort field views with `sortedByDeclaration()` Introduce `FieldView`, the common interface of the views of fields with a validation option, and `Iterable.sortedByDeclaration()`. The field views implement the interface via `(is)`; `BoundedFieldView` extends it. Generators and `SetOnceRenderer` now call the extension instead of repeating the sorting lambda. Group the tests of `mangled()` in a nested class of `JavaIdentifiersSpec` and name the suite after the file. Refresh the embedded `RequiredGenerator` snippets and point the built-in option guide at the extension. Co-Authored-By: Claude Opus 5.5 --- .../io/spine/tools/validation/FieldView.kt | 53 +++++++++++ .../validation/bound/BoundedFieldView.kt | 9 +- .../main/proto/spine/validation/views.proto | 6 ++ .../developer/adding-a-built-in-option.md | 6 +- .../developer/java-code-generation.md | 2 +- .../java/generate/option/DistinctGenerator.kt | 3 +- .../java/generate/option/GoesGenerator.kt | 3 +- .../java/generate/option/PatternGenerator.kt | 3 +- .../java/generate/option/RequiredGenerator.kt | 3 +- .../java/generate/option/ValidateGenerator.kt | 3 +- .../generate/option/bound/MaxGenerator.kt | 3 +- .../generate/option/bound/MinGenerator.kt | 3 +- .../generate/option/bound/RangeGenerator.kt | 3 +- .../java/setonce/SetOnceRenderer.kt | 3 +- .../java/generate/JavaIdentifiersSpec.kt | 89 ++++++++++--------- 15 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 context/src/main/kotlin/io/spine/tools/validation/FieldView.kt diff --git a/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt b/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt new file mode 100644 index 0000000000..3f50fc1749 --- /dev/null +++ b/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt @@ -0,0 +1,53 @@ +/* + * 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.tools.validation + +import io.spine.tools.compiler.ast.Field + +/** + * A common interface for the views of fields marked with a validation option. + */ +public interface FieldView { + + /** + * The field in which the option was discovered. + */ + public val subject: Field +} + +/** + * Sorts these views by the order of declaration of their fields. + * + * Querying returns views in no particular order, and the order changes from run + * to run. Sort the views before generating code for them, so that the generated + * code stays the same from build to build. + * + * The views of the fields of one message go in the order in which the fields + * are declared in the message. + */ +public fun Iterable.sortedByDeclaration(): List = + sortedBy { it.subject.orderOfDeclaration } diff --git a/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt b/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt index 9ae8c01379..1f8a276bdb 100644 --- a/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt +++ b/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt @@ -26,18 +26,13 @@ package io.spine.tools.validation.bound -import io.spine.tools.compiler.ast.Field import io.spine.tools.compiler.ast.File +import io.spine.tools.validation.FieldView /** * A common interface for the bounded numeric field views. */ -public interface BoundedFieldView { - - /** - * The field in which the option was discovered. - */ - public val subject: Field +public interface BoundedFieldView : FieldView { /** * The error message template. diff --git a/context/src/main/proto/spine/validation/views.proto b/context/src/main/proto/spine/validation/views.proto index f4bf2e7c4b..c3416d99cc 100644 --- a/context/src/main/proto/spine/validation/views.proto +++ b/context/src/main/proto/spine/validation/views.proto @@ -47,6 +47,7 @@ import "spine/validation/field_group.proto"; // message RequiredField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; @@ -60,6 +61,7 @@ message RequiredField { // A view on a field that is marked with `(validate) = true` option. message ValidateField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; @@ -70,6 +72,7 @@ message ValidateField { // A view of a field that is marked with `(set_once) = true` option. message SetOnceField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; @@ -83,6 +86,7 @@ message SetOnceField { // A view of a field that is marked with `(pattern)` option. message PatternField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; @@ -102,6 +106,7 @@ message PatternField { // A view of a field that is marked with `(goes)` option. message GoesField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; @@ -118,6 +123,7 @@ message GoesField { // A view of a field that is marked with `(distinct) = true` option. message DistinctField { option (entity).kind = PROJECTION; + option (is).java_type = "FieldView"; compiler.FieldRef id = 1; diff --git a/docs/content/docs/validation/developer/adding-a-built-in-option.md b/docs/content/docs/validation/developer/adding-a-built-in-option.md index 836bf4cab0..4b3ff57672 100644 --- a/docs/content/docs/validation/developer/adding-a-built-in-option.md +++ b/docs/content/docs/validation/developer/adding-a-built-in-option.md @@ -332,7 +332,7 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { private val allRequiredFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = @@ -348,8 +348,8 @@ The pattern is uniform across the built-ins: - Query the projection lazily — `querying` is not available until `inject()` returns, see “[The render lifecycle](java-code-generation.md#the-render-lifecycle)”. - Filter views by the message type currently being processed. -- Sort the selected views by `orderOfDeclaration`, so that the generated code is the same - from build to build. The views come in no particular order. +- Sort the selected field views with `sortedByDeclaration()`, so that the generated code + is the same from build to build. The views come in no particular order. - Delegate per-application code construction to a small helper class (`GenerateRequired` here). The helper produces a `CodeBlock` that runs inside the validate scope described in “[The validate scope](java-code-generation.md#the-validate-scope)” — diff --git a/docs/content/docs/validation/developer/java-code-generation.md b/docs/content/docs/validation/developer/java-code-generation.md index c1c20d5c9d..99aacd641e 100644 --- a/docs/content/docs/validation/developer/java-code-generation.md +++ b/docs/content/docs/validation/developer/java-code-generation.md @@ -137,7 +137,7 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { private val allRequiredFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt index 9fd1e149ff..2ba55ba1ff 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt @@ -60,6 +60,7 @@ import io.spine.tools.validation.java.generate.ValidateScope.parentName import io.spine.tools.validation.java.generate.ValidateScope.parentPath import io.spine.tools.validation.java.generate.ValidateScope.violations import io.spine.tools.validation.option.PATTERN +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.ConstraintViolation import io.spine.string.Placeholder import io.spine.validation.StandardPlaceholder.FIELD_DUPLICATES @@ -81,7 +82,7 @@ internal class DistinctGenerator : OptionGenerator() { private val allDistinctFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt index 226d573fa3..26ce74c91d 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt @@ -52,6 +52,7 @@ import io.spine.tools.validation.java.generate.ValidateScope.parentName import io.spine.tools.validation.java.generate.ValidateScope.parentPath import io.spine.tools.validation.java.generate.ValidateScope.violations import io.spine.tools.validation.option.GOES +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.ConstraintViolation import io.spine.string.Placeholder import io.spine.validation.StandardPlaceholder.FIELD_PATH @@ -73,7 +74,7 @@ internal class GoesGenerator : OptionGeneratorWithConverter() { private val allGoesFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt index 836d2481de..a2ab0f9a4b 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt @@ -66,6 +66,7 @@ import io.spine.tools.validation.java.generate.mangled import io.spine.tools.validation.option.PATTERN import io.spine.tools.validation.option.isRepeatedString import io.spine.tools.validation.option.isSingularString +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.ConstraintViolation import io.spine.string.Placeholder import io.spine.validation.StandardPlaceholder.FIELD_PATH @@ -90,7 +91,7 @@ internal class PatternGenerator : OptionGenerator() { private val allPatternFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt index 5930a4aa2d..d1f8040406 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt @@ -57,6 +57,7 @@ import io.spine.tools.validation.java.generate.ValidateScope.parentPath import io.spine.tools.validation.java.generate.ValidateScope.violations import io.spine.tools.validation.option.required.RequiredFieldSupport.isSupported import io.spine.tools.validation.option.IF_MISSING +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.ConstraintViolation import io.spine.string.Placeholder import io.spine.validation.StandardPlaceholder.FIELD_PATH @@ -76,7 +77,7 @@ internal class RequiredGenerator : OptionGeneratorWithConverter() { private val allRequiredFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt index 700df96627..505be8e277 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt @@ -56,6 +56,7 @@ import io.spine.tools.validation.java.generate.SingleOptionCode import io.spine.tools.validation.java.generate.ValidateScope.parentName import io.spine.tools.validation.java.generate.ValidateScope.parentPath import io.spine.tools.validation.java.generate.ValidateScope.violations +import io.spine.tools.validation.sortedByDeclaration import org.intellij.lang.annotations.Language /** @@ -71,7 +72,7 @@ internal class ValidateGenerator : OptionGeneratorWithConverter() { private val allValidateFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt index 8d47d53464..e0ab0525f4 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt @@ -45,6 +45,7 @@ import io.spine.tools.validation.java.generate.OptionGenerator import io.spine.tools.validation.java.generate.SingleOptionCode import io.spine.tools.validation.option.MAX import io.spine.string.Placeholder +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.StandardPlaceholder.FIELD_PATH import io.spine.validation.StandardPlaceholder.FIELD_TYPE import io.spine.validation.StandardPlaceholder.FIELD_VALUE @@ -65,7 +66,7 @@ internal class MaxGenerator : OptionGenerator() { private val allMaxFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt index b478b73687..32c92ef855 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt @@ -45,6 +45,7 @@ import io.spine.tools.validation.java.generate.OptionGenerator import io.spine.tools.validation.java.generate.SingleOptionCode import io.spine.tools.validation.option.MIN import io.spine.string.Placeholder +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.StandardPlaceholder.FIELD_PATH import io.spine.validation.StandardPlaceholder.FIELD_TYPE import io.spine.validation.StandardPlaceholder.FIELD_VALUE @@ -65,7 +66,7 @@ internal class MinGenerator : OptionGenerator() { private val allMinFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt index bb48cc2f6b..bbc39f9046 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt @@ -46,6 +46,7 @@ import io.spine.tools.validation.java.generate.OptionGenerator import io.spine.tools.validation.java.generate.SingleOptionCode import io.spine.tools.validation.option.RANGE import io.spine.string.Placeholder +import io.spine.tools.validation.sortedByDeclaration import io.spine.validation.StandardPlaceholder.FIELD_PATH import io.spine.validation.StandardPlaceholder.FIELD_TYPE import io.spine.validation.StandardPlaceholder.FIELD_VALUE @@ -65,7 +66,7 @@ internal class RangeGenerator : OptionGenerator() { private val allRangeFields by lazy { querying.select() .all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() } override fun codeFor(type: TypeName): List = diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt index eeccf7eb1a..b249bc651e 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt @@ -42,6 +42,7 @@ import io.spine.tools.compiler.render.SourceFileSet import io.spine.tools.validation.java.setonce.SetOnceNumberField.Companion.SupportedNumbers import io.spine.tools.validation.option.SET_ONCE import io.spine.tools.validation.SetOnceField +import io.spine.tools.validation.sortedByDeclaration /** * Takes the discovered [SetOnceField]s and modifies their Java builders to make sure @@ -65,7 +66,7 @@ internal class SetOnceRenderer : JavaRenderer() { // the code added to the shared builder methods, such as `mergeFrom()`, // goes in the same order from build to build. (this as Querying).select().all() - .sortedBy { it.subject.orderOfDeclaration } + .sortedByDeclaration() .associateWith { allCompilationMessages[it.id.type]!! } .forEach { (protoField, declaredIn) -> val javaConstraints = javaConstraints(protoField.subject, protoField.errorMessage) diff --git a/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt index 3826e22df5..1a8964114c 100644 --- a/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt +++ b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt @@ -32,61 +32,66 @@ import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldStartWith import javax.lang.model.SourceVersion import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -@DisplayName("`mangled()` should") +@DisplayName("Top-level functions in `JavaIdentifiers.kt` should") internal class JavaIdentifiersSpec { - @Test - fun `append the hash of the identifier`() { - // The expected hash is computed independently of the implementation. - // The URL-safe Base64 encoding of the first 10 bytes of the SHA-256 digest - // of `fooPattern` is `JfU6l-QD8acWPQ`, and `-` is not a Java identifier char. - mangled("fooPattern") shouldBe "fooPattern_JfU6lQD8acWPQ" - } - - @Test - fun `yield the same name for the same identifier`() { - mangled("validateNames") shouldBe mangled("validateNames") - } + @Nested inner class + `provide 'mangled()' that` { - @Test - fun `yield different hashes for different identifiers`() { - val fooHash = mangled("foo").removePrefix("foo_") - val barHash = mangled("bar").removePrefix("bar_") + @Test + fun `appends the hash of the identifier`() { + // The expected hash is computed independently of the implementation. + // The URL-safe Base64 encoding of the first 10 bytes of the SHA-256 digest + // of `fooPattern` is `JfU6l-QD8acWPQ`, and `-` is not a Java identifier char. + mangled("fooPattern") shouldBe "fooPattern_JfU6lQD8acWPQ" + } - fooHash shouldNotBe barHash - } + @Test + fun `yields the same name for the same identifier`() { + mangled("validateNames") shouldBe mangled("validateNames") + } - @Test - fun `yield valid Java identifiers`() { - // Some of these identifiers, such as `field2`, have `-` in the encoded digest. - // That char must be filtered out. - val identifiers = (1..100).map { "field$it" } + @Test + fun `yields different hashes for different identifiers`() { + val fooHash = mangled("foo").removePrefix("foo_") + val barHash = mangled("bar").removePrefix("bar_") - identifiers.forEach { identifier -> - val name = mangled(identifier) - name shouldStartWith "${identifier}_" - SourceVersion.isIdentifier(name) shouldBe true + fooHash shouldNotBe barHash } - } - @Test - fun `encode the given number of digest bytes`() { - // Base64 encodes every 3 bytes with 4 chars. So, the hash of the first - // 3 bytes is the beginning of the hash of the first 10 bytes. - val hash = mangled("validateNames", length = 3).removePrefix("validateNames_") + @Test + fun `yields valid Java identifiers`() { + // Some of these identifiers, such as `field2`, have `-` in the encoded digest. + // That char must be filtered out. + val identifiers = (1..100).map { "field$it" } - hash shouldBe mangled("validateNames").removePrefix("validateNames_").take(4) - } + identifiers.forEach { identifier -> + val name = mangled(identifier) + name shouldStartWith "${identifier}_" + SourceVersion.isIdentifier(name) shouldBe true + } + } - @Test - fun `reject a length outside the range from 1 to the digest size`() { - shouldThrow { - mangled("foo", length = 0) + @Test + fun `encodes the given number of digest bytes`() { + // Base64 encodes every 3 bytes with 4 chars. So, the hash of the first + // 3 bytes is the beginning of the hash of the first 10 bytes. + val hash = mangled("validateNames", length = 3).removePrefix("validateNames_") + + hash shouldBe mangled("validateNames").removePrefix("validateNames_").take(4) } - shouldThrow { - mangled("foo", length = 33) + + @Test + fun `rejects a length outside the range from 1 to the digest size`() { + shouldThrow { + mangled("foo", length = 0) + } + shouldThrow { + mangled("foo", length = 33) + } } } } From 2f47f6836eb5df4f80f308640acb2c10c5cc2155 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 19 Oct 2026 21:28:49 +0100 Subject: [PATCH 10/10] Adopt the CodeMatters copyright profile Make "CodeMatters Open-Source" the default copyright profile of the project and apply its notice to the source files changed on this branch. Co-Authored-By: Claude Opus 5.5 --- .idea/copyright/profiles_settings.xml | 4 +-- .../io/spine/tools/validation/FieldView.kt | 26 +++++------------ .../validation/bound/BoundedFieldView.kt | 26 +++++------------ .../main/proto/spine/validation/views.proto | 28 ++++++------------- .../gradle/ValidationGradlePlugin.kt | 26 +++++------------ .../gradle/WriteValidationSettings.kt | 26 +++++------------ .../gradle/ValidationGradlePluginSpec.kt | 26 +++++------------ .../gradle/WriteValidationSettingsSpec.kt | 26 +++++------------ .../java/generate/JavaIdentifiers.kt | 26 +++++------------ .../java/generate/OptionGenerator.kt | 26 +++++------------ .../java/generate/option/ChoiceGenerator.kt | 26 +++++------------ .../java/generate/option/DistinctGenerator.kt | 26 +++++------------ .../java/generate/option/GoesGenerator.kt | 26 +++++------------ .../java/generate/option/PatternGenerator.kt | 26 +++++------------ .../java/generate/option/RequiredGenerator.kt | 26 +++++------------ .../java/generate/option/ValidateGenerator.kt | 26 +++++------------ .../generate/option/bound/MaxGenerator.kt | 26 +++++------------ .../generate/option/bound/MinGenerator.kt | 26 +++++------------ .../generate/option/bound/RangeGenerator.kt | 26 +++++------------ .../java/setonce/SetOnceRenderer.kt | 26 +++++------------ .../java/generate/JavaIdentifiersSpec.kt | 26 +++++------------ .../test/options/DeclarationOrderIgTest.kt | 26 +++++------------ .../tools/validate/declaration_order.proto | 26 +++++------------ version.gradle.kts | 26 +++++------------ 24 files changed, 164 insertions(+), 440 deletions(-) diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index 0b8f9a1976..811272b735 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,7 +1,7 @@ - + - + diff --git a/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt b/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt index 3f50fc1749..09ca59c6b4 100644 --- a/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt +++ b/context/src/main/kotlin/io/spine/tools/validation/FieldView.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation diff --git a/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt b/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt index 1f8a276bdb..e475742074 100644 --- a/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt +++ b/context/src/main/kotlin/io/spine/tools/validation/bound/BoundedFieldView.kt @@ -1,27 +1,15 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.bound diff --git a/context/src/main/proto/spine/validation/views.proto b/context/src/main/proto/spine/validation/views.proto index c3416d99cc..15f6053209 100644 --- a/context/src/main/proto/spine/validation/views.proto +++ b/context/src/main/proto/spine/validation/views.proto @@ -1,27 +1,15 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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 * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ syntax = "proto3"; diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt index ed5c637743..1beb6fc46d 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.gradle diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt index d3d84c64ef..2135a46a97 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/WriteValidationSettings.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.gradle diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt index 5cb19e240a..69ce9e8e96 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/ValidationGradlePluginSpec.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.gradle diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt index 7840c8bb1f..d826d2f927 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/validation/gradle/WriteValidationSettingsSpec.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.gradle diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt index cdc99d201a..88fd55cd01 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiers.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt index bc19b95234..710004f0b1 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt index ef1729c882..edc55dc481 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ChoiceGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt index 2ba55ba1ff..3edd6b8d9c 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/DistinctGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt index 26ce74c91d..f321cee1b3 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/GoesGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt index a2ab0f9a4b..4d88dc92fc 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/PatternGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt index d1f8040406..310923bd70 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/RequiredGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt index 505be8e277..d7c10e68be 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/ValidateGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt index e0ab0525f4..b506da1acd 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MaxGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option.bound diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt index 32c92ef855..22155bac5c 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/MinGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option.bound diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt index bbc39f9046..c42af2fecc 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/generate/option/bound/RangeGenerator.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate.option.bound diff --git a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt index b249bc651e..2864d31954 100644 --- a/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt +++ b/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.setonce diff --git a/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt index 1a8964114c..18a669b9d2 100644 --- a/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt +++ b/java/src/test/kotlin/io/spine/tools/validation/java/generate/JavaIdentifiersSpec.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.validation.java.generate diff --git a/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt index 96cd1f69b6..339c136914 100644 --- a/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt +++ b/tests/validating/src/test/kotlin/io/spine/test/options/DeclarationOrderIgTest.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.test.options diff --git a/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto index 5420df4338..bec7f5beb4 100644 --- a/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto +++ b/tests/validating/src/testFixtures/proto/spine/test/tools/validate/declaration_order.proto @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ syntax = "proto3"; diff --git a/version.gradle.kts b/version.gradle.kts index 25631b4e07..66b0f862f6 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ /**