-
Notifications
You must be signed in to change notification settings - Fork 4
Fix/thread project root into verification #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ class TestSuiteGate(BaseCheck): | |
|
|
||
| def __init__(self, project_root: Optional[Path] = None): | ||
| self.project_root = project_root | ||
| self._test_file_cache: Optional[Dict[str, List[Path]]] = None | ||
| self._all_test_files: Optional[List[Path]] = None | ||
|
|
||
| def verify(self, original: str, transformed: str, file_path: Path) -> CheckResult: | ||
| start = time.monotonic() | ||
|
|
@@ -110,11 +112,27 @@ def _find_relevant_tests(self, file_path: Path) -> List[Path]: | |
| module_name = file_path.stem | ||
| search_root = self.project_root or file_path.parent | ||
|
Comment on lines
112
to
113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cache key collision can return wrong test files for same-stem modules.
💡 Proposed fix- module_name = file_path.stem
+ module_name = file_path.stem
+ cache_key = str(file_path.resolve())
search_root = self.project_root or file_path.parent
@@
- if module_name in self._test_file_cache:
- return self._test_file_cache[module_name]
+ if cache_key in self._test_file_cache:
+ return self._test_file_cache[cache_key]
@@
- self._test_file_cache[module_name] = test_files
+ self._test_file_cache[cache_key] = test_files
return test_filesAlso applies to: 131-133, 145-146 🤖 Prompt for AI Agents |
||
|
|
||
| if self._test_file_cache is None: | ||
| self._test_file_cache = {} | ||
| self._all_test_files = [] | ||
|
|
||
| test_dirs = [d for d in [search_root / "tests", search_root / "test"] if d.is_dir()] | ||
| search_dirs = test_dirs if test_dirs else [search_root] | ||
| excluded_dirs = {".git", ".rag", "__pycache__", "venv", ".venv", "env", "node_modules"} | ||
|
|
||
| for root_dir in search_dirs: | ||
|
Comment on lines
+118
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove trailing whitespace in this hunk to unblock pre-commit. Pre-commit is currently failing on Also applies to: 130-131, 144-145 🤖 Prompt for AI Agents |
||
| for py_file in root_dir.rglob("*.py"): | ||
| if any(excluded in py_file.parts for excluded in excluded_dirs): | ||
| continue | ||
| name = py_file.name | ||
| if name.startswith("test_") or name.endswith("_test.py"): | ||
| self._all_test_files.append(py_file) | ||
|
|
||
| if module_name in self._test_file_cache: | ||
| return self._test_file_cache[module_name] | ||
|
|
||
| test_files: List[Path] = [] | ||
| for py_file in search_root.rglob("*.py"): | ||
| name = py_file.name | ||
| if not (name.startswith("test_") or name.endswith("_test.py")): | ||
| continue | ||
| for py_file in self._all_test_files: # type: ignore | ||
| if py_file == file_path: | ||
| continue | ||
| try: | ||
|
|
@@ -123,6 +141,8 @@ def _find_relevant_tests(self, file_path: Path) -> List[Path]: | |
| test_files.append(py_file) | ||
| except Exception: | ||
| continue | ||
|
|
||
| self._test_file_cache[module_name] = test_files | ||
| return test_files | ||
|
|
||
| @staticmethod | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| """Tests for project_root threading into verification from AutoFixEngine.fix_file(). | ||
|
|
||
| Covers: | ||
| - _discover_project_root() walks up to the nearest VCS/project marker | ||
| - _discover_project_root() falls back to the file's directory when no marker exists | ||
| - fix_file(verify=True) passes an explicit project_root to VerificationEngine | ||
| - fix_file(verify=True) discovers the root when none is supplied, | ||
| instead of always using file_path.parent | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from refactron.autofix import engine as engine_module | ||
| from refactron.autofix.engine import AutoFixEngine, _discover_project_root | ||
| from refactron.core.models import CodeIssue, IssueCategory, IssueLevel | ||
|
|
||
|
|
||
| def _trailing_ws_issue(file_path: Path) -> CodeIssue: | ||
| return CodeIssue( | ||
| rule_id="remove_trailing_whitespace", | ||
| message="Trailing whitespace detected", | ||
| file_path=file_path, | ||
| line_number=1, | ||
| category=IssueCategory.STYLE, | ||
| level=IssueLevel.WARNING, | ||
| ) | ||
|
|
||
|
|
||
| # ─── _discover_project_root ────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def test_discover_project_root_finds_vcs_root(tmp_path): | ||
| """A nested file resolves to the ancestor that holds the .git directory.""" | ||
| (tmp_path / ".git").mkdir() | ||
| nested = tmp_path / "services" / "api" / "src" | ||
| nested.mkdir(parents=True) | ||
| file_path = nested / "module.py" | ||
| file_path.write_text("x = 1\n", encoding="utf-8") | ||
|
|
||
| assert _discover_project_root(file_path) == tmp_path | ||
|
|
||
|
|
||
| def test_discover_project_root_finds_pyproject(tmp_path): | ||
| """pyproject.toml is treated as a project-root marker.""" | ||
| (tmp_path / "pyproject.toml").write_text("[project]\n", encoding="utf-8") | ||
| nested = tmp_path / "pkg" | ||
| nested.mkdir() | ||
| file_path = nested / "module.py" | ||
| file_path.write_text("x = 1\n", encoding="utf-8") | ||
|
|
||
| assert _discover_project_root(file_path) == tmp_path | ||
|
|
||
|
|
||
| def test_discover_project_root_falls_back_to_file_dir(tmp_path): | ||
| """With no marker anywhere, the root falls back to the file's own directory.""" | ||
| nested = tmp_path / "loose" | ||
| nested.mkdir() | ||
| file_path = nested / "module.py" | ||
| file_path.write_text("x = 1\n", encoding="utf-8") | ||
|
|
||
| assert _discover_project_root(file_path) == nested | ||
|
|
||
|
|
||
| # ─── fix_file → VerificationEngine project_root ────────────────────────────── | ||
|
|
||
|
|
||
| class _RecordingVerificationEngine: | ||
| """Stand-in that records the project_root it was constructed with.""" | ||
|
|
||
| last_project_root = None | ||
|
|
||
| def __init__(self, project_root=None, checks=None): | ||
| type(self).last_project_root = project_root | ||
|
|
||
| def verify(self, original, transformed, file_path): | ||
| # Always allow the transform through so fix_file proceeds normally. | ||
| class _Result: | ||
| safe_to_apply = True | ||
| blocking_reason = None | ||
|
|
||
| return _Result() | ||
|
|
||
|
|
||
| def _patch_verification_engine(monkeypatch): | ||
| """Route `from refactron.verification import VerificationEngine` to the recorder.""" | ||
| import refactron.verification as verification_pkg | ||
|
|
||
| _RecordingVerificationEngine.last_project_root = None | ||
| monkeypatch.setattr(verification_pkg, "VerificationEngine", _RecordingVerificationEngine) | ||
|
|
||
|
|
||
| def test_fix_file_passes_explicit_project_root(tmp_path, monkeypatch): | ||
| """An explicit project_root must reach VerificationEngine unchanged.""" | ||
| _patch_verification_engine(monkeypatch) | ||
|
|
||
| nested = tmp_path / "a" / "b" | ||
| nested.mkdir(parents=True) | ||
| file_path = nested / "module.py" | ||
| file_path.write_text("x = 1 \n", encoding="utf-8") # trailing whitespace | ||
|
|
||
| AutoFixEngine().fix_file( | ||
| file_path, | ||
| [_trailing_ws_issue(file_path)], | ||
| dry_run=True, | ||
| verify=True, | ||
| project_root=tmp_path, | ||
| ) | ||
|
|
||
| assert _RecordingVerificationEngine.last_project_root == tmp_path | ||
|
|
||
|
|
||
| def test_fix_file_discovers_root_when_none_given(tmp_path, monkeypatch): | ||
| """Without an explicit root, fix_file discovers the VCS root, not file_path.parent.""" | ||
| _patch_verification_engine(monkeypatch) | ||
|
|
||
| (tmp_path / ".git").mkdir() | ||
| nested = tmp_path / "deep" / "nested" | ||
| nested.mkdir(parents=True) | ||
| file_path = nested / "module.py" | ||
| file_path.write_text("x = 1 \n", encoding="utf-8") # trailing whitespace | ||
|
|
||
| AutoFixEngine().fix_file( | ||
| file_path, | ||
| [_trailing_ws_issue(file_path)], | ||
| dry_run=True, | ||
| verify=True, | ||
| ) | ||
|
|
||
| # The discovered root must be the repo root, not the file's parent directory. | ||
| assert _RecordingVerificationEngine.last_project_root == tmp_path | ||
| assert _RecordingVerificationEngine.last_project_root != file_path.parent | ||
|
|
||
|
|
||
| def test_engine_module_exposes_discover_helper(): | ||
| """_discover_project_root is importable from the engine module.""" | ||
| assert hasattr(engine_module, "_discover_project_root") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add type annotation to module constant.
The constant
_PROJECT_ROOT_MARKERSlacks a type annotation. As per coding guidelines, type annotations are required inrefactron/with mypydisallow_untyped_defs = trueenabled.📝 Proposed fix
🤖 Prompt for AI Agents