proejct commit

This commit is contained in:
2026-09-24 15:45:31 +02:00
commit 82d52be8a5
63 changed files with 8748 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# Tests
## Tests ausführen
Voraussetzung ist die Entwicklungsumgebung aus dem [README](../README.md):
```bash
pip install -r requirements-dev.txt
```
Alle Tests ausführen (Dauer etwa 15 s):
```bash
pytest
```
Mit Testabdeckung und Liste der nicht abgedeckten Zeilen:
```bash
pytest --cov=sandbox --cov-report=term-missing
```
Nur die schnellen Tests (ohne Stresstest und Smoke-Test):
```bash
pytest -m "not slow"
```
Lint und Formatierung prüfen:
```bash
ruff check src tests
```
```bash
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:
```python
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:
```powershell
$env:SDL_VIDEODRIVER = "dummy"; python -m sandbox --demo --screenshot bild.png --seconds 2
```
Bash:
```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.