screen-locker/scripts/split_dart_test.py
Krzysztof kuhy Rudnicki 99302c2be7 Add todo_finish_dart_250_prompt.md and the Dart test splitter
A self-contained prompt for a fresh session to finish the last 10 files over
the 250-line cap, all Dart. It carries the parts that are expensive to
rediscover:

- scripts/split_dart_test.py, which works, plus the envelope it works in:
  one pass from the pristine file, no post-main() declarations, no local
  function declarations in the preamble. All four remaining test files sit
  outside that envelope, and the fix (hoist shared fakes into a
  _fixtures.dart sibling first, as its own commit) is written down.
- The three Dart constraints each of which cost a debugging cycle here:
  setState is @protected so it cannot be called from an extension; extension
  methods dispatch statically so a method a test fake overrides must stay in
  the class (this hung four tests on pumpAndSettle rather than failing an
  assertion); static members need qualifying inside an extension.
- 402 as the test-count invariant, and the note that dart analyze is a no-op
  on test/ so the count is the only signal there.
- That the local gatelock (0.5.0) is ahead of the pin (0.4.0), so the popup
  gate's failure here is environment drift and must NOT be "fixed" in code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:49:15 +02:00

76 lines
2.4 KiB
Python

"""Split a Dart test file at top-level `group(` boundaries.
Each output file reuses the original's imports and its main() setup preamble
(everything between `void main() {` and the first top-level group), so every
part runs the same fixtures the original did.
Usage:
split_dart_test.py <file> <out1>:<firstGroupIdx> [<out2>:<idx> ...]
Indices are 0-based positions in the top-level group list; each output takes
groups[idx:next_idx]. The source file keeps groups[:first_idx].
"""
from __future__ import annotations
from pathlib import Path
import re
import sys
def top_level_groups(lines: list[str]) -> list[int]:
"""Return 0-based line indices of every top-level test boundary.
Screen tests are often a flat list of `testWidgets(` calls with no
`group(` at all, so both count as a split point.
"""
return [
i
for i, ln in enumerate(lines)
if re.match(r"^ (group|testWidgets|test)\(", ln)
]
def split(path: Path, specs: list[tuple[Path, int]]) -> None:
"""Write each spec's group range to its own file, trimming the source."""
lines = path.read_text().splitlines(keepends=True)
groups = top_level_groups(lines)
main_idx = next(i for i, ln in enumerate(lines) if ln.startswith("void main("))
header = "".join(lines[:main_idx])
preamble = "".join(lines[main_idx + 1 : groups[0]])
# Fixtures and fake classes often sit AFTER main(). Every split file needs
# them too, or it fails to compile on a helper the original defined once.
main_end = next(
(i for i in range(len(lines) - 1, main_idx, -1) if lines[i].startswith("}")),
len(lines) - 1,
)
trailer = "".join(lines[main_end + 1 :])
bounds = [i for _, i in specs] + [len(groups)]
for (out, start), end in zip(specs, bounds[1:], strict=False):
first = groups[start]
last = groups[end] if end < len(groups) else main_end
body = "".join(lines[first:last]).rstrip("\n")
out.write_text(f"{header}void main() {{\n{preamble}{body}\n}}\n{trailer}")
keep = groups[specs[0][1]]
path.write_text(
"".join(lines[:keep]).rstrip("\n") + "\n}\n" + trailer,
)
def main() -> int:
"""Parse argv and run the split."""
src = Path(sys.argv[1])
specs = []
for arg in sys.argv[2:]:
name, idx = arg.rsplit(":", 1)
specs.append((src.parent / name, int(idx)))
split(src, specs)
return 0
if __name__ == "__main__":
sys.exit(main())