Viewing files at commit bd39a563b7f5Commit detailsView latest

Browse files
tests/test_conformance_gate.py 349 lines · 19.4 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
from unittest import mock
10
11
ROOT = Path(__file__).resolve().parents[1]
12
sys.path.insert(0, str(ROOT / "scripts"))
13
import devloop  # noqa: E402
14
import publish_gate  # noqa: E402
15
from oakbench import conformance_gate as gate  # noqa: E402
16
from oakbench.conformance import SCENARIOS  # noqa: E402
17
from oakbench.conformance_matrix import BEHAVIOURS  # noqa: E402
18
19
ALL = list(SCENARIOS) + [b.id for b in BEHAVIOURS]
20
PROFILE = "darwin-ci-ni"
21
SHA = "b" * 64
22
OBSERVED = json.loads((ROOT / "tests/data/conformance_oak_0105_failures.json").read_text())["details"]
23
24
25
def conformance_row(behaviour, code=0, failed=None, subject="oak", sha=SHA, profile=PROFILE, **extra):
26
    """failed: {leg: detail}; legacy scenarios use {'behaviour': failure}."""
27
    row = {"evidence_class": "diagnostic_conformance", "subject": subject, "scenario": behaviour,
28
           "returncode": code, "binary_sha256": sha, "fs_profile": profile, **extra}
29
    if code == 1:
30
        failed = failed or {"clone_fs": "injected"}
31
        if behaviour in SCENARIOS:
32
            row["failure"] = failed["behaviour"]
33
        else:
34
            row["legs"] = {leg: {"status": "fail", "detail": detail} for leg, detail in failed.items()}
35
            row["failed_legs"] = sorted(failed)
36
            first = sorted(failed)[0]
37
            row["failure"] = f"{first}: {failed[first]}"
38
    if code == 77:
39
        row.setdefault("skip_reason", "filesystem capability unavailable: case_sensitive_fs")
40
    return row
41
42
43
def healthy_rows(expected, subject="oak", sha=SHA, profile=PROFILE):
44
    """Oak 0.105.0 as observed: expected failures with their real details, recorded skips."""
45
    skips = set(expected["coverage"].get(profile, {}).get("expected_skips", []))
46
    rows = []
47
    for behaviour in ALL:
48
        if behaviour in expected["entries"] and gate.entry_applies(expected["entries"][behaviour], profile):
49
            rows.append(conformance_row(behaviour, 1, OBSERVED[behaviour], subject, sha, profile))
50
        elif behaviour in skips:
51
            rows.append(conformance_row(behaviour, 77, None, subject, sha, profile))
52
        else:
53
            rows.append(conformance_row(behaviour, 0, None, subject, sha, profile))
54
    return rows
55
56
57
def replace(rows, row):
58
    return [r for r in rows if r["scenario"] != row["scenario"] or r["subject"] != row["subject"]] + [row]
59
60
61
class EvaluateTests(unittest.TestCase):
62
    def setUp(self):
63
        self.expected = gate.load_expected()
64
65
    def verdict(self, rows):
66
        return gate.evaluate(rows, self.expected, ALL)
67
68
    def test_observed_oak_0105_details_match_every_entry(self):
69
        self.assertEqual(set(OBSERVED), set(self.expected["entries"]))
70
        result = self.verdict(healthy_rows(self.expected))
71
        self.assertEqual(result["verdict"], "PASS", result)
72
        self.assertEqual(len(result["expected_failures"]) + len(result["refused"]) + len(result["intermittent"]),
73
                         len(self.expected["entries"]))
74
        self.assertEqual(result["intermittent"], ["large_file_chunked_v1: fb-547"])
75
        self.assertFalse(any("unfiled tickets" in note for note in result["notes"]))
76
        self.assertEqual(sorted(x.split(":")[0] for x in result["refused"]),
77
                         ["path_control_chars_v1", "path_newline_v1", "path_roundtrip_v1", "path_tab_v1"])
78
79
    def test_new_failure_is_a_regression(self):
80
        rows = replace(healthy_rows(self.expected), conformance_row("eol_crlf_v1", 1, {"clone_fs": "changed=['crlf.txt']"}))
81
        result = self.verdict(rows)
82
        self.assertEqual(result["verdict"], "REGRESSED")
83
        self.assertIn("eol_crlf_v1", result["regressions"][0])
84
85
    def test_same_legs_with_a_different_error_is_a_regression(self):
86
        cases = {
87
            # EEXIST turning into a path escape at the same leg.
88
            "file_to_dir_v1": {"pull_fs": "extra=['../../etc/evil']"},
89
            # A policy refusal turning into a crash at the same leg.
90
            "path_tab_v1": {"commit_push": "oak commit --no-verify exited 101: thread 'main' panicked at core/src/tree.rs"},
91
            # The legacy single-leg scenario cannot hide a different failure.
92
            "path_roundtrip_v1": {"behaviour": "state mismatch: missing=[], extra=['tab'], changed=['nested/leaf']"},
93
            # A pattern must match the whole detail, not a substring.
94
            "file_to_deep_dir_v1": {"pull_fs": "oak pull exited 1: error: IO error: Not a directory (os error 20); "
95
                                               "extra=['../outside']"},
96
        }
97
        for behaviour, failed in cases.items():
98
            with self.subTest(behaviour=behaviour):
99
                result = self.verdict(replace(healthy_rows(self.expected), conformance_row(behaviour, 1, failed)))
100
                self.assertEqual(result["verdict"], "REGRESSED", result)
101
                self.assertIn("changed", result["regressions"][0])
102
103
    def test_known_failure_at_different_legs_is_a_regression(self):
104
        failed = dict(OBSERVED["file_to_dir_v1"], clone_fs="missing=['node/child.txt']")
105
        result = self.verdict(replace(healthy_rows(self.expected), conformance_row("file_to_dir_v1", 1, failed)))
106
        self.assertEqual(result["verdict"], "REGRESSED")
107
        self.assertIn("fb-546", result["regressions"][0])
108
109
    def test_fix_flips_to_fixed_without_regression(self):
110
        result = self.verdict(replace(healthy_rows(self.expected), conformance_row("path_tab_v1", 0)))
111
        self.assertEqual(result["verdict"], "PASS")
112
        self.assertIn("fb-531", result["fixed"][0])
113
114
    def test_intermittent_entry_pass_is_a_pass_and_other_failures_regress(self):
115
        rows = replace(healthy_rows(self.expected), conformance_row("large_file_chunked_v1", 0))
116
        result = self.verdict(rows)
117
        self.assertEqual(result["verdict"], "PASS")
118
        self.assertEqual(result["fixed"], [])
119
        self.assertIn("large_file_chunked_v1", result["passed"])
120
        rows = replace(rows, conformance_row("large_file_chunked_v1", 1, {"clone_fs": "changed=['large.bin']"}))
121
        self.assertEqual(self.verdict(rows)["verdict"], "REGRESSED")
122
123
    def test_profile_scoped_entry_is_not_fixed_or_expected_elsewhere(self):
124
        linux = healthy_rows(self.expected, profile="linux-cs-ns")
125
        # Case-only rename is scoped to darwin-ci-ni: a pass on Linux is a plain pass...
126
        rows = replace(linux, conformance_row("path_case_only_rename_v1", 0, profile="linux-cs-ns"))
127
        result = self.verdict(rows)
128
        self.assertEqual(result["fixed"], [])
129
        self.assertIn("path_case_only_rename_v1", result["passed"])
130
        self.assertTrue(any("no recorded coverage" in n for n in result["notes"]))
131
        # ...and the darwin failure shape on Linux is a regression, not an xfail.
132
        rows = replace(linux, conformance_row("path_case_only_rename_v1", 1, OBSERVED["path_case_only_rename_v1"],
133
                                              profile="linux-cs-ns"))
134
        self.assertEqual(self.verdict(rows)["verdict"], "REGRESSED")
135
136
    def test_coverage_loss_pass_or_xfail_to_skip_is_unmeasured(self):
137
        for behaviour in ("eol_lf_v1", "file_to_dir_v1"):
138
            with self.subTest(behaviour=behaviour):
139
                rows = replace(healthy_rows(self.expected), conformance_row(behaviour, 77))
140
                result = self.verdict(rows)
141
                self.assertEqual(result["verdict"], "UNMEASURED")
142
                self.assertIn("coverage loss", result["unmeasured"][0])
143
144
    # QA round 2: skips must never add up to a PASS.
145
    def all_skipped(self, profile=PROFILE, drop_profile=False):
146
        rows = [conformance_row(b, 77, None, profile=profile) for b in ALL]
147
        if drop_profile:
148
            for row in rows:
149
                row.pop("fs_profile")
150
        return rows
151
152
    def test_all_skip_on_unrecorded_profile_is_unmeasured(self):
153
        result = self.verdict(self.all_skipped(profile="linux-cs-ns"))
154
        self.assertEqual(result["verdict"], "UNMEASURED")
155
        self.assertTrue(any("zero measured" in item for item in result["unmeasured"]))
156
        self.assertTrue(any("unrecorded profile" in item for item in result["unmeasured"]))
157
158
    def test_rows_without_fs_profile_are_unmeasured(self):
159
        result = self.verdict(self.all_skipped(drop_profile=True))
160
        self.assertEqual(result["verdict"], "UNMEASURED")
161
        self.assertTrue(any("without fs_profile" in item for item in result["unmeasured"]))
162
        # Even one otherwise-healthy row missing the field is unmeasured.
163
        rows = healthy_rows(self.expected)
164
        rows[0].pop("fs_profile")
165
        self.assertEqual(self.verdict(rows)["verdict"], "UNMEASURED")
166
167
    def test_zero_measured_is_unmeasured_even_on_recorded_profile(self):
168
        result = self.verdict(self.all_skipped())
169
        self.assertEqual(result["verdict"], "UNMEASURED")
170
        self.assertEqual(result["measured"], 0)
171
172
    def test_unrecorded_profile_allows_only_known_filesystem_dependent_skips(self):
173
        linux = "linux-cs-ns"
174
        rows = healthy_rows(self.expected, profile=linux)
175
        # Legitimate on Linux: the case-insensitive-only behaviour skips.
176
        ok = replace(rows, conformance_row("path_case_only_rename_v1", 77, profile=linux,
177
                                           skip_reason="filesystem capability unavailable: case_insensitive_fs"))
178
        ok = replace(ok, conformance_row("huge_file_sparse_v1", 77, profile=linux, skip_reason="gated: off"))
179
        self.assertEqual(self.verdict(ok)["verdict"], "PASS")
180
        # A broken probe skipping symlink behaviours is not accepted.
181
        bad = replace(ok, conformance_row("symlink_relative_v1", 77, profile=linux,
182
                                          skip_reason="filesystem capability unavailable: symlink"))
183
        result = self.verdict(bad)
184
        self.assertEqual(result["verdict"], "UNMEASURED")
185
        self.assertIn("symlink_relative_v1", result["unmeasured"][0])
186
187
    def test_mixed_profiles_are_unmeasured(self):
188
        rows = replace(healthy_rows(self.expected), conformance_row("eol_lf_v1", 0, profile="linux-cs-ns"))
189
        self.assertEqual(self.verdict(rows)["verdict"], "UNMEASURED")
190
191
    def test_missing_rows_harness_errors_and_reader_skips_are_unmeasured(self):
192
        rows = [r for r in healthy_rows(self.expected) if r["scenario"] != "eol_lf_v1"]
193
        self.assertEqual(self.verdict(rows)["verdict"], "UNMEASURED")
194
        self.assertEqual(self.verdict(rows + [conformance_row("eol_lf_v1", 2, failure="harness")])["verdict"], "UNMEASURED")
195
        reader = conformance_row("eol_lf_v1", 77, skip_reason="leg unmeasured: reader bound")
196
        self.assertEqual(self.verdict(rows + [reader])["verdict"], "UNMEASURED")
197
        self.assertEqual(gate.evaluate([], self.expected)["verdict"], "UNMEASURED")
198
199
    def test_git_rows_never_gate(self):
200
        rows = healthy_rows(self.expected) + [conformance_row("eol_lf_v1", 1, {"clone_fs": "x"}, subject="git")]
201
        result = self.verdict(rows)
202
        self.assertEqual(result["verdict"], "PASS")
203
        self.assertEqual(len(result["git_reference_failures"]), 1)
204
205
    def test_list_validation(self):
206
        good = {"behaviour": "a", "ticket": "fb-1", "failed_legs": {"clone_fs": "x"}}
207
        bad = [dict(good, ticket="proposed:x"), dict(good, ticket="TODO"), dict(good, ticket="pending:Bad Slug"), dict(good, failed_legs=["clone_fs"]),
208
               dict(good, failed_legs={"clone_fs": ""}), dict(good, outcome="flaky"), dict(good, fs_profiles=[])]
209
        with tempfile.TemporaryDirectory() as tmp:
210
            path = Path(tmp) / "x.json"
211
            for entry in bad:
212
                path.write_text(json.dumps({"schema_version": 2, "entries": [entry]}))
213
                with self.assertRaises(ValueError):
214
                    gate.load_expected(path)
215
            path.write_text(json.dumps({"schema_version": 1, "entries": [good]}))
216
            with self.assertRaises(ValueError):
217
                gate.load_expected(path)
218
219
    def test_catch_all_patterns_are_rejected(self):
220
        broad = [".*", ".+", "(.*)", "(?:.*)(?:.*)", "[\\s\\S]*", ".*?", "\\S+.*", "x|.*", ".*error.*",
221
                 ".*panicked.*", ".*corrupt.*", "[a-z ]+", "(panicked|corrupt|.*)", "zzz-sentinel",
222
                 "[0-9a-f]{64}", "\\w{3,}\\d", "[^\\n]{3,}\\.\\s*", "a?b?c?.*", "(error)?.+",
223
                 # Oak/git-shaped catch-alls (leg detail shape "oak <cmd> exited N: error: ...")
224
                 ".*oak.*", "(?:oak|git) .*", "oak .* exited \\d+: .*", ".*error: .*", "oak \\w+ .*",
225
                 "oak (commit|push|pull|clone) .*exited \\d+: error: .*", "command .* failed \\(\\d+\\): .*",
226
                 "oak push --repo exited \\d+: .*", "(?:oak|git) \\S+( \\S+)* exited [0-9]+: (?:error|fatal): .+"]
227
        for pattern in broad:
228
            self.assertIsNotNone(gate.pattern_too_broad(pattern), pattern)
229
        with tempfile.TemporaryDirectory() as tmp:
230
            path = Path(tmp) / "x.json"
231
            for pattern in broad:
232
                entry = {"behaviour": "a", "ticket": "fb-1", "failed_legs": {"clone_fs": pattern}}
233
                path.write_text(json.dumps({"schema_version": 2, "entries": [entry]}))
234
                with self.assertRaisesRegex(ValueError, "catch-all", msg=pattern):
235
                    gate.load_expected(path)
236
            entry = {"behaviour": "a", "ticket": "fb-1", "failed_legs": {"clone_fs": "(unclosed"}}
237
            path.write_text(json.dumps({"schema_version": 2, "entries": [entry]}))
238
            with self.assertRaisesRegex(ValueError, "invalid"):
239
                gate.load_expected(path)
240
241
    def test_specific_patterns_are_accepted(self):
242
        specific = ["oak pull exited 1: error: IO error: File exists \\(os error 17\\)",
243
                    "missing=\\['file\\.txt'\\]", "exit(ed)? \\d+", "(foo|bar) failed at \\d+"]
244
        for pattern in specific:
245
            self.assertIsNone(gate.pattern_too_broad(pattern), pattern)
246
        # every shipped entry is specific
247
        for entry in gate.load_expected()["entries"].values():
248
            for pattern in entry["patterns"].values():
249
                self.assertIsNone(gate.pattern_too_broad(pattern.pattern), pattern.pattern)
250
251
252
class WiringTests(unittest.TestCase):
253
    def setUp(self):
254
        self.expected = gate.load_expected()
255
256
    def test_devloop_lane_is_default_and_maps_verdicts(self):
257
        self.assertIn("conformance", devloop.LANES)
258
        regressions, notes = devloop.conformance_checks(healthy_rows(self.expected))
259
        self.assertEqual(regressions, [])
260
        self.assertFalse(any(n.startswith("UNMEASURED") for n in notes))
261
        rows = replace(healthy_rows(self.expected), conformance_row("eol_lf_v1", 1, {"pull_fs": "x"}))
262
        regressions, notes = devloop.conformance_checks(rows)
263
        self.assertTrue(regressions and regressions[0].startswith("[conformance] CONFORMANCE eol_lf_v1"))
264
        rows = [r for r in healthy_rows(self.expected) if r["scenario"] != "eol_lf_v1"]
265
        regressions, notes = devloop.conformance_checks(rows)
266
        self.assertTrue(any(n.startswith("UNMEASURED conformance") for n in notes))
267
268
    def gate_output(self, rows):
269
        with tempfile.TemporaryDirectory() as tmp:
270
            path = Path(tmp) / "rows.jsonl"
271
            path.write_text("".join(json.dumps(r) + "\n" for r in rows))
272
            with contextlib.redirect_stdout(io.StringIO()) as out:
273
                code = publish_gate.main([str(path)])
274
            return code, out.getvalue()
275
276
    def oak_row(self, sha=SHA, **extra):
277
        return {"subject": "oak_installed", "binary_sha256": sha, "returncode": 0,
278
                "source": {"oak_hash": "abc", "oak_status": ""}, **extra}
279
280
    def test_requires_conformance_for_oak_rows(self):
281
        code, out = self.gate_output([self.oak_row()])
282
        self.assertIn("FAIL   conformance: Oak rows published without", out)
283
        code, out = self.gate_output([self.oak_row(), *healthy_rows(self.expected)])
284
        self.assertIn("PASS   [bbbbbbbbbbbb] conformance (oak, darwin-ci-ni): PASS", out)
285
        self.assertNotIn("FAIL   conformance", out)
286
287
    def test_oak_rows_without_binary_identity_fail(self):
288
        anonymous = self.oak_row()
289
        anonymous.pop("binary_sha256")
290
        code, out = self.gate_output([anonymous, *healthy_rows(self.expected)])
291
        self.assertIn("Oak rows lack binary identity", out)
292
        via_details = self.oak_row(subject_details={"oak_installed": {"binary_sha256": SHA}})
293
        via_details.pop("binary_sha256")
294
        code, out = self.gate_output([via_details, *healthy_rows(self.expected)])
295
        self.assertNotIn("lack binary identity", out)
296
297
    def test_rows_of_another_binary_never_satisfy_or_mix(self):
298
        other = "c" * 64
299
        code, out = self.gate_output([self.oak_row(sha=other), *healthy_rows(self.expected)])
300
        self.assertIn(f"conformance[{other[:12]}]: UNMEASURED", out)
301
        # A good binary's rows cannot mask a regressed binary's row, and vice versa.
302
        mixed = healthy_rows(self.expected) + replace(healthy_rows(self.expected, sha=other),
303
                                                      conformance_row("eol_lf_v1", 1, {"pull_fs": "x"}, sha=other))
304
        code, out = self.gate_output([self.oak_row(), self.oak_row(sha=other), *mixed])
305
        self.assertIn(f"conformance[{other[:12]}]: REGRESSED", out)
306
        self.assertNotIn(f"conformance[{SHA[:12]}]: REGRESSED", out)
307
308
    def test_publish_requires_a_recorded_profile(self):
309
        linux = [dict(row, fs_profile="linux-cs-ns") for row in healthy_rows(self.expected, profile="linux-cs-ns")]
310
        code, out = self.gate_output([self.oak_row(), *linux])
311
        self.assertEqual(code, 1)
312
        self.assertIn("has no recorded coverage", out)
313
        skipped = [conformance_row(b, 77, None, profile="linux-cs-ns") for b in ALL]
314
        code, out = self.gate_output([self.oak_row(), *skipped])
315
        self.assertIn("UNMEASURED", out)
316
        self.assertNotIn("PASS   [", out)
317
        missing = healthy_rows(self.expected)
318
        for row in missing:
319
            row.pop("fs_profile")
320
        code, out = self.gate_output([self.oak_row(), *missing])
321
        self.assertIn("FAIL   conformance[", out)
322
        self.assertNotIn("PASS   [", out)
323
324
    def test_pending_tickets_refused_by_publish_but_only_noted_by_devloop(self):
325
        base = gate.load_expected()
326
        for outcome in ("intermittent", "bug", "refused"):
327
            entries = dict(base["entries"])
328
            entries["large_file_chunked_v1"] = dict(entries["large_file_chunked_v1"], ticket="pending:db-locked",
329
                                                    outcome=outcome)
330
            expected = dict(base, entries=entries)
331
            self.assertEqual(gate.pending_tickets(expected), [("large_file_chunked_v1", "pending:db-locked", outcome)])
332
            with mock.patch.object(gate, "load_expected", return_value=expected):
333
                code, out = self.gate_output([self.oak_row(), *healthy_rows(base)])
334
                self.assertEqual(code, 1, outcome)
335
                self.assertIn("unfiled pending: tickets", out)
336
                self.assertIn("large_file_chunked_v1 (pending:db-locked", out)
337
                regressions, notes = devloop.conformance_checks(healthy_rows(base))
338
            # devloop still runs and only notes the placeholder
339
            self.assertEqual(regressions, [])
340
        self.assertEqual(gate.pending_tickets(base), [])
341
342
    def test_stale_list_fails(self):
343
        rows = replace(healthy_rows(self.expected), conformance_row("path_tab_v1", 0))
344
        code, out = self.gate_output([self.oak_row(), *rows])
345
        self.assertIn("expected-failures list is stale", out)
346
347
348
if __name__ == "__main__":
349
    unittest.main()