From 67dc32085548b452a1f8d2af885880345ebd35f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 17:07:29 +0000 Subject: [PATCH 01/15] Add JaCoCo test coverage reporting to build Enables automatic test coverage analysis via the JaCoCo Gradle plugin. Generates HTML and XML reports, excluding generated ANTLR and shaded code. Usage: ./gradlew test jacocoTestReport https://claude.ai/code/session_01R1nvwA9Gc7gv8Uxwyvy83u --- build.gradle | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/build.gradle b/build.gradle index 8f60a307d..04ab4a951 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ plugins { id "groovy" id "me.champeau.jmh" version "0.7.3" id "net.ltgt.errorprone" version '5.0.0' + id 'jacoco' // // Kotlin just for tests - not production code id 'org.jetbrains.kotlin.jvm' version '2.3.0' @@ -441,6 +442,30 @@ test.dependsOn testWithJava21 test.dependsOn testWithJava17 test.dependsOn testWithJava11 +jacoco { + toolVersion = "0.8.12" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + csv.required = false + } + + // Exclude generated ANTLR code from coverage + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + 'graphql/parser/antlr/**', + 'graphql/com/google/**', + 'graphql/org/antlr/**' + ]) + })) + } +} + /* * The gradle.buildFinished callback is deprecated BUT there does not seem to be a decent alternative in gradle 7 From e6f133659d5260e48b505a81ab4c816f8f83019c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 17:21:26 +0000 Subject: [PATCH 02/15] Add JaCoCo coverage report as PR comment Add Madrapps/jacoco-report action (pinned to commit SHA for supply chain safety) to post a sticky coverage summary comment on PRs. The action runs only in the matrix job that generates the JaCoCo report and updates the same comment on each new commit. https://claude.ai/code/session_01R1nvwA9Gc7gv8Uxwyvy83u --- .github/workflows/pull_request.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d80cee7e4..4c57b82fb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - gradle-argument: [ 'assemble && ./gradlew check -x test','testWithJava11', 'testWithJava17','testWithJava21', 'test -x testWithJava11 -x testWithJava17 -x testWithJava21' ] + gradle-argument: [ 'assemble && ./gradlew check -x test','testWithJava11', 'testWithJava17','testWithJava21', 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' ] steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 @@ -31,6 +31,18 @@ jobs: distribution: 'corretto' - name: build and test run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace + - name: Publish Coverage Report + uses: Madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2 + if: > + always() && + github.event_name == 'pull_request' && + contains(matrix.gradle-argument, 'jacocoTestReport') + with: + paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 0 + min-coverage-changed-files: 0 + update-comment: true - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2.23.0 if: always() From 90b8d6c75a3946cd07c9f5a5912a0fe4dea43964 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 20:40:18 +0000 Subject: [PATCH 03/15] Replace Madrapps/jacoco-report with per-package coverage table Swap the third-party JaCoCo action for a custom github-script step that parses the JaCoCo XML and posts a per-package coverage breakdown (line, branch, method) as a PR comment. Also uploads the full HTML report as a build artifact for class-level drill-down. https://claude.ai/code/session_01R1nvwA9Gc7gv8Uxwyvy83u --- .github/workflows/pull_request.yml | 99 +++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4c57b82fb..af24f4689 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -31,18 +31,103 @@ jobs: distribution: 'corretto' - name: build and test run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace - - name: Publish Coverage Report - uses: Madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2 + - name: Upload Coverage HTML Report + uses: actions/upload-artifact@v4 + if: > + always() && + contains(matrix.gradle-argument, 'jacocoTestReport') + with: + name: jacoco-html-report + path: build/reports/jacoco/test/html/ + retention-days: 14 + - name: Publish Per-Package Coverage Comment if: > always() && github.event_name == 'pull_request' && contains(matrix.gradle-argument, 'jacocoTestReport') + uses: actions/github-script@v7 with: - paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml - token: ${{ secrets.GITHUB_TOKEN }} - min-coverage-overall: 0 - min-coverage-changed-files: 0 - update-comment: true + script: | + const fs = require('fs'); + const path = require('path'); + const xmlFile = path.join(process.env.GITHUB_WORKSPACE, 'build/reports/jacoco/test/jacocoTestReport.xml'); + if (!fs.existsSync(xmlFile)) { + core.warning('JaCoCo XML report not found'); + return; + } + const xml = fs.readFileSync(xmlFile, 'utf8'); + + function extractCounters(element) { + const counters = {}; + const re = //g; + let m; + while ((m = re.exec(element)) !== null) { + counters[m[1]] = { missed: parseInt(m[2]), covered: parseInt(m[3]) }; + } + return counters; + } + + function pct(c) { + if (!c) return 'N/A'; + const total = c.missed + c.covered; + return total === 0 ? 'N/A' : (c.covered / total * 100).toFixed(1) + '%'; + } + + // Parse overall counters (last set of tags at report level) + const reportMatch = xml.match(/]*>([\s\S]*)<\/report>/); + const reportBody = reportMatch ? reportMatch[1] : xml; + + // Extract packages + const pkgRegex = /([\s\S]*?)<\/package>/g; + const packages = []; + let pm; + while ((pm = pkgRegex.exec(reportBody)) !== null) { + const name = pm[1].replace(/\//g, '.'); + const counters = extractCounters(pm[2]); + packages.push({ name, counters }); + } + + // Sort by package name + packages.sort((a, b) => a.name.localeCompare(b.name)); + + // Overall counters (direct children of , after all packages) + const overallCounters = extractCounters(reportBody.replace(//g, '')); + + let body = '## JaCoCo Coverage Report\n\n'; + body += '| Package | Line | Branch | Method |\n'; + body += '|:--------|-----:|-------:|-------:|\n'; + + for (const pkg of packages) { + const c = pkg.counters; + body += `| \`${pkg.name}\` | ${pct(c.LINE)} | ${pct(c.BRANCH)} | ${pct(c.METHOD)} |\n`; + } + + body += `| **Overall** | **${pct(overallCounters.LINE)}** | **${pct(overallCounters.BRANCH)}** | **${pct(overallCounters.METHOD)}** |\n`; + body += '\n> Full HTML report available as build artifact `jacoco-html-report`\n'; + + // Find and update or create the comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const marker = '## JaCoCo Coverage Report'; + const existing = comments.find(c => c.body && c.body.startsWith(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2.23.0 if: always() From 62c5cd0c028acaf090999647c1c875ada79c1aaa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 21:05:07 +0000 Subject: [PATCH 04/15] Replace test overview action with per-Java-version custom summary - Remove EnricoMi/publish-unit-test-result-action - Restructure build matrix to use labeled includes (java11/17/21/25) - Each test job parses JUnit XML results and uploads stats as artifact - New test-summary job collects all stats, compares against committed baseline (test-stats-baseline.json), and posts a PR comment showing per-version totals with deltas (passed/failed/errors/skipped) - The summary job commits the updated baseline to the PR branch so it arrives on master through the normal merge flow - Uses [skip ci] on baseline commits to prevent infinite loops https://claude.ai/code/session_01R1nvwA9Gc7gv8Uxwyvy83u --- .github/workflows/pull_request.yml | 181 ++++++++++++++++++++++++++--- test-stats-baseline.json | 6 + 2 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 test-stats-baseline.json diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index af24f4689..6d5198877 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,7 +12,8 @@ on: - 21.x - 20.x - 19.x -permissions: # For test comment bot +permissions: + contents: write checks: write pull-requests: write jobs: @@ -20,7 +21,21 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - gradle-argument: [ 'assemble && ./gradlew check -x test','testWithJava11', 'testWithJava17','testWithJava21', 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' ] + include: + - gradle-argument: 'assemble && ./gradlew check -x test' + label: 'check' + - gradle-argument: 'testWithJava11' + label: 'java11' + test-results-dir: 'testWithJava11' + - gradle-argument: 'testWithJava17' + label: 'java17' + test-results-dir: 'testWithJava17' + - gradle-argument: 'testWithJava21' + label: 'java21' + test-results-dir: 'testWithJava21' + - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' + label: 'java25' + test-results-dir: 'test' steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 @@ -33,9 +48,7 @@ jobs: run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace - name: Upload Coverage HTML Report uses: actions/upload-artifact@v4 - if: > - always() && - contains(matrix.gradle-argument, 'jacocoTestReport') + if: always() && matrix.label == 'java25' with: name: jacoco-html-report path: build/reports/jacoco/test/html/ @@ -44,7 +57,7 @@ jobs: if: > always() && github.event_name == 'pull_request' && - contains(matrix.gradle-argument, 'jacocoTestReport') + matrix.label == 'java25' uses: actions/github-script@v7 with: script: | @@ -128,15 +141,155 @@ jobs: body, }); } - - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@v2.23.0 - if: always() + - name: Parse Test Results + if: always() && matrix.label != 'check' + run: | + dir="build/test-results/${{ matrix.test-results-dir }}" + total=0; failures=0; errors=0; skipped=0 + for f in "$dir"/TEST-*.xml; do + [ -f "$f" ] || continue + t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + total=$((total + ${t:-0})) + failures=$((failures + ${fl:-0})) + errors=$((errors + ${e:-0})) + skipped=$((skipped + ${s:-0})) + done + passed=$((total - failures - errors - skipped)) + mkdir -p /tmp/test-stats + echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \ + > "/tmp/test-stats/${{ matrix.label }}.json" + - name: Upload Test Stats + if: always() && matrix.label != 'check' + uses: actions/upload-artifact@v4 + with: + name: test-stats-${{ matrix.label }} + path: /tmp/test-stats/${{ matrix.label }}.json + test-summary: + needs: buildAndTest + if: always() && github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 with: - files: | - **/build/test-results/test/TEST-*.xml - **/build/test-results/testWithJava11/TEST-*.xml - **/build/test-results/testWithJava17/TEST-*.xml - **/build/test-results/testWithJava21/TEST-*.xml + ref: ${{ github.head_ref }} + - name: Download Test Stats + uses: actions/download-artifact@v4 + with: + pattern: test-stats-* + merge-multiple: true + path: test-stats/ + - name: Generate Test Summary and Update Baseline + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const versions = ['java11', 'java17', 'java21', 'java25']; + const zero = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 }; + + // Read current stats from artifacts + const current = {}; + for (const v of versions) { + const file = path.join('test-stats', `${v}.json`); + if (fs.existsSync(file)) { + current[v] = JSON.parse(fs.readFileSync(file, 'utf8')); + } else { + current[v] = null; + } + } + + // Read baseline from repo + const baselineFile = 'test-stats-baseline.json'; + let baseline = {}; + if (fs.existsSync(baselineFile)) { + baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8')); + } + + function delta(curr, prev) { + const d = curr - prev; + if (d === 0) return '\u00b10'; + return d > 0 ? `+${d}` : `${d}`; + } + + function fmtCell(curr, prev) { + return `${curr} (${delta(curr, prev)})`; + } + + let body = '## Test Results\n\n'; + body += '| Java Version | Total | Passed | Failed | Errors | Skipped |\n'; + body += '|:-------------|------:|-------:|-------:|-------:|--------:|\n'; + + for (const v of versions) { + const c = current[v] || zero; + const b = baseline[v] || zero; + const label = v.replace('java', 'Java '); + if (!current[v]) { + body += `| ${label} | - | - | - | - | - |\n`; + } else { + body += `| ${label} | ${fmtCell(c.total, b.total)} | ${fmtCell(c.passed, b.passed)} | ${fmtCell(c.failed, b.failed)} | ${fmtCell(c.errors, b.errors)} | ${fmtCell(c.skipped, b.skipped)} |\n`; + } + } + + // Totals row + const totalCurr = { ...zero }; + const totalBase = { ...zero }; + let hasAny = false; + for (const v of versions) { + if (current[v]) { + hasAny = true; + for (const k of Object.keys(zero)) { + totalCurr[k] += current[v][k]; + totalBase[k] += (baseline[v] || zero)[k]; + } + } + } + if (hasAny) { + body += `| **Total** | **${fmtCell(totalCurr.total, totalBase.total)}** | **${fmtCell(totalCurr.passed, totalBase.passed)}** | **${fmtCell(totalCurr.failed, totalBase.failed)}** | **${fmtCell(totalCurr.errors, totalBase.errors)}** | **${fmtCell(totalCurr.skipped, totalBase.skipped)}** |\n`; + } + + // Post or update comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const marker = '## Test Results'; + const existing = comments.find(c => c.body && c.body.startsWith(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + // Write updated baseline for commit + const updated = {}; + for (const v of versions) { + updated[v] = current[v] || baseline[v] || zero; + } + fs.writeFileSync(baselineFile, JSON.stringify(updated, null, 2) + '\n'); + - name: Commit Updated Baseline + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add test-stats-baseline.json + git diff --cached --quiet || { + git commit -m "Update test stats baseline [skip ci]" + git push + } javadoc: runs-on: ubuntu-latest steps: diff --git a/test-stats-baseline.json b/test-stats-baseline.json new file mode 100644 index 000000000..7dce0836c --- /dev/null +++ b/test-stats-baseline.json @@ -0,0 +1,6 @@ +{ + "java11": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java17": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java21": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java25": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 } +} From eb3e93402007ff742fae63bac0c6304979ec8e7a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 08:49:22 +1000 Subject: [PATCH 05/15] Unify test results and coverage into single PR comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate the two separate PR comments (test results + JaCoCo coverage) into one combined comment. Baseline is now only updated on master, never on PR branches. - Rename test-stats-baseline.json → test-baseline.json with tests + coverage sections - PR workflow: upload JaCoCo XML as artifact, post single unified comment - PR workflow: change contents permission to read (no more bot commits on PRs) - Master workflow: add labeled matrix, test parsing, JaCoCo generation - Master workflow: add update-baseline job that commits test-baseline.json Co-Authored-By: Claude Opus 4.6 --- .github/workflows/master.yml | 131 ++++++++++++++++- .github/workflows/pull_request.yml | 216 +++++++++++++---------------- test-baseline.json | 13 ++ test-stats-baseline.json | 6 - 4 files changed, 231 insertions(+), 135 deletions(-) create mode 100644 test-baseline.json delete mode 100644 test-stats-baseline.json diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index d6c53ecaa..8705da2c4 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -4,14 +4,29 @@ on: push: branches: - master -permissions: # For test summary bot +permissions: + contents: write checks: write jobs: buildAndTest: runs-on: ubuntu-latest strategy: matrix: - gradle-argument: [ 'assemble && ./gradlew check -x test','testWithJava11', 'testWithJava17','testWithJava21', 'test -x testWithJava11 -x testWithJava17 -x testWithJava21' ] + include: + - gradle-argument: 'assemble && ./gradlew check -x test' + label: 'check' + - gradle-argument: 'testWithJava11' + label: 'java11' + test-results-dir: 'testWithJava11' + - gradle-argument: 'testWithJava17' + label: 'java17' + test-results-dir: 'testWithJava17' + - gradle-argument: 'testWithJava21' + label: 'java21' + test-results-dir: 'testWithJava21' + - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' + label: 'java25' + test-results-dir: 'test' steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 @@ -24,13 +39,115 @@ jobs: run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2.23.0 - if: always() + if: always() && matrix.label != 'check' with: files: | - **/build/test-results/test/TEST-*.xml - **/build/test-results/testWithJava11/TEST-*.xml - **/build/test-results/testWithJava17/TEST-*.xml - **/build/test-results/testWithJava21/TEST-*.xml + **/build/test-results/${{ matrix.test-results-dir }}/TEST-*.xml + - name: Upload Coverage XML Report + uses: actions/upload-artifact@v4 + if: always() && matrix.label == 'java25' + with: + name: coverage-report + path: build/reports/jacoco/test/jacocoTestReport.xml + retention-days: 1 + - name: Parse Test Results + if: always() && matrix.label != 'check' + run: | + dir="build/test-results/${{ matrix.test-results-dir }}" + total=0; failures=0; errors=0; skipped=0 + for f in "$dir"/TEST-*.xml; do + [ -f "$f" ] || continue + t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + total=$((total + ${t:-0})) + failures=$((failures + ${fl:-0})) + errors=$((errors + ${e:-0})) + skipped=$((skipped + ${s:-0})) + done + passed=$((total - failures - errors - skipped)) + mkdir -p /tmp/test-stats + echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \ + > "/tmp/test-stats/${{ matrix.label }}.json" + - name: Upload Test Stats + if: always() && matrix.label != 'check' + uses: actions/upload-artifact@v4 + with: + name: test-stats-${{ matrix.label }} + path: /tmp/test-stats/${{ matrix.label }}.json + update-baseline: + needs: buildAndTest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Download Test Stats + uses: actions/download-artifact@v4 + with: + pattern: test-stats-* + merge-multiple: true + path: test-stats/ + - name: Download Coverage Report + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-report + path: coverage/ + - name: Update Baseline + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const versions = ['java11', 'java17', 'java21', 'java25']; + const zeroTest = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 }; + const zeroCov = { covered: 0, missed: 0 }; + + // Read current baseline + const baselineFile = 'test-baseline.json'; + let baseline = { tests: {}, coverage: {} }; + if (fs.existsSync(baselineFile)) { + baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8')); + } + + // Update test stats from artifacts + const tests = baseline.tests || {}; + for (const v of versions) { + const file = path.join('test-stats', `${v}.json`); + if (fs.existsSync(file)) { + tests[v] = JSON.parse(fs.readFileSync(file, 'utf8')); + } else { + tests[v] = tests[v] || zeroTest; + } + } + + // Update coverage from JaCoCo XML + let coverage = baseline.coverage || {}; + const jacocoFile = path.join('coverage', 'jacocoTestReport.xml'); + if (fs.existsSync(jacocoFile)) { + const xml = fs.readFileSync(jacocoFile, 'utf8'); + const stripped = xml.replace(//g, ''); + const re = //g; + let m; + while ((m = re.exec(stripped)) !== null) { + if (m[1] === 'LINE') coverage.line = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + else if (m[1] === 'BRANCH') coverage.branch = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + else if (m[1] === 'METHOD') coverage.method = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + } + } + + const updated = { tests, coverage }; + fs.writeFileSync(baselineFile, JSON.stringify(updated, null, 2) + '\n'); + - name: Commit Updated Baseline + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add test-baseline.json + git diff --cached --quiet || { + git commit -m "Update test baseline [skip ci]" + git push + } javadoc: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6d5198877..183aa9cd6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,7 +13,7 @@ on: - 20.x - 19.x permissions: - contents: write + contents: read checks: write pull-requests: write jobs: @@ -53,94 +53,13 @@ jobs: name: jacoco-html-report path: build/reports/jacoco/test/html/ retention-days: 14 - - name: Publish Per-Package Coverage Comment - if: > - always() && - github.event_name == 'pull_request' && - matrix.label == 'java25' - uses: actions/github-script@v7 + - name: Upload Coverage XML Report + uses: actions/upload-artifact@v4 + if: always() && matrix.label == 'java25' with: - script: | - const fs = require('fs'); - const path = require('path'); - const xmlFile = path.join(process.env.GITHUB_WORKSPACE, 'build/reports/jacoco/test/jacocoTestReport.xml'); - if (!fs.existsSync(xmlFile)) { - core.warning('JaCoCo XML report not found'); - return; - } - const xml = fs.readFileSync(xmlFile, 'utf8'); - - function extractCounters(element) { - const counters = {}; - const re = //g; - let m; - while ((m = re.exec(element)) !== null) { - counters[m[1]] = { missed: parseInt(m[2]), covered: parseInt(m[3]) }; - } - return counters; - } - - function pct(c) { - if (!c) return 'N/A'; - const total = c.missed + c.covered; - return total === 0 ? 'N/A' : (c.covered / total * 100).toFixed(1) + '%'; - } - - // Parse overall counters (last set of tags at report level) - const reportMatch = xml.match(/]*>([\s\S]*)<\/report>/); - const reportBody = reportMatch ? reportMatch[1] : xml; - - // Extract packages - const pkgRegex = /([\s\S]*?)<\/package>/g; - const packages = []; - let pm; - while ((pm = pkgRegex.exec(reportBody)) !== null) { - const name = pm[1].replace(/\//g, '.'); - const counters = extractCounters(pm[2]); - packages.push({ name, counters }); - } - - // Sort by package name - packages.sort((a, b) => a.name.localeCompare(b.name)); - - // Overall counters (direct children of , after all packages) - const overallCounters = extractCounters(reportBody.replace(//g, '')); - - let body = '## JaCoCo Coverage Report\n\n'; - body += '| Package | Line | Branch | Method |\n'; - body += '|:--------|-----:|-------:|-------:|\n'; - - for (const pkg of packages) { - const c = pkg.counters; - body += `| \`${pkg.name}\` | ${pct(c.LINE)} | ${pct(c.BRANCH)} | ${pct(c.METHOD)} |\n`; - } - - body += `| **Overall** | **${pct(overallCounters.LINE)}** | **${pct(overallCounters.BRANCH)}** | **${pct(overallCounters.METHOD)}** |\n`; - body += '\n> Full HTML report available as build artifact `jacoco-html-report`\n'; - - // Find and update or create the comment - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const marker = '## JaCoCo Coverage Report'; - const existing = comments.find(c => c.body && c.body.startsWith(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } + name: coverage-report + path: build/reports/jacoco/test/jacocoTestReport.xml + retention-days: 1 - name: Parse Test Results if: always() && matrix.label != 'check' run: | @@ -173,15 +92,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - name: Download Test Stats uses: actions/download-artifact@v4 with: pattern: test-stats-* merge-multiple: true path: test-stats/ - - name: Generate Test Summary and Update Baseline + - name: Download Coverage Report + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-report + path: coverage/ + - name: Generate Unified Test Report Comment uses: actions/github-script@v7 with: script: | @@ -189,9 +112,10 @@ jobs: const path = require('path'); const versions = ['java11', 'java17', 'java21', 'java25']; - const zero = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 }; + const zeroTest = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 }; + const zeroCov = { covered: 0, missed: 0 }; - // Read current stats from artifacts + // --- Read current test stats from artifacts --- const current = {}; for (const v of versions) { const file = path.join('test-stats', `${v}.json`); @@ -202,16 +126,36 @@ jobs: } } - // Read baseline from repo - const baselineFile = 'test-stats-baseline.json'; - let baseline = {}; + // --- Read baseline from repo (read-only) --- + const baselineFile = 'test-baseline.json'; + let baseline = { tests: {}, coverage: {} }; if (fs.existsSync(baselineFile)) { baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8')); } + const baseTests = baseline.tests || {}; + const baseCov = baseline.coverage || {}; + // --- Parse JaCoCo XML for coverage --- + let covLine = null, covBranch = null, covMethod = null; + const jacocoFile = path.join('coverage', 'jacocoTestReport.xml'); + if (fs.existsSync(jacocoFile)) { + const xml = fs.readFileSync(jacocoFile, 'utf8'); + // Extract top-level counters (direct children of , outside tags) + const stripped = xml.replace(//g, ''); + const re = //g; + let m; + while ((m = re.exec(stripped)) !== null) { + const entry = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + if (m[1] === 'LINE') covLine = entry; + else if (m[1] === 'BRANCH') covBranch = entry; + else if (m[1] === 'METHOD') covMethod = entry; + } + } + + // --- Helpers --- function delta(curr, prev) { const d = curr - prev; - if (d === 0) return '\u00b10'; + if (d === 0) return '±0'; return d > 0 ? `+${d}` : `${d}`; } @@ -219,13 +163,30 @@ jobs: return `${curr} (${delta(curr, prev)})`; } - let body = '## Test Results\n\n'; + function pct(covered, missed) { + const total = covered + missed; + return total === 0 ? 0 : (covered / total * 100); + } + + function fmtPct(value) { + return value.toFixed(1) + '%'; + } + + function fmtPctDelta(curr, prev) { + const d = curr - prev; + if (Math.abs(d) < 0.05) return '±0.0%'; + return d > 0 ? `+${d.toFixed(1)}%` : `${d.toFixed(1)}%`; + } + + // --- Build Test Results table --- + let body = '\n## Test Report\n\n'; + body += '### Test Results\n'; body += '| Java Version | Total | Passed | Failed | Errors | Skipped |\n'; body += '|:-------------|------:|-------:|-------:|-------:|--------:|\n'; for (const v of versions) { - const c = current[v] || zero; - const b = baseline[v] || zero; + const c = current[v] || zeroTest; + const b = baseTests[v] || zeroTest; const label = v.replace('java', 'Java '); if (!current[v]) { body += `| ${label} | - | - | - | - | - |\n`; @@ -235,15 +196,15 @@ jobs: } // Totals row - const totalCurr = { ...zero }; - const totalBase = { ...zero }; + const totalCurr = { ...zeroTest }; + const totalBase = { ...zeroTest }; let hasAny = false; for (const v of versions) { if (current[v]) { hasAny = true; - for (const k of Object.keys(zero)) { + for (const k of Object.keys(zeroTest)) { totalCurr[k] += current[v][k]; - totalBase[k] += (baseline[v] || zero)[k]; + totalBase[k] += (baseTests[v] || zeroTest)[k]; } } } @@ -251,13 +212,40 @@ jobs: body += `| **Total** | **${fmtCell(totalCurr.total, totalBase.total)}** | **${fmtCell(totalCurr.passed, totalBase.passed)}** | **${fmtCell(totalCurr.failed, totalBase.failed)}** | **${fmtCell(totalCurr.errors, totalBase.errors)}** | **${fmtCell(totalCurr.skipped, totalBase.skipped)}** |\n`; } - // Post or update comment + // --- Build Coverage table --- + if (covLine || covBranch || covMethod) { + body += '\n### Code Coverage (Java 25)\n'; + body += '| Metric | Covered | Missed | Coverage | vs Master |\n'; + body += '|:---------|--------:|-------:|---------:|----------:|\n'; + + const metrics = [ + { name: 'Lines', curr: covLine, baseKey: 'line' }, + { name: 'Branches', curr: covBranch, baseKey: 'branch' }, + { name: 'Methods', curr: covMethod, baseKey: 'method' }, + ]; + + for (const { name, curr, baseKey } of metrics) { + if (!curr) continue; + const b = baseCov[baseKey] || zeroCov; + const currPct = pct(curr.covered, curr.missed); + const basePct = pct(b.covered, b.missed); + body += `| ${name} | ${curr.covered} | ${curr.missed} | ${fmtPct(currPct)} | ${fmtPctDelta(currPct, basePct)} |\n`; + } + + body += '\n> Full HTML report: build artifact `jacoco-html-report`\n'; + } + + // Timestamp + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); + body += `\n> *Updated: ${now}*\n`; + + // --- Post or update single comment --- const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); - const marker = '## Test Results'; + const marker = ''; const existing = comments.find(c => c.body && c.body.startsWith(marker)); if (existing) { await github.rest.issues.updateComment({ @@ -274,22 +262,6 @@ jobs: body, }); } - - // Write updated baseline for commit - const updated = {}; - for (const v of versions) { - updated[v] = current[v] || baseline[v] || zero; - } - fs.writeFileSync(baselineFile, JSON.stringify(updated, null, 2) + '\n'); - - name: Commit Updated Baseline - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add test-stats-baseline.json - git diff --cached --quiet || { - git commit -m "Update test stats baseline [skip ci]" - git push - } javadoc: runs-on: ubuntu-latest steps: diff --git a/test-baseline.json b/test-baseline.json new file mode 100644 index 000000000..105cfd575 --- /dev/null +++ b/test-baseline.json @@ -0,0 +1,13 @@ +{ + "tests": { + "java11": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java17": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java21": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, + "java25": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 } + }, + "coverage": { + "line": { "covered": 0, "missed": 0 }, + "branch": { "covered": 0, "missed": 0 }, + "method": { "covered": 0, "missed": 0 } + } +} diff --git a/test-stats-baseline.json b/test-stats-baseline.json deleted file mode 100644 index 7dce0836c..000000000 --- a/test-stats-baseline.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "java11": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, - "java17": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, - "java21": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 }, - "java25": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 } -} From c4660c7ae4bd9434c2ae75d2d5a2358d9bc678f7 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 09:00:55 +1000 Subject: [PATCH 06/15] Add per-class coverage deltas to PR comment Parse per-class counters from JaCoCo XML and compare against master baseline. The PR comment now shows a collapsible table of classes where line, branch, or method coverage changed. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/master.yml | 34 ++++++++++-- .github/workflows/pull_request.yml | 88 +++++++++++++++++++++++++++++- test-baseline.json | 9 ++- 3 files changed, 123 insertions(+), 8 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8705da2c4..a4ea57b74 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -123,17 +123,43 @@ jobs: } // Update coverage from JaCoCo XML - let coverage = baseline.coverage || {}; + let coverage = { overall: {}, classes: {} }; const jacocoFile = path.join('coverage', 'jacocoTestReport.xml'); if (fs.existsSync(jacocoFile)) { const xml = fs.readFileSync(jacocoFile, 'utf8'); + + // Overall counters (outside tags) const stripped = xml.replace(//g, ''); const re = //g; let m; while ((m = re.exec(stripped)) !== null) { - if (m[1] === 'LINE') coverage.line = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; - else if (m[1] === 'BRANCH') coverage.branch = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; - else if (m[1] === 'METHOD') coverage.method = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + if (m[1] === 'LINE') coverage.overall.line = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + else if (m[1] === 'BRANCH') coverage.overall.branch = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + else if (m[1] === 'METHOD') coverage.overall.method = { covered: parseInt(m[3]), missed: parseInt(m[2]) }; + } + + // Per-class counters from / elements + const pkgRe = /([\s\S]*?)<\/package>/g; + let pkgMatch; + while ((pkgMatch = pkgRe.exec(xml)) !== null) { + const pkgName = pkgMatch[1].replace(/\//g, '.'); + const pkgBody = pkgMatch[2]; + const classRe = /]*>([\s\S]*?)<\/class>/g; + let classMatch; + while ((classMatch = classRe.exec(pkgBody)) !== null) { + const className = classMatch[1].replace(/\//g, '.'); + const classBody = classMatch[2]; + const counters = { line: { ...zeroCov }, branch: { ...zeroCov }, method: { ...zeroCov } }; + const cntRe = //g; + let cntMatch; + while ((cntMatch = cntRe.exec(classBody)) !== null) { + const entry = { covered: parseInt(cntMatch[3]), missed: parseInt(cntMatch[2]) }; + if (cntMatch[1] === 'LINE') counters.line = entry; + else if (cntMatch[1] === 'BRANCH') counters.branch = entry; + else if (cntMatch[1] === 'METHOD') counters.method = entry; + } + coverage.classes[className] = counters; + } } } diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 183aa9cd6..abf680302 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -133,10 +133,12 @@ jobs: baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8')); } const baseTests = baseline.tests || {}; - const baseCov = baseline.coverage || {}; + const baseCov = (baseline.coverage || {}).overall || {}; + const baseClasses = (baseline.coverage || {}).classes || {}; // --- Parse JaCoCo XML for coverage --- let covLine = null, covBranch = null, covMethod = null; + const classCounters = {}; const jacocoFile = path.join('coverage', 'jacocoTestReport.xml'); if (fs.existsSync(jacocoFile)) { const xml = fs.readFileSync(jacocoFile, 'utf8'); @@ -150,6 +152,30 @@ jobs: else if (m[1] === 'BRANCH') covBranch = entry; else if (m[1] === 'METHOD') covMethod = entry; } + + // Extract per-class counters from / elements + const pkgRe = /([\s\S]*?)<\/package>/g; + let pkgMatch; + while ((pkgMatch = pkgRe.exec(xml)) !== null) { + const pkgName = pkgMatch[1].replace(/\//g, '.'); + const pkgBody = pkgMatch[2]; + const classRe = /]*>([\s\S]*?)<\/class>/g; + let classMatch; + while ((classMatch = classRe.exec(pkgBody)) !== null) { + const className = classMatch[1].replace(/\//g, '.'); + const classBody = classMatch[2]; + const counters = { line: { ...zeroCov }, branch: { ...zeroCov }, method: { ...zeroCov } }; + const cntRe = //g; + let cntMatch; + while ((cntMatch = cntRe.exec(classBody)) !== null) { + const entry = { covered: parseInt(cntMatch[3]), missed: parseInt(cntMatch[2]) }; + if (cntMatch[1] === 'LINE') counters.line = entry; + else if (cntMatch[1] === 'BRANCH') counters.branch = entry; + else if (cntMatch[1] === 'METHOD') counters.method = entry; + } + classCounters[className] = counters; + } + } } // --- Helpers --- @@ -232,6 +258,66 @@ jobs: body += `| ${name} | ${curr.covered} | ${curr.missed} | ${fmtPct(currPct)} | ${fmtPctDelta(currPct, basePct)} |\n`; } + body += '\n'; + + // --- Per-class coverage deltas --- + const changedClasses = []; + for (const [cls, curr] of Object.entries(classCounters)) { + const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov }; + const currLinePct = pct(curr.line.covered, curr.line.missed); + const baseLinePct = pct(base.line.covered, base.line.missed); + const currBranchPct = pct(curr.branch.covered, curr.branch.missed); + const baseBranchPct = pct(base.branch.covered, base.branch.missed); + const currMethodPct = pct(curr.method.covered, curr.method.missed); + const baseMethodPct = pct(base.method.covered, base.method.missed); + if (Math.abs(currLinePct - baseLinePct) >= 0.05 || + Math.abs(currBranchPct - baseBranchPct) >= 0.05 || + Math.abs(currMethodPct - baseMethodPct) >= 0.05) { + changedClasses.push({ + name: cls, + linePct: currLinePct, lineDelta: currLinePct - baseLinePct, + branchPct: currBranchPct, branchDelta: currBranchPct - baseBranchPct, + methodPct: currMethodPct, methodDelta: currMethodPct - baseMethodPct, + }); + } + } + // Also detect classes removed (in baseline but not in current) + for (const cls of Object.keys(baseClasses)) { + if (!classCounters[cls]) { + changedClasses.push({ + name: cls, + linePct: 0, lineDelta: -pct(baseClasses[cls].line.covered, baseClasses[cls].line.missed), + branchPct: 0, branchDelta: -pct(baseClasses[cls].branch.covered, baseClasses[cls].branch.missed), + methodPct: 0, methodDelta: -pct(baseClasses[cls].method.covered, baseClasses[cls].method.missed), + removed: true, + }); + } + } + + changedClasses.sort((a, b) => a.name.localeCompare(b.name)); + + function fmtClassPct(val, delta) { + const pctStr = fmtPct(val); + const deltaStr = fmtPctDelta(val, val - delta); + return `${pctStr} (${deltaStr})`; + } + + if (changedClasses.length > 0) { + body += `
Changed Class Coverage (${changedClasses.length} ${changedClasses.length === 1 ? 'class' : 'classes'})\n\n`; + body += '| Class | Line | Branch | Method |\n'; + body += '|:------|-----:|-------:|-------:|\n'; + for (const c of changedClasses) { + if (c.removed) { + body += `| \`${c.name}\` | *removed* | *removed* | *removed* |\n`; + } else { + body += `| \`${c.name}\` | ${fmtClassPct(c.linePct, c.lineDelta)} | ${fmtClassPct(c.branchPct, c.branchDelta)} | ${fmtClassPct(c.methodPct, c.methodDelta)} |\n`; + } + } + body += '\n
\n'; + } else { + body += '> No per-class coverage changes detected.\n'; + } + body += '\n> Full HTML report: build artifact `jacoco-html-report`\n'; } diff --git a/test-baseline.json b/test-baseline.json index 105cfd575..862179690 100644 --- a/test-baseline.json +++ b/test-baseline.json @@ -6,8 +6,11 @@ "java25": { "total": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0 } }, "coverage": { - "line": { "covered": 0, "missed": 0 }, - "branch": { "covered": 0, "missed": 0 }, - "method": { "covered": 0, "missed": 0 } + "overall": { + "line": { "covered": 0, "missed": 0 }, + "branch": { "covered": 0, "missed": 0 }, + "method": { "covered": 0, "missed": 0 } + }, + "classes": {} } } From 75d0a95ab36dab4fdc4f821430a619f2a1883b31 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 10:43:59 +1000 Subject: [PATCH 07/15] Add green/red color indicators to test and coverage deltas Test results: green for more total/passed, red for more failed/errors. Coverage deltas: green for increase, red for decrease. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index abf680302..e99256cda 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -179,14 +179,17 @@ jobs: } // --- Helpers --- - function delta(curr, prev) { + function delta(curr, prev, positiveIsGood) { const d = curr - prev; if (d === 0) return '±0'; - return d > 0 ? `+${d}` : `${d}`; + const str = d > 0 ? `+${d}` : `${d}`; + if (positiveIsGood === undefined) return str; + const icon = (positiveIsGood ? d > 0 : d < 0) ? ' 🟢' : ' 🔴'; + return str + icon; } - function fmtCell(curr, prev) { - return `${curr} (${delta(curr, prev)})`; + function fmtCell(curr, prev, positiveIsGood) { + return `${curr} (${delta(curr, prev, positiveIsGood)})`; } function pct(covered, missed) { @@ -201,7 +204,9 @@ jobs: function fmtPctDelta(curr, prev) { const d = curr - prev; if (Math.abs(d) < 0.05) return '±0.0%'; - return d > 0 ? `+${d.toFixed(1)}%` : `${d.toFixed(1)}%`; + const str = d > 0 ? `+${d.toFixed(1)}%` : `${d.toFixed(1)}%`; + const icon = d > 0 ? ' 🟢' : ' 🔴'; + return str + icon; } // --- Build Test Results table --- @@ -217,7 +222,7 @@ jobs: if (!current[v]) { body += `| ${label} | - | - | - | - | - |\n`; } else { - body += `| ${label} | ${fmtCell(c.total, b.total)} | ${fmtCell(c.passed, b.passed)} | ${fmtCell(c.failed, b.failed)} | ${fmtCell(c.errors, b.errors)} | ${fmtCell(c.skipped, b.skipped)} |\n`; + body += `| ${label} | ${fmtCell(c.total, b.total, true)} | ${fmtCell(c.passed, b.passed, true)} | ${fmtCell(c.failed, b.failed, false)} | ${fmtCell(c.errors, b.errors, false)} | ${fmtCell(c.skipped, b.skipped)} |\n`; } } @@ -235,7 +240,7 @@ jobs: } } if (hasAny) { - body += `| **Total** | **${fmtCell(totalCurr.total, totalBase.total)}** | **${fmtCell(totalCurr.passed, totalBase.passed)}** | **${fmtCell(totalCurr.failed, totalBase.failed)}** | **${fmtCell(totalCurr.errors, totalBase.errors)}** | **${fmtCell(totalCurr.skipped, totalBase.skipped)}** |\n`; + body += `| **Total** | **${fmtCell(totalCurr.total, totalBase.total, true)}** | **${fmtCell(totalCurr.passed, totalBase.passed, true)}** | **${fmtCell(totalCurr.failed, totalBase.failed, false)}** | **${fmtCell(totalCurr.errors, totalBase.errors, false)}** | **${fmtCell(totalCurr.skipped, totalBase.skipped)}** |\n`; } // --- Build Coverage table --- From 5a5783c57577dd391950f271d14d983519082a11 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 11:07:12 +1000 Subject: [PATCH 08/15] Show changed class coverage table without collapsing Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e99256cda..a09e6673a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -308,7 +308,7 @@ jobs: } if (changedClasses.length > 0) { - body += `
Changed Class Coverage (${changedClasses.length} ${changedClasses.length === 1 ? 'class' : 'classes'})\n\n`; + body += `**Changed Class Coverage** (${changedClasses.length} ${changedClasses.length === 1 ? 'class' : 'classes'})\n\n`; body += '| Class | Line | Branch | Method |\n'; body += '|:------|-----:|-------:|-------:|\n'; for (const c of changedClasses) { @@ -318,7 +318,7 @@ jobs: body += `| \`${c.name}\` | ${fmtClassPct(c.linePct, c.lineDelta)} | ${fmtClassPct(c.branchPct, c.branchDelta)} | ${fmtClassPct(c.methodPct, c.methodDelta)} |\n`; } } - body += '\n
\n'; + body += '\n'; } else { body += '> No per-class coverage changes detected.\n'; } From 7bf3735b36efc889e03e8e1916c39ebc438322b4 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 14:03:47 +1000 Subject: [PATCH 09/15] Shorten class names in coverage table to abbreviated packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e.g. graphql.execution.instrumentation.Foo$Bar → g.e.i.Foo$Bar Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a09e6673a..0d3e34c1e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -301,6 +301,15 @@ jobs: changedClasses.sort((a, b) => a.name.localeCompare(b.name)); + function shortenClass(name) { + const dollarIdx = name.indexOf('$'); + const mainPart = dollarIdx >= 0 ? name.substring(0, dollarIdx) : name; + const suffix = dollarIdx >= 0 ? name.substring(dollarIdx) : ''; + const parts = mainPart.split('.'); + const shortened = parts.map((p, i) => i < parts.length - 1 ? p[0] : p); + return shortened.join('.') + suffix; + } + function fmtClassPct(val, delta) { const pctStr = fmtPct(val); const deltaStr = fmtPctDelta(val, val - delta); @@ -313,9 +322,9 @@ jobs: body += '|:------|-----:|-------:|-------:|\n'; for (const c of changedClasses) { if (c.removed) { - body += `| \`${c.name}\` | *removed* | *removed* | *removed* |\n`; + body += `| \`${shortenClass(c.name)}\` | *removed* | *removed* | *removed* |\n`; } else { - body += `| \`${c.name}\` | ${fmtClassPct(c.linePct, c.lineDelta)} | ${fmtClassPct(c.branchPct, c.branchDelta)} | ${fmtClassPct(c.methodPct, c.methodDelta)} |\n`; + body += `| \`${shortenClass(c.name)}\` | ${fmtClassPct(c.linePct, c.lineDelta)} | ${fmtClassPct(c.branchPct, c.branchDelta)} | ${fmtClassPct(c.methodPct, c.methodDelta)} |\n`; } } body += '\n'; From cc22c5e61c4a262edf2760408e2ef56867522935 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 14:34:10 +1000 Subject: [PATCH 10/15] Show only deltas in per-class coverage table for compact rows Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0d3e34c1e..11afce0b5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -310,10 +310,8 @@ jobs: return shortened.join('.') + suffix; } - function fmtClassPct(val, delta) { - const pctStr = fmtPct(val); - const deltaStr = fmtPctDelta(val, val - delta); - return `${pctStr} (${deltaStr})`; + function fmtClassDelta(delta) { + return fmtPctDelta(delta, 0); } if (changedClasses.length > 0) { @@ -324,7 +322,7 @@ jobs: if (c.removed) { body += `| \`${shortenClass(c.name)}\` | *removed* | *removed* | *removed* |\n`; } else { - body += `| \`${shortenClass(c.name)}\` | ${fmtClassPct(c.linePct, c.lineDelta)} | ${fmtClassPct(c.branchPct, c.branchDelta)} | ${fmtClassPct(c.methodPct, c.methodDelta)} |\n`; + body += `| \`${shortenClass(c.name)}\` | ${fmtClassDelta(c.lineDelta)} | ${fmtClassDelta(c.branchDelta)} | ${fmtClassDelta(c.methodDelta)} |\n`; } } body += '\n'; From b4e086cbd8d725f0750ee430ab01b1449b9e650f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 19:11:30 +1000 Subject: [PATCH 11/15] Make per-class coverage table more compact Strip $InnerClass suffixes from class names and remove emoji from per-class delta cells. The +/- sign already conveys direction. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 11afce0b5..5f89f2ab2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -302,16 +302,15 @@ jobs: changedClasses.sort((a, b) => a.name.localeCompare(b.name)); function shortenClass(name) { - const dollarIdx = name.indexOf('$'); - const mainPart = dollarIdx >= 0 ? name.substring(0, dollarIdx) : name; - const suffix = dollarIdx >= 0 ? name.substring(dollarIdx) : ''; + const mainPart = name.replace(/\$.*$/, ''); const parts = mainPart.split('.'); const shortened = parts.map((p, i) => i < parts.length - 1 ? p[0] : p); - return shortened.join('.') + suffix; + return shortened.join('.'); } function fmtClassDelta(delta) { - return fmtPctDelta(delta, 0); + if (Math.abs(delta) < 0.05) return '±0.0%'; + return delta > 0 ? `+${delta.toFixed(1)}%` : `${delta.toFixed(1)}%`; } if (changedClasses.length > 0) { From 96ff2f6783335647e27ff7ac5a58854faafa4614 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 19:31:30 +1000 Subject: [PATCH 12/15] Restore emoji in per-class deltas, break long class names with
Inner class names are kept but wrapped onto a second line at the $ boundary using
to keep the table narrow. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5f89f2ab2..86a498b25 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -302,15 +302,17 @@ jobs: changedClasses.sort((a, b) => a.name.localeCompare(b.name)); function shortenClass(name) { - const mainPart = name.replace(/\$.*$/, ''); + const dollarIdx = name.indexOf('$'); + const mainPart = dollarIdx >= 0 ? name.substring(0, dollarIdx) : name; + const suffix = dollarIdx >= 0 ? name.substring(dollarIdx) : ''; const parts = mainPart.split('.'); const shortened = parts.map((p, i) => i < parts.length - 1 ? p[0] : p); - return shortened.join('.'); + const result = shortened.join('.') + suffix; + return result.replace(/\$/g, '
\\$'); } function fmtClassDelta(delta) { - if (Math.abs(delta) < 0.05) return '±0.0%'; - return delta > 0 ? `+${delta.toFixed(1)}%` : `${delta.toFixed(1)}%`; + return fmtPctDelta(delta, 0); } if (changedClasses.length > 0) { @@ -319,9 +321,9 @@ jobs: body += '|:------|-----:|-------:|-------:|\n'; for (const c of changedClasses) { if (c.removed) { - body += `| \`${shortenClass(c.name)}\` | *removed* | *removed* | *removed* |\n`; + body += `| ${shortenClass(c.name)} | *removed* | *removed* | *removed* |\n`; } else { - body += `| \`${shortenClass(c.name)}\` | ${fmtClassDelta(c.lineDelta)} | ${fmtClassDelta(c.branchDelta)} | ${fmtClassDelta(c.methodDelta)} |\n`; + body += `| ${shortenClass(c.name)} | ${fmtClassDelta(c.lineDelta)} | ${fmtClassDelta(c.branchDelta)} | ${fmtClassDelta(c.methodDelta)} |\n`; } } body += '\n'; From b2a6a702b427c5d5b373320d1ac975e6aecef9c1 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 19:59:20 +1000 Subject: [PATCH 13/15] Fix TestNG TCK tests to actually run under all Java versions The testng task was silently broken: useJUnitPlatform() in the global tasks.withType(Test) block overrode useTestNG(), and the task lacked testClassesDirs/classpath config. Guard useJUnitPlatform() to skip testng tasks, fix the testng task config, add testngWithJava{11,17,21} tasks, and wire them into both CI workflows with proper result parsing. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/master.yml | 46 ++++++++++++++++-------------- .github/workflows/pull_request.yml | 42 ++++++++++++++------------- build.gradle | 19 +++++++++++- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index a4ea57b74..03a5c2ae7 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -13,20 +13,20 @@ jobs: strategy: matrix: include: - - gradle-argument: 'assemble && ./gradlew check -x test' + - gradle-argument: 'assemble && ./gradlew check -x test -x testng -x testngWithJava11 -x testngWithJava17 -x testngWithJava21' label: 'check' - - gradle-argument: 'testWithJava11' + - gradle-argument: 'testWithJava11 testngWithJava11' label: 'java11' - test-results-dir: 'testWithJava11' - - gradle-argument: 'testWithJava17' + test-results-dirs: 'testWithJava11 testngWithJava11' + - gradle-argument: 'testWithJava17 testngWithJava17' label: 'java17' - test-results-dir: 'testWithJava17' - - gradle-argument: 'testWithJava21' + test-results-dirs: 'testWithJava17 testngWithJava17' + - gradle-argument: 'testWithJava21 testngWithJava21' label: 'java21' - test-results-dir: 'testWithJava21' - - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' + test-results-dirs: 'testWithJava21 testngWithJava21' + - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 testng jacocoTestReport' label: 'java25' - test-results-dir: 'test' + test-results-dirs: 'test testng' steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 @@ -42,7 +42,7 @@ jobs: if: always() && matrix.label != 'check' with: files: | - **/build/test-results/${{ matrix.test-results-dir }}/TEST-*.xml + **/build/test-results/*/TEST-*.xml - name: Upload Coverage XML Report uses: actions/upload-artifact@v4 if: always() && matrix.label == 'java25' @@ -53,18 +53,20 @@ jobs: - name: Parse Test Results if: always() && matrix.label != 'check' run: | - dir="build/test-results/${{ matrix.test-results-dir }}" total=0; failures=0; errors=0; skipped=0 - for f in "$dir"/TEST-*.xml; do - [ -f "$f" ] || continue - t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - total=$((total + ${t:-0})) - failures=$((failures + ${fl:-0})) - errors=$((errors + ${e:-0})) - skipped=$((skipped + ${s:-0})) + for dir_name in ${{ matrix.test-results-dirs }}; do + dir="build/test-results/$dir_name" + for f in "$dir"/TEST-*.xml; do + [ -f "$f" ] || continue + t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + total=$((total + ${t:-0})) + failures=$((failures + ${fl:-0})) + errors=$((errors + ${e:-0})) + skipped=$((skipped + ${s:-0})) + done done passed=$((total - failures - errors - skipped)) mkdir -p /tmp/test-stats @@ -205,4 +207,4 @@ jobs: java-version: '25' distribution: 'corretto' - name: publishToMavenCentral - run: ./gradlew assemble && ./gradlew check -x test -x testng --info && ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -x check --info --stacktrace + run: ./gradlew assemble && ./gradlew check -x test -x testng -x testngWithJava11 -x testngWithJava17 -x testngWithJava21 --info && ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -x check --info --stacktrace diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 86a498b25..d9b86894e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,20 +22,20 @@ jobs: strategy: matrix: include: - - gradle-argument: 'assemble && ./gradlew check -x test' + - gradle-argument: 'assemble && ./gradlew check -x test -x testng -x testngWithJava11 -x testngWithJava17 -x testngWithJava21' label: 'check' - - gradle-argument: 'testWithJava11' + - gradle-argument: 'testWithJava11 testngWithJava11' label: 'java11' - test-results-dir: 'testWithJava11' - - gradle-argument: 'testWithJava17' + test-results-dirs: 'testWithJava11 testngWithJava11' + - gradle-argument: 'testWithJava17 testngWithJava17' label: 'java17' - test-results-dir: 'testWithJava17' - - gradle-argument: 'testWithJava21' + test-results-dirs: 'testWithJava17 testngWithJava17' + - gradle-argument: 'testWithJava21 testngWithJava21' label: 'java21' - test-results-dir: 'testWithJava21' - - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 jacocoTestReport' + test-results-dirs: 'testWithJava21 testngWithJava21' + - gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 testng jacocoTestReport' label: 'java25' - test-results-dir: 'test' + test-results-dirs: 'test testng' steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 @@ -63,18 +63,20 @@ jobs: - name: Parse Test Results if: always() && matrix.label != 'check' run: | - dir="build/test-results/${{ matrix.test-results-dir }}" total=0; failures=0; errors=0; skipped=0 - for f in "$dir"/TEST-*.xml; do - [ -f "$f" ] || continue - t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') - total=$((total + ${t:-0})) - failures=$((failures + ${fl:-0})) - errors=$((errors + ${e:-0})) - skipped=$((skipped + ${s:-0})) + for dir_name in ${{ matrix.test-results-dirs }}; do + dir="build/test-results/$dir_name" + for f in "$dir"/TEST-*.xml; do + [ -f "$f" ] || continue + t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*') + total=$((total + ${t:-0})) + failures=$((failures + ${fl:-0})) + errors=$((errors + ${e:-0})) + skipped=$((skipped + ${s:-0})) + done done passed=$((total - failures - errors - skipped)) mkdir -p /tmp/test-stats diff --git a/build.gradle b/build.gradle index f0d8a77e4..20eb9578d 100644 --- a/build.gradle +++ b/build.gradle @@ -296,6 +296,9 @@ shadowJar.finalizedBy extractWithoutGuava, buildNewJar task testng(type: Test) { useTestNG() + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + dependsOn tasks.named('testClasses') } check.dependsOn testng @@ -372,7 +375,9 @@ int testCount = 0 long testTime = 0L tasks.withType(Test) { - useJUnitPlatform() + if (!name.startsWith('testng')) { + useJUnitPlatform() + } maxHeapSize = "1g" testLogging { events "FAILED", "SKIPPED" @@ -439,6 +444,18 @@ tasks.register('testWithJava11', Test) { } +['11', '17', '21'].each { ver -> + tasks.register("testngWithJava${ver}", Test) { + useTestNG() + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(ver) + } + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + dependsOn tasks.named('testClasses') + } +} + test.dependsOn testWithJava21 test.dependsOn testWithJava17 test.dependsOn testWithJava11 From 997ce34d35c302be9ff7a9a4ad083df497dee463 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 20:05:51 +1000 Subject: [PATCH 14/15] Fail PR if code coverage drops vs master baseline Add a coverage gate in the test-summary job that checks line, branch, and method coverage against the master baseline. The PR comment is posted first so developers always see the report, then core.setFailed() is called if any metric decreased. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d9b86894e..7e25093ab 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -363,6 +363,27 @@ jobs: body, }); } + + // --- Coverage gate: fail if any metric drops --- + if (covLine || covBranch || covMethod) { + const drops = []; + for (const { name, curr, baseKey } of [ + { name: 'Line', curr: covLine, baseKey: 'line' }, + { name: 'Branch', curr: covBranch, baseKey: 'branch' }, + { name: 'Method', curr: covMethod, baseKey: 'method' }, + ]) { + if (!curr) continue; + const b = baseCov[baseKey] || zeroCov; + const currPct = pct(curr.covered, curr.missed); + const basePct = pct(b.covered, b.missed); + if (currPct < basePct - 0.05) { + drops.push(`${name}: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%)`); + } + } + if (drops.length > 0) { + core.setFailed(`Coverage decreased:\n${drops.join('\n')}`); + } + } javadoc: runs-on: ubuntu-latest steps: From e87b0d30a16dddf4acb55631038c124178112731 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 5 Mar 2026 20:27:44 +1000 Subject: [PATCH 15/15] Stabilize TCK tests by using dedicated executors The TCK uses thread-locals to track recursion. When supplyAsync runs on the shared ForkJoinPool, tasks can complete on the same thread causing false spec303 failures. Use Executors.newSingleThreadExecutor() for all remaining TCK test classes, matching the earlier fix in dcb9b055e. Co-Authored-By: Claude Opus 4.6 --- ...ppingOrderedPublisherRandomCompleteTckVerificationTest.java | 3 ++- ...StageMappingPublisherRandomCompleteTckVerificationTest.java | 3 ++- .../CompletionStageMappingPublisherTckVerificationTest.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingOrderedPublisherRandomCompleteTckVerificationTest.java b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingOrderedPublisherRandomCompleteTckVerificationTest.java index 19ebffc46..8a8b84fed 100644 --- a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingOrderedPublisherRandomCompleteTckVerificationTest.java +++ b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingOrderedPublisherRandomCompleteTckVerificationTest.java @@ -12,6 +12,7 @@ import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executors; import java.util.function.Function; /** @@ -57,7 +58,7 @@ private static Function> mapperFunc() { throw new RuntimeException(e); } return i + "!"; - }); + }, Executors.newSingleThreadExecutor()); } static Random rn = new Random(); diff --git a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherRandomCompleteTckVerificationTest.java b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherRandomCompleteTckVerificationTest.java index 889b18eee..18ea18370 100644 --- a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherRandomCompleteTckVerificationTest.java +++ b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherRandomCompleteTckVerificationTest.java @@ -12,6 +12,7 @@ import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executors; import java.util.function.Function; /** @@ -57,7 +58,7 @@ private static Function> mapperFunc() { throw new RuntimeException(e); } return i + "!"; - }); + }, Executors.newSingleThreadExecutor()); } static Random rn = new Random(); diff --git a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherTckVerificationTest.java b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherTckVerificationTest.java index f68c7d3fa..9b8402560 100644 --- a/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherTckVerificationTest.java +++ b/src/test/groovy/graphql/execution/reactive/tck/CompletionStageMappingPublisherTckVerificationTest.java @@ -39,7 +39,7 @@ public Publisher createPublisher(long elements) { @Override public Publisher createFailedPublisher() { Publisher publisher = Flowable.error(() -> new RuntimeException("Bang")); - Function> mapper = i -> CompletableFuture.supplyAsync(() -> i + "!"); + Function> mapper = i -> CompletableFuture.supplyAsync(() -> i + "!", Executors.newSingleThreadExecutor()); return new CompletionStageMappingPublisher<>(publisher, mapper); }