mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
205 lines
6.4 KiB
Python
205 lines
6.4 KiB
Python
"""Discover backend test packages from INSTALLED_APPS and changed paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from functools import lru_cache
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
# Changes here rerun every installed test package.
|
|
_SHARED_PATH_PREFIXES: tuple[str, ...] = (
|
|
"dispatcharr/",
|
|
"pyproject.toml",
|
|
"manage.py",
|
|
"version.py",
|
|
"scripts/ci_backend_test_labels.py",
|
|
"scripts/ci_bootstrap_backend.sh",
|
|
".github/workflows/backend-tests.yml",
|
|
)
|
|
|
|
# No test package of its own; route to these installed app tests instead.
|
|
_PATH_ALIASES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
("apps/api/", ("__all__",)),
|
|
("apps/vod/", ("apps.output",)),
|
|
("apps/hdhr/", ("apps.output", "apps.channels")),
|
|
)
|
|
|
|
|
|
def repo_root() -> Path:
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def parse_installed_app_names(settings_path: str | None = None) -> tuple[str, ...]:
|
|
"""Read project app names from INSTALLED_APPS without importing Django."""
|
|
path = Path(settings_path) if settings_path else repo_root() / "dispatcharr" / "settings.py"
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.Assign):
|
|
continue
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name) and target.id == "INSTALLED_APPS":
|
|
entries = ast.literal_eval(node.value)
|
|
names: list[str] = []
|
|
for entry in entries:
|
|
name = _app_name_from_installed_entry(entry)
|
|
if name:
|
|
names.append(name)
|
|
return tuple(names)
|
|
raise RuntimeError(f"INSTALLED_APPS not found in {path}")
|
|
|
|
|
|
def _app_name_from_installed_entry(entry: str) -> str | None:
|
|
if ".apps." in entry and entry.endswith("Config"):
|
|
name = entry.rsplit(".apps.", 1)[0]
|
|
else:
|
|
name = entry
|
|
if name.startswith("apps.") or name == "core":
|
|
return name
|
|
return None
|
|
|
|
|
|
def _label_for_tests_dir(tests_dir: Path, base: Path) -> str | None:
|
|
if not tests_dir.is_dir() or not any(tests_dir.glob("test_*.py")):
|
|
return None
|
|
package_dir = tests_dir.parent
|
|
try:
|
|
rel = package_dir.relative_to(base)
|
|
except ValueError:
|
|
return None
|
|
return ".".join(rel.parts) + ".tests"
|
|
|
|
|
|
def _discover_test_labels_under_app(app_name: str, base: Path) -> list[str]:
|
|
"""Find test packages anywhere under an installed app's directory tree."""
|
|
app_dir = base.joinpath(*app_name.split("."))
|
|
if not app_dir.is_dir():
|
|
return []
|
|
|
|
labels: list[str] = []
|
|
seen: set[str] = set()
|
|
for tests_dir in app_dir.rglob("tests"):
|
|
label = _label_for_tests_dir(tests_dir, base)
|
|
if label and label not in seen:
|
|
seen.add(label)
|
|
labels.append(label)
|
|
return labels
|
|
|
|
|
|
def iter_test_package_labels(
|
|
*,
|
|
base: Path | None = None,
|
|
settings_path: str | None = None,
|
|
) -> list[str]:
|
|
"""Return dotted test package labels for installed apps that have tests/."""
|
|
base = base or repo_root()
|
|
labels: list[str] = []
|
|
seen: set[str] = set()
|
|
|
|
for app_name in parse_installed_app_names(settings_path):
|
|
for label in _discover_test_labels_under_app(app_name, base):
|
|
if label not in seen:
|
|
seen.add(label)
|
|
labels.append(label)
|
|
|
|
root_tests = base / "tests"
|
|
if root_tests.is_dir() and any(root_tests.glob("test_*.py")):
|
|
labels.append("tests")
|
|
|
|
return sorted(labels)
|
|
|
|
|
|
def _filesystem_prefix_for_app(app_name: str) -> str:
|
|
return app_name.replace(".", "/") + "/"
|
|
|
|
|
|
def _label_to_filesystem_prefix(label: str) -> str:
|
|
if label == "tests":
|
|
return "tests/"
|
|
return label.removesuffix(".tests").replace(".", "/") + "/"
|
|
|
|
|
|
def _labels_under_installed_app_tree(
|
|
path: str,
|
|
available: set[str],
|
|
settings_path: str | None,
|
|
) -> set[str]:
|
|
"""Include every test package under the installed app that owns this path."""
|
|
app_prefixes = sorted(
|
|
(
|
|
(_filesystem_prefix_for_app(name), name)
|
|
for name in parse_installed_app_names(settings_path)
|
|
),
|
|
key=lambda item: len(item[0]),
|
|
reverse=True,
|
|
)
|
|
|
|
for app_prefix, _ in app_prefixes:
|
|
if path == app_prefix.rstrip("/") or path.startswith(app_prefix):
|
|
return {
|
|
label
|
|
for label in available
|
|
if _label_to_filesystem_prefix(label).startswith(app_prefix)
|
|
}
|
|
return set()
|
|
|
|
|
|
def _normalize_path(path: str) -> str:
|
|
return PurePosixPath(path.strip().replace("\\", "/")).as_posix()
|
|
|
|
|
|
def _resolve_alias_labels(alias_apps: tuple[str, ...], available: set[str]) -> set[str]:
|
|
labels: set[str] = set()
|
|
for app_name in alias_apps:
|
|
if app_name == "__all__":
|
|
labels.update(available)
|
|
continue
|
|
label = f"{app_name}.tests"
|
|
if label in available:
|
|
labels.add(label)
|
|
return labels
|
|
|
|
|
|
def labels_for_changed_paths(
|
|
paths: list[str],
|
|
*,
|
|
full_suite: bool = False,
|
|
base: Path | None = None,
|
|
settings_path: str | None = None,
|
|
) -> list[str]:
|
|
"""Map changed repository paths to Django test package labels."""
|
|
all_labels = iter_test_package_labels(base=base, settings_path=settings_path)
|
|
available = set(all_labels)
|
|
|
|
if full_suite or not paths:
|
|
return all_labels
|
|
|
|
prefix_map = {
|
|
_label_to_filesystem_prefix(label): label for label in available
|
|
}
|
|
sorted_prefixes = sorted(prefix_map, key=len, reverse=True)
|
|
|
|
selected: set[str] = set()
|
|
for raw in paths:
|
|
path = _normalize_path(raw)
|
|
if not path:
|
|
continue
|
|
if any(path == prefix.rstrip("/") or path.startswith(prefix) for prefix in _SHARED_PATH_PREFIXES):
|
|
return all_labels
|
|
for alias_prefix, alias_apps in _PATH_ALIASES:
|
|
if path.startswith(alias_prefix):
|
|
selected.update(_resolve_alias_labels(alias_apps, available))
|
|
break
|
|
else:
|
|
matched = False
|
|
for prefix in sorted_prefixes:
|
|
if path.startswith(prefix):
|
|
selected.add(prefix_map[prefix])
|
|
matched = True
|
|
break
|
|
if not matched:
|
|
selected.update(
|
|
_labels_under_installed_app_tree(path, available, settings_path)
|
|
)
|
|
|
|
return sorted(selected)
|