canary-test-cli 6.8.0 → 6.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -612,48 +612,56 @@ function readReportText(reportPath) {
|
|
|
612
612
|
* blocks).
|
|
613
613
|
*/
|
|
614
614
|
export function resolveFromReport(units, reportPath) {
|
|
615
|
-
const { index } = readReportIndex(reportPath);
|
|
615
|
+
const { index, absence } = readReportIndex(reportPath);
|
|
616
616
|
if (index === null)
|
|
617
617
|
return null;
|
|
618
|
-
return matchUnitsToIndex(units, index);
|
|
618
|
+
return matchUnitsToIndex(units, index, absence);
|
|
619
619
|
}
|
|
620
620
|
/** Read + parse a coverage report, reporting each step's outcome separately. */
|
|
621
621
|
function readReportIndex(reportPath) {
|
|
622
|
+
const unusable = (found) => ({
|
|
623
|
+
found,
|
|
624
|
+
index: null,
|
|
625
|
+
absence: 'not-coverable',
|
|
626
|
+
});
|
|
622
627
|
if (!existsSync(reportPath))
|
|
623
|
-
return
|
|
628
|
+
return unusable(false);
|
|
624
629
|
const text = readReportText(reportPath);
|
|
625
630
|
// Present but unreadable/non-UTF-8 counts as found-and-unusable, not absent.
|
|
626
631
|
if (text === null)
|
|
627
|
-
return
|
|
632
|
+
return unusable(true);
|
|
628
633
|
const name = basename(reportPath).toLowerCase();
|
|
629
634
|
let index;
|
|
635
|
+
let absence;
|
|
630
636
|
if (name.endsWith('.json')) {
|
|
631
637
|
let parsed;
|
|
632
638
|
try {
|
|
633
639
|
parsed = JSON.parse(text);
|
|
634
640
|
}
|
|
635
641
|
catch {
|
|
636
|
-
return
|
|
642
|
+
return unusable(true);
|
|
637
643
|
}
|
|
638
644
|
index = parseCoverageJson(parsed);
|
|
645
|
+
absence = 'uncovered';
|
|
639
646
|
}
|
|
640
647
|
else if (name.endsWith('.info') || name.includes('lcov')) {
|
|
641
648
|
index = parseLcov(text);
|
|
649
|
+
absence = 'not-coverable';
|
|
642
650
|
}
|
|
643
651
|
else if (name.endsWith('.xml')) {
|
|
644
652
|
index = parseCobertura(text);
|
|
653
|
+
absence = 'not-coverable';
|
|
645
654
|
}
|
|
646
655
|
else {
|
|
647
656
|
// Unrecognized format → fall through to a lower fidelity tier.
|
|
648
|
-
return
|
|
657
|
+
return unusable(true);
|
|
649
658
|
}
|
|
650
|
-
if (index === null || Object.keys(index).length === 0)
|
|
651
|
-
return
|
|
652
|
-
}
|
|
653
|
-
return { found: true, index };
|
|
659
|
+
if (index === null || Object.keys(index).length === 0)
|
|
660
|
+
return unusable(true);
|
|
661
|
+
return { found: true, index, absence };
|
|
654
662
|
}
|
|
655
663
|
/** Resolve every unit the report index can speak to (COVERAGE_VERIFIED). */
|
|
656
|
-
function matchUnitsToIndex(units, index) {
|
|
664
|
+
function matchUnitsToIndex(units, index, absence) {
|
|
657
665
|
const results = [];
|
|
658
666
|
for (const unit of units) {
|
|
659
667
|
const hits = matchHits(unit.path, index);
|
|
@@ -665,17 +673,30 @@ function matchUnitsToIndex(units, index) {
|
|
|
665
673
|
continue;
|
|
666
674
|
}
|
|
667
675
|
const added = expandRanges(unit.added_ranges);
|
|
668
|
-
|
|
676
|
+
// The per-line form of the check above (#655): under a format that
|
|
677
|
+
// enumerates instrumented lines, a changed line with no record could not
|
|
678
|
+
// have been executed and is scored by neither side.
|
|
679
|
+
const coverable = absence === 'not-coverable' ? added.filter((ln) => ln in hits) : added;
|
|
680
|
+
if (coverable.length === 0) {
|
|
681
|
+
// Every changed line is non-coverable, so this report has nothing to say
|
|
682
|
+
// about the unit. An abstention — never a clean pass, never a finding.
|
|
683
|
+
// Falls through to the graph/heuristic tier exactly as an absent path does.
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const uncovered = coverable.filter((ln) => (hits[ln] ?? 0) <= 0);
|
|
669
687
|
const covered = uncovered.length === 0;
|
|
688
|
+
// State the denominator: "all covered" over 20 changed lines and over the 3
|
|
689
|
+
// of them that were coverable are very different claims (#508).
|
|
670
690
|
const evidence = covered
|
|
671
|
-
? `lines ${rangesStr(unit.added_ranges)}: all covered`
|
|
672
|
-
: `lines ${rangesStr(unit.added_ranges)}: ${uncovered.length} uncovered`;
|
|
691
|
+
? `lines ${rangesStr(unit.added_ranges)}: all ${coverable.length} coverable line(s) covered`
|
|
692
|
+
: `lines ${rangesStr(unit.added_ranges)}: ${uncovered.length} of ${coverable.length} coverable line(s) uncovered`;
|
|
673
693
|
results.push(makeResult({
|
|
674
694
|
unit,
|
|
675
695
|
covered,
|
|
676
696
|
fidelity: Fidelity.CoverageVerified,
|
|
677
697
|
evidence,
|
|
678
698
|
uncovered_lines: uncovered,
|
|
699
|
+
coverable_lines: coverable.length,
|
|
679
700
|
}));
|
|
680
701
|
}
|
|
681
702
|
return results;
|
|
@@ -1399,7 +1420,9 @@ export function resolveCoverageWithInput(units, options = {}) {
|
|
|
1399
1420
|
coverage.parsed = read.index !== null;
|
|
1400
1421
|
coverage.filesInReport =
|
|
1401
1422
|
read.index === null ? 0 : Object.keys(read.index).length;
|
|
1402
|
-
const report = read.index === null
|
|
1423
|
+
const report = read.index === null
|
|
1424
|
+
? null
|
|
1425
|
+
: matchUnitsToIndex(remaining, read.index, read.absence);
|
|
1403
1426
|
// An empty array (no unit matched the report) is falsy-equivalent in the
|
|
1404
1427
|
// Python `if report:` guard — fall through rather than lock in nothing.
|
|
1405
1428
|
if (report !== null && report.length > 0) {
|
|
@@ -565,6 +565,22 @@ const HIGH_UNCOVERED_SHARE = 0.5;
|
|
|
565
565
|
function addedLineCount(ranges) {
|
|
566
566
|
return ranges.reduce((total, [start, end]) => total + (end - start + 1), 0);
|
|
567
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* The fraction of what the report could speak to that came back unhit (#655).
|
|
570
|
+
*
|
|
571
|
+
* The denominator is the unit's **coverable** lines — added lines overstate it,
|
|
572
|
+
* because on a new file that is largely imports, types and blanks most changed
|
|
573
|
+
* lines were never instrumented at all, and dividing by them drives every share
|
|
574
|
+
* toward zero. Where the count is unknown (the graph and heuristic tiers, which
|
|
575
|
+
* are not graded by share anyway) it falls back to the added-line count, so
|
|
576
|
+
* grades predating that field are unchanged. An unknown denominator yields a
|
|
577
|
+
* full share rather than a low one — an absent measurement must never read as a
|
|
578
|
+
* good score (ADR 0010).
|
|
579
|
+
*/
|
|
580
|
+
function uncoveredShare(result, uncovered) {
|
|
581
|
+
const denominator = result.coverable_lines ?? addedLineCount(result.unit.added_ranges);
|
|
582
|
+
return denominator > 0 ? uncovered / denominator : 1;
|
|
583
|
+
}
|
|
568
584
|
/**
|
|
569
585
|
* Severity for an uncovered **coverage-verified** result (#553).
|
|
570
586
|
*
|
|
@@ -589,8 +605,7 @@ function coverageVerifiedSeverity(result) {
|
|
|
589
605
|
const uncovered = result.uncovered_lines?.length ?? 0;
|
|
590
606
|
if (uncovered === 0)
|
|
591
607
|
return Severity.HIGH;
|
|
592
|
-
const
|
|
593
|
-
const share = added > 0 ? uncovered / added : 1;
|
|
608
|
+
const share = uncoveredShare(result, uncovered);
|
|
594
609
|
if (uncovered >= CRITICAL_UNCOVERED_LINES &&
|
|
595
610
|
share >= CRITICAL_UNCOVERED_SHARE)
|
|
596
611
|
return Severity.CRITICAL;
|