3636 - gradle-argument : ' test -x testWithJava11 -x testWithJava17 -x testWithJava21 testng jacocoTestReport'
3737 label : ' java25'
3838 test-results-dirs : ' test testng'
39+ - gradle-argument : ' jcstress'
40+ label : ' jcstress'
3941 steps :
4042 - uses : actions/checkout@v6
4143 - uses : gradle/actions/wrapper-validation@v5
4547 java-version : ' 25'
4648 distribution : ' corretto'
4749 - name : build and test
48- run : ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
50+ run : |
51+ if [ "${{ matrix.label }}" = "jcstress" ]; then
52+ set -o pipefail
53+ mkdir -p build
54+ ./gradlew ${{matrix.gradle-argument}} --info --stacktrace 2>&1 | tee build/jcstress-output.txt
55+ else
56+ ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
57+ fi
4958 - name : Upload Coverage HTML Report
5059 uses : actions/upload-artifact@v4
5160 if : always() && matrix.label == 'java25'
6170 path : build/reports/jacoco/test/jacocoTestReport.xml
6271 retention-days : 1
6372 - name : Parse Test Results
64- if : always() && matrix.label != 'check'
73+ if : always() && matrix.label != 'check' && matrix.label != 'jcstress'
6574 run : |
6675 total=0; failures=0; errors=0; skipped=0
6776 for dir_name in ${{ matrix.test-results-dirs }}; do
@@ -82,13 +91,32 @@ jobs:
8291 mkdir -p /tmp/test-stats
8392 echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \
8493 > "/tmp/test-stats/${{ matrix.label }}.json"
94+ - name : Parse jcstress Results
95+ if : always() && matrix.label == 'jcstress'
96+ run : |
97+ total=0; passed=0; failed=0; errors=0; skipped=0
98+ if [ -f build/jcstress-output.txt ]; then
99+ line=$(grep 'Results:.*planned.*passed.*failed' build/jcstress-output.txt | tail -1)
100+ if [ -n "$line" ]; then
101+ total=$(echo "$line" | sed 's/.*Results: \([0-9]*\) planned.*/\1/')
102+ passed=$(echo "$line" | sed 's/.*; \([0-9]*\) passed.*/\1/')
103+ failed=$(echo "$line" | sed 's/.*passed, \([0-9]*\) failed.*/\1/')
104+ soft=$(echo "$line" | sed 's/.*failed, \([0-9]*\) soft.*/\1/')
105+ hard=$(echo "$line" | sed 's/.*soft errs, \([0-9]*\) hard.*/\1/')
106+ errors=$((soft + hard))
107+ fi
108+ fi
109+ mkdir -p /tmp/test-stats
110+ echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failed,\"errors\":$errors,\"skipped\":$skipped}" \
111+ > "/tmp/test-stats/${{ matrix.label }}.json"
85112 - name : Upload Test Stats
86113 if : always() && matrix.label != 'check'
87114 uses : actions/upload-artifact@v4
88115 with :
89116 name : test-stats-${{ matrix.label }}
90117 path : /tmp/test-stats/${{ matrix.label }}.json
91118 test-summary :
119+ name : " Test Report & Per-Class Coverage Gate"
92120 needs : buildAndTest
93121 if : always() && github.event_name == 'pull_request'
94122 runs-on : ubuntu-latest
@@ -106,14 +134,14 @@ jobs:
106134 with :
107135 name : coverage-report
108136 path : coverage/
109- - name : Generate Unified Test Report Comment
137+ - name : Post Test Report and Enforce Per-Class Coverage Gate
110138 uses : actions/github-script@v7
111139 with :
112140 script : |
113141 const fs = require('fs');
114142 const path = require('path');
115143
116- const versions = ['java11', 'java17', 'java21', 'java25'];
144+ const versions = ['java11', 'java17', 'java21', 'java25', 'jcstress' ];
117145 const zeroTest = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 };
118146 const zeroCov = { covered: 0, missed: 0 };
119147
@@ -270,6 +298,9 @@ jobs:
270298 // --- Per-class coverage deltas ---
271299 const changedClasses = [];
272300 for (const [cls, curr] of Object.entries(classCounters)) {
301+ // Skip classes with 0 total lines (interfaces, annotations, abstract classes)
302+ const totalLines = curr.line.covered + curr.line.missed;
303+ if (totalLines === 0) continue;
273304 const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov };
274305 const currLinePct = pct(curr.line.covered, curr.line.missed);
275306 const baseLinePct = pct(base.line.covered, base.line.missed);
@@ -290,6 +321,9 @@ jobs:
290321 }
291322 // Also detect classes removed (in baseline but not in current)
292323 for (const cls of Object.keys(baseClasses)) {
324+ // Skip baseline classes with 0 total lines (interfaces, annotations)
325+ const baseTotalLines = baseClasses[cls].line.covered + baseClasses[cls].line.missed;
326+ if (baseTotalLines === 0) continue;
293327 if (!classCounters[cls]) {
294328 changedClasses.push({
295329 name: cls,
@@ -364,25 +398,23 @@ jobs:
364398 });
365399 }
366400
367- // --- Coverage gate: fail if any metric drops ---
368- if (covLine || covBranch || covMethod) {
369- const drops = [];
370- for (const { name, curr, baseKey } of [
371- { name: 'Line', curr: covLine, baseKey: 'line' },
372- { name: 'Branch', curr: covBranch, baseKey: 'branch' },
373- { name: 'Method', curr: covMethod, baseKey: 'method' },
374- ]) {
375- if (!curr) continue;
376- const b = baseCov[baseKey] || zeroCov;
377- const currPct = pct(curr.covered, curr.missed);
378- const basePct = pct(b.covered, b.missed);
401+ // --- Coverage gate: fail if any class regresses on any metric ---
402+ const regressions = [];
403+ for (const [cls, curr] of Object.entries(classCounters)) {
404+ // Skip classes with 0 total lines (interfaces, annotations, abstract classes)
405+ const totalLines = curr.line.covered + curr.line.missed;
406+ if (totalLines === 0) continue;
407+ const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov };
408+ for (const [metric, key] of [['Line', 'line'], ['Branch', 'branch'], ['Method', 'method']]) {
409+ const currPct = pct(curr[key].covered, curr[key].missed);
410+ const basePct = pct(base[key].covered, base[key].missed);
379411 if (currPct < basePct - 0.05) {
380- drops .push(`${name }: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%)`);
412+ regressions .push(` ${cls} ${metric }: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%)`);
381413 }
382414 }
383- if (drops.length > 0) {
384- core.setFailed(`Coverage decreased:\n${drops.join('\n')}`);
385- }
415+ }
416+ if (regressions.length > 0) {
417+ core.setFailed(`Per-class coverage regressions detected:\n${regressions.join('\n')}\n\nUpdate test-baseline.json if these changes are intentional.`);
386418 }
387419 javadoc :
388420 runs-on : ubuntu-latest
@@ -411,7 +443,7 @@ jobs:
411443 exit 1
412444 fi
413445 if [ "${{ needs.test-summary.result }}" != "success" ] && [ "${{ needs.test-summary.result }}" != "skipped" ]; then
414- echo "test-summary failed with result: ${{ needs.test-summary.result }}"
446+ echo "Test Report & Per-Class Coverage Gate failed with result: ${{ needs.test-summary.result }}"
415447 exit 1
416448 fi
417449 if [ "${{ needs.javadoc.result }}" != "success" ]; then
0 commit comments