test_range_diff_supercharge.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Tests for ``muse range-diff`` — supercharged coverage. |
| 2 | |
| 3 | Coverage tiers |
| 4 | -------------- |
| 5 | - Unit: _parse_range, _resolve_ref (sha256: prefix), _compute_patch_id, |
| 6 | _patch_id_for_commit, _pair_series pairing logic |
| 7 | - Integration: identical series, changed/dropped/added, JSON schema, |
| 8 | text output, creation-factor variants, error cases |
| 9 | - End-to-end: full CLI via CliRunner |
| 10 | - Data integrity: old_count/new_count match pair list; files_changed per commit; |
| 11 | patch_id has sha256: prefix; duration_ms is numeric |
| 12 | - Performance: 50-commit series completes under 2 seconds |
| 13 | - Security: ANSI injection rejected; no control characters in output |
| 14 | - Stress: 50-commit mixed series (equivalent + changed + added) |
| 15 | |
| 16 | Supercharged JSON schema (all ``--json`` outputs) |
| 17 | -------------------------------------------------- |
| 18 | |
| 19 | :: |
| 20 | |
| 21 | { |
| 22 | "old_range": "base..old", |
| 23 | "new_range": "base..new", |
| 24 | "trivially_equivalent": true, |
| 25 | "old_count": 3, |
| 26 | "new_count": 3, |
| 27 | "stable": false, |
| 28 | "creation_factor": 0.6, |
| 29 | "pairs": [ |
| 30 | { |
| 31 | "old": { |
| 32 | "commit_id": "sha256:...", |
| 33 | "patch_id": "sha256:...", |
| 34 | "subject": "feat: add foo", |
| 35 | "files_changed": 2 |
| 36 | }, |
| 37 | "new": { ... }, |
| 38 | "status": "equivalent" |
| 39 | } |
| 40 | ], |
| 41 | "duration_ms": 12.3, |
| 42 | "exit_code": 0 |
| 43 | } |
| 44 | """ |
| 45 | |
| 46 | from __future__ import annotations |
| 47 | from collections.abc import Mapping |
| 48 | |
| 49 | import datetime |
| 50 | import argparse |
| 51 | import json |
| 52 | import pathlib |
| 53 | import re |
| 54 | import time |
| 55 | |
| 56 | import pytest |
| 57 | |
| 58 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 59 | from muse.core.object_store import write_object |
| 60 | from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id |
| 61 | from muse.core.commits import ( |
| 62 | CommitRecord, |
| 63 | write_commit, |
| 64 | ) |
| 65 | from muse.core.snapshots import ( |
| 66 | SnapshotRecord, |
| 67 | write_snapshot, |
| 68 | ) |
| 69 | from muse.core.types import Manifest, blob_id, long_id |
| 70 | from muse.core.paths import ref_path, muse_dir |
| 71 | |
| 72 | runner = CliRunner() |
| 73 | |
| 74 | _REPO_ID = "range-diff-super" |
| 75 | _counter = 0 |
| 76 | |
| 77 | |
| 78 | # --------------------------------------------------------------------------- |
| 79 | # Helpers |
| 80 | # --------------------------------------------------------------------------- |
| 81 | |
| 82 | |
| 83 | def _oid(content: bytes) -> str: |
| 84 | """sha256:-prefixed object ID — correct for all Muse APIs.""" |
| 85 | return blob_id(content) |
| 86 | |
| 87 | |
| 88 | def _init_repo(path: pathlib.Path) -> pathlib.Path: |
| 89 | dot_muse = muse_dir(path) |
| 90 | for d in ("commits", "snapshots", "objects", "refs/heads", "code"): |
| 91 | (dot_muse / d).mkdir(parents=True, exist_ok=True) |
| 92 | (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") |
| 93 | (dot_muse / "repo.json").write_text( |
| 94 | json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8" |
| 95 | ) |
| 96 | return path |
| 97 | |
| 98 | |
| 99 | def _env(repo: pathlib.Path) -> Mapping[str, str]: |
| 100 | return {"MUSE_REPO_ROOT": str(repo)} |
| 101 | |
| 102 | |
| 103 | def _write_files(root: pathlib.Path, files: Mapping[str, bytes]) -> Manifest: |
| 104 | manifest: Manifest = {} |
| 105 | for rel, content in files.items(): |
| 106 | oid = _oid(content) |
| 107 | write_object(root, oid, content) |
| 108 | manifest[rel] = oid |
| 109 | p = root / rel |
| 110 | p.parent.mkdir(parents=True, exist_ok=True) |
| 111 | p.write_bytes(content) |
| 112 | return manifest |
| 113 | |
| 114 | |
| 115 | def _commit( |
| 116 | root: pathlib.Path, |
| 117 | files: Mapping[str, bytes], |
| 118 | branch: str = "main", |
| 119 | parent_id: str | None = None, |
| 120 | message: str | None = None, |
| 121 | ) -> str: |
| 122 | global _counter |
| 123 | _counter += 1 |
| 124 | manifest = _write_files(root, files) |
| 125 | snap_id = compute_snapshot_id(manifest) |
| 126 | write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) |
| 127 | committed_at = datetime.datetime.now(datetime.timezone.utc) |
| 128 | msg = message or f"commit {_counter}" |
| 129 | commit_id = compute_commit_id( |
| 130 | [parent_id] if parent_id else [], snap_id, msg, committed_at.isoformat(), |
| 131 | ) |
| 132 | write_commit(root, CommitRecord( |
| 133 | commit_id=commit_id, branch=branch, |
| 134 | snapshot_id=snap_id, message=msg, committed_at=committed_at, |
| 135 | parent_commit_id=parent_id, |
| 136 | )) |
| 137 | branch_ref = ref_path(root, branch) |
| 138 | branch_ref.parent.mkdir(parents=True, exist_ok=True) |
| 139 | branch_ref.write_text(commit_id, encoding="utf-8") |
| 140 | return commit_id |
| 141 | |
| 142 | |
| 143 | def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult: |
| 144 | from muse.cli.app import main as cli |
| 145 | return runner.invoke(cli, ["range-diff", *args], env=_env(repo)) |
| 146 | |
| 147 | |
| 148 | # --------------------------------------------------------------------------- |
| 149 | # Unit — _parse_range |
| 150 | # --------------------------------------------------------------------------- |
| 151 | |
| 152 | |
| 153 | class TestParseRange: |
| 154 | def test_with_dotdot(self) -> None: |
| 155 | from muse.cli.commands.range_diff import _parse_range |
| 156 | base, tip = _parse_range("abc..def") |
| 157 | assert base == "abc" |
| 158 | assert tip == "def" |
| 159 | |
| 160 | def test_no_dotdot(self) -> None: |
| 161 | from muse.cli.commands.range_diff import _parse_range |
| 162 | base, tip = _parse_range("main") |
| 163 | assert base is None |
| 164 | assert tip == "main" |
| 165 | |
| 166 | def test_strips_whitespace(self) -> None: |
| 167 | from muse.cli.commands.range_diff import _parse_range |
| 168 | base, tip = _parse_range("base .. tip") |
| 169 | assert base == "base" |
| 170 | assert tip == "tip" |
| 171 | |
| 172 | def test_sha256_prefixed_base(self) -> None: |
| 173 | from muse.cli.commands.range_diff import _parse_range |
| 174 | sha = long_id("a" * 64) |
| 175 | base, tip = _parse_range(f"{sha}..main") |
| 176 | assert base == sha |
| 177 | assert tip == "main" |
| 178 | |
| 179 | |
| 180 | # --------------------------------------------------------------------------- |
| 181 | # Unit — _resolve_ref with sha256: prefix |
| 182 | # --------------------------------------------------------------------------- |
| 183 | |
| 184 | |
| 185 | class TestResolveRef: |
| 186 | def test_resolves_branch_name(self, tmp_path: pathlib.Path) -> None: |
| 187 | from muse.cli.commands.range_diff import _resolve_ref |
| 188 | root = _init_repo(tmp_path) |
| 189 | cid = _commit(root, {"a.py": b"x\n"}, branch="main") |
| 190 | resolved = _resolve_ref(root, "main") |
| 191 | assert resolved == cid |
| 192 | |
| 193 | def test_resolves_sha256_prefixed_commit_id(self, tmp_path: pathlib.Path) -> None: |
| 194 | """RED: _resolve_ref must accept sha256:-prefixed commit IDs.""" |
| 195 | from muse.cli.commands.range_diff import _resolve_ref |
| 196 | root = _init_repo(tmp_path) |
| 197 | cid = _commit(root, {"a.py": b"x\n"}, branch="main") |
| 198 | # cid from compute_commit_id is sha256:-prefixed |
| 199 | assert cid.startswith("sha256:") |
| 200 | resolved = _resolve_ref(root, cid) |
| 201 | assert resolved is not None, ( |
| 202 | f"_resolve_ref could not resolve sha256:-prefixed commit ID {cid[:20]}..." |
| 203 | ) |
| 204 | |
| 205 | def test_resolves_head(self, tmp_path: pathlib.Path) -> None: |
| 206 | from muse.cli.commands.range_diff import _resolve_ref |
| 207 | root = _init_repo(tmp_path) |
| 208 | cid = _commit(root, {"a.py": b"x\n"}, branch="main") |
| 209 | assert _resolve_ref(root, "HEAD") == cid |
| 210 | |
| 211 | def test_nonexistent_branch_returns_none(self, tmp_path: pathlib.Path) -> None: |
| 212 | from muse.cli.commands.range_diff import _resolve_ref |
| 213 | root = _init_repo(tmp_path) |
| 214 | assert _resolve_ref(root, "no-such-branch") is None |
| 215 | |
| 216 | def test_nonexistent_sha_returns_none(self, tmp_path: pathlib.Path) -> None: |
| 217 | from muse.cli.commands.range_diff import _resolve_ref |
| 218 | root = _init_repo(tmp_path) |
| 219 | fake = long_id("f" * 64) |
| 220 | assert _resolve_ref(root, fake) is None |
| 221 | |
| 222 | |
| 223 | # --------------------------------------------------------------------------- |
| 224 | # Unit — _compute_patch_id / _patch_id_for_commit |
| 225 | # --------------------------------------------------------------------------- |
| 226 | |
| 227 | |
| 228 | class TestPatchId: |
| 229 | def test_returns_sha256_prefixed_patch_id(self, tmp_path: pathlib.Path) -> None: |
| 230 | """RED: patch_id must have sha256: prefix — consistent with muse patch-id --json.""" |
| 231 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 232 | root = _init_repo(tmp_path) |
| 233 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 234 | cid = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="feat", parent_id=base) |
| 235 | pid, _ = _patch_id_for_commit(root, cid, stable=False) |
| 236 | assert pid.startswith("sha256:"), ( |
| 237 | f"patch_id should be sha256:-prefixed but got: {pid[:20]!r}" |
| 238 | ) |
| 239 | |
| 240 | def test_returns_files_changed_count(self, tmp_path: pathlib.Path) -> None: |
| 241 | """RED: _patch_id_for_commit must return (patch_id, files_changed) tuple.""" |
| 242 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 243 | root = _init_repo(tmp_path) |
| 244 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 245 | # commit adds 2 new files relative to parent |
| 246 | cid = _commit(root, {"base.py": b"base\n", "a.py": b"a\n", "b.py": b"b\n"}, |
| 247 | branch="feat", parent_id=base) |
| 248 | pid, fc = _patch_id_for_commit(root, cid, stable=False) |
| 249 | assert fc == 2, f"Expected 2 files_changed, got {fc}" |
| 250 | |
| 251 | def test_same_content_same_patch_id(self, tmp_path: pathlib.Path) -> None: |
| 252 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 253 | root = _init_repo(tmp_path) |
| 254 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 255 | c1 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b1", parent_id=base) |
| 256 | c2 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b2", parent_id=base) |
| 257 | pid1, _ = _patch_id_for_commit(root, c1, stable=False) |
| 258 | pid2, _ = _patch_id_for_commit(root, c2, stable=False) |
| 259 | assert pid1 == pid2 |
| 260 | |
| 261 | def test_different_content_different_patch_id(self, tmp_path: pathlib.Path) -> None: |
| 262 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 263 | root = _init_repo(tmp_path) |
| 264 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 265 | c1 = _commit(root, {"base.py": b"base\n", "a.py": b"v1\n"}, branch="b1", parent_id=base) |
| 266 | c2 = _commit(root, {"base.py": b"base\n", "a.py": b"v2\n"}, branch="b2", parent_id=base) |
| 267 | pid1, _ = _patch_id_for_commit(root, c1, stable=False) |
| 268 | pid2, _ = _patch_id_for_commit(root, c2, stable=False) |
| 269 | assert pid1 != pid2 |
| 270 | |
| 271 | def test_stable_ignores_trailing_whitespace(self, tmp_path: pathlib.Path) -> None: |
| 272 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 273 | root = _init_repo(tmp_path) |
| 274 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 275 | c1 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b1", parent_id=base) |
| 276 | c2 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1 \n"}, branch="b2", parent_id=base) |
| 277 | pid1, _ = _patch_id_for_commit(root, c1, stable=True) |
| 278 | pid2, _ = _patch_id_for_commit(root, c2, stable=True) |
| 279 | assert pid1 == pid2 |
| 280 | |
| 281 | def test_first_commit_zero_files_changed(self, tmp_path: pathlib.Path) -> None: |
| 282 | from muse.cli.commands.range_diff import _patch_id_for_commit |
| 283 | root = _init_repo(tmp_path) |
| 284 | # First commit has no parent — base_manifest is empty, so all files are "added" |
| 285 | cid = _commit(root, {"a.py": b"x\n", "b.py": b"y\n"}, branch="main") |
| 286 | _, fc = _patch_id_for_commit(root, cid, stable=False) |
| 287 | assert fc == 2 |
| 288 | |
| 289 | |
| 290 | # --------------------------------------------------------------------------- |
| 291 | # Integration — trivially equivalent |
| 292 | # --------------------------------------------------------------------------- |
| 293 | |
| 294 | |
| 295 | class TestTriviallyEquivalent: |
| 296 | def test_identical_series_exit_zero(self, tmp_path: pathlib.Path) -> None: |
| 297 | root = _init_repo(tmp_path) |
| 298 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 299 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 300 | c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1) |
| 301 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 302 | n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=n1) |
| 303 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 304 | assert result.exit_code == 0 |
| 305 | data = json.loads(result.stdout) |
| 306 | assert data["trivially_equivalent"] is True |
| 307 | assert all(p["status"] == "equivalent" for p in data["pairs"]) |
| 308 | |
| 309 | def test_both_empty_trivially_equivalent(self, tmp_path: pathlib.Path) -> None: |
| 310 | root = _init_repo(tmp_path) |
| 311 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 312 | result = _invoke(root, f"{base}..{base}", f"{base}..{base}", "--json") |
| 313 | assert result.exit_code == 0 |
| 314 | data = json.loads(result.stdout) |
| 315 | assert data["trivially_equivalent"] is True |
| 316 | assert data["pairs"] == [] |
| 317 | |
| 318 | def test_empty_old_all_added(self, tmp_path: pathlib.Path) -> None: |
| 319 | root = _init_repo(tmp_path) |
| 320 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 321 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 322 | result = _invoke(root, f"{base}..{base}", f"{base}..new", "--json") |
| 323 | data = json.loads(result.stdout) |
| 324 | assert all(p["status"] == "added" for p in data["pairs"]) |
| 325 | |
| 326 | def test_empty_new_all_dropped(self, tmp_path: pathlib.Path) -> None: |
| 327 | root = _init_repo(tmp_path) |
| 328 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 329 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 330 | result = _invoke(root, f"{base}..old", f"{base}..{base}", "--json") |
| 331 | data = json.loads(result.stdout) |
| 332 | assert all(p["status"] == "dropped" for p in data["pairs"]) |
| 333 | |
| 334 | |
| 335 | # --------------------------------------------------------------------------- |
| 336 | # Integration — differences |
| 337 | # --------------------------------------------------------------------------- |
| 338 | |
| 339 | |
| 340 | class TestDifferences: |
| 341 | def test_single_commit_changed(self, tmp_path: pathlib.Path) -> None: |
| 342 | root = _init_repo(tmp_path) |
| 343 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 344 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base) |
| 345 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base) |
| 346 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 347 | data = json.loads(result.stdout) |
| 348 | assert data["trivially_equivalent"] is False |
| 349 | assert len(data["pairs"]) == 1 |
| 350 | assert data["pairs"][0]["status"] == "changed" |
| 351 | |
| 352 | def test_commit_added(self, tmp_path: pathlib.Path) -> None: |
| 353 | root = _init_repo(tmp_path) |
| 354 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 355 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 356 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 357 | n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "extra.py": b"e=9\n"}, branch="new", parent_id=n1) |
| 358 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 359 | data = json.loads(result.stdout) |
| 360 | statuses = [p["status"] for p in data["pairs"]] |
| 361 | assert "added" in statuses |
| 362 | assert "equivalent" in statuses |
| 363 | |
| 364 | def test_commit_dropped(self, tmp_path: pathlib.Path) -> None: |
| 365 | root = _init_repo(tmp_path) |
| 366 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 367 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a") |
| 368 | c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1, message="add b") |
| 369 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base, message="add a and b squashed") |
| 370 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 371 | data = json.loads(result.stdout) |
| 372 | statuses = {p["status"] for p in data["pairs"]} |
| 373 | assert "dropped" in statuses or "changed" in statuses |
| 374 | |
| 375 | def test_exit_zero_when_equivalent(self, tmp_path: pathlib.Path) -> None: |
| 376 | root = _init_repo(tmp_path) |
| 377 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 378 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 379 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 380 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 381 | assert result.exit_code == 0 |
| 382 | |
| 383 | def test_exit_nonzero_when_differs(self, tmp_path: pathlib.Path) -> None: |
| 384 | root = _init_repo(tmp_path) |
| 385 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 386 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base) |
| 387 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base) |
| 388 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 389 | assert result.exit_code != 0 |
| 390 | |
| 391 | |
| 392 | # --------------------------------------------------------------------------- |
| 393 | # Integration — JSON schema supercharge (RED tests) |
| 394 | # --------------------------------------------------------------------------- |
| 395 | |
| 396 | |
| 397 | class TestJsonSchema: |
| 398 | def test_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 399 | """RED: duration_ms must be present in JSON output.""" |
| 400 | root = _init_repo(tmp_path) |
| 401 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 402 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 403 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 404 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 405 | data = json.loads(result.stdout) |
| 406 | assert "duration_ms" in data, "duration_ms missing from JSON" |
| 407 | assert isinstance(data["duration_ms"], (int, float)) |
| 408 | assert data["duration_ms"] >= 0 |
| 409 | |
| 410 | def test_has_exit_code(self, tmp_path: pathlib.Path) -> None: |
| 411 | """RED: exit_code must be present in JSON output.""" |
| 412 | root = _init_repo(tmp_path) |
| 413 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 414 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 415 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 416 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 417 | data = json.loads(result.stdout) |
| 418 | assert "exit_code" in data, "exit_code missing from JSON" |
| 419 | assert data["exit_code"] == 0 |
| 420 | |
| 421 | def test_exit_code_reflects_differences(self, tmp_path: pathlib.Path) -> None: |
| 422 | """exit_code in JSON must be 1 when series differ.""" |
| 423 | root = _init_repo(tmp_path) |
| 424 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 425 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base) |
| 426 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base) |
| 427 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 428 | data = json.loads(result.stdout) |
| 429 | assert "exit_code" in data |
| 430 | assert data["exit_code"] == 1 |
| 431 | |
| 432 | def test_has_old_count(self, tmp_path: pathlib.Path) -> None: |
| 433 | """RED: old_count must reflect the number of commits in the old range.""" |
| 434 | root = _init_repo(tmp_path) |
| 435 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 436 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 437 | c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1) |
| 438 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 439 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 440 | data = json.loads(result.stdout) |
| 441 | assert "old_count" in data, "old_count missing from JSON" |
| 442 | assert data["old_count"] == 2 |
| 443 | |
| 444 | def test_has_new_count(self, tmp_path: pathlib.Path) -> None: |
| 445 | """RED: new_count must reflect the number of commits in the new range.""" |
| 446 | root = _init_repo(tmp_path) |
| 447 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 448 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 449 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 450 | n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "c.py": b"c=3\n"}, branch="new", parent_id=n1) |
| 451 | n3 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "c.py": b"c=3\n", "d.py": b"d=4\n"}, branch="new", parent_id=n2) |
| 452 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 453 | data = json.loads(result.stdout) |
| 454 | assert "new_count" in data, "new_count missing from JSON" |
| 455 | assert data["new_count"] == 3 |
| 456 | |
| 457 | def test_has_stable_field(self, tmp_path: pathlib.Path) -> None: |
| 458 | """RED: stable must appear in JSON output reflecting the --stable flag.""" |
| 459 | root = _init_repo(tmp_path) |
| 460 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 461 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 462 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 463 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json", "--stable") |
| 464 | data = json.loads(result.stdout) |
| 465 | assert "stable" in data, "stable missing from JSON" |
| 466 | assert data["stable"] is True |
| 467 | |
| 468 | def test_stable_false_by_default(self, tmp_path: pathlib.Path) -> None: |
| 469 | root = _init_repo(tmp_path) |
| 470 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 471 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 472 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 473 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 474 | data = json.loads(result.stdout) |
| 475 | assert "stable" in data |
| 476 | assert data["stable"] is False |
| 477 | |
| 478 | def test_has_creation_factor(self, tmp_path: pathlib.Path) -> None: |
| 479 | """RED: creation_factor must appear in JSON, reflecting the value used.""" |
| 480 | root = _init_repo(tmp_path) |
| 481 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 482 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 483 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 484 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json", "--creation-factor", "0.3") |
| 485 | data = json.loads(result.stdout) |
| 486 | assert "creation_factor" in data, "creation_factor missing from JSON" |
| 487 | assert abs(data["creation_factor"] - 0.3) < 0.01 |
| 488 | |
| 489 | def test_patch_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None: |
| 490 | """RED: patch_id in pair commit info must be sha256:-prefixed.""" |
| 491 | root = _init_repo(tmp_path) |
| 492 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 493 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a") |
| 494 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a") |
| 495 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 496 | data = json.loads(result.stdout) |
| 497 | for pair in data["pairs"]: |
| 498 | for side in ("old", "new"): |
| 499 | info = pair.get(side) |
| 500 | if info is not None: |
| 501 | assert info["patch_id"].startswith("sha256:"), ( |
| 502 | f"pair[{side}].patch_id lacks sha256: prefix: {info['patch_id'][:20]!r}" |
| 503 | ) |
| 504 | |
| 505 | def test_pair_has_files_changed(self, tmp_path: pathlib.Path) -> None: |
| 506 | """RED: each pair commit info must include files_changed count.""" |
| 507 | root = _init_repo(tmp_path) |
| 508 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 509 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=base) |
| 510 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base) |
| 511 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 512 | data = json.loads(result.stdout) |
| 513 | for pair in data["pairs"]: |
| 514 | for side in ("old", "new"): |
| 515 | info = pair.get(side) |
| 516 | if info is not None: |
| 517 | assert "files_changed" in info, ( |
| 518 | f"pair[{side}] missing files_changed: {info}" |
| 519 | ) |
| 520 | assert isinstance(info["files_changed"], int) |
| 521 | assert info["files_changed"] >= 0 |
| 522 | |
| 523 | def test_old_count_new_count_match_pairs(self, tmp_path: pathlib.Path) -> None: |
| 524 | """old_count + new_count must be consistent with actual range walks.""" |
| 525 | root = _init_repo(tmp_path) |
| 526 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 527 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 528 | c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1) |
| 529 | c3 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n"}, branch="old", parent_id=c2) |
| 530 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 531 | n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=n1) |
| 532 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 533 | data = json.loads(result.stdout) |
| 534 | assert data["old_count"] == 3 |
| 535 | assert data["new_count"] == 2 |
| 536 | |
| 537 | def test_complete_schema_keys(self, tmp_path: pathlib.Path) -> None: |
| 538 | root = _init_repo(tmp_path) |
| 539 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 540 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 541 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 542 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 543 | data = json.loads(result.stdout) |
| 544 | for key in ("old_range", "new_range", "trivially_equivalent", |
| 545 | "old_count", "new_count", "stable", "creation_factor", |
| 546 | "pairs", "duration_ms", "exit_code"): |
| 547 | assert key in data, f"key {key!r} missing from JSON output" |
| 548 | |
| 549 | |
| 550 | # --------------------------------------------------------------------------- |
| 551 | # Integration — creation-factor |
| 552 | # --------------------------------------------------------------------------- |
| 553 | |
| 554 | |
| 555 | class TestCreationFactor: |
| 556 | def test_zero_no_fuzzy_pairing(self, tmp_path: pathlib.Path) -> None: |
| 557 | root = _init_repo(tmp_path) |
| 558 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 559 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base) |
| 560 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base) |
| 561 | result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "0.0", "--json") |
| 562 | data = json.loads(result.stdout) |
| 563 | statuses = {p["status"] for p in data["pairs"]} |
| 564 | assert "changed" not in statuses |
| 565 | assert "dropped" in statuses or "added" in statuses |
| 566 | |
| 567 | def test_one_all_positionally_paired(self, tmp_path: pathlib.Path) -> None: |
| 568 | root = _init_repo(tmp_path) |
| 569 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 570 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base) |
| 571 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base) |
| 572 | result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "1.0", "--json") |
| 573 | data = json.loads(result.stdout) |
| 574 | statuses = {p["status"] for p in data["pairs"]} |
| 575 | assert "changed" in statuses |
| 576 | |
| 577 | def test_creation_factor_clamped_to_range(self, tmp_path: pathlib.Path) -> None: |
| 578 | """creation_factor in JSON must be clamped to [0.0, 1.0].""" |
| 579 | root = _init_repo(tmp_path) |
| 580 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 581 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 582 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 583 | result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "99.0", "--json") |
| 584 | data = json.loads(result.stdout) |
| 585 | assert data["creation_factor"] <= 1.0 |
| 586 | |
| 587 | |
| 588 | # --------------------------------------------------------------------------- |
| 589 | # Integration — text output |
| 590 | # --------------------------------------------------------------------------- |
| 591 | |
| 592 | |
| 593 | class TestTextOutput: |
| 594 | def test_equivalent_shows_equals_symbol(self, tmp_path: pathlib.Path) -> None: |
| 595 | root = _init_repo(tmp_path) |
| 596 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 597 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a") |
| 598 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a") |
| 599 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 600 | assert result.exit_code == 0 |
| 601 | assert "=" in result.stdout |
| 602 | |
| 603 | def test_text_short_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None: |
| 604 | """Short IDs in pair lines must be ``sha256:<12 hex chars>`` — prefix is canonical.""" |
| 605 | root = _init_repo(tmp_path) |
| 606 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 607 | _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a") |
| 608 | _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a") |
| 609 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 610 | # Only inspect pair lines (start with =, !, <, >) |
| 611 | pair_lines = [l for l in result.stdout.splitlines() if l and l[0] in "=!<>"] |
| 612 | assert pair_lines, "No pair lines in text output" |
| 613 | sha256_short = re.compile(r"^sha256:[0-9a-f]{12}$") |
| 614 | found = [tok for line in pair_lines for tok in line.split() if sha256_short.match(tok)] |
| 615 | assert found, ( |
| 616 | f"No sha256:<12-hex> short IDs found in pair lines.\n" |
| 617 | f"Pair lines: {pair_lines}" |
| 618 | ) |
| 619 | |
| 620 | def test_text_short_ids_are_sha256_plus_8_hex(self, tmp_path: pathlib.Path) -> None: |
| 621 | """Short IDs must be sha256: + exactly 12 lowercase hex chars (19 total).""" |
| 622 | root = _init_repo(tmp_path) |
| 623 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 624 | _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base, message="changed commit") |
| 625 | _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base, message="changed commit") |
| 626 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 627 | sha256_short = re.compile(r"^sha256:[0-9a-f]{12}$") |
| 628 | found = [] |
| 629 | for line in result.stdout.splitlines(): |
| 630 | for token in line.split(): |
| 631 | if sha256_short.match(token): |
| 632 | found.append(token) |
| 633 | assert found, f"No sha256:<12-hex> tokens found in output:\n{result.stdout}" |
| 634 | for tok in found: |
| 635 | assert len(tok) == 19, f"Expected 19 chars (sha256: + 12 hex), got {len(tok)}: {tok!r}" |
| 636 | |
| 637 | def test_changed_shows_exclamation_symbol(self, tmp_path: pathlib.Path) -> None: |
| 638 | root = _init_repo(tmp_path) |
| 639 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 640 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base, message="add a v1") |
| 641 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base, message="add a v2") |
| 642 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 643 | assert result.exit_code != 0 |
| 644 | assert "!" in result.stdout |
| 645 | |
| 646 | def test_added_shows_gt_symbol(self, tmp_path: pathlib.Path) -> None: |
| 647 | root = _init_repo(tmp_path) |
| 648 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 649 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 650 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 651 | n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "x.py": b"x\n"}, branch="new", parent_id=n1) |
| 652 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 653 | assert ">" in result.stdout |
| 654 | |
| 655 | def test_dropped_shows_lt_symbol(self, tmp_path: pathlib.Path) -> None: |
| 656 | root = _init_repo(tmp_path) |
| 657 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 658 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 659 | c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1) |
| 660 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base, message="squashed") |
| 661 | result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "0.0") |
| 662 | assert "<" in result.stdout |
| 663 | |
| 664 | |
| 665 | # --------------------------------------------------------------------------- |
| 666 | # Integration — error cases |
| 667 | # --------------------------------------------------------------------------- |
| 668 | |
| 669 | |
| 670 | class TestErrors: |
| 671 | def test_nonexistent_old_ref(self, tmp_path: pathlib.Path) -> None: |
| 672 | root = _init_repo(tmp_path) |
| 673 | _commit(root, {"a.py": b"x\n"}, branch="main") |
| 674 | result = _invoke(root, "ghost..no-such", "main..main") |
| 675 | assert result.exit_code != 0 |
| 676 | |
| 677 | def test_nonexistent_new_ref(self, tmp_path: pathlib.Path) -> None: |
| 678 | root = _init_repo(tmp_path) |
| 679 | base = _commit(root, {"a.py": b"x\n"}, branch="main") |
| 680 | result = _invoke(root, f"{base}..main", "ghost..no-such") |
| 681 | assert result.exit_code != 0 |
| 682 | |
| 683 | |
| 684 | # --------------------------------------------------------------------------- |
| 685 | # Data integrity |
| 686 | # --------------------------------------------------------------------------- |
| 687 | |
| 688 | |
| 689 | class TestDataIntegrity: |
| 690 | def test_files_changed_accurate(self, tmp_path: pathlib.Path) -> None: |
| 691 | """files_changed on a pair entry must match the actual diff size.""" |
| 692 | root = _init_repo(tmp_path) |
| 693 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 694 | # commit changes exactly 3 files relative to parent |
| 695 | c1 = _commit(root, { |
| 696 | "readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n" |
| 697 | }, branch="old", parent_id=base) |
| 698 | n1 = _commit(root, { |
| 699 | "readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n" |
| 700 | }, branch="new", parent_id=base) |
| 701 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 702 | data = json.loads(result.stdout) |
| 703 | pair = data["pairs"][0] |
| 704 | # 3 files added relative to base |
| 705 | assert pair["old"]["files_changed"] == 3 |
| 706 | assert pair["new"]["files_changed"] == 3 |
| 707 | |
| 708 | def test_old_count_zero_when_old_range_empty(self, tmp_path: pathlib.Path) -> None: |
| 709 | root = _init_repo(tmp_path) |
| 710 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 711 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 712 | result = _invoke(root, f"{base}..{base}", f"{base}..new", "--json") |
| 713 | data = json.loads(result.stdout) |
| 714 | assert data["old_count"] == 0 |
| 715 | assert data["new_count"] == 1 |
| 716 | |
| 717 | def test_commit_ids_are_sha256_prefixed(self, tmp_path: pathlib.Path) -> None: |
| 718 | root = _init_repo(tmp_path) |
| 719 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 720 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a") |
| 721 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a") |
| 722 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 723 | data = json.loads(result.stdout) |
| 724 | for pair in data["pairs"]: |
| 725 | for side in ("old", "new"): |
| 726 | info = pair.get(side) |
| 727 | if info is not None: |
| 728 | assert info["commit_id"].startswith("sha256:"), ( |
| 729 | f"commit_id lacks sha256: prefix: {info['commit_id']!r}" |
| 730 | ) |
| 731 | |
| 732 | def test_pair_subject_matches_commit_message(self, tmp_path: pathlib.Path) -> None: |
| 733 | root = _init_repo(tmp_path) |
| 734 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 735 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="feat: add alpha") |
| 736 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="feat: add alpha") |
| 737 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 738 | data = json.loads(result.stdout) |
| 739 | pair = data["pairs"][0] |
| 740 | assert pair["old"]["subject"] == "feat: add alpha" |
| 741 | assert pair["new"]["subject"] == "feat: add alpha" |
| 742 | |
| 743 | def test_duration_ms_is_plausible(self, tmp_path: pathlib.Path) -> None: |
| 744 | root = _init_repo(tmp_path) |
| 745 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 746 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 747 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 748 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 749 | data = json.loads(result.stdout) |
| 750 | assert 0 <= data["duration_ms"] < 10_000 |
| 751 | |
| 752 | |
| 753 | # --------------------------------------------------------------------------- |
| 754 | # Security |
| 755 | # --------------------------------------------------------------------------- |
| 756 | |
| 757 | |
| 758 | class TestSecurity: |
| 759 | def test_ansi_in_old_range_rejected(self, tmp_path: pathlib.Path) -> None: |
| 760 | root = _init_repo(tmp_path) |
| 761 | result = _invoke(root, "\x1b[31mbad\x1b[0m..main", "main..main") |
| 762 | assert result.exit_code != 0 |
| 763 | |
| 764 | def test_ansi_in_new_range_rejected(self, tmp_path: pathlib.Path) -> None: |
| 765 | root = _init_repo(tmp_path) |
| 766 | result = _invoke(root, "main..main", "\x1b[31mbad\x1b[0m..main") |
| 767 | assert result.exit_code != 0 |
| 768 | |
| 769 | def test_control_char_in_range_rejected(self, tmp_path: pathlib.Path) -> None: |
| 770 | root = _init_repo(tmp_path) |
| 771 | result = _invoke(root, "main\x00trick..main", "main..main") |
| 772 | assert result.exit_code != 0 |
| 773 | |
| 774 | def test_no_ansi_in_json_output(self, tmp_path: pathlib.Path) -> None: |
| 775 | root = _init_repo(tmp_path) |
| 776 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 777 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 778 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 779 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 780 | assert "\x1b[" not in result.stdout |
| 781 | |
| 782 | def test_no_ansi_in_text_output(self, tmp_path: pathlib.Path) -> None: |
| 783 | root = _init_repo(tmp_path) |
| 784 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 785 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 786 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 787 | result = _invoke(root, f"{base}..old", f"{base}..new") |
| 788 | assert "\x1b[" not in result.stdout |
| 789 | |
| 790 | |
| 791 | # --------------------------------------------------------------------------- |
| 792 | # Performance |
| 793 | # --------------------------------------------------------------------------- |
| 794 | |
| 795 | |
| 796 | class TestPerformance: |
| 797 | def test_50_commit_equivalent_series_under_2_seconds(self, tmp_path: pathlib.Path) -> None: |
| 798 | root = _init_repo(tmp_path) |
| 799 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 800 | old_id = base |
| 801 | new_id = base |
| 802 | for i in range(50): |
| 803 | content = f"v = {i}\n".encode() |
| 804 | old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id, message=f"add f{i}") |
| 805 | new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id, message=f"add f{i}") |
| 806 | t0 = time.monotonic() |
| 807 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 808 | elapsed = time.monotonic() - t0 |
| 809 | assert result.exit_code == 0 |
| 810 | assert elapsed < 2.0, f"range-diff took {elapsed:.2f}s — expected < 2s" |
| 811 | data = json.loads(result.stdout) |
| 812 | assert data["trivially_equivalent"] is True |
| 813 | assert len(data["pairs"]) == 50 |
| 814 | |
| 815 | def test_duration_ms_under_threshold(self, tmp_path: pathlib.Path) -> None: |
| 816 | root = _init_repo(tmp_path) |
| 817 | base = _commit(root, {"readme.txt": b"base\n"}, branch="main") |
| 818 | c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base) |
| 819 | n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base) |
| 820 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 821 | data = json.loads(result.stdout) |
| 822 | assert data["duration_ms"] < 2_000 |
| 823 | |
| 824 | |
| 825 | # --------------------------------------------------------------------------- |
| 826 | # Stress |
| 827 | # --------------------------------------------------------------------------- |
| 828 | |
| 829 | |
| 830 | class TestStress: |
| 831 | def test_50_commits_all_equivalent(self, tmp_path: pathlib.Path) -> None: |
| 832 | root = _init_repo(tmp_path) |
| 833 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 834 | old_id = base |
| 835 | new_id = base |
| 836 | for i in range(50): |
| 837 | content = f"v = {i}\n".encode() |
| 838 | old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id, message=f"add f{i}") |
| 839 | new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id, message=f"add f{i}") |
| 840 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 841 | assert result.exit_code == 0 |
| 842 | data = json.loads(result.stdout) |
| 843 | assert data["trivially_equivalent"] is True |
| 844 | assert len(data["pairs"]) == 50 |
| 845 | assert data["old_count"] == 50 |
| 846 | assert data["new_count"] == 50 |
| 847 | |
| 848 | def test_50_commits_mixed(self, tmp_path: pathlib.Path) -> None: |
| 849 | root = _init_repo(tmp_path) |
| 850 | base = _commit(root, {"base.py": b"base\n"}, branch="main") |
| 851 | old_id = base |
| 852 | new_id = base |
| 853 | # First 25: identical |
| 854 | for i in range(25): |
| 855 | content = f"v = {i}\n".encode() |
| 856 | old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id) |
| 857 | new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id) |
| 858 | # Next 25: different content |
| 859 | for i in range(25, 50): |
| 860 | old_id = _commit(root, {f"f{i}.py": f"old_{i}\n".encode()}, branch="old", parent_id=old_id) |
| 861 | new_id = _commit(root, {f"f{i}.py": f"new_{i}\n".encode()}, branch="new", parent_id=new_id) |
| 862 | # New has 10 extra commits |
| 863 | for i in range(50, 60): |
| 864 | new_id = _commit(root, {f"extra{i}.py": b"extra\n"}, branch="new", parent_id=new_id) |
| 865 | result = _invoke(root, f"{base}..old", f"{base}..new", "--json") |
| 866 | assert result.exit_code != 0 |
| 867 | data = json.loads(result.stdout) |
| 868 | equivalent = [p for p in data["pairs"] if p["status"] == "equivalent"] |
| 869 | assert len(equivalent) == 25 |
| 870 | added = [p for p in data["pairs"] if p["status"] == "added"] |
| 871 | assert len(added) == 10 |
| 872 | assert data["old_count"] == 50 |
| 873 | assert data["new_count"] == 60 |
| 874 | |
| 875 | |
| 876 | # --------------------------------------------------------------------------- |
| 877 | # TestRegisterFlags — argparse-level verification |
| 878 | # --------------------------------------------------------------------------- |
| 879 | |
| 880 | |
| 881 | class TestRegisterFlags: |
| 882 | """Verify that register() wires --json / -j correctly.""" |
| 883 | |
| 884 | def _make_parser(self) -> "argparse.ArgumentParser": |
| 885 | import argparse |
| 886 | from muse.cli.commands.range_diff import register |
| 887 | ap = argparse.ArgumentParser() |
| 888 | subs = ap.add_subparsers() |
| 889 | register(subs) |
| 890 | return ap |
| 891 | |
| 892 | def test_json_flag_long(self) -> None: |
| 893 | ns = self._make_parser().parse_args(["range-diff", "main..feat/x", "main..feat/y", "--json"]) |
| 894 | assert ns.json_out is True |
| 895 | |
| 896 | def test_j_alias(self) -> None: |
| 897 | ns = self._make_parser().parse_args(["range-diff", "main..feat/x", "main..feat/y", "-j"]) |
| 898 | assert ns.json_out is True |
| 899 | |
| 900 | def test_default_is_text(self) -> None: |
| 901 | ns = self._make_parser().parse_args(["range-diff", "main..feat/x", "main..feat/y"]) |
| 902 | assert ns.json_out is False |
| 903 | |
| 904 | def test_dest_is_json_out(self) -> None: |
| 905 | ns = self._make_parser().parse_args(["range-diff", "main..feat/x", "main..feat/y", "-j"]) |
| 906 | assert hasattr(ns, "json_out") |
| 907 | assert not hasattr(ns, "fmt") |