83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
# file: scripts/audit_rust_test_layout.py
|
|
# version: 1
|
|
|
|
"""Audit the Knowledge Rust unit/integration test layout."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
_TEST_ATTRIBUTE = re.compile(r"(?m)^\s*#\s*\[\s*test\s*\]")
|
|
_PATH_ATTRIBUTE = re.compile(r'#\s*\[\s*path\s*=\s*"([^"]+)"\s*\]')
|
|
_FORBIDDEN_MODULE = re.compile(r"(?m)^\s*mod\s+unit_tests\s*;")
|
|
|
|
|
|
def audit_crate(crate_root: pathlib.Path) -> list[str]:
|
|
"""Return layout violations for one crate."""
|
|
errors: list[str] = []
|
|
src_root = crate_root / "src"
|
|
referenced_unit_tests: set[pathlib.Path] = set()
|
|
|
|
if src_root.is_dir():
|
|
for source in sorted(src_root.rglob("*.rs")):
|
|
text = source.read_text(encoding="utf-8")
|
|
relative = source.relative_to(crate_root)
|
|
|
|
if _TEST_ATTRIBUTE.search(text):
|
|
errors.append(f"{crate_root.name}/{relative}: #[test] must live under unit_tests/")
|
|
|
|
if source.name in {"tests.rs", "unit_tests.rs"}:
|
|
errors.append(f"{crate_root.name}/{relative}: test implementation file must not live under src/")
|
|
|
|
if _FORBIDDEN_MODULE.search(text):
|
|
errors.append(f"{crate_root.name}/{relative}: use #[path = \"../unit_tests/...\"] instead of mod unit_tests;")
|
|
|
|
for match in _PATH_ATTRIBUTE.finditer(text):
|
|
candidate = (source.parent / match.group(1)).resolve()
|
|
try:
|
|
candidate.relative_to((crate_root / "unit_tests").resolve())
|
|
except ValueError:
|
|
continue
|
|
referenced_unit_tests.add(candidate)
|
|
|
|
unit_root = crate_root / "unit_tests"
|
|
if unit_root.is_dir():
|
|
for test_file in sorted(unit_root.rglob("*.rs")):
|
|
resolved = test_file.resolve()
|
|
if resolved not in referenced_unit_tests:
|
|
relative = test_file.relative_to(crate_root)
|
|
errors.append(f"{crate_root.name}/{relative}: unit test file is not attached by any #[path] module")
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the audit."""
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("root", nargs="?", default="crates")
|
|
arguments = parser.parse_args()
|
|
|
|
root = pathlib.Path(arguments.root)
|
|
errors: list[str] = []
|
|
crate_count = 0
|
|
|
|
for manifest in sorted(root.glob("*/Cargo.toml")):
|
|
crate_count += 1
|
|
errors.extend(audit_crate(manifest.parent))
|
|
|
|
if errors:
|
|
for error in errors:
|
|
print(error, file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Rust test-layout audit: clean ({crate_count} crate(s))")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|