Files
pygame-physics-opus5.5/docs/TESTING.md
T
2026-09-24 15:45:31 +02:00

6.2 KiB
Raw Blame History

Tests

Tests ausführen

Voraussetzung ist die Entwicklungsumgebung aus dem README:

pip install -r requirements-dev.txt

Alle Tests ausführen (Dauer etwa 15 s):

pytest

Mit Testabdeckung und Liste der nicht abgedeckten Zeilen:

pytest --cov=sandbox --cov-report=term-missing

Nur die schnellen Tests (ohne Stresstest und Smoke-Test):

pytest -m "not slow"

Lint und Formatierung prüfen:

ruff check src tests
ruff format --check src tests

Ruff prüft neben Stil und typischen Fehlern auch, dass alle Module, Klassen und öffentlichen Funktionen einen Docstring (D) und Type Hints (ANN) haben und dass in Vergleichen keine Magic Numbers stehen (PLR2004). Die Konfiguration steht in pyproject.toml.

Headless-Modus

UI- und Integrationstests laufen ohne sichtbares Fenster. tests/conftest.py setzt vor dem ersten Import von pygame:

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")

SDL rendert dann in einen unsichtbaren Puffer und braucht weder Bildschirm noch Soundkarte. Die Tests laufen so auch in CI oder über SSH.

Die UI-Tests bedienen die Anwendung wie ein Mensch: Sie legen echte pygame-Events (MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN …) in die Event-Queue und rufen dann App.frame() auf. Die Hilfsfunktionen dafür stehen in tests/app/driver.py. So wird die ganze Kette getestet: Event → Sidebar/InputHandler → Werkzeuge → Physik → Zeichnen.

Die Anwendung selbst lässt sich ebenfalls headless starten, etwa für Screenshots. PowerShell:

$env:SDL_VIDEODRIVER = "dummy"; python -m sandbox --demo --screenshot bild.png --seconds 2

Bash:

SDL_VIDEODRIVER=dummy python -m sandbox --demo --screenshot bild.png --seconds 2

Was wird getestet?

Die Erwartungswerte der Physik-Tests stammen aus der klassischen Mechanik – Impulserhaltung, Stoßgesetz, Coulomb-Reibung, Rollbedingung – und nicht aus der Implementierung selbst.

Physik (tests/physics/, ohne pygame)

Datei Inhalt
test_vector.py alle Vektoroperationen, Nullvektor-Normalisierung ohne NaN, Unveränderlichkeit, Hash/Gleichheit, Kopieren/Pickle
test_bodies.py Fläche und Trägheitsmoment, Masse = Fläche × Dichte, statische Körper, Materialprüfung, Impulse mit/ohne Drehung, Energie und Impuls
test_collision.py jede Formpaarung mit Normalenrichtung, Tiefe und Kontaktpunkt; Randfälle (gleicher Mittelpunkt, Kreismittelpunkt im Rechteck, knapp verfehlte Ecke, Körper hinter der Wand); Zufallstest mit über 500 Kontakten: Verschieben um die Tiefe trennt die Körper
test_broadphase.py Sweep-and-Prune liefert dieselben Paare wie der Brute-Force-Vergleich
test_solver.py elastischer Stoß: Impuls- und Energieerhaltung, Lehrbuchformel; e = 0: gemeinsame Geschwindigkeit; Energieverlust bei e < 1 exakt nach ΔE = ½·μ·(1 − e²)·v²; Restitutionsschwelle; schiefer Stoß ohne Reibung; Coulomb-Reibung (Bremsweg v²/2μg); Rutschen → Rollen mit ⅔·v₀; Rollwiderstand; Auflagekraft = m·g·dt; Impulsgrenzen; Trennen überlappender Körper ohne Geschwindigkeit
test_world.py freier Fall ≈ ½·g·t² (und exakt nach semi-implizitem Euler); Ball bleibt am Boden liegen und verlässt die Welt nie; Sprunghöhe = e² · Fallhöhe (1 %); ein vollkommen elastischer Ball springt über viele Sprünge immer wieder genau auf seine Fallhöhe; Energie steigt ohne äußere Kräfte nie; Stapel bleibt stehen; gemischter Haufen schläft ein; kein Tunneln bei Höchstgeschwindigkeit; Notbremse; Schlafen/Aufwecken, auch wenn die Basis eines schlafenden Stapels weggezogen oder weggestoßen wird (nichts schläft in der Luft weiter); Determinismus; 100 Körper × 1000 Schritte ohne NaN (Stresstest)
test_timestep.py Akkumulator, keine verlorenen Schritte durch Rundung, Obergrenze gegen die "Spiral of Death"
test_sleep.py Union-Find, Inselbildung (Wände verbinden nicht), Einschlafen und Aufwachen nur als ganze Insel

Interaktion (tests/interaction/, ohne pygame)

Datei Inhalt
test_velocity_tracker.py exakte Messung konstanter Bewegung, Unabhängigkeit von der Bildrate, Abklingen beim Stillhalten, Glättung von Zittern
test_drag_and_tools.py Seitenverhältnis mit gleicher Fläche, Mindestgröße, Körper erzeugen, Obergrenze, Vorschaufarbe = echte Farbe; Greifen mit Versatz, Ziel in der Welt, Folgen, Geschwindigkeitsgrenze, gezogener Körper schiebt andere weg, gegen ein Hindernis gedrückter Körper zittert nicht, Werfen, Pause
test_demo.py Beispielszene passt in die Welt, der Turm bleibt stehen

Anwendung (tests/app/, headless mit pygame)

Datei Inhalt
test_app_integration.py Objekt erstellen (Klick, Ziehen aus der Sidebar, Abbruch), greifen, ziehen, werfen, Stillhalten ohne Wurf, gezogener Körper schiebt andere, bleibt in der Welt; löschen (Rechtsklick, Entf, Alles löschen); Pause, Schwerkraft, Regler (Klick, Ziehen, Mausrad), Tastenkürzel, Fenster schließen, Debug-Ansicht, Screenshot, Kommandozeile
test_widgets.py Regler-Abbildung und Rasterung, deaktivierte Regler, Umschalter, Farbwahl, Zeichnen aller Widgets, Viewport, Winkel-Interpolation
test_entry_points_and_edge_cases.py python -m sandbox, CLI ohne Optionen, Ende der Hauptschleife, Randfälle der Eingabe, Schrift-Fallback
test_smoke.py Smoke-Test: Die echte Hauptschleife läuft 3 s headless, während ein zufällig klickender "Affe" Klicks, Ziehen, Rechtsklicks, Mausrad und Tasten sendet – ohne Exception, danach ein gültiger Zustand

Architektur (tests/test_architecture.py)

  • Die Physik-Module und config.py importieren nur erlaubte Module (statische Prüfung über den Syntaxbaum).
  • import sandbox.physics lädt in einem frischen Interpreter kein pygame.

Testabdeckung

Stand der letzten Messung: 219 Tests, 100 % Zeilen- und Branch-Abdeckung über das ganze Paket sandbox (inklusive UI), davon 100 % für sandbox.physics.

Die hohe Abdeckung ist kein Selbstzweck. Entscheidend sind die physikalischen Erwartungswerte: Ein Test, der nur Code ausführt, ohne das Ergebnis gegen die Physik zu prüfen, wäre wertlos.