From a97af695432f799fdaff5abcc0d0455bebf634cd Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Fri, 17 Jul 2026 12:21:00 +0200 Subject: [PATCH] Add rule and checks to validate no test cross-imports are performed --- .cursor/rules/pr-review.mdc | 8 ++- .cursor/rules/repository-structure.mdc | 7 ++- tests/test_the_test/test_conventions.py | 70 ++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/pr-review.mdc b/.cursor/rules/pr-review.mdc index c773de337f4..9d811e7ee0f 100644 --- a/.cursor/rules/pr-review.mdc +++ b/.cursor/rules/pr-review.mdc @@ -58,7 +58,13 @@ When adding a new weblog, verify the name is unique across all languages. Search - Ref: [build.md](mdc:docs/execute/build.md) -## 10. Manifest YAML Syntax +## 10. No Cross-Test-File Imports + +A `test_*.py` file must never import from another `test_*.py` file (absolute like `from tests.xxx.test_yyy import ...` or relative like `from .test_yyy import ...`). If logic is shared between test files, it must be moved to a local `utils.py` file in the same folder instead. Flag any PR that adds such an import. + +- Ref: [repository-structure.mdc](mdc:.cursor/rules/repository-structure.mdc), enforced by `tests/test_the_test/test_conventions.py::test_no_cross_test_file_imports` + +## 11. Manifest YAML Syntax - `bug` and `flaky` markers must include a JIRA ticket (e.g., `bug (JIRA-123)`) - Values with special YAML characters (`>`, `<`, `:`, `#`) must be quoted diff --git a/.cursor/rules/repository-structure.mdc b/.cursor/rules/repository-structure.mdc index a96fcad25a3..ce4ed1083ea 100644 --- a/.cursor/rules/repository-structure.mdc +++ b/.cursor/rules/repository-structure.mdc @@ -119,4 +119,9 @@ system-tests/ def test_XYZ(self): ... ``` -- Never define a setup method without a matching test method. \ No newline at end of file +- Never define a setup method without a matching test method. + +## 6. No Cross-Test-File Imports +- A test file (`test_*.py`) must never import anything from another test file, whether via an absolute (`from tests.xxx.test_yyy import ...`) or relative (`from .test_yyy import ...`) import. +- If logic needs to be shared between test files, move it into a local `utils.py` file in the same folder and have both test files import from there. +- This is enforced by `tests/test_the_test/test_conventions.py::test_no_cross_test_file_imports`. \ No newline at end of file diff --git a/tests/test_the_test/test_conventions.py b/tests/test_the_test/test_conventions.py index bf3577db1e0..92292bc3224 100644 --- a/tests/test_the_test/test_conventions.py +++ b/tests/test_the_test/test_conventions.py @@ -1,5 +1,7 @@ +import ast import os -from utils import scenarios +from pathlib import Path +from utils import scenarios, logger @scenarios.test_the_test @@ -26,5 +28,71 @@ def test_utils(): raise ValueError(f"File {os.path.join(folder, file)} is not a test file or a utils file {folder}") +def _is_test_file(path: Path) -> bool: + return path.suffix == ".py" and path.name.startswith("test_") + + +def _resolve_import_target(current_file: Path, module: str | None, level: int) -> Path | None: + """Resolve an import (relative or absolute) to the .py file path it points at, if any.""" + if not module: + # `from . import something` / `from .. import something`: imports a package, not a specific file + return None + + if level > 0: + directory = current_file.parent + for _ in range(level - 1): + directory = directory.parent + return directory.joinpath(*module.split(".")).with_suffix(".py") + + if not module.startswith("tests."): + # not an import of another test module, nothing to resolve + return None + + return Path(*module.split(".")).with_suffix(".py") + + +@scenarios.test_the_test +def test_no_cross_test_file_imports(): + """A test file must never import from another test file. If logic is shared, it must live in a local utils.py.""" + + has_error = False + + for folder, _, files in os.walk("tests"): + if folder.startswith("tests/fuzzer"): + # do not check these folders, they are particular use cases + continue + + for file in files: + if not file.startswith("test_") or not file.endswith(".py"): + continue + + path = Path(folder, file) + + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + targets = [_resolve_import_target(path, node.module, node.level)] + elif isinstance(node, ast.Import): + targets = [_resolve_import_target(path, alias.name, 0) for alias in node.names] + else: + continue + + for target in targets: + if target is None or target == path: + continue + + if _is_test_file(target): + logger.error(f"{path} imports from another test file: {target}") + has_error = True + + if has_error: + raise ValueError( + "Some test file imports from another test file. " + "Shared logic must be moved to a local utils.py file instead." + ) + + if __name__ == "__main__": test_utils() + test_no_cross_test_file_imports()