From 460c4788f5856a9e756a575538a1dbe858bf682d Mon Sep 17 00:00:00 2001 From: Satyaki Ghosh Date: Sat, 4 Jul 2026 14:36:57 -0400 Subject: [PATCH] Add coverage reporting --- .../actions/print-build-versions/action.yml | 7 +++ .github/workflows/configs.yml | 12 ++++ .github/workflows/validate.yml | 16 ++++- .gitignore | 2 + .../tests/kotlin/build.gradle.kts | 48 +++++++++++++++ src/bindings-jvm/tests/run.sh | 2 +- src/bindings-wasm/build.sh | 2 +- src/bindings-wasm/package-lock.json | 61 +++++++++++++++++++ src/bindings-wasm/tests/package.json | 4 +- src/bindings-wasm/tests/run.sh | 4 +- src/bindings-wasm/tests/vitest.config.ts | 27 ++++---- 11 files changed, 166 insertions(+), 19 deletions(-) create mode 100644 src/bindings-wasm/package-lock.json diff --git a/.github/actions/print-build-versions/action.yml b/.github/actions/print-build-versions/action.yml index f1847ae..ca52777 100644 --- a/.github/actions/print-build-versions/action.yml +++ b/.github/actions/print-build-versions/action.yml @@ -10,6 +10,10 @@ inputs: description: "Path to Cargo.lock used as a fallback source for the wasm-bindgen version" required: false default: "src/Cargo.lock" + jacoco-version: + description: "JaCoCo version, resolved by Gradle during the build rather than from a runner CLI" + required: false + default: "" runs: using: composite @@ -18,6 +22,7 @@ runs: shell: bash env: CARGO_LOCK: ${{ inputs.cargo-lock }} + JACOCO_VERSION: ${{ inputs.jacoco-version }} run: | echo "::group::Build tool versions ($(uname -s) $(uname -m))" echo "os: $(uname -srm)" @@ -64,4 +69,6 @@ runs: fi if command -v cargo-about >/dev/null 2>&1; then echo "cargo-about: $(cargo-about --version 2>/dev/null | head -1)"; else echo "cargo-about: not installed"; fi if command -v cargo-audit >/dev/null 2>&1; then echo "cargo-audit: $(cargo-audit --version 2>/dev/null | head -1)"; else echo "cargo-audit: not installed"; fi + if command -v cargo-llvm-cov >/dev/null 2>&1; then echo "cargo-llvm-cov: $(cargo-llvm-cov --version 2>/dev/null | head -1)"; else echo "cargo-llvm-cov: not installed"; fi + if [ -n "$JACOCO_VERSION" ]; then echo "jacoco: $JACOCO_VERSION (resolved by Gradle)"; fi echo "::endgroup::" diff --git a/.github/workflows/configs.yml b/.github/workflows/configs.yml index 44e858e..7620576 100644 --- a/.github/workflows/configs.yml +++ b/.github/workflows/configs.yml @@ -45,6 +45,12 @@ on: cargo-audit-version: description: "cargo-audit version" value: ${{ jobs.get-configs.outputs.cargo-audit-version }} + cargo-llvm-cov-version: + description: "cargo-llvm-cov version" + value: ${{ jobs.get-configs.outputs.cargo-llvm-cov-version }} + jacoco-version: + description: "JaCoCo version for the JVM binding coverage report" + value: ${{ jobs.get-configs.outputs.jacoco-version }} permissions: contents: read @@ -67,6 +73,8 @@ jobs: WASM_PACK_VERSION: '0.14.0' CARGO_ABOUT_VERSION: '0.9.0' CARGO_AUDIT_VERSION: '0.22.2' + CARGO_LLVM_COV_VERSION: '0.8.7' + JACOCO_VERSION: '0.8.13' # keep in sync with bindings-jvm/tests/kotlin/build.gradle.kts outputs: app-name: ${{ env.APP_NAME }} working-dir: ${{ env.WORKING_DIR }} @@ -82,6 +90,8 @@ jobs: wasm-pack-version: ${{ env.WASM_PACK_VERSION }} cargo-about-version: ${{ env.CARGO_ABOUT_VERSION }} cargo-audit-version: ${{ env.CARGO_AUDIT_VERSION }} + cargo-llvm-cov-version: ${{ env.CARGO_LLVM_COV_VERSION }} + jacoco-version: ${{ env.JACOCO_VERSION }} steps: - name: Provide configs run: | @@ -99,3 +109,5 @@ jobs: echo "wasm-pack-version = ${{ env.WASM_PACK_VERSION }}" echo "cargo-about-version = ${{ env.CARGO_ABOUT_VERSION }}" echo "cargo-audit-version = ${{ env.CARGO_AUDIT_VERSION }}" + echo "cargo-llvm-cov-version = ${{ env.CARGO_LLVM_COV_VERSION }}" + echo "jacoco-version = ${{ env.JACOCO_VERSION }}" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ce0126f..be8d215 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -96,6 +96,8 @@ jobs: test: needs: [ format, lint, audit, get-configs ] runs-on: ubuntu-latest + env: + WORKING_DIR: ${{ needs.get-configs.outputs.working-dir }} steps: - uses: actions/checkout@v6 with: @@ -105,18 +107,24 @@ jobs: uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ needs.get-configs.outputs.rust-toolchain }} + components: llvm-tools-preview - name: Cache Cargo uses: Swatinem/rust-cache@v2 with: - workspaces: ${{ needs.get-configs.outputs.working-dir }} + workspaces: ${{ env.WORKING_DIR }} + + - name: Install cargo-llvm-cov ${{ needs.get-configs.outputs.cargo-llvm-cov-version }} + uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov@${{ needs.get-configs.outputs.cargo-llvm-cov-version }} - name: Log tool versions uses: ./.github/actions/print-build-versions - name: Test - working-directory: ${{ needs.get-configs.outputs.working-dir }} - run: cargo test --locked --release --workspace --no-fail-fast + working-directory: ${{ env.WORKING_DIR }} + run: cargo llvm-cov --locked --release --workspace --no-fail-fast test-jvm: needs: [ format, lint, audit, get-configs ] @@ -149,6 +157,8 @@ jobs: - name: Log tool versions uses: ./.github/actions/print-build-versions + with: + jacoco-version: ${{ needs.get-configs.outputs.jacoco-version }} - name: Test working-directory: ${{ env.WORKING_DIR }}/bindings-jvm/tests diff --git a/.gitignore b/.gitignore index 3ceafa8..f0c0f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ src/bindings-jvm/generated/**/* # Node / WASM **/node_modules/ **/package-lock.json +!src/bindings-wasm/package-lock.json # Output (regenerable) **/output/** @@ -24,6 +25,7 @@ src/data-source/generated/patched_schemas/ src/**/reports/ **/cdk.out/ *.pyc +**/coverage/** # IDE **/*.iml diff --git a/src/bindings-jvm/tests/kotlin/build.gradle.kts b/src/bindings-jvm/tests/kotlin/build.gradle.kts index 70d9fcf..d264969 100644 --- a/src/bindings-jvm/tests/kotlin/build.gradle.kts +++ b/src/bindings-jvm/tests/kotlin/build.gradle.kts @@ -1,11 +1,16 @@ plugins { kotlin("jvm") version "2.3.10" + jacoco } repositories { mavenCentral() } +jacoco { + toolVersion = "0.8.13" +} + val bindingsJar = file("${rootProject.projectDir}/../../generated/cloudformation-validate.jar") dependencies { @@ -27,6 +32,49 @@ tasks.test { showStackTraces = true exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL } + + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + classDirectories.setFrom( + zipTree(bindingsJar).matching { + include("com/amazonaws/cloudformation/validation/**/*.class") + exclude( + "**/FfiConverter*", + "**/ForeignBytes*", + "**/RustBuffer*", + "**/Uniffi*", + "**/*Cleaner*", + "**/NoPointer*", + "**/UByteArray*", + ) + }, + ) + + reports { + xml.required.set(true) + html.required.set(true) + } + + doLast { + val reportFile = reports.xml.outputLocation.get().asFile + val factory = javax.xml.parsers.DocumentBuilderFactory.newInstance() + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) + val report = factory.newDocumentBuilder().parse(reportFile).documentElement + val counters = report.childNodes + println("Kotlin coverage:") + for (i in 0 until counters.length) { + val node = counters.item(i) + if (node.nodeName != "counter") continue + val type = node.attributes.getNamedItem("type").nodeValue + val covered = node.attributes.getNamedItem("covered").nodeValue.toInt() + val missed = node.attributes.getNamedItem("missed").nodeValue.toInt() + val total = covered + missed + val pct = if (total > 0) 100.0 * covered / total else 0.0 + println(" %-12s %6d/%-6d %6.2f%%".format(type, covered, total, pct)) + } + } } kotlin { diff --git a/src/bindings-jvm/tests/run.sh b/src/bindings-jvm/tests/run.sh index 5814d01..26bc740 100755 --- a/src/bindings-jvm/tests/run.sh +++ b/src/bindings-jvm/tests/run.sh @@ -7,4 +7,4 @@ KOTLIN_DIR="$SCRIPT_DIR/kotlin" echo "Running Kotlin smoke tests..." cd "$KOTLIN_DIR" -gradle test --no-daemon --rerun-tasks +gradle test jacocoTestReport --no-daemon --rerun-tasks --console=rich diff --git a/src/bindings-wasm/build.sh b/src/bindings-wasm/build.sh index 40f181f..1ca8541 100755 --- a/src/bindings-wasm/build.sh +++ b/src/bindings-wasm/build.sh @@ -32,7 +32,7 @@ if [ -f "$WASM_DTS" ] && ! grep -q "^export type JsonValue" "$WASM_DTS"; then fi cd "$SCRIPT_DIR" -npm ci --silent 2>/dev/null +npm ci --silent npm run build:ts # ── Package metadata ────────────────────────────────────────────────────────── diff --git a/src/bindings-wasm/package-lock.json b/src/bindings-wasm/package-lock.json new file mode 100644 index 0000000..4ba04c0 --- /dev/null +++ b/src/bindings-wasm/package-lock.json @@ -0,0 +1,61 @@ +{ + "name": "@aws/cloudformation-validate", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@aws/cloudformation-validate", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "25.9.3", + "prettier": "3.8.4", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.15.0", + "npm": ">=10.5.0" + } + }, + "node_modules/@types/node": { + "version": "25.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "dev": true, + "license": "MIT" + } + } +} diff --git a/src/bindings-wasm/tests/package.json b/src/bindings-wasm/tests/package.json index 2b8ca14..5e57c28 100644 --- a/src/bindings-wasm/tests/package.json +++ b/src/bindings-wasm/tests/package.json @@ -1,10 +1,12 @@ { "private": true, "scripts": { - "test": "vitest run" + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "devDependencies": { "vitest": "4.1.8", + "@vitest/coverage-v8": "4.1.8", "@types/node": "25.9.3" } } diff --git a/src/bindings-wasm/tests/run.sh b/src/bindings-wasm/tests/run.sh index 8534b22..748f60a 100755 --- a/src/bindings-wasm/tests/run.sh +++ b/src/bindings-wasm/tests/run.sh @@ -6,5 +6,5 @@ BINDINGS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" echo "Running smoke tests..." cd "$SCRIPT_DIR" -npm ci --silent 2>/dev/null -npm test +npm install --silent +npm run test:coverage diff --git a/src/bindings-wasm/tests/vitest.config.ts b/src/bindings-wasm/tests/vitest.config.ts index fee73ac..f6a1206 100644 --- a/src/bindings-wasm/tests/vitest.config.ts +++ b/src/bindings-wasm/tests/vitest.config.ts @@ -1,15 +1,20 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - testTimeout: 120_000, - hookTimeout: 120_000, - teardownTimeout: 120_000, - pool: "threads", - poolOptions: { - threads: { - singleThread: true, - }, + root: '..', + test: { + include: ['tests/**/*.test.ts'], + testTimeout: 120_000, + hookTimeout: 120_000, + teardownTimeout: 120_000, + fileParallelism: false, + coverage: { + provider: 'v8', + include: ['dist/**/*.js'], + exclude: ['dist/package.json'], + reporter: ['text', 'lcov', 'json-summary'], + reportsDirectory: 'tests/coverage', + reportOnFailure: true, + }, }, - }, });