Skip to content

Commit 6aa273f

Browse files
committed
fix: report lost baseline coverage as worse [skip ci]
1 parent f6ef819 commit 6aa273f

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

‎scripts/verify/diff.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ def coverage_gained(b, m):
151151
return sorted(b["unobserved"] - m["unobserved"]) if m["ran"] else []
152152

153153

154+
def coverage_lost(b, m):
155+
return sorted(m["unobserved"] - b["unobserved"]) if b["ran"] and m["ran"] else []
156+
157+
154158
def distribution(summary, cls, metric):
155159
values = []
156160
for sample in summary["samples"]:
@@ -177,12 +181,14 @@ def compare_case(b, m, rel, abs_):
177181
return "N/A", ["unsupported on baseline: " + "; ".join(b.get("why", [])[:1])]
178182
if not b["ran"] and not m["ran"]:
179183
return "EQUAL", []
184+
lost = coverage_lost(b, m)
185+
worse.extend(f"{cls}: baseline observed -> main unobserved (lost coverage)" for cls in lost)
180186
bc, mc = b["classes"], m["classes"]
181187
stochastic = "missing-after-hold"
182188
distribution_classes = (set(bc) | set(mc)) & {stochastic}
183189
if b["case"].startswith("recording/") or m["case"].startswith("recording/"):
184190
distribution_classes.add(stochastic)
185-
for cls in sorted(distribution_classes):
191+
for cls in sorted(distribution_classes - set(lost)):
186192
metrics = {"count"} | set(b["measures"].get(cls, {})) | set(m["measures"].get(cls, {}))
187193
for metric in sorted(metrics):
188194
bv, mv = distribution(b, cls, metric), distribution(m, cls, metric)
@@ -252,8 +258,8 @@ def main():
252258
out = {}
253259
print(f"# {args.title}\n")
254260
print(f"tolerance: rel {args.rel_tol}, abs {args.abs_tol}\n")
255-
print("| lane | cases base/main | WORSE | BETTER | EQUAL | coverage gained | insufficient | overlapping distributions | n/a on base | only base | only main | junit WORSE | junit BETTER |")
256-
print("|---|---|---|---|---|---|---|---|---|---|---|---|---|")
261+
print("| lane | cases base/main | WORSE | BETTER | EQUAL | coverage gained | coverage lost | insufficient | overlapping distributions | n/a on base | only base | only main | junit WORSE | junit BETTER |")
262+
print("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|")
257263
detail = []
258264
for lane in lanes:
259265
b, m = brec.get(lane, {}), mrec.get(lane, {})
@@ -278,6 +284,9 @@ def main():
278284
gained = coverage_gained(bs, ms)
279285
if gained and verdict != "COVERAGE-GAINED":
280286
res["COVERAGE-GAINED"].append((key, [f"{cls}: baseline unobserved -> main observed" for cls in gained]))
287+
lost = coverage_lost(bs, ms)
288+
if lost:
289+
res["COVERAGE-LOST"].append((key, [f"{cls}: baseline observed -> main unobserved (lost coverage)" for cls in lost]))
281290
bj, mj = bjunit.get(lane, {}), mjunit.get(lane, {})
282291
jworse, jbetter = [], []
283292
bad = {"fail", "error"}
@@ -298,12 +307,12 @@ def main():
298307
jworse.append((test, ["fail -> error (crash/timeout)"]))
299308
print(
300309
f"| {lane} | {len(b)}/{len(m)} | {len(res['WORSE'])} | {len(res['BETTER'])} | {len(res['EQUAL'])}"
301-
f" | {len(res['COVERAGE-GAINED'])} | {len(res['INSUFFICIENT'])} | {len(res['OVERLAP'])} | {len(res['N/A'])} | {len(res['ONLY-BASE'])} | {len(res['ONLY-MAIN'])} | {len(jworse)} | {len(jbetter)} |"
310+
f" | {len(res['COVERAGE-GAINED'])} | {len(res['COVERAGE-LOST'])} | {len(res['INSUFFICIENT'])} | {len(res['OVERLAP'])} | {len(res['N/A'])} | {len(res['ONLY-BASE'])} | {len(res['ONLY-MAIN'])} | {len(jworse)} | {len(jbetter)} |"
302311
)
303312
out[lane] = {k: v for k, v in res.items()}
304313
out[lane]["JUNIT-WORSE"] = jworse
305314
out[lane]["JUNIT-BETTER"] = jbetter
306-
for label in ("WORSE", "BETTER", "COVERAGE-GAINED", "INSUFFICIENT", "OVERLAP", "JUNIT-WORSE", "JUNIT-BETTER", "N/A", "ONLY-BASE", "ONLY-MAIN"):
315+
for label in ("WORSE", "BETTER", "COVERAGE-GAINED", "COVERAGE-LOST", "INSUFFICIENT", "OVERLAP", "JUNIT-WORSE", "JUNIT-BETTER", "N/A", "ONLY-BASE", "ONLY-MAIN"):
307316
items = out[lane].get(label) or []
308317
if not items:
309318
continue

‎scripts/verify/test_diff.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import importlib.util
2+
import json
3+
import subprocess
4+
import sys
5+
import tempfile
26
import unittest
37
from pathlib import Path
48

@@ -24,6 +28,60 @@ def compare(base, main):
2428

2529

2630
class ComparisonTests(unittest.TestCase):
31+
def test_lost_coverage_is_worse_for_passing_and_failing_baselines(self):
32+
main = record(unobserved={"sync-screen-mic": "track absent"})
33+
for failed in [False, True]:
34+
with self.subTest(failed=failed):
35+
base = record()
36+
if failed:
37+
base["failures"] = {"sync-screen-mic": []}
38+
verdict, reasons = compare([base], [main])
39+
self.assertEqual(verdict, "WORSE")
40+
self.assertEqual(reasons, ["sync-screen-mic: baseline observed -> main unobserved (lost coverage)"])
41+
42+
def test_coverage_gain_does_not_hide_simultaneous_loss(self):
43+
base = record(unobserved={"sync-screen-mic": "track absent"})
44+
main = record(unobserved={"sync-screen-system": "track absent"})
45+
bs, ms = diff.summarize([base]), diff.summarize([main])
46+
self.assertEqual(diff.compare_case(bs, ms, 0, 0)[0], "WORSE")
47+
self.assertEqual(diff.coverage_gained(bs, ms), ["sync-screen-mic"])
48+
self.assertEqual(diff.coverage_lost(bs, ms), ["sync-screen-system"])
49+
50+
def test_partial_baseline_observation_still_detects_total_loss(self):
51+
unseen = record(unobserved={"sync-screen-mic": "track absent"})
52+
self.assertEqual(compare([unseen, record()], [unseen, unseen])[0], "WORSE")
53+
54+
def test_partial_main_observation_is_not_total_loss(self):
55+
unseen = record(unobserved={"sync-screen-mic": "track absent"})
56+
self.assertEqual(diff.coverage_lost(diff.summarize([record()]), diff.summarize([unseen, record()])), [])
57+
58+
def test_unobserved_on_both_sides_is_not_lost_coverage(self):
59+
unseen = record(unobserved={"sync-screen-mic": "track absent"})
60+
self.assertEqual(compare([unseen], [unseen]), ("EQUAL", []))
61+
62+
def test_lost_distribution_coverage_is_worse_instead_of_insufficient(self):
63+
unseen = record(unobserved={"missing-after-hold": "track absent"})
64+
self.assertEqual(compare([record(1)], [unseen])[0], "WORSE")
65+
66+
def test_cli_reports_coverage_lost_and_gained_separately(self):
67+
with tempfile.TemporaryDirectory() as root:
68+
root = Path(root)
69+
base = record(unobserved={"sync-screen-mic": "track absent"})
70+
main = record(unobserved={"sync-screen-system": "track absent"})
71+
for side, rec in [("base", base), ("main", main)]:
72+
path = root / side / "verify-report-macos-fast"
73+
path.mkdir(parents=True)
74+
rec.update({"suite": "export", "case": "a"})
75+
(path / "records.jsonl").write_text(json.dumps(rec) + "\n")
76+
result = subprocess.run(
77+
[sys.executable, "-B", str(Path(__file__).with_name("diff.py")), str(root / "base"), str(root / "main"), "--json", str(root / "diff.json")],
78+
check=True, capture_output=True, text=True,
79+
)
80+
self.assertIn("| coverage gained | coverage lost |", result.stdout)
81+
output = json.loads((root / "diff.json").read_text())["macos"]
82+
for category in ["WORSE", "COVERAGE-LOST", "COVERAGE-GAINED"]:
83+
self.assertEqual(output[category][0][0], "export::a")
84+
2785
def test_absent_track_is_coverage_gained_even_when_main_fails(self):
2886
base = record(unobserved={"sync-screen-mic": "track absent"})
2987
main = record()

0 commit comments

Comments
 (0)