80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""File-size and naming validation for the managed testing surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def _expand_globs(root: Path, patterns: Iterable[str]) -> list[Path]:
|
|
"""Expand a set of relative glob patterns to unique file paths."""
|
|
matched: set[Path] = set()
|
|
for pattern in patterns:
|
|
matched.update(path for path in root.glob(pattern) if path.is_file())
|
|
return sorted(matched)
|
|
|
|
|
|
def _is_unchanged_legacy(
|
|
config: dict[str, Any], root: Path, path: Path, line_count: int
|
|
) -> bool:
|
|
"""Allow an over-cap legacy file only while its exact bytes stay unchanged."""
|
|
relative = path.relative_to(root).as_posix()
|
|
record = config.get("legacy_line_exceptions", {}).get(relative)
|
|
if not isinstance(record, dict) or record.get("lines") != line_count:
|
|
return False
|
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
return record.get("sha256") == digest
|
|
|
|
|
|
def run_check(contract: dict[str, Any], root: Path) -> list[str]:
|
|
"""Return human-readable issues for naming and file-size rules."""
|
|
config = contract.get("hygiene", {})
|
|
max_lines = int(config.get("max_lines", 500))
|
|
issues: list[str] = []
|
|
|
|
for path in _expand_globs(root, config.get("line_limit_globs", [])):
|
|
line_count = sum(1 for _ in path.open("r", encoding="utf-8"))
|
|
if line_count > max_lines and not _is_unchanged_legacy(
|
|
config, root, path, line_count
|
|
):
|
|
issues.append(
|
|
f"file exceeds {max_lines} LOC: {path.relative_to(root)} ({line_count})"
|
|
)
|
|
|
|
for rule in config.get("naming_rules", []):
|
|
pattern = re.compile(rule["pattern"])
|
|
for path in _expand_globs(root, [rule["glob"]]):
|
|
if path.name == "conftest.py":
|
|
continue
|
|
if not pattern.match(path.name):
|
|
issues.append(
|
|
f"naming rule failed ({rule['description']}): {path.relative_to(root)}"
|
|
)
|
|
|
|
return issues
|
|
|
|
|
|
def count_files_over_line_limit(contract: dict[str, Any], root: Path) -> int:
|
|
"""Return the number of managed files that exceed the configured LOC cap."""
|
|
|
|
config = contract.get("hygiene", {})
|
|
max_lines = int(config.get("max_lines", 500))
|
|
count = 0
|
|
for path in _expand_globs(root, config.get("line_limit_globs", [])):
|
|
line_count = sum(1 for _ in path.open("r", encoding="utf-8"))
|
|
if line_count > max_lines and not _is_unchanged_legacy(
|
|
config, root, path, line_count
|
|
):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def count_files_with_line_limit(contract: dict[str, Any], root: Path) -> int:
|
|
"""Return the number of managed files included in the LOC cap."""
|
|
|
|
config = contract.get("hygiene", {})
|
|
return len(_expand_globs(root, config.get("line_limit_globs", [])))
|