| 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_commit_needs_dirty_state(self) -> None:
|
| 428 | self.assertIn("needs status.dirty", self.run_op("commit.dirty", "git").failed)
|
| 429 |
|
| 430 | def test_timed_failure_is_recorded_not_raised(self) -> None:
|
| 431 | outcome = self.run_op("status.clean", "oak", cap("", returncode=3))
|
| 432 | self.assertIn("exit 3", outcome.failed)
|
| 433 | self.assertIsNone(outcome.oracle_ok)
|
| 434 |
|
| 435 | def test_check_worktree(self) -> None:
|
| 436 | root = self.tmp / "git" / "work"
|
| 437 | path = gen.file_path(self.spec, 5)
|
| 438 | self.expected.sample[path] = gen.git_blob_id((root / path).read_bytes())
|
| 439 | ctx = self.ctx("git")
|
| 440 | ok, detail = ops_mod.check_worktree(ctx, root, self.spec.files)
|
| 441 | self.assertTrue(ok, detail)
|
| 442 | (root / path).write_bytes(b"changed")
|
| 443 | ok, detail = ops_mod.check_worktree(ctx, root, self.spec.files)
|
| 444 | self.assertFalse(ok)
|
| 445 | self.assertEqual(detail["sample_mismatch_count"], 1)
|
| 446 |
|
| 447 |
|
| 448 | class RowTest(unittest.TestCase):
|
| 449 | def prepared(self) -> SimpleNamespace:
|
| 450 | tier = tiers_mod.TIERS["fresh"]
|
| 451 | fields = {"tier": "fresh", "tier_kind": "tier", "fixture_id": tier.fixture_id,
|
| 452 | "fixture_manifest_sha256": "a" * 64, "fixture_spec_sha256": "b" * 64}
|
| 453 | return SimpleNamespace(tier=tier, fixture_fields=lambda: fields, phases={"ingest.oak_import": 1.5},
|
| 454 | notes=[], manifest={"refs_sha256": "a" * 64, "spec_sha256": "b" * 64})
|
| 455 |
|
| 456 | def row(self, subject: str, kind: str, op: str, run: int, outcome: ops_mod.Outcome) -> dict:
|
| 457 | return runner_mod.build_row(bench_id="b", prepared=self.prepared(),
|
| 458 | subject=SimpleNamespace(name=subject, kind=kind), op=op, run=run,
|
| 459 | outcome=outcome, details={}, env_sample={}, lock_wait_ms=0.0,
|
| 460 | order_position=0, runner_fields={})
|
| 461 |
|
| 462 | def test_rows_validate_for_all_outcomes(self) -> None:
|
| 463 | ok = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["git", "status"], cap(""))], oracle_ok=True)
|
| 464 | skip = ops_mod.Outcome(skip_reason="no_equivalent")
|
| 465 | fail = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["oak", "status"], cap("", 2))], failed="exit 2")
|
| 466 | bad = ops_mod.Outcome(timed=[ops_mod.Timed("main", ["oak", "log"], cap(""))], oracle_ok=False)
|
| 467 | rows = [self.row("git", "git", "status.clean", 1, ok), self.row("oak_installed", "oak", "blame", 1, skip),
|
| 468 | self.row("oak_installed", "oak", "status.clean", 1, fail),
|
| 469 | self.row("oak_installed", "oak", "log.path", 1, bad)]
|
| 470 | rows.append(runner_mod.abort_row(rows[0], "diff.dirty", "status.dirty"))
|
| 471 | rows += runner_mod.ingest_rows(self.prepared(), "b", {})
|
| 472 | self.assertEqual(validate_rows(rows, "scale"), [])
|
| 473 | self.assertEqual([r["returncode"] for r in rows[:5]], [0, 77, 1, 1, 77])
|
| 474 | self.assertEqual(rows[3]["failure"], "oracle_failed")
|
| 475 | self.assertFalse(any(r["claim_eligible"] for r in rows))
|
| 476 | self.assertTrue(all(r["oracle_timed"] is False for r in rows[:4]))
|
| 477 |
|
| 478 | def test_cell_report_gives_floors_and_mde(self) -> None:
|
| 479 | rows = []
|
| 480 | for run in range(1, 7):
|
| 481 | for subject, kind, base in (("git", "git", 10.0), ("git_aa", "git", 10.0),
|
| 482 | ("oak_installed", "oak", 20.0), ("oak_installed_aa", "oak", 20.0)):
|
| 483 | outcome = ops_mod.Outcome(timed=[ops_mod.Timed("main", [kind], cap(""))], oracle_ok=True)
|
| 484 | row = self.row(subject, kind, "status.clean", run, outcome)
|
| 485 | row["elapsed_ms"] = base + (run % 3) * 0.2
|
| 486 | rows.append(row)
|
| 487 | report = runner_mod.cell_report(rows, ["git", "git_aa", "oak_installed", "oak_installed_aa"])
|
| 488 | item = report[0]
|
| 489 | self.assertIsNotNone(item["git_aa_floor_pct"])
|
| 490 | self.assertIsNotNone(item["oak_vs_git_mde_pct_80"])
|
| 491 | self.assertIsNotNone(item["oak_vs_oak_mde_pct_80"])
|
| 492 | self.assertAlmostEqual(item["oak_vs_git_delta_pct_dev_only"], 100.0, delta=5)
|
| 493 | self.assertFalse(item["claim_eligible"])
|
| 494 | text = runner_mod.summary_markdown(self.prepared(), report,
|
| 495 | {"bench_id": "b", "runs": 6, "subjects": ["git"]})
|
| 496 | self.assertIn("DEVELOPMENT ONLY", text)
|
| 497 | self.assertRegex(text, r"\| 1\.9\dx \|")
|
| 498 |
|
| 499 |
|
| 500 | @unittest.skipUnless(GIT, "git not available")
|
| 501 | class GitOnlyScheduleTest(unittest.TestCase):
|
| 502 | """End to end on git only: prepare, interleave git + git_aa, every oracle passes."""
|
| 503 |
|
| 504 | def test_schedule(self) -> None:
|
| 505 | from oakbench.environment import base_env
|
| 506 | tmp = Path(tempfile.mkdtemp(prefix="scale-e2e-"))
|
| 507 | try:
|
| 508 | spec = gen.GenSpec(name="e2e", seed="e2e", files=24, file_bytes=64, depth=3, fanout=4, commits=9,
|
| 509 | branches=2, conflict_files=2)
|
| 510 | tier = tiers_mod.Tier(tier_id="e2e", kind="tier", spec=spec, dirty=tiers_mod.DirtyShape(2, 1, 1, 1))
|
| 511 | env = base_env(home=tmp / "home")
|
| 512 | # default logger (stderr) on purpose: it must be importable and callable
|
| 513 | prepared = ws.prepare(tier, tmp / "work", git_bin=GIT, oak_bin="oak", env=env, kinds=("git",))
|
| 514 | subjects = [SimpleNamespace(name=n, kind="git", bin=Path(GIT)) for n in ("git", "git_aa")]
|
| 515 | try:
|
| 516 | rows = runner_mod.run_schedule(prepared, subjects, runs=1, ops=ops_mod.ALL_OPS, bench_id="e2e",
|
| 517 | seed=1, scratch=tmp / "scratch", details={}, runner_fields={},
|
| 518 | timeout_s=120, lock_wait_ms=None, log=lambda *_: None)
|
| 519 | finally:
|
| 520 | prepared.close()
|
| 521 | self.assertEqual(validate_rows(rows, "scale"), [])
|
| 522 | failed = [(r["operation"], r.get("failure"), r.get("oracle_detail")) for r in rows if r["returncode"] == 1]
|
| 523 | self.assertEqual(failed, [])
|
| 524 | measured = {r["operation"] for r in rows if r["returncode"] == 0}
|
| 525 | self.assertEqual(measured, set(ops_mod.ALL_OPS))
|
| 526 | self.assertTrue(all(r["oracle_ok"] for r in rows if r["returncode"] == 0))
|
| 527 | finally:
|
| 528 | shutil.rmtree(tmp, ignore_errors=True)
|
| 529 |
|
| 530 |
|
| 531 | if __name__ == "__main__":
|
| 532 | unittest.main()
|