Viewing files at commit 21316fe59317Commit detailsView latest

Browse files
scripts/oakbench/conformance_gate.py 106 lines · 5.5 KB · python
1
"""Conformance gate: diagnostic conformance rows vs an explicit expected-failures list.
2
3
Semantics (strict xfail):
4
- a behaviour that fails and is not listed is a REGRESSION (blocks);
5
- a listed behaviour that fails at a *different* set of legs than recorded is
6
  also a regression (the failure changed shape, e.g. rejection -> corruption);
7
- a listed behaviour that now passes is FIXED: the verdict is not a regression,
8
  but the list is stale and ``publish_gate`` refuses until it is updated;
9
- harness errors, reader-unmeasured legs, and required behaviours with no row
10
  make the result UNMEASURED; capability/gated skips are recorded, not failed.
11
12
Only the subject named by the list (Oak) is gated; Git rows are the reference
13
arm and are reported, never gating.
14
"""
15
from __future__ import annotations
16
17
import json
18
from pathlib import Path
19
import re
20
21
ROOT = Path(__file__).resolve().parents[2]
22
EXPECTED_FAILURES = ROOT / "config" / "conformance_expected_failures.json"
23
TICKET = re.compile(r"^(fb-\d+|proposed:[a-z0-9-]+)$")
24
CAPABILITY_SKIP_PREFIXES = ("filesystem capability unavailable", "gated:", "gated behaviour")
25
26
27
def load_expected(path: Path = EXPECTED_FAILURES) -> dict:
28
    data = json.loads(Path(path).read_text())
29
    if data.get("schema_version") != 1 or not isinstance(data.get("entries"), list):
30
        raise ValueError("conformance expected-failures: unsupported schema")
31
    entries = {}
32
    for entry in data["entries"]:
33
        behaviour = entry.get("behaviour")
34
        if not behaviour or behaviour in entries:
35
            raise ValueError(f"conformance expected-failures: bad or duplicate behaviour {behaviour!r}")
36
        if not TICKET.match(str(entry.get("ticket", ""))):
37
            raise ValueError(f"conformance expected-failures: {behaviour} needs a ticket (fb-N or proposed:slug)")
38
        if not isinstance(entry.get("failed_legs"), list) or not entry["failed_legs"]:
39
            raise ValueError(f"conformance expected-failures: {behaviour} needs failed_legs")
40
        entries[behaviour] = entry
41
    return {"subject": data.get("subject", "oak"), "entries": entries}
42
43
44
def row_failed_legs(row: dict) -> list:
45
    if "failed_legs" in row:
46
        return sorted(row["failed_legs"])
47
    # Legacy v1 scenarios have no legs: a failure is the whole behaviour.
48
    return ["behaviour"] if row.get("returncode") == 1 else []
49
50
51
def evaluate(rows: list, expected: dict, required=None) -> dict:
52
    subject = expected["subject"]
53
    entries = expected["entries"]
54
    result = {"subject": subject, "passed": [], "expected_failures": [], "fixed": [], "regressions": [],
55
              "unmeasured": [], "skipped": [], "git_reference_failures": [], "differential_differs": []}
56
    seen = set()
57
    for row in rows:
58
        if row.get("evidence_class") != "diagnostic_conformance":
59
            continue
60
        behaviour = row.get("scenario")
61
        code = row.get("returncode")
62
        if row.get("subject") != subject:
63
            if row.get("subject") == "git" and code == 1:
64
                result["git_reference_failures"].append(f"{behaviour}: {row.get('failure')}")
65
            continue
66
        seen.add(behaviour)
67
        entry = entries.get(behaviour)
68
        differential = str(row.get("git_differential") or "")
69
        if differential.startswith("differs"):
70
            result["differential_differs"].append(f"{behaviour}: {differential}")
71
        if code == 0:
72
            (result["fixed"] if entry else result["passed"]).append(
73
                f"{behaviour}: now passes; remove {entry['ticket']} from the expected-failures list" if entry else behaviour)
74
        elif code == 1:
75
            legs = row_failed_legs(row)
76
            if entry is None:
77
                result["regressions"].append(f"{behaviour}: {row.get('failure')}")
78
            elif sorted(entry["failed_legs"]) != legs:
79
                result["regressions"].append(
80
                    f"{behaviour}: known failure ({entry['ticket']}) changed shape: expected legs "
81
                    f"{sorted(entry['failed_legs'])}, observed {legs}: {row.get('failure')}")
82
            else:
83
                result["expected_failures"].append(f"{behaviour}: {entry['ticket']}")
84
        elif code == 77 and str(row.get("skip_reason", "")).startswith(CAPABILITY_SKIP_PREFIXES):
85
            result["skipped"].append(f"{behaviour}: {row.get('skip_reason')}")
86
        else:
87
            result["unmeasured"].append(f"{behaviour}: {row.get('failure') or row.get('skip_reason') or code}")
88
    for behaviour in sorted(set(required or ()) - seen):
89
        result["unmeasured"].append(f"{behaviour}: no {subject} conformance row")
90
    if not seen:
91
        result["unmeasured"].append(f"no {subject} conformance rows")
92
    result["verdict"] = ("REGRESSED" if result["regressions"] else
93
                         "UNMEASURED" if result["unmeasured"] else "PASS")
94
    return result
95
96
97
def summary_lines(result: dict) -> list:
98
    lines = [f"conformance ({result['subject']}): {result['verdict']} - {len(result['passed'])} pass, "
99
             f"{len(result['expected_failures'])} expected-fail, {len(result['fixed'])} fixed, "
100
             f"{len(result['regressions'])} regression(s), {len(result['unmeasured'])} unmeasured, "
101
             f"{len(result['skipped'])} capability/gated skip(s)"]
102
    for key, label in (("regressions", "REGRESSION"), ("fixed", "FIXED"), ("unmeasured", "UNMEASURED"),
103
                       ("expected_failures", "XFAIL"), ("differential_differs", "GIT-DIFF"),
104
                       ("git_reference_failures", "GIT-REF"), ("skipped", "SKIP")):
105
        lines += [f"  {label} {item}" for item in result[key]]
106
    return lines