Am vorvergangenen Wochenende (9./10. Januar 2021) gab es relativ viel Wirbel um die Plattform Parler, die offensichtlich als Twitterersatz für hauptsächlich amerikanische Konservative
diente. Amazon Web Services (AWS) kündigte Parler alle Services, da dort jede Menge Hass verbreitet wurde. Bevor alles abgeschaltet war, wurden wohl alle öffentlich verfügbaren Daten über ein API heruntergeladen. Ein paar der Fehler, die bei dem API gemacht wurden, werde ich kurz in der Schulung im März erläutern.
In diesem Blogpost möchte ich auf einen Aspekt eingehen, der viel über Benutzer verraten kann: Metadaten bei Bildern und Videos. Bei den öffentlichen Bildern und Videos auf Parler waren in den Bildern noch alle Metadaten enthalten. So konnte der Twitternutzer kcimc ein Bild erstellen, auf der gut zu erkennen ist, woher die Parler-Nutzer kommen:
Quelle: https://twitter.com/kcimc/status/1348815246039805953
Sollte es nicht Sinn und Zweck der Veröffentlichung sein, den Besucher*innen die GPS-Daten zur Verfügung zu stellen, sollte man diese Metadaten entfernen.
Schauen wir uns aber erstmal an, welche Daten an einem Bild gespeichert sind. Dazu nehme ich ein Bild, dass ich mal in Frankfurt gemacht habe. Das Bild ist auch im Coderepository zu finden.
Alle möglichen Daten – unter anderem die Geodaten, aber auch Daten zur verwendeten Kamera und deren Einstellungen für das Bild – finden sich in den sogenannten EXIF-Daten. EXIF steht hierbei für Exchangeable Image File Format
. Die Daten werden als Plain-Text in den Header des Bildes geschrieben. Es gibt sehr viele Programme, mit denen die Daten ausgelesen und geändert werden können, aber hier geht es mehr um die Serverseite.
Zur Bearbeitung der Metadaten mit Perl eignet sich das Modul Image::ExifTool
. Das Auslesen der Daten damit sind nur wenige Zeilen Code:
use Data::Printer;
use File::Basename;
use Image::ExifTool;
my $image = $ARGV[0] || dirname(__FILE__) . '/20191106_171714.jpg';
my $exif = Image::ExifTool->new;
my $info = $exif->ImageInfo( $image );
p $info;
Aus den Informationen lässt sich viel herauslesen, z.B. die Kameraeinstellungen wie
Flash "Fired",
FlashpixVersion "0100",
FNumber 1.9,
FocalLength "3.7 mm",
FocalLengthIn35mmFormat "28 mm",
FocalLength35efl "3.7 mm (35 mm equivalent: 28.0 mm)",
ShutterSpeed "1/15",
ShutterSpeedValue "1/15",
Aber eben auch die GPS-Informationen:
GPSLatitude "50 deg 6' 41.10" N",
GPSLatitudeRef "North",
'GPSLatitude (1)' "50 deg 6' 41.10"",
GPSLongitude "8 deg 44' 6.90" E",
GPSLongitudeRef "East",
'GPSLongitude (1)' "8 deg 44' 6.90"",
GPSPosition "50 deg 6' 41.10" N, 8 deg 44' 6.90" E",
Und das Löschen ist damit genauso schnell erledigt:
use Data::Printer;
use File::Basename;
use Image::ExifTool;
my $image = dirname(__FILE__) . '/20191106_171714.jpg';
my $exif = Image::ExifTool->new;
my @tags = (
'GPSLatitude', 'GPSLatitudeRef', 'GPSLatitude (1)',
'GPSLongitude', 'GPSLongitudeRef', 'GPSLongitude (1)',
);
for my $tag ( @tags ) {
$exif->SetNewValue( $attr );
}
$exif->SaveNewValues();
$exif->WriteInfo( $image );
Nach der Instanziierung des ExifTools definieren wir einfach eine Liste der Tags, die wir löschen wollen. Wird bei SetNewValue
nur der Name angegeben, wird das Tag gelöscht. Um alle Tags zu löschen, könnten wir auch einfach $exif->SaveNewValue('\*')
aufrufen.
Wenn man als Benutzer*in ganz sicher sein möchte, dass die Geodaten nicht ungewollt preisgegeben werden, muss man entweder vor dem Hochladen/Posten diese Daten wie gezeigt entfernen oder gleich die Nutzung der Standortdaten durch die Kamera unterbinden. Nutzt man die Teilen
-Funktion bei der Foto-App auf Mobiltelefonen, werden die Geodaten bereits gelöscht.
Als Betreiber*in einer Seite, auf der Nutzer*innen Fotos hochladen können, sollte man sich überlegen, ob man einige der Daten nicht lieber entfernt. In den Metadaten können viele Datenschutzrelevante Informationen zu finden sein. Auf Dr. Datenschutz gibt es einen interessanten Artikel dazu: https://www.dr-datenschutz.de/metadaten-wer-wann-mit-wem-wie-lange/.
Allerdings ist zu beachten, dass in den EXIF-Daten auch Urherberdaten gespeichert sein können, die besser nicht gelöscht werden. Das musste auch Facebook 2016 erfahren...
Zusammenfassend lässt sich sagen, dass die EXIF-Daten für einige Zwecke sehr nützlich sind, aber auch ungewollt Daten preisgeben können. Man sollte sich also anschauen, was man mit Bildern und/oder Videos anfangen möchte und welche Informationen dafür notwendig sind.
Permalink: /2021-01-19-geodaten-bilder-videos-exif
Git ist eine weit verbreitete Software zur Versionsverwaltung. Wir nutzen Git seit vielen Jahren, um unseren Perl-Code zu verwalten. Soll im Git-Workflow etwas erzwungen werden, kommen sogenannte Git-Hooks zum Einsatz.
Wenn Code committet oder zum Server ge*pusht* wird, werden diese Hooks ausgeführt. Das sind Skripte, die automatisch bei diesen Events ausgeführt werden. Eine Liste mit den ganzen Events ist im Git-Handbuch zu finden. Man unterscheidet die Hooks danach, ob sie auf dem Client oder auf dem Server ausgeführt werden.
Git ist bei den Hooks sehr flexibel und die Hooks können in allen möglichen Programmiersprachen umgesetzt werden – auch in Perl. Hier soll ein Beispiel gezeigt werden, wie ein solcher Hook aussehen kann. Ziel ist es, bei einem vorhandenen cpanfile die darin genannten Perl-Module zu prüfen, ob sie in CPAN::Audit genannt sind. Damit soll einfach sichergestellt werden, dass schon bei der Entwicklung auf Sicherheitslücken in eingesetzten Perl-Modulen hingewiesen wird.
Im ersten Schritt schreiben wir ein Skript, das auf dem Client ausgeführt wird. Die Hooks liegen im Ordner .git/hook. Dort erstellen wir eine Datei mit dem Namen des Events auf das wir reagieren wollen. In diesem Fall möchten wir die Prüfung nach jedem Commit machen. Daher erstellen wir die Datei post-commit und machen diese ausführbar.
In diesem Fall werden keine Paramter übergeben. Wir werten mit CPANfile::Parse::PPI
die Datei cpanfile aus und prüfen dann die Module mittels CPAN::Audit
:
#!/usr/bin/perl
use v5.24;
use strict;
use warnings;
use File::Basename;
use File::Spec;
use CPAN::Audit::DB;
use CPAN::Audit::Query;
use CPANfile::Parse::PPI;
my $basedir = File::Spec->catdir( dirname(__FILE__), '..','..');
my $file = File::Spec->catfile( $basedir, qw/cpanfile/ );
my $db = CPAN::Audit::DB->db;
my $query = CPAN::Audit::Query->new( db => $db );
my $cpanfile = CPANfile::Parse::PPI->new( $file );
MODULE:
for my $module ( $cpanfile->modules->@* ) {
my $distname = $db->{module2dist}->{$module->{name}};
next MODULE if !$distname;
my @advisories = $query->advisories_for( $distname, $module->{version} );
next MODULE if !@advisories;
print sprintf "There are advisories for %s %s\n", $module->{name}, $module->{version};
}
Angenommen wir haben folgendes cpanfile:
requires 'Mojolicious' => 8.42;
requires 'Archive::Zip' => 0;
Dann kommt bei einem Commit diese Meldung:
So kann schon während der Entwicklung sichergestellt werden, dass man auf Module aufmerksam gemacht wird, für die Sicherheitslücken bekannt sind.
Dieser Git-Hook ist ganz praktisch, hat aber – wie alle clientseitigen Hooks – das Problem, dass sie bei einem git clone nicht mitgeklont werden. Die Hooks müssen also auf anderem Wege verteilt werden. Und es ist nicht sichergestellt, dass alle Hooks in den Arbeitskopien auch tatsächlich angewendet werden.
Sollen also gewisse Richtlinien erzwungen werden, ist es notwendig die Hooks auf dem Server einzurichten – mit einem passenden Event.
Soll der gleiche Hook wie auf dem Server nach einem push ausgeführt werden, muss das Skript direkt auf dem Server abgelegt werden. Testen können wir das, indem ein neues Git-Repository erstellt wird:
mkdir TestRepository
cd TestRepository
git init --bare
Da wir das init mit dem Parameter --bare
aufgerufen haben, werden in dem Repository keine Dateien mit Code zu finden sein. Die Verzeichnisstruktur ist auch eine andere. Hier haben wir direkt die Struktur, wie sie in der Arbeitskopie im Verzeichnis .git/ zu finden ist.
Der Hook von oben muss in das Verzeichnis hooks/ kopiert werden, aber mit einem neuen Namen: post-receive. Das ist das Event, das nach einem push ausgelöst wird. Es verhindert also nicht, dass die Entwickler*innen Code auf den Server schieben, aber man kann beliebige Meldungen ausgeben. Soll ein push erfolglos sein, wenn ein Modul mit Sicherheitslücken im cpanfile gelistet ist, müssen wir den Hook von oben als pre-receive speichern.
Eine weitere Änderung ist notwendig, da – wie oben geschrieben – keine Dateien mit Code im Repository zu finden sind. Inhalte der Dateien lassen sich mit git show
auslesen, daher sieht hier der entsprechende Teil des Hooks folgendermaßen aus:
my @args = <>;
chomp @args;
my $branch = (split /\s+/, $args[0])[-1];
my $required = qx{git show $branch:cpanfile};
my $db = CPAN::Audit::DB->db;
my $query = CPAN::Audit::Query->new( db => $db );
my $cpanfile = CPANfile::Parse::PPI->new( \$required );
Dieser Hook bekommt die Parameter alter Commithash
, neuer Commithash
und Branch über STDIN. Hier wird also immer das cpanfile des Branchs ausgelesen, der zum Server geschickt wird.
Wird jetzt ein push der Änderungen gemacht, meldet sich der Server mit
Das was bisher gezeigt wurde, gilt für git pur. Wir verwenden zur Verwaltung unserer Git-Repositories Gitlab. Neben den klassischen Git-Features nutzen wir hier vor allem die Continuous Integration Features.
Mit Gitlab sieht der Einsatz der serverseitigen Hooks etwas anders aus: Zuerst brauchen wir den Pfad zum (bereits existierenden) Repository. Dazu öffnen wir den Adminbereich und schauen uns das gewünschte Projekt/Repository an. Dort finden wir einen Eintrag Gitaly relative path: **@hashed/2c/69/2c69\[...\]bde.git**
. Wir müssen auf dem Server in das Verzeichnis von Gitaly, z.B. /srv/gitlab/data/git-data/repositories/. Danach in das Verzeichnis wechseln, das im Adminbereich von Gitlab ausgelesen wurde.
Gitlab hat den oben gezeigten hooks-Ordner für etwas anderes benutzt. Aus diesem Grund dürfen die Hooks aber nicht in das Verzeichnis hooks/, sondern es muss ein Verzeichnis custom_hooks/ angelegt werden. Dort wird der Hook gespeichert. Der Rest bleibt wie gezeigt.
Quellen:
Permalink: /2021-01-06-git-hooks-mit-perl
Das in vielerlei Hinsicht ungewöhnliche Jahr 2020 ist vorbei. Wir wünschen allen treuen und neuen Leser*innen unseres Blogs ein frohes neues Jahr. Wir hoffen, dass Sie gut durch das vergangene Jahr gekommen sind und dass 2021 besser wird.
Trotz der vielen Einschränkungen waren wir nicht untätig. Erstmal ein paar Daten:
Wir möchten uns bei all unseren Kunden und Freunden bedanken. Nur mit Ihnen/Euch konnte das Jahr so erfolgreich abgeschlossen werden. Wir haben viele Ideen bekommen, die wir größtenteils umsetzen konnten.
Wir hoffen, dass der Deutsche Perl-/Raku-Workshop 2021 in Leipzig auch offline stattfinden kann. Dann werden auch wir unsere Schulungen wie geplant abhalten können. Auf dem Workshop werden wir wahrscheinlich ein paar Vorträge halten. Sobald die Organisatoren das Programm veröffentlichen werden wir uns auf diesem Kanal melden.
Auch für die geplanten anderen Schulungen planen wir zweigleisig – sowohl als Präsenzveranstaltung als auch in einer Online-Variante.
Wir wünschen Ihnen ein erfolgreiches Jahr 2021 und freuen uns auf ein spannendes Jahr.
Permalink: /2021-01-01-frohes-neues-jahr
Auch in den letzten beiden Monaten dieses Jahres waren wir nicht ganz untätig – teilweise mit Hilfe anderer Perl-Programmierer*innen.
In den folgenden Abschnitten stelle ich unsere neuen bzw. aktualisierten CPAN-Pakete vor:
Ein neues Modul, das ein statisches Parsen von *cpanfile*s ermöglicht. Ich habe schon in einem Blogpost erwähnt, warum wir nicht Module::CPANfile
nutzen. Da ich noch mehre dieser Anwendungsfälle habe, habe ich ein Modul daraus gebaut: CPANfile::Parse::PPI
.
use v5.24;
use CPANfile::Parse::PPI;
my $path = '/path/to/cpanfile';
my $cpanfile = CPANfile::Parse::PPI->new( $path );
# or
# my $cpanfile = CPANfile::Parse::PPI->new( \$content );
for my $module ( $cpanfile->modules->@* ) {
my $stage = "";
$stage = "on $module->{stage}" if $module->{stage};
say sprintf "%s is %s", $module->{name}, $module->{type};
}
In dieser Distribution habe ich zwei neue Regeln hinzugefügt:
Zum einen eine Regel, mit der die Nutzung von *List::Util*s first
gefordert wird, wenn im Code ein grep
genutzt aber nur das erste Element weiterverwendet wird, wie in diesem Beispiel:
my ($first_even) = grep{
$_ % 2 == 0
} @array;
Die zweite Regel fordert die Nutzung des postderef-Features. Das heißt, dass dieser Code
my @array = @{ $arrayref };
umgeschrieben werden soll als
my @array = $arrayref->@*;
Außerdem gab es kleine Änderungen an den Metadaten. Danke an Gabor Szabo für den Pull Request.
Hier haben wir keine Arbeit aufwenden müssen, sondern durften auf die Fähigkeiten in der Perl-Community zurückgreifen. Andrew Fresh hat einen Pull Request eingereicht, der die Tests die dieses Modul für das generierte Plugin erstellt, lauffähig macht. Vielen Dank dafür!
Auch hier haben wir Pull Requests aus der Perl-Community erhalten:
Vor kurzem kam die erste Anforderung, eines unserer Module für OTOBO zu portieren. Da wir die Spezifikationsdateien unserer Erweiterungen generieren lassen, musste das Kommando entsprechend angepasst werden.
In der Metadaten-Datei (z.B. die für DashboardMyTickets) muss nur noch als Produkt OTOBO
angegeben werden, dann wird die .sopm-Datei richtig generiert.
Das ist das zweite Modul, das für die Nutzung mit OTOBO ertüchtigt wurde. Mit der neuesten Version können Addons für OTOBO geparst werden. Das wurde notwendig, um einen OPAR-Klon für OTOBO aufsetzen zu können.
Zusätzlich wurde die Synopsis durch Håkon Hægland verbessert. Danke für den entsprechenden Pull Request.
Permalink: /2020-12-29-cpan-update-november-dezember
Mit Test::Class::Moose können Entwickler die Mittel von Moose nutzen, um ihre Testsuite objektorientiert aufzubauen. In einem vorherigen Artikel habe ich gezeigt, wie diese Tests durch einen Testtreiber ausgeführt werden. In diesem Artikel zeigen ich an einem einfachen Beispiel die grundsätzliche Verwendung von Test::Class::Moose.
In Perl schreiben Entwickler seit Jahrzehnten Tests mit den Werkzeugen, die das Modul Test::More und seiner Verwandten zur Verfügung stellen. Hier ein Beispiel aus der Dokumentation dieses Moduls:
ok( $exp{9} == 81, 'simple exponential' );
ok( Film->can('db_Main'), 'set_db()' );
ok( $p->tests == 4, 'saw tests' );
ok( !grep(!defined $_, @items), 'all items defined' );
Die Testwerkzeuge wie ok
sind einfache Funktionen, die einen erwarteten mit einem tatsächlich berechneten Wert vergleichen. Die mit diesen Werkzeugen erstellten Testsuites bestehen häufig aus prozedural geschriebenen Testskripten, die in einem Verzeichnis namens »t
« liegen. Sie werden dann vom Programm prove
oder einem geskripteten Testtreiber ausgeführt, der das Modul Test:: Harness verwendet.
Die so aufgebauten Testsuiten werden mit zunehmender Größe unübersichtlich. Zudem ist es nicht immer einfach, gezielt einzelne Funktionalitäten zu testen. Dies liegt zum Teil auch daran, dass Test::More
keine Möglichkeit bietet, Tests zu feiner zu gruppieren und dann nur diese auszuführen.
Eine Abhilfe kann hier der objektorientierte Aufbau von Testsuites schaffen. Die Idee ist hierbei, einzelnen Funktionalitäten durch Testklassen auf Fehler überprüfen zu lassen. Hierfür gibt es seit vielen Jahren die Distribution Test::Class.
Um Tests feinkörniger auswählen zu können, als es mit Test::Class
möglich ist, hat Curtis Ovid
Poe einen ähnlichen Ansatz mit Moose umgesetzt: Test::Class::Moose.
Der Ansatz von Test::Class::Moose
scheint recht einfach zu sein. Die eigentlichen Tests werden weiterhin mit Test::More
geschrieben, Test::Class::Moose
bietet nur einen Rahmen, um die Testsuite besser zu strukturieren, indem die Testsuite objektorientiert aufgebaut wird. Der getestete Code muss dafür nicht unbedingt objektorientiert sein.
Damit Test::Class::Moose
genutzt werden kann, muss eine Testklasse von Test::Class::Moose
abgeleitet werden.
Eine beispielhafte Testklasse, die Test::Class::Moose
und die Methode add
der Klasse Calculator
testet, sieht so aus:
package TestFor::Calculator;
use Test::Class::Moose;
with 'Test::Class::Moose::Role::AutoUse';
sub test_add {
my $c = Calculator->new;
subtest 'identity' => sub {
my $n = 42;
my $expected = $n;
my $got = $c->add( $n, 0 );
is $got, $expected;
};
}
Die Datei liegt in t/lib/TestFor/Calculator.pm
.
In der ersten Zeile folgen wir dem Beispiel aus der Dokumentation von Test::Class::Moose
und legen als Namensraum TestFor::Calculator
fest. Die zweite Zeile lädt Test::Class::Moose
und erledigt damit die Hauptarbeit. Es sorgt nicht nur für die Ableitung der Testklasse, sondern bindet unter anderem auch die Pragmas strict
und warnings
sowie das Modul Test::Most
ein. Es nimmt somit den Entwickler:innen viel Schreibarbeit ab und macht den Quelltext kürzer, übersichtlicher und einheitlicher.
Der eigentliche Test befindet sich in der Methode test_add
und arbeitet mit dem Testwerkzeug is
, das automatisch bereitgestellt wird. Alle Methoden einer Klasse, die mit test_
beginnen, werden als Testmethoden aufgefasst und in alphabetischer Reihenfolge ausgeführt.
Die dazugehörige Beispielklasse Calculator
(lib/Calculator.pm) erhält die Methode add
, um zwei Zahlen zu addieren:
package Calculator;
use Moose;
use v5.20;
use feature 'signatures';
no warnings 'experimental::signatures';
sub add ($self, $n, $m) {
return $n+$m;
}
1;
Wir nutzen nun folgenden in meinem anderen Artikel beschriebenen Testrunner (t/run_test_class.t
), um den Test laufen zu lassen:
use Test::Class::Moose::CLI;
Test::Class::Moose::CLI->new_with_options->run;
Der Testlauf sieht dann wie folgt aus:
$ prove -lv t/run_test_class.t
t/run_test_class.t ..
1..1
ok 1 - TestFor::Calculator {
1..1
ok 1 - test_add {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
1..1
}
}
ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.39 cusr 0.03 csys = 0.45 CPU)
Result: PASS
Wir fügen nun in unsere Testklasse einen Test für eine neue Method sub
ein, die zwei Zahlen voneinander abziehen soll:
sub test_sub {
my $c = Calculator->new;
subtest 'identity' => sub {
my $n = 42;
my $expected = $n;
my $got = $c->sub( $n, 0 );
is $got, $expected;
};
subtest 'negate' => sub {
my $m = 42;
my $expected = -$m;
my $got = $c->sub( 0, $m );
is $got, $expected;
};
}
Wir implementieren nun die neue Methode ...
sub sub ($self, $n, $m) {
return $n-$m;
}
... und rufen die Tests für diese Methode auf. Hier nutzen wir eine Eigenschaft vom Testrunner: Anders als bei der alleinigen Verwendung von Test::More
können wir auch nur Teile einer Testklasse ausführen.
Es ist hier ein Auswahl von Testmethoden möglich, indem wir den Namen über die Option --methods
angeben. Hinweis: Damit die Übergabe funktioniert, muss prove
mit einem Doppeldoppelpunkt mitgeteilt werden, dass die folgenden Argument nicht ausgewertet, sondern an das aufgerufene Testprogramm übergeben werden soll:
$ prove -lv t/run_test_class.t :: --methods test_sub
t/run_test_class.t ..
1..1
ok 1 - TestFor::Calculator {
1..1
ok 1 - test_sub {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
# Subtest: negate
ok 2 - negate {
ok 1
1..1
}
1..2
}
}
ok
All tests successful.
Files=1, Tests=1, 1 wallclock secs ( 0.02 usr 0.00 sys + 0.41 cusr 0.04 csys = 0.47 CPU)
Result: PASS
Da unsere schmalen Testmenge keine Fehler in der neuen Methode gefunden hat, rufen wir nun mutig alle Tests auf:
$ prove -lv t/run_test_class.t
t/run_test_class.t ..
1..1
ok 1 - TestFor::Calculator {
1..2
ok 1 - test_add {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
1..1
}
ok 2 - test_sub {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
# Subtest: negate
ok 2 - negate {
ok 1
1..1
}
1..2
}
}
ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.40 cusr 0.04 csys = 0.47 CPU)
Result: PASS
Die Einschränkung der auszuführenden Testmenge ist nicht nur auf Methodennamen beschränkt, sondern umfasst auch Testklassen. Mehr dazu kann in meinem Artikel zum Testrunner erfahren werden oder in der Dokumentation von Test::Class::Moose::CLI.
Test::Class::Moose
führt Tests mit dem vorgestellten Testtreiber stets in der gleichen Reihenfolge aus. Der Treiber lädt zunächst alle Testklassen und führt dann in alphabetischer Reihenfolge der Klassen die Testmethoden ebenfalls in alphabetischer Reihenfolge aus. Damit sind Testläufe reproduzierbar und ihre Ergebnisse vergleichbar.
Um verdeckte Abhängigkeiten von Testmethoden zu finden, können diese auch in zufälliger Reihenfolge ausgeführt werden:
$ prove -lv t/run_test_class.t :: --randomize-methods
t/run_test_class.t ..
1..1
ok 1 - TestFor::Calculator {
1..2
ok 1 - test_sub {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
# Subtest: negate
ok 2 - negate {
ok 1
1..1
}
1..2
}
ok 2 - test_add {
# Subtest: identity
ok 1 - identity {
ok 1
1..1
}
1..1
}
}
ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.39 cusr 0.04 csys = 0.46 CPU)
Result: PASS
Auch zu --randomize-methods
gibt es ein Gegenstück für Testklassen: --randomize-classes
.
Eingangs habe ich erwähnt, dass Test::Class::Moose
die Infrastruktur für den Aufbau einer Testsuite zur Verfügung stellt. Die eigentlichen Tests schreiben Entwickler weiterhin mit Test::More
.
Seit mehreren Jahren kann Test2 als Ersatz für Test::More
produktiv genutzt werden. Um diese Distribution zu nutzen, muss nur wenig geändert werden.
Im Normalfall bindet das Ableiten von Test::Class::Moose
auch gleich das Modul Test::Most
ein, das wiederum Test::More
und verwandte Module einbindet. Um dies zu unterbinden und Test2
zu verwenden, muss folgende Zeile beim Ableiten von Test::Class::Moose
verwendet werden:
use Test::Class::Moose bare => 1;
Die verwendeten Testwerkzeuge müssen dann manuell eingebunden werden. In unserem Beispiel brauchen wir Test2::Tools::Compare
für die Testwerkzeuge zum Vergleichen und Test2::Tools::Subtests
für die Aufteilung in Untertests:
use Test::Class::Moose bare => 1;
use Test2::Tools::Compare;
use Test2::Tools::Subtest qw(subtest_buffered);
Die Anleitung von Test::Class::Moose
zeigt in einem Beispiel, wie man eine eigene Basisklasse für Testklassen definieren kann, um sich diese Schreibarbeit zu ersparen.
Untertests funktionieren in Test2
etwas anders, da zwei Werkzeuge bereitgestellt werden. Warum ist das so?
Test::More
gibt Entwickler:innen subtest
an die Hand. Diese Funktion gibt Text sofort auf der Konsole aus, sobald er von aufgerufenen Code erzeugt wird. Dieses Verhalten bietet Test2
mit dem Werkzeug subtest_streamed
. Bei nebenläufig ausgeführtem Code kann dies zu unübersichtlicher Ausgabe führen. Daher bietet Test2
auch die Funktion subtest_buffered
an, die die Ausgabe puffert und in der korrekten Reihenfolge ausgibt.
Wir müssen also unsere beispielhafte Testklasse etwas umschreiben:
package TestFor::Calculator;
use Test::Class::Moose bare => 1;
use Test2::Tools::Compare;
use Test2::Tools::Subtest qw(subtest_buffered);
with 'Test::Class::Moose::Role::AutoUse';
sub test_add {
my $c = Calculator->new;
subtest_buffered 'identity' => sub {
my $n = 42;
my $expected = $n;
my $got = $c->add( $n, 0 );
is $got, $expected;
};
}
sub test_sub {
my $c = Calculator->new;
subtest_buffered 'identity' => sub {
my $n = 42;
my $expected = $n;
my $got = $c->sub( $n, 0 );
is $got, $expected;
};
subtest_buffered 'negate' => sub {
my $m = 42;
my $expected = -$m;
my $got = $c->sub( 0, $m );
is $got, $expected;
};
}
Wenn wir die Tests wie bisher mit prove
aufrufen, werden wir keine Änderung feststellen. Die Ausgabe ist die gleiche, allerdings laufen die Tests nun unter Test2
.
Um eine deutliche Änderung zu sehen, können wir die Tests mit yath
, dem Gegenstück von prove
unter Test2
, aufrufen:
$ yath -lv t/run_test_class.t
** Defaulting to the 'test' command **
( LAUNCH ) job 1 t/run_test_class.t
[ PLAN ] job 1 Expected assertions: 1
[ PASS ] job 1 +~TestFor::Calculator
[ PLAN ] job 1 | Expected assertions: 2
[ PASS ] job 1 +~test_add
[ PASS ] job 1 | +~identity
[ PASS ] job 1 | | + <UNNAMED ASSERTION>
[ PLAN ] job 1 | | | Expected assertions: 1
job 1 | | ^
[ PLAN ] job 1 | | Expected assertions: 1
job 1 | ^
[ PASS ] job 1 +~test_sub
[ PASS ] job 1 | +~identity
[ PASS ] job 1 | | + <UNNAMED ASSERTION>
[ PLAN ] job 1 | | | Expected assertions: 1
job 1 | | ^
[ PASS ] job 1 | +~negate
[ PASS ] job 1 | | + <UNNAMED ASSERTION>
[ PLAN ] job 1 | | | Expected assertions: 1
job 1 | | ^
[ PLAN ] job 1 | | Expected assertions: 2
job 1 | ^
job 1 ^
( PASSED ) job 1 t/run_test_class.t
( TIME ) job 1 Startup: 0.41443s | Events: 0.01167s | Cleanup: 0.01234s | Total: 0.43844s
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 1
Assertion Count: 9
Wall Time: 0.65 seconds
CPU Time: 0.83 seconds (usr: 0.12s | sys: 0.02s | cusr: 0.60s | csys: 0.09s)
CPU Usage: 127%
--> Result: PASSED <--
Durch wenige Anpassungen in den Tests kann so die moderne Testinfrastruktur von Perl genutzt werden.
Mit der Distribution Test::Class::Moose
können Entwickler:innen ihre Testsuite übersichtlich strukturieren. Entwickler:innen können diesen Rahmen nutzen, um mit den Testwerkzeugen aus Test::More
und dem neueren Test2
ihre Tests feinkörnig auszuführen.
In diesem Artikel konnte ich nur einen kurzen Überblick geben. Mehr zur Motivation und den Möglichkeiten von Test::Class::Moose
hat Dave Rolsky 2016 in einem Vortrag gezeigt.
Permalink: /2020-12-15-einfuehrung-test-class-moose
In den vergangenen beiden Artikeln über unser Nuclino-Backup habe ich erst vorgestellt, wie wir die Backups erstellen und anschließend wie wir das Programm schneller gemacht haben.
Jetzt wo wir das Backup haben, kommen natürlich noch weitere Ideen. Wie können wir die Daten nutzen, so dass wir Nuclino als eine große Datenquelle nutzen und dann alle möglichen Produkte
daraus ziehen: Dokumentation für Kunden, Schulungsunterlagen als PDF, Konzepte für Projekte, Blogposts etc.
Die Texte in Nuclino sind alle mit Markdown verfasst. Die Blogposts sind seit jeher als Markdown gespeichert und PDFs kann man bequem mit pandoc aus Markdown-Dateien generieren.
Was wir als erstes brauchen ist ein Mapping, damit wir bei den Brains – Sammlungen von Dokumenten in einem Workspace – anstelle der UUIDs mit konkreten Namen arbeiten können.
Außerdem benötigen wir ein Mapping der UUIDs zu Dateinamen und Titeln. Mit Hilfe dieses Mappings Übersetzen wir die Links mit der Nuclino-Domain in Links mit einer beliebigen neuen Domain setzen die Titel der Dateien als Überschrift in die Dokumente ein.
Wir wollen also so etwas:
{
"brains" : {
"0184e034-3156-11eb-bf6d-2bbce7395d9b":"Kunden",
"0f0b2e2a-3156-11eb-a397-97d9e025800c":"FeatureAddons",
"14c45b66-3156-11eb-bf04-835bcdf7bdc7":"PerlAcademy",
...
},
"documents" : {
"19c8e5e6-3156-11eb-ba6d-8beba50b7276": {
"file":"index.html",
"title":"index.html"
},
"27fc869a-3156-11eb-98f5-8f9deed3e298": {
"file":"GraphQL vs. REST 0051eb4b.md",
"title":"GraphQL vs. REST"
},
...
}
}
Wer sich mit Nuclino auseinandersetzt, wird schnell feststellen, dass da nur ganz wenige normale
HTTP-Anfragen abgesetzt werden. Der Informationsaustausch zwischen Browser und Server erfolgt über Websockets.
Also ist wieder eine Analyse der Kommunikation notwendig. Da helfen wieder die Entwicklertools des Browsers mit der Netzwerkanalyse. Auch wenn man in Nuclino arbeitet, kommen keine weiteren Requests in der Übersicht hinzu.
Dann kommt man schnell zu dem einen Websocket...
Diese Kommunikation muss mit Mojolicious nachgebaut werden. Das schöne an Mojolicious ist, dass es schon alles mitbringt – so auch die Unterstützung für Websockets.
my $ua = Mojo::UserAgent->new(
cookie_jar => Mojo::UserAgent::CookieJar->new,
max_connections => 200,
inactivity_timeout => 20,
request_timeout => 20,
);
my ($info, @brains) = _get_brains( $ua );
my @promises = _create_mapping( $ua, $info, \@brains, $home_dir );
Mit der Funktion _get_brains
werden die Zip-Dateien der Brains geholt und in $info
stehen der Benutzername und weitere Infos. Auch bei _create_mapping
arbeiten wir wieder mit Promises. Ein Promise
repräsentiert das Ergebnis einer asynchronen Operation. Je nachdem, ob die Operation erfolgreich oder fehlerhaft beendet wurde, wird das Promise entsprechend gekennzeichnet.
In _create_mapping
bauen wir die Websocket-Verbindung mit Nuclino auf.
sub _create_mapping {
my ( $ua, $info, $brains, $home_dir ) = @_;
my $mapping = {};
my $promise = Mojo::Promise->new;
$ua->websocket(
'wss://api.nuclino.com/syncing' => {
Via => '1.1 vegur',
Origin => 'https://app.nuclino.com',
} => ['v1.proto'] => sub {
# ... hier der Code fuer den Informationsaustausch
});
}
Ist die Verbindung aufgebaut, müssen wir festlegen, was mit eingehenden Nachrichten passieren soll. Und wenn die Verbindung geschlossen wird, müssen die zusammengetragenen Informationen als JSON-Datei gespeichert wird.
Betrachten wir also nur die Subroutine, die bei websocket
als Callback übergeben wird:
my ($ua, $tx) = @_;
return if !$tx->isa('Mojo::Transaction::WebSocket');
$tx->on( message => sub {
my ($tx, $msg) = @_;
_handle_message( $tx, $msg, $mapping );
});
$tx->on( finish => sub {
my ($tx) = @_;
# generate JSON file for mapping
$home_dir->child('backups', 'mapping.json')->spurt(
encode_json $mapping
);
$promise->resolve(1);
});
Trifft eine Nachricht vom Server ein, wird ein message-Event gefeuert. Wir reagieren mit $tx->on( message => sub {...} );
darauf. Was in der Funktion _handle_message
passiert, schauen wir uns später an.
Mit $tx->on( finish => sub {...} );
reagieren wir auf das Ende der Verbindung. Da wird einfach nur die JSON-Datei geschrieben und mit $promise->resolve(1)
wird gesagt, dass das Promise erfolgreich abgearbeitet wurde.
Im weiteren Verlauf des Callbacks setzen wir die Eingangs-Requests ab, die weiter oben beschrieben sind:
$tx->send( encode_json +{
ns => 'sd',
data => {
a => 's',
c => 'ot_config',
d => 'a16b46be-31a7-11eb-ad70-e338b15b7ae1',
}
});
# hier noch die Requests fuer die Kommandos
# ot_user, ot_user_private, ot_team
Mojo::IOLoop->timer( 3 => sub {
for my $brain ( @{$brains} ) {
$tx->send( encode_json +{
ns => 'sd',
data => {
a => 's',
c => 'ot_brain',
d => $brain,
}
});
}
});
Die UUID im ersten Request ist immer die gleiche. Das ist vermutlich die UUID unseres Unternehmensaccounts.
Danach folgen noch drei weitere Requests. Diese können direkt nacheinander abgesetzt werden.
Abschließend folgt noch für jeden brain ein Request. Hier hat es in den Tests nie geklappt, die direkt nach den zuvor genannten Requests abzusetzen. Die genannten vier Requests müssen erst abgearbeitet sein, bevor es weiter geht.
Mit den hier gezeigten Requests werden die Infos zu einem brain geholt. Die Daten kommen als JSON an und sehen folgendermaßen aus:
{
"ns":"sd",
"data":{
"data":{
"v":2,
"data":{
"kind":"PUBLIC",
"name":"Werkzeuge",
"teamId":"1f2966e2-321d-11eb-8f00-5b043dffbd53",
"members":[],
"createdAt":"2019-07-17T15:40:28.897Z",
"creatorId":"2a9e9ad8-321d-11eb-a81f-631faf46d6c4",
"mainCellId":"320c3df2-321d-11eb-9604-fbfbed008d34",
"defaultView":"TREE",
"trashCellId":"377bf386-321d-11eb-b969-3fea1f109107",
"archiveCellId":"3de4e624-321d-11eb-a864-67366681e78f",
"formerMembers":[],
"pinnedCellIds":[],
"defaultMemberRole":"MEMBER"
},
"type":"https://nuclino.com/ot-types/json01"
},
"a":"s",
"c":"ot_brain",
"d":"7091aff1-1513-4e68-ae9f-d6f8936fcf14"
}
}
Die Funktion _handle_message
nimmt die Antworten des Servers auseinander und sammelt die Daten für das Mapping. Die Antworten des Servers für ot_brain enthalten Angaben über die Zellen – also die Seiten des Brains. Für jede dieser Seiten werden wieder Requests abgesetzt.
sub _handle_message {
my ($tx, $msg, $mapping) = @_;
my $data;
eval {
$data = decode_json( encode_utf8 $msg );
};
return if !$data;
my $command = $data->{data}->{c} || '';
return if !( first { $command eq $_ }qw(ot_cell ot_brain) );
# ziehe die Informationen aus den JSON-Antworten
for my $cell ( @cell_ids ) {
$tx->send( encode_json +{
ns => 'sd',
data => {
a => 's',
c => 'ot_cell',
d => $cell,
}
});
}
}
Die Ergebnisse der weiter oben gezeigten Kommandos wie ot_user,* ot_user_private*, *ot_config und ot_team* interessieren uns nicht, da dort keine Informationen über die Dateien zu finden sind. Aus diesem Grund werden die hier nicht behandelt.
In den Antworten für das ot_brain stehen dann Informationen, mit denen wir weiterarbeiten können. In mainCellId steht die UUID des Hauptdokuments. Für dieses Dokument holen wir mit ot_cell die näheren Informationen*.*
{
"ns":"sd",
"data":{
"data":{
"v":18,
"data":{
"kind":"MAIN",
"title":"Main",
"brainId":"a29de3ff-18a5-4d51-ab9b-a6c3b5d82b6c",
"sharing":{},
"childIds":[
"bb10e948-ba91-4097-83a1-60c9ea6ec17b",
"4d44737d-e113-4786-bb98-06d8755d2bb7",
"..."
],
"createdAt":"2019-07-17T14:47:35.743Z",
"creatorId":"34e53829-cafb-4859-9e7b-0aae34295d04",
"memberIds":[],
"updatedAt":"2019-07-17T14:47:35.743Z",
"activities":[],
"contentMeta":{},
},
"type":"https://nuclino.com/ot-types/json01"
},
"a":"s",
"c":"ot_cell",
"d":"d56942a8-aa6e-4197-93fd-1f88967dedc6"
}
}
Die wichtigen Informationen sind der Typ des Dokuments – MAIN ist das Startdokument eines brains, PARENT ein Knoten und LEAF ein Dokument mit Text –, der Titel und die Kinddokumente, für die dann ebenfalls die Informationen geholt werden.
Was haben wir gelernt? Wir haben die Websocket-Kommunikation zwischen Nuclino und dem Browser analysiert, und wir haben gesehen, dass Mojolicious bei der Umsetzung extrem hilfreich ist, weil die Unterstützung für Websockets direkt mit eingebaut ist.
Permalink: /2020-12-11-nuclino-backup-III
Nachdem wir uns das Jahr 2020 Zeit genommen haben, um die Perl-Academy etwas umzubauen (mit neuem Design, der Einführung dieses Blogs, mit Gregor als zusätzlicher Trainer, ...), wollen wir heute unseren Plan für 2021 vorstellen.
Wir werden wenige feste Termine für offene Schulungen haben. Wir wollen mehr Firmenschulungen anbieten und offene Schulungen zusätzlich planen, wenn sich Interessenten melden.
Unsere Schulungsthemen werden wir am Lebenszyklus einer Anwendung ausrichten. Auch in agilen Umgebungen hat man immer wieder den Lebenszyklus in klein. Momentan haben wir vier Themen geplant:
Einsteigen werden wir mit User Story Mapping
. Kein originäres Perl-Thema, aber eine ganz praktische Vorgehensweise, um eine Anwendung aus Sicht des Benutzers zu planen.
Die mit User Story Mapping
geplante Software werden wir mit Mojolicious umsetzen. Dabei werden wir mit einer kleinen Version anfangen um die Grundlagen von Mojolicious kennenzulernen. Anschließend werden wir die Anwendung wachsen lassen, um die weitergehenden Fähigkeiten von Mojolicious nutzen zu können.
Natürlich muss man nicht erst die User-Story-Mapping Schulung besucht haben, um an der Mojolicious-Schulung teilzunehmen.
Das Thema Sicherheit in Perl-Anwendungen
spielt natürlich auch bei Mojolicious-Anwendungen ein Thema. Wir haben eine eigene Schulung daraus gemacht, um dem Ganzen genügend Raum zu geben und uns nicht auf Mojolicious zu beschränken.
Als viertes Thema bieten wir Gitlab und Perl
an. Die Versionsverwaltung begleitet die Anwendung ein Leben lang. Wir zeigen, was man abseits der reinen Versionsverwaltung mit Gitlab machen kann – von Continuous Integration
bis zum Ausliefern der Anwendung.
Interesse an einer der Schulungen? Dann melden Sie sich bei uns!
Permalink: /2020-12-07-schulungen-2021
In verschiedenen Projekten setzen wir ein jeweils selbst gehostetes Gitlab ein. Gitlab bietet viel mehr als nur die Versionskontrolle. Gregor hat vor einiger Zeit schon einmal etwas dazu geschrieben, wie wir bei Perl-Services.de Gitlab benutzen.
Bei einem Projekt stand der Umzug in die Cloud
zu AWS an und damit auch das Gitlab, das in dem Projekt genutzt wurde. Für das Projekt gibt es aber nicht alle Möglichkeiten der AWS-Cloud, so dass das Gitlab nicht in gewohnter Umgebung (ein Ubuntu-Linux) fortgeführt werden konnte, sondern auf ein RHEL8-System wechseln musste.
Bevor der Umzug losgeht, muss eine Serverinstanz mit RHEL8 erstellt werden. Nach der Erstellung per SSH mit der Maschine verbinden.
Da RHEL8 kein Docker mehr unterstützt, kommt Podman zum Einsatz. Podman ist eine Container-Engine ähnlich zu Docker. Es können die Befehle von Docker genutzt werden, da Podman einen docker-Alias anlegt. Man bekommt Podman auch, wenn man
yum install docker
nutzt.
Wenn man von einem anderen Server migriert, muss man dort erst einmal schauen, welche Version zum Einsatz kommt. Die gleiche Version muss dann auch (erst einmal) auf dem neuen Server laufen. Wenn es neuere Versionen von Gitlab gibt, sollte nach dem Umzug dann noch das Update gemacht werden.
Die entsprechend getaggten Gitlab-Images sind unter https://hub.docker.com/r/gitlab/gitlab-ce/tags zu finden.
Auf dem neuen Server führt man einfach
docker pull gitlab/gitlab-ce:<version>-ce.0 aus.
Damit die Daten auch persistent auf der EC2-Instanz bleiben, werden beim Starten des Containers Verzeichnisse der Instanz in den Container gemappt. Die müssen vor dem Start des Containers existieren:
$ mkdir -p /srv/gitlab/config
$ mkdir -p /srv/gitlab/data/backups
$ mkdir -p /srv/gitlab/logs
Als systemd-Service einrichten
Um anschließend Gitlab als Service starten und stoppen zu können, muss es entsprechend eingerichtet sein. Dazu in der Datei /etc/sysconfig/gitlab folgendes eintragen:
IMAGE=docker.io/gitlab/gitlab-ce:<version>-ce.0
HOSTNAME=git.domain.tld
NAME=gitlab-ce
Damit sind dann Umgebungsvariablen gesetzt, die in der Service-Datei genutzt werden. Die ist unter /etc/systemd/system/gitlab.service zu finden. In dieser Service-Datei ist beschrieben, wie der Gitlab-Service aussieht:
[Unit]
Description=GitLab Podman container
After=network.target
[Service]
Type=simple
TimeoutStartSec=5m
Restart=always
RestartSec=30s
EnvironmentFile=/etc/sysconfig/gitlab
ExecStartPre=-/usr/bin/podman rm ${NAME}
ExecStart=/usr/bin/podman run \
--name ${NAME} \
--hostname ${HOSTNAME} \
--publish 443:443 \
--publish 80:80 \
--publish 8014:22 \
--volume /srv/gitlab/config:/etc/gitlab:Z \
--volume /srv/gitlab/logs:/var/log/gitlab:Z \
--volume /srv/gitlab/data:/var/opt/gitlab:Z \
${IMAGE}
ExecReload=-/usr/bin/podman stop ${NAME}
ExecReload=-/usr/bin/podman rm ${NAME}
ExecStop=-/usr/bin/podman stop ${NAME}
[Install]
WantedBy=multi-user.target
Ich gehe hier nicht auf alle Einzelheiten ein. Nur so viel: Wo die Umgebungsvariablen zu finden sind, wird in EnvironmentFile definiert. Dort wir die Datei eingetragen, die im Schritt vorher definiert wurde. Bei den Befehlen zum Starten, Neustarten und Stoppen des Dienstes taucht hier podman auf. Wer Docker einsetzt, muss bei diesen Befehlen dann das podman durch docker ersetzen.
Die Port-Weiterleitungen können auch abweichen. In dem Projekt wurde der Port 22 von Gitlab über den Port 8014 des Servers bekanntgemacht, damit der Server selbst per SSH über den Port 22 ansprechbar bleibt.
Abschließend muss der Service noch aktiviert werden:
systemctl enable gitlab.service
machen, damit das Gitlab bei jedem Systemstart ebenfalls gestartet wird
Bevor man das Backup einspielen kann, muss es erstellt werden. Dazu auf der alten Instanz folgendes machen:
$ docker exec -it gitlab /bin/bash/
gitlab-rake gitlab:backup:create
Das erstellt eine .tar-Datei in */srv/gitlab/data/backups *. Diese muss an die gleiche Stelle auf dem neuen Server geschoben werden. Der Dateiname ist nach diesem Muster aufgebaut: <Timestamp>*<YYYY>*<mm>*<dd>*<version>.tar
.
In dem Backup sind nicht die Dateien /srv/gitlab/config/gitlab.rb und /srv/gitlab/config/gitlab-secrets.json enthalten. Diese müssen separat auf den neuen Server kopiert werden.
Anschließend müssen auf dem neuen Server ein paar Services von Gitlab gestoppt werden:
$ docker exec -it gitlab /bin/bash
/ gitlab-ctl reconfigure
/ gitlab-ctl start
/ gitlab-ctl stop unicorn
/ gitlab-ctl stop sidekiq
Sind die Services gestoppt, kann das Backup eingespielt werden:
/ gitlab-rake gitlab:backup:restore --trace
Abschließend gitlab neu starten:
/ gitlab-ctl restart
Permalink: /2020-12-04-gitlab-umziehen
Gregor hat vor längerer Zeit das Modul Test::Perl::Critic::Progressive
und ich vor kurzem das Modul PPI
vorgestellt. In diesem Blogpost zeige ich, wie man mit PPI
Perl::Critic-Regeln umsetzen kann, die dann in den Tests verwendet werden.
Nachdem ich eine Anwendung programmiert hatte, hat Gregor den Code angeschaut. Dabei ist er über dieses Stück Code gestolpert und meinte, es sähe schräg
aus:
my ($doc) = grep{
$_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };
Für mich ist das völlig normal, ich nutze das häufiger. Aber ich gebe zu, dass man das schöner schreiben könnte.
Da man in diesem Beispiel nur an dem ersten Treffer
interessiert ist, könnte man statt des grep
die Funktion first
aus dem Modul List::Util
nutzen. Damit würde es dann schon so aussehen:
my $doc = first {
$_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };
Natürlich muss man am Anfang noch das Modul laden. Der Unterschied ist nur klein, aber für Einsteiger vielleicht doch einfacher, weil man hier nicht auf den Kontext achten muss. Beim grep
gibt es unterschiedliche Ergebnisse, je nachdem ob man das Ergebnis in Skalar- oder im Listenkontext nutzt – einfach mal dieses Skript bei perlbanjo.com ausführen...
Das nächste was man schöner schreiben könnte, ist die Dereferenzierung der Arrayreferenz:
@{ $app_config->{docs} || [] }
Seit der Version 5.20 gibt es das (bis 5.24 experimentelle) Feature postderef . Da sieht die Schreibweise dann etwas schöner aus:
$app_config->{docs}->@*
Auch hier muss man am Anfang des Codes noch etwas einfügen, aber der Lesefluss ist in meinen Augen deutlich besser. Zum Schluss noch ein Leerzeichen zwischen das schließende
des }
grep
s und der Dereferenzierung. Insgesamt wäre es dann
my $doc = first {
$_->{virtual_path} eq $virt_path
} $app_config->{docs}->@*;
Nachdem wir geklärt haben, welchen Code wir beanstanden wollen, und wie er schlussendlich aussehen soll, können wir uns an die Umsetzung der Regeln machen.
In meinen Augen liefert PPI::Dumper
einen sehr guten Einstiegspunkt. Einfach den unerwünschten
Code in allen möglichen Fassungen in den __DATA__
-Bereich des Skriptes packen und ausführen. Damit sieht man, welche PPI-Knoten es gibt und an welchen man ansetzen muss. Der Dump des Perl Document Object Model (POM) in unserem Beispiel sieht auszugsweise folgendermaßen aus:
PPI::Document
PPI::Token::Whitespace '\n'
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Token::Symbol '@list'
PPI::Token::Whitespace ' '
PPI::Token::Operator '='
PPI::Token::Whitespace ' '
PPI::Token::Word 'grep'
PPI::Token::Whitespace ' '
PPI::Structure::Block { ... }
Bei jeder Perl::Critic-Regel muss man angeben, auf welchen Knoten die Regel angewandt werden soll. In diesem Fall ist das immer ein Knoten vom Typ PPI::Token::Word.
Gehen wir einen Teil nach dem anderen durch. Zuerst wollen wir first
statt grep
nutzen, wenn auf der linken Seite nur eine einelementige Liste angegeben ist.
Die passenden Beispiele dazu sind:
my @list = grep { ausdruck() } @liste; # grep ok
my $list = grep { ausdruck() } @liste; # grep ok
my ($list) = grep { ausdruck() } @liste; # grep not ok
my ($list, $second) = grep { ausdruck() } @liste; # grep ok
if( grep{ ausdruck() }@liste ) { # grep ok
}
# ggf. noch weitere Beispiele
Wenn wir uns den Dump anschauen, merken wir folgendes:
Vergleiche Varianten der Variablenliste:
PPI::Token::Whitespace '\n'
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Structure::List ( ... )
PPI::Statement::Expression
PPI::Token::Symbol '$list'
# vs
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Structure::List ( ... )
PPI::Statement::Expression
PPI::Token::Symbol '$list'
PPI::Token::Operator ','
PPI::Token::Whitespace ' '
PPI::Token::Symbol '$second'
Es muss also eine Liste mit genau einem PPI::Token::Symbol sein, dessen Sigil ein $
ist.
Schauen wir uns das Grundgerüst für eine Perl::Critic-Regel an:
package Perl::Critic::Policy::PerlAcademy::ProhibitGrepToGetFirstFoundElement;
# ABSTRACT: Use List::Utils 'first' instead of grep if you want to get the first found element
use 5.006001;
use strict;
use warnings;
use Readonly;
use Perl::Critic::Utils qw{ :severities };
use base 'Perl::Critic::Policy';
our $VERSION = '2.02';
#-----------------------------------------------------------------------------
Readonly::Scalar my $DESC => q{Use List::Utils 'first' instead of grep if you want to get the first found element};
Readonly::Scalar my $EXPL => [ ];
#-----------------------------------------------------------------------------
sub default_severity { return $SEVERITY_MEDIUM }
sub default_themes { return qw<perl_academy> }
sub applies_to {
return qw<
PPI::Token::Word
>;
}
Das kann so 1:1 kopiert werden. Der Paketnamen muss angepasst werden sowie die Beschreibungen.
Bei der default_severity
muss man sich überlegen, wie wichtig einem diese Regel ist. Regeln können in sogenannte Themes eingeteilt werden.
Mit applies_to
legt man eine Liste von Klassennamen fest, auf deren Objekte die Regel angewandt wird. Wir haben oben festgestellt, dass wir auf PPI::Token::Word-Objekte reagieren müssen.
Ob die Regel eingehalten wird, wird dann in der Methode violates geprüft. Ist alles ok, wird undef zurückgegeben, ansonsten ein Objekt, das die Regelmissachtung beschreibt:
sub violates {
my ( $self, $elem, $doc ) = @_;
# ... code zur pruefung der regel ...
return $self->violation( $DESC, $EXPL, $elem );
}
Neben dem Objekt der Regel selbst bekommt man das Element (hier ein Objekt von PPI::Token::Word) und das PPI::Document-Objekt übergeben.
Als erstes prüfen wir, ob es ein grep-Befehl ist:
# other statements than grep aren't catched
return if $elem->content ne 'grep';
Dann interessieren wir uns nur für das grep, wenn das Ergebnis nicht in einer Variablen gespeichert wird. Dazu müssen wir in der Hierarchie des POM eine Stufe nach oben gehen:
my $parent = $elem->parent;
# grep in boolean or void context isn't checked
return if !$parent->isa('PPI::Statement::Variable');
Und bei den Variablen muss eine Liste gegeben sein
my $list = first{ $_->isa('PPI::Structure::List') } $parent->schildren;
return if !$list;
Die Liste darf nur ein Element haben und das muss ein Scalar sein.
my $symbols = $list->find('PPI::Token::Symbol');
return if !$symbols;
return if 1 != @{ $symbols };
return if '$' ne substr $symbols->[0], 0, 1;
Sollte das alles zutreffen, ist das ein Regelverstoß:
return $self->violation( $DESC, $EXPL, $elem );
Diese gesamte Regel ist mittlerweile auch auf CPAN bei meinen Regeln – und zwar als Perl::Critic::Policy::Reneeb::ProhibitGrepToGetFirstFoundElement – zu finden.
Die beiden anderen Punkte zum schöneren
Code umzusetzen bleibt dem geneigten Leser überlassen.
Permalink: /2020-12-01-perl-critic-regeln-erstellen
In meinem Artikel über die Optimierung von Docker-Images habe ich erwähnt, dass wir die »Gitlab-CI« einsetzen. In diesem Artikel beschreibe ich nun näher, was das eigentlich ist und welche Erfahrungen wir gemacht haben.
Mit »CI« wird in der Software-Entwicklung »continuous integration« bezeichnet, also die fortlaufende Integration von Software-Komponenten. Dies ist ein Vorgehen im Team und weniger ein Werkzeug, bei dem Änderungen an der gemeinsamen Code-Basis fortlaufend an einer definierten Stelle zusammengeführt und auf ihre Qualität hin untersucht werden.
Diese Untersuchung besteht in der Regel aus einer Reihe von automatisierten Tests, die Abweichungen von gewünschten Eigenschaften aufzeigen sollen. Hierzu kann auch das Einhalten von Codier-Richtlinien zählen.
Durch die häufige Integration und einem hohen Grad an Automatisierung möchte das Team sicherstellen, dass Fehler und Abweichungen schneller gefunden und behoben werden können.
Die fortlaufende Integration ist also ein Baustein, um qualitätssichernde Maßnahmen im Softwareentwicklungsprozess umzusetzen.
Das Produkt Gitlab unterstützt fortlaufende Integration durch eine textuelle Beschreibung von »Pipelines«, in denen konfigurierbare Befehle ausgeführt werden.
Nach einem Push von Änderungen in einem Repository wird ein Docker-Container gestartet, in dem dann die beschriebenen Befehle ausgeführt werden.
Das folgende Beispiel zeigt eine hypothetische Distribution Foo::Bar::Baz, die mit Dist::Zilla in Ubuntu gebaut und getestet wird.
image: ubuntu
stages:
- build
build:
stage: build
script:
- dzil build
- dzil cover
- dzil test
coverage: '/^Total.+\s+(\d+.\d)$/'
artifacts:
when: always
name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
paths:
- foo-bar-baz-*.tar.gz
Diese Befehle werden typischerweise das Compilieren und Linken von Software sein, an die sich Testautomatisierung und weitere Prüfungen anschließen.
Wenn alle Befehle erfolgreich ausgeführt wurden, ist der Build erfolgreich. Erfolg oder Misserfolg eines Builds werden dem Entwickler, dessen Änderungen die Pipeline ausgelöst haben, per E-Mail mitgeteilt. Der Zustand eines Builds ist zusätzlich im Gitlab-Projekt sichtbar.
Welche Arten von Pipelines es gibt und wie dies in Gitlabs Oberfläche dargestellt wird, ist in Gitlabs Dokumentation nachzulesen.
Eine Pipeline wird wie oben beschrieben nach einem Push der Änderungen gestartet. Bei der Konfiguration dieser Pipelines legen wir auf zwei Aspekte großen Wert: Geschwindigkeit und Reproduzierbarkeit des Builds.
Geschwindigkeit ist für uns wichtig, da wir binnen weniger Minuten Rückmeldung über die Qualität der Änderungen erhalten wollen. Wenn hier etwas bricht, wollen wir den Fehler sofort korrigieren, da diese Behebungen meist einfacher sind als bei großen Änderungen.
Geschwindigkeit erreichen wir durch konfektionierte Docker-Images. Außerdem stimmen wir die Reihenfolge der Tests aufeinander ab und verfolgen beim Schreiben der Tests einen »fail fast«-Ansatz. Wenn ein Build fehlschlägt, bekommen wir so früh Rückmeldung.
Die Reproduzierbarkeit eines Builds ist für uns wichtig, damit wir Builds nachvollziehen können, indem wir die Docker-Images auf beliebigen Systemen bauen. In der Vergangenheit hatte uns Gitlab öfter Fehler in Builds gemeldet, die wir dann lokal auf unseren Entwicklungsrechnern nicht nachvollziehen konnten. Grund hierfür war die Installation von Abhängigkeiten und dadurch entstandene Versionsunterschiede.
Die Reproduzierbarkeit erhalten wir, indem wir so gut es geht die Versionen der Abhängigkeiten angeben und während des Builds keine Komponenten nachinstallieren. Da Installationen entfallen, erhöht sich als Nebenwirkung auch die Build-Geschwindigkeit.
Für uns hat sich Dist::Zilla als sehr hilfreich erwiesen. Einerseits ermöglichen uns die Kommandos dieses Werkzeugs, die Konfiguration einer Pipeline übersichtlich zu halten. Andererseits bietet die Funktionalität von dzil
vieles, was wir sinnvoll einsetzen können.
Kommandos, die wir nutzen, sind zum Beispiel »test« für das Ausführen der Tests, »build« zum Erstellen der Distribution und »cover« zum Messen der Testabdeckung.
Wir setzen Dist::Zilla ein, um am Ende eines Builds die Distribution als Artefakt »an den Build zu hängen«. Das von dzil erstellte Archiv wird einem Build zugeordnet; wir können es dann im Browser, in anderen Projekten oder über die API übertragen.
Auch bei der Versionsverwaltung ist Dist::Zilla nützlich: Sein git-Bundle hilft dabei, Branches nach dem Veröffentlichen einer Distribution zu taggen. Somit ist automatisiert und nachvollziehbar im Git-Repository eine Versionshistorie abrufbar.
Das Produkt Gitlab hilft einem Team beim fortlaufenden Integrieren von Software-Komponenten. Entwickler können »Pipelines« über Textdateien beschreiben, in denen Befehle nach Änderungen einer gemeinsamen Code-Basis ausgeführt werden. Wir können damit schnelle Rückmeldungen über Code-Änderungen und reproduzierbare Builds erhalten. Dist::Zilla hilft uns, einen Build übersichtlich zu beschreiben und effizient durchzuführen.
Permalink: /2020-11-26-wie-setzen-wir-gitlab-ci-ein