Viewing files at commit c44c91118a6cCommit detailsView latest

Browse files
tests/test_scale_suite.py 556 lines · 30.2 KB · python
1
"""Instrument tests for suites/scale (generator, sizer, tiers, op oracles, rows).
2
3
Hermetic: no Oak binary and no network. Git-backed tests use the resolved
4
real git and skip when it is absent. Oak-side oracles are exercised with
5
canned subject output (a stub timed runner), so every op's untimed oracle is
6
tested for both a passing and a failing observation.
7
"""
8
9
from __future__ import annotations
10
11
import hashlib
12
import io
13
import json
14
import re
15
import shutil
16
import subprocess
17
import sys
18
import tempfile
19
import unittest
20
from pathlib import Path
21
from types import SimpleNamespace
22
23
ROOT = Path(__file__).resolve().parents[1]
24
sys.path.insert(0, str(ROOT / "suites" / "scale"))
25
sys.path.insert(0, str(ROOT / "scripts"))
26
27
from oakbench.execution import TimedCapture, run_timed  # noqa: E402
28
from oakbench.rows import validate_rows  # noqa: E402
29
from scalesuite import generator as gen  # noqa: E402
30
from scalesuite import ops as ops_mod  # noqa: E402
31
from scalesuite import runner as runner_mod  # noqa: E402
32
from scalesuite import sizer as sz  # noqa: E402
33
from scalesuite import tiers as tiers_mod  # noqa: E402
34
from scalesuite import workspace as ws  # noqa: E402
35
36
try:
37
    from oakbench.subjects import which_measured_git
38
    GIT = which_measured_git()
39
except Exception:  # pragma: no cover
40
    GIT = shutil.which("git")
41
42
SMALL = gen.GenSpec(name="t", seed="unit-seed", files=60, file_bytes=96, depth=4, fanout=4, commits=30,
43
                    touches_per_commit=3, branches=4, branch_commit_files=2, tags=3, binaries=1,
44
                    binary_bytes=(2 << 20) + 17, binary_revisions=2, ignore_patterns=("build/",),
45
                    conflict_files=3)
46
47
48
def stream_sha(spec: gen.GenSpec) -> str:
49
    buffer = io.BytesIO()
50
    gen.write_stream(spec, buffer)
51
    return hashlib.sha256(buffer.getvalue()).hexdigest()
52
53
54
def git(*args: str, cwd: Path) -> str:
55
    return subprocess.run([GIT, *args], cwd=cwd, check=True, capture_output=True, text=True,
56
                          env=gen.git_env()).stdout
57
58
59
class GeneratorDeterminismTest(unittest.TestCase):
60
    def test_same_spec_same_stream(self) -> None:
61
        self.assertEqual(stream_sha(SMALL), stream_sha(SMALL))
62
63
    def test_seed_changes_stream_and_spec_hash(self) -> None:
64
        other = gen.GenSpec(**{**SMALL.to_json(), "seed": "other-seed", "ignore_patterns": ("build/",)})
65
        self.assertNotEqual(stream_sha(SMALL), stream_sha(other))
66
        self.assertNotEqual(SMALL.spec_sha256(), other.spec_sha256())
67
68
    def test_spec_hash_is_stable_text(self) -> None:
69
        self.assertEqual(SMALL.spec_sha256(), gen.GenSpec(**{**SMALL.to_json(), "ignore_patterns": ("build/",)}).spec_sha256())
70
        self.assertRegex(SMALL.spec_sha256(), r"^[0-9a-f]{64}$")
71
72
    def test_validation(self) -> None:
73
        with self.assertRaises(ValueError):
74
            gen.GenSpec(name="x", seed="s", files=0).validate()
75
        with self.assertRaises(ValueError):
76
            gen.GenSpec(name="x", seed="s", binary_revisions=2, commits=3).validate()
77
        with self.assertRaises(ValueError):
78
            gen.GenSpec(name="x", seed="s", file_bytes=8).validate()
79
80
    def test_every_path_has_exact_depth_and_is_unique(self) -> None:
81
        for spec in (SMALL, gen.GenSpec(name="d", seed="s", files=5000, depth=40, fanout=8),
82
                     gen.GenSpec(name="w", seed="s", files=5000, depth=2, fanout=10),
83
                     gen.GenSpec(name="r", seed="s", files=7, depth=1, fanout=4)):
84
            cache = gen.layout(spec)
85
            paths = [gen.file_path(spec, i, cache) for i in range(spec.files)]
86
            self.assertEqual(len(set(paths)), spec.files)
87
            self.assertTrue(all(p.count("/") + 1 == spec.depth for p in paths), spec)
88
89
    def test_content_unique_sized_and_deterministic(self) -> None:
90
        factory = gen.ContentFactory(SMALL)
91
        blobs = {factory.text(i, "r1") for i in range(SMALL.files)}
92
        self.assertEqual(len(blobs), SMALL.files)
93
        self.assertTrue(all(len(b) == SMALL.file_bytes for b in blobs))
94
        self.assertEqual(factory.text(3, "r9"), gen.ContentFactory(SMALL).text(3, "r9"))
95
        self.assertNotEqual(factory.text(3, "r9"), factory.text(3, "r8"))
96
97
    def test_binary_is_sparse_friendly_and_exact_size(self) -> None:
98
        factory = gen.ContentFactory(SMALL)
99
        data = b"".join(factory.binary_segments(0, 1))
100
        self.assertEqual(len(data), SMALL.binary_bytes)
101
        self.assertEqual(data[gen.SEGMENT_RANDOM:gen.SEGMENT], bytes(gen.SEGMENT - gen.SEGMENT_RANDOM))
102
        self.assertNotEqual(data[:gen.SEGMENT_RANDOM], bytes(gen.SEGMENT_RANDOM))
103
        sha1, _ = gen.binary_sha1_and_sha256(factory, 0, 1)
104
        self.assertEqual(sha1, gen.git_blob_id(data))
105
106
    def test_branch_changes_match_stream_facts(self) -> None:
107
        buffer = io.BytesIO()
108
        facts = gen.write_stream(SMALL, buffer)
109
        self.assertEqual(facts["branch_sample"]["changed_paths"],
110
                         sorted(path for _, path in gen.branch_changes(SMALL, 0)))
111
        self.assertEqual(facts["total_commits"], SMALL.commits + SMALL.branches + 2)
112
113
114
@unittest.skipUnless(GIT, "git not available")
115
class GeneratedFixtureTest(unittest.TestCase):
116
    @classmethod
117
    def setUpClass(cls) -> None:
118
        cls.tmp = Path(tempfile.mkdtemp(prefix="scale-gen-"))
119
        cls.manifest = gen.generate(SMALL, cls.tmp / "a", GIT)
120
        cls.repo = cls.tmp / "a" / "src.git"
121
122
    @classmethod
123
    def tearDownClass(cls) -> None:
124
        shutil.rmtree(cls.tmp, ignore_errors=True)
125
126
    def test_same_seed_same_fixture_hash(self) -> None:
127
        again = gen.generate(SMALL, self.tmp / "b", GIT)
128
        self.assertEqual(again["refs_sha256"], self.manifest["refs_sha256"])
129
        self.assertEqual(again["head_commit"], self.manifest["head_commit"])
130
131
    def test_reuse_is_detected(self) -> None:
132
        self.assertTrue(gen.generate(SMALL, self.tmp / "a", GIT)["reused"])
133
134
    def test_manifest_facts_agree_with_git(self) -> None:
135
        probe = self.manifest["probe_path"]
136
        log = git("log", "--oneline", "main", "--", probe, cwd=self.repo).splitlines()
137
        self.assertEqual(len(log), self.manifest["probe_path_commit_count"])
138
        self.assertEqual(int(git("rev-list", "--count", "main", cwd=self.repo)), SMALL.commits)
139
        changed = git("diff", "--name-only", "main...b00000", cwd=self.repo).split()
140
        self.assertEqual(sorted(changed), self.manifest["branch_sample"]["changed_paths"])
141
        self.assertEqual(len(git("tag", "--list", cwd=self.repo).split()), SMALL.tags)
142
        conflicts = subprocess.run([GIT, "merge-tree", "--write-tree", "--name-only", "bench-ours", "bench-theirs"],
143
                                   cwd=self.repo, capture_output=True, text=True)
144
        self.assertEqual(conflicts.returncode, 1)
145
        self.assertEqual(len(conflicts.stdout.split("\n\n")[0].splitlines()[1:]), SMALL.conflict_files)
146
147
    def test_binary_blob_id_matches_generator(self) -> None:
148
        blob = git("rev-parse", f"b00000:{gen.binary_path(0)}", cwd=self.repo).strip()
149
        # branch b00000 forks from the tip, where the binary is at revision binary_revisions
150
        sha1, _ = gen.binary_sha1_and_sha256(gen.ContentFactory(SMALL), 0, SMALL.binary_revisions)
151
        self.assertEqual(blob, sha1)
152
153
    def test_sizer_values(self) -> None:
154
        stats = sz.measure(self.repo, GIT)
155
        values = stats["values"]
156
        self.assertEqual(stats["checkout_scope"], "all_commits")
157
        self.assertEqual(values["uniqueCommitCount"], self.manifest["total_commits"])
158
        self.assertEqual(values["maxCheckoutPathDepth"], SMALL.depth)
159
        self.assertEqual(values["maxCheckoutBlobCount"], SMALL.files + SMALL.binaries + 1)  # + .gitignore
160
        self.assertEqual(values["referenceCount"], 1 + SMALL.branches + 2 + SMALL.tags)
161
        self.assertEqual(values["maxBlobSize"], SMALL.binary_bytes)
162
        self.assertEqual(values["maxHistoryDepth"], SMALL.commits + 1)
163
        self.assertEqual(values["maxCommitParentCount"], 1)
164
        self.assertEqual(values["uniqueTagCount"], 0)
165
        longest = max(len(p) for p in git("ls-tree", "-r", "--name-only", "main", cwd=self.repo).split())
166
        self.assertEqual(values["maxCheckoutPathLength"], longest)
167
        self.assertEqual(stats["level_of_concern"]["maxCheckoutPathDepth"],
168
                         round(SMALL.depth / 10, 4))
169
        compact = sz.compact(stats)
170
        self.assertEqual(compact["maxCheckoutPathDepth"], SMALL.depth)
171
172
    def test_sizer_ref_tips_scope_and_tag_depth(self) -> None:
173
        clone = self.tmp / "tags.git"
174
        subprocess.run([GIT, "clone", "-q", "--bare", str(self.repo), str(clone)], check=True, env=gen.git_env())
175
        env = {**gen.git_env(), "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
176
        subprocess.run([GIT, "-C", str(clone), "tag", "-a", "-m", "one", "a1", "main"], check=True, env=env)
177
        subprocess.run([GIT, "-C", str(clone), "tag", "-a", "-m", "two", "a2", "a1"], check=True, env=env)
178
        stats = sz.measure(clone, GIT, max_checkout_roots=2)
179
        self.assertEqual(stats["checkout_scope"], "ref_tips")
180
        self.assertEqual(stats["values"]["maxTagDepth"], 2)
181
        self.assertEqual(stats["values"]["uniqueTagCount"], 2)
182
        self.assertIsNone(stats["values"]["uniqueTreeEntries"])
183
184
185
@unittest.skipUnless(GIT, "git not available")
186
class RegistryPinTest(unittest.TestCase):
187
    """Cross-machine determinism: regenerating a tier reproduces the pinned identity."""
188
189
    def test_pinned_tiers_regenerate_identically(self) -> None:
190
        registry = json.loads((ROOT / "config" / "fixtures.json").read_text())["fixtures"]
191
        tmp = Path(tempfile.mkdtemp(prefix="scale-pin-"))
192
        try:
193
            checked = 0
194
            for name in ("fresh", "default"):
195
                entry = registry[f"scale-{name}-v1"]
196
                tier = tiers_mod.TIERS[name]
197
                self.assertEqual(entry["spec_sha256"], tier.spec.spec_sha256())
198
                self.assertEqual(entry["seed"], tier.spec.seed)
199
                manifest = gen.generate(tier.spec, tmp / name, GIT)
200
                self.assertEqual(manifest["refs_sha256"], entry["refs_sha256"], name)
201
                self.assertEqual(manifest["head_commit"], entry["head_commit"], name)
202
                checked += 1
203
            self.assertEqual(checked, 2)
204
        finally:
205
            shutil.rmtree(tmp, ignore_errors=True)
206
207
    def test_every_tier_is_registered_with_its_spec_hash(self) -> None:
208
        registry = json.loads((ROOT / "config" / "fixtures.json").read_text())["fixtures"]
209
        for name, tier in tiers_mod.TIERS.items():
210
            self.assertEqual(registry[f"scale-{name}-v1"]["spec_sha256"], tier.spec.spec_sha256(), name)
211
212
213
class SizerPureTest(unittest.TestCase):
214
    def test_parse_tree(self) -> None:
215
        oid = bytes(range(20))
216
        raw = b"100644 a.txt\0" + oid + b"40000 dir\0" + oid[::-1]
217
        self.assertEqual(sz.parse_tree(raw), [(b"100644", b"a.txt", oid), (b"40000", b"dir", oid[::-1])])
218
219
    def test_level_of_concern_uses_git_sizer_reference_values(self) -> None:
220
        self.assertEqual(sz.level_of_concern("maxCheckoutBlobCount", 1_000_000), 20.0)
221
        self.assertEqual(sz.level_of_concern("referenceCount", 25_000), 1.0)
222
        self.assertIsNone(sz.level_of_concern("maxBlobSize", None))
223
224
225
class TierTest(unittest.TestCase):
226
    def test_all_14_core_shapes_are_runnable_specs(self) -> None:
227
        shapes = tiers_mod.core_shape_tiers(0.05)
228
        self.assertEqual(len(shapes), 14)
229
        for shape_id, tier in shapes.items():
230
            tier.spec.validate()
231
            self.assertGreaterEqual(tier.spec.branches, 1, shape_id)
232
            self.assertEqual(tier.kind, "shape")
233
        self.assertEqual(shapes["deep-tree"].spec.depth, 64)
234
        self.assertEqual(shapes["conflict-heavy"].spec.conflict_files, 250)
235
        self.assertEqual(shapes["binary-history"].spec.binary_revisions, shapes["binary-history"].spec.commits)
236
        self.assertIn("build/", shapes["ignored-heavy"].spec.ignore_patterns)
237
238
    def test_scale_is_part_of_identity(self) -> None:
239
        a = tiers_mod.core_shape_tiers(0.05)["normal"]
240
        b = tiers_mod.core_shape_tiers(0.1)["normal"]
241
        self.assertNotEqual(a.fixture_id, b.fixture_id)
242
243
    def test_tier_ids_unique_and_xl_opt_in(self) -> None:
244
        ids = [tier.fixture_id for tier in tiers_mod.TIERS.values()]
245
        self.assertEqual(len(ids), len(set(ids)))
246
        with self.assertRaises(SystemExit):
247
            tiers_mod.resolve("xl-tree")
248
        self.assertEqual(tiers_mod.resolve("xl-tree", allow_xl=True).spec.files, 1_000_000)
249
        with self.assertRaises(SystemExit):
250
            tiers_mod.resolve("xl-combined", allow_xl=True)
251
        combined = tiers_mod.resolve("xl-combined", allow_xl=True, allow_xl_combined=True).spec
252
        self.assertGreaterEqual(combined.commits, 100_000)
253
        self.assertGreaterEqual(combined.files, 1_000_000)
254
        self.assertGreaterEqual(combined.depth, 32)
255
        self.assertGreaterEqual(combined.branches, 10_000)
256
        self.assertGreaterEqual(combined.binary_bytes, 2 << 30)
257
        for tier in tiers_mod.TIERS.values():
258
            tier.spec.validate()
259
260
261
class CatalogTest(unittest.TestCase):
262
    def test_scale_yaml_matches_catalog(self) -> None:
263
        self.assertEqual((ROOT / "scenarios" / "scale.yaml").read_text(), ops_mod.catalog_yaml())
264
265
    def test_tperf_refs_are_file_names_only(self) -> None:
266
        pattern = re.compile(r"^p\d{4}-[a-z0-9-]+\.sh$")
267
        for info in ops_mod.OP_INFO.values():
268
            for ref in info.tperf:
269
                self.assertRegex(ref, pattern)
270
271
    def test_units_cover_ops(self) -> None:
272
        self.assertEqual(sorted(ops_mod.ALL_OPS), sorted(ops_mod.OPS))
273
274
    def test_porcelain_paths(self) -> None:
275
        text = ' M a/b.txt\n D c.txt\nR  old.txt -> new.txt\n?? added/x/\nM "sp ace.txt"\n'
276
        self.assertEqual(ops_mod.porcelain_paths(text),
277
                         ["a/b.txt", "c.txt", "old.txt", "new.txt", "added/x/", "sp ace.txt"])
278
279
280
# ---------------------------------------------------------------------------
281
# Oracle tests with canned subject output
282
# ---------------------------------------------------------------------------
283
284
285
def cap(stdout: str = "", returncode: int = 0, raw: bytes | None = None) -> TimedCapture:
286
    data = stdout.encode()
287
    return TimedCapture(elapsed_ms=1.0, returncode=returncode, stdout_text=stdout[:4000], stdout_bytes=len(data),
288
                        stdout_truncated=False, stderr_text="", stderr_bytes=0, stderr_truncated=False,
289
                        stdout_full_text=stdout, stdout_raw=raw if raw is not None else data)
290
291
292
class OracleTest(unittest.TestCase):
293
    def setUp(self) -> None:
294
        self.tmp = Path(tempfile.mkdtemp(prefix="scale-oracle-"))
295
        self.spec = gen.GenSpec(name="o", seed="oracle", files=40, file_bytes=64, depth=3, fanout=4, commits=12,
296
                                branches=2, branch_commit_files=2)
297
        facts = gen.write_stream(self.spec, io.BytesIO())
298
        factory = gen.ContentFactory(self.spec)
299
        for kind in ("git", "oak"):
300
            work = self.tmp / kind / "work"
301
            for i in range(self.spec.files):
302
                path = work / gen.file_path(self.spec, i)
303
                path.parent.mkdir(parents=True, exist_ok=True)
304
                path.write_bytes(factory.text(i, "r1"))
305
        branch_paths = facts["branch_sample"]["changed_paths"]
306
        self.expected = ws.Expected(
307
            main_file_count=self.spec.files, sample={}, probe_path=facts["probe_path"], probe_bytes=b"probe\nx\n",
308
            probe_lines=2, probe_commit_count=facts["probe_path_commit_count"], main_commits=self.spec.commits,
309
            branch="b00000", branch_paths=branch_paths, branch_bytes={}, main_bytes_of_branch_paths={},
310
            conflict={}, protected=[facts["probe_path"], *branch_paths], all_paths_sample_size=0)
311
        tier = tiers_mod.Tier(tier_id="t", kind="tier", spec=self.spec, dirty=tiers_mod.DirtyShape(3, 2, 1, 1))
312
        root = self.tmp
313
        self.prepared = SimpleNamespace(tier=tier, expected=self.expected, state={}, env={}, git_bin=GIT or "git",
314
                                        oak_base_branch="base", oak_branches_created=2,
315
                                        path=lambda *parts: root.joinpath(*parts))
316
        self.untimed_calls: list = []
317
318
    def tearDown(self) -> None:
319
        shutil.rmtree(self.tmp, ignore_errors=True)
320
321
    def ctx(self, kind: str, *outputs, run: int = 1) -> ops_mod.Ctx:
322
        queue = list(outputs)
323
        subject = SimpleNamespace(name=kind, kind=kind, bin=kind)
324
325
        def fake_runner(command, cwd, env, admitted, **kwargs):
326
            item = queue.pop(0)
327
            return item() if callable(item) else item
328
329
        ctx = ops_mod.Ctx(self.prepared, subject, run, fake_runner, self.tmp / "scratch", 60.0)
330
        ctx.untimed = lambda args, cwd, check=True, binary=None: (  # type: ignore[assignment]
331
            self.untimed_calls.append(list(args)) or SimpleNamespace(returncode=0, stdout=b"", stderr=b""))
332
        ctx.git = lambda args, cwd, check=True: (  # type: ignore[assignment]
333
            self.untimed_calls.append(["git", *args]) or SimpleNamespace(returncode=0, stdout=b"", stderr=b""))
334
        return ctx
335
336
    def run_op(self, op: str, kind: str, *outputs: TimedCapture) -> ops_mod.Outcome:
337
        return ops_mod.execute(self.ctx(kind, *outputs), op)
338
339
    def test_status_clean(self) -> None:
340
        self.assertTrue(self.run_op("status.clean", "git", cap("")).oracle_ok)
341
        self.assertFalse(self.run_op("status.clean", "oak", cap("M a.txt\n")).oracle_ok)
342
343
    def test_log_counts(self) -> None:
344
        lines = "".join(f"h{i} c{i}\n" for i in range(self.spec.commits))
345
        self.assertTrue(self.run_op("log.full", "git", cap(lines)).oracle_ok)
346
        self.assertFalse(self.run_op("log.full", "oak", cap(lines + "extra\n")).oracle_ok)
347
        self.assertTrue(self.run_op("log.n", "oak", cap(lines)).oracle_ok)
348
        self.assertFalse(self.run_op("log.n", "oak", cap("h1 c1\n")).oracle_ok)
349
        probe = "".join(f"p{i}\n" for i in range(self.expected.probe_commit_count))
350
        self.assertTrue(self.run_op("log.path", "git", cap(probe)).oracle_ok)
351
        self.assertFalse(self.run_op("log.path", "oak", cap("No matching commits\n")).oracle_ok)
352
353
    def test_blame(self) -> None:
354
        self.assertTrue(self.run_op("blame", "git", cap("a 1) probe\nb 2) x\n")).oracle_ok)
355
        self.assertFalse(self.run_op("blame", "git", cap("a 1) probe\n")).oracle_ok)
356
        self.assertEqual(self.run_op("blame", "oak").skip_reason, "oak_cli_has_no_blame_or_annotate_command")
357
358
    def test_file_show(self) -> None:
359
        self.assertTrue(self.run_op("file.show", "git", cap(raw=b"probe\nx\n")).oracle_ok)
360
        self.assertFalse(self.run_op("file.show", "git", cap(raw=b"other\n")).oracle_ok)
361
        # oak writes --output; nothing written -> mismatch
362
        self.assertFalse(self.run_op("file.show", "oak", cap("{}")).oracle_ok)
363
364
    def test_branch_list(self) -> None:
365
        self.assertTrue(self.run_op("branch.list", "git", cap("* main\n  b00000\n  b00001\n")).oracle_ok)
366
        self.assertFalse(self.run_op("branch.list", "git", cap("* main\n")).oracle_ok)
367
        listing = json.dumps([{"name": "main"}, {"name": "b00000"}, {"name": "b00001"}])
368
        self.assertTrue(self.run_op("branch.list", "oak", cap(listing)).oracle_ok)
369
        self.assertFalse(self.run_op("branch.list", "oak", cap("not json")).oracle_ok)
370
371
    def test_review_branch_diff(self) -> None:
372
        paths = self.expected.branch_paths
373
        self.assertTrue(self.run_op("review.branch_diff", "git",
374
                                    cap("".join(f"M\t{p}\n" for p in paths))).oracle_ok)
375
        oak_ok = json.dumps({"changed_files": [{"path": p} for p in paths]})
376
        self.assertTrue(self.run_op("review.branch_diff", "oak", cap(oak_ok)).oracle_ok)
377
        self.assertFalse(self.run_op("review.branch_diff", "oak", cap(json.dumps({"changed_files": []}))).oracle_ok)
378
379
    def test_merge_preview_oak(self) -> None:
380
        good = json.dumps({"clean": True, "changed_files": [{"path": p} for p in self.expected.branch_paths],
381
                           "merge_safety": {"certified": False}})
382
        outcome = self.run_op("merge.preview", "oak", cap(good, returncode=7))
383
        self.assertTrue(outcome.oracle_ok)
384
        self.assertFalse(outcome.extra["merge_certified"])
385
        self.assertIn(["switch", "b00000"], self.untimed_calls)
386
        self.assertEqual(self.untimed_calls[-1], ["switch", "base"])  # always switched back
387
        bad = json.dumps({"clean": False, "changed_files": []})
388
        self.assertFalse(self.run_op("merge.preview", "oak", cap(bad)).oracle_ok)
389
        self.assertIsNotNone(self.run_op("merge.preview", "oak", cap(good, returncode=2)).failed)
390
391
    def test_local_merge_ops_skip_for_oak(self) -> None:
392
        self.assertIn("oak_serve_lacks_merge_to_main", self.run_op("merge.local", "oak").skip_reason)
393
        self.assertEqual(self.run_op("merge.conflict", "git").skip_reason, "fixture_has_no_conflict_shape")
394
395
    def _listing(self, kind: str = "git") -> str:
396
        state = self.prepared.state[kind]["dirty"]
397
        text = "".join(f" M {p}\n" for p in state["modified"]) + "".join(f" D {p}\n" for p in state["deleted"])
398
        return text + "".join(f" D {old}\n" for old, _ in state["renamed"]) + "?? added/\n?? renamed/\n"
399
400
    def test_dirty_cycle_status_and_diff_oracles(self) -> None:
401
        good = ops_mod.execute(self.ctx("git", lambda: cap(self._listing())), "status.dirty")
402
        self.assertTrue(good.oracle_ok, good.oracle_detail)
403
        dirty = self.prepared.state["git"]["dirty"]
404
        self.assertEqual(dirty["applied"], [3, 2, 1, 1])
405
        self.assertFalse(set(dirty["modified"]) & set(self.expected.protected))
406
        diff = "".join(f"diff --git a/{p} b/{p}\n" for p in dirty["modified"] + dirty["deleted"])
407
        self.assertTrue(ops_mod.execute(self.ctx("git", cap(diff)), "diff.dirty").oracle_ok)
408
        self.assertFalse(ops_mod.execute(self.ctx("git", cap("diff --git a/x b/x\n")), "diff.dirty").oracle_ok)
409
        partial = ops_mod.execute(self.ctx("oak", lambda: cap(self._listing().split("\n", 1)[1]), run=2),
410
                                  "status.dirty")
411
        self.assertFalse(partial.oracle_ok)
412
413
    def test_dirty_cycle_runs_on_throwaway_branch(self) -> None:
414
        ops_mod.execute(self.ctx("oak", lambda: cap(self._listing("oak"))), "status.dirty")
415
        self.assertIn(["switch", "-c", "dirty-oak-r1"], self.untimed_calls)
416
        self.untimed_calls.clear()
417
        out = ops_mod.execute(self.ctx("oak", cap("{}")), "commit.dirty")
418
        self.assertIsNone(out.failed, out.failed)
419
        self.assertEqual(self.untimed_calls[-1], ["switch", "base"])
420
        self.assertEqual(self.prepared.state["oak"]["harness_commits"], 0)
421
422
    def test_recover_returns_to_base(self) -> None:
423
        ctx = self.ctx("oak")
424
        self.assertIsNone(ops_mod.recover(ctx))
425
        self.assertEqual(self.untimed_calls[-1], ["switch", "base"])
426
427
    def test_oak_state_oracles_use_independent_reader(self) -> None:
428
        from unittest import mock
429
        from oakbench import oak_sqlite_reader as reader
430
        probe = self.expected.probe_path
431
        self.prepared.state["oak"] = {"harness_commits": 0, "deleted": set(), "server_extra": 0, "branch_seq": 0}
432
        with mock.patch.object(reader, "committed_payloads",
433
                               return_value=({probe: self.expected.probe_bytes}, {"head": "h", "reader_version": "v"})):
434
            outcome = ops_mod.execute(self.ctx("oak", cap("")), "gc")
435
        self.assertEqual(outcome.oracle_reader, "oak_sqlite_reader_independent")
436
        self.assertTrue(outcome.oracle_ok, outcome.oracle_detail)
437
        with mock.patch.object(reader, "committed_payloads", return_value=({probe: b"torn"}, {"head": "h"})):
438
            self.assertFalse(ops_mod.execute(self.ctx("oak", cap("")), "gc").oracle_ok)
439
        with mock.patch.object(reader, "committed_payloads", side_effect=reader.Unavailable("database too big")):
440
            unmeasured = ops_mod.execute(self.ctx("oak", cap("")), "gc")
441
        self.assertIsNone(unmeasured.oracle_ok)
442
        self.assertIsNone(unmeasured.failed)
443
        self.assertIn("reader_unavailable", unmeasured.oracle_detail)
444
445
    def test_commit_needs_dirty_state(self) -> None:
446
        self.assertIn("needs status.dirty", self.run_op("commit.dirty", "git").failed)
447
448
    def test_timed_failure_is_recorded_not_raised(self) -> None:
449
        outcome = self.run_op("status.clean", "oak", cap("", returncode=3))
450
        self.assertIn("exit 3", outcome.failed)
451
        self.assertIsNone(outcome.oracle_ok)
452
453
    def test_check_worktree(self) -> None:
454
        root = self.tmp / "git" / "work"
455
        path = gen.file_path(self.spec, 5)
456
        self.expected.sample[path] = gen.git_blob_id((root / path).read_bytes())
457
        ctx = self.ctx("git")
458
        ok, detail = ops_mod.check_worktree(ctx, root, self.spec.files)
459
        self.assertTrue(ok, detail)
460
        (root / path).write_bytes(b"changed")
461
        ok, detail = ops_mod.check_worktree(ctx, root, self.spec.files)
462
        self.assertFalse(ok)
463
        self.assertEqual(detail["sample_mismatch_count"], 1)
464
465
466
class RowTest(unittest.TestCase):
467
    def prepared(self) -> SimpleNamespace:
468
        tier = tiers_mod.TIERS["fresh"]
469
        fields = {"tier": "fresh", "tier_kind": "tier", "fixture_id": tier.fixture_id,
470
                  "fixture_manifest_sha256": "a" * 64, "fixture_spec_sha256": "b" * 64}
471
        return SimpleNamespace(tier=tier, fixture_fields=lambda: fields, phases={"ingest.oak_import": 1.5},
472
                               notes=[], manifest={"refs_sha256": "a" * 64, "spec_sha256": "b" * 64})
473
474
    def row(self, subject: str, kind: str, op: str, run: int, outcome: ops_mod.Outcome) -> dict:
475
        return runner_mod.build_row(bench_id="b", prepared=self.prepared(),
476
                                    subject=SimpleNamespace(name=subject, kind=kind), op=op, run=run,
477
                                    outcome=outcome, details={}, env_sample={}, lock_wait_ms=0.0,
478
                                    order_position=0, runner_fields={})
479
480
    def test_rows_validate_for_all_outcomes(self) -> None:
481
        ok = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["git", "status"], cap(""))], oracle_ok=True)
482
        skip = ops_mod.Outcome(skip_reason="no_equivalent")
483
        fail = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["oak", "status"], cap("", 2))], failed="exit 2")
484
        bad = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["oak", "log"], cap(""))], oracle_ok=False)
485
        rows = [self.row("git", "git", "status.clean", 1, ok), self.row("oak_installed", "oak", "blame", 1, skip),
486
                self.row("oak_installed", "oak", "status.clean", 1, fail),
487
                self.row("oak_installed", "oak", "log.path", 1, bad)]
488
        rows.append(runner_mod.abort_row(rows[0], "diff.dirty", "status.dirty"))
489
        prepared = self.prepared()
490
        prepared.oak_identity = {"binary_sha256": "c" * 64}
491
        rows += runner_mod.ingest_rows(prepared, "b", {})
492
        self.assertEqual(validate_rows(rows, "scale"), [])
493
        self.assertEqual([r["returncode"] for r in rows[:5]], [0, 77, 1, 1, 77])
494
        self.assertEqual(rows[3]["failure"], "oracle_failed")
495
        self.assertFalse(any(r["claim_eligible"] for r in rows))
496
        self.assertTrue(all(r["oracle_timed"] is False for r in rows[:4]))
497
        import publish_gate
498
        for row in rows:
499
            if row["subject_kind"] == "oak" and row["operation"].startswith("ingest."):
500
                self.assertEqual(publish_gate.oak_binary_identity(row), "c" * 64)
501
502
    def test_cell_report_gives_floors_and_mde(self) -> None:
503
        rows = []
504
        for run in range(1, 7):
505
            for subject, kind, base in (("git", "git", 10.0), ("git_aa", "git", 10.0),
506
                                        ("oak_installed", "oak", 20.0), ("oak_installed_aa", "oak", 20.0)):
507
                outcome = ops_mod.Outcome(timed=[ops_mod.Timed("main", [kind], cap(""))], oracle_ok=True)
508
                row = self.row(subject, kind, "status.clean", run, outcome)
509
                row["elapsed_ms"] = base + (run % 3) * 0.2
510
                rows.append(row)
511
        report = runner_mod.cell_report(rows, ["git", "git_aa", "oak_installed", "oak_installed_aa"])
512
        item = report[0]
513
        self.assertIsNotNone(item["git_aa_floor_pct"])
514
        self.assertIsNotNone(item["oak_vs_git_mde_pct_80"])
515
        self.assertIsNotNone(item["oak_vs_oak_mde_pct_80"])
516
        self.assertAlmostEqual(item["oak_vs_git_delta_pct_dev_only"], 100.0, delta=5)
517
        self.assertFalse(item["claim_eligible"])
518
        text = runner_mod.summary_markdown(self.prepared(), report,
519
                                           {"bench_id": "b", "runs": 6, "subjects": ["git"]})
520
        self.assertIn("DEVELOPMENT ONLY", text)
521
        self.assertRegex(text, r"\| 1\.9\dx \|")
522
523
524
@unittest.skipUnless(GIT, "git not available")
525
class GitOnlyScheduleTest(unittest.TestCase):
526
    """End to end on git only: prepare, interleave git + git_aa, every oracle passes."""
527
528
    def test_schedule(self) -> None:
529
        from oakbench.environment import base_env
530
        tmp = Path(tempfile.mkdtemp(prefix="scale-e2e-"))
531
        try:
532
            spec = gen.GenSpec(name="e2e", seed="e2e", files=24, file_bytes=64, depth=3, fanout=4, commits=9,
533
                               branches=2, conflict_files=2)
534
            tier = tiers_mod.Tier(tier_id="e2e", kind="tier", spec=spec, dirty=tiers_mod.DirtyShape(2, 1, 1, 1))
535
            env = base_env(home=tmp / "home")
536
            # default logger (stderr) on purpose: it must be importable and callable
537
            prepared = ws.prepare(tier, tmp / "work", git_bin=GIT, oak_bin="oak", env=env, kinds=("git",))
538
            subjects = [SimpleNamespace(name=n, kind="git", bin=Path(GIT)) for n in ("git", "git_aa")]
539
            try:
540
                rows = runner_mod.run_schedule(prepared, subjects, runs=1, ops=ops_mod.ALL_OPS, bench_id="e2e",
541
                                               seed=1, scratch=tmp / "scratch", details={}, runner_fields={},
542
                                               timeout_s=120, lock_wait_ms=None, log=lambda *_: None)
543
            finally:
544
                prepared.close()
545
            self.assertEqual(validate_rows(rows, "scale"), [])
546
            failed = [(r["operation"], r.get("failure"), r.get("oracle_detail")) for r in rows if r["returncode"] == 1]
547
            self.assertEqual(failed, [])
548
            measured = {r["operation"] for r in rows if r["returncode"] == 0}
549
            self.assertEqual(measured, set(ops_mod.ALL_OPS))
550
            self.assertTrue(all(r["oracle_ok"] for r in rows if r["returncode"] == 0))
551
        finally:
552
            shutil.rmtree(tmp, ignore_errors=True)
553
554
555
if __name__ == "__main__":
556
    unittest.main()