"""Architektur-Tests: Die Physik-Schicht bleibt frei von pygame und fremden Bibliotheken. Anforderung: Die gesamte Physik darf nur das Standardmodul ``math`` verwenden (zusätzlich ``typing``/``dataclasses`` für Typangaben und Datenstrukturen). Diese Tests prüfen das statisch über den Syntaxbaum und zur Laufzeit in einem frischen Interpreter. """ import ast import os import subprocess import sys from pathlib import Path import pytest SOURCE_ROOT = Path(__file__).resolve().parents[1] / "src" / "sandbox" PHYSICS_FILES = sorted((SOURCE_ROOT / "physics").glob("*.py")) PURE_FILES = [*PHYSICS_FILES, SOURCE_ROOT / "config.py"] ALLOWED_STDLIB = {"__future__", "math", "typing", "dataclasses"} ALLOWED_PROJECT = {"sandbox.config"} def _imported_modules(path: Path) -> set[str]: tree = ast.parse(path.read_text(encoding="utf-8")) modules: set[str] = set() for node in ast.walk(tree): if isinstance(node, ast.Import): modules.update(alias.name for alias in node.names) elif isinstance(node, ast.ImportFrom): modules.add(node.module or "") return modules @pytest.mark.parametrize("path", PURE_FILES, ids=lambda path: path.name) def test_physics_and_config_import_only_allowed_modules(path: Path) -> None: forbidden = { module for module in _imported_modules(path) if module not in ALLOWED_STDLIB and module not in ALLOWED_PROJECT and not module.startswith("sandbox.physics") } assert not forbidden, f"{path.name} importiert nicht erlaubte Module: {forbidden}" def test_physics_files_were_found() -> None: assert len(PHYSICS_FILES) >= 8 def test_importing_the_physics_package_does_not_load_pygame() -> None: code = "import sys, sandbox.physics, sandbox.config; print('pygame' in sys.modules)" result = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, check=True, env={**os.environ, "PYTHONPATH": str(SOURCE_ROOT.parent)}, ) assert result.stdout.strip() == "False"