Testosteron (Linux-Magazin, Juni 2012)

Ein neuer Service auf travis-ci.org schnappt sich Github-Projekte, rattert bei neuem Code durch deren Testsuites und benachrichtigt die Eigentümer, falls der Build bricht.

Gerade Open-Source-Projekte profitieren von agilen Entwicklungsverfahren mit Continuous-Integration (CI). Jede Veränderung des Codes im öffentlichen Repository sollte sofort den Lauf der Testsuite anstoßen, um die Entwicklergemeinde unmittelbar auf etwaige Probleme hinzuweisen.

Abbildung 1: Legt der Travis-User den Schalter für ein Projekt um, benachrichtigt Github den Travis-Service bei jedem Code-Update.

Konvention statt Klimmzügen

Wer nun meinte, es erfordere einige Klimmzüge, um mittels CI-Tools with Jenkins ([2], ehemals "Hudson") und den dafür notwendigen Konfigurationsdaten zu einer Dauertestumgebung zu gelangen, sah sich kürzlich überrumpelt: Auf travis-ci.org klickt der Entwickler lediglich den Login-Button, der zum Projekthoster Github verzweigt, dort die Repositories des Users aufspürt, und ebenfalls per Knopfdruck die Genehmigung einholt, diese im Administrationsbereich zu manipulieren. Zurück bei Travis findet der verduzte Entwickler dann eine Liste seiner Github-Projekte mit formschönen "On/Off"-Knöpfen (Abbildung 1). Ein Klick auf "On" stellt den CI-Server auf das Projekt ein. Nun muss nur noch eine etwa dreizeilige YAML-Datei in die Projektwurzel (Abbildung 2), und ab sofort wirft Travis bei jedem Push ins Github-Repo die Testsuite an (Abbildung 3) und meldet per Email, falls etwas zusammenbricht. Dabei unterstützt der Travis-Service neuerdings sogar Perl, und in drei verschiedenen Versionen (wahlweise 5.10, 5.12, 5,14), nachdem Java, JavaScript (mit Node.js), Python, PHP, Ruby, und sogar Clojure, Erlang, Groovy, Haskell und Scala schon seit längerem im Programm stehen.

Einfaches ist einfach

Im einfachsten Fall definiert die YAML-Datei .travis.yml wie in Abbildung 2 gezeigt nur die verwendete Sprache im Projekt (language: perl) und Travis leitet daraus per Konvention ab, wie sprachtypische Testsuites aufzurufen sind. In Perl geschieht dies üblicherweise mit dem Zweisatz perl Makefile.PL; make test, aber auch mit neueren Build.PL-Dateien kommt es klar. Nutzt ein Projekt von der Norm abweichende Testsuites, definiert der Entwickler einfach in .travis.yml, mit welchen Skripts diese aufzurufen sind.

Abbildung 2: In der Datei .travis.yml im Root-Verzeichnis eines Repositories steht, in welchen Umgebungen travis-ci.org Tests fahren soll.

Abbildung 3: Direkt nach einem Code-Update auf Github wirft Travis-ci.org die Test-Suite an.

Bei Null anfangen

Dabei läuft bei einem Perl-Projekt nicht nur die Testsuite ab, sondern der Travis-Service stellt auf einem seiner "Worker"-Systeme eine sauber aufgeräumte Umgebung bereit, holt die in den Projektdateien angegebenen CPAN-Module als Tarbälle vom nächsten CPAN-Mirror, installiert sie in der definierten Reihenfolge und stößt erst dann die Testsuite an. All das erfordert keinerlei Konfiguration, der Service leitet diese Schritte und Aktionen allein von den mit "Language: Perl" festgelegten Standardkonventionen ab.

Laufen die Vorbereitungen inklusive der eigentlichen Testsuite problemlos durch, zeigt Travis dies im Ausgabefenster an (Abbildung 4) und markiert den Durchlauf in der Übersicht mit einem grünen Punkt. Falls im vorherigen Durchlauf ein Fehler aufgetreten war, geht im Erfolgsfall ebenfalls eine Email an den oder die Entwickler, ansonsten schweigt Travis still.

Abbildung 4: Die Testsuite läuft erfolgreich durch.

Sofort steigt Rauch auf

Nun besteht für in die freie Wildbahn entlassene CPAN-Module bereits ein hervorragend funktionierendes Smoketest-Framework, das sich ungefragt Tarbälle vom CPAN holt, entpackt, in unterschiedlichen Produktionsumgebungen installiert und austestet. Treten Fehler auf, schicken die Smoketester Emails an die von den Entwicklern auf dem CPAN hinterlegten Adressen. Als agiler Ingenieur möchte man jedoch möglichst nach jedem Check-in kontinuierliche Unit-Tests fahren, um Fehler rechtzeitig aufzuzeigen, und nicht erst nach der Veröffentlichung des Tarballs auf dem CPAN.

Abbildung 5: Der in Ruby geschriebene Service-Hook alarmiert die travis-ci.org-Website, falls ein Entwickler neuen Code ans Github-Projekt geschickt hat.

Es verblüfft zunächst, wie unmittelbar nach dem Kommando git push auf Github die Testsuite auf travis-ci.org zu laufen beginnt. Github bietet für diese verzögerungsfreie Kommunikation im Repository sogenannte "Service-Hooks" an, die Anbieter wie Travis als Open-Source-Code in Ruby erstellen (Abbildung 5) und nach einem Review auf Github installieren. Von den paar Dutzend verfügbaren Service-Hooks darf der Eigentümer eines Entwicklungs-Repositories dann einen oder mehrere aktivieren, und Github führt den Code anschließend jedesmal sofort aus, falls ein Projektmitarbeiter Code eincheckt.

Abbildung 6: travis-ci.org möchte Admin-Zugang zu allen Repositories des Users erhalten, um dort jeweils den Service-Hook zu installieren.

Auf Nummer Sicher

Der Travis-Service macht es dem Repository-Eigentümer noch einfacher, denn es beantragt auf Github per OAuth Admin-Rechte auf dem Repository des Users (Abbildung 6). Nun versichern die Travis-Tester zwar, damit keinen Unfug zu treiben, doch sicher ist das nicht, und so empfiehlt es sich, die Rechte im Admin-Bereich des Repos kurz nach der Installation des Service-Hooks wieder zu entziehen (Abbildung 7). Das Anstoßen der Testsuite leidet dadurch nicht.

Abbildung 7: Nach der Installation des Service-Hooks widerruft der vorsichtige User die Genehmigung wieder zurück.

Schlechter Build, böses Karma

Vom Lesen des "Getting Started"-Guides ([3]) bis zur permanent laufenden CI-Umgebung vergehen dank dieser ausgefeilten Integration so selten mehr als fünf Minuten. Da der Travis-Service gebräuchliche Sprachkonventionen kennt, brauchen Entwickler normalerweise keine einzige zusätzliche Zeile Code zu schreiben. Wer allerdings zusätzliche Funktionen möchte, dem macht es der Travis-Service einfach, denn alle Testergebnisse stehen über kompakte REST-URLs als Web-Service zum Abruf bereit (Abbildungen 8 und 9). Die zurückkommenden kompakten JSON-Daten eignen sich für extravaganten interaktiven Browsercode, aber auch für neugierige Perl-Programme, die mit Hilfe des JSON-Moduls vom CPAN in den Daten herumschnüffeln können.

Abbildung 8: Per REST-API können Applikationen den Test-Status eines Projektes abfragen.

Abbildung 9: Die letzten Builds mit ihren Ergebnissen.

Wer zum Beispiel wissen möchte, welche Entwickler am häufigsten Code einchecken, der die Testsuite zum Abbruch zwingt, um ihnen negative Karma-Punkte zuzuweisen, klopft schnell ein Skript nach Listing 1 zusammen. Es holt die Testergebnisse der vergangenen Woche vom Travis-Service und ermittelt für jeden fehlgeschlagenen Lauf den für den eingecheckten Code verantwortlichen Entwickler. Das CPAN-Modul JSON exportiert auf Anfrage in Zeile 3 die Funktion from_json(), die später einen vom REST-Server kommenden JSON-String in eine Perl-interne Datenstruktur umformt.

Listing 1: karma

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use JSON qw( from_json );
    04 use DateTime;
    05 use DateTime::Format::Strptime;
    06 use Log::Log4perl qw(:easy);
    07 use LWP::Simple qw( get );
    08 
    09 Log::Log4perl->easy_init($DEBUG);
    10 
    11 my $github_api_url = 
    12   "https://api.github.com/repos";
    13 my $travis_api_url = 
    14   "http://travis-ci.org";
    15 
    16 my( $repo ) = @ARGV;
    17 die "usage: $0 name/repo" if 
    18   !defined $repo;
    19 
    20 my $build_json = get( 
    21   "$travis_api_url/$repo/builds.json" );
    22 my $build_data = from_json( $build_json );
    23 
    24 my $f = DateTime::Format::Strptime->new(
    25   pattern   => "%Y-%m-%dT%H:%M:%SZ",
    26   time_zone => "America/Los_Angeles",
    27 );
    28 
    29 my $last_week_dt = DateTime->today(
    30   time_zone => "local" )->
    31       add( weeks => -1 );
    32 
    33 my %build_breakers = ();
    34 
    35 for my $build ( @$build_data ) {
    36 
    37   if( $build->{ result } == 0 ) {
    38     DEBUG "Build $build->{ commit } ok";
    39     next;
    40   }
    41 
    42   my $build_dt = $f->parse_datetime( 
    43     $build->{ started_at } );
    44 
    45   if( $build_dt < $last_week_dt ) {
    46     DEBUG "Ignoring old build $build_dt";
    47   }
    48         
    49   my $github_request = 
    50     "$github_api_url/$repo/commits/" .
    51     "$build->{ commit }";
    52 
    53   DEBUG "Fetching $github_request";
    54 
    55   my $github_json = get( 
    56     "$github_api_url/$repo/commits" .
    57     "/$build->{ commit }" );
    58   my $github_data = 
    59     from_json( $github_json );
    60 
    61   my $committer = $github_data->
    62      { commit }->{ committer }->{ email };
    63 
    64   DEBUG "$committer broke build ",
    65     "$build->{ commit }";
    66   $build_breakers{ $committer }++;
    67 }
    68 
    69 for my $build_breaker ( 
    70         sort keys %build_breakers ) {
    71   print "$build_breaker: ",
    72     "-$build_breakers{ $build_breaker }\n";
    73 }

Da das Skript einige Zeit läuft, sorgt Log::Log4perl im DEBUG-Modus (Zeile 9) dafür, dass auf der Standard-Error-Ausgabe aktuelle Aktionen des Skripts wie das Einholen der REST-Anfragen erscheinen.

Von Travis nach Github

Der Travis-Service weiß allerdings nur die ID eines Commits, die für das Revision-Control-System git typische SHA1-Hash im Hexformat. Um festzustellen, welcher Entwickler den Code auf Github eingestellt hat, fragt Listing 1 unter der URL der Github-API (Zeile 11) nach den Metadaten des Commits und bekommt dort in einem weiteren JSON-String unter anderem auch die Email-Adresse des verantwortlichen Entwicklers heraus. Beide REST-APIs gleichen sich wie ein Ei dem anderen, was wohl eine Folge der zugrunde liegenden Ruby-Implementierung ist.

Da sich das Skript aus Zeitgründen nur für Commits der letzten Woche interessiert, verwirft es diejenigen Einträge aus den JSON-Daten mit älteren Commit-Zeitstempeln. In den JSON-Daten liegt das Datum der auslösenden Commits im Format "YYYY-MM-DDTHH::MM::SS" vor, doch der Format-Parser DateTime::Format::Strptime vom CPAN wandelt es jeweils in ein DateTime-Objekt um, damit Datumsvergleiche später einfach von der Hand gehen. So definiert Zeile 29 ein DateTime-Objekt mit dem Datum eines Tages, der genau eine Woche zurückliegt. Um festzustellen, ob ein Commit mehr als eine Woche zurückliegt, muss Zeile 45 dann nur noch zwei DateTime-Objekte mit dem überladenen <-Operator vergleichen.

Liegt ein Commit-Zeitstempel im aktuell untersuchten Bereich, schnappt sich Zeile 49 die Commit-ID, baut daraus einen URL für die ebenfalls für jedermann offene Github-API zusammen und die aus dem CPAN-Modul LWP::Simple exportierte Funktion get() schickt den HTTP-Request ab. Zurück kommt ein JSON-String, der in Perl nach der Umwandlung als Hash vorliegt. Unter dem Schlüssel commit findet sich ein Hash, mit einer weiteren Datenstruktur unter dem Eintrag committer. Dieser Hash enthält schließlich ein Feld email, das die Email-Adresse des armen Sünders offenbart.

Abbildung 10: Das Skript karma vergibt negative Punkte an Committer, deren Code einen Fehler in der Testsuite auslöste.

Zeile 66 füllt diesen Wert in einen Hash, der die versaubeutelten Commits pro Email-Adresse auftabuliert. Die For-Schleife ab Zeile 69 iteriert dann alphabetisch über die Entwickler, und die print-Funktion im Schleifenrumpf gibt zur Entwickler-Email jeweils die negative Karma-Punktezahl aus. Von Facebook ist bekannt ([4]), dass Release-Ingenieure an schusslige Entwickler ebenfalls negative Karma-Punkte vergeben und in Verruf geratene Kollegen während neuer Releases kritisch beäugen. Im Einzelfall lässt sich das Release-Team angeblich mit edlen alkoholischen Getränken beruhigen, doch Wiederholungstäter finden peinliche Kommentare in ihren jährlichen Beurteilungen.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/06/Perl

[2]

"Jenkins, an extendable open source continuous integration server", http://jenkins-ci.org/

[3]

"Getting Started", http://about.travis-ci.org/docs/user/getting-started/

[4]

Ryan Paul, "Exclusive: a behind-the-scenes look at Facebook release engineering", http://arstechnica.com/business/news/2012/04/exclusive-a-behind-the-scenes-look-at-facebook-release-engineering.ars

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.