From 03b8c528ac8e01dd6189e94c964c875c997e2b83 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 12:33:13 +0200 Subject: [PATCH 1/6] Provide code coverage verification. --- .github/workflows/coverage.yml | 53 +++++ PROJECT.md | 5 + codecov.yml | 8 + dependencies.md | 208 ++++++++++++------ gradle-plugin/build.gradle.kts | 57 +++++ .../embedcode/gradle/EmbedCodePluginSpec.kt | 20 +- pom.xml | 12 + 7 files changed, 286 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/coverage.yml create mode 100644 codecov.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..1a36cdf --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,53 @@ +name: Coverage + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +jobs: + coverage: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set Up Java 17 for Compatibility Tests + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Save Java 17 Toolchain + run: echo "EMBED_CODE_JAVA_17_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + + - name: Set Up Java 25 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Generate Test Coverage + run: >- + ./gradlew + -Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME" + --no-configuration-cache + :gradle-plugin:jacocoTestReport + + - name: Upload Coverage + uses: codecov/codecov-action@v7 + with: + files: ./gradle-plugin/build/reports/jacoco/test/jacocoTestReport.xml + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/PROJECT.md b/PROJECT.md index 46592ac..4e53f55 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -38,6 +38,8 @@ Embed Code configuration file and do not need to install the executable or Kotli - `config/detekt/`: project-specific Detekt rules and the baseline for existing findings. - `scripts/check_agent_config.py` and `scripts/tests/`: deterministic validation and tests. - `.github/workflows/check.yml`: agent configuration and Ubuntu and Windows build verification. +- `.github/workflows/coverage.yml`: JaCoCo report generation and Codecov upload, using the patch + target defined in `codecov.yml`. - `.claude/commands/`: thin Claude slash-command entry points for deliberate workflows. - `.agents/guidelines/`: shared writing, English-language, and project-ownership rules. - `.agents/skills/`: repository engineering, test, writing, review, and security workflows. @@ -87,6 +89,9 @@ version numbers into agent guidance where a durable source path is sufficient. - Keep tests offline with deterministic fixtures, temporary directories, and loopback HTTP servers. - Use `./gradlew test` for unit tests, `./gradlew functionalTest` for TestKit tests, and `./gradlew check` for both plus plugin validation. +- Use `./gradlew :gradle-plugin:jacocoTestReport` to combine unit and TestKit coverage into + Codecov XML. The coverage-only TestKit task instruments forked builds and does not support the + configuration cache; the ordinary functional suite still verifies configuration-cache reuse. - Use `./gradlew :buildSrc:check` for the build-logic tests and Detekt analysis. `buildSrc` is a separate build, so the root `check` task does not reach them; CI runs the task explicitly. - Use `./gradlew generateDependencyReports` after changing dependencies or the plugin version. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..886a1c5 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: off + patch: + default: + target: 90% + threshold: 0% + only_pulls: true diff --git a/dependencies.md b/dependencies.md index 5997961..55d4a2f 100644 --- a/dependencies.md +++ b/dependencies.md @@ -209,98 +209,139 @@ > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) > - **POM License**: Apache-2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) +## BSD-3-Clause + +**48** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**49** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**50** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +## EPL-2.0 + +**51** **Group:** `org.jacoco` **Name:** `org.jacoco.agent` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.agent-0.8.14.jar/about.html` + +**52** **Group:** `org.jacoco` **Name:** `org.jacoco.ant` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.ant-0.8.14.jar/about.html` + +**53** **Group:** `org.jacoco` **Name:** `org.jacoco.core` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.core-0.8.14.jar/about.html` + +**54** **Group:** `org.jacoco` **Name:** `org.jacoco.report` **Version:** `0.8.14` +> - **POM License**: EPL-2.0 - [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/) +> - **Embedded license files**: `org.jacoco.report-0.8.14.jar/about.html` + ## Eclipse Public License v2.0 -**48** **Group:** `org.junit` **Name:** `junit-bom` **Version:** `6.1.2` +**55** **Group:** `org.junit` **Name:** `junit-bom` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) -**49** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter` **Version:** `6.1.2` +**56** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-6.1.2.jar/META-INF/LICENSE.md` -**50** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-api` **Version:** `6.1.2` +**57** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-api` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-api-6.1.2.jar/META-INF/LICENSE.md` -**51** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-engine` **Version:** `6.1.2` +**58** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-engine` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-engine-6.1.2.jar/META-INF/LICENSE.md` -**52** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-params` **Version:** `6.1.2` +**59** **Group:** `org.junit.jupiter` **Name:** `junit-jupiter-params` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-jupiter-params-6.1.2.jar/META-INF/LICENSE.md` -**53** **Group:** `org.junit.platform` **Name:** `junit-platform-commons` **Version:** `6.1.2` +**60** **Group:** `org.junit.platform` **Name:** `junit-platform-commons` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-commons-6.1.2.jar/META-INF/LICENSE.md` -**54** **Group:** `org.junit.platform` **Name:** `junit-platform-engine` **Version:** `6.1.2` +**61** **Group:** `org.junit.platform` **Name:** `junit-platform-engine` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-engine-6.1.2.jar/META-INF/LICENSE.md` -**55** **Group:** `org.junit.platform` **Name:** `junit-platform-launcher` **Version:** `6.1.2` +**62** **Group:** `org.junit.platform` **Name:** `junit-platform-launcher` **Version:** `6.1.2` > - **POM Project URL**: [https://junit.org/](https://junit.org/) > - **POM License**: Eclipse Public License v2.0 - [https://www.eclipse.org/legal/epl-v20.html](https://www.eclipse.org/legal/epl-v20.html) > - **Embedded license files**: `junit-platform-launcher-6.1.2.jar/META-INF/LICENSE.md` ## The Apache License, Version 2.0 -**56** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` +**63** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` > - **Project URL**: [https://github.com/FasterXML/woodstox](https://github.com/FasterXML/woodstox) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `woodstox-core-6.5.1.jar/META-INF/LICENSE` -**57** **Group:** `org.apiguardian` **Name:** `apiguardian-api` **Version:** `1.1.2` +**64** **Group:** `org.apiguardian` **Name:** `apiguardian-api` **Version:** `1.1.2` > - **Project URL**: [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `apiguardian-api-1.1.2.jar/META-INF/LICENSE` -**58** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `1.6.10` +**65** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `1.6.10` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**59** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `2.0.21` +**66** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-reflect` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**60** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `2.0.21` +**67** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**61** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-common` **Version:** `2.0.21` +**68** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-common` **Version:** `2.0.21` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**62** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `1.8.20` +**69** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `1.8.20` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**63** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `1.8.20` +**70** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `1.8.20` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**64** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.12.0` +**71** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.12.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**65** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.9.1` +**72** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-html-jvm` **Version:** `0.9.1` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**66** **Group:** `org.jspecify` **Name:** `jspecify` **Version:** `1.0.0` +**73** **Group:** `org.jspecify` **Name:** `jspecify` **Version:** `1.0.0` > - **Manifest Project URL**: [https://jspecify.dev/docs/start-here](https://jspecify.dev/docs/start-here) > - **POM Project URL**: [http://jspecify.org/](http://jspecify.org/) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**67** **Group:** `org.opentest4j` **Name:** `opentest4j` **Version:** `1.3.0` +**74** **Group:** `org.opentest4j` **Name:** `opentest4j` **Version:** `1.3.0` > - **Manifest License**: The Apache License, Version 2.0 > - **POM Project URL**: [https://github.com/ota4j-team/opentest4j](https://github.com/ota4j-team/opentest4j) > - **POM License**: The Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -308,186 +349,186 @@ ## The Apache Software License, Version 2.0 -**68** **Group:** `com.fasterxml.jackson` **Name:** `jackson-bom` **Version:** `2.15.3` +**75** **Group:** `com.fasterxml.jackson` **Name:** `jackson-bom` **Version:** `2.15.3` > - **POM Project URL**: [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**69** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-annotations` **Version:** `2.15.3` +**76** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-annotations` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-annotations-2.15.3.jar/META-INF/LICENSE` - `jackson-annotations-2.15.3.jar/META-INF/NOTICE` -**70** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.15.3` +**77** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-core-2.15.3.jar/META-INF/LICENSE` - `jackson-core-2.15.3.jar/META-INF/NOTICE` -**71** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-databind` **Version:** `2.15.3` +**78** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-databind` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-databind-2.15.3.jar/META-INF/LICENSE` - `jackson-databind-2.15.3.jar/META-INF/NOTICE` -**72** **Group:** `com.fasterxml.jackson.dataformat` **Name:** `jackson-dataformat-xml` **Version:** `2.15.3` +**79** **Group:** `com.fasterxml.jackson.dataformat` **Name:** `jackson-dataformat-xml` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-dataformat-xml-2.15.3.jar/META-INF/LICENSE` - `jackson-dataformat-xml-2.15.3.jar/META-INF/NOTICE` -**73** **Group:** `com.fasterxml.jackson.module` **Name:** `jackson-module-kotlin` **Version:** `2.15.3` +**80** **Group:** `com.fasterxml.jackson.module` **Name:** `jackson-module-kotlin` **Version:** `2.15.3` > - **Project URL**: [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `jackson-module-kotlin-2.15.3.jar/META-INF/LICENSE` - `jackson-module-kotlin-2.15.3.jar/META-INF/NOTICE` -**74** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` +**81** **Group:** `com.fasterxml.woodstox` **Name:** `woodstox-core` **Version:** `6.5.1` > - **Project URL**: [https://github.com/FasterXML/woodstox](https://github.com/FasterXML/woodstox) > - **POM License**: The Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `woodstox-core-6.5.1.jar/META-INF/LICENSE` -**75** **Group:** `dev.detekt` **Name:** `detekt-api` **Version:** `2.0.0-alpha.5` +**82** **Group:** `dev.detekt` **Name:** `detekt-api` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**76** **Group:** `dev.detekt` **Name:** `detekt-cli` **Version:** `2.0.0-alpha.5` +**83** **Group:** `dev.detekt` **Name:** `detekt-cli` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**77** **Group:** `dev.detekt` **Name:** `detekt-core` **Version:** `2.0.0-alpha.5` +**84** **Group:** `dev.detekt` **Name:** `detekt-core` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**78** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api` **Version:** `2.0.0-alpha.5` +**85** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**79** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api-standalone` **Version:** `2.0.0-alpha.5` +**86** **Group:** `dev.detekt` **Name:** `detekt-kotlin-analysis-api-standalone` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**80** **Group:** `dev.detekt` **Name:** `detekt-metrics` **Version:** `2.0.0-alpha.5` +**87** **Group:** `dev.detekt` **Name:** `detekt-metrics` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**81** **Group:** `dev.detekt` **Name:** `detekt-parser` **Version:** `2.0.0-alpha.5` +**88** **Group:** `dev.detekt` **Name:** `detekt-parser` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**82** **Group:** `dev.detekt` **Name:** `detekt-psi-utils` **Version:** `2.0.0-alpha.5` +**89** **Group:** `dev.detekt` **Name:** `detekt-psi-utils` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**83** **Group:** `dev.detekt` **Name:** `detekt-report-checkstyle` **Version:** `2.0.0-alpha.5` +**90** **Group:** `dev.detekt` **Name:** `detekt-report-checkstyle` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**84** **Group:** `dev.detekt` **Name:** `detekt-report-html` **Version:** `2.0.0-alpha.5` +**91** **Group:** `dev.detekt` **Name:** `detekt-report-html` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**85** **Group:** `dev.detekt` **Name:** `detekt-report-markdown` **Version:** `2.0.0-alpha.5` +**92** **Group:** `dev.detekt` **Name:** `detekt-report-markdown` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**86** **Group:** `dev.detekt` **Name:** `detekt-report-sarif` **Version:** `2.0.0-alpha.5` +**93** **Group:** `dev.detekt` **Name:** `detekt-report-sarif` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**87** **Group:** `dev.detekt` **Name:** `detekt-rules` **Version:** `2.0.0-alpha.5` +**94** **Group:** `dev.detekt` **Name:** `detekt-rules` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**88** **Group:** `dev.detekt` **Name:** `detekt-rules-comments` **Version:** `2.0.0-alpha.5` +**95** **Group:** `dev.detekt` **Name:** `detekt-rules-comments` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**89** **Group:** `dev.detekt` **Name:** `detekt-rules-complexity` **Version:** `2.0.0-alpha.5` +**96** **Group:** `dev.detekt` **Name:** `detekt-rules-complexity` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**90** **Group:** `dev.detekt` **Name:** `detekt-rules-coroutines` **Version:** `2.0.0-alpha.5` +**97** **Group:** `dev.detekt` **Name:** `detekt-rules-coroutines` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**91** **Group:** `dev.detekt` **Name:** `detekt-rules-empty-blocks` **Version:** `2.0.0-alpha.5` +**98** **Group:** `dev.detekt` **Name:** `detekt-rules-empty-blocks` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**92** **Group:** `dev.detekt` **Name:** `detekt-rules-exceptions` **Version:** `2.0.0-alpha.5` +**99** **Group:** `dev.detekt` **Name:** `detekt-rules-exceptions` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**93** **Group:** `dev.detekt` **Name:** `detekt-rules-naming` **Version:** `2.0.0-alpha.5` +**100** **Group:** `dev.detekt` **Name:** `detekt-rules-naming` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**94** **Group:** `dev.detekt` **Name:** `detekt-rules-performance` **Version:** `2.0.0-alpha.5` +**101** **Group:** `dev.detekt` **Name:** `detekt-rules-performance` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**95** **Group:** `dev.detekt` **Name:** `detekt-rules-potential-bugs` **Version:** `2.0.0-alpha.5` +**102** **Group:** `dev.detekt` **Name:** `detekt-rules-potential-bugs` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**96** **Group:** `dev.detekt` **Name:** `detekt-rules-style` **Version:** `2.0.0-alpha.5` +**103** **Group:** `dev.detekt` **Name:** `detekt-rules-style` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**97** **Group:** `dev.detekt` **Name:** `detekt-tooling` **Version:** `2.0.0-alpha.5` +**104** **Group:** `dev.detekt` **Name:** `detekt-tooling` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**98** **Group:** `dev.detekt` **Name:** `detekt-utils` **Version:** `2.0.0-alpha.5` +**105** **Group:** `dev.detekt` **Name:** `detekt-utils` **Version:** `2.0.0-alpha.5` > - **POM Project URL**: [https://detekt.dev](https://detekt.dev) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**99** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations` **Version:** `0.23.1` +**106** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations` **Version:** `0.23.1` > - **POM Project URL**: [https://github.com/drewhamilton/Poko](https://github.com/drewhamilton/Poko) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**100** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations-jvm` **Version:** `0.23.1` +**107** **Group:** `dev.drewhamilton.poko` **Name:** `poko-annotations-jvm` **Version:** `0.23.1` > - **POM Project URL**: [https://github.com/drewhamilton/Poko](https://github.com/drewhamilton/Poko) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**101** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k` **Version:** `0.6.0` +**108** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k` **Version:** `0.6.0` > - **POM Project URL**: [https://detekt.github.io/detekt](https://detekt.github.io/detekt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**102** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k-jvm` **Version:** `0.6.0` +**109** **Group:** `io.github.detekt.sarif4k` **Name:** `sarif4k-jvm` **Version:** `0.6.0` > - **POM Project URL**: [https://detekt.github.io/detekt](https://detekt.github.io/detekt) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**103** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` +**110** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` > - **Project URL**: [http://github.com/FasterXML/stax2-api](http://github.com/FasterXML/stax2-api) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The BSD License - [http://www.opensource.org/licenses/bsd-license.php](http://www.opensource.org/licenses/bsd-license.php) > - **Embedded license files**: `stax2-api-4.2.1.jar/META-INF/LICENSE` -**104** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` +**111** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` > - **POM Project URL**: [http://www.jetbrains.org](http://www.jetbrains.org) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**105** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `23.0.0` +**112** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `23.0.0` > - **POM Project URL**: [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**106** **Group:** `org.jetbrains` **Name:** `markdown` **Version:** `0.7.3` +**113** **Group:** `org.jetbrains` **Name:** `markdown` **Version:** `0.7.3` > - **POM Project URL**: [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**107** **Group:** `org.jetbrains` **Name:** `markdown-jvm` **Version:** `0.7.3` +**114** **Group:** `org.jetbrains` **Name:** `markdown-jvm` **Version:** `0.7.3` > - **POM Project URL**: [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) -**108** **Group:** `org.jetbrains.dokka` **Name:** `analysis-kotlin-symbols` **Version:** `2.2.0` +**115** **Group:** `org.jetbrains.dokka` **Name:** `analysis-kotlin-symbols` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) > - **Embedded license files**: `analysis-kotlin-symbols-2.2.0.jar/META-INF/LICENSE` @@ -497,45 +538,66 @@ - `analysis-kotlin-symbols-2.2.0.jar/META-INF/README.md` - `analysis-kotlin-symbols-2.2.0.jar/org/apache/log4j/NOTICE` -**109** **Group:** `org.jetbrains.dokka` **Name:** `analysis-markdown` **Version:** `2.2.0` +**116** **Group:** `org.jetbrains.dokka` **Name:** `analysis-markdown` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**110** **Group:** `org.jetbrains.dokka` **Name:** `dokka-base` **Version:** `2.2.0` +**117** **Group:** `org.jetbrains.dokka` **Name:** `dokka-base` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**111** **Group:** `org.jetbrains.dokka` **Name:** `dokka-core` **Version:** `2.2.0` +**118** **Group:** `org.jetbrains.dokka` **Name:** `dokka-core` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**112** **Group:** `org.jetbrains.dokka` **Name:** `templating-plugin` **Version:** `2.2.0` +**119** **Group:** `org.jetbrains.dokka` **Name:** `templating-plugin` **Version:** `2.2.0` > - **POM Project URL**: [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**113** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.7.3` +**120** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**114** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.8.0` +**121** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-bom` **Version:** `1.8.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**115** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core` **Version:** `1.7.3` +**122** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**116** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.7.3` +**123** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.7.3` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) -**117** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.8.0` +**124** **Group:** `org.jetbrains.kotlinx` **Name:** `kotlinx-coroutines-core-jvm` **Version:** `1.8.0` > - **POM Project URL**: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) > - **POM License**: The Apache Software License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) +**125** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**126** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + +**127** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.9` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: `https://asm.ow2.io/LICENSE.txt` +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: BSD-3-Clause - [https://asm.ow2.io/license.html](https://asm.ow2.io/license.html) +> - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) + ## The BSD License -**118** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` +**128** **Group:** `org.codehaus.woodstox` **Name:** `stax2-api` **Version:** `4.2.1` > - **Project URL**: [http://github.com/FasterXML/stax2-api](http://github.com/FasterXML/stax2-api) > - **POM License**: The Apache Software License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) > - **POM License**: The BSD License - [http://www.opensource.org/licenses/bsd-license.php](http://www.opensource.org/licenses/bsd-license.php) @@ -543,13 +605,13 @@ ## The MIT License -**119** **Group:** `org.checkerframework` **Name:** `checker-qual` **Version:** `3.19.0` +**129** **Group:** `org.checkerframework` **Name:** `checker-qual` **Version:** `3.19.0` > - **Manifest License**: MIT > - **POM Project URL**: [https://checkerframework.org](https://checkerframework.org) > - **POM License**: The MIT License - [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT) > - **Embedded license files**: `checker-qual-3.19.0.jar/META-INF/LICENSE.txt` -**120** **Group:** `org.jsoup` **Name:** `jsoup` **Version:** `1.16.1` +**130** **Group:** `org.jsoup` **Name:** `jsoup` **Version:** `1.16.1` > - **Project URL**: [https://jsoup.org/](https://jsoup.org/) > - **POM License**: The MIT License - [https://jsoup.org/license](https://jsoup.org/license) > - **Embedded license files**: `jsoup-1.16.1.jar/META-INF/LICENSE` diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 7867481..854e558 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -37,11 +37,13 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.Sync import org.gradle.plugin.compatibility.compatibility +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension plugins { id("jvm-module") id("dokka-configuration") `java-gradle-plugin` + jacoco `maven-publish` } @@ -113,10 +115,65 @@ val functionalTest = tasks.register("functionalTest") { shouldRunAfter(tasks.test) } +val testKitCoverageData = layout.buildDirectory.file("jacoco/testKit.exec") +val coverageFunctionalTest = tasks.register("coverageFunctionalTest") { + description = "Runs TestKit functional tests and collects plugin coverage." + group = LifecycleBasePlugin.VERIFICATION_GROUP + testClassesDirs = functionalTestSourceSet.output.classesDirs + classpath = functionalTestSourceSet.runtimeClasspath + useJUnitPlatform() + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(BuildSettings.bytecodeVersion)) + }, + ) + extensions.configure { + isEnabled = false + setDestinationFile(testKitCoverageData.map { file -> file.asFile }) + } + outputs.file(testKitCoverageData) + outputs.doNotCacheIf("TestKit builds depend on the host environment.") { true } + notCompatibleWithConfigurationCache( + "JaCoCo coverage of forked TestKit builds requires execution-time agent paths.", + ) + doFirst { + val coverageDataFile = testKitCoverageData.get().asFile + coverageDataFile.delete() + val jacocoExtension = extensions.getByType() + val childJvmArgument = + Regex("""destfile=[^,]+""").replaceFirst( + jacocoExtension.asJvmArg, + Regex.escapeReplacement("destfile=${coverageDataFile.absolutePath}"), + ) + systemProperty( + "io.spine.embedcode.gradle.testkit.coverage.jvm-argument", + childJvmArgument, + ) + } + shouldRunAfter(tasks.test) +} + tasks.check { dependsOn(functionalTest) } +tasks.jacocoTestReport { + dependsOn(tasks.test, coverageFunctionalTest) + executionData( + tasks.test.map { task -> + task.extensions.getByType().destinationFile + }, + coverageFunctionalTest.map { task -> + task.extensions.getByType().destinationFile + }, + ) + reports { + xml.required.set(true) + html.required.set(false) + csv.required.set(false) + } +} + base { archivesName.set("embed-code-gradle-plugin") } diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index ebb5f3b..313c89c 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -111,9 +111,9 @@ internal class EmbedCodePluginSpec { @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `reuse the configuration cache`() { - runner(":checkEmbedding").build() + runner(":checkEmbedding", collectCoverage = false).build() - val result = runner(":checkEmbedding").build() + val result = runner(":checkEmbedding", collectCoverage = false).build() result.output shouldContain "Reusing configuration cache." result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS @@ -1051,9 +1051,16 @@ internal class EmbedCodePluginSpec { */ private fun runner( vararg arguments: String, + collectCoverage: Boolean = testKitCoverageJvmArgument != null, ): GradleRunner { val gradleArguments = arguments.toMutableList() - gradleArguments.add("--configuration-cache") + if (collectCoverage) { + gradleArguments.add( + "-Dorg.gradle.jvmargs=${requireNotNull(testKitCoverageJvmArgument)}", + ) + } else { + gradleArguments.add("--configuration-cache") + } gradleArguments.add("--stacktrace") return GradleRunner.create() .withProjectDir(projectDirectory.toFile()) @@ -1065,7 +1072,7 @@ internal class EmbedCodePluginSpec { * Runs check mode with [gradleVersion]. */ private fun runCheckModeWithGradle(gradleVersion: String) { - val result = runner(":checkEmbedding") + val result = runner(":checkEmbedding", collectCoverage = false) .withGradleVersion(gradleVersion) .build() @@ -1283,7 +1290,12 @@ internal class EmbedCodePluginSpec { private val HttpServer.releaseBaseUrl: String get() = "http://127.0.0.1:${address.port}/releases" + private val testKitCoverageJvmArgument: String? + get() = System.getProperty(TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY) + private companion object { + const val TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY = + "io.spine.embedcode.gradle.testkit.coverage.jvm-argument" val TEST_RELEASE_TAG = DEFAULT_EMBED_CODE_VERSION val TEST_RELEASE_VERSION = TEST_RELEASE_TAG.removePrefix("v") } diff --git a/pom.xml b/pom.xml index cfb82b2..ece629c 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,18 @@ 2.0.0-alpha.5 provided + + org.jacoco + org.jacoco.agent + 0.8.14 + provided + + + org.jacoco + org.jacoco.ant + 0.8.14 + provided + org.jetbrains.dokka all-modules-page-plugin From d43823855cc9824a62dfc0cc202178073e6c279b Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 12:52:19 +0200 Subject: [PATCH 2/6] Improve test coverage. --- .../embedcode/gradle/EmbedCodePluginSpec.kt | 5 +- .../io/spine/embedcode/gradle/ChecksumSpec.kt | 54 +++++++++++++++++++ .../embedcode/gradle/EmbedCodeJsonSpec.kt | 4 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index 313c89c..c415750 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -1172,7 +1172,10 @@ internal class EmbedCodePluginSpec { "" } val secondSource = if (includeSecondSource) { - "namedSource(\"$secondSourceName\", layout.projectDirectory.dir(\"browser\"))" + "namedSource(" + + "\"$secondSourceName\", " + + "providers.provider { layout.projectDirectory.dir(\"browser\") }" + + ")" } else { "" } diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt index 790eacf..fa86f45 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt @@ -56,6 +56,17 @@ internal class ChecksumSpec { assertEquals("Invalid SHA-256 digest `$digest`.", error.message) } + @Test + fun `reject a digest with an invalid length`() { + val digest = "0".repeat(63) + + val error = assertThrows(GradleException::class.java) { + normalizeSha256(digest) + } + + assertEquals("Invalid SHA-256 digest `$digest`.", error.message) + } + @Test fun `distinguish release assets in cache metadata`() { val baseUrl = "https://github.com/SpineEventEngine/embed-code-go/releases" @@ -96,6 +107,16 @@ internal class ChecksumSpec { assertNull(githubReleaseApi("https:///releases", "v1.2.4")) } + @Test + fun `ignore a GitHub URL outside the release path`() { + assertNull( + githubReleaseApi( + "https://github.com/SpineEventEngine/embed-code-go/downloads", + "v1.2.4", + ), + ) + } + @Test fun `read an asset digest from GitHub JSON`() { val digest = "2".repeat(64) @@ -105,6 +126,39 @@ internal class ChecksumSpec { assertEquals(digest, parseGitHubAssetSha256(json, "embed-code-linux")) } + @Test + fun `reject a non-object GitHub release response`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256("[]", "embed-code-linux") + } + + assertEquals("The GitHub release response is not a JSON object.", error.message) + } + + @Test + fun `reject a GitHub release response without assets`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256("{}", "embed-code-linux") + } + + assertEquals("The GitHub release response does not contain assets.", error.message) + } + + @Test + fun `reject a GitHub release response without the requested asset`() { + val error = assertThrows(GradleException::class.java) { + parseGitHubAssetSha256( + """{"assets":[{"name":"embed-code-macos"}]}""", + "embed-code-linux", + ) + } + + assertEquals( + "The GitHub release does not contain asset `embed-code-linux`.", + error.message, + ) + } + @Test fun `resolve an asset digest from GitHub release metadata`() { val digest = "3".repeat(64) diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt index 5ec1fe4..a8241ac 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt @@ -40,7 +40,7 @@ internal class EmbedCodeJsonSpec { "C:\\docs\nline", listOf("**/\"quoted\".md", "line\nbreak"), listOf("drafts\\**"), - "---\n---", + "---\b\u000C\r\t\u0001---", info = true, stacktrace = false, ) @@ -54,7 +54,7 @@ internal class EmbedCodeJsonSpec { "docs-path": "C:\\docs\nline", "doc-includes": ["**/\"quoted\".md", "line\nbreak"], "doc-excludes": ["drafts\\**"], - "separator": "---\n---", + "separator": "---\b\f\r\t\u0001---", "info": true, "stacktrace": false } From ebcd9da7eb2ff71dc5c04f7a093f0dc298e17e01 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 13:30:14 +0200 Subject: [PATCH 3/6] Improve code coverage percent. --- .../embedcode/gradle/EmbedCodePluginSpec.kt | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index c415750..01917c4 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -137,7 +137,7 @@ internal class EmbedCodePluginSpec { ) Files.createDirectories(asset.parent) ZipOutputStream(Files.newOutputStream(asset)).use { zip -> - zip.putNextEntry(ZipEntry("embed-code-linux")) + zip.putNextEntry(ZipEntry("bin/embed-code-linux")) zip.write("Linux executable".toByteArray()) zip.closeEntry() } @@ -150,6 +150,32 @@ internal class EmbedCodePluginSpec { Files.readString(installedExecutable(releaseTag)) shouldBe "Linux executable" } + @Test + fun `reject a Linux ZIP release without the expected executable`() { + val releaseTag = "v1.2.5-missing-executable" + val asset = releaseDirectory.resolve( + "download/$releaseTag/embed-code-linux.zip", + ) + Files.createDirectories(asset.parent) + ZipOutputStream(Files.newOutputStream(asset)).use { zip -> + zip.putNextEntry(ZipEntry("README.md")) + zip.write("No executable in this archive".toByteArray()) + zip.closeEntry() + } + writeBuildFile(version = releaseTag, sha256 = sha256(asset)) + selectLinuxReleaseAsset() + + val result = runner(":installEmbedCode").buildAndFail() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.FAILED + result.output shouldContain "does not contain `embed-code-linux`" + Files.exists(installedExecutable(releaseTag)) shouldBe false + val installation = installationDirectory(releaseTag) + listOf("asset.sha256", "executable.sha256", "source.sha256").forEach { marker -> + Files.exists(installation.resolve(marker)) shouldBe false + } + } + @Test fun `install a bare Linux asset from a release published before ZIP packaging`() { val asset = releaseDirectory.resolve( @@ -349,6 +375,48 @@ internal class EmbedCodePluginSpec { Files.isExecutable(executable) shouldBe true } + @Test + fun `restore missing asset checksum metadata from the verified cached asset`() { + val downloads = AtomicInteger() + val server = startReleaseServer(downloads) + try { + writeBuildFile(downloadBaseUrl = server.releaseBaseUrl) + runner(":installEmbedCode").build() + val assetChecksum = installationDirectory().resolve("asset.sha256") + Files.delete(assetChecksum) + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + downloads.get() shouldBe 1 + Files.readString(assetChecksum).trim() shouldBe releaseAssetSha256() + result.output shouldContain "Reusing verified Embed Code" + } finally { + server.stop(0) + } + } + + @Test + fun `replace malformed executable checksum metadata from the verified cached asset`() { + val downloads = AtomicInteger() + val server = startReleaseServer(downloads) + try { + writeBuildFile(downloadBaseUrl = server.releaseBaseUrl) + runner(":installEmbedCode").build() + val executableChecksum = installationDirectory().resolve("executable.sha256") + Files.writeString(executableChecksum, "not-a-sha256") + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + downloads.get() shouldBe 1 + Files.readString(executableChecksum).trim() shouldBe sha256(installedExecutable()) + result.output shouldContain "Reusing verified Embed Code" + } finally { + server.stop(0) + } + } + @Test fun `remove staged release assets after cache reuse`() { runner(":installEmbedCode").build() @@ -686,6 +754,39 @@ internal class EmbedCodePluginSpec { Files.exists(projectDirectory.resolve("escaped/release-asset")) shouldBe false } + @Test + fun `reject a regular file in the installation path`() { + val installationRoot = projectDirectory.resolve("build/embed-code") + val blockingFile = installationRoot.resolve("blocked") + Files.writeString( + projectDirectory.resolve("build.gradle.kts"), + """ + + tasks.named("installEmbedCode") { + cachedAssetFile.set( + layout.buildDirectory.file("embed-code/blocked/release-asset") + ) + doFirst { + val blockingFile = cachedAssetFile.get().asFile.parentFile + check(blockingFile.deleteRecursively()) + blockingFile.writeText("unchanged") + } + } + """.trimIndent(), + StandardOpenOption.APPEND, + ) + + val result = runner( + ":installEmbedCode", + useConfigurationCache = false, + ).buildAndFail() + + result.output shouldContain "Embed Code installation path component" + result.output shouldContain "must be a directory." + Files.readString(blockingFile) shouldBe "unchanged" + Files.exists(installedExecutable()) shouldBe false + } + @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `reject a symbolic-link installation root`() { @@ -1052,13 +1153,14 @@ internal class EmbedCodePluginSpec { private fun runner( vararg arguments: String, collectCoverage: Boolean = testKitCoverageJvmArgument != null, + useConfigurationCache: Boolean = !collectCoverage, ): GradleRunner { val gradleArguments = arguments.toMutableList() if (collectCoverage) { gradleArguments.add( "-Dorg.gradle.jvmargs=${requireNotNull(testKitCoverageJvmArgument)}", ) - } else { + } else if (useConfigurationCache) { gradleArguments.add("--configuration-cache") } gradleArguments.add("--stacktrace") From 8ec33a9645e0b0c284283dcdb34c3fe39704f8dc Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 14:39:31 +0200 Subject: [PATCH 4/6] Improve coverage. --- .github/workflows/coverage.yml | 4 + PROJECT.md | 7 +- README.md | 3 + codecov.yml | 8 +- gradle-plugin/build.gradle.kts | 26 +-- .../embedcode/gradle/EmbedCodePluginSpec.kt | 55 ++++- .../io/spine/embedcode/gradle/Checksum.kt | 58 +++++ .../embedcode/gradle/InstallEmbedCodeTask.kt | 199 +----------------- .../embedcode/gradle/InstallationDirectory.kt | 188 +++++++++++++++++ .../io/spine/embedcode/gradle/ChecksumSpec.kt | 127 +++++++++++ .../embedcode/gradle/EmbedCodeJsonSpec.kt | 1 + .../gradle/InstallationDirectorySpec.kt | 91 ++++++++ 12 files changed, 549 insertions(+), 218 deletions(-) create mode 100644 gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt create mode 100644 gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1a36cdf..b6e9cc2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: coverage: runs-on: ubuntu-latest diff --git a/PROJECT.md b/PROJECT.md index 4e53f55..1238bcc 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -89,9 +89,10 @@ version numbers into agent guidance where a durable source path is sufficient. - Keep tests offline with deterministic fixtures, temporary directories, and loopback HTTP servers. - Use `./gradlew test` for unit tests, `./gradlew functionalTest` for TestKit tests, and `./gradlew check` for both plus plugin validation. -- Use `./gradlew :gradle-plugin:jacocoTestReport` to combine unit and TestKit coverage into - Codecov XML. The coverage-only TestKit task instruments forked builds and does not support the - configuration cache; the ordinary functional suite still verifies configuration-cache reuse. +- Use `./gradlew --no-configuration-cache :gradle-plugin:jacocoTestReport` to combine unit and + TestKit coverage into Codecov XML. The coverage-only TestKit task instruments forked builds and + does not support the configuration cache; the ordinary functional suite still verifies + configuration-cache reuse. - Use `./gradlew :buildSrc:check` for the build-logic tests and Detekt analysis. `buildSrc` is a separate build, so the root `check` task does not reach them; CI runs the task explicitly. - Use `./gradlew generateDependencyReports` after changing dependencies or the plugin version. diff --git a/README.md b/README.md index ca008df..8f97fa7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build on Ubuntu and Windows][build-badge]][gh-actions] +[![Codecov][coverage-badge]][codecov] [![license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) # Embed Code Gradle plugin @@ -178,5 +179,7 @@ consuming project's `settings.gradle.kts`. Adding it only to the regular The plugin is available under the [Apache License 2.0](LICENSE). [build-badge]: https://github.com/SpineEventEngine/embed-code-gradle-plugin/actions/workflows/check.yml/badge.svg +[codecov]: https://codecov.io/gh/SpineEventEngine/embed-code-gradle-plugin +[coverage-badge]: https://codecov.io/gh/SpineEventEngine/embed-code-gradle-plugin/graph/badge.svg [embed-code]: https://github.com/SpineEventEngine/embed-code-go [gh-actions]: https://github.com/SpineEventEngine/embed-code-gradle-plugin/actions diff --git a/codecov.yml b/codecov.yml index 886a1c5..65d365e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,8 +1,14 @@ coverage: status: - project: off + project: + default: + informational: true patch: default: target: 90% threshold: 0% only_pulls: true + +ignore: + - "buildSrc/**" + - "scripts/**" diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 3b94287..5b1bf07 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -34,6 +34,7 @@ import io.spine.embedcode.gradle.report.DependencyMarkdownReportRenderer import io.spine.embedcode.gradle.report.GenerateDependencyPom import io.spine.embedcode.gradle.report.UpdateDependencyReports import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.Action import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.Sync import org.gradle.plugin.compatibility.compatibility @@ -100,9 +101,7 @@ configurations[functionalTestSourceSet.runtimeOnlyConfigurationName].extendsFrom configurations.testRuntimeOnly.get(), ) -val functionalTest = tasks.register("functionalTest") { - description = "Runs TestKit functional tests." - group = LifecycleBasePlugin.VERIFICATION_GROUP +val configureFunctionalTest = Action { testClassesDirs = functionalTestSourceSet.output.classesDirs classpath = functionalTestSourceSet.runtimeClasspath useJUnitPlatform() @@ -115,24 +114,25 @@ val functionalTest = tasks.register("functionalTest") { shouldRunAfter(tasks.test) } +val functionalTest = tasks.register("functionalTest") { + configureFunctionalTest.execute(this) + description = "Runs TestKit functional tests." + group = LifecycleBasePlugin.VERIFICATION_GROUP + extensions.configure { + isEnabled = false + } +} + val testKitCoverageData = layout.buildDirectory.file("jacoco/testKit.exec") val coverageFunctionalTest = tasks.register("coverageFunctionalTest") { + configureFunctionalTest.execute(this) description = "Runs TestKit functional tests and collects plugin coverage." group = LifecycleBasePlugin.VERIFICATION_GROUP - testClassesDirs = functionalTestSourceSet.output.classesDirs - classpath = functionalTestSourceSet.runtimeClasspath - useJUnitPlatform() - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(BuildSettings.bytecodeVersion)) - }, - ) extensions.configure { isEnabled = false setDestinationFile(testKitCoverageData.map { file -> file.asFile }) } outputs.file(testKitCoverageData) - outputs.doNotCacheIf("TestKit builds depend on the host environment.") { true } notCompatibleWithConfigurationCache( "JaCoCo coverage of forked TestKit builds requires execution-time agent paths.", ) @@ -140,6 +140,7 @@ val coverageFunctionalTest = tasks.register("coverageFunctionalTest") { val coverageDataFile = testKitCoverageData.get().asFile coverageDataFile.delete() val jacocoExtension = extensions.getByType() + // TestKit daemons use another working directory, so JaCoCo needs an absolute destination. val childJvmArgument = Regex("""destfile=[^,]+""").replaceFirst( jacocoExtension.asJvmArg, @@ -150,7 +151,6 @@ val coverageFunctionalTest = tasks.register("coverageFunctionalTest") { childJvmArgument, ) } - shouldRunAfter(tasks.test) } tasks.check { diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index 01917c4..3479ebc 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -32,6 +32,7 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -137,7 +138,7 @@ internal class EmbedCodePluginSpec { ) Files.createDirectories(asset.parent) ZipOutputStream(Files.newOutputStream(asset)).use { zip -> - zip.putNextEntry(ZipEntry("bin/embed-code-linux")) + zip.putNextEntry(ZipEntry("embed-code-linux")) zip.write("Linux executable".toByteArray()) zip.closeEntry() } @@ -150,6 +151,27 @@ internal class EmbedCodePluginSpec { Files.readString(installedExecutable(releaseTag)) shouldBe "Linux executable" } + @Test + fun `install a Linux ZIP release with a nested executable`() { + val releaseTag = "v1.2.5-nested-executable" + val asset = releaseDirectory.resolve( + "download/$releaseTag/embed-code-linux.zip", + ) + Files.createDirectories(asset.parent) + ZipOutputStream(Files.newOutputStream(asset)).use { zip -> + zip.putNextEntry(ZipEntry("bin/embed-code-linux")) + zip.write("Nested Linux executable".toByteArray()) + zip.closeEntry() + } + writeBuildFile(version = releaseTag, sha256 = sha256(asset)) + selectLinuxReleaseAsset() + + val result = runner(":installEmbedCode").build() + + result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS + Files.readString(installedExecutable(releaseTag)) shouldBe "Nested Linux executable" + } + @Test fun `reject a Linux ZIP release without the expected executable`() { val releaseTag = "v1.2.5-missing-executable" @@ -1147,20 +1169,45 @@ internal class EmbedCodePluginSpec { result.task(":checkEmbedding")?.outcome shouldBe TaskOutcome.SUCCESS } + @Test + fun `reject coverage collection with the configuration cache`() { + val error = assertThrows(IllegalArgumentException::class.java) { + runner( + "help", + collectCoverage = true, + useConfigurationCache = true, + ) + } + + assertEquals( + "TestKit coverage collection is incompatible with the configuration cache.", + error.message, + ) + } + /** * Creates a runner using the plugin-under-test classpath. + * + * @param arguments Gradle tasks and options passed to the consuming build. + * @param collectCoverage whether to attach the JaCoCo agent to the consuming build. + * @param useConfigurationCache whether to enable Gradle's configuration cache. */ private fun runner( vararg arguments: String, collectCoverage: Boolean = testKitCoverageJvmArgument != null, useConfigurationCache: Boolean = !collectCoverage, ): GradleRunner { + require(!collectCoverage || !useConfigurationCache) { + "TestKit coverage collection is incompatible with the configuration cache." + } val gradleArguments = arguments.toMutableList() if (collectCoverage) { gradleArguments.add( - "-Dorg.gradle.jvmargs=${requireNotNull(testKitCoverageJvmArgument)}", + "-Dorg.gradle.jvmargs=$DEFAULT_GRADLE_DAEMON_JVM_ARGUMENTS " + + requireNotNull(testKitCoverageJvmArgument), ) - } else if (useConfigurationCache) { + } + if (useConfigurationCache) { gradleArguments.add("--configuration-cache") } gradleArguments.add("--stacktrace") @@ -1401,6 +1448,8 @@ internal class EmbedCodePluginSpec { private companion object { const val TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY = "io.spine.embedcode.gradle.testkit.coverage.jvm-argument" + const val DEFAULT_GRADLE_DAEMON_JVM_ARGUMENTS = + "-Xmx512m -XX:MaxMetaspaceSize=384m" val TEST_RELEASE_TAG = DEFAULT_EMBED_CODE_VERSION val TEST_RELEASE_VERSION = TEST_RELEASE_TAG.removePrefix("v") } diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt index 41b53d9..60d47c3 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt @@ -28,8 +28,11 @@ package io.spine.embedcode.gradle import groovy.json.JsonSlurper import org.gradle.api.GradleException +import java.io.IOException +import java.net.HttpURLConnection import java.net.URI import java.net.URLEncoder +import java.net.URLConnection import java.nio.channels.Channels import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -40,6 +43,10 @@ import java.security.MessageDigest import java.util.HexFormat private const val SHA256_LENGTH = 64 +private const val CHECKSUM_METADATA_CONNECT_TIMEOUT_MILLIS = 30_000 +private const val CHECKSUM_METADATA_READ_TIMEOUT_MILLIS = 120_000 +private const val FIRST_SUCCESSFUL_HTTP_STATUS = 200 +private const val LAST_SUCCESSFUL_HTTP_STATUS = 299 /** * Calculates the lowercase SHA-256 digest of [file]. @@ -139,6 +146,57 @@ internal fun parseGitHubAssetSha256(json: String, assetName: String): String { return normalizeSha256(digest) } +/** + * Reads GitHub release metadata and limits token use to the GitHub API host. + */ +internal fun readGitHubReleaseMetadata( + source: URI, + githubToken: String?, + readMetadata: (URI, String?) -> String = ::readChecksumMetadata, +): String { + val token = if (source.host.equals("api.github.com", ignoreCase = true)) { + githubToken?.trim()?.ifEmpty { null } + } else { + null + } + return readMetadata(source, token) +} + +/** + * Reads UTF-8 checksum metadata from [source] without following HTTP redirects. + */ +internal fun readChecksumMetadata(source: URI, githubToken: String? = null): String { + var connection: URLConnection? = null + try { + connection = source.toURL().openConnection() + connection.connectTimeout = CHECKSUM_METADATA_CONNECT_TIMEOUT_MILLIS + connection.readTimeout = CHECKSUM_METADATA_READ_TIMEOUT_MILLIS + connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") + connection.setRequestProperty("Accept", "application/vnd.github+json") + if (githubToken != null) { + connection.setRequestProperty("Authorization", "Bearer $githubToken") + } + if (connection is HttpURLConnection) { + connection.instanceFollowRedirects = false + val status = connection.responseCode + if (status < FIRST_SUCCESSFUL_HTTP_STATUS || status > LAST_SUCCESSFUL_HTTP_STATUS) { + throw GradleException( + "Could not read checksum metadata: HTTP $status from $source.", + ) + } + } + return connection.getInputStream() + .bufferedReader(StandardCharsets.UTF_8) + .use { reader -> reader.readText() } + } catch (exception: IOException) { + throw GradleException("Could not read checksum metadata from $source.", exception) + } finally { + if (connection is HttpURLConnection) { + connection.disconnect() + } + } +} + /** * Resolves the trusted digest for a downloaded release asset. * diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt index 15af6b1..26c5036 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt @@ -422,12 +422,7 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { releaseTag, asset, ) { metadataSource -> - val token = if (metadataSource.host.equals("api.github.com", ignoreCase = true)) { - githubToken.orNull?.trim()?.ifEmpty { null } - } else { - null - } - readText(metadataSource, token) + readGitHubReleaseMetadata(metadataSource, githubToken.orNull) } /** @@ -492,163 +487,6 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { const val READ_TIMEOUT_MILLIS = 120_000 const val BUFFER_SIZE = 8_192 - /** - * Detects symbolic links and directory redirects such as Windows junctions. - */ - fun isRedirectingFileSystemEntry(path: Path): Boolean { - return try { - if (Files.isSymbolicLink(path)) { - true - } else { - val parent = path.parent ?: return false - val expectedRealPath = parent.toRealPath().resolve(path.fileName).normalize() - path.toRealPath() != expectedRealPath - } - } catch (exception: IOException) { - throw GradleException( - "Could not inspect Embed Code installation path `$path`.", - exception, - ) - } - } - - /** - * Owns the normalized installation root and validates paths used by one task action. - * - * The task checks all configured paths before [prepare] creates the root. Later - * operations reuse this instance and recheck the root without repeating its creation. - */ - private class InstallationDirectory(path: Path) { - - private val root = path.toAbsolutePath().normalize() - - /** - * Normalizes [path] and verifies that it is a strict descendant of the root. - */ - fun requireInside(path: Path): Path { - val normalizedPath = path.toAbsolutePath().normalize() - if (normalizedPath == root || !normalizedPath.startsWith(root)) { - throw GradleException( - "Embed Code installation path `$normalizedPath` " + - "must remain inside `$root`.", - ) - } - return normalizedPath - } - - /** - * Creates the installation root and rejects a symlink or non-directory root. - */ - fun prepare() { - try { - if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { - Files.createDirectories(root) - } - } catch (exception: IOException) { - throw GradleException( - "Could not create Embed Code installation directory `$root`.", - exception, - ) - } - requireRealRoot() - } - - /** - * Rejects a symbolic-link, redirecting, missing, or non-directory root. - */ - private fun requireRealRoot() { - if ( - isRedirectingFileSystemEntry(root) || - !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation directory `$root` must be a real directory, " + - "not a symbolic link or redirecting entry.", - ) - } - } - - /** - * Rejects symbolic links in every existing component from the installation root. - */ - fun requireNoSymbolicLinks(path: Path) { - requireRealRoot() - val normalizedPath = requireInside(path) - var current = root - root.relativize(normalizedPath).forEach { component -> - current = current.resolve(component) - if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { - if (isRedirectingFileSystemEntry(current)) { - throw GradleException( - "Embed Code installation path `$current` must not be a " + - "symbolic link or redirecting filesystem entry.", - ) - } - if ( - current != normalizedPath && - !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation path component `$current` " + - "must be a directory.", - ) - } - } - } - } - - /** - * Creates [directory] component by component without following symbolic links. - */ - fun createDirectoriesSafely(directory: Path) { - requireRealRoot() - val normalizedDirectory = directory.toAbsolutePath().normalize() - if (normalizedDirectory != root) { - requireInside(normalizedDirectory) - } - var current = root - root.relativize(normalizedDirectory).forEach { component -> - current = current.resolve(component) - if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { - if ( - isRedirectingFileSystemEntry(current) || - !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) - ) { - throw GradleException( - "Embed Code installation directory `$current` " + - "must be a real directory, not a symbolic link " + - "or redirecting entry.", - ) - } - } else { - try { - Files.createDirectory(current) - } catch (exception: IOException) { - throw GradleException( - "Could not create Embed Code installation directory `$current`.", - exception, - ) - } - } - } - requireRealPathInside(normalizedDirectory) - } - - /** - * Verifies that [directory] resolves below the real installation root. - */ - private fun requireRealPathInside(directory: Path) { - val realRoot = root.toRealPath() - val realDirectory = directory.toRealPath() - if (!realDirectory.startsWith(realRoot)) { - throw GradleException( - "Embed Code installation directory `$realDirectory` " + - "must remain inside `$realRoot`.", - ) - } - } - } - /** * Adds an upgrade hint when an exact tag may be missing its former automatic prefix. */ @@ -714,41 +552,6 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { } } - /** - * Reads UTF-8 text from [source], reporting HTTP failures clearly. - */ - fun readText(source: URI, githubToken: String? = null): String { - var connection: URLConnection? = null - try { - connection = source.toURL().openConnection() - connection.connectTimeout = CONNECT_TIMEOUT_MILLIS - connection.readTimeout = READ_TIMEOUT_MILLIS - connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") - connection.setRequestProperty("Accept", "application/vnd.github+json") - if (githubToken != null) { - connection.setRequestProperty("Authorization", "Bearer $githubToken") - } - if (connection is HttpURLConnection) { - connection.instanceFollowRedirects = false - val status = connection.responseCode - if (status < 200 || status > 299) { - throw GradleException( - "Could not read checksum metadata: HTTP $status from $source.", - ) - } - } - return connection.getInputStream() - .bufferedReader(StandardCharsets.UTF_8) - .use { reader -> reader.readText() } - } catch (exception: IOException) { - throw GradleException("Could not read checksum metadata from $source.", exception) - } finally { - if (connection is HttpURLConnection) { - connection.disconnect() - } - } - } - /** * Reads a UTF-8 cache metadata value without following a symbolic link. */ diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt new file mode 100644 index 0000000..ffa70f4 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt @@ -0,0 +1,188 @@ +/* + * 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.embedcode.gradle + +import org.gradle.api.GradleException +import java.io.IOException +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path + +/** + * Owns a normalized installation root and validates paths used by one task action. + * + * The task checks all configured paths before [prepare] creates the root. Later operations reuse + * this instance and recheck the root without repeating its creation. + */ +internal class InstallationDirectory(path: Path) { + + private val root = path.toAbsolutePath().normalize() + + /** + * Normalizes [path] and verifies that it is a strict descendant of the root. + */ + internal fun requireInside(path: Path): Path { + val normalizedPath = path.toAbsolutePath().normalize() + if (normalizedPath == root || !normalizedPath.startsWith(root)) { + throw GradleException( + "Embed Code installation path `$normalizedPath` must remain inside `$root`.", + ) + } + return normalizedPath + } + + /** + * Creates the installation root and rejects a symlink or non-directory root. + */ + internal fun prepare() { + try { + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(root) + } + } catch (exception: IOException) { + throw GradleException( + "Could not create Embed Code installation directory `$root`.", + exception, + ) + } + requireRealRoot() + } + + /** + * Rejects a symbolic-link, redirecting, missing, or non-directory root. + */ + private fun requireRealRoot() { + if ( + isRedirectingFileSystemEntry(root) || + !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation directory `$root` must be a real directory, " + + "not a symbolic link or redirecting entry.", + ) + } + } + + /** + * Rejects symbolic links in every existing component from the installation root. + */ + internal fun requireNoSymbolicLinks(path: Path) { + requireRealRoot() + val normalizedPath = requireInside(path) + var current = root + root.relativize(normalizedPath).forEach { component -> + current = current.resolve(component) + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + if (isRedirectingFileSystemEntry(current)) { + throw GradleException( + "Embed Code installation path `$current` must not be a " + + "symbolic link or redirecting filesystem entry.", + ) + } + if ( + current != normalizedPath && + !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation path component `$current` must be a directory.", + ) + } + } + } + } + + /** + * Creates [directory] component by component without following symbolic links. + */ + internal fun createDirectoriesSafely(directory: Path) { + requireRealRoot() + val normalizedDirectory = directory.toAbsolutePath().normalize() + if (normalizedDirectory != root) { + requireInside(normalizedDirectory) + } + var current = root + root.relativize(normalizedDirectory).forEach { component -> + current = current.resolve(component) + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + if ( + isRedirectingFileSystemEntry(current) || + !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) + ) { + throw GradleException( + "Embed Code installation directory `$current` " + + "must be a real directory, not a symbolic link " + + "or redirecting entry.", + ) + } + } else { + try { + Files.createDirectory(current) + } catch (exception: IOException) { + throw GradleException( + "Could not create Embed Code installation directory `$current`.", + exception, + ) + } + } + } + requireRealPathInside(normalizedDirectory) + } + + /** + * Verifies that [directory] resolves below the real installation root. + */ + internal fun requireRealPathInside(directory: Path) { + val realRoot = root.toRealPath() + val realDirectory = directory.toRealPath() + if (!realDirectory.startsWith(realRoot)) { + throw GradleException( + "Embed Code installation directory `$realDirectory` " + + "must remain inside `$realRoot`.", + ) + } + } +} + +/** + * Detects symbolic links and directory redirects such as Windows junctions. + */ +internal fun isRedirectingFileSystemEntry(path: Path): Boolean { + return try { + if (Files.isSymbolicLink(path)) { + true + } else { + val parent = path.parent ?: return false + val expectedRealPath = parent.toRealPath().resolve(path.fileName).normalize() + path.toRealPath() != expectedRealPath + } + } catch (exception: IOException) { + throw GradleException( + "Could not inspect Embed Code installation path `$path`.", + exception, + ) + } +} diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt index fa86f45..c8208e9 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt @@ -26,6 +26,8 @@ package io.spine.embedcode.gradle +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer import org.gradle.api.GradleException import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals @@ -33,7 +35,10 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import java.net.InetSocketAddress import java.net.URI +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference @DisplayName("Checksum support should") internal class ChecksumSpec { @@ -126,6 +131,115 @@ internal class ChecksumSpec { assertEquals(digest, parseGitHubAssetSha256(json, "embed-code-linux")) } + @Test + fun `send GitHub headers when reading checksum metadata`() { + val authorization = AtomicReference() + val accept = AtomicReference() + val userAgent = AtomicReference() + val server = startServer() + server.createContext("/metadata") { exchange -> + authorization.set(exchange.requestHeaders.getFirst("Authorization")) + accept.set(exchange.requestHeaders.getFirst("Accept")) + userAgent.set(exchange.requestHeaders.getFirst("User-Agent")) + exchange.respond(200, """{"assets":[]}""") + } + server.start() + + try { + val result = readChecksumMetadata(server.uri("/metadata"), "secret-token") + + assertEquals("""{"assets":[]}""", result) + assertEquals("Bearer secret-token", authorization.get()) + assertEquals("application/vnd.github+json", accept.get()) + assertEquals("embed-code-gradle-plugin", userAgent.get()) + } finally { + server.stop(0) + } + } + + @Test + fun `not follow checksum metadata redirects`() { + val redirectedRequests = AtomicInteger() + val server = startServer() + server.createContext("/metadata") { exchange -> + exchange.responseHeaders.add("Location", server.uri("/redirected").toString()) + exchange.respond(302) + } + server.createContext("/redirected") { exchange -> + redirectedRequests.incrementAndGet() + exchange.respond(200, """{"assets":[]}""") + } + server.start() + + try { + val source = server.uri("/metadata") + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source, "secret-token") + } + + assertEquals( + "Could not read checksum metadata: HTTP 302 from $source.", + error.message, + ) + assertEquals(0, redirectedRequests.get()) + } finally { + server.stop(0) + } + } + + @Test + fun `reject a failed checksum metadata response`() { + val server = startServer() + server.createContext("/metadata") { exchange -> + exchange.respond(503) + } + server.start() + + try { + val source = server.uri("/metadata") + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source) + } + + assertEquals( + "Could not read checksum metadata: HTTP 503 from $source.", + error.message, + ) + } finally { + server.stop(0) + } + } + + @Test + fun `use a trimmed token for the GitHub API host`() { + val source = URI.create("https://api.github.com/repos/owner/repository/releases/tags/v1") + var receivedToken: String? = null + + val result = readGitHubReleaseMetadata(source, " secret-token ") { uri, token -> + assertEquals(source, uri) + receivedToken = token + "metadata" + } + + assertEquals("metadata", result) + assertEquals("secret-token", receivedToken) + } + + @Test + fun `omit a token outside the GitHub API host`() { + val source = URI.create("https://metadata.example.com/releases/v1") + var receivedToken: String? = "not-called" + + readGitHubReleaseMetadata(source, "secret-token") { _, token -> + receivedToken = token + "metadata" + } + + assertNull(receivedToken) + } + @Test fun `reject a non-object GitHub release response`() { val error = assertThrows(GradleException::class.java) { @@ -256,4 +370,17 @@ internal class ChecksumSpec { error.message, ) } + + private fun startServer(): HttpServer = + HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + + private fun HttpServer.uri(path: String): URI = + URI.create("http://127.0.0.1:${address.port}$path") + + private fun HttpExchange.respond(status: Int, body: String = "") { + val content = body.toByteArray() + sendResponseHeaders(status, content.size.toLong()) + responseBody.use { output -> output.write(content) } + close() + } } diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt index a8241ac..a705af6 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/EmbedCodeJsonSpec.kt @@ -40,6 +40,7 @@ internal class EmbedCodeJsonSpec { "C:\\docs\nline", listOf("**/\"quoted\".md", "line\nbreak"), listOf("drafts\\**"), + // Kotlin has no `\f` escape, so form feed uses its Unicode code point. "---\b\u000C\r\t\u0001---", info = true, stacktrace = false, diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt new file mode 100644 index 0000000..08e304d --- /dev/null +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt @@ -0,0 +1,91 @@ +/* + * 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.embedcode.gradle + +import org.gradle.api.GradleException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Files +import java.nio.file.Path + +@DisplayName("`InstallationDirectory` should") +internal class InstallationDirectorySpec { + + @TempDir + private lateinit var temporaryDirectory: Path + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `reject a symbolic link while creating a directory`() { + val root = temporaryDirectory.resolve("installation") + val outside = temporaryDirectory.resolve("outside") + Files.createDirectories(outside) + val redirect = root.resolve("redirect") + val installation = InstallationDirectory(root) + installation.prepare() + Files.createSymbolicLink(redirect, outside) + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(redirect.resolve("child")) + } + + assertEquals( + "Embed Code installation directory `$redirect` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertFalse(Files.exists(outside.resolve("child"))) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `reject a directory whose real path escapes the installation root`() { + val root = temporaryDirectory.resolve("installation") + val outside = temporaryDirectory.resolve("outside") + Files.createDirectories(outside) + val redirect = root.resolve("redirect") + val installation = InstallationDirectory(root) + installation.prepare() + Files.createSymbolicLink(redirect, outside) + + val error = assertThrows(GradleException::class.java) { + installation.requireRealPathInside(redirect) + } + + assertEquals( + "Embed Code installation directory `${outside.toRealPath()}` " + + "must remain inside `${root.toRealPath()}`.", + error.message, + ) + } +} From af1eee5798171563362c4e32a06063ce84937c1d Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 15:00:43 +0200 Subject: [PATCH 5/6] Improve test coverage. --- .../io/spine/embedcode/gradle/ChecksumSpec.kt | 43 ++++++ .../gradle/InstallationDirectorySpec.kt | 124 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt index c8208e9..039c894 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt @@ -35,14 +35,20 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir import java.net.InetSocketAddress import java.net.URI +import java.nio.file.Files +import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference @DisplayName("Checksum support should") internal class ChecksumSpec { + @TempDir + private lateinit var temporaryDirectory: Path + @Test fun `normalize a prefixed uppercase digest`() { val digest = "A".repeat(64) @@ -240,6 +246,43 @@ internal class ChecksumSpec { assertNull(receivedToken) } + @Test + fun `omit an absent or blank token for the GitHub API host`() { + val source = URI.create("https://api.github.com/repos/owner/repository/releases/tags/v1") + var requests = 0 + + listOf(null, " ").forEach { configuredToken -> + readGitHubReleaseMetadata(source, configuredToken) { _, receivedToken -> + assertNull(receivedToken) + requests++ + "metadata" + } + } + + assertEquals(2, requests) + } + + @Test + fun `read release metadata through the default transport`() { + val metadata = temporaryDirectory.resolve("metadata.json") + Files.writeString(metadata, "metadata") + + val result = readGitHubReleaseMetadata(metadata.toUri(), "secret-token") + + assertEquals("metadata", result) + } + + @Test + fun `report a checksum metadata read failure`() { + val source = temporaryDirectory.resolve("missing.json").toUri() + + val error = assertThrows(GradleException::class.java) { + readChecksumMetadata(source) + } + + assertEquals("Could not read checksum metadata from $source.", error.message) + } + @Test fun `reject a non-object GitHub release response`() { val error = assertThrows(GradleException::class.java) { diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt index 08e304d..db54904 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt @@ -30,6 +30,7 @@ import org.gradle.api.GradleException import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledOnOs @@ -44,6 +45,129 @@ internal class InstallationDirectorySpec { @TempDir private lateinit var temporaryDirectory: Path + @Test + fun `reject the installation root as its own descendant`() { + val root = temporaryDirectory.resolve("installation") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.requireInside(root) + } + + assertEquals( + "Embed Code installation path `$root` must remain inside `$root`.", + error.message, + ) + } + + @Test + fun `report a failure to prepare the installation root`() { + val blockingFile = temporaryDirectory.resolve("blocked") + Files.writeString(blockingFile, "unchanged") + val root = blockingFile.resolve("installation") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.prepare() + } + + assertEquals( + "Could not create Embed Code installation directory `$root`.", + error.message, + ) + assertEquals("unchanged", Files.readString(blockingFile)) + } + + @Test + fun `reject a regular file as the installation root`() { + val root = temporaryDirectory.resolve("installation") + Files.writeString(root, "unchanged") + val installation = InstallationDirectory(root) + + val error = assertThrows(GradleException::class.java) { + installation.prepare() + } + + assertEquals( + "Embed Code installation directory `$root` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertEquals("unchanged", Files.readString(root)) + } + + @Test + fun `create a missing directory and accept the installation root`() { + val root = temporaryDirectory.resolve("installation") + val child = root.resolve("child") + val installation = InstallationDirectory(root) + installation.prepare() + + installation.createDirectoriesSafely(root) + installation.createDirectoriesSafely(child) + + assertTrue(Files.isDirectory(child)) + } + + @Test + fun `reject a regular file while creating a directory`() { + val root = temporaryDirectory.resolve("installation") + val blockingFile = root.resolve("blocked") + val installation = InstallationDirectory(root) + installation.prepare() + Files.writeString(blockingFile, "unchanged") + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(blockingFile.resolve("child")) + } + + assertEquals( + "Embed Code installation directory `$blockingFile` must be a real directory, " + + "not a symbolic link or redirecting entry.", + error.message, + ) + assertEquals("unchanged", Files.readString(blockingFile)) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `report a directory creation failure`() { + val root = temporaryDirectory.resolve("installation") + val invalidDirectory = root.resolve("a".repeat(256)) + val installation = InstallationDirectory(root) + installation.prepare() + + val error = assertThrows(GradleException::class.java) { + installation.createDirectoriesSafely(invalidDirectory) + } + + assertEquals( + "Could not create Embed Code installation directory `$invalidDirectory`.", + error.message, + ) + assertFalse(Files.exists(invalidDirectory)) + } + + @Test + fun `report a failure to inspect a filesystem entry`() { + val missing = temporaryDirectory.resolve("missing") + + val error = assertThrows(GradleException::class.java) { + isRedirectingFileSystemEntry(missing) + } + + assertEquals( + "Could not inspect Embed Code installation path `$missing`.", + error.message, + ) + } + + @Test + @EnabledOnOs(OS.LINUX, OS.MAC) + fun `accept the filesystem root as non-redirecting`() { + assertFalse(isRedirectingFileSystemEntry(Path.of("/"))) + } + @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `reject a symbolic link while creating a directory`() { From b33862a42f0e23d2e6ca59947bc9c43fbab99d4d Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 28 Jul 2026 15:30:33 +0200 Subject: [PATCH 6/6] Improve coverage behavior. --- .github/workflows/coverage.yml | 1 - codecov.yml | 4 ---- .../embedcode/gradle/EmbedCodePluginSpec.kt | 18 +-------------- .../io/spine/embedcode/gradle/Checksum.kt | 12 +++++----- .../embedcode/gradle/InstallEmbedCodeTask.kt | 11 +++++----- .../embedcode/gradle/InstallationDirectory.kt | 10 ++++----- .../io/spine/embedcode/gradle/ChecksumSpec.kt | 8 +++++-- .../gradle/InstallationDirectorySpec.kt | 22 ------------------- 8 files changed, 23 insertions(+), 63 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b6e9cc2..ea32665 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -53,5 +53,4 @@ jobs: uses: codecov/codecov-action@v7 with: files: ./gradle-plugin/build/reports/jacoco/test/jacocoTestReport.xml - fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/codecov.yml b/codecov.yml index 65d365e..5a9052a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,7 +8,3 @@ coverage: target: 90% threshold: 0% only_pulls: true - -ignore: - - "buildSrc/**" - - "scripts/**" diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt index 3479ebc..5122e4f 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/embedcode/gradle/EmbedCodePluginSpec.kt @@ -32,7 +32,6 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -1169,22 +1168,6 @@ internal class EmbedCodePluginSpec { result.task(":checkEmbedding")?.outcome shouldBe TaskOutcome.SUCCESS } - @Test - fun `reject coverage collection with the configuration cache`() { - val error = assertThrows(IllegalArgumentException::class.java) { - runner( - "help", - collectCoverage = true, - useConfigurationCache = true, - ) - } - - assertEquals( - "TestKit coverage collection is incompatible with the configuration cache.", - error.message, - ) - } - /** * Creates a runner using the plugin-under-test classpath. * @@ -1448,6 +1431,7 @@ internal class EmbedCodePluginSpec { private companion object { const val TEST_KIT_COVERAGE_JVM_ARGUMENT_PROPERTY = "io.spine.embedcode.gradle.testkit.coverage.jvm-argument" + // `org.gradle.jvmargs` replaces Gradle's defaults, so retain them before the JaCoCo agent. const val DEFAULT_GRADLE_DAEMON_JVM_ARGUMENTS = "-Xmx512m -XX:MaxMetaspaceSize=384m" val TEST_RELEASE_TAG = DEFAULT_EMBED_CODE_VERSION diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt index 60d47c3..ef83b43 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/Checksum.kt @@ -31,8 +31,8 @@ import org.gradle.api.GradleException import java.io.IOException import java.net.HttpURLConnection import java.net.URI -import java.net.URLEncoder import java.net.URLConnection +import java.net.URLEncoder import java.nio.channels.Channels import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -43,8 +43,6 @@ import java.security.MessageDigest import java.util.HexFormat private const val SHA256_LENGTH = 64 -private const val CHECKSUM_METADATA_CONNECT_TIMEOUT_MILLIS = 30_000 -private const val CHECKSUM_METADATA_READ_TIMEOUT_MILLIS = 120_000 private const val FIRST_SUCCESSFUL_HTTP_STATUS = 200 private const val LAST_SUCCESSFUL_HTTP_STATUS = 299 @@ -147,12 +145,12 @@ internal fun parseGitHubAssetSha256(json: String, assetName: String): String { } /** - * Reads GitHub release metadata and limits token use to the GitHub API host. + * Reads GitHub release metadata with [readMetadata] and limits token use to the GitHub API host. */ internal fun readGitHubReleaseMetadata( source: URI, githubToken: String?, - readMetadata: (URI, String?) -> String = ::readChecksumMetadata, + readMetadata: (URI, String?) -> String, ): String { val token = if (source.host.equals("api.github.com", ignoreCase = true)) { githubToken?.trim()?.ifEmpty { null } @@ -169,8 +167,8 @@ internal fun readChecksumMetadata(source: URI, githubToken: String? = null): Str var connection: URLConnection? = null try { connection = source.toURL().openConnection() - connection.connectTimeout = CHECKSUM_METADATA_CONNECT_TIMEOUT_MILLIS - connection.readTimeout = CHECKSUM_METADATA_READ_TIMEOUT_MILLIS + connection.connectTimeout = HTTP_CONNECT_TIMEOUT_MILLIS + connection.readTimeout = HTTP_READ_TIMEOUT_MILLIS connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") connection.setRequestProperty("Accept", "application/vnd.github+json") if (githubToken != null) { diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt index 26c5036..6787472 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallEmbedCodeTask.kt @@ -54,6 +54,9 @@ import java.nio.file.StandardCopyOption import java.nio.file.StandardOpenOption import java.util.zip.ZipInputStream +internal const val HTTP_CONNECT_TIMEOUT_MILLIS = 30_000 +internal const val HTTP_READ_TIMEOUT_MILLIS = 120_000 + /** * Downloads and prepares the Embed Code executable selected for the host. * @@ -422,7 +425,7 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { releaseTag, asset, ) { metadataSource -> - readGitHubReleaseMetadata(metadataSource, githubToken.orNull) + readGitHubReleaseMetadata(metadataSource, githubToken.orNull, ::readChecksumMetadata) } /** @@ -483,8 +486,6 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { private companion object { - const val CONNECT_TIMEOUT_MILLIS = 30_000 - const val READ_TIMEOUT_MILLIS = 120_000 const val BUFFER_SIZE = 8_192 /** @@ -517,8 +518,8 @@ public abstract class InstallEmbedCodeTask : DefaultTask() { var connection: URLConnection? = null try { connection = source.toURL().openConnection() - connection.connectTimeout = CONNECT_TIMEOUT_MILLIS - connection.readTimeout = READ_TIMEOUT_MILLIS + connection.connectTimeout = HTTP_CONNECT_TIMEOUT_MILLIS + connection.readTimeout = HTTP_READ_TIMEOUT_MILLIS connection.setRequestProperty("User-Agent", "embed-code-gradle-plugin") if (connection is HttpURLConnection) { diff --git a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt index ffa70f4..0cd0c88 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/embedcode/gradle/InstallationDirectory.kt @@ -45,7 +45,7 @@ internal class InstallationDirectory(path: Path) { /** * Normalizes [path] and verifies that it is a strict descendant of the root. */ - internal fun requireInside(path: Path): Path { + fun requireInside(path: Path): Path { val normalizedPath = path.toAbsolutePath().normalize() if (normalizedPath == root || !normalizedPath.startsWith(root)) { throw GradleException( @@ -58,7 +58,7 @@ internal class InstallationDirectory(path: Path) { /** * Creates the installation root and rejects a symlink or non-directory root. */ - internal fun prepare() { + fun prepare() { try { if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { Files.createDirectories(root) @@ -90,7 +90,7 @@ internal class InstallationDirectory(path: Path) { /** * Rejects symbolic links in every existing component from the installation root. */ - internal fun requireNoSymbolicLinks(path: Path) { + fun requireNoSymbolicLinks(path: Path) { requireRealRoot() val normalizedPath = requireInside(path) var current = root @@ -118,7 +118,7 @@ internal class InstallationDirectory(path: Path) { /** * Creates [directory] component by component without following symbolic links. */ - internal fun createDirectoriesSafely(directory: Path) { + fun createDirectoriesSafely(directory: Path) { requireRealRoot() val normalizedDirectory = directory.toAbsolutePath().normalize() if (normalizedDirectory != root) { @@ -155,7 +155,7 @@ internal class InstallationDirectory(path: Path) { /** * Verifies that [directory] resolves below the real installation root. */ - internal fun requireRealPathInside(directory: Path) { + private fun requireRealPathInside(directory: Path) { val realRoot = root.toRealPath() val realDirectory = directory.toRealPath() if (!realDirectory.startsWith(realRoot)) { diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt index 039c894..45bcf04 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/ChecksumSpec.kt @@ -263,11 +263,15 @@ internal class ChecksumSpec { } @Test - fun `read release metadata through the default transport`() { + fun `read release metadata through the provided transport`() { val metadata = temporaryDirectory.resolve("metadata.json") Files.writeString(metadata, "metadata") - val result = readGitHubReleaseMetadata(metadata.toUri(), "secret-token") + val result = readGitHubReleaseMetadata( + metadata.toUri(), + "secret-token", + ::readChecksumMetadata, + ) assertEquals("metadata", result) } diff --git a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt index db54904..61ca93b 100644 --- a/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt +++ b/gradle-plugin/src/test/kotlin/io/spine/embedcode/gradle/InstallationDirectorySpec.kt @@ -190,26 +190,4 @@ internal class InstallationDirectorySpec { ) assertFalse(Files.exists(outside.resolve("child"))) } - - @Test - @EnabledOnOs(OS.LINUX, OS.MAC) - fun `reject a directory whose real path escapes the installation root`() { - val root = temporaryDirectory.resolve("installation") - val outside = temporaryDirectory.resolve("outside") - Files.createDirectories(outside) - val redirect = root.resolve("redirect") - val installation = InstallationDirectory(root) - installation.prepare() - Files.createSymbolicLink(redirect, outside) - - val error = assertThrows(GradleException::class.java) { - installation.requireRealPathInside(redirect) - } - - assertEquals( - "Embed Code installation directory `${outside.toRealPath()}` " + - "must remain inside `${root.toRealPath()}`.", - error.message, - ) - } }