Zum Inhalt

Entwicklung: Bilder und Prüfungen der Karten

← Dokumentation

Die vollständige Entwickler-Dokumentation ist auf Englisch: Development. Diese Seite beschreibt auf Deutsch, wie die Bilder der Dokumentation entstehen und wie sie aktuell bleiben, wie die Karten bei jeder Änderung auf ihr Aussehen und ihre Barrierefreiheit geprüft werden, wie der Browsertest dabei wartet und in vier Teilen nebeneinander läuft, wie ihre Tests die Timer der Karten prüfen, wann die End-to-End-Tests laufen, welche Hosts die Jobs erreichen und wie die Mutationstests zeigen, was die Tests nicht prüfen.

Auf dieser Seite: Bilder der Dokumentation · In jedem Lauf dieselben Bilder · Gruppen nebeneinander · Screenshot-Bot · Bildvergleich · Wie der Browsertest wartet · Der Browsertest in Teilen · Prüfung der Barrierefreiheit · Timer der Karten · Wann die End-to-End-Tests laufen · Netzwerk der Jobs · Mutationstests

Bilder der Dokumentation

tests/e2e/screenshots.sh erzeugt jedes Bild in docs/images/en und docs/images/de neu aus der Demo, auch die animierten WebP-Bilder, und komprimiert die PNG-Bilder mit pngquant. tests/e2e/screenshots.py hat eine Funktion pro Bild oder Animation, sodass sich ein einzelnes Bild neu erzeugen lässt, indem man seine Funktion gegen eine laufende Demo aufruft. Jedes Bild zeigt das simulierte Board und erfundene Spieler, persönliche Daten können also nicht erscheinen; die geheime Adresse der Online-Brücke wird abgedeckt. Die Suche im Netzwerk öffnet das Werkzeug nie, sie würde echte Boards zeigen. Die Diagramme in docs/images entstehen dagegen mit scripts/render_diagrams.sh.

Schlägt eine Aufnahme fehl, speichert das Werkzeug jede offene Seite in tests/e2e/artifacts/. Git ignoriert diesen Ordner.

In jedem Lauf dieselben Bilder

Zwei Läufe des Werkzeugs erzeugen dieselben Bilder, Byte für Byte. Ein Bild, das sich ändert, zeigt also immer eine Karte oder eine Seite, die sich geändert hat, und Screenshot-Bot und Bildvergleich erkennen das:

  • Eine eingefrorene Uhr. Die Demo der Screenshots steht auf Dienstag, dem 29. September 2026, 20:30 Uhr (DEMO_TIME in screenshots.sh), ganz gleich, wann sie läuft. tests/e2e/frozen/sitecustomize.py stellt diese Uhr für Home Assistant und jedes Skript in seinem Container, über compose.frozen.yaml, das demo.sh hinzunimmt, wenn DEMO_TIME gesetzt ist. Die Uhr geht nur weiter, wenn die Demo sie weiterstellt: Jede Bewegung am simulierten Board, ein Dart, das Herausziehen oder ein neuer Status, dauert zwei Sekunden, in demo.py wie in screenshots.py, geschrieben in die Datei .demo_clock in der Konfiguration der Demo. So kommt jede Zeit, die Home Assistant festhält, in jedem Lauf gleich heraus, auf die Millisekunde, während der Verlauf des Boards, die Länge einer Session und die Trainingszeit des Wochenberichts aussehen wie an einem echten Abend; der Wochenbericht bekommt die Minute, auf die er wartet, auf dieselbe Weise. Auch der Browser zeigt die Zeit der Demo: Die Uhr von Playwright steht auf ihr still, außer in den Seiten der Spiel-Animationen, in denen das Ergebnis eines Turnierspiels seine Zeit lang stehen bleiben muss; ihre Bilder zeigen keine Uhrzeit. Die Seite der Statistik-Grafiken wünscht reduzierte Bewegung, damit Home Assistant ihre Linien sofort zeichnet statt als Animation, die die stehende Uhr anhalten würde.
  • Eine Quelle für Änderungen. Die eingefrorene Demo fragt das Board nicht regelmäßig ab: Jede Änderung kommt über die Verbindung zum Board, in der Reihenfolge, in der das Board sie sendet, nie zuerst aus einer Abfrage, deren Zeitpunkt vom Lauf abhängt.
  • Feste Zufallszahlen. Die Darts des Bots und jede andere Zufallszahl ohne eigenen Startwert beginnen mit demselben Startwert.
  • Ruhige Bilder. Vor jedem Screenshot und jedem Bild einer Animation wartet das Werkzeug auf Bilder, die noch laden, lässt Übergänge und Einblendungen an ihr Ende springen und hält endlose Animationen wie die blinkenden Felder im selben Moment an. Das wiederholt es, bis sich keine Karte mehr ändert und zwei Blicke im Abstand einer Zehntelsekunde nichts finden, das sich bewegt. Davor wartet eine Seite, die sich öffnet oder ihre Größe ändert, bis sie hat, was Home Assistant ihr schickt: jede Karte gezeichnet, ihre Schriften geladen und keine Anfrage des Frontends am Websocket ohne Antwort, mit loaded() aus pages.py des Browsertests, statt einer festen Zeit, die auf einem langsamen Runner zu kurz ist. Chromium zeichnet jedes Bild ganz und animiert auf seinem Hauptthread (STEADY_CHROMIUM in screenshots.py). Die Werkzeugleiste eines Dashboards wirft keinen Schatten auf das, was unter ihr scrollt, eine Karte, die höher als der Bildschirm ist, wird vom Anfang der Seite aufgenommen, und der Dialog der Board-Ereignisse öffnet sich erst, wenn der Rekorder das letzte Ereignis geschrieben hat, das er zeigt (RECORDED), nicht nach einer festen Zeit.
  • Farben in fester Reihenfolge. Home Assistant gibt jedem Zustand, den es in einem Verlauf oder einer Aktivitätsliste zeichnet, eine eigene Farbe, die nächste seiner Palette, in der Reihenfolge, in der es die Zustände zuerst zeichnet, solange die Seite besteht; die Ereignis-Entität des Boards bekommt mit jedem Ereignis einen neuen Zustand. Die Zeitleiste eines Verlaufs zeichnet in jedem Pixel nur den Zustand, der dort am längsten dauert. Welche Zustände sie zuerst färbt, hängt also von ihrer Breite in dem Moment ab, in dem ihre Daten ankommen, und Verlauf und Aktivität des Dialogs kommen in beliebiger Reihenfolge an. Deshalb zeichnet das Werkzeug, bevor sich der Dialog öffnet, jedes Ereignis der letzten 24 Stunden einmal, das neueste zuerst, in einer eigenen, unsichtbaren Aktivitätsliste (COLOUR_EVENTS in screenshots.py): Der Dialog findet jedes Ereignis gefärbt vor, und die neuesten bekommen immer die ersten Farben der Palette, ganz gleich, wie viele Ereignisse davor kamen.
  • Alle Texte der Einstellungen. Home Assistant lädt die Texte seiner Einstellungen, etwa die Namen ihrer Tabs, getrennt von seinen übrigen Texten. Es führt jede Ladung mit den Texten zusammen, die es schon hat, zeigt aber nur, was seine neueste Ladung zusammengestellt hat, und verlässt sich darauf, dass seine Timer ohne Verzögerung in der Reihenfolge laufen, in der sie angefordert wurden. Das müssen sie nicht: Chrome hält einen Timer zurück, der tief in anderen Timern angefordert wird, und die stehende Uhr von Playwright einen, der während eines Ticks angefordert wird. Überholt eine spätere Ladung die der Einstellungen, fehlen einer Seite der Einstellungen die Namen ihrer Tabs und die Köpfe ihrer Spalten, solange die Seite besteht; so sind zwei Läufe des Screenshot-Bots gescheitert. Deshalb warten die Seiten der Einstellungen, bis ihre Texte zusammengeführt sind, und lassen Home Assistant seine Texte dann noch einmal aktualisieren (SETTINGS_TEXTS in screenshots.py).
  • Feste Adressen. Die Online-Brücke zeigt homeassistant.local als Adresse von Home Assistant (internal_url in demo.configuration.yaml), nicht die Adresse, die Docker dem Container zufällig gibt.
  • Eine feste Version. Hinter den Optionen der Online-Brücke zeigt die Seite der Integration ihre Version, und jedes Release ändert sie. In der eingefrorenen Demo lässt sitecustomize.py Home Assistant nur die Hauptversion aus dem Manifest lesen: Die Seite zeigt Version 1 für jedes Release 1.x. So ändert ein Release kein Bild, und der Bildvergleich meldet das Bild danach nicht bei jedem Pull Request. Alles andere auf der Seite zeigt Home Assistant so, wie es ist; außerhalb der eingefrorenen Demo zeigt es die volle Version.
  • Festgelegte Kompression. pngquant 2.18.0 aus Ubuntu 24.04 komprimiert jedes PNG-Bild direkt nach der Aufnahme. tests/e2e/pngquant.sh installiert es im festgelegten Playwright-Image von Launchpad, wo jedes veröffentlichte Paket seine Adresse behält, und prüft beide Pakete gegen ihre SHA-256-Prüfsumme. Die Animationen entstehen mit dem festgelegten Pillow.

Um herauszufinden, was sich zwischen zwei Läufen unterscheidet, behält KEEP_RAW=1 jedes Bild und jedes Bild einer Animation so, wie es aufgenommen wurde, vor der Kompression, in tests/e2e/artifacts/raw/, einem Ordner pro Sprache; tests/e2e/compare.py vergleicht zwei Ordner mit Bildern.

Ohne DEMO_TIME, wie bei demo.sh für eine Vorschau oder bei browser.sh, läuft die Demo mit der echten Uhr.

Gruppen nebeneinander

Das Werkzeug erzeugt seine Bilder in drei festen Gruppen (GROUPS in screenshots.py), und jede Gruppe beginnt mit einer frischen Demo: die Karten mit den ersten Spielen; weitere Spiele mit einem Turnier und danach die Spieler; die Korrekturen eines Darts und ein Spiel gegen den Bot, die Formate, der Kalender und die Dartscheibe ohne Autodarts. Die Abschnitte einer Gruppe laufen in ihrer Reihenfolge, und ein späterer sieht, was ein früherer gespielt hat: Die Spielerkarte zeigt die fünf Spieler des Turniers davor. Eine Gruppe sieht nie, was eine andere gespielt hat. Sie erzeugt also allein dieselben Bilder wie nach den anderen, und die Gruppen können nebeneinander laufen, jede auf einem eigenen Runner:

Wie Bildvergleich und Screenshot-Bot die Bilder in Gruppen erzeugen: Jede Sprache wird in drei Gruppen gleichzeitig erzeugt, jede Gruppe auf einem eigenen Runner aus einer frischen Demo. Die Karten und die ersten Spiele bilden Gruppe 1, weitere Spiele mit den Spielern Gruppe 2, die Korrekturen eines Darts, die Formate, der Kalender und die Dartscheibe ohne Autodarts Gruppe 3. Danach setzt ein Job je Sprache die Bilder seiner drei Gruppen zusammen und vergleicht sie mit den eingecheckten Bildern. Der Bildvergleich schreibt den Vergleich in einen Kommentar am Pull Request; der Screenshot-Bot aktualisiert seinen Pull Request mit jeder Sprache, die erzeugt wurde.

screenshots.sh und visual.sh erzeugen jede Gruppe nacheinander, jede aus einer frischen Demo, und SCREENSHOT_GROUP=2/3 erzeugt nur die zweite Gruppe. IMAGES nennt den Ordner unterhalb des Repositorys, in den die Bilder kommen, ohne Angabe docs/images. Screenshot-Bot und Bildvergleich erzeugen jede Gruppe jeder Sprache in einem eigenen Job, sechs Jobs nebeneinander mit je etwa acht Minuten, wo vorher zwei Jobs zwanzig Minuten brauchten. Ein Job je Sprache setzt danach seine Gruppen zusammen und vergleicht sie mit den eingecheckten Bildern (tests/e2e/collect.sh), wie visual.sh es auf einem Rechner tut; er schlägt fehl, wenn eine Gruppe keine Bilder hinterlassen hat oder zwei Gruppen dasselbe Bild erzeugt haben.

Jeder Job einer Gruppe nennt in seiner Zusammenfassung die Sekunden seiner Abschnitte, und das Protokoll zeigt sie als Screenshot section: people (186.2 s). Wird eine Gruppe deutlich länger als die anderen, wandert ein Abschnitt in eine andere Gruppe, solange die Abschnitte, die voneinander abhängen, in ihrer Reihenfolge zusammenbleiben: die Karten vor den Spielen, deren Legs die Grafiken der Woche beenden würden; das Turnier vor den Spielern; das erste Spiel des Bots vor den Formaten, die wieder gegen den Bot spielen; der Kalender und der Wochenbericht, der eine neue Woche beginnt, und danach die Dartscheibe ohne Autodarts, deren zweites Board das erste der Karten ohne eines wäre, als Letzte ihrer Gruppe. Ein Umzug ändert einmal die Bilder, deren Daten aus den Abschnitten davor kamen, und der Screenshot-Bot bringt sie nach dem Merge nach docs/images. tests/test_screenshot_groups.py prüft, dass jeder Abschnitt zu genau einer Gruppe gehört, dass diese Reihenfolgen gelten und dass die Skripte und beide Workflows so viele Gruppen erzeugen, wie GROUPS hat.

Screenshot-Bot

Der Workflow Screenshot bot (.github/workflows/screenshots.yml) hält die Bilder aktuell. Wenn eine Änderung der Karten (custom_components/autodarts/frontend/) oder des Screenshot-Werkzeugs (tests/e2e/, außer Browsertest und End-to-End-Test) auf main landet, jeden Donnerstag und auf Abruf erzeugt er jedes Bild beider Sprachen neu, in Gruppen nebeneinander, und vergleicht es Pixel für Pixel mit dem eingecheckten. Ein Bild, dessen Pixel gleich geblieben sind, behält seine Datei. Nur die erzeugten Bilder kommen über die von main: Die Diagramme in docs/images, die main bis zum Ende des Erzeugens neu haben kann, bleiben, wie sie sind, und ein Bild, das weder das Werkzeug noch ein Diagramm in docs/diagrams mehr erzeugt, fällt weg.

Auch ein Release startet den Bot. Es schreibt seine Version in custom_components/autodarts/manifest.json, und die Integrationsseite in online-bridge.png zeigt davon die Hauptversion (eine feste Version): Ein Release derselben Hauptversion erzeugt jedes Bild wie zuvor, eine neue Hauptversion wie 2.0.0 ändert dieses Bild. Den Release-Pull-Request merget der Maintainer oder die Release-App, also startet sein Push auf main Workflows wie jeder andere; nur ein Merge mit dem GITHUB_TOKEN eines Workflows startete keine. So folgt das Bild einer neuen Hauptversion binnen einer Stunde statt erst am Donnerstag, und der Bildvergleich späterer Pull Requests meldet es nicht tagelang als geändert.

Sein Job timeline zeichnet zugleich die Spiele der Live-Demo neu auf, und der Pull Request bringt eine geänderte Aufzeichnung mit den Bildern.

Hat sich ein Bild geändert, öffnet der Bot einen Pull Request, docs: update the screenshots of the cards, oder aktualisiert den noch offenen, und schaltet Auto-Merge ein: Die Bilder folgen, sobald jede Prüfung bestanden ist. Die Beschreibung nennt jedes geänderte Bild mit dem Anteil seiner Pixel, der sich geändert hat; der Workflow-Lauf behält die Bilder vorher und nachher mit ihren Unterschieden drei Tage lang. Der Pull Request kommt von der Release-App, damit seine Prüfungen laufen, und seine Commits gehen über die API von GitHub: GitHub signiert sie, wie main es verlangt, und sie tragen das Signed-off-by: der App. Der Branch des Pull Requests beginnt bei main, wie es nach dem Erzeugen der Bilder steht, nicht beim Commit, aus dem sie erzeugt wurden: Ein Branch, dessen Workflows von denen auf main abweichen, gilt GitHub als Änderung der Workflows, und dafür hat die App keine Berechtigung. Die Bilder kommen zuerst auf einen eigenen Branch, und der Branch des Pull Requests springt in einem Schritt dorthin: Zeigte er auch nur kurz auf einen Commit, den main schon hat, schlösse das seinen Pull Request. Lässt sich der Branch eines offenen Pull Requests wegen der Workflows, die sich seitdem auf main geändert haben, nicht verschieben, beginnt er neu, und ein neuer Pull Request löst den alten ab. Hat main die Bilder eines noch offenen Pull Requests schon, schließt der Bot ihn. Eine Gruppe, die sich nicht erzeugen lässt, hält nur ihre Sprache auf: Der Pull Request übernimmt die Bilder der Sprache, die erzeugt wurde, und behält für die andere die erzeugten Bilder, die er hatte, aus dem Branch der App in diesem Repository oder wie auf main: Ein Diagramm kommt von main, und der Pull Request eines Forks mit einem Branch gleichen Namens gibt ihm nichts; solange eine Sprache fehlt, schließt der Bot keinen Pull Request. Wurde keine Sprache erzeugt, schlägt der Job fehl, und der Pull Request bleibt, wie er ist. Das Erzeugen der Bilder läuft ohne jedes Geheimnis; nur der letzte Job, der den Pull Request schreibt, nutzt den Schlüssel der Release-App aus der Umgebung release.

Live-Demo der Website

Die Live-Demo spielt die Karten auf der Website, ohne Home Assistant: ein 501 gegen den Bot, ein Cricket-Spiel und ein Turnier, in den fünf Sprachen der Karten, auf einem Desktop und einem Handy. Niemand zeichnet sie von Hand auf, und nichts darin ist erfunden.

  • scripts/record_demo.py lässt die Integration die Spiele im Home Assistant der Tests spielen, an einem simulierten Board, mit einer Uhr, die nur mit den Spielen weitergeht (tests/demo_timeline.py beschreibt sie). Nach jedem Zug hält es die Zustände der Entitäten, die sich geändert haben, und die Antworten auf den Positionsbefehl der Trainingskarte in docs/demo/timeline.json fest, eine Zeile je Schritt. Zwei Aufzeichnungen desselben Codes sind Byte für Byte gleich. tests/test_demo_timeline.py spielt die Spiele in jedem Lauf der Tests, also schlägt dort fehl, was sie kaputt macht.
  • docs/demo/index.html und docs/demo/demo.js sind die Seite. Ein kleiner Ersatz für Home Assistant gibt den Karten die Zustände jedes Schritts, wie Home Assistant es täte, mit jedem Datum und jeder Uhrzeit darin auf den Moment der Wiedergabe verschoben, damit ein Countdown zählt und „seit“ heute zeigt. Eine Schaltfläche einer Karte oder ein Tipp auf eine Entität zeigt nur einen Hinweis, dass das eine Aufzeichnung ist; nichts verlässt die Seite. Die Website nimmt die Karten unverändert aus der Integration (on_post_build in scripts/mkdocs_hooks.py), also zeigt die Demo immer die Karten ihrer Version.
  • Der Screenshot-Bot zeichnet die Spiele mit den Bildern neu auf, in seinem Job timeline, und sein Pull Request bringt die neue Aufzeichnung auf main. So folgt die Demo der Integration und den Karten wie die Bilder.
  • tests/frontend/demo.test.mjs testet die Seite mit den Karten in happy-dom, und der Job live demo der End-to-End-Tests spielt sie in Chromium (bash tests/e2e/live_demo.sh): in jeder Sprache, auf einem Desktop und einem Handy, jeden Schritt, ohne Fehler in der Konsole und ohne Seite, die breiter als der Bildschirm ist.

Zum Neuaufzeichnen und Ansehen, mit den Paketen der Tests und der Website:

python scripts/record_demo.py
mkdocs serve

Dann öffnest du http://127.0.0.1:8000/demo/index.html?lang=de.

Wie die Live-Demo der Website die Karten spielt: scripts/record_demo.py lässt die Integration ein 501 gegen den Bot, ein Cricket-Spiel und ein Turnier im Home Assistant der Tests spielen, an einem simulierten Board mit einer Uhr, die nur mit den Spielen weitergeht. Nach jedem Zug hält es die Zustände, die sich geändert haben, und die Positionen der Darts in docs/demo/timeline.json fest. Der Screenshot-Bot zeichnet sie mit den Bildern neu auf, und sein Pull Request bringt sie auf main. Die Website entsteht aus main und legt die Karten der Integration neben die Seite der Demo. Im Browser spielt demo.js die Schritte ab und gibt die Zustände an die Karten wie Home Assistant, zur Zeit der Wiedergabe; eine Schaltfläche einer Karte sagt nur, dass das eine Aufzeichnung ist. Der Job live demo der End-to-End-Tests spielt sie in jeder Sprache auf einem Desktop und einem Handy.

Bildvergleich

Der Workflow Visual check (.github/workflows/visual.yml) erzeugt die Bilder auf dieselbe Weise für jeden Pull Request, der die Karten oder das Screenshot-Werkzeug ändert, und vergleicht sie mit den eingecheckten. Er meldet nur, denn eine gewollte Änderung kommt nach dem Merge über den Screenshot-Bot nach docs/images:

  • Die Zusammenfassung des Jobs compare jeder Sprache nennt jedes Bild, das anders aussieht, mit dem Anteil seiner Pixel, der sich geändert hat.
  • Die Artefakte visual-en und visual-de behalten jedes geänderte Bild vorher und nachher und ein Bild des Unterschieds: das neue Bild in Grau, jedes geänderte Pixel in Rot.
  • Der Workflow Visual check comment (visual-comment.yml) schreibt die Liste in einen Kommentar am Pull Request, mit Links zu den Artefakten, und aktualisiert ihn nach jedem Lauf.

Ein Bild, das sich nicht erzeugen lässt, lässt die Prüfung fehlschlagen, und die andere Sprache bekommt trotzdem ihren Bericht: die Zusammenfassung ihres Jobs compare, ihr Artefakt und den Kommentar, der ihre Bilder auflistet und die Sprache nennt, die sich nicht erzeugen ließ. Der Kommentar-Workflow startet, wenn eine Prüfung endet (workflow_run), damit er auch Pull Requests aus Forks kommentieren darf, checkt ihren Code aber nie aus und führt ihn nie aus: Er liest nur die Berichte der Prüfung, übernimmt daraus nichts als Bildnamen und Zahlen, die er prüft, und schreibt den Kommentar selbst. Die Prüfung läuft nur, wenn sich ihre Dateien ändern, und ist deshalb keine Pflichtprüfung.

Derselbe Vergleich läuft auch lokal; er lässt die neuen Bilder in docs/images und den Bericht in tests/e2e/artifacts/visual/. Jede Gruppe einer Sprache braucht etwa sechs Minuten, und eine Gruppe allein erzeugt nur ihre eigenen Bilder:

LANGUAGES=de bash tests/e2e/visual.sh
SCREENSHOT_GROUP=2/3 LANGUAGES=de bash tests/e2e/visual.sh   # nur die zweite Gruppe
git checkout -- docs/images   # zurück zu den eingecheckten Bildern

Wie der Browsertest wartet

Der Browsertest (tests/e2e/browser.py) wartet auf das, was er prüft, nie eine feste Zeit, die auf einem langsamen Runner zu kurz und überall sonst zu lang wäre. tests/e2e/pages.py enthält seine Wartefunktionen:

  • show() öffnet eine Ansicht auf mehreren Seiten zugleich: beim ersten Mal mit einem Laden der Seite, danach in der App, wie es ihre Links tun. So bleiben das Frontend und sein Websocket erhalten, und eine Ansicht braucht eine Zehntelsekunde statt eines Ladens der ganzen App. Eine Adresse mit einer Abfrage oder einem Fragment, etwa ?edit=1 für den Bearbeitungsmodus, lädt die Seite, und ebenso jede Ansicht danach: Die App liest sie nur beim Start und behält, was sie setzen, in den Ansichten, die sie danach in sich öffnet. Es wartet, bis die Karten der Ansicht die der Ansicht davor ersetzt und sich gezeichnet haben.
  • expect_state() wartet, bis jede Seite die Zustände zeigt, die ein Zug am Board setzt, etwa num_throws 2 nach dem zweiten Dart einer Aufnahme oder local_status takeout_in_progress, während die Darts herausgezogen werden.
  • settle() wartet, bis eine Seite zur Ruhe gekommen ist: Ihre Karten haben sich gezeichnet, keine Anfrage des Frontends am Websocket von Home Assistant wartet auf ihre Antwort, 100 ms kam kein Frame, keine Animation einer Karte läuft, und der Browser hat seitdem zwei Frames gezeichnet. Ein Skript, das jede Seite vor dem von Home Assistant ausführt, verfolgt dafür den Websocket.

Wie der Browsertest wartet: Alle Bildschirme eines Schritts sind zugleich offen. Nach einem Zug am Board, etwa einem Dart, wartet der Test, bis jede Seite den Zustand zeigt, den der Zug setzt, etwa zwei Darts in der Aufnahme. Nach einer Ansicht, die er öffnet, wartet er, bis die Karten der Ansicht davor jede Seite verlassen und die neuen sich gezeichnet haben. Dann wartet er, bis jede Seite zur Ruhe gekommen ist: ihre Karten gezeichnet, keine Anfrage am Websocket wartet auf ihre Antwort, 100 ms kein Frame, keine Animation einer Karte läuft und zwei Frames gezeichnet. Danach prüft er jede Seite, das Layout, die Höhen oder die Barrierefreiheit, und macht mit dem nächsten Zug oder der nächsten Ansicht weiter. Eine Seite, die nicht rechtzeitig dahin kommt, lässt den Schritt mit dem fehlschlagen, worauf sie wartete und was sie zeigt.

Die Schritte, die viele Bildschirme ansehen, öffnen sie nebeneinander und spielen jedes Spiel einmal für alle: every screen size seine neun Bildschirme und danach die Telefone in vier weiteren Sprachen, steady heights seine vier Bildschirme mit je drei Ansichten, accessibility seine vier Bildschirme und Designs, auf denen axe-core zugleich prüft, und a dartboard without Autodarts seine neun Bildschirme. Eine Wartezeit, die abläuft, nennt die Seite mit der Größe ihres Bildschirms und ihrer Sprache, worauf sie wartete und was die Seite stattdessen zeigte, etwa die Anfrage ohne Antwort oder den Zustand jeder Entität.

tests/test_e2e_waits.py schlägt fehl, wenn der Browsertest mit page.wait_for_timeout eine feste Zeit wartet, außer bei den zwei Wartezeiten, die er mit ihren Gründen nennt, jede einmal aufgerufen: ein Finger, der 120 ms auf der Lupe ruht, bevor er loslässt, und die sechs Sekunden, die der Safari-Schritt autodarts-card.js zurückhält. Kein Skript, das einen Browser steuert, darf time.sleep aufrufen: Die synchrone API von Playwright verarbeitet die Ereignisse des Browsers, etwa eine Anfrage, die eine Route zurückhält, nur in ihren eigenen Aufrufen, also hielte ein Schlaf sie alle an.

Nach jedem Schritt zeigt das Protokoll seine Sekunden, etwa Browser step: steady heights (74.2 s), und browser-steps-table.md bei den Artefakten führt jeden Schritt mit seinen Sekunden auf; in der CI zeigt die Zusammenfassung des Jobs diese Tabelle.

Der Browsertest in Teilen

Nacheinander brauchen die Schritte fünf Minuten, zu lang für einen Job. Jeder Schritt gehört zu einer von vier Gruppen mit etwa gleich vielen Sekunden (steps() in tests/e2e/browser.py), und BROWSER_SHARD=k/n führt die Gruppen g mit (g - 1) % n = k - 1 aus: 1/1, die Vorgabe, jede Gruppe, 1/2 die Gruppen 1 und 3, 2/4 die Gruppe 2. BROWSER_WORKERS=W teilt den Teil des Rechners noch einmal: Worker w führt den Teil (k - 1) * W + w von n * W auf einer eigenen Demo-Instanz aus, alle nebeneinander, sodass 1/2 allein die Gruppen 1 und 3 ausführt, 1/2 mit zwei Workern aber die Teile 1/4 und 2/4, die Gruppen 1 und 2. Jeder Worker hat seinen Browser, einem eigenen Compose-Projekt (autodarts_browser_1, …), dem nächsten Port ab 18125 und einem eigenen Ordner bei den Artefakten (shard-3-of-4). Jede Zeile eines Workers im Protokoll beginnt mit seinem Teil, etwa [3/4]. Mehr Teile als Gruppen lehnt browser.sh ab, bevor es irgendetwas startet, etwa BROWSER_SHARD=2/4 mit BROWSER_WORKERS=2.

BROWSER_WORKERS=4 bash tests/e2e/browser.sh                   # alle Schritte, vier Teile zugleich
BROWSER_SHARD=2/4 bash tests/e2e/browser.sh                   # ein Teil: die gleichbleibenden Höhen
BROWSER_SHARD=1/2 BROWSER_WORKERS=2 bash tests/e2e/browser.sh # der erste Runner der CI: Gruppen 1 und 2

BROWSER_STEPS führt nur die genannten Schritte aus, durch Kommas getrennt: Der ganze Name eines Schritts führt diesen allein aus, etwa BROWSER_STEPS="training card" ohne den training card editor, der Anfang von Namen jeden Schritt, mit dem sie beginnen, etwa BROWSER_STEPS="touch".

Jeder Schritt beginnt mit dem Board zwischen zwei Spielen, was auch immer der Schritt davor hinterließ: Board.between_games() in browser.py beendet ein Turnier, schaltet das Tastenfeld, das Spiel und die Teams aus, stellt einen Spieler, ein Leg und einen Satz ohne den Bot ein, startet die Erkennung, räumt das Board ab und wartet, bis Home Assistant das alles zeigt, über eine eigene Seite ohne Karte. Eine Karte, die ein Spiel zeigte, öffnet acht Sekunden nach seinem Ende von selbst die Spielauswahl; ein weiterlaufendes Spiel würde sie also mitten im nächsten Schritt öffnen. Nur die Trainingskarte und die Live-Karte lesen die Demo so, wie demo.py sie hinterließ, ihre Sitzung und die Aufnahme auf dem Board; sie kommen in Gruppe 1 zuerst, und vor ihnen wird nichts zurückgesetzt. So besteht jeder Teil und jeder Schritt für sich, wie der ganze Lauf.

tests/test_e2e_shards.py prüft die Gruppen ohne Browser: Jeder Schritt hat eine, jede Zahl von Teilen von eins bis vier führt jeden Schritt genau einmal und in seiner Reihenfolge aus, die Schritte, die die Demo wie zu Beginn lesen, kommen zuerst, und die Dartscheibe ohne Autodarts zuletzt. Außerdem führt er browser.sh mit einem Ersatz für docker aus, der zeigt, welchen Teil, welches Projekt und welche Artefakte jeder Worker bekommt, und dass ein fehlgeschlagener Worker den Lauf fehlschlagen lässt und als einziger seine Protokolle behält. Wächst ein Schritt, zeigt browser-steps-table.md seines Teils das, und ein Schritt, der in steps() in eine andere Gruppe wandert, gleicht die Teile wieder aus.

Prüfung der Barrierefreiheit

Der Schritt accessibility des Browsertests (tests/e2e/browser.py) lässt axe-core über jede Karte jeder Ansicht beider Dashboards laufen, über den Ruhebildschirm, ein Übungsspiel auf der Live-Karte und die eigenen Seiten der Anzeigetafel: die Spielauswahl, das Tastenfeld mit seiner Scheibe zum Antippen, die Scheibe einer Korrektur und Cricket. Das geschieht auf einem Laptop (1280 × 800) und einem iPhone (393 × 852) mit Touch, jeweils im dunklen und im hellen Design, nach den Regeln von WCAG 2.1 in den Stufen A und AA, etwa zum Kontrast von Text, zu Namen von Bedienelementen und Rollen und zum Erreichen dessen, was scrollt, mit der Tastatur.

  • Ein schwerer oder kritischer Verstoß lässt den Schritt fehlschlagen, mit Bildschirm, Ansicht, Karte, Regel, Element und dem, was nicht stimmt. Mittlere und leichte nennt das Protokoll.
  • tests/e2e/accessibility-baseline.toml führt die Verstöße, die vorerst hingenommen werden, jeden mit seinem Grund: Ein Eintrag nennt die Regel, die Karte und ein target, das Teil des Selektors ist, den axe-core dem Element gibt. Der Schritt nennt einen Eintrag, der nicht mehr vorkommt, damit er geht. Die Liste ist leer: Jede Karte besteht.
  • Der Schritt behält jeden Verstoß und jede geprüfte Ansicht in accessibility.json, bei den übrigen Artefakten des Browsertests.

axe-core kommt aus package.json, festgelegt in package-lock.json; führe also vor browser.sh npm ci aus, wie es die CI tut. BROWSER_STEPS=accessibility führt die Prüfung allein aus. Behebe einen Verstoß lieber in der Karte, als ihn hinzunehmen: Gedämpfte Wörter nehmen --ad-muted-text aus den Bausteinen, das den Kontrast in beiden Designs und auf einer getönten Kachel hält, jedes Bedienelement hat Wörter oder ein aria-label, und was scrollt, erreicht auch die Tastatur.

Timer der Karten

Eine Karte auf einem Dashboard hat Timer: den Countdown bis zum nächsten Match eines Turniers, die Zeit, die das Ergebnis oder die Zusammenfassung eines Matches zeigt, und den Ruhemodus; die Trainingskarte folgt den Ereignissen des Boards über den History-Stream von Home Assistant. Nur das Verlassen der Seite hält sie an, in disconnectedCallback. Home Assistant kann einer Karte auch danach noch neue Zustände geben, deshalb startet eine Karte einen Timer oder einen Stream nur, solange sie auf der Seite ist (isConnected). Kommt sie zurück, aktualisiert connectedCallback sie, und das startet beides neu.

Die Timer einer Karte und die Seite: Home Assistant gibt einer Karte neue Zustände. Eine Karte auf der Seite startet, was sie braucht, etwa den Countdown eines Turniers, die Zeit der Zusammenfassung, den Ruhemodus und den History-Stream; eine Karte, die die Seite verlassen hat, startet nichts. Das Verlassen der Seite hält jeden Timer und den Stream an, und eine Karte, die zurückkommt, wird aktualisiert und startet sie neu. In npm test verlassen die Karten eines Tests die Seite, wenn er endet; läuft dann noch ein Timer, schlägt der Test mit seinem Namen und dem fehl, was der Timer aufruft, und der Timer hält an.

npm test hält jede Karte daran:

  • tests/frontend/dom.mjs verfolgt die echten Timer, die die Karten und die Tests stellen. Endet ein Test, enden seine simulierten Timer, und seine Karten verlassen die Seite, sodass der nächste Test ohne eine Karte und einen Timer von ihm beginnt. Ein Timer, der dann noch läuft, würde die Datei für immer laufen lassen. Stattdessen hält er an, und der Test schlägt fehl, mit seinem Namen und dem, was der Timer aufruft:
✖ a scoreboard of a waiting tournament counts down with real timers
  Error: The test "a scoreboard of a waiting tournament counts down with real timers" left 1 timer running after its cards left the page:
  setInterval(() => this._update(), 1000)

Karten, die eine Datei außerhalb ihrer Tests auf die Seite stellt, verlassen sie nach ihrem letzten Test, und die Datei schlägt genauso mit ihrem Namen fehl. - Ein Test, der nie endet, schlägt nach 60 Sekunden mit seinem Namen fehl (--test-timeout=60000 in package.json), und die übrigen Dateien laufen weiter.

Simulierte Timer (context.mock.timers) enden mit ihrem Test und zählen nicht. Um zu prüfen, dass eine Karte keinen Timer startet, vergleicht ein Test timers() aus dom.mjs davor und danach, wie es a scoreboard that left the page starts no countdown and no timer of the tournament in tests/frontend/tournament.test.mjs tut.

Die Karten eines Tests verlassen die Seite mit ihm, weil eine Karte, die blieb, npm test auf einem langsamen Rechner zufällig scheitern ließ (#106). Der Countdown eines wartenden Turniers zeichnete eine Karte eines früheren Tests jede Sekunde neu, auch während ein späterer Test setTimeout simulierte. Die Karte löschte ihren Timer des Ruhemodus dann mit der Simulation, die nichts löscht, und der echte Timer lief weiter, nachdem die Karten die Seite verlassen hatten. dom.mjs beendet die Simulation, bevor die Karten gehen, sodass sie ihre echten Timer mit dem echten clearTimeout löschen.

Wann die End-to-End-Tests laufen

Der Browsertest mit der Prüfung der Barrierefreiheit gehört zu den End-to-End-Tests in Docker, dem Workflow Docker E2E (e2e.yml). Mit ihren Jobs nebeneinander dauern sie etwa fünf Minuten, länger als die meisten anderen Prüfungen, und die meisten Pull Requests ändern nichts, was sie testen. Deshalb fragt der Job changes zuerst die API, welche Dateien ein Pull Request ändert, ohne Checkout und ohne Action eines Dritten. Die Test-Jobs laufen nur, wenn eine davon zu dem gehört, was die Tests ausführen oder einbinden: die Integration in custom_components/, die Blueprints in blueprints/, die Tests selbst in tests/e2e/, package.json und package-lock.json mit axe-core und der Workflow .github/workflows/e2e.yml. Der Job live demo entscheidet für sich: Er läuft, wenn ein Pull Request etwas ändert, das die Live-Demo der Website spielt, die Karten in custom_components/autodarts/frontend/, die Seite in docs/demo/ mit docs/branding/ und scripts/mkdocs_hooks.py, ihren Browsertest (tests/e2e/live_demo.py, tests/e2e/live_demo.sh, tests/e2e/requirements-browser.txt) oder den Workflow. Ein Pull Request, der nur die Live-Demo ändert, etwa einer des Screenshot-Bots mit einer neuen Aufzeichnung, startet so nur diesen einen Job und keinen mit Home Assistant. Eine umbenannte Datei zählt auch mit ihrem alten Namen. Scheitert die API oder ändert ein Pull Request mehr Dateien, als die API auflistet (3000), laufen die Tests. Jeder Push auf main, jede Nacht und jeder Lauf auf Abruf testen alles, nachts auch gegen die Beta von Home Assistant.

Wann die End-to-End-Tests laufen: Ein Push auf main, der nächtliche Lauf und ein Lauf auf Abruf führen die Test-Jobs immer aus. Für einen Pull Request fragt der Job changes die API nach seinen Dateien; nutzen die Tests eine davon oder sind die Dateien unbekannt, laufen die Test-Jobs nebeneinander etwa 5 Minuten, sonst werden sie in Sekunden übersprungen. Die Pflichtprüfung e2e besteht, wenn jeder Test-Job bestanden hat oder keiner laufen musste, und schlägt fehl, wenn ein Job fehlschlug oder abgebrochen wurde.

main verlangt eine einzige Prüfung der End-to-End-Tests, e2e, vom Job gate. Eine Matrix, die eine Bedingung überspringt, meldet keinen ihrer Job-Namen, und ein übersprungener Job zählt als bestanden. Deshalb läuft das Gate immer und entscheidet selbst: Es besteht, wenn jeder Test-Job bestanden hat oder changes nichts zu testen fand, und schlägt fehl, wenn ein Test-Job fehlschlug, abgebrochen oder übersprungen wurde, obwohl er laufen musste. Ein Pull Request, der nur die Dokumentation oder andere Workflows ändert, bekommt e2e etwa eine Minute nach dem Push. tests/test_consistency.py schlägt fehl, wenn eine Compose-Datei eine Datei des Repositorys einbindet oder der Browsertest eine liest, die in dieser Liste fehlt.

Die Test-Jobs laufen nebeneinander. Drei Jobs scenario führen tests/e2e/run.sh aus, auf Board Manager 1, Board Manager 2 und Home Assistant 2026.8.0. Sechs Jobs browser führen den Browsertest aus, drei für das aktuelle und drei für das älteste unterstützte Home Assistant. Jeder Browser-Job lädt die Images von Home Assistant und Playwright zugleich. Der erste startet zwei Worker, BROWSER_SHARD=1/2 mit BROWSER_WORKERS=2, die Karten, Spiele und Formulare neben den gleichbleibenden Höhen; der zweite führt BROWSER_SHARD=3/4 aus, der dritte BROWSER_SHARD=4/4, sodass die vier Teile einer Version zugleich laufen. Die Touchscreens, Safari und die Barrierefreiheit haben einen Runner für sich: Zweimal konnte WebKit von Safari die Skripte von Home Assistant selbst nicht laden, während ein zweiter Browser daneben die neun Bildschirme von every screen size zugleich auf den vier Kernen eines Runners öffnete. tests/test_e2e_shards.py schlägt fehl, wenn der Job mit Safari einen zweiten Worker startet. Ein Schritt, der fehlschlägt, lässt nur seinen Job fehlschlagen, und der Bot für unzuverlässige Tests führt nur diesen Job und das Gate erneut aus. Der Job nennt den Teil und den Schritt, der fehlschlug, in einer Anmerkung und in seiner Zusammenfassung, mit dem Befehl, der den Schritt allein ausführt, etwa BROWSER_STEPS='steady heights' bash tests/e2e/browser.sh. Der nächtliche Job gegen die Beta führt das Szenario und danach jede Gruppe von Schritten nacheinander auf einer Demo-Instanz aus, aus demselben Grund.

Die End-to-End-Tests nebeneinander: Findet der Job changes etwas zu testen, starten neun Test-Jobs zugleich. Drei führen das Szenario aus, für Board Manager 1, Board Manager 2 und Home Assistant 2026.8.0. Sechs führen den Browsertest aus, drei für das aktuelle und drei für das älteste unterstützte Home Assistant, sodass alle vier Teile einer Version zugleich laufen, jeder auf einer eigenen Demo-Instanz. Der erste Runner einer Version führt zwei Worker aus, die Karten, Spiele und Formulare neben den gleichbleibenden Höhen; der zweite das Tablet, jede Bildschirmgröße und die Dartscheibe ohne Autodarts; der dritte die Touchscreens, Safari und die Barrierefreiheit allein, damit sich WebKit von Safari die CPU mit keinem anderen Browser teilt. Die Pflichtprüfung e2e wartet auf alle.

Netzwerk der Jobs

Jeder Job beginnt mit Harden-Runner. In den projekteigenen Workflows, den End-to-End-Tests, dem Bildvergleich und seinem Kommentar, dem Screenshot-Bot und dem Übersetzungsbot, sperrt er jeden Host, den die Schritte des Jobs nicht brauchen (egress-policy: block), damit eine kompromittierte Action, ein Paket oder ein Test kein Token und keine Daten anderswohin schicken kann. Ein Job, der keinen Container startet, läuft außerdem ohne sudo und Docker, damit kein Schritt die Sperre aufheben kann. Die Dienste von GitHub Actions selbst, etwa den Speicher der Artefakte und Caches, lässt Harden-Runner von sich aus zu.

Jobs Hosts Wofür
Übersetzungsbot, pull request des Screenshot-Bots api.github.com, github.com Sie dürfen Issues und Branches schreiben, also erreichen sie nur GitHub.
Kommentar des Bildvergleichs api.github.com Er liest die Berichte der Prüfung und schreibt den Kommentar, sonst nichts.
changes der End-to-End-Tests api.github.com die Dateien des Pull Requests
scenario github.com, ghcr.io, pkg-containers.githubusercontent.com der Code und das Image von Home Assistant
browser und die nächtliche Beta dieselben, mcr.microsoft.com, *.data.mcr.microsoft.com, registry.npmjs.org, pypi.org, files.pythonhosted.org das Image von Playwright, axe-core und die Python-Pakete des Browsertests
render des Bildvergleichs und des Screenshot-Bots wie browser ohne npm, dazu launchpad.net, launchpadlibrarian.net, brands.home-assistant.io pngquant und die Symbole der anderen Integrationen von Home Assistant, die die Bilder zeigen
compare und collect github.com, pypi.org, files.pythonhosted.org Pillow für den Vergleich
live demo der End-to-End-Tests github.com, mcr.microsoft.com, *.data.mcr.microsoft.com, pypi.org, files.pythonhosted.org das Image von Playwright und die Python-Pakete des Browsertests
timeline des Screenshot-Bots github.com, pypi.org, files.pythonhosted.org die Pakete der Tests, deren Home Assistant die Spiele der Live-Demo spielt; Python kommt vom Runner
das Gate e2e keine Es liest nur die Ergebnisse der anderen Jobs. Sein Lauf zeigt die Warnung, dass allowed-endpoints und denied-endpoints leer sind: Das ist gewollt, denn eine leere Liste sperrt jeden Host außer den Diensten von GitHub Actions.

Welche Hosts die Jobs erreichen: Harden-Runner, der erste Schritt jedes Jobs der projekteigenen Workflows, lässt einen Job nur die Hosts erreichen, die er nennt. Die Bots, die schreiben dürfen, der Übersetzungsbot, der Pull Request des Screenshot-Bots und der Kommentar des Bildvergleichs, erreichen nur GitHub und laufen ohne sudo und Docker. Die End-to-End-Tests und die Bilder erreichen den Code, die Images von Home Assistant und Playwright und ihre Pakete. Das Home Assistant, das sie starten, hat kein Radio Browser und ruft nichts im Internet auf, außer den Symbolen, die die Bilder zeigen. Jeder andere Host ist gesperrt, und das Protokoll des letzten Schritts des Jobs nennt ihn.

Das Home Assistant der Tests ruft selbst nichts im Internet auf. Sein Onboarding richtet Radio Browser ein, das gleich beim Start Radiosender im Internet nachschlägt, bevor ein Test es entfernen könnte. Deshalb nimmt tests/e2e/offline/radio_browser als benutzerdefinierte Integration im Container seinen Platz ein, und sein Konfigurationsablauf lehnt das Angebot ab; das Szenario prüft, dass kein Eintrag von Radio Browser existiert. Nur die Demo der Bilder lädt die Symbole der eingebauten Integrationen, die die Integrationsseite zeigt, über den Proxy von Home Assistant aus dessen CDN.

Ein Host, den ein Job nicht nennt, lässt seinen Schritt fehlschlagen, etwa mit einer DNS-Abfrage, die scheitert, oder einer abgewiesenen Verbindung. Den Host selbst nennt das Protokoll des letzten Schritts des Jobs, Post Block every host that the job doesn't need, als domain not allowed: <host>, und die Seite der Insights von StepSecurity, die Harden-Runner in seinem ersten Schritt verlinkt. Ein neuer Host kommt mit dem Schritt, der ihn braucht, in allowed-endpoints des Jobs, mit einem Kommentar, der sagt, wofür, und in tests/test_workflow_network.py. Dieser Test schlägt fehl für einen Job dieser Workflows, den er nicht kennt, einen Host, den ein Job nicht braucht, einen Job, der wieder nur aufzeichnet (audit), einen Job ohne Container, der sudo behält, und einen Job, der schreiben darf und mehr als GitHub erreicht. Harden-Runner filtert die Adressen des Runners und des Standardnetzes von Docker; die Container der Compose-Netze bekommen ihre Adressen von seinem DNS, das nur die Hosts des Jobs auflöst. Die Workflows, die der Blueprint pflegt, folgen dessen eigenen Regeln.

Mutationstests

Tests können eine Zeile ausführen, ohne zu prüfen, was sie tut. Jeden Samstag findet der Workflow Mutation (mutation.yml, den der Blueprint pflegt) solche Zeilen: mutmut ändert den Code auf Tausende kleine Arten, die Mutanten, etwa <= statt < oder eine andere Zahl, und führt gegen jeden Mutanten die Tests aus, die die geänderte Funktion aufrufen. Ein Test, der scheitert, erledigt den Mutanten; ein Mutant, den jeder Test durchlässt, überlebt und zeigt auf eine Zeile, die die Tests ausführen, aber nicht prüfen. Die Zusammenfassung des Laufs zeigt den Wert, den Anteil der Mutanten, die ein Test erledigt hat, und die überlebenden Mutanten; ein niedriger Wert lässt nichts fehlschlagen. Der Workflow läuft auch auf Abruf über den Reiter Actions.

Wie die Mutationstests laufen: Jeden Samstag und auf Abruf kopiert mutmut das Repository nach mutants/ und ändert die Spielregeln auf Tausende kleine Arten. Es führt jeden Test einmal aus und merkt sich, welche Funktionen jeder Test aufruft; die mit mutation_unsafe markierten Tests werden übersprungen, weil sie den Code als Text lesen oder Git fragen. Die Tests der Mutanten müssen dann ohne jeden Mutanten bestehen. Jeder Mutant führt die Tests aus, die seine Funktion aufrufen, in einem Prozess, der vom ersten Lauf abzweigt, und jeder Test beginnt mit leeren Caches der Integration. Ein Mutant, der einen Test scheitern lässt, ist erledigt; einer, den jeder Test durchlässt, überlebt.

[tool.mutmut] in pyproject.toml legt fest, was sich ändert. only_mutate nennt sieben Module der Spielregeln: die Checkouts, die Punkte der Darts, die Doubles, die Positionen, Cricket, den Bot und die Partyspiele. Die meisten Tests prüfen sie direkt, und ein Lauf auf den vier Kernen eines Runners erreicht alle ihre 2.804 Mutanten in unter zwei Stunden. Das X01-Übungsspiel, die Trainingsspiele und die Turniere kämen mit etwa 5.900 Mutanten dazu, mehr, als die 320 Minuten eines Laufs erlauben; sie folgen, sobald der Workflow den Lauf in Teile aufteilt. Der Mutation-Lauf vom 13. Oktober 2026 hat 1 h 51 min gedauert: Er hat 2.503 der 2.804 Mutanten erledigt, 294 überleben lassen und 7 am Zeitlimit gestoppt, ein Wert von 89 %. Die meisten Überlebenden stecken in den Partyspielen, im Bot und in den Positionen.

Vier Regeln halten die Tests unter mutmut richtig, und tests/test_mutation.py prüft jede davon:

  • Die Kopie. mutmut führt die Tests in seiner Kopie des Repositorys aus, mutants/, mit dem geänderten Code, den Dateien und Ordnern aus also_copy und ohne Git. Ein Test, der den Code als Text liest, etwa die Prüfung, dass jede Meldung ausgelöst wird, oder Git fragt, scheitert dort bei jedem Mutanten. Er bekommt @pytest.mark.mutation_unsafe, und tests/conftest.py überspringt ihn, sobald mutmut MUTANT_UNDER_TEST gesetzt hat. also_copy nennt jede Datei und jeden Ordner oben im Repository, die Git verfolgt, außer der Integration, und dazu die Dateien und Ordner, die kommende Blueprint-Updates hinzufügen, denn mutmut überspringt einen Pfad, den es nicht gibt. Fehlt ein neuer, scheitert ein Test und nennt die Einträge, die dazukommen müssen.
  • Die Caches. mutmut führt zuerst jeden Test einmal aus und merkt sich die Funktionen, die er aufruft, dann die Tests jedes Mutanten in einem Prozess, der von diesem ersten Lauf abzweigt. Ein Ergebnis, das ein functools-Cache dort behalten hat, stammt aus dem Code ohne den Mutanten und verdeckt ihn, und ein Test, der sein Ergebnis im Cache findet, wird nicht gemerkt. Deshalb leert die Fixture fresh_caches vor jedem Test die Caches der Integration, etwa die von checkout.py; das kostet keine Zeit, die sich zeigt.
  • Die Decorators. mutmut lässt eine Funktion mit Decorator unverändert, außer statischen und Klassenmethoden. Deshalb umhüllt ein functools-Cache seine Funktion, statt sie zu dekorieren, checkout = lru_cache(maxsize=8192)(_uncached_checkout), und ein Test scheitert, wenn ein Modul aus only_mutate eine Funktion mit etwas anderem als diesen, property oder abstractmethod dekoriert. Die Properties bleiben unverändert.
  • Die Parameter. mutmut führt jeden Test in seinem ersten Prozess mehrmals aus, um sich die Funktionen zu merken, die er aufruft, und dann ohne jeden Mutanten, und zweigt den Prozess jedes Mutanten davon ab. Ein Parameter, den ein Lauf geändert hat, erreicht so jeden späteren Lauf, so wie wenn eine IDE einen gescheiterten Test noch einmal ausführt. Ein Test, der seine parametrize-Werte ändert, scheitert deshalb sofort; ändere stattdessen eine Kopie (setup = dict(setup)).

Selbst ausführen lassen sie sich unter Linux oder macOS (mutmut nutzt fork, unter Windows also in WSL oder Docker):

.venv/bin/pip install --require-hashes --no-deps -r .github/mutation-requirements.txt
.venv/bin/mutmut run "custom_components.autodarts.checkout*"
.venv/bin/mutmut results
.venv/bin/mutmut show <Name eines Mutanten>

Unter mutmut ziehen die Property-Tests dieselben Beispiele wie in CI, damit ein Mutant auf jedem Rechner auf dieselbe Weise erledigt wird oder überlebt, und jeder Test bekommt das Zehnfache seines Zeitlimits: Jeder Aufruf einer geänderten Funktion läuft durch ein Trampolin von mutmut, das die langsamsten Tests um ein Mehrfaches verlangsamt, und einen Mutanten, der hängt, hält mutmut mit einem eigenen Limit an. mutmut behält seine Kopie und seine Ergebnisse in mutants/; lösche den Ordner danach.