Lithmustest (Linux-Magazin, Mai 2016)

Ob eine neue Linux-Installation alle gewünschten Funktionen bringt, findet der sorgfältige Admin nicht erst im Laufe der Zeit heraus, sondern gleich nach der Inbetriebnahme mittels einer Testsuite.

Im Rahmen des Schwerpunkts "Eigenes Repository" bat mich die Redaktion des Linux-Magazins diesen Monat um ein Testverfahren, um auf einer neu installierten Distro die Funktion eingehängter Drucker zu prüfen. Auf Ubuntu kommt unter dem Suchbegriff "Printers" dazu ein Dialog mit allen angeschlossenen Druckern hoch (Abbildung 1) und per Hand lässt sich dort einfach ein Drucker auswählen und dazu überreden, eine Testseite auszudrucken. Doch wie kann dies ein automatisch abgefeuertes Tool erledigen?

Erster Versuch

Das Linux Desktop Testing Project "LDTP" hat sich dem Testen von GUIs auf allerlei Plattformen inklusive Windows verschrieben, nutzt die Accessibility-Funktionen von Window-Managern wie Gnome oder KDE und erlaubt deren Fernsteuerung. Allerdings befindet sich das Projekt in keinem guten Zustand, denn die Dokumentation ist lückenhaft und selbst auf Nachfrage beim Entwickler konnte ich die GUI nicht dazu bewegen, entsprechend zu reagieren, weil entweder der LDTP-Dämons abstürzte oder anders als in der spärlichen Dokumentation beschrieben reagierte. Sicher eine gute Idee, aber die Wartung müsste sich deutlich verbessern, wenn das Projekt denn von praktischem Nutzen sein soll.

Abbildung 1: Alle installierten Drucker im Printers-Dialog

Auf Kommando

Aus der Patsche halfen mir die Kommandozeilen-Tools lpstat, lpr und lpq, die alle konfigurierten Drucker auflisten, ein Dokument drucken, sowie die Druckerwarteschlange visualisieren können. In ein Perlskript verpackt finden sie den Standarddrucker, lassen ihn ein Testdokument drucken und prüfen, ob der Auftrag erst in der Warteschlange erscheint und dann von dort nach Erledigung gelöscht wird. Zeigt die Ausgabe im Perl-üblichen TAP ("Test Anything Protocol") dann durchgehend "ok" und kein "not ok", gilt der Test als bestanden und die frisch installierte Distro ist funktionsfähig. Abbildung 2 zeigt die Ausgabe eines erfolgreichen Druckertests.

Abbildung 2: Die Testsuite findet den Default-Drucker, druckt dort ein Dokument und prüft die Warteschlange auf Erfolg.

Das Testskript in Listing 1 ruft hierzu über Perls Backquote-Mechanismus die Kommandozeilentools auf und speichert deren Ausgabe in Variablen zur späteren Überprüfung mittels regulärer Ausdrücke. Die des Test-Moduls Test::More exportieren die Funktionen ok und is, die Vergleiche zwischen Ist- und Sollwert vornehmen und schlagen Alarm falls ein unerwartetes Ergebnis auftritt. Die Funktion ok() prüft hierzu, ob der erste ihr hereingereichte Parameter einen wahren Wert enthält, während is() den gesehenen Wert (erster Parameter) mit dem erwarteten Wert (zweiter Parameter) vergleicht.

Von der Kommandozeile aus aufgerufen zeigt lpstat in Abbildung 2, dass auf meiner Ubuntu-Installation ein Multifunktionsdrucker namens "MFC7420" konfiguriert ist, sowie eine Reihe von Labeldruckern der Marke "Dymo", die ich vor drei Monaten in der Perl-Snapshot-Reihe vorgestellt habe ([3]). Da der Distrotest nur die Funktion des Defaultdruckers prüft, ruft das Testskript in Listing 1 lpstat -d auf, schnappt sich die Ausgabe, die den Defaultdrucker benennt, und verifiziert, ob eine Zeile mit einem Doppelpunkt herauskommt.

Abbildung 3: Das Kommando lpstat listet alle konfigurierten Drucker auf.

Standarddrucker

Findet lpstat einen Defaultdrucker, ist dies schon einmal ein Zeichen, dass die neu installierte Distro richtig konfiguriert ist. Für weitere Tests muss das Skript tatsächlich einen Druckauftrag abschicken, wer nicht unnötig Papier verschwenden will, sollte lptest-default mit der Option --noprint aufrufen, dann überspringt das Testskript den mit SKIP markierten Bereich ab Zeile 24. In diesem Fall zeigt die Ausgabe die eingeschränkte Testabdeckung:

    $ ./lptest-default --norealprint
    ok 1 - found default printer MFC7420
    ok 2 # skip printing disabled
    1..2

Listing 1: lptest-default

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Test::More;
    04 use Getopt::Long;
    05 use Path::Tiny;
    06 
    07 my $realprint = 1;
    08 GetOptions( "realprint!" => \$realprint );
    09 
    10 my $lpstat = "lpstat";
    11 my $lpr    = "lpr";
    12 my $lpq    = "lpq";
    13 
    14 my( $default_printer ) = 
    15   ( `$lpstat -d` =~ /: (.*)/ );
    16 
    17 if( !defined $default_printer ) {
    18     die "Cannot find default printer";
    19 }
    20 
    21 ok 1,
    22   "found default printer $default_printer";
    23 
    24 SKIP: {
    25     if( !$realprint ) {
    26         skip "printing disabled", 1;
    27     }
    28 
    29     ok !lpq_busy(), "lpq empty";
    30 
    31     my $temp = Path::Tiny->tempfile;
    32     $temp->spew( "This is a test." );
    33 
    34     my $rc = system $lpr, "-P", 
    35         $default_printer, $temp->absolute;
    36     is $rc, 0, "printing with $lpr";
    37 
    38     ok lpq_busy(), "lpq busy";
    39 
    40     while( lpq_busy() ) {
    41         sleep 1;
    42     }
    43 
    44     ok !lpq_busy(), "lpq empty";
    45 }
    46 
    47 done_testing;
    48 
    49 sub lpq_busy {
    50     my $queue = `$lpq`;
    51 
    52     return $queue =~ /active/;
    53 }

Die in Zeile 8 aufgerufene Funktion Getoptions() definiert hierzu einen Kommandozeilenparameter --realprint den Zeile 7 von Haus auf 1 setzt, also ohne Zutun des Users den Echtdruck ausführt. Das Ausrufezeichen am Ende der Option "realprint!" weist Getoptions() an, dass die Verneinung mit --norealprint auf der Kommandozeile die Variable $realprint im Skript auf einen falschen Wert setzt und damit den Echtdruck deaktiviert.

Ans Eingemachte

Im Normalfall eines richtigen Drucktests schaut der Aufruf von lpq_busy() in Zeile 29 mittels der ab Zeile 49 definierten Funktion über lpq nach, ob die Druckerschlange einen Auftrag enthält. Die Warteschlange ist bei einem neuinstallierten System leer, also meldet der Test in Zeile 29 Erfolg, falls lpq_busy() einen falschen Wert zurückliefert. Anschließend schreibt die Funktion spew() des CPAN-Moduls Path::Tiny einen Textstring in eine temporärer Datei, die system() mit dem Kommandozeilentool lpr an die Druckerschlange schickt. Der Aufruf von lpr sollte einen Exit-Code von 0 liefern, was die Testsuitenfunktion is verifiziert.

Kehrt lpr zurück, steht der Auftrag bereits in der Druckerschlange und ein nachfolgender Aufruf von lpq zeigt ihn dort an. Die while-Schleife ab Zeile 40 untersucht nun im Sekundentakt, ob der Auftrag irgendwann aus der Schlange verschwindet, was ein untrügliches Zeichen dafür ist, dass er tatsächlich auf dem Drucker gelandet ist und der Test damit endgültig erfolgreich abgeschlossen wurde. Nach diesen fünf Tests beendet done_testing() in Zeile 47 den Reigen und meldet der steuernden Testsuite mit dem Textstring "1..5", dass auch nichts zwischendurch abgestürzt ist.

Abbildung 4: Der Testrunner C fasst die Ausgabe einer oder mehrerer Testscripte zusammen

Teil des Ganzen

Besteht der Systemtest aus einer oder mehreren solcher Testskripts, ist es üblich, diese mit einem Testrunner wie prove zu starten, das der Perl-Distribution beiliegt. Dieses Tool schluckt die Ausgabe einzelner Tests und gibt im Erfolgsfall nur eine Zusammenfassung des Ergebnisses aus. Im Fehlerfall kommen hingegen weitere Details hoch, die helfen, die Ursache einzukreisen. Abbildung 4 zeigt die Ausgabe im Erfolgsfall, und es bietet sich an, nicht nur ein sondern gleich mehrere Skripts, eventuell mit Hilfe eines Glob-Zeichens wie in

    $ prove "/var/tests/*"

ablaufen zu lassen. Wichtig ist, dass alles automatisch läuft und die Einzelschritte keine manuellen Eingriffe erfordern.

Abbildung 5: Der Paket-Tausendsassa fpm schnürt aus der Testsuite ein Debianpaket.

Gut verpackt

Wie zieht sich nun Münchhausen an den Haaren aus dem Sumpf, wie kommt nun die Testsuite anfangs auf das System mit der zu testenden Installation? Es bietet sich an, das Testskript in ein Paket im Format der verwendeten Distribution zu verschnüren, es im Repository abzulegen, und bei Bedarf mittels des Paketmanagers von dort zu installieren. Am einfachsten geht das mit dem Paketschnürer fpm ([4]), der Debians .deb und RedHats .rpm, sowie das .pkg-Format von OSX beherrscht. Abbildung 5 zeigt das Schnüren eines Debian-Pakets allmytests. Da das Skript das CPAN-Modul Path::Tiny benötigt, das jedoch glücklicherweise bereits als libpath-tiny-perl im Debian-Repo existiert, bindet es fpm einfach mittels der Option -d ein. Installiert der User dann mittels sudo apt-get install das allmytests-Paket vom Repo, holt der Paketmanager das abhängige Paket kurzerhand ebenfalls von dort und löst die Abhängigkeit damit elegant auf. Ist der Perl-Core noch nicht Teil der Distribution, kommt er auf die gleiche Weise mit einer weiteren Option -d aufs System. Ist ein verwendetes CPAN-Modul noch nicht Teil der Distribution, hilft das in einer zurückliegenden Ausgabe des Perl-Snapshots vorgestellte Carton-Modul ([5]), es mit der Testsuite zu bündeln. Mit einer stetig wachsenden Testsuite lassen sich so auch bei flexiblen Änderungen Stabilität garantieren und Regressionen vermeiden.

Infos

[1]

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

[2]

LDTP ("Linux Desktop Test Project"): https://ldtp.freedesktop.org/wiki/

[3]

TODO Michael Schilli, "Dymo", Linux-Magazin, 2015

[4]

"Effing Package Management: fpm", https://github.com/jordansissel/fpm/wiki

[5]

TODO Michael Schilli, "Pro Tricks", Linux-Magazin, 2015

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in 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.