Skip to content

Commit 8d6eaba

Browse files
committed
Merge remote-tracking branch 'origin/26.x' into codex/backport-4390-to-26.x
# Conflicts: # build.gradle
2 parents 1060a22 + a82f21b commit 8d6eaba

29 files changed

Lines changed: 1175 additions & 186565 deletions

‎.github/workflows/pr-report.yml‎

Lines changed: 0 additions & 385 deletions
This file was deleted.

‎.github/workflows/pull_request.yml‎

Lines changed: 7 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
name: Pull Request Build
1+
name: 26.x Pull Request Build
22
# For pull requests: builds and test
3+
#
4+
# This name intentionally differs from "Pull Request Build". The default-branch
5+
# PR Test Report workflow listens for that name and compares results against the
6+
# master baseline, which is not maintained for 26.x.
37
on:
48
push:
59
branches:
@@ -25,16 +29,12 @@ jobs:
2529
label: 'check'
2630
- gradle-argument: 'testWithJava11 testngWithJava11'
2731
label: 'java11'
28-
test-results-dirs: 'testWithJava11 testngWithJava11'
2932
- gradle-argument: 'testWithJava17 testngWithJava17'
3033
label: 'java17'
31-
test-results-dirs: 'testWithJava17 testngWithJava17'
3234
- gradle-argument: 'testWithJava21 testngWithJava21'
3335
label: 'java21'
34-
test-results-dirs: 'testWithJava21 testngWithJava21'
35-
- gradle-argument: 'test testng jacocoTestReport'
36+
- gradle-argument: 'test testng'
3637
label: 'java25'
37-
test-results-dirs: 'test testng'
3838
- gradle-argument: 'jcstress'
3939
label: 'jcstress'
4040
steps:
@@ -46,177 +46,7 @@ jobs:
4646
java-version: '25'
4747
distribution: 'corretto'
4848
- name: build and test
49-
run: |
50-
if [ "${{ matrix.label }}" = "jcstress" ]; then
51-
set -o pipefail
52-
mkdir -p build
53-
./gradlew ${{matrix.gradle-argument}} --info --stacktrace 2>&1 | tee build/jcstress-output.txt
54-
else
55-
./gradlew ${{matrix.gradle-argument}} --info --stacktrace
56-
fi
57-
- name: Upload Coverage HTML Report
58-
uses: actions/upload-artifact@v7
59-
if: always() && matrix.label == 'java25'
60-
with:
61-
name: jacoco-html-report
62-
path: build/reports/jacoco/test/html/
63-
retention-days: 14
64-
- name: Upload Coverage XML Report
65-
uses: actions/upload-artifact@v7
66-
if: always() && matrix.label == 'java25'
67-
with:
68-
name: coverage-report
69-
path: build/reports/jacoco/test/jacocoTestReport.xml
70-
retention-days: 1
71-
- name: Parse Test Results
72-
if: always() && matrix.label != 'check' && matrix.label != 'jcstress'
73-
run: |
74-
total=0; failures=0; errors=0; skipped=0
75-
for dir_name in ${{ matrix.test-results-dirs }}; do
76-
dir="build/test-results/$dir_name"
77-
for f in "$dir"/TEST-*.xml; do
78-
[ -f "$f" ] || continue
79-
t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
80-
fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
81-
e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
82-
s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
83-
total=$((total + ${t:-0}))
84-
failures=$((failures + ${fl:-0}))
85-
errors=$((errors + ${e:-0}))
86-
skipped=$((skipped + ${s:-0}))
87-
done
88-
done
89-
passed=$((total - failures - errors - skipped))
90-
mkdir -p /tmp/test-stats
91-
echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \
92-
> "/tmp/test-stats/${{ matrix.label }}.json"
93-
- name: Parse jcstress Results
94-
if: always() && matrix.label == 'jcstress'
95-
run: |
96-
total=0; passed=0; failed=0; errors=0; skipped=0
97-
if [ -f build/jcstress-output.txt ]; then
98-
line=$(grep 'Results:.*planned.*passed.*failed' build/jcstress-output.txt | tail -1)
99-
if [ -n "$line" ]; then
100-
total=$(echo "$line" | sed 's/.*Results: \([0-9]*\) planned.*/\1/')
101-
passed=$(echo "$line" | sed 's/.*; \([0-9]*\) passed.*/\1/')
102-
failed=$(echo "$line" | sed 's/.*passed, \([0-9]*\) failed.*/\1/')
103-
soft=$(echo "$line" | sed 's/.*failed, \([0-9]*\) soft.*/\1/')
104-
hard=$(echo "$line" | sed 's/.*soft errs, \([0-9]*\) hard.*/\1/')
105-
errors=$((soft + hard))
106-
fi
107-
fi
108-
mkdir -p /tmp/test-stats
109-
echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failed,\"errors\":$errors,\"skipped\":$skipped}" \
110-
> "/tmp/test-stats/${{ matrix.label }}.json"
111-
- name: Upload Test Stats
112-
if: always() && matrix.label != 'check'
113-
uses: actions/upload-artifact@v7
114-
with:
115-
name: test-stats-${{ matrix.label }}
116-
path: /tmp/test-stats/${{ matrix.label }}.json
117-
test-summary:
118-
name: "Per-Class Coverage Gate"
119-
needs: buildAndTest
120-
if: >-
121-
always() &&
122-
github.event_name == 'pull_request' &&
123-
github.event.pull_request.base.ref == 'master'
124-
runs-on: ubuntu-latest
125-
steps:
126-
- uses: actions/checkout@v6
127-
- name: Download Coverage Report
128-
uses: actions/download-artifact@v8
129-
continue-on-error: true
130-
with:
131-
name: coverage-report
132-
path: coverage/
133-
- name: Enforce Per-Class Coverage Gate
134-
uses: actions/github-script@v8
135-
with:
136-
script: |
137-
const fs = require('fs');
138-
const path = require('path');
139-
const { parseJacocoXml, pct, zeroCov, isRegression } = require('./.github/scripts/parse-jacoco.js');
140-
141-
// --- Read baseline from repo ---
142-
const baselineFile = 'test-baseline.json';
143-
let baseline = { tests: {}, coverage: {} };
144-
if (fs.existsSync(baselineFile)) {
145-
baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8'));
146-
}
147-
const baseClasses = (baseline.coverage || {}).classes || {};
148-
149-
// --- Parse JaCoCo XML for per-class coverage ---
150-
const jacocoFile = path.join('coverage', 'jacocoTestReport.xml');
151-
const parsed = parseJacocoXml(jacocoFile);
152-
if (!parsed) {
153-
console.log('No JaCoCo report found, skipping coverage gate.');
154-
return;
155-
}
156-
const classCounters = parsed.classes;
157-
158-
// --- Coverage gate: fail if any class regresses on any metric ---
159-
// A regression requires BOTH a percentage drop AND an increase in the
160-
// absolute number of missed items. This avoids false positives when
161-
// well-covered code is extracted/moved out of a class (which lowers the
162-
// percentage without actually losing any test coverage).
163-
const regressions = [];
164-
for (const [cls, curr] of Object.entries(classCounters)) {
165-
const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov };
166-
const classRegressions = [];
167-
for (const [metric, key] of [['Line', 'line'], ['Branch', 'branch'], ['Method', 'method']]) {
168-
const currPct = pct(curr[key].covered, curr[key].missed);
169-
const basePct = pct(base[key].covered, base[key].missed);
170-
const currMissed = curr[key].missed;
171-
const baseMissed = base[key].missed;
172-
if (isRegression(currPct, basePct, currMissed, baseMissed)) {
173-
classRegressions.push(` ${cls} ${metric}: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%, missed: ${currMissed} was ${baseMissed})`);
174-
}
175-
}
176-
if (classRegressions.length > 0) {
177-
regressions.push(...classRegressions);
178-
// Add method-level details for this regression
179-
const currMethods = curr.methods || [];
180-
const baseMethods = (base.methods || []);
181-
if (currMethods.length > 0 || baseMethods.length > 0) {
182-
const baseByKey = {};
183-
for (const m of baseMethods) baseByKey[m.name + m.desc] = m;
184-
const seen = new Set();
185-
for (const m of currMethods) {
186-
const key = m.name + m.desc;
187-
seen.add(key);
188-
const bm = baseByKey[key];
189-
const currLinePct = pct(m.counters.line.covered, m.counters.line.missed);
190-
const baseLinePct = bm ? pct(bm.counters.line.covered, bm.counters.line.missed) : null;
191-
const lineChanged = baseLinePct !== null && Math.abs(currLinePct - baseLinePct) >= 0.05;
192-
const isNew = !bm;
193-
// When baseline method data exists, only show methods that actually
194-
// changed or are new. Fall back to showing all uncovered methods
195-
// when no baseline method data is available yet.
196-
const hasMissed = m.counters.line.missed > 0 || m.counters.branch.missed > 0;
197-
const show = baseMethods.length > 0
198-
? (lineChanged || isNew)
199-
: (hasMissed || isNew);
200-
if (show) {
201-
const displayName = m.name === '<init>' ? 'constructor' : m.name;
202-
let detail = ` ${displayName} — Line: ${currLinePct.toFixed(1)}%`;
203-
if (baseLinePct !== null) detail += ` (was ${baseLinePct.toFixed(1)}%)`;
204-
else detail += ' (new)';
205-
regressions.push(detail);
206-
}
207-
}
208-
for (const bm of baseMethods) {
209-
if (!seen.has(bm.name + bm.desc)) {
210-
const displayName = bm.name === '<init>' ? 'constructor' : bm.name;
211-
regressions.push(` ${displayName} — removed`);
212-
}
213-
}
214-
}
215-
}
216-
}
217-
if (regressions.length > 0) {
218-
core.setFailed(`Per-class coverage regressions detected:\n${regressions.join('\n')}\n\nUpdate test-baseline.json if these changes are intentional.`);
219-
}
49+
run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
22050
javadoc:
22151
runs-on: ubuntu-latest
22252
steps:
@@ -233,7 +63,6 @@ jobs:
23363
if: always()
23464
needs:
23565
- buildAndTest
236-
- test-summary
23766
- javadoc
23867
runs-on: ubuntu-latest
23968
steps:
@@ -243,10 +72,6 @@ jobs:
24372
echo "buildAndTest failed with result: ${{ needs.buildAndTest.result }}"
24473
exit 1
24574
fi
246-
if [ "${{ needs.test-summary.result }}" != "success" ] && [ "${{ needs.test-summary.result }}" != "skipped" ]; then
247-
echo "Per-Class Coverage Gate failed with result: ${{ needs.test-summary.result }}"
248-
exit 1
249-
fi
25075
if [ "${{ needs.javadoc.result }}" != "success" ]; then
25176
echo "javadoc failed with result: ${{ needs.javadoc.result }}"
25277
exit 1

0 commit comments

Comments
 (0)