diff --git a/tests/categorizer.py b/tests/categorizer.py index 2716e841..1ab54fa1 100644 --- a/tests/categorizer.py +++ b/tests/categorizer.py @@ -1,5 +1,7 @@ +import ast from dataclasses import dataclass, field from enum import Enum +from pathlib import Path class FixtureClass(str, Enum): UNIT = "unit" @@ -26,3 +28,50 @@ class CategoryRecord: test_order: dict[str, int] = field(default_factory=dict) source: str = "auto" warnings: list[str] = field(default_factory=list) + +_OPT_IN_PREFIXES: tuple[str, ...] = ("test_clean_install", "test_docker_build") +_PERF_KEYWORDS: tuple[str, ...] = ("perf", "stress", "phase_3_final", "phase_4_stress") +_FIXTURE_ARGS: dict[FixtureClass, frozenset[str]] = { + FixtureClass.LIVE_GUI: frozenset({"live_gui"}), + FixtureClass.MOCK_APP: frozenset({"mock_app", "app_instance"}), +} + +def _fixture_args_used(source: str) -> set[str]: + try: + tree = ast.parse(source) + except SyntaxError: + return set() + found: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + found.update(a.arg for a in node.args.args) + found.update(a.arg for a in node.args.kwonlyargs) + found.update(a.arg for a in node.args.posonlyargs) + return found + +def _classify_fixture_class(path: Path, source: str) -> FixtureClass: + name = path.name + for prefix in _OPT_IN_PREFIXES: + if name.startswith(prefix): + return FixtureClass.OPT_IN + args = _fixture_args_used(source) + for fc, fixture_names in _FIXTURE_ARGS.items(): + if args & fixture_names: + return fc + lowered = name.lower() + for kw in _PERF_KEYWORDS: + if kw in lowered: + return FixtureClass.PERFORMANCE + return FixtureClass.UNIT + +def auto_classify(path: Path, durations: dict[str, float] | None = None) -> CategoryRecord: + source = path.read_text(encoding="utf-8", errors="replace") + fixture_class = _classify_fixture_class(path, source) + return CategoryRecord( + filename=path.name, + fixture_class=fixture_class, + subsystems=[], + speed=Speed.MEDIUM, + batch_group="", + source="auto", + )