Viewing files at commit c85a464b4b25Commit detailsView latest

Browse files
tests/test_conformance_gate.py 227 lines · 12.0 KB · python
1
"""Conformance gate semantics and its wiring into devloop and publish_gate."""
2
import contextlib
3
import io
4
import json
5
from pathlib import Path
6
import sys
7
import tempfile
8
import unittest
9
10
ROOT = Path(__file__).resolve().parents[1]
11
sys.path.insert(0, str(ROOT / "scripts"))
12
import devloop  # noqa: E402
13
import publish_gate  # noqa: E402
14
from oakbench import conformance_gate as gate  # noqa: E402
15
from oakbench.conformance import SCENARIOS  # noqa: E402
16
from oakbench.conformance_matrix import BEHAVIOURS  # noqa: E402
17
18
ALL = list(SCENARIOS) + [b.id for b in BEHAVIOURS]
19
PROFILE = "darwin-ci-ni"
20
SHA = "b" * 64
21
OBSERVED = json.loads((ROOT / "tests/data/conformance_oak_0105_failures.json").read_text())["details"]
22
23
24
def conformance_row(behaviour, code=0, failed=None, subject="oak", sha=SHA, profile=PROFILE, **extra):
25
    """failed: {leg: detail}; legacy scenarios use {'behaviour': failure}."""
26
    row = {"evidence_class": "diagnostic_conformance", "subject": subject, "scenario": behaviour,
27
           "returncode": code, "binary_sha256": sha, "fs_profile": profile, **extra}
28
    if code == 1:
29
        failed = failed or {"clone_fs": "injected"}
30
        if behaviour in SCENARIOS:
31
            row["failure"] = failed["behaviour"]
32
        else:
33
            row["legs"] = {leg: {"status": "fail", "detail": detail} for leg, detail in failed.items()}
34
            row["failed_legs"] = sorted(failed)
35
            first = sorted(failed)[0]
36
            row["failure"] = f"{first}: {failed[first]}"
37
    if code == 77:
38
        row.setdefault("skip_reason", "filesystem capability unavailable: case_sensitive_fs")
39
    return row
40
41
42
def healthy_rows(expected, subject="oak", sha=SHA, profile=PROFILE):
43
    """Oak 0.105.0 as observed: expected failures with their real details, recorded skips."""
44
    skips = set(expected["coverage"].get(profile, {}).get("expected_skips", []))
45
    rows = []
46
    for behaviour in ALL:
47
        if behaviour in expected["entries"]:
48
            rows.append(conformance_row(behaviour, 1, OBSERVED[behaviour], subject, sha, profile))
49
        elif behaviour in skips:
50
            rows.append(conformance_row(behaviour, 77, None, subject, sha, profile))
51
        else:
52
            rows.append(conformance_row(behaviour, 0, None, subject, sha, profile))
53
    return rows
54
55
56
def replace(rows, row):
57
    return [r for r in rows if r["scenario"] != row["scenario"] or r["subject"] != row["subject"]] + [row]
58
59
60
class EvaluateTests(unittest.TestCase):
61
    def setUp(self):
62
        self.expected = gate.load_expected()
63
64
    def verdict(self, rows):
65
        return gate.evaluate(rows, self.expected, ALL)
66
67
    def test_observed_oak_0105_details_match_every_entry(self):
68
        self.assertEqual(set(OBSERVED), set(self.expected["entries"]))
69
        result = self.verdict(healthy_rows(self.expected))
70
        self.assertEqual(result["verdict"], "PASS", result)
71
        self.assertEqual(len(result["expected_failures"]) + len(result["refused"]), len(self.expected["entries"]))
72
        self.assertEqual(sorted(x.split(":")[0] for x in result["refused"]),
73
                         ["path_control_chars_v1", "path_newline_v1", "path_roundtrip_v1", "path_tab_v1"])
74
75
    def test_new_failure_is_a_regression(self):
76
        rows = replace(healthy_rows(self.expected), conformance_row("eol_crlf_v1", 1, {"clone_fs": "changed=['crlf.txt']"}))
77
        result = self.verdict(rows)
78
        self.assertEqual(result["verdict"], "REGRESSED")
79
        self.assertIn("eol_crlf_v1", result["regressions"][0])
80
81
    def test_same_legs_with_a_different_error_is_a_regression(self):
82
        cases = {
83
            # EEXIST turning into a path escape at the same leg.
84
            "file_to_dir_v1": {"pull_fs": "extra=['../../etc/evil']"},
85
            # A policy refusal turning into a crash at the same leg.
86
            "path_tab_v1": {"commit_push": "oak commit --no-verify exited 101: thread 'main' panicked at core/src/tree.rs"},
87
            # The legacy single-leg scenario cannot hide a different failure.
88
            "path_roundtrip_v1": {"behaviour": "state mismatch: missing=[], extra=['tab'], changed=['nested/leaf']"},
89
            # A pattern must match the whole detail, not a substring.
90
            "file_to_deep_dir_v1": {"pull_fs": "oak pull exited 1: error: IO error: Not a directory (os error 20); "
91
                                               "extra=['../outside']"},
92
        }
93
        for behaviour, failed in cases.items():
94
            with self.subTest(behaviour=behaviour):
95
                result = self.verdict(replace(healthy_rows(self.expected), conformance_row(behaviour, 1, failed)))
96
                self.assertEqual(result["verdict"], "REGRESSED", result)
97
                self.assertIn("changed", result["regressions"][0])
98
99
    def test_known_failure_at_different_legs_is_a_regression(self):
100
        failed = dict(OBSERVED["file_to_dir_v1"], clone_fs="missing=['node/child.txt']")
101
        result = self.verdict(replace(healthy_rows(self.expected), conformance_row("file_to_dir_v1", 1, failed)))
102
        self.assertEqual(result["verdict"], "REGRESSED")
103
        self.assertIn("fb-546", result["regressions"][0])
104
105
    def test_fix_flips_to_fixed_without_regression(self):
106
        result = self.verdict(replace(healthy_rows(self.expected), conformance_row("path_tab_v1", 0)))
107
        self.assertEqual(result["verdict"], "PASS")
108
        self.assertIn("fb-531", result["fixed"][0])
109
110
    def test_profile_scoped_entry_is_not_fixed_or_expected_elsewhere(self):
111
        linux = healthy_rows(self.expected, profile="linux-cs-ns")
112
        # Case-only rename is scoped to darwin-ci-ni: a pass on Linux is a plain pass...
113
        rows = replace(linux, conformance_row("path_case_only_rename_v1", 0, profile="linux-cs-ns"))
114
        result = self.verdict(rows)
115
        self.assertEqual(result["fixed"], [])
116
        self.assertIn("path_case_only_rename_v1", result["passed"])
117
        self.assertTrue(any("no recorded coverage" in n for n in result["notes"]))
118
        # ...and the darwin failure shape on Linux is a regression, not an xfail.
119
        rows = replace(linux, conformance_row("path_case_only_rename_v1", 1, OBSERVED["path_case_only_rename_v1"],
120
                                              profile="linux-cs-ns"))
121
        self.assertEqual(self.verdict(rows)["verdict"], "REGRESSED")
122
123
    def test_coverage_loss_pass_or_xfail_to_skip_is_unmeasured(self):
124
        for behaviour in ("eol_lf_v1", "file_to_dir_v1"):
125
            with self.subTest(behaviour=behaviour):
126
                rows = replace(healthy_rows(self.expected), conformance_row(behaviour, 77))
127
                result = self.verdict(rows)
128
                self.assertEqual(result["verdict"], "UNMEASURED")
129
                self.assertIn("coverage loss", result["unmeasured"][0])
130
131
    def test_mixed_profiles_are_unmeasured(self):
132
        rows = replace(healthy_rows(self.expected), conformance_row("eol_lf_v1", 0, profile="linux-cs-ns"))
133
        self.assertEqual(self.verdict(rows)["verdict"], "UNMEASURED")
134
135
    def test_missing_rows_harness_errors_and_reader_skips_are_unmeasured(self):
136
        rows = [r for r in healthy_rows(self.expected) if r["scenario"] != "eol_lf_v1"]
137
        self.assertEqual(self.verdict(rows)["verdict"], "UNMEASURED")
138
        self.assertEqual(self.verdict(rows + [conformance_row("eol_lf_v1", 2, failure="harness")])["verdict"], "UNMEASURED")
139
        reader = conformance_row("eol_lf_v1", 77, skip_reason="leg unmeasured: reader bound")
140
        self.assertEqual(self.verdict(rows + [reader])["verdict"], "UNMEASURED")
141
        self.assertEqual(gate.evaluate([], self.expected)["verdict"], "UNMEASURED")
142
143
    def test_git_rows_never_gate(self):
144
        rows = healthy_rows(self.expected) + [conformance_row("eol_lf_v1", 1, {"clone_fs": "x"}, subject="git")]
145
        result = self.verdict(rows)
146
        self.assertEqual(result["verdict"], "PASS")
147
        self.assertEqual(len(result["git_reference_failures"]), 1)
148
149
    def test_list_validation(self):
150
        good = {"behaviour": "a", "ticket": "fb-1", "failed_legs": {"clone_fs": "x"}}
151
        bad = [dict(good, ticket="proposed:x"), dict(good, ticket="TODO"), dict(good, failed_legs=["clone_fs"]),
152
               dict(good, failed_legs={"clone_fs": ""}), dict(good, outcome="flaky"), dict(good, fs_profiles=[])]
153
        with tempfile.TemporaryDirectory() as tmp:
154
            path = Path(tmp) / "x.json"
155
            for entry in bad:
156
                path.write_text(json.dumps({"schema_version": 2, "entries": [entry]}))
157
                with self.assertRaises(ValueError):
158
                    gate.load_expected(path)
159
            path.write_text(json.dumps({"schema_version": 1, "entries": [good]}))
160
            with self.assertRaises(ValueError):
161
                gate.load_expected(path)
162
163
164
class WiringTests(unittest.TestCase):
165
    def setUp(self):
166
        self.expected = gate.load_expected()
167
168
    def test_devloop_lane_is_default_and_maps_verdicts(self):
169
        self.assertIn("conformance", devloop.LANES)
170
        regressions, notes = devloop.conformance_checks(healthy_rows(self.expected))
171
        self.assertEqual(regressions, [])
172
        self.assertFalse(any(n.startswith("UNMEASURED") for n in notes))
173
        rows = replace(healthy_rows(self.expected), conformance_row("eol_lf_v1", 1, {"pull_fs": "x"}))
174
        regressions, notes = devloop.conformance_checks(rows)
175
        self.assertTrue(regressions and regressions[0].startswith("[conformance] CONFORMANCE eol_lf_v1"))
176
        rows = [r for r in healthy_rows(self.expected) if r["scenario"] != "eol_lf_v1"]
177
        regressions, notes = devloop.conformance_checks(rows)
178
        self.assertTrue(any(n.startswith("UNMEASURED conformance") for n in notes))
179
180
    def gate_output(self, rows):
181
        with tempfile.TemporaryDirectory() as tmp:
182
            path = Path(tmp) / "rows.jsonl"
183
            path.write_text("".join(json.dumps(r) + "\n" for r in rows))
184
            with contextlib.redirect_stdout(io.StringIO()) as out:
185
                code = publish_gate.main([str(path)])
186
            return code, out.getvalue()
187
188
    def oak_row(self, sha=SHA, **extra):
189
        return {"subject": "oak_installed", "binary_sha256": sha, "returncode": 0,
190
                "source": {"oak_hash": "abc", "oak_status": ""}, **extra}
191
192
    def test_requires_conformance_for_oak_rows(self):
193
        code, out = self.gate_output([self.oak_row()])
194
        self.assertIn("FAIL   conformance: Oak rows published without", out)
195
        code, out = self.gate_output([self.oak_row(), *healthy_rows(self.expected)])
196
        self.assertIn("PASS   [bbbbbbbbbbbb] conformance (oak, darwin-ci-ni): PASS", out)
197
        self.assertNotIn("FAIL   conformance", out)
198
199
    def test_oak_rows_without_binary_identity_fail(self):
200
        anonymous = self.oak_row()
201
        anonymous.pop("binary_sha256")
202
        code, out = self.gate_output([anonymous, *healthy_rows(self.expected)])
203
        self.assertIn("Oak rows lack binary identity", out)
204
        via_details = self.oak_row(subject_details={"oak_installed": {"binary_sha256": SHA}})
205
        via_details.pop("binary_sha256")
206
        code, out = self.gate_output([via_details, *healthy_rows(self.expected)])
207
        self.assertNotIn("lack binary identity", out)
208
209
    def test_rows_of_another_binary_never_satisfy_or_mix(self):
210
        other = "c" * 64
211
        code, out = self.gate_output([self.oak_row(sha=other), *healthy_rows(self.expected)])
212
        self.assertIn(f"conformance[{other[:12]}]: UNMEASURED", out)
213
        # A good binary's rows cannot mask a regressed binary's row, and vice versa.
214
        mixed = healthy_rows(self.expected) + replace(healthy_rows(self.expected, sha=other),
215
                                                      conformance_row("eol_lf_v1", 1, {"pull_fs": "x"}, sha=other))
216
        code, out = self.gate_output([self.oak_row(), self.oak_row(sha=other), *mixed])
217
        self.assertIn(f"conformance[{other[:12]}]: REGRESSED", out)
218
        self.assertNotIn(f"conformance[{SHA[:12]}]: REGRESSED", out)
219
220
    def test_stale_list_fails(self):
221
        rows = replace(healthy_rows(self.expected), conformance_row("path_tab_v1", 0))
222
        code, out = self.gate_output([self.oak_row(), *rows])
223
        self.assertIn("expected-failures list is stale", out)
224
225
226
if __name__ == "__main__":
227
    unittest.main()