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
Test::Class::Moose hilft beim Organisieren von Tests dadurch, dass objektorientiertes Schreiben von Tests ermöglicht wird.
Mit der Klasse Test::Class::Moose::Runner können diese Tests parametrisiert ausgeführt werden.
Die Klasse Test::Class::Moose::CLI unterstützt beim Schreiben eines Testtreibers, um Tests komfortabel auf der Kommandozeile auszuführen.
Test::Class::Moose bietet objektorientierte Hilfsmittel, um eine Testsuite zu organisieren.
Tests können über Klassen und Methoden verteilt und mit Tags versehen ein, um sie etwa als langsam oder für bestimmte Funktionalitäten auszuzeichnen. Ein Beispiel:
package TestFor::My::Test::Module;
use Test::Class::Moose;
use My::Module;
sub test_construction {
my $test = shift;
my $obj = My::Module->new;
isa_ok $obj, 'My::Module';
}
sub test_database : Tags( database ) { ... }
sub test_network : Tests(7) Tags( online api ) { ... }
Der Aufruf dieser Tests erfordert das Laden der Testklassen und den Aufruf der Methoden, die die eigentlichen Tests enthalten.
In der Praxis wird die vollständige Testsuite auf CI-Systemen ausgeführt. Bei der Fehlersuche führen Entwickler:innen in der Regel jedoch nur jene Tests aus, die für die fehlerhafte Komponente relevant sind.
Um diese Testauswahl effizient zu ermöglichen und bei der Fehlersuche flexibel auswählen zu können, ist ein Kommandozeilenwerkzeug hilfreich: der Testtreiber.
Genau diesen liefert Test::Class::Moose::CLI.
Der Testtreiber selbst ist ein Testprogramm wie jedes andere, das mit prove
aufgerufen werden kann. Er umfasst in der Minimalfassung nur zwei Zeilen:
use Test::Class::Moose::CLI;
Test::Class::Moose::CLI->new_with_options->run;
Wenn diese in der Datei t/tcm.t
abgelegt werden, dann kann er mit prove
aufgerufen werden:
> prove -lv t/tcm.t
Es werden dann alle Testklassen geladen, die sich in oder unterhalb des Verzeichnisses t/lib
befinden. In alphabetischer Reihenfolge der Paketnamen werden dann die Testmethoden ebenfalls ist alphabetischer Reihenfolge ausgeführt.
Eine lebendige Testsuite hat üblicherweise eine Größe, die eine Ausführung aller Tests bei der Fehlersuche unpraktisch erscheinen lässt. Entwickler:innen sind bei der Fehlersuche typischerweise vor allem daran interessiert, eine bestimmte Menge von Tests schnell auszuführen.
Die Menge der ausgeführten Tests kann mit diesem einfachen Testtreiber über einige Parameter mitgeteilt werden. Sollen nur die Tests einer bestimmten Testklasse aufgerufen werden, so kann diese über das Argument --classes
angegeben werden:
> prove -lv t/tcm.t :: --classes Foo
Diese Einschränkung funktioniert auch für Methoden:
> prove -lv t/tcm.t :: --methods Bar
Es gibt auch einen Parameter für die Einschränkung nach den eingangs erwähnten Tags:
> prove -lv t/tcm.t :: --tags Baz
Damit würden alle Methoden in allen Testklassen ausgeführt, die mit dem Tag Baz
versehen sind.
Die erwähnten Optionen können mehrfach angegeben und mit einem vorangestellten exclude-
negiert werden:
> prove -lv t/tcm.t :: --classes Foo --classes Foo2 --exclude-classes NoFoo \
--tags fast --exclude-tags db
Neben der Menge der ausgeführten Tests kann auch die Reihenfolge ihrer Ausführung geändert werden. Die Vorgabe der Ausführung nach alphabetischer Sortierung kann durch eine zufällige Reihenfolge ersetzt werden. Sinnvoll kann dies sein, um eine Stichprobe zu ziehen, ob die Testmethoden isoliert voneinander arbeiten – wenn hier ein Test fehlschlägt, dann wird ein vermeintlicher lokaler Zustand aus einer Testmethode in einer anderen Testmethode verwendet.
Im folgenden Beispiel werden alle Testmethoden aller Testklassen in jeweils in zufälliger Reihenfolge ausgeführt:
> prove -lv t/tcm.t :: –randomize–classes –randomize-methods
Beide Optionen sind auch einzeln verwendbar.
Die hier vorgestellten Optionen sind nur ein Teil der vorhandenen, die der Dokumentation von Test::Class::Moose::CLI beschrieben sind.
Gerade während der Fehlersuche ist ein komfortabler Aufruf von Tests hilfreich, um schnell Rückmeldung von den Tests zu erhalten.
Für die auf Test::Class::Moose basierenden Tests bietet das vorgestellte Modul eine Grundlage für einen eigenen Testtreiber.
Bereits über den vorgestellten minimalen Treiber erhalten Entwickler:innen eine flexible Möglichkeit, die Menge und Reihenfolge der auszuführenden Tests zu beeinflussen.
Auch während der aktuell hohen Infektionszahlen in der Corona-Pandemie schauen wir nach vorne. Nach aktuellem Stand findet der Deutsche Perl-/Raku-Workshop 2021 Ende März in Leipzig statt (sollte die Corona-Situation das nicht hergeben, wird da mit Sicherheit reagiert).
Wir wollen den Workshop zum Anlass nehmen, am Tag vorher (23. März 2021) zwei Halbtagesschulungen anzubieten:
Den Tag beginnen wird Gregor mit Gitlab und Perl
. In dem Kurs, der von 08:00 Uhr bis 12:00 Uhr stattfinden wird, geht es um die Entwicklung von Perl-Distributionen mit Hilfe der Plattform Gitlab. Gregor wird anhand einer CPAN-Distribution kurz die hierfür relevanten integrierten Komponenten der Plattform vorstellen und insbesondere auf die continuous integration eingehen. Ein Schwerpunkt liegt hier auf der schnellen Rückmeldung an Entwickler nach einem Commit.
Am Nachmittag (13:30 Uhr bis 17:30 Uhr) geht es dann um REST-APIs mit Mojolicious. Ich werde hierbei kurz auf REST an sich eingehen, bevor eine Schnittstelle definiert und dann als Mojolicious-Anwendung mit Leben gefüllt wird. Dabei werden dann auch Themen wie Sicherheit etc. angesprochen. Zum Abschluss wird die Schnittstelle noch getestet.
Als Veranstaltungsort ist das Hotel Michaelis geplant und die Teilnehmerzahl ist auf 10 Personen pro Schulung beschränkt.
Wer Interesse an einer der Schulungen hat, kann sich gerne bei uns melden. Die Teilnahme an einer Schulung kostet 200,00 € netto (238,00 € inkl. MwSt), ein Kombiticket für beide Schulungen kostet 350,00 € netto (416,50 € inkl. MwSt).
Wir werden natürlich die Entwicklungen bzgl. Corona weiter im Auge behalten. Sollte eine Präsenzveranstaltung nicht möglich sein, werden wir die Schulungen als Online-Veranstaltung anbieten.
Permalink: /2020-11-10-gpw-schulungen
Only perl can parse Perl
Dieses Zitat von Tom Christiansen gilt in Teilen immer noch. Perl (die Sprache) ist eigentlich alles das, was perl
(der Interpreter) sauber ausführen kann. Dass das nicht nur der Code mit der Syntax ist, die man so hinlänglich sieht, kann man an etlichen Acme::
-Modulen sehen. So kann man wirklich sauberen Code mit Acme::Bleach
schreiben. Das Modul wandelt normalen
Perl-Code in Whitespaces um – und das Programm läuft immer noch. Ein weiteres Beispiel: Zu meinen Ausbildungszeiten in der Bioinformatik habe ich (nicht produktiven) Code mit Acme::DoubleHelix
in eine Doppelhelix-Darstellung umgewandelt.
Um diese Darstellung von Code geht es aber nicht, wenn wir etwas mit PPI
machen. Das Modul dient der statischen Analyse von Perl-Code. Zum Beispiel als Backend
für Perl::Critic
und das von Gregor letztens vorgestellte Test::Perl::Critic::Progressive
. Da PPI
eine statische Analyse durchführt, wird der Code nicht ausgeführt. Deshalb können manche Konstrukte aber nicht tiefergehend betrachtet werden:
require $module;
Welches Modul hier geladen wird, kann PPI
nicht feststellen. Für viele Anwendungsbereiche ist das aber auch irrelevant. Es ermöglicht aber viele Anwendungen, ohne dass man Perl-Code ausführen muss oder selbst mit mehr oder weniger guten regulären Ausdrücken im Code etwas suchen muss.
Aber nicht nur wegen Perl::Critic ist PPI unser Modul des Monats
. Es vereinfacht auch unsere Arbeit bei einigen Anwendungen.
Zum einen bei der Bereitstellung der Perl-API-Dokumentation der ((OTRS)) Community Edition unter https://otrs.perl-services.de aber auch bei der Anwendung zur Messung der Modul-Verbreitung unter https://usage.perl-academy.de .
Bei der OTRS-Dokumentation schauen wir im Code nach, ob es überhaupt POD in der Datei gibt und schmeißt den Rest quasi weg. Damit wird es einfacher, Teile der Dokumentation auseinanderzunehmen, ohne dass der restliche Code in die Quere kommt.
So sieht der entsprechende Code aus:
my @perl_files = $archive->membersMatching('\.(?:pm|pod)$');
FILE:
for my $file ( @perl_files ) {
my $path = $file->fileName;
next FILE if $path =~ m{cpan-lib};
next FILE if $path !~ m{Kernel};
my $code = $file->contents;
my $doc = PPI::Document->new( \$code );
my $pod = $doc->find( 'PPI::Token::Pod' );
next FILE if !$pod;
my $pod_string = join "\n\n", map{ $_->content }@{ $pod };
my $package = $doc->find_first('PPI::Statement::Package');
$pod_string =~ m{
^=head\d+ \s+ DESCRIPTION \s+
(?<description>.*?) \s+
^=(?:head|cut)
}xms;
my $description = $+{description} || '';
}
In $archive
steckt ein Objekt von Archive::Zip
und über das holen wir alle Perl-Module und POD-Dateien, die in der Zip-Datei stecken. Wir holen immer die Zip-Datei, damit wir nicht zig Git-Klone bei uns rumfliegen haben und wir die vielleicht gar nicht mehr brauchen.
Von jeder dieser Dateien holen wir mit ->contents
den Inhalt der Datei. Mit PPI::Document->new( \\$code )
erzeugen wir dann das Objekt von PPI::Document
und durch die Skalarreferenz weiß PPI, dass nicht eine Datei geparst werden soll, sondern der Inhalt der Skalarvariablen. Soll eine Datei geparst werden, nimmt man einfach PPI::Document->new( $path_to_file )
.
Anschließend suchen wir alle POD-Teile in der Datei (find
), und wenn gar kein POD enthalten ist, brauchen wir gar nicht weiterzumachen. Der Methode find
muss man den Paketnamen übergeben, nach dessen Objekten man sucht. Welche Klassen es gibt ist, wird in der PPI-Dokumentation gezeigt.
Da wir auch noch den Paketnamen brauchen, der im Perl-Code zu finden ist, suchen wir das erste Element der Klasse PPI::Statement::Package
.
Somit haben wir dann alles, um das HTML aus POD zu generieren.
Die zweite Anwendung nutzt PPI,
um cpanfiles
zu parsen. Warum wir nicht das Modul Module::CPANfile
nutzen? Wenn man sich das anschaut, lädt es den Perl-Code mit eval
. Und da wir keine Kontrolle darüber haben, was die Besucher in die Dateien schreiben, die hochgeladen werden, nehmen wir lieber einen Parser zur statischen Analyse.
Schauen wir uns also mal an, was PPI
aus einem cpanfile macht. Das cpanfile hat diesen Inhalt:
on 'test' => sub {
requires 'Perl::Critic::RENEEB' => '2.01';
requires "Proc::Background" => 0;
requires 'Test::BDD::Cucumber';
};
on 'runtime' => sub {
requires 'Class::Unload';
};
on 'develop' => sub {
requires 'CryptX' => '0.64';
requires 'DBIx::Class::DeploymentHandler';
requires q~Dist::Zilla~;
requires MySQL::Workbench::DBIC => '1.13';
};
Um uns anzuschauen, wie das Perl Document Object Tree dazu aussieht, nutzen wir das folgende Skript (Perl::Critic
hat ein etwas mächtigeres Skript im Repository, das aber leider nicht installiert wird):
#!/usr/bin/perl
use strict;
use warnings;
use PPI;
use PPI::Dumper;
use Mojo::File qw(curfile);
curfile->sibling('examples')->list->first( sub {
my $code = $_->slurp;
my $doc = PPI::Document->new( \$code );
PPI::Dumper->new( $doc )->print and return;
});
Das erzeugt dann folgende Ausgabe:
PPI::Document
PPI::Statement
PPI::Token::Word 'on'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''test''
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Word 'sub'
PPI::Token::Whitespace ' '
PPI::Structure::Block { ... }
PPI::Token::Whitespace '\n'
PPI::Token::Whitespace ' '
PPI::Statement
PPI::Token::Word 'requires'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''Perl::Critic::RENEEB''
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''2.01''
PPI::Token::Structure ';'
PPI::Token::Whitespace '\n'
PPI::Token::Whitespace ' '
PPI::Statement
PPI::Token::Word 'requires'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Double '"Proc::Background"'
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Number '0'
PPI::Token::Structure ';'
PPI::Token::Whitespace '\n'
[...]
Wir wollen einfach die Modulnamen daraus haben, also der erste String nach dem Literal requires. An dem Dump kann man gut sehen, dass das Literal immer ein Objekt der Klasse PPI::Token::Word
ist. Aber nicht jedes Objekt der Klasse enthält das Literal requires. Wir sehen zum Beispiel auch sub. Das müssen wir berücksichtigen. Nach dem Literal kommen beliebige Whitespaces, die uns aber nicht interessieren. PPI kennt da das Prinzip der significant siblings/children
. Da in dem Baum die Elemente von requires und der Modulname auf einer Ebene sind, interessieren wir uns für das nächste signifikate Geschwister.
So sieht der Code dann in der Anwendung aus:
my $code = $file->slurp;
my $doc = PPI::Document->new( \$code );
my $requires = $doc->find(
sub { $_[1]->isa('PPI::Token::Word') and $_[1]->content eq 'requires' }
);
if ( !$requires ) {
$file->remove;
return;
}
REQUIRED:
for my $required ( @{ $requires || [] } ) {
my $value = $required->snext_sibling;
my $can_string = $value->can('string') ? 1 : 0;
my $prereq = $can_string ?
$value->string :
$value->content;
save_prereq( $prereq );
}
Der Anfang ist ähnlich zu oben. Wir lesen die Datei ein und erzeugen ein Objekt von PPI::Document
. Während wir oben der Methode find
nur die Klasse angegeben haben, deren Objekte wir alle haben wollen, möchten wir hier die Suche etwas einschränken. Nämlich alle PPI::Token::Word
-Objekte, deren Inhalt required
ist. Übergibt man eine anonyme Subroutine an find
, wird für jedes Element unterhalb des zu durchsuchenden Knoten diese Subroutine aufgerufen.
Als Parameter werden zwei Parameter übergeben. Zum einen das Element auf dem die Suche ausgeführt wird und zum anderen das Element, das geprüft wird.
Die Methode content
liefert den Code 1:1.
Für alle gefundenen requires bekommen wir die Objekte der Klasse PPI::Token::Word
. Für jedes dieser Objekte wollen wir das erste signifikante Geschwister. Mit next_sibling
würde man die Whitespaces bekommen, die interessieren uns aber nicht. Aus diesem Grund müssen wir snext_sibling
verwenden.
Schauen wir noch mal kurz auf die verschiedenen Varianten der Modulangaben:
requires 'Perl::Critic::RENEEB' => '2.01';
requires "Proc::Background" => 0;
requires q~Dist::Zilla~;
requires MySQL::Workbench::DBIC => '1.13';
... und die dazugehörigen PPI-Elemente:
PPI::Token::Quote::Single ''Perl::Critic::RENEEB''
PPI::Token::Quote::Double '"Proc::Background"'
PPI::Token::Quote::Literal 'q~Dist::Zilla~'
PPI::Token::Word 'MySQL::Workbench::DBIC'
Da die Whitespace
-Knoten übergangen werden bekommen wir einen der hier gezeigten Knoten. Mit der Methode content
bekommen wir den Wert wie er hier zu sehen ist, also inklusive der Anführungszeichen bzw. des q\~\~
.
Wir müssen uns also auf die unterschiedlichen Methoden stützen, die die Objekte mitliefern. Bei den ganzen Quote::\*
-Klassen kann die Methode string
genutzt werden, bei PPI::Token::Word
die Methode content
.
Noch kurz zu den s...
-Methoden: Als significant werden alle Teile bezeichnet, die für die Ausführung des Codes wichtig sind. Kommentare, POD-Dokumenation, __END__
-Bereiche und Whitespaces gehören nicht dazu.
Die Skripte und Beispiele habe ich auch wieder ins Git-Repository gepackt.
Permalink: /2020-10-21-ppi
Wenn man als Entwickler mit der Erstellung von Docker-Images anfängt, dann ist das Image zunächst auf Funktionalität optimiert. Mit fortschreitender Entwicklung wird eine schnelle Rückmeldung über das Build-Ergebnis an die Entwickler wichtiger. Zudem soll ein Build reproduzierbar sein, damit von der CI-Umgebung gemeldete Fehler nachvollzogen und Änderungen für den Betrieb einplant werden können. Dieser Artikel zeigt, worauf man bei achten sollte, damit das klappt.
Wir nutzen Gitlab und haben uns zu Beginn der Nutzung dieses Dienstes darauf konzentriert, überhaupt einen Build nach einem Commit durchlaufen zu lassen. Das ist manchmal gar nicht so einfach, da diverse Abhängigkeiten benötigt werden, die vor Ausführung von Tests in einem laufenden Container installiert werden müssen.
Wir bei Perl-Services haben uns zu Beginn auf die CI-Stufe von Gitlab verlassen. Da es hier aber mitunter etwas dauern kann, bis ein Build nach einem Commit startet, haben wir schnell gemerkt, dass lokales Bauen von Images sinnvoller ist. Dafür haben wir dann ein Makefile erstellt:
#!/bin/make -f
#
# Makefile pointers...
# Self-documenting:
# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
# Argument passing:
# https://stackoverflow.com/questions/2826029/passing-additional-variables-from-command-line-to-make
#
# To add a new option, follow the format below, e.g.:
# make command: ## Documentation of command
# ./the-command-to-run.sh
# Default to displaying the help
.DEFAULT_GOAL := help
.PHONY: help
REGISTRY=registry.gitlab.com
IMAGE_NAME=perlservices/groupname/projectname-docker
IMAGE_TAG=latest
FULL_IMAGE_NAME=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
# Displays all the make options and their descriptions
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
build: ## Build Docker image
docker build -t ${FULL_IMAGE_NAME} .
shell: ## Start a container from the image built and run a shell in it
docker run --rm -it --entrypoint /bin/bash \
-v $$PWD/src:/src \
-p 8080:8080 \
${FULL_IMAGE_NAME}
Dieses Makefile erstellt beim Aufruf von make build
ein Docker-Image anhand des im gleichen Verzeichnisses liegenden Dockerfile
s. Der Aufruf von make shell
startet einen Container mit diesem Image und öffnet eine Shell darin, wobei hier das lokale Verzeichnis src
im Container unter /src
zur Verfügung steht.
Mit Hilfe dieses Makefiles können wir recht einfach ein Docker-Image erstellen und dieses lokal weiterentwickeln.
Damit eine Perl-Distribution im Rahmen eines Docker-Images erstellt werden kann, werden viele Bausteine benötigt.
Einerseits benötigen wir auf Systemebene einige Werkzeuge. Dazu gehört zum Beispiel curl
, um andere Builds auf Gitlab anzustoßen. Diese installieren wir über den Paketmanager des Betriebssystems.
Anderseits brauchen wir auf Anwendungsebene Module, die wir installieren müssen. Wenn sie in der geeigneten Version als Paket des Betriebssystems vorliegen, installieren wir sie ebenfalls mit dem Paketmanager des Betriebssystems. Wenn dies nicht der Fall ist, installieren wir sie von CPAN.
Diese Installationen dauern entsprechend lange und greifen auf externe Quellen zu. Dadurch dauert der Build lange und kann schlimmstenfalls fehlschlagen.
Daher haben wir hier in zwei Schritten optimiert: Wir erstellen für eine zu entwickelnde Perl-Distribution (oder Anwendung) ein spezifisches Image, das alle benötigten Abhängigkeiten enthält.
Im Dockerfile installieren wir zunächst mit apt-get install
die benötigten Pakete des Betriebssystems:
FROM registry.gitlab.com/perlservices/groupname/projectname-docker:latest
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
lsb-core \
wget
# Add PostgreSQL repo for the matching release of the OS
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
postgresql \
postgresql-client \
git \
apache2 \
curl \
phppgadmin \
php-pgsql \
cpanminus \
libdbd-pg-perl \
libnet-ssleay-perl \
libxml-parser-perl \
gosu \
...
Die Perl-Abhängigkeiten legen wir in einem cpanfile
fest:
...
requires 'Mojolicious' => 8;
requires 'Mojolicious::Plugin::Bcrypt' => 0;
requires 'Mojolicious::Plugin::Status' => 0;
requires 'Moo' => 0;
requires 'MySQL::Workbench::DBIC', '>= 1.13';
requires 'MySQL::Workbench::Parser', '>= 1.06';
...
Das so erstellte Image verwenden wir dann in der .gitlab-ci.yaml
unseres Projektes. So können dann sofort nach einem Commit in der CI-Stufe die konfigurierten Befehle ausgeführt werden, denn alle Abhängigkeiten sind bereits im Image vorhanden.
Gerade zu Beginn der Arbeit mit den oben beschriebenen projektspezifischen Docker-Images haben wir öfter Probleme mit der Nachvollziehbarkeit von Fehlern im Build auf Gitlab gehabt. Ursache dafür war, dass sich Abhängigkeiten in jedem Build geändert haben.
Beispiele für diese Änderungen:
Es wird dann unter Umständen auf Gitlab ein Fehler im Build angezeigt, der lokal beim Entwickler nicht mehr oder anders auftritt.
Durch das Erstellen von projektspezifischen Docker-Images werden schon viele Abhängigkeiten festgezurrt, so dass hier weitestgehend reproduzierbare Builds umgesetzt werden konnten.
Eine wesentliche Abhängigkeit ist jedoch noch nicht festgezurrt: Die des grundlegenden Images für alle Builds – Das verwendete Betriebssystem.
Vielfach wird in Beispielen für ein Dockerfile
entweder kein Tag bei einem verwendeten Image angegeben oder eines mit dem Namen »latest«. Beide führen dazu, dass jeweils die letzte Fassung eines Images verwendet wird:
FROM debian:latest
Im schlimmsten Fall kann es dadurch dazu kommen, dass zwischen zwei Builds ein Versionssprung des Betriebssystems stattfindet und alle abhängigen Images neu gebaut werden. In der Regel läuft dies nicht ohne Probleme ab und zieht weitere Arbeiten nach sich.
Die dann auftretenden Probleme im Build können im günstigsten Fall eine kurze Verzögerung bedeuten, im schlimmsten Fall ungeplante Wartungsarbeiten nach sich ziehen.
Außerdem bedeuten gerade bei Betriebssystemen neue Versionen größere Änderungen im Betrieb, die sorgfältig vorbereitet und eingeplant werden müssen.
Diese Art von Überraschung kann umgangen werden, indem in Dockerfile
s bei jedem verwendeten Image stets ein Tag mit angegeben wird, das eine feste Version spezifiziert.
Es sollte also auf Angaben wie »rolling« und »latest« verzichtet werden.
FROM ubuntu:20.04
Während Entwickler zunächst auf Funktionalität bedacht sind, wenn sie in CI-Umgebungen mit dem Erstellen von Docker-Images beginnen, zeigt sich sehr schnell in der täglichen Arbeit, dass weitere Faktoren wichtig sind. Schnelle Rückmeldung durch kurze Durchlaufzeiten und stabiler Betrieb durch reproduzierbare Builds werden mit zunehmender Zeit wichtiger.
Dieser Artikel hat beschrieben, wie durch projektspezifische Images die Durchlaufzeit verringert werden kann. Außerdem wurde gezeigt, wie durch das ausdrückliche Konfigurieren von Softwareversionen planvolle Updates ohne große Überraschungen im Betrieb möglich werden.
Permalink: /2020-10-14-optimierung-von-docker-images
Wir haben bei uns etliche Mojolicious-Anwendungen laufen. Dieses Blog, Perl-Academy.de sind Anwendungen und bei OPMToolbox.de sind es mehrere Anwendungen.
Die liegen teilweise auf dem gleichen Server. Jetzt stellt sich vielleicht die Frage, wie wir das dann betreiben. Die Antwort ist einfach: In der Regel gibt es für jede Anwendung einen eigenen User auf dem Server. Der startet die Anwendung mit hypnotoad. Und es gibt einen nginx, der als Reverse Proxy dient.
Warum jeweils ein eigener User? Warum alles hinter dem nginx?
Es gibt immer einen eigenen User, weil wir nicht alles mit root oder einem anderen User laufen lassen möchten. Und es hat den Vorteil, dass wir bei jedem User eine eigene Perl-Umgebung aufbauen können. Das heißt, dass wir mit perlbrew mindestens die Perl-Version installieren, mit der die Anwendung entwickelt wurde und auch die Module installieren, die die Anwendung braucht.
Das sieht dann so aus
$ useradd -m -s /bin/bash academy-blog
$ su - academy-blog
$ wget -O - https://install.perlbrew.pl | bash
$ source perl5/perlbrew/etc/bashrc
$ perlbrew install 5.30.2
$ perlbrew use 5.30.2
$ wget -O - https://cpanmin.us | perl - App::cpanminus
$ git clone <academy-blog> blog
$ cd blog
$ cpanm --installdeps .
$ hypnotoad blog
So muss ich auch nicht aufpassen, ob eine neue Mojolicious-Version eine Funktion rausgeschmissen hat oder eine andere Inkompatibilität mit sich bringt. So lange es läuft und an der Anwendung nichts geändert wird, kann die Umgebung so bleiben wie sie ist.
Eine andere Anwendung beeinflusst diese nicht. Egal, ob die andere Anwendung mit einer neuere Mojolicious-Version entwickelt wurde oder es vielleicht auch Module gibt, die sich gegenseitig ausschließen.
Natürlich bringt das unter Umständen mehr Aufwand mit sich, wenn in Modulen Sicherheitslücken auftauchen und dann eben mehrere Installationen angepasst werden müssen. Aber das kommt zum Glück nicht häufig vor.
Damit der User immer gleich das richtige
Perl nutzt, nehmen wir noch eine kleine Änderung in der .bashrc vor: Wir laden dort perlbrew und die richtige Perl-Version:
source /home/academy-blog/perl5/perlbrew/etc/bashrc
perlbrew use 5.30.2
Wie wir sicherstellen, dass die Anwendung auch nach einem Serverneustart automatisch starten, zeige ich in einem späteren Blogpost.
Warum keine Container wie z.B. Docker? Weil wir (noch) nicht dazu gekommen sind, alles in Docker-Container zu packen. Das wird langfristig unser Ziel sein. Sobald wir soweit sind, berichten wir hier auch darüber.
Wenn alles installiert ist und die Anwendung läuft, ist sie von außen noch nicht erreichbar, weil wir die Anwendung immer nur auf 127.0.0.1:#port
lauschen lassen. Es wäre ja auch blöd, wenn die Besucher*innen bei jeder Anwendung auch den Port kennen müssen.
Aus diesem Grund nutzen wir den nginx als Reverse Proxy. Außerdem ist die Nutzung von SSL damit ziemlich einfach.
Als erstes installieren wir den nginx
$ apt install nginx
Nach der Installation von nginx, packen wir für jede Anwendung eine eigene Konfigurationsdatei in den Ordner /etc/nginx/conf.d.\ Hier ein Beispiel:
upstream academy-blog {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name blog.perl-academy.de;
location /.well-known/acme-challenge {
root /home/academy-blog/letsencrypt/challenge/;
}
location / {
return 301 https://blog.perl-academy.de$request_uri;
}
}
server {
listen 443 ssl;
server_name perl-academy.de www.perl-academy.de;
ssl_certificate /home/academy-blog/letsencrypt/live/blog.perl-academy.de/fullchain.pem
ssl_certificate_key /home/academy-blog/letsencrypt/live/blog.perl-academy.de/privkey.pem;
root /home/academy-blog/web/public/;
try_files $uri @academy_app;
location @academy_app {
proxy_read_timeout 300;
proxy_pass http://academy-blog;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS 0;
}
}
Der upstream-Block bestimmt einen anderen Server, an den Anfragen weitergeleitet werden können. Hier nutzen wir einen sprechenden Namen. Es ist zu sehen, dass hypnotoad hier auf Port 8080 lauscht.
Anschließend kommt ein server-Block, der Anfragen auf Port 80 (http) behandelt. Wir legen den Servernamen fest. Es werden also Anfragen auf Port 80 für blog.perl-academy.de behandelt. Innerhalb dieses Blocks gibt es zwei location-Blöcke. Der erste ist wichtig, um später bequem LetsEncrypt-Zertifikate installieren zu können. Der zweit leitet HTTP-Anfragen auf HTTPS um.
Der zweite server-Block sorgt dafür, dass nginx auf Port 443 lauscht und SSL aktiviert ist. So lange die LetsEncrypt-Zertifikate nicht da sind, sollte dieser server-Block deaktiviert/auskommentiert werden.
Wir setzen den root der Dateien auf den public-Ordner der Anwendung. Im Zusammenspiel mit dem try_files
sorgen wir dafür, dass statische Dateien nicht über die Mojolicious-Anwendung ausgeliefert werden, sondern direkt vom nginx. try_files
prüft, ob eine Datei (die dann in $uri
steht) existiert und liefert sie dann aus. Existiert sie nicht, greift der benamte Fallback (hier: @academy_app
). Diesen benamten Fallback kann man sich wie eine Subroutine vorstellen. Der Fallback ist dann darauf definiert mit dem location-Block
Das Entscheidende ist die Zeile mit proxy_pass
. Die bedeutet, das die Anfragen weitergeleitet werden. Hier taucht auch wieder der Name auf, den wir weiter oben bei upstream definiert haben.
Nachdem der nginx läuft, gehen wir den nächsten Schritt an: SSL-Verschlüsselung. Am einfachsten ist es, LetsEncrypt-Zertifikate zu nutzen. Dazu müssen wir certbot
installieren:
$ apt install certbot
Anschließend können wir als User die SSL-Zertifikate generieren lassen:
$ su - academy-blog
$ mkdir -p letsencrypt/challenge
$ certbot certonly --config-dir /home/academy-blog/letsencrypt \
--work-dir /home/academy-blog/letsencrypt \
--logs-dir /home/academy-blog/letsencrypt \
-d blog.perl-academy.de
Das Programm führt dann durch die Konfiguration. Da wir in der nginx-Konfiguration schon angegeben haben, wo an welcher Stelle LetsEncrypt auf Dateien zugreifen kann. An diese Stelle soll certbot
Dateien packen:
How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Spin up a temporary webserver (standalone)
2: Place files in webroot directory (webroot)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Plugins selected: Authenticator webroot, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): xxxx@perl-services.de
Man muss noch den Terms of Service
zustimmen und anschließend noch das Verzeichnis, das über nginx erreichbar ist, angeben und dann sollte das Zertifikat erzeugt werden.
Läuft alles durch, kann der server-Block für das SSL in der nginx-Konfiguration wieder aktiviert werden.
Da die LetsEncrypt-Zertifikate nur jeweils 3 Monate gültig sind, müssen diese regelmäßig erneuert werden. Hier empfiehlt es sich, einen Cronjob einzurichten:
$ crontab -e
Und dort
0 5 15,30 * * certbot renew --config-dir ... --work-dir ... --logs-dir ...
eintragen.
Für jede Anwendung nutzen wir einen eigenen User, damit wir komplett eigenständige Umgebungen haben. Die Anwendungen sind hinter\
einem nginx als Reverse Proxy. Für die SSL-Verschlüsselung nutzen wir certbot
. In einem Bild zusammengefasst, sieht\
das dann so aus:
Sollten Sie noch Fragen oder Anregungen haben, melden Sie sich einfach.
Permalink: /2020-10-05-wie-wir-apps-betreiben
Ja, ich weiß dass der September schon vorbei ist. Aber der Inhalt bezieht sich auf das was im September so passiert ist ;-) In den letzten Wochen haben wir wieder ein paar unserer Module auf CPAN aktualisiert. Dieser Artikel gibt einen kurzen Überblick, was sich so getan hat.
Markdown::Table
ist ein neues Modul, das wir zum Generieren von Tabellen in Markdown nutzen können. Der für uns aktuell wichtigere Teil ist aber das Parsen von Tabellen in Markdown-Dokumenten.
Wir nutzen - wie vielleicht schon bekannt - Nuclino zur Ablage von Informationen. Unter anderem auch für den Redaktionsplan
für dieses Blog. Dort steht in einer Tabelle, wann welcher Artikel geplant ist.
Mit den Daten dieser Tabelle soll ein interner Kalender gepflegt werden, so dass Erinnerungen etc. generiert werden.
Aus der SYNOPSIS:
use Markdown::Table;
my $markdown = q~
This table shows all employees and their role.
| Id | Name | Role |
|---|---|---|
| 1 | John Smith | Testrole |
| 2 | Jane Smith | Admin |
~;
my @tables = Markdown::Table->parse(
$markdown,
);
print $tables[0]->get_table;
Wir machen auch ziemlich viel mit der ((OTRS)) Community Edition: Schulungen, Anpassungen und vieles mehr. Für Erweiterungen gibt es im ((OTRS)) einen Paketmanager, der die Pakete in einem bestimmten XML-Format erwartet (sog. OPM-Dateien). Die Spezifikation
für die Pakete wird selbst auch in XML festgehalten. Mir ist das zu aufwendig und schreibe die Meta-Daten in eine JSON-Datei. Die wird dann mit dem sopm-Kommando des Tools opmbuild ausgewertet und die Spezifikationsdatei (.sopm) wird geschrieben.
Neu ist die Unterstützung vom Löschen von Spalten in Tabellen mittels ColumnDrop.
Vor einigen Tagen habe ich ja Data::Validate::WithYAML
vorgestellt. Das aktualisierte Modul ist das passende Mojolicious
-Plugin dazu. Damit wird die Validierung der Benutzereingaben zum Kinderspiel.
In der neuen Version nutzen wir die Module, die Mojolicious mitliefert anstelle von File::Spec
und File::Basename
. Mojo::File ist ein tolles Modul wenn man mit Dateien arbeitet.
Weiterhin gibt es in dem Plugin jetzt eine Unterstützung für steps
in den YAML-Dateien. Damit kann man die gleichen Dateien nutzen, die man bei reiner Data::Validate::WithYAML
-Nutzung verwendet.
Permalink: /2020-10-02-cpan-news-september-2020
Akzeptanztests sind Tests, die als natürlichsprachliche Szenarien aus Anwendersicht formuliert werden. Sie dienen als lebendige Dokumentation eines Systems und können das gemeinsame Verständnis im Team herstellen. Test::BDD::Cucumber
ist eine Distribution, mit der Fehler in Perl-Anwendungen durch die Ausführung von Akzeptanztests gefunden werden können.
In Perl sind Unit-Tests weit verbreitet. Oftmals sind diese als einzige Testart nach außen sichtbar, weil sie in CPAN-Distributionen ausgeliefert werden. Mit der CPAN Testers Matrix gibt es eine transparente Möglichkeit, die Ergebnisse der Unit-Tests einzusehen.
Integrations- oder Systemtests sind bei weitem nicht so bekannt in der Perl-Welt, weil es hierfür weder ein einheitliches Framework noch eine ähnlich transparente Plattform gibt.
Unit-Tests arbeiten »von innen nach außen« (»inside-out«): Die Units bilden die untersten Bausteine, aus denen Anwendungen erstellt werden. Unit-Tests sind gut geeignet für die testgetriebende Entwicklung von Units und üblicherweise schnell in der Ausführung. Deswegen ist es praktisch möglich und ratsam, eine hohe Testabdeckung durch eine Vielzahl von Testfällen zu erreichen.
In der Softwaretechnik gibt es die Disziplin des »behaviour driven development« (BDD), in der Akzeptanztests ein Bestandteil sind.
Diese Art von Systemtests heißt so, weil mit ihnen die Bedingungen geprüft werden, unter denen ein Anwender die Umsetzung der Software akzeptiert. Sie sind natürlichsprachlich aus Sicht des Anwenders formuliert.
Da Anwender in Funktionalitäten denken, werden Akzeptanztests pro Feature geschrieben. Sie bestehen aus Szenarien, die exemplarisch die Verwendung der Software im Rahmen eines Features beschreiben.
Es wird das vollständig integrierte System »von außen nach innen« (»outside-in«) getestet.
Da das vollintegrierte System getestet wird, sind diese Test langsam. Es werden daher nur einzelne bespielhafte Fälle – die Szenarien – getestet.
Akzeptanztests passen gut zu testgetriebener Entwicklung einer Anwendung. In dieser Vorgehensweise spezifiziert man erst das Verhalten der Software aus Sicht des Anwenders in Szenarien und implementiert anschließend das Feature. Die Szenarien helfen beim Konzentrieren auf das Wesentliche eines Features.
Wie sieht nun ein solcher Akzeptanztest aus? Das folgende Beispiel zeigt ein Szenario eines erfolgreichen Logins.
Feature: Login
Scenario: User logs in providing correct credentials
Given I am an existing user
When I log in providing the correct password
Then the login succeeds
And I am at the dashboard page
Szenarien werden nach dem Schema »Arrange, Act, Assert« (AAA) mit der Given-When-Then-Schablone geschrieben. Das Szenario beschreibt aus der Sicht der Benutzers die wesentliche Funktionalität: Nach einem erfolgreichen Login wird der Benutzer auf sein Dashboard geleitet.
Der »Given«-Schritt entspricht »Arrange«, in dem die Voraussetzungen für den Test geschaffen werden. In diesem Fall wird das getestete System so verändert, dass ein Benutzer existiert.
Der »When«-Schritt entspricht »Act«. Hier wird die zu testende Funktionalität ausgeführt.
Im abschließenden »Then«-Schritt (»Assert«) werden die erwarteten Ergebnisse geprüft.
Die Ausführung solcher Szenarien kann in Perl mit einer Reihe von Distributionen erfolgen. Die ausgereifteste ist Test::BDD::Cucumber. Die Akzeptanztests werden standardmäßig im Verzeichnis t/features
abgelegt. Szenarien eines Features werden in einer Datei beschrieben, die auf .feature
endet.
Die einzelnen Schritte werden als Zeichenketten mit regulären Ausdrücken geprüft. Welche dies sind, legen die Entwickler über Subroutinenaufrufe fest. Diese werden in allen Dateien im Verzeichnis t/features/step_definitions
erwartet.
Für unser oben gezeigtes Szenario könnte die Deklaration der einzelnen Schritte wie folgt aussehen:
Given qr{there is an existing user} => sub {
my $context = shift;
# Benutzer anlegen
...
};
When qr{I log in providing the correct password} => sub {
my $context = shift;
# Anmeldung mit korrektem Passwort durchführen
...
};
Then qr{the login succeeds} => sub {
my $context = shift;
# prüfen, ob Login erfolgreich war
...
};
Then qr{I am at the dashboard page} => sub {
my $context = shift;
# prüfen, ob Dashboard angezeigt wird
...
};
Jeder Schritt wird durch diese regulären Ausdrücke geprüft. Bei einer Übereinstimmung wird der entsprechende Code ausgeführt.
Wie im Beispiel zu sehen ist, wird an die Subroutine der Kontext übergeben, in dem der Code ausgeführt wird. Über diesen Kontext können Informationen zwischen den Schritten ausgetauscht werden, ohne auf globale Zustände zugreifen zu müssen. In unserem Beispiel würde hier zum Beispiel der angelegte Benutzer übergeben werden, so dass Name und Passwort nicht fest im Code verdrahtet werden müssen.
Der jeweilige Then-Schritt prüft den erwarteten Zustand mit den aus Test::More
bekannten Werkzeugen.
Die Akzeptanztests werden mit dem Werkzeug pherkin
in der Shell aufgerufen. Bei der Ausführung eines Test werden alle Schritte ausgegeben. Fehler werden laut und deutlich in der Konsole gemeldet:
$ pherkin -Ilib t/features/login.feature
Feature: Login
Scenario: User logs in providing correct credentials
Given I am an existing user
When I log in providing the correct password
Then the login succeeds
step defined at t/features/login.feature line N.
ok - Starting to execute step: the login succeeds
not ok 1
# Failed test at ...
In den »Then«-Schritten können alle Test::Builder
-basierten Testwerkzeuge verwendet werden, die die Entwickler auch sonst in ihren Unit-Tests einsetzen.
Die obigen Beispiele können nur skizzenhaft das Vorgehen zeigen. Die Test::BDD::Cucumber
-Distribution enthält einige lauffähige Beispiele, die konkret die Funktionalität der Distribution zeigen.
Durch das Verwenden von Akzeptanztests ist das Team gezwungen, sich aus der Sicht der Benutzer der Implementierung zu nähern. Dadurch wird häufig deutlich, dass das Wesentliche der Umsetzung einer Software in der Regel nicht in den grafischen Details liegt, sondern vielmehr in der Funktionalität, die der Benutzer erwartet.
Das Team formuliert die Szenarien in einer allgemeinen Sprache (»ubiquitäre Sprache«) der Anwendungsdomäne. Da dieses bereits bekannte Vokabular verwendet wird, kann das gemeinsame Verständnis über die Funktionsweise der Software erhöht werden. Da die Tests zudem ausführbar sind, erhält das Team so eine ausführbare, lebendige Dokumentation des tatsächliche funktionierenden Systems.
Akzeptanztests testen die lauffähige Software mit all ihren Komponenten und nicht nur einzelne Teile. Sie sind daher sehr gut dazu geeignet, um nach Abschluss von Unit- und Integrationstests Fehler in der Zusammenarbeit aller Softwarebausteine einer Anwendung zu finden.
Abschließend führe ich noch einige bewährte Vorgehensweisen auf, um diese Vorteile tatsächlich nutzen zu können.
Das Testen mit Akzeptanztests erfordert im Team einiges Umdenken im Vergleich zum Testen mit Unit-Tests. Es wird aber damit belohnt, dass im Ergebnis eine lebendige und ausführbare Dokumentation des Systems vorliegt. Diese Dokumentation wurde mit einem gemeinsamen Vokabular verfasst, das das gemeinsame Verständnis ermöglicht.
Test::BDD::Cucumber
ist eine Distribution, mit der Akzeptanztests in Perl geschrieben werden können. Die Distribution kann verwendet werden, um nach der erfolgreichen Ausführung von Unit-Tests Fehler im voll integrierten System zu finden.
Im ersten Teil habe ich gezeigt, wie wir mit Mojolicious
das Login umsetzen und die sogenannten Brains von Nuclino als zip-Archiv holen. Jetzt geht es zum einen darum, die Inhalte als einzelne Markdown-Dateien abzulegen und in unserem Gitlab zu speichern und zum anderen die Abarbeitung zu beschleunigen.
Fangen wir mit dem Entpacken an... Hier nutzen wir Archive::Zip
# siehe Blogpost vom 15.08.2020
my ($info,@brains) = _get_brains( $ua );
my @zips = _download_backups( $ua, \@brains );
# ab hier ist es neuer Code
_extract_backups( \@zips );
_commit_and_push( $home_dir );
sub _extract_backups {
my $zips = shift;
say 'Extract backups ...';
my $obj = Archive::Zip->new;
for my $zip ( @{ $zips || [] } ) {
$obj->read( $zip->to_string );
$obj->extractTree('', $zip->dirname->to_string );
}
$_->remove for @{ $zips || [] };
}
Jedes einzelne zip-Datei wird eingelesen und der Inhalt wird in den backups-Ordner entpackt. Wir verzichten auf das Kommandozeilentool unzip
, weil wir die Perl-Abhängigkeiten in einem cpanfile beschreiben können und nicht daran denken müssen das Tool zu installieren.
sub _commit_and_push {
my $home_dir = shift;
my $git = Git::Repository->new(
work_tree => $home_dir->to_string,
);
say "commit the changes";
$git->run(qw/add --all backups/);
my $date = Mojo::Date->new->to_datetime;
$git->run("commit", "-m", "nuclino backup $date") ;
$git->run(qw/push origin master/);
}
Zur Interaktion mit git
nehmen wir Git::Repository
, damit wir uns nicht um das Wechseln in Verzeichnisse etc. kümmern müssen. Die einzelnen run
-Befehle enthält die gleichen Parameter wie git
-Kommandos im Terminal.
Damit hätten wir ein Backup unserer Nuclino-Dokumente in einem git
-Repository. Jetzt kümmern wir uns um Kleinigkeiten
.
Die Laufzeit spielt in diesem Skript nicht wirklich eine Rolle, da der aktuelle Stand nur einige Male am Tag geholt wird, ist es dann egal ob das Skript 10 oder 20 Sekunden läuft. Wir wollen aber die Möglichkeiten von Mojolicious nutzen.
Um die Zeit optimal zu nutzen, sollen mehrere Brains parallel abgeholt werden. Mit dem Committen der Änderungen muss allerdings gewartet werden, bis die zip-Dateien aller Brains abgeholt und entpackt wurden.
Zum Parallelisieren und wieder zusammenführen, nutzen wir Promises. Der Mojo::UserAgent
hat schon entsprechende Methoden parat, mit denen Promises erzeugt werden können. Auf Promises werde ich auch in einem späteren Blogpost noch näher eingehen.
Bildlich dargestellt, soll das Ergebnis sich folgendermaßen verhalten:
Durch die Promises stößt man das Herunterladen des Archivs an und ohne auf das Ergebnis zu warten, geht man zum nächsten Brain weiter. Damit das Programm aber nicht weitermacht bis alle Archive heruntergeladen und entpackt sind, benötigt man einen Mechanismus, der auf die ganzen Promises wartet.
Um parallel zwei URLs abzurufen und die Titel der Seiten auszugeben, kann man dieses einfache Programm nehmen.
#!/usr/bin/perl
use Mojo::Base -strict;
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;
for my $url ( qw/perl-academy.de perl-services.de/ ) {
$ua->get_p( "https://www." . $url )->then( sub {
my ($tx) = @_;
say $tx->res->dom->find('title')->first->text;
})->wait;
}
An die bekannten Methoden wie get
, post
usw. wird einfach das _p
angehängt und man bekommt ein Objekt vom Typ Mojo::Promise
zurück. Wird kein Fehler geworfen, wird die Subroutine ausgeführt, die man der Methode then
übergibt. Möchte man einen Fehler abfangen, muss man die Methode catch
verwenden.
Um die Mojo-eigene Eventloop zu starten (und zu stoppen wenn der Promise erfüllt wurde) wird anschließend noch wait
aufgerufen.
In dem Callback, den ich hier der Methode then
übergebe wird einfach aus der Antwort ($tx->res
) das DOM geholt, dort nach dem title-Tag gesucht und den Text des ersten Treffers ausgegeben.
Übertragen auf unser Nuclino-Backup rufen wir das Archiv des Brains nicht mehr mit get
ab, sondern mit get_p
. Wir warten nicht, bis alle Archive geholten wurden, bis diese entpackt werden. Das machen wir für das jeweilige Archiv wenn der Promise erfolgreich aufgelöst wurde (then
).
Allerdings wollen wir den commit erst machen, wenn alle Archive geholt wurden.
Unser obiges Beispiel sieht entsprechend angepasst so aus:
#!/usr/bin/perl
use Mojo::Base -strict;
use Mojo::UserAgent;
use Mojo::Promise;
my $counter = 0;
my $ua = Mojo::UserAgent->new;
my @promises;
for my $url ( qw/perl-academy.de perl-services.de/ ) {
my $promise = $ua->get_p( "https://www." . $url )->then( sub {
my ($tx) = @_;
say $tx->res->dom->find('title')->first->text;
$counter++;
});
push @promises, $promise;
}
Mojo::Promise->all( @promises )->then( sub {
say "Found $counter titles";
})->wait;
Um zu zeigen, dass wirklich auf beide Abfragen gewartet wird, habe ich einen Zähler eingebaut. Wir nutzen auch kein wait
beim get_p
-Aufruf. Um auf eine Reihe von Promises zu warten nutzen wir all
von Mojo::Promise
. Das selbst wieder ein Promise zurückliefert. Wenn dieses all
-Promise erfolgreich ist, wird der Callback ausgeführt, der bei then
übergeben wird.
Der angepasste Code in unserem Programm sieht dann so aus:
# siehe Blogpost vom 15.08.20
my ($info,@brains) = _get_brains( $ua );
my @promises = _download_backups( $ua, \@brains );
Mojo::Promise->all( @promises )->then( sub {
_commit_and_push( $home_dir );
})->wait;
sub _download_backups {
my $ua = shift;
my $brains = shift;
say 'Download backups...';
my @promises;
my $backup_path = path(__FILE__)->dirname->child('..', 'backups')->realpath;
$backup_path->remove_tree({ keep_root => 1 });
for my $brain ( @{ $brains || [] } ) {
say '... for brain ' . $brain;
my $url = sprintf 'https://files.nuclino.com/export/brains/%s.zip?format=md', $brain;
my $promise = $ua->get_p(
$url
)->then( sub {
my ($tx_backup) = @_;
my $dir = $backup_path->child( $brain );
$dir->make_path;
my $zip_file = $dir->child( $brain . '.zip' );
$tx_backup->res->save_to( $zip_file->to_string );
_extract_backups( $zip_file->to_abs );
});
push @promises, $promise;
}
return @promises;
}
Es wird noch einen dritten Teil der Reihe geben, weil wir mittlerweile das Backup als Datenquelle für andere Anwendungen nutzen und dafür weitere Arbeiten nötig waren.
Permalink: /2020-09-22-nuclino-backup-II
Schon relativ früh in der Webentwicklung merkt man: Benutzereingaben sind böse! Man sollte ihnen einfach nicht vertrauen. Es muss ja nicht immer gleich eine Sicherheitslücke entstehen, es reicht schon, wenn Nutzer:innen einfach falsche Daten eingeben, die eine Weiterverarbeitung unmöglich machen.
Aus diesem Grund nehmen wir mal ein eigenes Modul als Modul des Monats
: Data::Validate::WithYAML
Dieses Modul ist nicht mehr ganz so jung (erster Commit in Github ist von 2008 – und das war ein Import vom CVS-Repository), aber es ist bei uns immer noch in etlichen Anwendungen im Einsatz. Auf unseren Webseiten gibt es verschiedene Formulare, wie das typische Kontaktformular oder das Bestellen von Demoinstanzen unserer Erweiterungen für die ((OTRS)) Community Edition.
Ich habe keines der damals existierenden Module genommen, weil ich bei Änderungen am Formular und/oder Erkenntnissen was valide
ist ohne Code-Änderungen in der Überprüfung der Formularinhalte übernehmen wollte.
Nehmen wir mal ein Formular zur Anmeldung Hundesteuer:
Das ist ein bunter Mischmasch an erlaubten Werten in den Feldern... Das muss alles geprüft werden. Im ersten Feld (Anzahl der Hunde) wird es eine natürliche Zahl sein. Bei der Anrede gibt es eine Auswahl an Werten, der Name ist ziemlich frei, bei der Adresse könnte man prüfen, ob diese tatsächlich existiert und für die E-Mail-Adressen gibt es auch Regeln.
YAML ist ein tolles Format für Konfigurationsdateien. Aus diesem Grund beschreiben wir die Regeln für die Felder in einer YAML-Datei. In dem Dokument muss das Formular angegeben werden und dort die einzelnen Felder.
---
name_des_formulars:
feld1:
...
feld2:
...
Bei der Definition gibt man dann an, ob es ein Pflichtfeld ist oder nicht
---
name_des_formulars:
feld1:
type: required
...
feld2:
type: optional
...
Alle weiteren Angaben bei den Feldern beschreiben die Regeln für dieses Feld. In dem Modul sind schon einige Regeln definiert:
regex
Der eingegebene Wert muss dem regulären Ausdruck entsprechen.
not_regex
Der eingegebene Wert darf nicht dem regulären Ausdruck entsprechen. Wir nutzen das z.B., um Spam zu finden.
min
Der eingegebene Wert muss größer sein als der bei min angegebene Wert.
max
Der eingegebene Wert muss kleiner sein als der bei max angegebene Wert.
enum
Eine Liste von gültigen Werten.
length
Die Länge des eingegebenen Wertes muss dem Längenbereich entsprechen...
length: 1,
... der Wert muss mindestens 1 Zeichen lang sein.
length: 3,5
... der Wert muss zwischen 3 und 5 Zeichen (Grenzen eingeschlossen) lang sein.
length: ,5
... der Wert darf maximal 3 Zeichen lang sein.
length: 3
... der Wert muss exakt 3 Zeichen lang sein.
datatype
Für einige Datentypen gibt es auch schon vorgefertigte Prüfungen:
Mit diesen grundlegenden Regeln kommt man bei dem obigen Formular schon ein Stück weiter:
---
hundesteuer:
anzahl_hunde:
type: required
min: 1
datatype: positive_int
anrede:
type: required
enum:
- Herr
- Frau
- ''
name:
type: required
length: 3,
geburtsdatum:
type: required
regex: ^[0-9]+\.[0-9]+\.[0-9]+$
wohnort:
type: required
length: 2,
email:
type: required
regex: \@[\w\.]+
Das ist mit Sicherheit noch nicht optimal, denn ergibt es Sinn, dass jemand 1.000 Hunde anmeldet? Es wäre ein Geburtsdatum 9999.13913.1 möglich oder dass eine Adresse angegeben wird, die gar nicht existiert. E-Mailadressen mit einem so einfachen regulären Ausdruck zu prüfen ist nicht die beste Idee.
Aus diesem Grund gibt es noch weitergehende Möglichkeiten, eine der wichtigsten ist die Benutzung von Plugins. Für E-Mail-Adressen gibt es beispielsweise schon fertige Plugins wie Data::Validate::WithYAML::Plugin::EmailMX.
email:
type: required
plugin: EmailMX
Weiterhin ist es möglich, mehrere Regeln zu kombinieren. So könnte man bei der Mail z.B. den regulären Ausdruck, eine Mindestlänge und das Plugin verwenden:
email:
type: required
regex: \@[\w\.]+
length: 6,300
plugin: EmailMX
Zu jedem Feld kann man auch noch eine message angeben, die im Fehlerfalle von validate
zurückgegeben wird.
Sind die Regeln für das Formular in der YAML-Datei hinterlegt, kann man das Formular validieren:
use Data::Validate::WithYAML;
use Data::Printer;
my $validator = Data::Validate::WithYAML->new(
$path_to_yaml_config,
);
# Simulation der Benutzereingaben
my %input_hash = (
anrede => 'Herr',
anzahl_hunde => 1,
name => 'Franziska Meier',
);
my %errors = $validator->validate( step1 => %input_hash );
p %errors;
Für die Verwendung von Data::Validate::WithYAML
in Mojolicious gibt es auch ein entsprechendes Plugin. Damit wird die Verwendung weiter vereinfacht. Die YAML-Datei bleibt die gleiche. In der Mojolicious-Anwendung wird das Plugin geladen:
#!/usr/bin/env perl
use Mojolicious::Lite;
plugin 'Data::Validate::WithYAML' => {
conf_path => app->home . '/conf'
};
get '/' => 'hundesteuer_form';
post '/' => sub {
my ($c) = shift;
my %errors = $c->validate( 'form' => 'step1' );
if ( %errors ) {
$c->stash( errormsgs => \%errors );
return $c->render('hundesteuer_form');
}
$c->render( data => $c->dumper( $c->req->params ) );
};
app->start;
__DATA__
@@ hundesteuer_form.html.ep
... Code siehe Gitlab-Repository ...
Das Mojolicious-Plugin stellt die Methode validate
zur Verfügung. Dort gibt man nur noch den Dateinamen (ohne .json) an. Wenn alles ok ist, liefert die Methode nichts zurück, ansonsten gibt es einen Hash mit den Fehlern.
Dieser Code ist auch im Repository vorhanden.
Permalink: /2020-09-15-data-validate-withyaml
Wir nutzen in verschiedenen Projekte DBIx::Class
als Abstraktionsschicht für die Datenbank. Das Schema der Datenbank entwickeln wir mit dem Tool MySQL-Workbench und die daraus resultierende Datei nutzen wir zur Code-Generierung wie in einem früheren Blogpost beschrieben.
Mit diesem generierten Code liefern wir auch die Änderung an der Datenbank aus. Dazu nutzen wir das Modul DBIx::Class::DeploymentHandler. Das funktioniert echt einfach:
use DBIx::Class::DeploymentHandler;
use Projekt::Schema; # DBIx::Class Schema
my $db = Projekt::Schema->connect(
'DBI:Pg:dbname=projekt_db;host=unser.projekt.host',
'db_user',
'db_password',
);
my $deployer = DBIx::Class::DeploymentHandler->new({
schema => $db,
script_directory => '/path/to/db_upgrades',
databases => ['Pg'],
force_overwrite => 1,
sql_translator_args => {
quote_identifiers => 1,
},
});
$deployer->prepare_install;
$deployer->install;
Wir instanziieren erst das Schema der Datenbank. Anschließend erzeugen wir ein Objekt um die Änderungen an der Datenbank auszurollen. Das Objekt benötigt noch den Pfad zu einem Verzeichnis in dem die Änderungsanweisungen gespeichert werden.
Bei prepare_install
werden die YAML-Dateien in *db_upgrades *erstellt. In diesem YAML-Dateien ist eine Beschreibung der Datenbank: Welche Tabellen mit welchen Spalten gibt es. Weiterhin werden SQL-Dateien erstellt, die für die Datenbank passend die SQL-Befehle enthält.
Mit install
werden dann die Änderungen in der Datenbank ausgerollt.
Und dabei sind wir auf ein Problem gestoßen, denn bei der Installation der Datenbank haben wir jede Menge Warnungen bekommen:
NOTICE: identifier "common_internal_workphase_activity_idx_common_internal_workphase_id" will be truncated to "common_internal_workphase_activity_idx_common_internal_workphas"
NOTICE: identifier "common_project_workphase_activity_idx_common_project_workphase_id" will be truncated to "common_project_workphase_activity_idx_common_project_workphase_"
NOTICE: identifier "contract_remuneration_engineering_idx_contract_remuneration_engineering_facilitygroup_id" will be truncated to "contract_remuneration_engineering_idx_contract_remuneration_eng"
NOTICE: identifier "contract_remuneration_facilitygroup_costgroup_idx_contract_remuneration_engineering_facilitygroup_id" will be truncated to "contract_remuneration_facilitygroup_costgroup_idx_contract_remu"
NOTICE: identifier "contract_remuneration_engineering_costgroup_idx_contract_remuneration_engineering_id" will be truncated to "contract_remuneration_engineering_costgroup_idx_contract_remune"
Ursache ist, dass in PostgreSQL die Identifier nicht länger als 63 Zeichen sein. Sind sie es doch, werden sie gekürzt. Jetzt haben uns zwei Fragen beschäftigt:
Die erste Frage war relativ schnell beantwortet: Ja, geht. Aber dazu muss man PostgreSQL selbst kompilieren, da die maximale Länge im Quellcode hinterlegt ist (Immerhin, mit Closed Source hätte man nicht mal die Chance die Änderungen vorzunehmen und die Software neu zu bauen 😉 ). Mehr dazu gibt es in der Postgres-Doku.
Die zweite Frage war etwas schwieriger zu beantworten. Wir haben diese Identifier nirgends definiert. Also mussten wir nachvollziehen, wie DBIx::Class::DeploymentHandler arbeitet.
Da der fehlerhafte Befehl in der SQL-Datei erstmals auftaucht, musste es beim Erstellen der SQL-Befehle passieren. Das SQL kommt von dem Modul SQL::Translator, und der Index wird in der Methode add_index
aus SQL::Translator::Schema::Table
erstellt.
Letztendlich war die Lösung relativ einfach: Wir überschreiben einfach genau diese Methode:
use SQL::Translator::Schema::Table;
use Digest::MD5 qw(md5_hex);
{
no warnings 'redefine';
my %indexes;
sub SQL::Translator::Schema::Table::add_index {
my $self = shift;
my $index_class = 'SQL::Translator::Schema::Index';
my $index;
if ( UNIVERSAL::isa( $_[0], $index_class ) ) {
$index = shift;
$index->table( $self );
}
else {
my %args = @_;
$args{'table'} = $self;
my $md5 = md5_hex( $args{$name} );
$args{name} = 'idx__' . $args{table} . '__' . $md5;
$index = $index_class->new( \%args ) or return
$self->error( $index_class->error );
}
foreach my $ex_index ($self->get_indices) {
return if ($ex_index->equals($index));
}
push @{ $self->_indices }, $index;
return $index;
}
}
Das ist das Schöne an Perl: Man kann Lösungen finden und anwenden ohne dass man den ursprünglichen Code anfasst. Hier nutzen wir die Möglichkeit, Subroutinen von jeder beliebigen Stelle aus für alle möglichen Namensräume zu definieren. Dazu muss nur der Vollqualifizierte
Name der Subroutine angegeben werden.
Das einzige was wir beachten müssen ist, dass wir das Modul SQL::Translator::Schema::Table
laden bevor wir die Subroutine definieren, denn sonst würde die Methode aus dem Modul unsere Subroutine wieder überschreiben.
Solche Änderungen sollten aber immer das letzte Mittel sein, da das Überschreiben von Subroutinen an ganz anderen Stellen als sie eigentlich definiert wurden, zu längeren Debugging-Sitzungen führen kann wenn mal Fehler auftreten.
Permalink: /2020-09-07-dbix-class-deployment-handler
Aus unseren Unternehmenswerten, die wir in einem internen Wiki aufgeschrieben haben:
Förderung von Open Source. Wir nutzen Open Source und wollen etwas an die Gemeinschaft zurückgeben. Im Speziellen wollen wir uns um Perl kümmern, da uns viel an der Sprache und der Community liegt.
Wir machen schon relativ viel. So haben Gregor und Renée schon einige CPAN-Module veröffentlicht (die zum Teil in Zukunft in unserem Account PERLSRVDE auftauchen werden), helfen bei der Organisation des Deutschen Perl-Workshops und betreiben eine Art CPAN für ((OTRS)) Community Edition Erweiterungen.
Vor kurzem haben wir beschlossen, dass wir noch mehr machen wollen. Es ist jetzt vorgesehen, dass jeder von uns pro Quartal mindestens an einen Ticket bei Perl::Critic
und dessen Abhängigkeiten arbeiten wird.
Klingt nicht viel, aber es ist ein weiterer Punkt, um unsere Unternehmenswerte zu leben. Und irgendwann müssen wir auch noch Geld verdienen...
Wir werden hier über unsere Arbeiten berichten. Die ersten Sachen haben wir auch schon gemacht. Bei der Ursachensuche zu Perl-Critic/Perl-Critic#917 bin ich darauf gestoßen, dass format in PPI
falsch geparst wird und habe dann einen entsprechenden Fix geschrieben, der leider noch nicht integriert wurde. Außerdem wurde der Pull-Request #922 angenommen.
Permalink: /2020-09-04-neues-opensource-committment
Gitlab ist eine webbasierte Plattform für die kontinuierliche Auslieferung von Software. Diese Plattform bietet viele Funktionen, die über die reine Softwareentwicklung und das Deployment hinausgehen. Wir erstellen damit Perl-Distributionen, Docker-Images sowie Dokumentation.
Gitlab ist eine webbasierte Plattform für die kontinuierliche Auslieferung von Software. Unter anderem integriert es Komponenten wie ein Wiki, ein Ticket-System und die Unterstützung des Versionskontrollsystems Git. Außerdem bietet es eine automatisierte Ausführung von beliebigem Code in Docker-Images an, die von Commits ausgelöst werden können.
Welche Befehle ausgeführt werden, kann man in einer YAML-Datei beschreiben. Dort wird auch beschrieben, welches Docker-Image dafür verwendet werden soll und unter welchen Bedingungen die Befehle ausgeführt werden sollen.
Wie wir bei Perl-Services diese Möglichkeiten nutzen, beschreibe ich in diesem Artikel.
Als wir begonnen haben, Gitlab zu evaluieren, haben wir zunächst kleinere Perl-Distributionen als Projekt angelegt und die Git-Repositorys eingebunden. In den YAML-Dateien dieser Projekte haben wir dann konfiguriert, dass nach jedem Commit die Unit-Tests ausgeführt werden und zum Abschluss die Perl-Distribution paketiert werden sollen. Hier ein Auszug aus einem mittlerweile so nicht mehr existenten Projekt:
image: ubuntu:rolling
build:
script:
- apt-get update -qq
- cpanm --installdeps .
- dzil authordeps | cpanm
- dzil listdeps | cpanm
- dzil build
artifacts:
when: always
name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
paths:
- *.tar.gz
Unser Ansatz war zunächst, Fehler durch Tests zu finden und bei erfolgreichem Testdurchlauf die Perl-Distribution zu erstellen. Die benötigten Abhängigkeiten haben wir mit cpanm
installiert. Dabei haben wir festgestellt, dass wir gerne schneller Rückmeldung erhalten würden. Dies führte dann dazu, dass wir die Build-Pipeline optimiert haben.
Verkürzte Durchlaufzeiten erreichen wir einerseits durch eine Anpassung der Befehle, andererseits aber auch dadurch, dass wir ein Docker-Image verwenden, das für diese Distribution optimiert ist und bereits alle Abhängigkeiten enthält.
Was sind die nächsten Schritte? Wir prüfen, ob wir die erfolgreich gebauten Distributionen automatisiert in einen Pinto-Server laden oder auf CPAN veröffentlichen.
Im vorherigen Abschnitt habe ich erwähnt, dass wir Docker-Images für die Entwicklung von Perl-Distributionen optimieren. Wir wollen dafür alle Abhängigkeiten einer Perl-Distribution in einem Docker-Image zur Verfügung stellen, damit wir die Durchlaufzeit der Perl-Distribution klein halten und dann schnell Rückmeldung nach einem Commit bekommen.
Wie haben wir dies mit Gitlab erreicht? Wir setzen Dist::Zilla
ein und beschreiben Abhängigkeiten über dessen Konfiguration dist.ini
und das cpanfile
. Im verwendeten Docker-Image werden diese darüber beschriebenen Abhängigkeiten mit cpanm
installiert.
Dafür haben wir Gitlab so konfiguriert, dass bei einer Änderung dieser Dateien im Master-Branch der Perl-Distribution das verwendete Docker-Image neu erstellt wird. Hier ein beispielhafter Auszug aus einer Gitlab-Konfiguration:
trigger_image_build:
stage: trigger
script:
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.com/api/v4/projects/9398220/trigger/pipeline
only:
refs:
- /^master$/
changes:
- cpanfile
- dist.ini
Die Beschreibungen der Abhängigkeiten werden beim Erstellen des Docker-Images aus dem abhängigen Projekt gelesen. Hier ein beispielhafter Auszug aus einem Dockerfile:
RUN cd /home/mydocker/src && \
git clone https://gitlab-ci-token:${CI_TOKEN}@gitlab.com/perlservices/project.git && \
[...]
RUN cd /home/mydocker/src/project && \
cpanm --skip-satisfied --installdeps . && \
dzil authordeps --missing | cpanm --skip-satisfied && \
dzil listdeps | cpanm --skip-satisfied
Die Gitlab-Dokumentation hat uns hier teilweise eher verwirrt als geholfen, aber letzten Endes haben wir ein perfekt abgestimmtes Image erhalten, in dem alle Abhängigkeiten enthalten sind. Dadurch können in dem Projekt, das auf diesem Image basiert, nach einem Commit sofort die Unit-Tests ausgeführt werden können.
Die Durchlaufzeiten sind daher sehr gering und liegen bei einem gehosteten Gitlab-Repository bei etwa drei Minuten. Der Löwenanteil dieser drei Minuten liegt darin begründet, dass wir auf einen freien Gitlab-Runner warten müssen.
Um die Durchlaufzeiten weiter zu verringern, migrieren wir die Repositorys derzeit auf einen eigenen Gitlab-Server.
Hinweis: Das müssen wir wegen dem Fall des Privacy Shields ohnehin machen 🙂
Die in Projekten zu erstellende Dokumentation reicht je nach Wunsch des Kunden von kurzen Beschreibungen über HTML-Dateien bis zu sauber mit LaTeX gesetzten Dokumenten. Das Ausgangsformat der Dokumente ist für uns stets Markdown, da dies einfach zu schreiben ist, wir uns auf das Wesentliche konzentrieren können und alle gewünschten Zielformate erzeugt werden können.
In Gitlab erzeugen wir mit pandoc
aus den Quelldateien das Zielformat. Gegebenenfalls verarbeiten wir das Resultat automatisiert weiter. Wenn als Ergebnis beispielsweise ein PDF erzeugt werden soll, dann bedeutet dies, dass wir aus Markdown LaTeX-Dokumente erzeugen und diese im Anschluss mit lualatex
in ein PDF wandeln. Damit dies möglich ist, haben wir uns ein Docker-Image erstellt, in dem TeX Live installiert ist.
Da wir lualatex aus TeX Live einsetzen, ist unser Docker-Image mehrere Gigabyte groß. Diese Kröte müssen wir wohl schlucken
. Pandoc
leistet sehr gute Dienste und die Entscheidung für Markdown als Ausgangsformat hat sich bewährt.
Wir werden diese Kombination beibehalten, jedoch gelegentlich prüfen, ob wir das Docker-Image etwas verkleinern können 🙂
Wir erstellen mit Gitlab automatisiert Perl-Distributionen, Docker-Images und Dokumente in unterschiedlichen Formaten. Gitlab als Plattform hat sich hierfür bewährt. Durch den Einsatz einer selbst betriebenen Gitlab-Instanz erhoffen wir uns geringere Durchlaufzeiten.
Permalink: /2020-09-01-wie-nutzen-wir-gitlab
Codier-Richtlinien haben als Ziel, die Formulierung von Code zu vereinheitlichen und ihn dadurch zu verbessern. Wenn sie bei einer bestehenden Code-Basis eingeführt werden, gibt es in der Regel zu Beginn viele Verstöße. Test::Perl::Critic::Progressive
ist ein Werkzeug, um die Richtlinien für Perl-Code schrittweise durchzusetzen.
Das Werkzeug perlcritic
ist seit vielen Jahren das Standardwerkzeug, um Perl-Code statisch zu prüfen. Eine statische Prüfung wird im Gegensatz zur dynamischen Prüfung vor der Ausführung durchgeführt; es bedeutet also, dass der Code untersucht wird, ohne ihn auszuführen. Mit perlcritic
kann so die Einhaltung von Codier-Richtlinien festgestellt werden.
Das folgende Beispiel zeigt einen Aufruf von perlcritic
, der eine Datei auf Einhaltung der Richtlinie CodeLayout::RequireTidy
prüft und einen Verstoß meldet:
$ perlcritic -s CodeLayout::RequireTidy lib/App/perldebs.pm
[CodeLayout::RequireTidyCode] Code is not tidy at lib/App/perldebs.pm line 1.
Durch die Einhaltung von Richtlinien kann eine Team unter anderem Folgendes erreichen:
Die Ablauflogik des Kommandozeilenwerkzeugs perlcritic
kann über das\
Modul Test::Perl::Critic
in die Testsuite eingebunden werden. Verstöße gegen die Richtlinien können so recht einfach aufgedeckt werden:
use Test::Perl::Critic;
all_critic_ok();
Eine Organisation erstellt selten zunächst ihre Richtlinien und entwickelt dann erst die Software anhand dieser Richtlinien. In der Regel ist der umgekehrte Fall der Normalfall: Eine existierende Code-Basis ist ohne Richtlinien geschrieben worden und die Organisation erhofft sich durch Durchsetzung der Richtlinien beispielsweise die oben genannten Vorteile.
Wenn nun bei vorhandener Code-Basis Richtlinien eingeführt werden, gibt es üblicherweise eine Reihe von Verstößen, da der Code vorher beliebig formuliert werden konnte. Die Entwickler sehen sich nun mit dem Problem konfrontiert, dass sie sich einerseits an die Richtlinien halten wollen oder sogar müssen, andererseits der existierende Code nicht zusätzlich zum Tagesgeschäft kurzfristig umgeschrieben werden kann.
Wie kann dieses Problem gelöst werden?
Das Modul Test::Perl::Critic::Progressive
kann hier eine technische Hilfestellung bieten. Es ist hiermit möglich, bei einer beliebigen Anzahl von Verstößen diese Schritt für Schritt zu beheben. Die Code-Basis kann so iterativ in eine Form überführt werden, die den Richtlinien entspricht.
Wie arbeiten dieses Modul nun?
Test::Perl::Critic::Progressive
arbeitet ähnlich wie Test::Perl::Critic
. Es verwendet die Prüflogik von perlcritic
und sammelt beim ersten Aufruf die Anzahl der Verstöße jeder einzelnen Richtlinie. Die Sammlung wird in der Datei .perlcritic-history
als Hash abgelegt:
$VAR1 = [
{
'Perl::Critic::Policy::CodeLayout::RequireTidy' => 10,
'Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval' => 85,
'Perl::Critic::Policy::BuiltinFunctions::RequireGlobFunction' => 0,
'Perl::Critic::Policy::ClassHierarchies::ProhibitOneArgBless' => 2,
...
}
];
Bei jedem weiteren Aufruf wird diese Sammlung erneut erstellt. Beide Sammlungen werden nun in einem Test miteinander verglichen. Dieser Test schlägt fehl, wenn die Anzahl von Verstößen bei einer Richtlinie angestiegen ist:
CodeLayout::RequireTidy: Got 54 violation(s). Expected no more than 36.
Mit Test::Perl::Critic::Progressive
kann man also sicherstellen, dass die Anzahl der Verstöße gegen die festgelegten Richtlinien nicht ansteigt.
Neben dieser technischen Lösung muss es noch organisatorische Änderungen geben, damit der Code auch wirklich verbessert wird. In der Regel bedeutet dies, dass den Entwicklern Zeit für die Verbesserung gegeben werden muss.
Nur dann, wenn das Team neben den technischen Lösung des Auffindens von Problemen zusätzlich noch Verfahren umsetzt, um den Code anhand der Empfehlungen der Richtlinien umzuformulieren, können die Entwickler schrittweise die Verstöße reduzieren. Sie werden dann nach und nach die Code-Basis überarbeiten und im Sinne der Richtlinien verbessern.
Test::Perl::Critic::Progressive
durch seine iterative Arbeitsweise gut geeignet, in agilen Entwicklungsteams eingesetzt zu werden.perlcritic
vorhanden sind, sollte auf keinen Fall vollständig eingebunden werden, da er veraltet ist.Test::Perl::Critic::Progressive
in Versionskontrollsystemen so eingesetzt werden, dass abgelehnter Code nicht eingecheckt werden kann, um dem Entwickler schnellstmögliche Rückmeldung zu geben.Test::Perl::Critic::Progressive
durchgeführter Test in einem CI-System zu Beginn der Testsuite laufen, um dem Entwickler schnell Rückmeldung zu geben.Test::Perl::Critic::Progressive
verwendet Perl::Critic
und somit die Distribution PPI
. Letztere ist bei großen Codemengen langsam und führt zu langen Feedback-Zyklen. Test::Perl::Critic::Progressive
sollte in einem CI-System daher nicht auf allen Quellen angewendet werden. Ist das nicht möglich, so sollte die Verwendung parallel zur restlichen Testsuite ablaufen.Test::Perl::Critic::Progressive
ist ein Werkzeug, um Codier-Richtlinien schrittweise durchzusetzen. In Verbindung mit einem Vorgehen zum Beheben von Verstößen gegen diese Richtlinien kann es eingesetzt werden, um eine Code-Basis im Team nach und nach zu verbessern.
Test::Perl::Critic::Progressive
auf Jenkins unter Verwendung von Git.Perl::Critic
: Ein verwandtes Werkzeug, mit dem eine Entwicklerin iterativ in einer Arbeitssitzung Verstöße gegen Richtlinien beheben kann.Permalink: /2020-08-25-test-perl-critic-progressive
Mich selbst interessiert auch die Frage, welches Modul vom CPAN wird eigentlich besonders häufig genutzt? Da CPAN keine Daten dazu speichert, wie oft ein Modul heruntergeladen wird ist das nicht so einfach zu beantworten. Ein Tweet hat mich dazu gebracht eine kleine Anwendung zu schreiben, bei der jeder seine/ihre cpanfiles hochladen kann: https://usage.perl-academy.de.
Momentan werden dabei nur die requires ausgewertet.
Ja, so lange es nicht besonders weit verbreitet ist, ist die Datenlage sehr dünn und natürlich kann man das leicht manipulieren in dem man die eigenen cpanfiles mehrfach hochlädt. Um eine Idee von häufig genutzten Modulen zu bekommen, ist die Anwendung ganz nützlich.
Ich plane mit der Zeit noch einige statistische Auswertungen hinzuzufügen. Ich werde dann hier im Blog davon berichten. Bis dahin hoffe ich, dass noch einige cpanfiles hochgeladen werden. Jede/r kann mithelfen!
Mit was haben wir die Anwendung umgesetzt? Natürlich mit Mojolicious. Weiterhin kommen noch Minion als JobQueue und PPI zum Parsen der cpanfiles zum Einsatz. Diese beiden Module werden wir hier im Blog noch genauer vorstellen.
Permalink: /2020-08-17-module-usage
Wir bei Perl-Services.de nutzen seit längerem Nuclino als Wissensdatenbank. Natürlich wollen wir unser Wissen auch gut gesichert wissen. Zum einen falls Nuclino irgendwann mal dicht machen sollte, zum anderen aber auch, damit wir jederzeit den Anbieter wechseln könnten.
Aus diesem Grund haben wir uns die Frage gestellt, wie wir ein Backup der Nuclino-Seiten umsetzen können. Aktuell gibt es leider noch kein API, über das wir das Backup erstellen und herunterladen können. Also müssen wir irgendwie anders an die Daten kommen. Jede einzelne Seite besuchen und herunterladen? Da bräuchten wir eine Liste der Seiten. Das muss doch auch anders gehen...
Nuclino ist eine Webanwendung, in der man schnell viel Texte ablegen kann. Diese Texte werden in hierarchischer Form abgelegt. Man kann sogenannte Workspaces zu den verschiedensten Themen anlegen. Innerhalb dieser Workspaces kann man die Texte wiederum in Clustern gruppieren.
Das sieht dann beispielsweise so aus:
Zur Formatierung wird Markdown eingesetzt. Die Markdown-Syntax wird auch direkt umgesetzt und gerendert. Ein Vorteil der Anwendung ist, dass mehrere Personen gleichzeitig an einem Dokument arbeiten können und man bekommt die Änderungen gleich mit. So ist es z.B. möglich, direkt während eines Telefonats Dokumente zu ändern und über die Änderungen zu sprechen.
Die Gliederung der Texte ist erstmal - wie bereits gezeigt - hierarchisch. Man kann aber auch andere Darstellungen wählen, z.B. als Graphen oder als Board:
In den Texten selbst können Inhalte aus verschiedenen Diensten wie zum Beispiel Prezi oder Draw.io eingebettet werden.
Doch zurück zum Backup. Da es keine API gibt, mussten wir uns anschauen, wie die Anwendung arbeitet. Welche Funktionalitäten gibt es in der Webanwendung? Wie werden diese erreicht? Welche Requests werden ausgelöst?
Wer in der Anwendung etwas stöbert oder einfach die Hilfeseiten besucht, wird bei den Workspace-Settings die Funktion
Workspace exportieren
finden:
Man sieht beim Browser die Zieladresse des Links. Man bekommt also ein Zip-Archiv mit den Dokumenten im Markdown-Format – allerdings
nur, wenn man die UUID des Workspaces (oder wie im Link bezeichnet des Brains
) kennt.
Eine Liste der Workspaces händisch pflegen? Kommt nicht in Frage, das muss auch anders gehen! Die Anwendung selbst muss ja auch
irgendwoher wissen, welche Workspaces (oder Brains
) es gibt. Also mal die Developertools des Browsers geöffnet und dann schauen was passiert...
Wenn man sich die Netzwerkanalyse anschaut, fällt beim Login auf, dass neben Google-Analytics und Google-Fonts einige Requests an api.nuclino.com geschickt werden. Unter anderem der Login an sich, aber auch etwas mit inital-state.
Auf den Request sendet der Server als Antwort eine JSON-Struktur, in der auch die Brains
zu finden sind:
{
"response":{
"userId":"<user-uuid>",
"teams":[
{
"id":"<team-uuid>",
"data":{
"name":"<team-name>",
"brains":[
"734eaf2c-b066-11ea-81bd-a36778484b6c",
"820c6310-b066-11ea-9648-bbd09f732919",
...
],
...
Jetzt haben wir alle Informationen, um die Daten aus Nuclino zu sichern. Wir wollen aber mehr. Wir wollen auch die Änderungen sehen, die von Mal zu Mal vorgenommen wurden. Dafür verwalten wir unsere Datensicherung in einem Git-Repository.
Unser Skript muss also folgendes tun:
Die ersten beiden Punkte werden wie folgt abgearbeitet:
use Mojo::File qw(path);
use Mojo::JSON qw(decode_json);
use Mojo::UserAgent;
use Mojo::UserAgent::CookieJar;
my $ua = Mojo::UserAgent->new(
cookie_jar => Mojo::UserAgent::CookieJar->new,
max_connections => 200,
);
my $config = decode_json path(__FILE__)->sibling('nuclino.json')->slurp;
my $header = {
# header wie in den Developertools zu sehen
};
my $base_url = 'https://api.nuclino.com/api/users/';
my $tx_init = $ua->get('https://app.nuclino.com/login');
my $tx_login = $ua->post(
$base_url . 'auth' => $header => json => $config,
);
my $tx_initial_state = $ua->get(
$base_url . 'me/initial-state' => $header
);
my @brains = @{ $tx_initial_state->res->json('/response/teams/0/data/brains') || [] };
In $ua
steckt ein Objekt von Mojo::UserAgent
.
Mit diesem wird erstmal die Login-Seite abgerufen, um
ein initiales Cookie zu erhalten. Ohne dieses Cookie ist der Login nicht möglich.
Anschließend loggt sich das Skript bei Nuclino ein. Hier müssen die Header so gesetzt werden, wie es der Browser auch macht.
Mit dem Request an initial-state wird dann das JSON geholt, in dem die UUIDs der Brains
stehen.
Hier zeigt sich dann auch die Eleganz der Mojo
-Klassen: Mit
$tx->res->json('/response/teams/0/data/brains');
kommt man sehr einfach an die Daten. Wenn die Gegenstelle in den HTTP-Headern den richtigen
Content-Type setzt, kann man über das Response-Objekt (->res
) auf ein
Mojo::JSON::Pointer
-Objekt
(->json
) und damit auf die JSON-Datenstruktur zugreifen. Dazu wird ein XPath-änlicher String
übergeben.
Jetzt, wo wir die UUIDs der Brains
haben, können wir die Zip-Archive einfach herunterladen:
my $backup_path = path(__FILE__)->dirname->child('..', 'backups');
for my $brain ( @brains ) {
my $url = sprintf 'https://files.nuclino.com/export/brains/%s.zip?format=md', $brain;
my $tx_backup = $ua->get(
$url => {
# header wie in den Developertools zu sehen
},
);
my $dir = $backup_path->child( $brain );
$dir->make_path;
my $zip_file = $dir->child( $brain . '.zip' );
push @zips, $zip_file->to_abs;
$tx_backup->res->save_to( $zip_file );
}
Hier wird mit Mojo::File
erstmal das Zielverzeichnis erstellt.
Anschließend wird für jeden Workspace das Zip-Archiv geholt. Für jeden Workspace wird ein eigenes
Verzeichnis erstellt, in das wir dann später die Dateien entpacken können.
Als Ergebnis des GET-Requests bekommen wir ein Mojo::Transaction
-Objekt.
Wie schon oben gesehen, wollen wir wieder etwas mit der Antwort
(->res
) anfangen. Wenn es keine Multipart-Antwort ist, kann der Inhalt der Antwort mit save_to
in eine
Datei gespeichert werden.
Im nächsten Teil vollenden wir das Skript, indem wir die Dateien im Git-Repository speichern und das Skript noch etwas schneller machen.
Permalink: /2020-08-15-nuclino-backup-I
Auch wenn in diesem Jahr dank Corona einiges durcheinandergewirbelt wird, lässt sich für viele Probleme eine Lösung finden. So standen und stehen die Veranstalter von Konferenzen vor dem Problem, dass sich nicht allzuviele Leute auf engem Raum treffen dürfen.
Gerade im IT-Bereich ist es aber kein Problem Konferenzen auch rein virtuell abzuhalten. So gab es im Juni die erste Perl und Raku Conference in the Cloud
und gerade vor ein paar Tagen haben wir an der ContainerConf
einige sehr interessante Vorträge gehört.
Auch die Macher der FrOSCon halten die diesjährige Konferenz online ab. Diesesmal sind wir als Partner/Sponsor mit dabei - wie schon häufiger in den letzten Jahren.
Die FrOSCon ist eine zweitägige Veranstaltung rund um Freie und Open Source Software. Normalerweise an der Hochschule Bonn-Rhein-Sieg in St. Augustin, diesmal eben online.
Auch wenn es schade ist, dass wir die vielen Leute nicht offline treffen können, freuen wir uns auf ein spannendes Vortragsprogramm. Schaut einfach mal vorbei!
Vor einigen Jahren haben wir mehrere Perl-Dev-Rooms auf der FrOSCon veranstaltet. Wenn Ihr Interesse an einem solchen Raum auf zukünftigen Veranstaltungen habt, meldet euch doch.
Permalink: /2020-08-06-froscon
Perl hat eine lange Tradition im Testen. Klassischerweise werden Tests mit Hilfe der Distribution Test::More geschrieben und mit prove ausgeführt. Test::More wird jedoch seit längerem schon vielen Anforderungen nicht mehr gerecht.
Test2 ist eine Neuentwicklung eines Test-Frameworks, das diese Anforderungen erfüllt und für zukünftige Erweiterungen gerüstet ist. In diesem Artikel stellen wir dieses Framework vor.
In den letzten Jahren haben sich die Anforderungen an ein Test-Framework geändert. Die Verwendung von Test::More ist teilweise problematisch. Einige Probleme möchte ich hier aufführen:
Test Anything Protocol). Dieses Protokoll ist eher für Maschinen als Menschen gemacht und teilweise schwer zu lesen. Zudem unterstützt es Subtests nicht vollständig.
Test2 soll alles besser machen und erweiterbar sein. Zudem bietet es erweiterte Funktionalitäten und wird aktiv weiterentwickelt. Der Autor Chad Granum stellt seit 2015 wesentliche Neuentwicklungen auf der Perl Konferenz vor.
Test2 ist praktisch gut ausgreift, sodass Teile von Test::More dieses neue Framework bereits seit Jahren verwenden.
TAP ist das Test Anything Protocol, das Testprogramme und deren Aufrufer verbindet. Es sind in der Ausgabe von Testprogrammen die Zeilen, die mit #
beginnen. Sie werden von den Funktionen, die in den auf Test::Builder basierten Modulen implementiert sind, auf STDOUT ausgegeben. Außerdem gibt es Funktionen, die Diagnosemeldungen auf STDERR ausgeben.
Der Aufrufer von Testprogrammen ist bei Perl bisher in der Regel prove, das Test::Harness (Test-Harnisch
) zum Parsen von TAP verwendet und bei Bedarf STDOUT und STDERR teilweise oder ganz ausgibt.
Die Tests werden mit Test::More implementiert, das wiederum auf Test::Builder aufsetzt. Große Tests können in kleinere Subtests unterteilt werden, die dann im TAP etwas eingerückt dargestellt werden.
Im Wesentlichen werden hier von dem aufrufenden prove Textausgaben verarbeitet.
Test2 bietet eine Reihe von Vorzügen, die ich hier nur in Teilen ausführen kann.
Das Design ist mit den Erfahrungen der letzten 30 Jahre Perl gewählt worden und macht Erweiterungen einfacher möglich. Wegen der einfacheren Arbeit wird Test2 im Gegensatz zu Test::Harness weiterentwickelt. Ein neueres Feature ist zum Beispiel das Setzen von Timeouts, nach dessen Überschreitung ein Test abgebrochen wird.
Die Standard-Testwerkzeuge wurden behutsam sinnvoll ergänzt, andere Module werden dadurch überflüssig. is und like können nun zum Beispiel auch Datenstrukturen vergleichen. Dafür musste bisher ein eigenes Testmodul geladen werden.
Ein kurzes Beispiel:
use Test2::V0;
my $some_hash = { a => 1, b => 2, c => 3 };
is(
$some_hash,
{ a => 1, b => 2, c => 4 },
"The hash we got matches our expectations"
);
done_testing;
Wenn man dieses Testprogramm mit dem prove-Ersatz yath aufruft, erhält man folgende Ausgabe:
$ yath test2-tools-compare-is.t
** Defaulting to the 'test' command **
[ FAIL ] job 1 + The hash we got matches our expectations
[ DEBUG ] job 1 test2-tools-compare-is.t line 5
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 | PATH | GOT | OP | CHECK |
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 | {c} | 3 | eq | 4 |
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 Seeded srand with seed '20200721' from local date.
( FAILED ) job 1 test2-tools-compare-is.t
< REASON > job 1 Test script returned error (Err: 1)
< REASON > job 1 Assertion failures were encountered (Count: 1)
The following jobs failed:
+--------------------------------------+--------------------------+
| Job ID | Test File |
+--------------------------------------+--------------------------+
| 3DC85D18-CB27-11EA-A9FD-3FD6B4ADC425 | test2-tools-compare-is.t |
+--------------------------------------+--------------------------+
Yath Result Summary
-----------------------------------------------------------------------------------
Fail Count: 1
File Count: 1
Assertion Count: 1
Wall Time: 0.30 seconds
CPU Time: 0.49 seconds (usr: 0.12s | sys: 0.03s | cusr: 0.27s | csys: 0.07s)
CPU Usage: 165%
--> Result: FAILED <--
Im Gegensatz zum alleinigen TAP kann Test2 beliebige Ausgaben erzeugen, die man als Plugins nachrüsten kann. TAP ist nur eine Variante.
Das Programm prove wurde durch yath (yet another test harness
) ersetzt. Die damit erzeugte Ausgabe ist für Menschen lesbar, und nicht für Maschinen. yath protokolliert Testergebnisse als Ereignisse in Form von JSON in Dateien, die dann gut automatisiert verarbeitet werden können. Dieses Protokoll kannst du hinterher mit Bordmitteln von yath oder durch andere Programme untersuchen.
Es ist möglich, eine Testsuite mit yath ohne jegliche Ausgabe in zum Beispiel einer CI-Umgebung laufen zu lassen und das Protokoll des Testlaufs bei einem Fehlschlag beliebig oft zu untersuchen. Der folgende Aufruf ruft alle Tests im Verzeichnis t/ rekursiv auf und speichert das Protokoll des Testlaufs in komprimierter Form:
$ yath -q -B
** Defaulting to the 'test' command **
Wrote log file: /private/var/folders/cz/3j2yfjp51xq3sm7x1_f77wdh0000gn/T/2020-07-21_17:25:38_6E265890-CB66-11EA-B019-30DDB4ADC425.jsonl.bz2
The following jobs failed:
+--------------------------------------+-----------+
| Job ID | Test File |
+--------------------------------------+-----------+
| 6E274872-CB66-11EA-B019-30DDB4ADC425 | t/fail.t |
+--------------------------------------+-----------+
Yath Result Summary
-----------------------------------------------------------------------------------
Fail Count: 1
File Count: 2
Assertion Count: 4
Wall Time: 0.34 seconds
CPU Time: 0.56 seconds (usr: 0.13s | sys: 0.03s | cusr: 0.31s | csys: 0.09s)
CPU Usage: 162%
--> Result: FAILED <--
Dieses Protokoll kann dann beispielsweise auf einen Entwicklerrechner heruntergeladen und dort untersucht werden. Beispielsweise kannst du die Ereignisse eines fehlgeschlagenen Tests über dessen UUID erneut abspielen, ohne den Test selbst laufen zu lassen. Du erhältst dann exakt die gleiche Ausgabe wie bei der Ausführung der Tests:
$ yath replay /private/var/folders/cz/3j2yfjp51xq3sm7x1_f77wdh0000gn/T/2020-07-21_17:25:38_6E265890-CB66-11EA-B019-30DDB4ADC425.jsonl.bz2 6E274872-CB66-11EA-B019-30DDB4ADC425
[ FAIL ] job 1 + The hash we got matches our expectations
[ DEBUG ] job 1 t/fail.t line 5
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 | PATH | GOT | OP | CHECK |
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 | {c} | 3 | eq | 4 |
( DIAG ) job 1 +------+-----+----+-------+
( DIAG ) job 1 Seeded srand with seed '20200721' from local date.
( FAILED ) job 1 t/fail.t
< REASON > job 1 Test script returned error (Err: 1)
< REASON > job 1 Assertion failures were encountered (Count: 1)
The following jobs failed:
+--------------------------------------+-----------+
| Job ID | Test File |
+--------------------------------------+-----------+
| 6E274872-CB66-11EA-B019-30DDB4ADC425 | t/fail.t |
+--------------------------------------+-----------+
Yath Result Summary
---------------------------------------
Fail Count: 1
File Count: 2
Assertion Count: 4
--> Result: FAILED <--
STDOUT und STDERR können selbst bei Trennung von nebenläufig ausgeführten Tests über die erwähnten Ereignisse synchronisiert werden. Es ist also problemlos möglich, die Diagnosemeldungen einzelnen Tests zuzuordnen (das ist in obigem Beispiel über die angegebene Jobnummer zu erkennen). Insbesondere die Ausgabe von nebenläufig ausgeführten Tests kann weiterhin den Tests einfach zugeordnet werden. Das war mit prove bisher nicht möglich.
In der Distribution Test2::Harness::UI wird eine Webanwendung geliefert, mit der du die Ergebnisse des Testlaufs untersuchen kannst. Sie ist noch in einem frühen Entwicklungsstadium, funktioniert aber bereits. Beispiele dafür kannst du in einem Vortrag von seinem Autor Chad Granum sehen (YouTube-Video).
yath kann als Prozess laufen und Module vorladen, sodass sie nicht in jedem Testprogramm geladen werden müssen. Das bringt enorme Geschwindigkeitsvorteile – der Entwickler erhält schneller Rückmeldung und muss nicht auf Testergebnisse warten.
Testprogramm können mit Kommentaren versehen werden, die yath auswertet. Beispielsweise zeichnet der Kommentar #HARNESS-CAT-LONG
ein Testprogramm als Langläufer aus, der bei nebenläufigem Aufruf vor den Kurzläufern gestartet wird, damit man nicht am Ende einer Testsuite noch auf den Langläufer warten muss.
Grundsätzlich sind Test2 und yath stabil und für den Produktiveinsatz geeignet.
Tatsächlich wird Test2 bereits seit Jahren von Test::More verwendet, da dies teilweise darin neu implementiert wurde (schau dir mal Test::Builder.pm an).
Eine Umstellung von Testprogrammen von Test::More auf Test2 ist in der Regel mit wenig Aufwand möglich. Es gibt dafür in der Test2-Distribution eine ausführliche Anleitung, in der die wenigen Inkompatibilitäten erklärt sind.
yath kannst du sofort ohne weiteren Aufwand als Ersatz für prove einsetzen.
Test-Harnisch
Permalink: /2020-08-01-test2
Vor etwas mehr als einem Monat hat Sawyer X eine neue Version von Perl 5 veröffentlicht: Perl 5.32. Wie schon in den Versionen davor gibt es wieder einige nützliche Features, die hinzugekommen sind.
Diese werde ich in den folgenden Abschnitten etwas näher erläutern.
Wenn man wissen möchte, ob ein Objekt das Objekt einer bestimmten Klasse ist, war das bisher umständlich. Eine Möglichkeit ist, mit Scalar::Utils
’-Funktion blessed
und der universellen Methode isa
zu arbeiten:
use v5.10;
use strict;
use warnings;
use Scalar::Util qw(blessed);
use HTTP::Tiny;
my $ua = HTTP::Tiny->new;
say blessed($ua) && $ua->isa('HTTP::Tiny');
Warum das blessed
? Weil es sein könnte, dass in $c
irgendetwas drinsteckt. Es muss ja kein Objekt sein. Und wenn es eine einfache Arrayreferenz wäre, dann käme ohne das blessed
eine Fehlermeldung:
Can't call method "isa" on unblessed reference at blessed_and_isa.pl ....
Eine weitere – unschönere Möglichkeit – wäre es, mit ref
zu arbeiten:
use v5.10;
use strict;
use warnings;
use HTTP::Tiny;
my $ua = HTTP::Tiny->new;
say ref( $ua ) eq 'HTTP::Tiny';
Das berücksichtigt keine Vererbung.
Mit dem neuen Infix-Operator isa
ist diese Prüfung auf elegante Art und Weise möglich:
use v5.32;
use strict;
use warnings;
use feature 'isa';
no warnings 'experimental::isa';
use HTTP::Tiny;
my $ua = HTTP::Tiny->new;
say $ua isa HTTP::Tiny;
Noch ist das Feature als experimentelles Feature umgesetzt und dementsprechend muss man das Feature einschalten und dann die Warnung dazu ausschalten. Aber es vereinfacht die Abfrage, ob ein Objekt zu einer Klasse gehört oder nicht.
Auf der rechten Seite von isa
kann übrigens auch ein String oder eine Variable stehen:
say $ua isa "HTTP::Tiny";
# oder
my $class = 'HTTP::Tiny';
say $ua isa $class;
Wenn sich nichts an dem Feature ändert, wird es in der Version 5.36 seinen experimentellen Status verlieren.
Endlich!
möchte man rufen und den Perl-Entwicklern danken, denn endlich sind verkette Vergleiche möglich. Jedenfalls mit einigen Operatoren und wenn sie die gleiche Vorrangigkeit haben. So kann man z.B. wesentlich einfacher und lesbarer prüfen, ob eine Zahl zwischen zwei Werten ist:
use v5.10;
use strict;
use warnings;
my $zahl = 3;
if ( 1 < $zahl < 10) {
say "Zahl zwischen 1 und 10"
}
Auch mit den String-Vergleichen und einem Misch-Masch funktioniert das:
$ perl -E 'my $str = "b"; say 1 if "a" lt $str lt "z";'
1
$ perl -E 'my $str = "d"; say 1 if "a" lt $str gt "c";'
1
$ perl -E 'my $str = "b"; say 1 if "a" lt $str gt "c";'
$ perl -E 'my $str = "d"; say 1 if "a" lt $str >= 0;'
1
$ perl -E 'my $str = "3d"; say 1 if "a" lt $str >= 0;'
$ perl -E 'my $str = "3d"; say 1 if "a" gt $str >= 0;'
1
$ perl -E 'my $str = "3d"; say 1 if "a" gt $str >= 10;'
$ perl -E 'my $str = "30d"; say 1 if "a" gt $str >= 10;'
1
Mit Perl 5.32 wird Unicode 13.0 ausgeliefert. Diese Version enthält 4 neue Skripte (u.a. jesidisch) und etliche neue Emoticons. Das macht insgesamt 5.930 neue Zeichen in Unicode.
Auch die Unterstützung von Unicode wurde verbessert. So können in Regulären Ausdrücken neben den bekannten \N{...}
Ausdrücken auch in \p{...}
die Namen der Zeichen verwendet werden:
use v5.32;
use strict;
use warnings;
my $string = "A";
if ( $string =~ m/^\p{na=LATIN CAPITAL LETTER A}$/) {
say "yupp, ist 'A'"
}
Besonders interessant wird es, wenn man hier Variableninterpolation nutzt:
my $string = "A";
for my $wanted ( 'SMALL', 'CAPITAL') {
if ( $string =~ m/^\p{na=LATIN $wanted LETTER A}$/) {
say $wanted;
}
}
Das funktioniert mit \N{...}
nicht.
Das ganze kann man auch weiter treiben mit den Subpatterns (was allerdings schon seit Perl 5.30 geht):
no warnings 'experimental::uniprop_wildcards';
my @strings = (
"\N{GRINNING FACE}",
"\N{GRINNING FACE WITH SMILING EYES}",
"\N{KISSING FACE}",
);
for my $string ( @strings ) {
if ( $string =~ m!^\p{na=/(GRINNING|SMILING) FACE/}$!) {
say "It's a grinning or smiling face";
}
}
Es wurden drei Sicherheitslücken geschlossen:
Alle drei Sicherheit betreffen Reguläre Ausdrücke. In der Regel ist man davon aber nur betroffen, wenn man ungeprüft Reguläre Ausdrücke von anderen ausführt.
Der Perl-Core liefert viele Module mit. Viele dieser Module werden aber parallel dazu auch auf CPAN veröffentlicht, so dass man auch in älteren Perl-Versionen die verbesserten Module nutzen kann.
Eine Vielzahl dieser Module wurden im Perl-Core auch aktualisiert. Dies sind z.B.
openüberarbeitet
Die Perl-Dokumentation ist sehr umfangreich, aber auch dort gibt es unmodernen Perl-Code und manchmal sind Zusammenhänge nicht ausführlich beschrieben.
Die Funktion open
ist essentiell in Perl. Die Dokumentation dazu wurde von Jason McIntosh im Zuge eines Grants der Perl-Foundation umgeschrieben. Im gleichen Zug wurde auch die Dokumentation perlopentut – das etliche Code-Schnipsel für open
enthält – erweitert.
Ok, das ist keine Weiterentwicklung der Sprache an sich, aber ich sehe das dennoch als gute Sache – es wird für die Community einfacher, sich an der Enwicklung zu beteiligen.
Einen GitHub-Account haben viele, so dass die Beteiligung deutlich einfacher ist. Viele sind auch eher auf GitHub unterwegs als dass sie auf rt.cpan.org geschaut haben. So bekommt man als interessierte Person Diskussionen in den Tickets besser mit.
Durch den Umstieg können auch die GitHub-Actions genutzt werden, so dass die Commits gleich in mehreren Konfigurationen getestet werden können.
Wer die neuen Features wie den isa
-Operator ausprobieren möchte, dem kann ich perlbanjo.com empfehlen. Einfach die einzelnen Code-Snippets dorthin kopieren und laufen lassen.
Eine ausführlichere Liste der Änderungen – z.B. mit der Liste der aktualisierten Module – ist wie immer in den perldelta - what is new for perl v5.32.0 - Perldoc Browser zu finden.
Noch Fragen? Dann am besten eine Mail an uns schicken oder uns auf Twitter kontaktieren.
Permalink: /2020-07-22-neues-in-perl5-32
Gregor und ich haben einige CPAN-Module. Wir wollen hier an dieser Stelle auch immer wieder auf wichtige Neuerungen in unseren Modulen aufmerksam machen.
In den letzten Wochen habe ich vermehrt an MySQL::Workbench::Parser
und MySQL::Workbench::DBIC
gearbeitet um die Abbildung von
Views zu unterstützen.
In der Workbench können ganz einfach Views hinzugefügt werden. In der Konfiguration muss nur das SQL-Statement angegeben werden, wie der View aufgebaut werden soll:
Der Vorteil der Workbench liegt darin, dass man mit Kunden und auch untereinander einfach die Workbench-Datei austauschen kann und man etwas grafisches vor Augen hat wenn man das Datenbankschema bespricht.
Wir nutzen die Workbench und die beiden Module in verschiedenen Projekten um
DBIx::Class
-Klassen für den Zugriff auf die Datenbank zu erzeugen.
Mit einem kleinen Skript lässt sich der Perl-Code ganz einfach generieren:
use Mojo::File qw(curfile);
use MySQL::Workbench::DBIC;
my $foo = MySQL::Workbench::DBIC->new(
file => curfile->sibling('view.mwb')->to_string,
output_path => curfile->sibling('lib')->to_string,
namespace => 'Project::DB',
schema_name => 'Schema',
column_details => 1, # default 1
);
$foo->create_schema;
print sprintf "Version %s of DB created\n", $foo->version;
Damit werden dann folgende Dateien erstellt:
$ tree lib
lib
└── Project
└── DB
├── Schema
│ └── Result
│ ├── table1.pm
│ └── table2.pm
└── Schema.pm
table1.pm
und table2.pm
repräsentieren die entsprechenden Tabellen (siehe Abbildung weiter oben) und
mit den neuen Versionen von MySQL::Workbench::Parser
und MySQL::Workbench::DBIC
werden die Klassen
view1.pm
und view2.pm
erzeugt.
Die Klassen sehen wie folgt aus:
package Project::DB::Schema::Result::view1;
# ABSTRACT: Result class for view1
use strict;
use warnings;
use base qw(DBIx::Class);
our $VERSION = 0.05;
__PACKAGE__->load_components( qw/PK::Auto Core/ );
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table( 'view1' );
__PACKAGE__->result_source_instance->view_definition(
"CREATE VIEW `view1` AS
SELECT
cidr, col2
FROM
table1;"
);
__PACKAGE__->result_source_instance->deploy_depends_on(
["Project::DB::Schema::Result::table1"]
);
__PACKAGE__->add_columns(
cidr => {
data_type => 'INT',
is_numeric => 1,
},
col2 => {
data_type => 'VARCHAR',
is_nullable => 1,
size => 45,
},
);
Die entscheidenden Zeilen sind
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->result_source_instance->view_definition(
"CREATE VIEW `view1` AS
SELECT
cidr, col2
FROM
table1;"
);
Als Klasse, wird hier DBIx::Class::ResultSource::View
verwendet, das extra für Views
existiert. Anschließend erfolgt die SQL-Definition, wie sie in der Workbench eingetragen
wurde.
Hier wird absichtlich kein
__PACKAGE__->result_source_instance->is_virtual(1);
verwendet, weil die Views tatsächlich in der Datenbank angelegt werden sollen. Mit is_virtual(1)
wird der View rein virtuell behandelt.
Über diese Module habe ich übrigens im letzten Jahr auf dem Deutschen Perl-Workshop einen Vortrag gehalten:
Die Code-Beispiele liegen wieder im Gitlab-Repository.
Permalink: /2020-07-17-cpan-updates
Markdown ist ziemlich weit verbreitet, wenn es um Textdokumente geht. Das Format bietet auch viele Vorteile gegenüber anderen Auszeichnungssprachen:
Das Ursprungsmarkdown kann nur einen beschränkten Satz an Befehlen
wie Überschriften, Hervorhebung und Links.
Mit der Zeit sind – wie zu erwarten war – einige Erweiterungen entwickelt
worden. So gibt es z.B. noch MultiMarkdown,
CommonMark und Hoedown.
Mit diesen Erweiterungen werden dann auch Tabellen, Code-Blöcke und noch mehr unterstützt.
Wir setzen bei verschiedensten Projekte Markdown ein – z.B. sind die Artikel für dieses Blog alle in Markdown geschrieben. Das hat den Vorteil, dass wir die Texte in jedem Editor schreiben können und wenn sie auf Gitlab landen, werden sie auch im Browser gleich ansehnlich dargestellt.
Damit Sie die Artikel auch hier im Blog so ansehnlich angezeigt bekommen, müssen wir das Markdown in HTML
umwandeln. Dazu setzen wir das Modul Text::Markdown::Hoedown
ein.
Es ist daher unser erstes Modul des Monats!
Die Nutzung des Moduls ist ziemlich einfach:
use v5.20;
use Text::Markdown::Hoedown;
my $md = q~
# Blogartikel
Das [Perl-Academy Blog](https://blog.perl-academy.de)
enthält einige *interessante* Blogartikel
~;
say markdown $md;
Der obige Code gibt folgendes HTML aus:
$ perl hoedown/hoedown.pl
<h1 id="toc_0">Blogartikel</h1>
<p>Das <a href="https://blog.perl-academy.de">Perl-Academy Blog</a>
enthält einige <em>interessante</em> Blogartikel</p>
Wie weiter oben schon erwähnt, enthält das Standardmarkdown nur wenige Elemente. Hoedown unterstützt aber einige Erweiterungen, die man explizit und einzeln aktivieren muss. Tabellen werden im Standard nicht unterstützt. Die Erweiterungen unterstützen aber zum Glück einheitliche Syntax, für Tabellen sieht das so aus:
my $md = q~
# veröffentlichte Artikel
Datum | Titel
------+-------
01.07.2020 | Blog-Revival
10.07.2020 | Modul des Monats Juli 2020
~;
say markdown $md
Das gibt erstmal nur
$ perl hoedown/table.pl
<h1 id="toc_0">veröffentlichte Artikel</h1>
<p>Datum | Titel
-----------+-------
01.07.2020 | Blog-Revival
10.07.2020 | Modul des Monats Juli 2020</p>
aus. Bei dem markdown
-Aufruf kann man noch Optionen mitgeben. Unter anderem
kann man hier festlegen, welche Erweiterungen aktiviert werden sollen. Für die Tabellen
wäre das HOEDOWN_EXT_TABLES
. Der Aufruf sieht dann folgendermaßen aus:
say markdown $md,
extensions => HOEDOWN_EXT_TABLES;
Damit Sie die Code-Beispiele mit so einem schönen Highlighting sehen können, nutzen
wir im Markdown sogenannte Fenced codes
. Diese werden mit ``` eingeschlossen.
Damit das korrekte Highlighting für die Programmiersprache verwendet
wird, muss diese zu Beginn angegeben werden. Für Perl-Code sieht dies
dann so aus:
# Perl-Code
```perl
use Text::Markdown::Hoedown;
my $md = '# Test';
print markdown $md;
```
Da das ebenfalls nicht zum Standard gehört, muss man die entsprechende Erweiterung aktivieren:
say markdown $md,
extensions => HOEDOWN_EXT_TABLES | HOEDOWN_EXT_FENCED_CODE;
Der Code-Block ergibt folgenden HTML-Code:
<pre><code class="language-perl">use Text::Markdown::Hoedown;
my $md = '# Test';
print markdown $md;
</code></pre>
Das Highlighting wird also nicht von Hoedown gemacht, sondern es wird
nur ein einfacher <code>
-Block erzeugt, dessen Klasse entsprechend
der angegebenen Programmiersprache gesetzt wird.
Für die Farbgebung hier nutzen wir prism.js.
Die Beispielprogramme habe ich in einem extra Gitlab-Repository bereitgestellt.
Permalink: /2020-07-10-modul-des-monats-markdown
Auf Perl-Academy.de war es jetzt längere Zeit ziemlich ruhig. Seit über einem Jahr haben wir keine Schulungstermine mehr veröffentlicht und noch länger war hier nichts zu lesen.
Wenn man zeigen möchte, dass etwas nicht in der Versenkung verschwindet, sondern wie Phönix aus der Asche aufsteht, dann muss das auch offensichtlich sein. Die erste augenscheinliche Änderung auf Perl-Academy ist das Design.
Die zweite Änderung ist hier dieser Blog. Vorher gab es nur einige sporadische Einträge im News
-Bereich. Diesen Blog wollen
wir allgemeiner halten und dazu in regelmäßigen Abständen Artikel zu Perl,
Modulen, Softwareentwicklung allgemein und unseren Schulungen veröffentlichen.
Hier kommt eine weitere Änderung zum Tragen: Ich bin nicht mehr alleine. Neben mir wird auch Gregor in diesem Blog und bei Schulungen aktiv werden. Gregor wird sich bei Gelegenheit auch hier noch vorstellen.
Außerdem überarbeiten wir unser Schulungsangebot. Einige Themen nehmen wir aus dem Programm, andere Schulungen gestalten wir etwas um.
Wenn Sie Fragen, Wünsche oder Anregungen haben, schreiben Sie uns doch bitte eine Nachricht.
In diesem Sinne wünsche ich Ihnen schon einmal viel Spaß beim Durchstöbern der Artikel.
Permalink: /2020-07-01-academy-revival