Posts Tagged ‘FTP’

Test – Tutorial

Sunday, November 15th, 2009

Vor einiger Zeit hatte ich mit Getty, einem anderen Padre-Entwickler, eine Diskussion. Er meinte, dass das schreiben von Tests eine Menge Zeit beansprucht und er es vorher noch nie gemacht hat. Doch er ließ es auf einen Versuch ankommen und war erfolgreich.

Perl test? Was ist das?

Viele Entwickler testen indem sie ihr Program starten oder es mit einem Webbrowser aufrufen und ein wenig  darin rumklicken. Manchmal geraten sie sogar in die Versuchung es in ihrer gedachten Funktion zu nutzen :-)

Perl tests sind wesentlich effizienter. Sie füttern das Programm mit vielen verschiedenen Daten und überprüfen dabei ob alles so ist wie es soll und ob Fehler korrekt abgefangen werden. Besser gesagt: Sie sollten es tun – wenn du sie schreibst.

The story begins…

Ich schrieb Padre::File::FTP kurz vor Fertigstellung der letzten Version. Das Ganze war in weniger als einer Stunde fertig und arbeitete problemlos, war jedoch in einigen Bereichen verbesserungswürdig. Eine dieser Verbesserungen, die exists – Methode, nahm ich mir heute morgen vor.

Dies ist die Methode:

	sub exists {
	        my $self = shift;
	        return if !defined( $self->{_ftp} );
	        return $self->size ? 1 : 0;
	}

Wenn ein Net::FTP – Objekt existiert, überprüfe die Größe der Datei. Eine Datei die Speicherplatz belegt muss auch existieren. Doch es gefiel mir nicht dass leere Dateien nicht erkannt wurden; dies sollte sich heute ändern.

FTP und Net::FTP besitzen keine “exist” Funktionen. Neben der Größenabfrage stellt FTP ein “ls” Kommando bereit, welches dem unter Linux und Windows (genannt “dir”) existierenden ähnlich ist. Doch ich hatte keine Ahnung ob mir das weiterhelfen würde. FTP-Server benutzen gerne ihr eigenes Format für Dateilisten und ich erwartete eine Menge Probleme.

Kenne den Feind

Net::FTP sagt über “ls”…

ls ( [ DIR ] )

Get a directory listing of DIR, or the current directory.In an array context, returns a list of lines returned from the server. In a scalar context, returns a reference to a list.

Kein Wort über das Format oder welche Daten zurückgegeben werden und auch kein Beispiel. Um ein solches zu bekommen, schrieb ich die folgende Zeile in die ->exists – Methode:

print STDERR join("\n",$self->{_ftp})."\n";

Aber… wie starte ich ->exists jetzt? Das war eine der Situationen in denen ein nettes kleines Test-Script viel Zeit sparen kann.

Ein neues Test-Script…

Padre bietet im Datei -> Neu – Menü einige Templates an, unter anderem auch für ein Test-Script, aber ich entschied mit einer Kopie des bestehenden t/92-padre-file.t anzufangen.

Die meisten Zeilen flogen einfach raus und ein paar Minuten später hatte ich folgendes Grundgerüst:

#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Padre::File;
if ( ! $ENV{PADRE_NETWORK_T}) {
	plan(tests => 1);
SKIP: {
	skip 'This test file requires permission to connect to the internet.',1;}
	exit;
}
my $file; # Define for later usage
done_testing();

Gerade ein paar Minuten alt, aber schon ein fertiges Test-Script – auch wenn es noch nichts testet.

Bisher besteht es nur aus den Perl-Grundlagen (Interpreter, Pragmas), Test::More (zum testen) und Padre::File (das zu testende Modul). Da es (für FTP-Tests) zwangsweise auf das Internet zugreifen muss, sollte es nicht ohne Zustimmung des Benutzers laufen. Wenn die dazu notwendige Umgebungsvariable nicht gesetzt ist, beendet es sich einfach mit einem entsprechenden Hinweis.

done_testing(); ist eine aus Test::More importierte Funktion um den erfolgreichen Abschluss der Tests zu vermelden. Fehlt dieser Aufruf, geht Test::More davon aus, daß das Test-Script abgebrochen wurde.

Die Tests

Zur Erinnerung, ich wollte die ->exists – Methode testen, also legen wir los:

# Plain file from CPAN
$file = Padre::File->new('ftp://ftp.cpan.org/pub/CPAN/README');
ok( defined($file), 'FTP: Create Padre::File object' );
ok( ref($file) eq 'Padre::File::FTP', 'FTP: Check module' );
ok( $file->{protocol} eq 'ftp', 'FTP: Check protocol' );
ok( $file->size > 0,            'FTP: file size' );

ok( $file->exists, 'FTP: Exists' );

Einer der vielen Vorteile von Test-Scripten lässt sich bereits in diesen paar Zeilen sehr gut sehen. Ich will die ->exists – Methode testen, also erstelle ich ein Padre::File – Objekt mittels einer FTP-URL. Drei einfache weitere Zeilen geben mir die Sicherheit einer definierten Testumgebung: Wurde ein Objekt erstellt? Ist es ein FTP-Objekt?

Alle weiteren Tests können jetzt auf einer klar definierten und eindeutigen Testumgebung aufbauen. Ohne diese Zeilen würde ich nur vermuten, das alles richtig gelaufen ist. Padre::File->new() hätte fehlschlagen oder mir ein HTTP – Objekt zurückliefern können. Jeder weitere Test wäre sinnlos und ich würde nach Fehlern suchen, die nicht existieren (da die FTP-Tests nicht zwangsweise auch bei HTTP funktionieren).

Ein simples “prove -l t/94-padre-file-remote.t” gab mir die Gewissheit, das die bestehende ->exists – Methode funktioniert und nebenbei noch die gewünschten Beispieldaten.

Die Ergebnisse ausnutzen

Hier ist die neue ->exists Methode:

sub exists {
 my $self = shift;
 return if !defined( $self->{_ftp} );
 # Cache basename value
 my $basename = $self->basename;
 for ($self->{_ftp}->ls($self->{_file})) {
 return 1 if $_ eq $self->{_file};
 return 1 if $_ eq $basename;
 }
 # Fallback if ->ls didn't help. A file heaving a size should exist.
 return 1 if $self->size;
 return 0;
}

It loops through the ->ls – results looking for a full-path or filename-only match. If this fails returning true on match. It also tries the old ->size hack if ->ls failed (due to unexpected syntax or other bad things).

Sie geht alle von ->ls zurückgegebenen Ergebnisse zurück und sucht nach dem vollen Pfad oder dem reinen Dateinamen der gesuchten Datei. Sollte das nicht funktionieren, versucht sie es nochmal mit der alten Größenermittlung.

Noch mehr Tests…

Ein weiterer großer Vorteil von Test-Scripten: Sie können mit minimalem Aufwand wesentlich mehr Tests ausführen als dies manuell möglich wäre – und vor allem in wesentlich kürzerer Zeit.

Ein paar kopierte Zeilen (Padre hat hierfür die schöne Strg+D – Funktion), eine Schleife und folgendes Ergebnis entstand :

# Test some FTP servers
for my $url (
 'ftp://ftp.ubuntu.com/ubuntu/project/ubuntu-archive-keyring.gpg',
 'ftp://ftp.proftpd.org/README.MIRRORS', # Proftpd
 'ftp://ftp.redhat.com/pub/redhat/linux/README', # vsftpd
 'ftp://ftp.cisco.com/test.html', # Apache FTP
 'ftp://ftp.gwdg.de/pub/mozilla.org/_please_use_ftp5.gwdg.de_', # Empty file
# TODO: Find a public FTP server using Microsoft FTP service and add it
 ) {
 $url =~ /ftp\:\/\/(.+?)\// and my $server = $1;
 $file = Padre::File->new($url);
 ok( defined($file), 'FTP '.$server.': Create Padre::File object' );
 ok( $file->exists, 'FTP '.$server.': Exists' );
 my $size = $file->size;
 ok( defined($size), 'FTP '.$server.': '.$size.' bytes' );
}

Diese paar Zeilen testen die ->exists und ->size – Methoden gegen 5 verschiedene URLs auf 5 verschiedenen FTP-Servern, jeder mit anderer Software. Übrigends: Die 5 URLs rauszusuchen dauerte länger als das Schreiben der Tests.

Ergebnisse

Die Entwicklung dieses Test-Scripts beanspruchte weniger Zeit als ich gebraucht hätte, um alle Tests auch nur einmal in Padre selbst auszuführen. Mit einem einfachen prove – Aufruf werden 26 Tests gegen Padre::File::FTP durchgeführt. Sobald sich jemand mit einem FTP-Server meldet, der Probleme macht, reicht eine zusätzliche Zeile (in der for-Schleife) um diesen zu testen.

Jeder Padre – Entwickler und Benutzer kann den Test starten und wir finden dadurch möglicherweise (zukünftige) Bugs noch bevor ihre Auswirkungen in Padre selbst entdeckt werden. Zu jedem Testfehler wird die Zeilennummer im Test-Script ausgegeben und erspart einem die Suche nach einem Bug, der seine Auswirkungen erst ein dutzend Funktionsaufrufe später zeigt.

Tests kosten Zeit, aber man spart durch sie meistens wesentlich mehr Zeit, spätestens wenn der erste Bug auftaucht, der mit einem (bestehenden) Testscript in Sekunden identifiziert oder zumindest eingegrenzt ist.

Das fertige Test-Script gibt es hier.

How to write a test

Saturday, November 14th, 2009

I had a chat with Getty, another Padre developer, some time ago. He said that writing Tests takes much time and he never did one before. Getty gave testing a try and reported success not long ago.

Perl test? What’s this?

Many developers run tests by starting their program or webbrowser and clicking somewhere within the application. They even try to actually use it the way it should be used – sometimes.

Perl tests are much more powerful. They throw a huge number of things against your application and check if everything goes right and they check that errors are handled correctly. Better: They should do – if you write them.

The story begins…

I wrote Padre::File::FTP just before the last release. It was a less-than-one-hour hack and it worked fine, but there are some things left which need to be improved. I decided to face one of them, the exists – method, this morning.

Here is that method:

	sub exists {
	        my $self = shift;
	        return if !defined( $self->{_ftp} );
	        return $self->size ? 1 : 0;
	}

If there is a Net::FTP – object, check the size. Whatever has a size, must exist. But I didn’t like that zero-sized files were not detected properly and this was going to change today.

FTP and Net::FTP don’t have “exist” functions. Besides the size-request, FTP provides a “ls” command like it exists on Linux and (called “dir”) on Windows, but I didn’t know if it was useful at all. FTP-Servers like to return their own file listing format and I expected a lot of problems.

Know about your enemy

Net::FTP says about “ls”…

ls ( [ DIR ] )Get a directory listing of DIR, or the current directory.

In an array context, returns a list of lines returned from the server. In a scalar context, returns a reference to a list.

Nothing about the format, what information is returned or a sample. To get a sample, I added the following line to the exists method:

print STDERR join("\n",$self->{_ftp})."\n";

But… how to run this method? This is one of the situations where a test script would safe your day.

A new test script was born

Padre got a lovely function to create a new test from scratch within the file – new – menu. But I decided to start using a copy of the existing t/92-padre-file.t today.

I deleted most of the lines and did some minor changes and got the following core few minutes later:

#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Padre::File;
if ( ! $ENV{PADRE_NETWORK_T}) {
	plan(tests => 1);
SKIP: {
	skip 'This test file requires permission to connect to the internet.',1;}
	exit;
}
my $file; # Define for later usage
done_testing();

This is already a complete test script, but it doesn’t run any tests. It has the Perl basics (Perl interpreter, pragmas), pulls in Test::More for testing and Padre::File for being tested. As it should run internet tests, it shouldn’t run without the users explicit permission (given by setting an environment variable). It only shows a message and exits without internet permission

done_testing(); ist a Test::More function indicating that all tests have been completed successful (= without test script crash).

Adding the tests

Remember: I want to run the ->exists method of Padre::File::FTP and here we go:

# Plain file from CPAN
$file = Padre::File->new('ftp://ftp.cpan.org/pub/CPAN/README');
ok( defined($file), 'FTP: Create Padre::File object' );
ok( ref($file) eq 'Padre::File::FTP', 'FTP: Check module' );
ok( $file->{protocol} eq 'ftp', 'FTP: Check protocol' );
ok( $file->size > 0,            'FTP: file size' );

ok( $file->exists, 'FTP: Exists' );

You could see one of the biggest advantages of test scripts in this short sample: I want to check the exists function. All I need is a Padre::File – object created using a FTP url. But adding three simple lines gives me a complete check of the the Padre::File::FTP->new method and the Padre::File->new method. Did I get a object? Is it a FTP object?

Running these simple tests creates a known testing environment: I’ld expect to have a good object in $file but I didn’t know it for sure. ->new may fail or return a Padre::File::HTTP object and every following test won’t give any useful result.

Running the test using “prove -l t/94-padre-file-remote.t” returned a successful test result (all tests work using the old ->exists method) and the required information about Net::FTP->ls.

Using the results

Here is the new ->exists method:

sub exists {
 my $self = shift;
 return if !defined( $self->{_ftp} );
 # Cache basename value
 my $basename = $self->basename;
 for ($self->{_ftp}->ls($self->{_file})) {
 return 1 if $_ eq $self->{_file};
 return 1 if $_ eq $basename;
 }
 # Fallback if ->ls didn't help. A file heaving a size should exist.
 return 1 if $self->size;
 return 0;
}

It loops through the ->ls – results looking for a full-path or filename-only match. If this fails returning true on match. It also tries the old ->size hack if ->ls failed (due to unexpected syntax or other bad things).

More tests

Another advantage of test scripts: They could test more cases than you could or would do yourself.

Cloning some lines (Padre has Ctrl-D for this) resulted in the following additional tests:

# Test some FTP servers
for my $url (
 'ftp://ftp.ubuntu.com/ubuntu/project/ubuntu-archive-keyring.gpg',
 'ftp://ftp.proftpd.org/README.MIRRORS', # Proftpd
 'ftp://ftp.redhat.com/pub/redhat/linux/README', # vsftpd
 'ftp://ftp.cisco.com/test.html', # Apache FTP
 'ftp://ftp.gwdg.de/pub/mozilla.org/_please_use_ftp5.gwdg.de_', # Empty file
# TODO: Find a public FTP server using Microsoft FTP service and add it
 ) {
 $url =~ /ftp\:\/\/(.+?)\// and my $server = $1;
 $file = Padre::File->new($url);
 ok( defined($file), 'FTP '.$server.': Create Padre::File object' );
 ok( $file->exists, 'FTP '.$server.': Exists' );
 my $size = $file->size;
 ok( defined($size), 'FTP '.$server.': '.$size.' bytes' );
}

These few lines run the ->exists and ->size methods on a 5 different URLs. Believe me, getting those URLs took much longer than writing the test.

Benefits

Writing this test used up less time than running all these tests manually once (using Padre). One simple prove command could run 26 tests using the Padre::File::FTP module and if anybody reports a FTP server which can’t be used, one additional line (in the for-loop – preparation part) is enough for a sufficient test run.

Every Padre developer and user could replay this test script and we may detect some bugs before they show up in Padre itself. Every failed test is reported including the test script line number – no need to reverse follow the results of the results of the results of a bug.

Writing tests is time-consuming but you’ll get a much bigger payback on time compared to manual testing and bug-hunting.

The final test script is shown here.

FTP-Dateien: 15 Monate warten – in einer Stunde erledigt

Friday, November 6th, 2009

Trac, die Padre Ticketverwaltung, führt das 15 Monate alte Ticket #12 “Add remote editing capability via ftp and ssh” als “offen”.

Vor 7 Wochen wurde Padre::File entwickelt, ein Modul, das alle Dateizugriffe von Padre übernehmen und abstrahieren soll.

Gestern Abend fing ich aus einer Laune heraus an, Padre::File::FTP zu schreiben, ein Modul das die direkte Bearbeitung von Dateien auf einem FTP-Server erlauben soll. Überraschenderweise war das Modul nach einer Stunde nicht nur  angefangen, sondern (in einer ersten, einfachen Version) fertig.

Changeset 9096 brachte die erste Version des neuen Moduls online. Hier ein kurzer Überblick:

                if ($url !~ /ftp\:\/?\/?((.+?)(\:(.+?))?\@)?([a-z0-9\-\.]+)(\:(\d+))?(\/.+)$/i) {
	                # URL parsing failed
	                # TODO: Warning should go to a user popup not to the text console
	                warn 'Unable to parse '.$url;
	                return;
	        }
	        # Login data
	        if (defined($2)) {
	         $self->{_user} = $2;
	         $self->{_pass} = $4 if defined($4);
	        } else {
	         $self->{_user} = 'ftp';
	         $self->{_pass} = 'padre_user@devnull.perlide.org';
	        }
	        # Host & port
	        $self->{_host} = $5;
	        $self->{_port} = $7 || 21;
	        # Path & filename
	        $self->{_file} = $8;

Padre sollte alle “Dateien” als “URLs” behandeln können und intern entscheiden, welches Modul für den Zugriff notwendig ist.

Die RegEx filetiert die URL in (optional) Benutzername/Passwort, den Servernamen, optional einen Port und einen Dateinamen (mit Pfad auf dem Server). Ist kein Benutzername angegeben, werden fest eingestellte Standardwerte für anonymen FTP-Zugriff verwendet. Alle Daten werden im “self” – Hash gespeichert, damit die einzelnen Methoden schnell, einfach und effizient darauf zugreifen können.

 $self->{_ftp} = Net::FTP->new(
 Host => $self->{_host},
 Port => $self->{_port},
 Timeout => 120, # TODO: Make this configurable
 Passive => 1, # TODO: Make this configurable
 );

Servername und Port stammen aus der zerlegten URL und fest eingestellte Werte für Zeitüberschreitung und Passive-FTP-Modus sind für die erste Modulversion auch akzeptabel.

Bei Verbindungsproblemen wird eine Warnung ausgegeben (hier fehlt noch eine komplette Fehlerbehandlung).

$self->{_file_temp} = File::Temp->new( UNLINK => 1 );
$self->{_tmpfile} = $self->{_file_temp}->filename;

Net::FTP bietet einen Direktzugriff auf die Datenverbindung, aber für den Anfang reichen auch die gute alten get- und put – Befehle. Diese benötigen allerdings eine temporäre lokale Datei und File::Temp ist hierfür die beste Lösung zumal es betriebssystemunabhängig arbeitet und die temporäre Datei automatisch aufräumt sobald das File::Temp objekt nicht mehr verwendet wird.

Einige weitere Hilfsfunktionen (wie ->size oder ->exist) waren schnell gemacht, am wichtigsten war aber der Dateizugriff:

	sub read {
	        my $self = shift;
	        $self->{_ftp}->get($self->{_file},$self->{_tmpfile}) or warn $@;
	        open my $tmpfh,$self->{_tmpfile};
	        return join('',<$tmpfh>);
	}

Diese Methode läd die Datei zunächst herunter und speichert sie in der temporären Datei. Abgesehen vom direkten Zugriff auf die Datenverbindung bietet Net::FTP leider keinen besseren Weg um an den Dateiinhalt zu kommen.

Nachdem das Modul funktionierte fehlte nur noch ein Schritt um Padre FTP – URLs nahezubringen und dieser wurde mit changeset 9097 erledigt.

Alles zusammen – einschließlich Modulstruktur und Einarbeitung in Net::FTP war in weniger als einer Stunde erledigt, was ich wirklich nicht erwartet hatte.

Entwicklung mit und für Padre ist häufig schneller als erwartet.

FTP-editing: A 15 month old story done in one hour

Friday, November 6th, 2009

Padre’s trac still contains ticket #12 “Add remote editing capability via ftp and ssh” which is 15 month old.

7 weeks ago, Padre::File was added, which should take over all file activity within Padre.

I just started Padre::File::FTP yesterday night which plugs in an FTP server as file storage . Net::FTP was easy to use and I wondered that basic FTP support including write access was not only started, but finished within an hour.

Changeset 9096 added the FTP module including many TODO’s. Here is a quick walkthrough:

                if ($url !~ /ftp\:\/?\/?((.+?)(\:(.+?))?\@)?([a-z0-9\-\.]+)(\:(\d+))?(\/.+)$/i) {
	                # URL parsing failed
	                # TODO: Warning should go to a user popup not to the text console
	                warn 'Unable to parse '.$url;
	                return;
	        }
	        # Login data
	        if (defined($2)) {
	         $self->{_user} = $2;
	         $self->{_pass} = $4 if defined($4);
	        } else {
	         $self->{_user} = 'ftp';
	         $self->{_pass} = 'padre_user@devnull.perlide.org';
	        }
	        # Host & port
	        $self->{_host} = $5;
	        $self->{_port} = $7 || 21;
	        # Path & filename
	        $self->{_file} = $8;

Padre should be able to open everything in one-line-URL-format. The URL parsing regular expression accepts optional username/passwort, a server, a optional port and a filename. If no username is defined, the module switches to hard-coded anonymous FTP values. Everything is stored into the blessed “self” hash as individual keys for easy access.

 $self->{_ftp} = Net::FTP->new(
 Host => $self->{_host},
 Port => $self->{_port},
 Timeout => 120, # TODO: Make this configurable
 Passive => 1, # TODO: Make this configurable
 );

Host and port are used from the URL, static values for timeout and passive mode are ok for a first start and until there is a configuration dialog for Padre::File preferences. A warning is issues on connection problems (this needs to be improved).

$self->{_file_temp} = File::Temp->new( UNLINK => 1 );
$self->{_tmpfile} = $self->{_file_temp}->filename;

Net::FTP offers direct access for data connections but I preferred the good-old get and put commands for this first basic solution. A local tempfile is required for them and File::Temp provides a good OS-independent way of managing tempfiles. The object needs to be stored as the temp file will be finally removed as soon as the File::Temp object is destroyed.

There are some helper methods (like ->exists or ->size), but file access is the most important thing we need:

	sub read {
	        my $self = shift;
	        $self->{_ftp}->get($self->{_file},$self->{_tmpfile}) or warn $@;
	        open my $tmpfh,$self->{_tmpfile};
	        return join('',<$tmpfh>);
	}

This method downloads the FTP-file into a local temporary file and just returns the local content. Net::FTP lacks an easy way to get a file directly into a variable, but this is also ok – we’re still talking about a first basic solution.

The next step was done in changeset 9097: Adding support for FTP-style URLs to Padre::File.

Everything – including generation of the module, looking up Net::FTP documentation was done in less than one hour. I really didn’t expect it to be this simple and fast but you see – helping on Padre is much less time-consuming than you might think.