Skip to content

Commit a12be23

Browse files
andimarekclaude
andcommitted
Extract shared isRegression() to deduplicate hybrid coverage check
The hybrid regression check (percentage drop + missed count increase) was duplicated between pull_request.yml and pr-report.yml. Extract it into parse-jacoco.js and update both workflows. The PR report now shows 🟡 instead of 🔴 for classes where coverage percentage dropped but missed count did not increase (e.g. code extraction/refactoring). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4817db5 commit a12be23

File tree

3 files changed

+26
-13
lines changed

3 files changed

+26
-13
lines changed

.github/scripts/parse-jacoco.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,12 @@ function pct(covered, missed) {
9292
return total === 0 ? 0 : (covered / total * 100);
9393
}
9494

95-
module.exports = { parseJacocoXml, pct, zeroCov };
95+
// A coverage metric is a "real regression" when BOTH the percentage drops
96+
// beyond the tolerance AND the absolute number of missed items increases.
97+
// This avoids false positives when well-covered code is extracted/moved out
98+
// of a class (which lowers the percentage without actually losing coverage).
99+
function isRegression(currPct, basePct, currMissed, baseMissed, tolerance = 0.05) {
100+
return currPct < basePct - tolerance && currMissed > baseMissed;
101+
}
102+
103+
module.exports = { parseJacocoXml, pct, zeroCov, isRegression };

.github/workflows/pr-report.yml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
}
6565
console.log(`Posting report for PR #${prNumber}`);
6666
67-
const { parseJacocoXml, pct, zeroCov } = require('./.github/scripts/parse-jacoco.js');
67+
const { parseJacocoXml, pct, zeroCov, isRegression } = require('./.github/scripts/parse-jacoco.js');
6868
6969
const versions = ['java11', 'java17', 'java21', 'java25', 'jcstress'];
7070
const zeroTest = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0 };
@@ -232,20 +232,25 @@ jobs:
232232
return result.replace(/\$/g, '<br>\\$');
233233
}
234234
235-
function fmtClassDelta(delta) {
236-
return fmtPctDelta(delta, 0);
235+
function fmtClassDelta(delta, currMissed, baseMissed) {
236+
if (Math.abs(delta) < 0.05) return '±0.0%';
237+
const str = delta > 0 ? `+${delta.toFixed(1)}%` : `${delta.toFixed(1)}%`;
238+
if (delta > 0) return str + ' 🟢';
239+
// Negative delta: use hybrid check — only 🔴 if missed count also increased
240+
if (currMissed !== undefined && baseMissed !== undefined && currMissed <= baseMissed) {
241+
return str + ' 🟡';
242+
}
243+
return str + ' 🔴';
237244
}
238245
239246
// Build a method-level detail block for classes with coverage decreases.
240247
// Uses method name+desc as a stable key to match between baseline and current.
241248
function buildMethodDetails(c) {
242249
if (c.removed) return '';
243-
// Use the same hybrid check as the coverage gate: percentage
244-
// dropped AND absolute missed count increased. This avoids
245-
// showing details for classes where code was merely extracted/moved.
246-
const lineRegressed = c.lineDelta < -0.05 && c.currMissed.line > c.baseMissed.line;
247-
const branchRegressed = c.branchDelta < -0.05 && c.currMissed.branch > c.baseMissed.branch;
248-
const methodRegressed = c.methodDelta < -0.05 && c.currMissed.method > c.baseMissed.method;
250+
// Only show method details for true regressions (hybrid check)
251+
const lineRegressed = isRegression(c.linePct, c.linePct - c.lineDelta, c.currMissed.line, c.baseMissed.line);
252+
const branchRegressed = isRegression(c.branchPct, c.branchPct - c.branchDelta, c.currMissed.branch, c.baseMissed.branch);
253+
const methodRegressed = isRegression(c.methodPct, c.methodPct - c.methodDelta, c.currMissed.method, c.baseMissed.method);
249254
if (!lineRegressed && !branchRegressed && !methodRegressed) return '';
250255
251256
const currMethods = c.currMethods || [];
@@ -328,7 +333,7 @@ jobs:
328333
if (c.removed) {
329334
body += `| ${shortenClass(c.name)} | *removed* | *removed* | *removed* |\n`;
330335
} else {
331-
body += `| ${shortenClass(c.name)} | ${fmtClassDelta(c.lineDelta)} | ${fmtClassDelta(c.branchDelta)} | ${fmtClassDelta(c.methodDelta)} |\n`;
336+
body += `| ${shortenClass(c.name)} | ${fmtClassDelta(c.lineDelta, c.currMissed.line, c.baseMissed.line)} | ${fmtClassDelta(c.branchDelta, c.currMissed.branch, c.baseMissed.branch)} | ${fmtClassDelta(c.methodDelta, c.currMissed.method, c.baseMissed.method)} |\n`;
332337
}
333338
}
334339
body += '\n';

.github/workflows/pull_request.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ jobs:
132132
script: |
133133
const fs = require('fs');
134134
const path = require('path');
135-
const { parseJacocoXml, pct, zeroCov } = require('./.github/scripts/parse-jacoco.js');
135+
const { parseJacocoXml, pct, zeroCov, isRegression } = require('./.github/scripts/parse-jacoco.js');
136136
137137
// --- Read baseline from repo ---
138138
const baselineFile = 'test-baseline.json';
@@ -165,7 +165,7 @@ jobs:
165165
const basePct = pct(base[key].covered, base[key].missed);
166166
const currMissed = curr[key].missed;
167167
const baseMissed = base[key].missed;
168-
if (currPct < basePct - 0.05 && currMissed > baseMissed) {
168+
if (isRegression(currPct, basePct, currMissed, baseMissed)) {
169169
classRegressions.push(` ${cls} ${metric}: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%, missed: ${currMissed} was ${baseMissed})`);
170170
}
171171
}

0 commit comments

Comments
 (0)