Skip to content

Commit ea0343e

Browse files
committed
feat(test): robust changed-file detection (CI list + self-healing git)
1 parent c99289b commit ea0343e

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

toolchain/mfc/test/coverage.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import gzip
99
import hashlib
1010
import json
11+
import subprocess
1112
from pathlib import Path
1213
from typing import Optional, Tuple
1314

@@ -115,3 +116,39 @@ def select_tests(cases, coverage_map, changed_files):
115116
else: # rung 7: skip
116117
skipped.append(case)
117118
return to_run, skipped, f"selected {len(to_run)}/{len(cases)} by coverage overlap"
119+
120+
121+
def _git(args, cwd, timeout=60):
122+
return subprocess.run(["git", *args], capture_output=True, text=True, cwd=cwd, timeout=timeout, check=False)
123+
124+
125+
def _merge_base(cwd, branch):
126+
for ref in (branch, f"origin/{branch}"):
127+
r = _git(["merge-base", ref, "HEAD"], cwd)
128+
if r.returncode == 0 and r.stdout.strip():
129+
return r.stdout.strip()
130+
return None
131+
132+
133+
def get_changed_files(root_dir, compare_branch="master", explicit: Optional[str] = None):
134+
"""Set of changed repo-relative paths, or None if undeterminable (-> run all).
135+
136+
`explicit` is a newline-separated list from CI (paths-filter); preferred when given.
137+
Otherwise use git merge-base, self-healing a shallow clone with a deepen+retry.
138+
"""
139+
if explicit is not None:
140+
return {f for f in explicit.splitlines() if f.strip()}
141+
try:
142+
base = _merge_base(root_dir, compare_branch)
143+
if base is None:
144+
_git(["fetch", "origin", f"{compare_branch}:{compare_branch}", "--depth=1"], root_dir, 120)
145+
_git(["fetch", "--deepen=200"], root_dir, 120)
146+
base = _merge_base(root_dir, compare_branch)
147+
if base is None:
148+
return None
149+
diff = _git(["diff", base, "HEAD", "--name-only", "--no-color"], root_dir)
150+
if diff.returncode != 0:
151+
return None
152+
return {f for f in diff.stdout.splitlines() if f.strip()}
153+
except (subprocess.TimeoutExpired, OSError):
154+
return None

toolchain/mfc/test/test_coverage_unit.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import tempfile
2+
import types as _types
23
from pathlib import Path
4+
from unittest.mock import patch
35

4-
from mfc.test.coverage import is_always_run_all, load_map, param_hash, save_map, select_tests
6+
from mfc.test.coverage import get_changed_files, is_always_run_all, load_map, param_hash, save_map, select_tests
57

68

79
def test_param_hash_is_order_independent():
@@ -134,3 +136,35 @@ def test_case_coverage_key_ignores_trace():
134136
a = TestCase("1D -> Foo", {"m": 100})
135137
b = TestCase("totally -> different -> trace", {"m": 100})
136138
assert a.coverage_key() == b.coverage_key()
139+
140+
141+
def test_changed_files_prefers_explicit_list():
142+
files = get_changed_files("/repo", "master", explicit="src/a.fpp\nsrc/b.fpp\n")
143+
assert files == {"src/a.fpp", "src/b.fpp"}
144+
145+
146+
def test_changed_files_deepens_then_recovers():
147+
state = {"deepened": False}
148+
149+
def fake_run(cmd, **kw):
150+
sub = cmd[1] if len(cmd) > 1 else ""
151+
if sub == "fetch":
152+
state["deepened"] = True
153+
return _types.SimpleNamespace(returncode=0, stdout="", stderr="")
154+
if sub == "merge-base":
155+
return _types.SimpleNamespace(returncode=0 if state["deepened"] else 1, stdout="base\n", stderr="")
156+
if sub == "diff":
157+
return _types.SimpleNamespace(returncode=0, stdout="src/x.fpp\n", stderr="")
158+
return _types.SimpleNamespace(returncode=0, stdout="", stderr="")
159+
160+
with patch("subprocess.run", fake_run):
161+
assert get_changed_files("/repo", "master") == {"src/x.fpp"}
162+
163+
164+
def test_changed_files_returns_none_when_unrecoverable():
165+
def fake_run(cmd, **kw):
166+
rc = 1 if (len(cmd) > 1 and cmd[1] == "merge-base") else 0
167+
return _types.SimpleNamespace(returncode=rc, stdout="", stderr="boom")
168+
169+
with patch("subprocess.run", fake_run):
170+
assert get_changed_files("/repo", "master") is None

0 commit comments

Comments
 (0)