Continuous Integration Prozess
Samstag, 9. Oktober 2004

Angeregt durch einen Artikel von Martin Fowler über Continuous Integration habe ich versucht etwas Gleichwertiges für meine Firma einzurichten.

Wir arbeiten mit Delphi, Team Coherence, Finalbuilder und TestComplete. Die letzten beiden sind nicht wirklich relevant für die Implementation, die ersten beiden schon: Die Anwendung ist in Delphi geschrieben und macht ausgiebig von der Team Coherence TcVcsApi Gebrauch.

Der Team Coherence Server läuft auf einem Windows 2000 Server und für die automatischen Builds und Tests verwenden wir eine dedizierte Build-und-Test-Station, wobei beide Aufgaben natürlich auch auf dem gleichen Rechner durchgeführt werden können.

Schreiben wir mal ein paar Spezifikationen für das System auf:

  1. Automatische Compile Checks und Regression Tests sollen immer dann stattfinden, wenn jemand Änderungen eingecheckt hat. Da CheckIns im Allgemeinen keine atomaren Prozesse sind, müssen wir Vorkehrungen treffen, die den Benutzern die Gelegenheit gibt, ihre Arbeit abzuschließen und eine konsistente Source-Basis herzustellen.
  2. Bei jedem Build und Test werden immer nur die Benutzer benachrichtigt, die tatsächlich Änderungen eingecheckt haben. Sprechen wir also im Folgenden von Benutzern, sind genau diese gemeint.
  3. Zunächst wird versucht die Sourcen zu kompilieren. Im Fehlerfall bekommen die Benutzer eine Fehlermeldung. Jeder dieser Fehler sollte sofort behoben werden, da er dirket die Arbeit der anderen Programmierer beeinflußt. Da diese Fehler meistens auf neue Dateien zurückzuführen sind, die im Repository fehlen, sollte dies kein Problem sein, wenn man sich den Compiler-Output ansieht.
  4. Nach erfolgreicher Kompilierung werden die Regression Tests durchgeführt und die Benutzer über das Ergebnis informiert. Ein fehlgeschlagener Regression Test muß erst behoben werden, bevor die neue Version an die QA weitergeleitet wird.
  5. Wenn alles gut gegangen ist, verteilen wir die Dateien an das QA Team und beglücken sie mit der Nachricht über neue Arbeit.

Somit besteht unsere erste Aufgabe darin, festzustellen, ob kürzlich etwas eingecheckt wurde. Da im Repository nicht nur Source-Dateien, sondern auch Sprach-Resourcen, Dokumentation, Testdaten und Anderes abgelegt sind, müssen wir entscheiden, welche Dateien für Kompilierung und Regression Tests relevant sind und somit auf CheckIns überwacht werden müssen. Der einfachste Ansatz besteht darin, nur die Sourcecodes zu überwachen, die in einer eigenen Verzeichnisstruktur des Projekts liegen sollten. Ausgefeiltere Lösungen können auch die Testdaten oder was sonst noch sinnvoll erscheint mit einbeziehen.

Die nächste Frage ist: Wie können wir erkennen, welche Revisionen kürzlich eingecheckt sind? Was bedeutet "kürzlich" in diesem Zusammenhang?

Es stellt sich als weitaus effektiver heraus, sich erst auf den zweiten Teil der ersten Spezifikation zu konzentrieren, der uns zwingt, erst eine kleine Weile nach dem letzten CheckIn etwas zu tun. Dazu berechnen wir die letzte erlaubte CheckIn-Zeit ein paar Minuten vor "jetzt" und iterieren über alle Dateien in dem überwachten Verzeichnis und seinen Unterverzeichnissen und überprüfen den Zeitstempel der Tip-Revision (die direkt aus dem File-Info entnommen werden kann ohne in die Revisionen zu müssen) mit unserem berechneten Wert. Wenn wir einen neueren Zeitstempel finden, können wir daraus die Zeit für den nächsten Check berechnen, womit wir garantieren, nur die eingestellte Verzögerungszeit einzuhalten, nicht mehr. Nennen wir dies den "First-Level-Check". Wenn alle Dateien im Ordner älter als der angegebene Wert sind, können wir etwas tiefer einsteigen in den "Second-Level-Check".

Wir definieren eine Datei als "geändert" wenn ein CheckIn nach dem letzten erfolgreichen Regression Test stattgefunden hat. Wir erreichen das durch das Anhängen eines neuen Versionslabels an jeden Satz Revisionen, die es durch den Regression Test geschafft haben. So können wir von der Tip-Revision jeder Datei die Revisionen hinunter iterieren bis zu der Revision, die dieses Label des letzten positiven Tests angehängt hat. Machen wir doch ein Beispiel:

Seien "FileA" und "FileB" zwei von unseren Sourcedateien und sei "V6.0.6.19" das Label, das den letzten erfolgreichen Regression Test markiert. Ein Blick in Team Coherence zeigt uns

FileA:

  • Revision 1.19 attached labels: V6.0.6.18, V6.0.6.19
  • Revision 1.18 attached labels: V6.0.6.17
  • Revision 1.17 attached labels: V6.0.6.16
  • ...

FileB:

  • Revision 1.24
  • Revision 1.23
  • Revision 1.22 attached labels: V6.0.6.19
  • Revision 1.21
  • Revision 1.20 attached labels: V6.0.6.18
  • ...

Wir können sehen, daß FileA seit Regression Test V6.0.6.19 nicht verändert wurde, während FileB seit dem zweimal verändert wurde.

Haben wir nun das Label "V6.0.6.19" für den letzten erfolgreichen Regression Test, iterieren wir durch alle relevanten Dateien und für jede Datei iterieren wir beginnend mit der Tip-Revision die Revisionen nach unten durch bis wir das Label "V6.0.6.19" finden oder den Boden der Revisionen erreichen. Haben wir das Label gefunden und ist die gelabelte Revision gleichzeitig die Tip-Revision, hat sich die Datei nicht geändert. Sind die Revisionen unterschiedlich, hat sich die Datei verändert. Wurde das Label gar nicht gefunden, ist die Datei neu und wir betrachten das erst recht als Änderung.

Nachdem wir veränderte Dateien gefunden haben starten wir den Build-und-Test-Prozess und geben ihm den Zeitstempel mit, den wir für die Überprüfung verwendet haben. Der Zeitstempel ist notwendig, da zwischenzeitlich CheckIns vorgekommen sein können. Der Build-Prozess verwendet nur Revisionen, die nicht neuer als dieser Zeitstempel sind, und hängt ein spezielles Test-Label an sie, das bei einem erfolgreichen Test wieder entfernt wird.

Angenommend die Compile- und Regression-Tests zeigen keine Probleme und haben das neue Versionslabel an die Revisionen gehängt, erfahren wir davon und verwenden dieses Label für unsere weiteren Überprüfungen. Sollte uns jemand dazwischengefunkt und zwischenzeitlich etwas eingecheckt haben, so werden wir dies bei unserer nächsten Überprüfung gewahr und starten den Prozess erneut. Da die Verzögerungszeit normalerweise kürzer als die Dauer des Tests ist, wir die nächste Überprüfung kurz nach Beenden des Build-Prozesses gestartet.

Im seltenen Fall des Fehlschlagens des Tests werden wir diese geänderten Tests bei jeder Überprüfung wiederfinden, da sie sich gegenüber des Referenzlabels verändert haben. Da der letzte Regression Test fehlgeschlagen ist, bleibt das alte Label des letzten erfolgreichen Regression Tests erhalten. Wenn wir dies nicht richtig abfangen, werden wir wiederholt neue Tests starten und die Benutzer mit Fehlermeldungen überschütten, die sie schon längst kennen, und sie damit nur von der Fehlerbehebung abhalten.

Um dies zu umgehen achten wir beim Iterieren der Revisionen auch auf dieses Testlabel. Wenn wir es in der Tip-Revision finden, brauchen wir keinen neuen Test. Finden wir es aber in einer älteren Revision, hat offenbar jemand die fehlerhafte Revision behoben und dies rechtfertigt einen neuen Test.

Da gibt es aber noch eine häßliche Kleinigkeit in diesem ganzen System - erkannt? Wenn sich seit dem letzten erfolgreichen Regression Test gar nichts geändert hat, sind die Zeitstempel aller Dateien alt genug, den First-Level-Check zu bestehen und wir gehen jedesmal zum Second-Level-Check über. Dies ist offensichtlich überflüssig, da das Erkennen von keinem neuen CheckIn einfach sein sollte. Hier haben wir einen Fall, bei dem ein CheckIn getriggertes Server-Side-AddIn im Vorteil ist. Aber wir kriegen das auch hin. Erinnern Sie sich, im First-Level-Check sehen wir uns die Zeitstempel jeder Datei an, um eine "zu junge" Datei zu finden. Wenn wir uns nun den Zeitstempel der jüngsten Datei in jedem Fall merken, können wir schon im First-Level-Check leicht eine neuere jüngste Datei erkennen. Ist der Zeitstempel der jüngsten Datei nicht neuer als der von der letzten Überprüfung, ist nichts passiert.

Home  |  Archiv   |  Bücher
Made with CityDesk