Mutationstests, Fuzzing und Property-Tests¶
Die Tests über die Prüfungen eines Pull Requests hinaus: ob die Tests eine Änderung des Codes bemerken, und Eingaben, die niemand aufgeschrieben hat.
Mutationstests¶
Jede Woche laufen Tausende kleiner Änderungen des Codes gegen die Tests, ein großes Projekt in Teilen von je einem Job, und die Zusammenfassung zeigt den Anteil, den sie bemerken, und zuerst die Überlebenden, die die Logik ändern, mit ihren Änderungen; ein niedriger Wert ist ein Bericht, nie ein Fehlschlag, aber ein gefallener Wert von main öffnet ein Issue, und ein Lauf, der nichts misst, schlägt fehl, mit den Tests, die ohne jeden Mutanten fehlschlagen; ein Pull Request, der ändert, wie sie laufen, probiert sie aus.
Womit: mutation.yml: mutmut, Infection.
Die Mutationstests zeigen die Zeilen, die die Tests ausführen, aber nicht prüfen: Jeden Samstag ändern sie den Code auf Tausende kleine Weisen, die Mutanten, und führen die Tests aus, die jeden davon abdecken. Ein Mutant, den kein Test bemerkt, überlebt.
- Der Wert ist der Anteil der Mutanten, den ein Test bemerkt hat, unter denen des Codes, den die Tests ausführen: bei mutmut die getöteten unter den getöteten und den überlebenden, bei Infection sein Covered Code MSI. Das Dashboard zeigt ihn als gefangene Mutanten.
- Die Überlebenden, die die Logik ändern, kommen zuerst: Die Zusammenfassung ordnet jeden Überlebenden von mutmut nach seiner Änderung, wie
mutmut showsie ausgibt. Zuerst kommen die, die die Logik ändern, einen Operator, einen Vergleich, eine Zahl oder eine Bedingung, die ein Test bemerken sollte, nach Funktion, die meisten zuerst, jeder mit seiner Änderung; dann eine Tabelle der Funktionen, deren Überlebende None übergeben oder ein Argument auslassen, und eine derer, die nur einen Text ändern, etwa eine Meldung, eine Hilfe oder eine Zeile des Logs. Der Wert der Logik zählt nur die Überlebenden der Logik, undsurvivors.mdim Artefakt mutation enthält die Änderung jedes Überlebenden. Eine Zeile, deren Text kein Test prüfen muss, bekommt# pragma: no mutate, und mutmut lässt sie in Ruhe. - Ein großes Projekt läuft in Teilen: Der Job plan schätzt die Mutanten jeder Funktion und teilt den Code in Teile mit etwa 3.000 Mutanten, höchstens acht: ganze Module, und die Funktionen eines Moduls, das größer ist als ein Teil. Jeder Teil ist ein eigener Job, der nur seine Mutanten ausführt, damit ein Projekt mit 10.000 Mutanten innerhalb der sechs Stunden eines Jobs von GitHub fertig wird, und der Job report fügt die Teile zu einem Wert zusammen. Die Variable
MUTATION_PARTSdes Repositorys setzt die Zahl der Teile anstelle der Schätzung. - Ein gefallener Wert öffnet ein Issue: Der Bericht vergleicht einen vollständigen Lauf des Standard-Branches mit dem Lauf davor, aus
mutation.jsonseines Artefakts. Fällt der Wert oder der Wert der Logik um mehr als einen halben Punkt, öffnet er das Issue Mutation score dropped, mit beiden Werten und den Funktionen, die Überlebende der Logik dazubekamen, hält es aktuell, solange der Wert darunter bleibt, und schließt es, sobald ein Lauf wieder beim Wert vor dem Fall ist. Ein Lauf, der an seinem Zeitlimit stoppte oder einen Teil verlor, wird mit keinem anderen verglichen. - Ein niedriger Wert lässt nichts fehlschlagen, auch nicht unter einem Minimum, das die eigene Konfiguration des Projekts setzt, aber ein Lauf, in dem kein Mutant lief, schon: mutmut führt jeden Test zuerst einmal ohne Mutanten aus, und schlägt einer dort fehl, nennt ihn die Zusammenfassung unter The tests fail without any mutant, mit dem Ende der Ausgabe von mutmut oder Infection, und eine Annotation des Jobs nennt den ersten, die der Bot-Health-Bot in seinem Issue zitiert. Eine kaputte Konfiguration oder ein Test, der in der Kopie nicht laufen kann, zeigt sich in der Woche, in der sie kaputtgeht, und der Bot-Health-Bot öffnet sein Issue.
- Ein Pull Request, der ändert, wie die Mutanten laufen,
mutation.ymloder das gepinnte mutmut, probiert sie aus, jeden Teil 25 Minuten lang, und zeigt einen Fehler als roten Check vor dem wöchentlichen Lauf. Der Check ist nicht erforderlich, also wartet ein Pull Request, der sich selbst zusammenführt, etwa ein Update von mutmut, nicht auf ihn: Sein roter Check ist eine Warnung zum Lesen. - mutmut führt die Tests in einer Kopie des Repositorys aus,
mutants/, mit dem geänderten Code und ohne Git. Ein Test, der dort nicht funktionieren kann, weil er den Code als Text liest, Git fragt oder das Werkzeug startet, das in der Kopie der Code mit den Mutanten ist, bekommt die Markierung@pytest.mark.mutation_unsafe, und die Mutationstests lassen ihn aus. Ebenso ein Test, der ein Modul des Codes in ein eigenes Repository kopiert und dort ausführt, etwapins.pyfür den Fix-Bot: Außerhalb vonmutants/findet der Code mit den Mutanten keine Konfiguration von mutmut, und ein Test des Blueprints schlägt fehl, wenn einem solchen Test des Blueprints die Markierung fehlt. Diepyproject.tomleines neuen Python-Projekts registriert die Markierung, undMUTANT_UNDER_TEST, das mutmut setzt, sagt einem Test, dass er dort läuft. mutmut lernt, welche Tests eine Funktion abdecken, indem es alle in einem Prozess ausführt: Ein Test einer Funktion mit einem Cache vonfunctools, etwa@functools.cache, leert ihn mitcache_clear(), damit er den Code ausführt und nicht das Ergebnis, das ein anderer Test hinterließ. - mutmut kopiert eine Funktion für jeden ihrer Mutanten ganz, daher macht eine lange Funktion mit vielen davon, etwa ein Parser mit den Optionen jedes Befehls, ein Modul von vielen Megabyte, das jeder Teil und jeder Test, der in der Kopie das Werkzeug startet, minutenlang kompiliert, über ihre Zeitgrenzen hinaus. Eine solche Funktion wird in kleinere geteilt: Der Parser von
blueprint.pyfügt jede Gruppe seiner Befehle in einer eigenen Funktion hinzu, und ein Test des Blueprints schlägt fehl, wenn eine Funktion seines Codes mehr als 50.000 Zeilen Kopien machen würde. - Eine Konfiguration des Projekts hat Vorrang: eine Tabelle
[tool.mutmut]inpyproject.tomloder ein Abschnitt[mutmut]insetup.cfgfür mutmut, eineinfection.json5für Infection. Ohne sie mutiert mutmutcustom_components/,src/oder die Module und Pakete der Wurzel, und Infection mutiertlib/auf PHP 8.3, mittests/bootstrap.php, das die Klassen von Nextcloud lädt. Der Plan folgtsource_paths,only_mutateunddo_not_mutatevon mutmut.
Fuzzing¶
Für ein Python-Paket oder eine GitHub Action: abdeckungsgesteuertes Fuzzing jedes Ziels in fuzz/, je eine Minute in jedem Pull Request, der den Code, die Ziele oder die Werkzeuge ändert, und jede Woche 25 Minuten, die sich alle Ziele teilen; ein Fund lässt den Lauf scheitern, mit der Eingabe, die ihn auslöste, im Log.
Womit: fuzz.yml: Atheris.
Property-Tests¶
In Python- und Home-Assistant-Projekten: die Property-Tests jede Nacht mit neuen zufälligen Eingaben, über die festen Eingaben der CI hinaus; ein Fehlschlag zeigt seinen Seed in der Zusammenfassung, und ein Lauf von Hand mit diesem Seed wiederholt ihn.
Womit: properties.yml: Hypothesis.