| 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
|