Skip to content

Commit 1fd2ee2

Browse files
committed
fix(verify): discover related tests across languages
Extend related test discovery beyond Python using conservative conventions for Go, Rust, JavaScript/TypeScript, C, and C++ test files. Keep Python's existing scoped pytest behavior, but continue running project-level verification for non-Python backends so file paths are not misused as backend-specific test selectors. Update post-verification logging to report related test hints separately from the actual project test command, and add regression coverage for multi-language discovery plus non-Python full-suite safety.
1 parent f546ec9 commit 1fd2ee2

4 files changed

Lines changed: 209 additions & 31 deletions

File tree

CoderMind/scripts/code_gen/post_verify.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,18 @@ def _git_diff_test_files(prefix: str = "tests/") -> list:
111111
# fall back to all tests so no regression goes undetected.
112112
test_files = _git_diff_test_files("tests/test_")
113113

114+
regular_file = not (task.file_path.startswith("<") and task.file_path.endswith(">"))
115+
backend_hint_files = test_files or ([task.file_path] if regular_file else None)
116+
backend = resolve_test_backend(valid_files=backend_hint_files, repo_path=repo_path)
117+
run_test_files = test_files if backend.name == "python" else None
118+
114119
logger.info(
115-
"Post-verification: running pytest on %s",
116-
test_files if test_files else "all tests",
120+
"Post-verification: related test files=%s; running %s project tests on %s",
121+
test_files if test_files else "none",
122+
backend.display_name,
123+
run_test_files if run_test_files else "all tests",
117124
)
118125

119-
backend = resolve_test_backend(valid_files=test_files or None, repo_path=repo_path)
120126
if backend.name == "python":
121127
try:
122128
ensure_deps_installed(repo_path)
@@ -125,7 +131,7 @@ def _git_diff_test_files(prefix: str = "tests/") -> list:
125131

126132
result = run_project_tests(
127133
repo_path,
128-
test_files=test_files or None,
134+
test_files=run_test_files,
129135
timeout=timeout,
130136
extra_args=[f"--timeout={DEFAULT_TEST_TIMEOUT}", "--timeout-method=thread"],
131137
backend=backend,

CoderMind/scripts/code_gen/test_runner.py

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -116,35 +116,22 @@ def find_test_files_in_directory(
116116
return sorted(test_files)
117117

118118

119-
def find_related_test_files(
120-
source_file: str,
121-
repo_root: Path
122-
) -> List[str]:
123-
"""Find test files related to a source file using path-signature matching.
124-
125-
Builds a canonical signature from the source path by stripping common
126-
prefixes (``src/``, ``lib/``) and the project-package directory, then
127-
joining remaining directory parts + stem with ``_``.
128-
129-
Example::
130-
131-
src/flask_blog/auth/views.py → signature "auth_views"
132-
tests/test_auth_views.py → match ✓
133-
134-
If no signature match is found, falls back to simple stem matching
135-
(the legacy behavior).
136-
137-
Args:
138-
source_file: Path to the source file (relative to repo root)
139-
repo_root: Repository root path
119+
def _existing_relative_paths(repo_root: Path, candidates: List[Path]) -> List[str]:
120+
"""Return existing candidate paths relative to ``repo_root`` without duplicates."""
121+
seen: Set[str] = set()
122+
found: List[str] = []
123+
for candidate in candidates:
124+
if not candidate.exists() or not candidate.is_file():
125+
continue
126+
rel = str(candidate.relative_to(repo_root))
127+
if rel not in seen:
128+
seen.add(rel)
129+
found.append(rel)
130+
return found
140131

141-
Returns:
142-
List of related test file paths (relative to repo root)
143-
"""
144-
source_path = Path(source_file)
145-
if source_path.suffix != '.py':
146-
return []
147132

133+
def _find_related_python_tests(source_path: Path, repo_root: Path) -> List[str]:
134+
"""Find Python tests related to ``source_path`` using legacy heuristics."""
148135
# --- Build canonical signature from source path ---
149136
# Strip known prefixes: "src", "lib"
150137
skip_prefixes = {'src', 'lib'}
@@ -197,6 +184,78 @@ def find_related_test_files(
197184
return related_tests
198185

199186

187+
def _find_related_non_python_tests(source_path: Path, repo_root: Path) -> List[str]:
188+
"""Find likely related non-Python test files using common naming conventions.
189+
190+
These are discovery hints only. Non-Python post-verification still runs the
191+
backend's project-level test command unless a backend-specific selector layer
192+
explicitly supports scoped execution.
193+
"""
194+
suffix = source_path.suffix.lower()
195+
stem = source_path.stem
196+
parent = repo_root / source_path.parent
197+
tests_dir = repo_root / "tests"
198+
199+
if suffix == ".go":
200+
return _existing_relative_paths(repo_root, [parent / f"{stem}_test.go"])
201+
202+
if suffix == ".rs":
203+
return _existing_relative_paths(repo_root, [
204+
parent / f"{stem}_test.rs",
205+
tests_dir / f"{stem}.rs",
206+
tests_dir / f"{stem}_test.rs",
207+
])
208+
209+
if suffix in {".ts", ".tsx", ".js", ".jsx"}:
210+
variants = [
211+
f"{stem}.test{suffix}",
212+
f"{stem}.spec{suffix}",
213+
]
214+
return _existing_relative_paths(repo_root, [
215+
*(parent / name for name in variants),
216+
*(parent / "__tests__" / name for name in variants),
217+
*(tests_dir / name for name in variants),
218+
*(tests_dir / source_path.parent.name / name for name in variants),
219+
])
220+
221+
c_suffixes = {".c": [".c"], ".cpp": [".cpp", ".cc", ".cxx"], ".cc": [".cc", ".cpp", ".cxx"], ".cxx": [".cxx", ".cpp", ".cc"]}
222+
if suffix in c_suffixes:
223+
names = []
224+
for ext in c_suffixes[suffix]:
225+
names.extend([f"test_{stem}{ext}", f"{stem}_test{ext}"])
226+
return _existing_relative_paths(repo_root, [
227+
*(parent / name for name in names),
228+
*(tests_dir / name for name in names),
229+
])
230+
231+
return []
232+
233+
234+
def find_related_test_files(
235+
source_file: str,
236+
repo_root: Path
237+
) -> List[str]:
238+
"""Find test files likely related to a source file.
239+
240+
Python keeps the legacy path-signature matching. Other languages use
241+
conservative file-name conventions (Go ``*_test.go``, JS/TS
242+
``*.test.*``/``*.spec.*``, C/C++ ``test_*``/``*_test`` and Rust common
243+
integration-test names). These results are discovery hints; backend-specific
244+
test execution decides whether scoped execution is safe.
245+
246+
Args:
247+
source_file: Path to the source file (relative to repo root)
248+
repo_root: Repository root path
249+
250+
Returns:
251+
List of related test file paths (relative to repo root)
252+
"""
253+
source_path = Path(source_file)
254+
if source_path.suffix == '.py':
255+
return _find_related_python_tests(source_path, repo_root)
256+
return _find_related_non_python_tests(source_path, repo_root)
257+
258+
200259
def extract_files_from_diff(diff_content: str) -> Tuple[List[str], List[str]]:
201260
"""Extract file paths from a git diff.
202261
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import os
2+
import sys
3+
from dataclasses import dataclass
4+
5+
# Ensure scripts/ is importable when tests run from the project root.
6+
_project_root = os.path.join(os.path.dirname(__file__), "..")
7+
sys.path.insert(0, os.path.join(_project_root, "scripts"))
8+
9+
from code_gen import post_verify as post_verify_module
10+
from code_gen.test_runner import TestResult as RunnerTestResult
11+
from common.task_batch import PlannedTask
12+
13+
14+
@dataclass
15+
class _Backend:
16+
name: str = "go"
17+
display_name: str = "Go"
18+
19+
20+
def test_post_verify_keeps_non_python_project_tests_unscoped(monkeypatch, tmp_path):
21+
source = tmp_path / "internal" / "store" / "store.go"
22+
test = tmp_path / "internal" / "store" / "store_test.go"
23+
source.parent.mkdir(parents=True)
24+
source.write_text("package store\n", encoding="utf-8")
25+
test.write_text("package store\n", encoding="utf-8")
26+
27+
task = PlannedTask(
28+
task="Implement store",
29+
file_path="internal/store/store.go",
30+
units_key=["Store"],
31+
unit_to_code={"Store": ""},
32+
unit_to_features={"Store": []},
33+
)
34+
35+
calls = {}
36+
37+
def fake_resolve_test_backend(valid_files=None, repo_path=None):
38+
calls["valid_files"] = valid_files
39+
return _Backend()
40+
41+
def fake_run_project_tests(repo_root, test_files=None, timeout=300, extra_args=None, env=None, backend=None):
42+
calls["run_test_files"] = test_files
43+
calls["backend"] = backend
44+
return RunnerTestResult(success=True, return_code=0, output="ok", test_files=test_files or [])
45+
46+
monkeypatch.setattr(post_verify_module, "resolve_test_backend", fake_resolve_test_backend)
47+
monkeypatch.setattr(post_verify_module, "run_project_tests", fake_run_project_tests)
48+
49+
passed, summary = post_verify_module.post_verify(tmp_path, task)
50+
51+
assert passed is True
52+
assert "passed=0" in summary
53+
assert calls["valid_files"] == ["internal/store/store_test.go"]
54+
assert calls["run_test_files"] is None
55+
assert calls["backend"].name == "go"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
import sys
3+
4+
# Ensure scripts/ is importable when tests run from the project root.
5+
_project_root = os.path.join(os.path.dirname(__file__), "..")
6+
sys.path.insert(0, os.path.join(_project_root, "scripts"))
7+
8+
from code_gen.test_runner import find_related_test_files
9+
10+
11+
def _write(path, text=""):
12+
path.parent.mkdir(parents=True, exist_ok=True)
13+
path.write_text(text, encoding="utf-8")
14+
15+
16+
def test_find_related_go_test_file(tmp_path):
17+
_write(tmp_path / "internal" / "store" / "store.go")
18+
_write(tmp_path / "internal" / "store" / "store_test.go")
19+
20+
assert find_related_test_files("internal/store/store.go", tmp_path) == [
21+
"internal/store/store_test.go"
22+
]
23+
24+
25+
def test_find_related_typescript_test_file(tmp_path):
26+
_write(tmp_path / "src" / "client.ts")
27+
_write(tmp_path / "src" / "client.test.ts")
28+
29+
assert find_related_test_files("src/client.ts", tmp_path) == [
30+
"src/client.test.ts"
31+
]
32+
33+
34+
def test_find_related_c_test_file(tmp_path):
35+
_write(tmp_path / "src" / "task.c")
36+
_write(tmp_path / "tests" / "test_task.c")
37+
38+
assert find_related_test_files("src/task.c", tmp_path) == [
39+
"tests/test_task.c"
40+
]
41+
42+
43+
def test_find_related_cpp_test_file(tmp_path):
44+
_write(tmp_path / "src" / "task.cpp")
45+
_write(tmp_path / "tests" / "task_test.cpp")
46+
47+
assert find_related_test_files("src/task.cpp", tmp_path) == [
48+
"tests/task_test.cpp"
49+
]
50+
51+
52+
def test_find_related_rust_test_file(tmp_path):
53+
_write(tmp_path / "src" / "lib.rs")
54+
_write(tmp_path / "tests" / "lib_test.rs")
55+
56+
assert find_related_test_files("src/lib.rs", tmp_path) == [
57+
"tests/lib_test.rs"
58+
]

0 commit comments

Comments
 (0)