Das Problem beim Zusammenführen
PHPUnit kann mit der CLI-Option --coverage-php serialisierte Code Coverage-Daten in eine Datei schreiben. Das ist beispielsweise dann nützlich, wenn wir Code Coverage-Daten aus mehreren Testsuite-Läufen sammeln und anschließend zu einem einzigen Bericht zusammenführen möchten.
An dieser Stelle kommt PHPCOV ins Spiel: Es bietet seit langem den Befehl merge zum Kombinieren mehrerer serialisierter Code Coverage-Datendateien sowie den Befehl patch-coverage zur Berechnung der Code Coverage von Zeilen, die durch einen Patch geändert wurden.
Allerdings war PHPCOV aufgrund von Einschränkungen im Format der serialisierten Code Coverage-Daten von phpunit/php-code-coverage nicht so nützlich, wie es hätte sein können. Diese Einschränkungen machten es schwierig oder sogar unmöglich, Code Coverage-Datendateien in gängigen realen Szenarien zusammenzuführen.
Es gab drei Hauptprobleme:
- Serialisierte Code Coverage-Datendateien enthielten absolute Pfade. Das bedeutete, dass das Zusammenführen von Dateien, die auf unterschiedlichen Maschinen (oder sogar in unterschiedlichen Verzeichnissen auf derselben Maschine) erzeugt wurden, fehlschlug oder falsche Ergebnisse lieferte. Dies war Issue #925.
- Wenn PHPUnit als PHAR verwendet wurde, enthielten die serialisierten Daten interne
PHPUnitPHAR\-Namespace-Präfixe, die während des PHAR-Build-Prozesses hinzugefügt werden. Dies machte es unmöglich, Coverage-Daten, die mit dem PHAR von PHPUnit erzeugt wurden, mit Daten aus einem über Composer installierten PHPUnit zusammenzuführen oder PHPCOV (selbst als PHAR ausgeliefert) mit Datendateien zu verwenden, die vom PHAR von PHPUnit erzeugt wurden. Dies war PHPCOV-Issue #109. - Der alte Ansatz zum Zusammenführen lud alle Coverage-Daten in ein einziges
CodeCoverage-Objekt im Speicher. Bei großen Projekten mit vielen Testsuite-Läufen erforderte dies viel Speicher und war langsam.
Ein neues Serialisierungsformat
phpunit/php-code-coverage 14.0 führt eine vollständig neu gestaltete Serialisierungsschicht ein. Die alte Klasse PHP, die das gesamte CodeCoverage-Objekt einfach serialisierte, wurde durch eine neue Klasse Serializer ersetzt, die ein strukturiertes, versioniertes Format schreibt.
Das neue Format enthält die folgenden Informationen:
- Build-Informationen: ein Zeitstempel, der Name und die Version der PHP-Laufzeit, die Version von phpunit/php-code-coverage sowie der Name und die Version des Code Coverage-Treibers
- Basispfad: das längste gemeinsame Pfadpräfix aller abgedeckten Dateien, das aus den Dateipfaden in den Coverage-Daten entfernt wird
- Code Coverage-Daten: die eigentlichen Zeilen- (und optional Branch- und Path-) Coverage-Daten mit relativen Dateipfaden
- Testergebnisse: Informationen über die ausgeführten Tests, einschließlich ihrer Größe, ihres Status und ihrer Ausführungszeit
- Git-Informationen (optional): die Origin-URL des Repositorys, der Branch, der Commit-Hash und der Status des Arbeitsverzeichnisses
Die Verwendung relativer statt absoluter Pfade ist die entscheidende Verbesserung für das Zusammenführen. Der neue PathReducer ermittelt das längste gemeinsame Pfadpräfix aller abgedeckten Dateien, entfernt es aus jedem Dateipfad in den Coverage-Daten und speichert es als basePath. Das bedeutet, dass Coverage-Datendateien, die auf unterschiedlichen Maschinen oder in unterschiedlichen Verzeichnissen erzeugt wurden, ohne Probleme zusammengeführt werden können, solange die relative Struktur der Quelldateien gleich ist.
Das Namespace-Präfix PHPUnitPHAR\ wird nun automatisch aus serialisierten Daten entfernt, wenn PHPUnit als PHAR ausgeführt wird. Dies stellt sicher, dass Coverage-Datendateien, die mit dem PHAR von PHPUnit erzeugt wurden, vollständig kompatibel mit denen aus einem über Composer installierten PHPUnit und mit dem PHAR von PHPCOV sind.
Ein neuer Merger
phpunit/php-code-coverage 14.0 führt außerdem eine neue Klasse Merger ein, die die alte Methode CodeCoverage::merge() für das externe Zusammenführen ersetzt. Anstatt vom Aufrufer zu verlangen, jede Datei in ein CodeCoverage-Objekt zu laden und die Objekte dann nacheinander zusammenzuführen, arbeitet der neue Merger direkt auf den serialisierten Dateien.
Der Merger validiert, dass alle zusammenzuführenden Dateien mit derselben PHP-Laufzeitversion und demselben Code Coverage-Treiber erzeugt wurden. Wenn die Dateien Git-Informationen enthalten, überprüft er auch, dass sie sich alle auf denselben Repository-Zustand beziehen (gleiche Origin-URL, gleicher Branch, gleicher Commit und gleicher Status des Arbeitsverzeichnisses). Schlägt eine dieser Prüfungen fehl, wird eine aussagekräftige Exception geworfen.
Auch der Speicherverbrauch wurde verbessert: Der Zusammenführungsprozess vermeidet das Anlegen unnötiger Zwischen-Arrays, was den Speicherbedarf beim Kombinieren einer großen Anzahl von Coverage-Datendateien reduziert.
Die Klassen Unserializer und Merger arbeiten ausschließlich mit Coverage-Daten-Artefakten, nicht mit den Quelldateien, die abgedeckt wurden. Allerdings benötigt ein Werkzeug wie PHPCOV weiterhin Zugriff auf den abgedeckten Quellcode, um auf Basis serialisierter Code Coverage-Daten Code Coverage-Berichte zu generieren.
Git-Informationen
Seit PHPUnit 13.1 enthält die serialisierte Code Coverage-Datendatei Details zum aktuellen Zustand des Git-Repositorys, wenn die CLI-Option --include-git-information zusammen mit der CLI-Option --coverage-php verwendet wird: die Origin-URL, den Namen des Branches, den Commit-Hash, ob das Arbeitsverzeichnis sauber ist, und die Ausgabe von git status.
Diese Informationen erfüllen zwei Zwecke. Erstens sorgen sie für Nachvollziehbarkeit: Wenn wir uns eine Coverage-Datendatei ansehen, können wir genau erkennen, aus welcher Version des Codes sie erzeugt wurde. Zweitens, und noch wichtiger, ermöglichen sie es dem Merger zu überprüfen, dass alle zusammenzuführenden Dateien für denselben Zustand der Software erzeugt wurden. Dies verhindert, dass versehentlich Coverage-Daten aus unterschiedlichen Versionen des Codes zusammengeführt werden, was zu irreführenden Ergebnissen führen würde.
PHPCOV
PHPCOV 13.0 baut auf den neuen Fähigkeiten von phpunit/php-code-coverage 14.0 auf. Sein Befehl merge verwendet jetzt die neue Klasse Merger, was bedeutet, dass die langjährigen Einschränkungen rund um absolute Pfade und PHAR-Kompatibilität gelöst sind.
Der Befehl merge nimmt ein Verzeichnis mit .cov-Dateien entgegen und erzeugt einen kombinierten Code Coverage-Bericht. In PHPCOV 13.0 funktioniert das auch dann zuverlässig, wenn die einzelnen Dateien auf unterschiedlichen Maschinen (zum Beispiel in parallelen CI-Jobs) oder mit unterschiedlichen PHPUnit-Installationsmethoden (PHAR vs. Composer) erzeugt wurden. Das zusammengeführte Ergebnis kann in jedem der unterstützten Berichtsformate ausgegeben werden: Clover XML, OpenClover XML, Cobertura XML, Crap4j XML, HTML, reiner Text oder das XML-Format von PHPUnit.
Ein typischer CI-Workflow zum Zusammenführen von Code Coverage aus parallelen Testläufen sieht folgendermaßen aus:
- Jeder CI-Job führt PHPUnit mit
--coverage-php /pfad/zu/verzeichnis/$(uuidgen).cov --include-git-informationaus - Die
.cov-Dateien aus allen Jobs werden in einem einzigen Verzeichnis gesammelt, zum Beispiel/pfad/zu/verzeichnis -
phpcov merge --html bericht /pfad/zu/verzeichniserzeugt aus den Dateien in/pfad/zu/verzeichniseinen kombinierten HTML-Bericht im Verzeichnisbericht
Dieser Workflow funktioniert nun zuverlässig, unabhängig davon, ob die CI-Jobs auf unterschiedlichen Maschinen laufen, unterschiedliche Verzeichnisstrukturen verwenden oder ob PHPUnit als PHAR oder über Composer installiert ist.
Zusammenfassung
Die Kombination aus phpunit/php-code-coverage 14.0 und PHPCOV 13.0 löst langjährige Einschränkungen, die das Zusammenführen von Code Coverage-Daten fragil und fehleranfällig machten. Das neue Serialisierungsformat mit relativen Pfaden, automatischer Entfernung des PHAR-Präfixes, Build-Metadaten, optionaler Verifikation des Git-Zustands sowie eine eigene Merger-Klasse bilden eine solide Grundlage für zuverlässiges Zusammenführen von Code Coverage-Daten in realen CI-Umgebungen. Das Patch-Coverage-Feature von PHPCOV hilft Teams dabei, ihre Testbemühungen auf den Code zu fokussieren, der sich tatsächlich geändert hat.