Skip to content

Commit bc5169d

Browse files
refactor(result): centralize level-failure policy (#88)
Signed-off-by: Loek <solloek369@gmail.com>
1 parent b321f9d commit bc5169d

4 files changed

Lines changed: 92 additions & 20 deletions

File tree

src/trace_tests/cli.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
import datetime as _dt
6-
import json
76
import importlib.metadata
7+
import json
88
import pathlib
99
import re
1010
import sys
@@ -17,7 +17,7 @@
1717
from trace_tests import report as report_mod
1818
from trace_tests.loader import LoadError, load_record
1919
from trace_tests.modules.tr_env import DEFAULT_MAX_AGE_SECONDS
20-
from trace_tests.modules.unverified import unverified_fails
20+
from trace_tests.modules.unverified import finding_counts_as_level_failure
2121
from trace_tests.result import Status
2222
from trace_tests.runner import run
2323

@@ -50,30 +50,22 @@ def _print_report(path: str, fmt: str, level: int, results: dict[str, list[Any]]
5050
skips = 0
5151
passes = 0
5252
unverified = 0
53-
unverified_failing = 0
54-
5553
for module, findings in results.items():
5654
for f in findings:
5755
prefix = _fmt_status(f.status)
5856
click.echo(f" {module} {prefix} {f.message}")
59-
if f.failed():
60-
failures += 1
61-
elif f.passed():
57+
counts_as_failure = finding_counts_as_level_failure(f, level)
58+
failures += int(counts_as_failure)
59+
if f.passed():
6260
passes += 1
6361
elif f.unverified():
6462
unverified += 1
65-
if unverified_fails(f.code, level):
66-
unverified_failing += 1
67-
else:
63+
elif f.skipped():
6864
skips += 1
6965

70-
# Defense in depth: an unverified finding must fail the run from the level
71-
# its code is registered at, even if a module forgot to emit a hard FAIL.
72-
# The level is per-code rather than blanket; an unregistered code fails
73-
# from level 1, which is what the blanket rule did for all of them.
74-
failures += unverified_failing
75-
76-
total = passes + failures + skips + (unverified - unverified_failing)
66+
# Contribution is projected exactly once per finding above. Status counters
67+
# remain presentation data and must not independently decide the verdict.
68+
total = sum(len(findings) for findings in results.values())
7769
click.echo("")
7870
if failures == 0:
7971
if unverified:

src/trace_tests/modules/unverified.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
from __future__ import annotations
2424

25+
from trace_tests.result import Finding
26+
2527
#: Lowest conformance level at which an unverified finding under this code
2628
#: counts as a failure. Registered here and in ``docs/levels.md``; the two are
2729
#: held equal by ``tests/test_docs_match_the_modules.py``.
@@ -38,3 +40,12 @@
3840
def unverified_fails(code: str, level: int) -> bool:
3941
"""Return True when an unverified finding under *code* must fail at *level*."""
4042
return level >= UNVERIFIED_FAILS_FROM_LEVEL.get(code, DEFAULT_FAILS_FROM_LEVEL)
43+
44+
45+
def finding_counts_as_level_failure(finding: Finding, level: int) -> bool:
46+
"""Return whether one finding contributes failure at one level."""
47+
if finding.failed():
48+
return True
49+
if finding.unverified():
50+
return unverified_fails(finding.code, level)
51+
return False

src/trace_tests/report.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from dataclasses import dataclass
2626
from typing import Any
2727

28-
from trace_tests.modules.unverified import unverified_fails
28+
from trace_tests.modules.unverified import finding_counts_as_level_failure
2929
from trace_tests.result import Finding, Status
3030

3131
__all__ = [
@@ -107,13 +107,17 @@ def verdict(self) -> str:
107107

108108

109109
def _tally(results: dict[str, list[Finding]], level: int) -> tuple[int, int]:
110-
failures = sum(1 for fs in results.values() for f in fs if f.failed())
110+
failures = sum(
111+
1
112+
for findings in results.values()
113+
for finding in findings
114+
if finding_counts_as_level_failure(finding, level)
115+
)
111116
unverified_findings = [f for fs in results.values() for f in fs if f.unverified()]
112117
# Mirrors the CLI: an unverified finding is a failure from the level its code
113118
# is registered at. A report that called such a record "PASS" at a level that
114119
# required the check would be worse than no report. Per-code rather than
115120
# blanket; an unregistered code fails from level 1, as the blanket rule did.
116-
failures += sum(1 for f in unverified_findings if unverified_fails(f.code, level))
117121
return failures, len(unverified_findings)
118122

119123

tests/test_level_failure_policy.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""CLI and portable reports share one level-failure projection."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from trace_tests import cli as cli_module
8+
from trace_tests import report as report_module
9+
from trace_tests.cli import _print_report
10+
from trace_tests.modules.unverified import finding_counts_as_level_failure
11+
from trace_tests.report import _tally
12+
from trace_tests.result import Finding, Status
13+
14+
15+
@pytest.mark.parametrize("level", [0, 1, 2])
16+
@pytest.mark.parametrize("status", list(Status))
17+
def test_cli_report_and_policy_agree_for_every_status_and_level(
18+
status: Status,
19+
level: int,
20+
) -> None:
21+
finding = Finding("TR-NEW-001", status, "synthetic")
22+
expected = finding_counts_as_level_failure(finding, level)
23+
results = {"TR-X": [finding]}
24+
25+
report_failures, _ = _tally(results, level)
26+
cli_exit = _print_report("record.json", "trace", level, results)
27+
28+
assert bool(report_failures) is expected
29+
assert bool(cli_exit) is expected
30+
31+
32+
@pytest.mark.parametrize(
33+
("status", "forced_contribution"),
34+
[
35+
(Status.PASS, True),
36+
(Status.FAIL, False),
37+
(Status.SKIP, True),
38+
(Status.UNVERIFIED, False),
39+
],
40+
)
41+
def test_each_projection_delegates_once_even_for_wrong_branch_differentials(
42+
monkeypatch: pytest.MonkeyPatch,
43+
status: Status,
44+
forced_contribution: bool,
45+
) -> None:
46+
finding = Finding("TR-X-001", status, "synthetic")
47+
results = {"TR-X": [finding]}
48+
cli_calls: list[tuple[str, int]] = []
49+
report_calls: list[tuple[str, int]] = []
50+
51+
def cli_policy(item: Finding, level: int) -> bool:
52+
cli_calls.append((item.code, level))
53+
return forced_contribution
54+
55+
def report_policy(item: Finding, level: int) -> bool:
56+
report_calls.append((item.code, level))
57+
return forced_contribution
58+
59+
monkeypatch.setattr(cli_module, "finding_counts_as_level_failure", cli_policy)
60+
monkeypatch.setattr(report_module, "finding_counts_as_level_failure", report_policy)
61+
62+
assert bool(_print_report("record.json", "trace", 2, results)) is forced_contribution
63+
assert bool(_tally(results, 2)[0]) is forced_contribution
64+
assert cli_calls == [("TR-X-001", 2)]
65+
assert report_calls == [("TR-X-001", 2)]

0 commit comments

Comments
 (0)