Die Testsuite von Ramps Python-Monolithen näherte sich allmählich 20 Minuten, als das Team auf über 5.000 Tests skalierte, überwiegend funktionaler/integrationstestartiger Natur. Statt die Suite neu zu schreiben, ging das Infrastrukturteam die Laufzeit direkt an: die CI-Parallelität erhöhen, indem ein Datenbank-Setup-Engpass behoben wurde, der parallele Worker auf vier begrenzte; die Test-Datenbank zwischen lokalen Läufen nicht mehr von Grund auf abbauen und neu aufbauen; und einen inkrementellen Test-Runner bauen, der nur Tests ausführt, die für eine gegebene Codeänderung relevant sind.
Die erste Version des inkrementellen Runners nutzte coverage.py, um für jeden Test abzubilden, welche Codezeilen er ausführte, und verglich dann bei einem neuen Commit die geänderten Dateien mit dieser Abbildung. Es funktionierte, hatte aber eine spezifische Lücke: Ramps Codebasis verwendet Flask-SQLAlchemy-Modellklassen, und die Coverage-Messung schreibt fast die gesamte Ausführung den Interna von SQLAlchemy zu statt der Deklaration der Modellklasse selbst. Das Hinzufügen einer Spalte zu einem Modell registrierte sich unter reiner Zeilen-Coverage nicht als 'diese Klasse hat sich geändert', sodass der inkrementelle Runner Tests überspringen konnte, die einen schemaändernden Fehler hätten erkennen sollen.
Um das zu beheben, ersetzte das Team coverage.py durch Pythons eingebautes sys.settrace, inspizierte `call`-Events auf ein `self`- oder `cls`-Argument, löste dieses Objekt zu seiner Klasse auf und führte einen Breitensuchlauf über `__bases__` durch, um jede Vorfahrenklasse der Datei zuzuordnen, in der sie definiert war. Dadurch konnte der Snapshot nicht nur festhalten, welche Zeilen ein Test ausführte, sondern auch, von welchen Klassen in seiner vollständigen Vererbungskette er abhing – sodass eine Änderung an der Deklaration einer Basisklasse korrekt die Tests für jede Unterklasse invalidierte, selbst wenn kein Codepfad der Unterklasse direkt ausgeführt wurde.
In Kombination mit der Parallelitätsbehebung und dem Überspringen vollständiger Datenbank-Neuaufbauten deckte der inkrementelle Runner bei einem typischen Commit 30–40 % der Suite ab und fing etwa 90 % der Fehler ab, die ein vollständiger Lauf abgefangen hätte, was Ingenieuren in wenigen Minuten umsetzbares Signal lieferte statt der vollen 10–20.
Klassenvererbung zur Laufzeit nachverfolgen, sodass eine nicht ausgeführte Änderung an einer Basisklasse dennoch ihre Tests auslöst
Coverage.py zeichnet ausgeführte Zeilen auf, nicht die Klassen, zu denen sie gehören, sodass Ausführung innerhalb der ORM-Interna von SQLAlchemy nicht der Modellklasse des Nutzers zugeordnet wird.
sys.settrace legt Call-Frames direkt offen, sodass die Inspektion von `self`/`cls` wiederherstellt, 'welche Klasse ist das', unabhängig davon, wo die Ausführung auf Zeilenebene stattfand.
Das Durchlaufen von `__bases__` von dieser Klasse aus verwandelt ein Call-Event in eine vollständige Vorfahrenliste, sodass eine Änderung an einer Basisklasse jeder Datei in der Kette zugeordnet wird.
Den inkrementellen Runner parallel zur vollständigen Suite laufen zu lassen, nicht an ihrer Stelle, bedeutete, dass die Erkennungslücke von ~10 % nie zu einem Produktionsrisiko wurde.
Eine coverage-basierte Abkürzung für Abhängigkeiten kann echte Abhängigkeiten wie Vererbung übersehen, die Coverage-Tools nicht dem geänderten Code zuordnen – prüfen Sie auf diesen blinden Fleck, bevor Sie dem schnellen Pfad vertrauen.
Ramp befand den inkrementellen Runner über diesen Punkt hinaus nicht weiterer Investitionen für wert – das Schließen der verbleibenden Erkennungslücke von ~10 % würde eine Umstrukturierung um etwas wie Bazel erfordern, damals nicht die höchste Priorität. Allein die Parallelitätsbehebung (Verdopplung der Maschinengröße nach Entfernen eines redundanten Datenbank-Resets pro Worker) halbierte die CI-Zeit, und das Überspringen vollständiger Datenbank-Neuaufbauten lokal sparte 10–30 Sekunden pro Lauf. Das Team veröffentlichte die auf sys.settrace basierende Vererbungsdurchquerung als Alternative zu Open-Source-Optionen wie pytest-testmon, denen Ramps Parallelisierungsunterstützung fehlte.
DEN BELEGEN FOLGEN
Die Quellen
- Faster Python Tests Ramp Engineering (Builders Blog) · builders.ramp.com ·