Skip to content

Fix dataloader dispatch in @defer when multiple deferred fragments exist #3158

Fix dataloader dispatch in @defer when multiple deferred fragments exist

Fix dataloader dispatch in @defer when multiple deferred fragments exist #3158

Workflow file for this run

name: Pull Request Build
# For pull requests: builds and test
on:
push:
branches:
- '!master'
pull_request:
branches:
- master
- 23.x
- 22.x
- 21.x
- 20.x
- 19.x
permissions:
contents: read
jobs:
buildAndTest:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- gradle-argument: 'assemble && ./gradlew check -x test -x testng -x testngWithJava11 -x testngWithJava17 -x testngWithJava21'
label: 'check'
- gradle-argument: 'testWithJava11 testngWithJava11'
label: 'java11'
test-results-dirs: 'testWithJava11 testngWithJava11'
- gradle-argument: 'testWithJava17 testngWithJava17'
label: 'java17'
test-results-dirs: 'testWithJava17 testngWithJava17'
- gradle-argument: 'testWithJava21 testngWithJava21'
label: 'java21'
test-results-dirs: 'testWithJava21 testngWithJava21'
- gradle-argument: 'test -x testWithJava11 -x testWithJava17 -x testWithJava21 testng jacocoTestReport'
label: 'java25'
test-results-dirs: 'test testng'
- gradle-argument: 'jcstress'
label: 'jcstress'
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'corretto'
- name: build and test
run: |
if [ "${{ matrix.label }}" = "jcstress" ]; then
set -o pipefail
mkdir -p build
./gradlew ${{matrix.gradle-argument}} --info --stacktrace 2>&1 | tee build/jcstress-output.txt
else
./gradlew ${{matrix.gradle-argument}} --info --stacktrace
fi
- name: Upload Coverage HTML Report
uses: actions/upload-artifact@v4
if: always() && matrix.label == 'java25'
with:
name: jacoco-html-report
path: build/reports/jacoco/test/html/
retention-days: 14
- 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' && matrix.label != 'jcstress'
run: |
total=0; failures=0; errors=0; skipped=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
echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \
> "/tmp/test-stats/${{ matrix.label }}.json"
- name: Parse jcstress Results
if: always() && matrix.label == 'jcstress'
run: |
total=0; passed=0; failed=0; errors=0; skipped=0
if [ -f build/jcstress-output.txt ]; then
line=$(grep 'Results:.*planned.*passed.*failed' build/jcstress-output.txt | tail -1)
if [ -n "$line" ]; then
total=$(echo "$line" | sed 's/.*Results: \([0-9]*\) planned.*/\1/')
passed=$(echo "$line" | sed 's/.*; \([0-9]*\) passed.*/\1/')
failed=$(echo "$line" | sed 's/.*passed, \([0-9]*\) failed.*/\1/')
soft=$(echo "$line" | sed 's/.*failed, \([0-9]*\) soft.*/\1/')
hard=$(echo "$line" | sed 's/.*soft errs, \([0-9]*\) hard.*/\1/')
errors=$((soft + hard))
fi
fi
mkdir -p /tmp/test-stats
echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failed,\"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:
name: "Per-Class Coverage Gate"
needs: buildAndTest
if: always() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Download Coverage Report
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: coverage-report
path: coverage/
- name: Enforce Per-Class Coverage Gate
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const zeroCov = { covered: 0, missed: 0 };
// --- Read baseline from repo ---
const baselineFile = 'test-baseline.json';
let baseline = { tests: {}, coverage: {} };
if (fs.existsSync(baselineFile)) {
baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8'));
}
const baseClasses = (baseline.coverage || {}).classes || {};
// --- Parse JaCoCo XML for per-class coverage ---
const classCounters = {};
const jacocoFile = path.join('coverage', 'jacocoTestReport.xml');
if (!fs.existsSync(jacocoFile)) {
console.log('No JaCoCo report found, skipping coverage gate.');
return;
}
const xml = fs.readFileSync(jacocoFile, 'utf8');
const pkgRe = /<package\s+name="([^"]+)">([\s\S]*?)<\/package>/g;
let pkgMatch;
while ((pkgMatch = pkgRe.exec(xml)) !== null) {
const pkgBody = pkgMatch[2];
const classRe = /<class\s+name="([^"]+)"[^>]*>([\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 = /<counter type="(\w+)" missed="(\d+)" covered="(\d+)"\/>/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;
}
}
function pct(covered, missed) {
const total = covered + missed;
return total === 0 ? 0 : (covered / total * 100);
}
// --- Coverage gate: fail if any class regresses on any metric ---
const regressions = [];
for (const [cls, curr] of Object.entries(classCounters)) {
const totalLines = curr.line.covered + curr.line.missed;
if (totalLines === 0) continue;
const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov };
for (const [metric, key] of [['Line', 'line'], ['Branch', 'branch'], ['Method', 'method']]) {
const currPct = pct(curr[key].covered, curr[key].missed);
const basePct = pct(base[key].covered, base[key].missed);
if (currPct < basePct - 0.05) {
regressions.push(` ${cls} ${metric}: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%)`);
}
}
}
if (regressions.length > 0) {
core.setFailed(`Per-class coverage regressions detected:\n${regressions.join('\n')}\n\nUpdate test-baseline.json if these changes are intentional.`);
}
javadoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'corretto'
- name: Verify Javadoc
run: ./gradlew javadoc --info --stacktrace
allBuildAndTestSuccessful:
if: always()
needs:
- buildAndTest
- test-summary
- javadoc
runs-on: ubuntu-latest
steps:
- name: Verify all jobs passed
run: |
if [ "${{ needs.buildAndTest.result }}" != "success" ]; then
echo "buildAndTest failed with result: ${{ needs.buildAndTest.result }}"
exit 1
fi
if [ "${{ needs.test-summary.result }}" != "success" ] && [ "${{ needs.test-summary.result }}" != "skipped" ]; then
echo "Per-Class Coverage Gate failed with result: ${{ needs.test-summary.result }}"
exit 1
fi
if [ "${{ needs.javadoc.result }}" != "success" ]; then
echo "javadoc failed with result: ${{ needs.javadoc.result }}"
exit 1
fi
echo "All build and test jobs passed successfully."