|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import itertools |
3 | 4 | import re |
4 | 5 | import subprocess |
5 | 6 | import sys |
@@ -243,6 +244,55 @@ def test_diff_context_merges_contiguous_fragments(tmp_path): |
243 | 244 | assert start <= end, f"fragment line range inverted: {f['lines']}" |
244 | 245 |
|
245 | 246 |
|
| 247 | +def test_diff_context_no_contained_or_unmerged_fragments(tmp_path): |
| 248 | + """Regression (f1a1647d): the render merge pass must, per (path, role), |
| 249 | + drop any fragment fully contained in another and merge line-contiguous |
| 250 | + runs. A change editing several functions in one file produces multiple |
| 251 | + same-role fragments, exercising that pass; the output must never carry a |
| 252 | + range nested inside a sibling nor an unmerged `next.start == cur.end + 1` |
| 253 | + pair (both are pure per-fragment scaffolding duplication).""" |
| 254 | + import json |
| 255 | + from collections import defaultdict |
| 256 | + |
| 257 | + repo = Pygit2Repo(tmp_path / "repo") |
| 258 | + base_src = "".join(f"def f{i}(x):\n y = x + {i}\n return y\n\n" for i in range(12)) |
| 259 | + repo.add_file("src/mod.py", base_src) |
| 260 | + repo.add_file("src/other.py", "from mod import f0, f5, f11\n\ndef run():\n return f0(1) + f5(2) + f11(3)\n") |
| 261 | + base = repo.commit("init") |
| 262 | + edited_src = "".join( |
| 263 | + ( |
| 264 | + f"def f{i}(x):\n y = x * {i}\n z = y + 1\n return z\n\n" |
| 265 | + if i in (2, 5, 9) |
| 266 | + else f"def f{i}(x):\n y = x + {i}\n return y\n\n" |
| 267 | + ) |
| 268 | + for i in range(12) |
| 269 | + ) |
| 270 | + repo.add_file("src/mod.py", edited_src) |
| 271 | + head = repo.commit("edit several functions") |
| 272 | + |
| 273 | + stdout, _ = _run(repo.path, [".", "--diff", f"{base}..{head}", "--budget", "8000", "-f", "json"]) |
| 274 | + fragments = json.loads(stdout)["fragments"] |
| 275 | + |
| 276 | + by_key: dict[tuple[str, str | None], list[tuple[int, int]]] = defaultdict(list) |
| 277 | + for f in fragments: |
| 278 | + start, end = (int(x) for x in f["lines"].split("-")) |
| 279 | + by_key[(f["path"], f.get("role"))].append((start, end)) |
| 280 | + |
| 281 | + assert any( |
| 282 | + len(ranges) >= 2 for ranges in by_key.values() |
| 283 | + ), "fixture must yield a multi-fragment group to exercise the merge pass" |
| 284 | + |
| 285 | + for (path, role), ranges in by_key.items(): |
| 286 | + ordered = sorted(ranges) |
| 287 | + for (a_start, a_end), (b_start, b_end) in itertools.pairwise(ordered): |
| 288 | + assert not ( |
| 289 | + b_start >= a_start and b_end <= a_end |
| 290 | + ), f"contained fragment {(b_start, b_end)} inside {(a_start, a_end)} for {path} role={role}" |
| 291 | + assert ( |
| 292 | + b_start != a_end + 1 |
| 293 | + ), f"unmerged contiguous fragments {(a_start, a_end)} and {(b_start, b_end)} for {path} role={role}" |
| 294 | + |
| 295 | + |
246 | 296 | def test_diff_context_scopes_markdown_preamble_change_not_whole_file(tmp_path): |
247 | 297 | """Regression (#91): a change to a lone H1's own preamble (before its |
248 | 298 | first `##` child heading) used to select a fragment spanning the H1's |
|
0 commit comments