proejct commit
This commit is contained in:
+124
@@ -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.
|
||||
Reference in New Issue
Block a user