- step 1) an online repository on github
- step 2) a new module with module-starter
- step 3) a local repository with git
- step 1) POD documentation
- step 2) first test
- step 3) commit changes with git
- step 4) pushing to github repository
- step 1) first lines of code
- step 2) testing on our own
- step 3) add dependencies in Makefile.PL
- step 4) run the new test
- step 5) commit, add new files and push with git
- step 1) the educated documentation
- step 2) git status again and commit again
- step 3) more code...
- step 4) ...means more and more tests
- step 5) git: a push for two commits
- step 1) more validation in the code
- step 2) a git excursus
- step 3) add deeper tests
- step 4) who is ahead? git branches and log
- step 5) overall check and release
- step 6) test list form
- step 1) the problem of empty lists
- step 2) adding a Carp to the lake
- step 3) prepare the fishing road: add a dependency for our test
- step 4) go fishing the Carp in our test
- step 5) document the new warning feature
- step 1) sharing
- step 2) files in a CPAN distribution
- step 3) another kind of test: MANIFEST
- step 4) another kind of test: POD and POD coverage
- step 5) some README and final review of the work
- step 6) try a real CPAN client installation
- step 7) looking toward quality
- option one - the bare bone module
- option two - the Exporter module
- option three - the OO module
- other testing modules
- advanced Makefile.PL usage
- advanced testing code
- CORE documentation about modules
- CORE documentation about testing
- further readings about modules
- further readings about testing
This tutorial is not about coding: that's it! The code, idea and implementation presented below are, by choice, futile, piffling and trifling ( the module we write following this tutorial is available, archived in its own repository).
This tutorial, on the other hand, tries to show the beginner one possible path in module creation. As always in perl there are many ways to get the job done and mine is far from the most optimal one, but as I have encountered many difficulties in choosing my own path, perhaps sharing my way can help someone else.
There are other similar but different sources of knowledge about module creation, notably José's Guide for creating Perl modules: read this for some points I do not explore (well, read it anyway; it's worth it)
As for every job, check your equipment before starting. You probably already have perl, a shell (or something less fortunate if you are on windows, like me ;) and a favourite text editor or IDE.
But here in this tutorial we'll use git
in the command line and github to store our work in a central point (very handy feature). So get a github
account and a git
client.
This tutorial will focus on the importance (I'd say preeminence or even predominance) of testing while developing a perl module. I wrote lonely scripts for years then I realized that even if my script seemed robust, I have no way to test them in a simple and reliable way.
So we will use the core module Test::More and the CPAN Test::Exception in our module so get it installed using your cpan
or cpanm
client. Take a look to Test::Simple if you are not used to test.
We also use the core module Carp to report errors from the user's point of view.
We use Module::Starter to have the skeleton of our module done for us, but, as always there are valid alternatives. Install it.
We'll document our module using POD (Plain Old Documentation) see perlpod for a reference.
Some of your programs or modules work on lists and arrays. Functions inside these programs accept ranges but while you intend them to only be simple ranges ( 0,1,2
or 0..2
) you discover that your programs crashed many times because other humans or other programs passed ranges like: 0,1..3,2
(where 2
is present twice) or 3,2,1
(and your application is silently expecting 1,2,3
) or 9..1
or even 0,1,good,13..15
not being a range at all, or simply 1-3
being a range for the user but not for your perl code that reads it as -2
.
Tired of the situation you plan a new module to validate ranges. Range::Validator
is the name you choose. Your initial plan is to expose just one sub: validate
As in masonry, you need a well prepared plan before starting excavations. Then you need points and lines drawn on the terrain: everything that makes the job complex is part of the job itself.
Look around: you can bet someone else got your same idea before you. You can also bet he or she was smarter than you and it was already uploaded to CPAN.
Sharing early is a good principle: if you already have an idea for your module (even before implementing it), it can be worth asking in a forum dedicated to Perl (like perlmonks.org) and posting a RFC post (Request For Comments), or using the dedicated website like prepan.org (though it is not a crowdy place nowadays..;).
Plan it well: it is difficult, but remember that to repair something poorly planned is always a worse task. The base knowledge is in the core documentation: perlnewmod is the place to start and perlmodstyle is what comes next. Dont miss the basic documentation.
If you want to read more see, in my bibliotheca, the scaffold dedicated to modules.
Choose your names carefully: the module name and names of methods or functions your module exports. Good code with poorly named methods is many times more unusable for others than the author.
Programming is a matter of interfaces. Coding is easy, engineering is hard. You can change the implementation a million, you can never change how other people use your code. So plan well what you offer with your module. In the future you can add new features; you cannot remove even one of them because someone may already be using it in production. Play nice: plan well.
You can learn a lot from this wonderful post: On Interfaces and APIs
Create an empty repository on github named Range-Validator (they do not accept ::
in names) see instructions here
Open a shell to your script's location and run the program module-starter
that comes from Module::Starter
. It wants an e-mail address, the author name, and obviously the module name:
shell> module-starter --module Range::Validator --author MyName --email [email protected] Added to MANIFEST: Changes Added to MANIFEST: ignore.txt Added to MANIFEST: lib/Range/Validator.pm Added to MANIFEST: Makefile.PL Added to MANIFEST: MANIFEST Added to MANIFEST: README Added to MANIFEST: t/00-load.t Added to MANIFEST: t/manifest.t Added to MANIFEST: t/pod-coverage.t Added to MANIFEST: t/pod.t Added to MANIFEST: xt/boilerplate.t Created starter directories and files
A lot of work done for us! The module-starter
program created all the above files in a new folder named Range-Validator
. Let's see the content:
---Range-Validator | Changes | ignore.txt | Makefile.PL | MANIFEST | README | |---lib | | ---Range | Validator.pm | |----t | 00-load.t | manifest.t | pod-coverage.t | pod.t | |----xt boilerplate.t
We now have a good starting point to work on. Spend some time to review the contents of the files to get familiar with them.
Open another shell for the git client (I prefer to have two, feel free to use just one) to the same path of the above created folder and initialize a git repository (local for the moment) there:
git-client> git init Initialized empty Git repository in /path/to/Range-Validator/.git/
Nothing impressive.. What happened? The above command created a .git
directory, ~15kb of info, to track all changes you'll make to your files inside the Range-Validator
folder. In other words it created a git repository. Empty. Empty?!? And all my files?
It's time for a command you'll use many, many times: git status
git-client> git status On branch master No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) Changes MANIFEST Makefile.PL README ignore.txt lib/ t/ xt/ nothing added to commit but untracked files present (use "git add" to track)
Many terms in the above output would be worth explaining, but not by me. Just be sure to understand what branch
, commit
, tracked/untracked
means in the git world. Luckily the command is so sweet to add a hint for us in the last line: (use "git add" to track)
Git is built for this reason: it can track all modifications we do to the code base and it takes a picture (a snapshot in git terminology) of the whole code base every time we commit changes. But git init
initialized an empty repository; we must tell git which files to add to the tracked ones.
We simply want to track all files module-starter
created for us: git add .
adds the current directory and all its contents to the tracked content. Run it and check the status again:
git-client> git add . git-client> git status On branch master No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: Changes new file: MANIFEST new file: Makefile.PL new file: README new file: ignore.txt new file: lib/Range/Validator.pm new file: t/00-load.t new file: t/manifest.t new file: t/pod-coverage.t new file: t/pod.t new file: xt/boilerplate.t
We added all content but we still have not committed anything! git commit -m "some text"
will commit all changes using the message provided as a label for the commit (without -m
git will open a text editor to enter the text). Run it and recheck the status again:
git-client> git commit -m "module-starter created content" [master (root-commit) 1788c12] module-starter created content 11 files changed, 409 insertions(+) create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100644 ignore.txt create mode 100644 lib/Range/Validator.pm create mode 100644 t/00-load.t create mode 100644 t/manifest.t create mode 100644 t/pod-coverage.t create mode 100644 t/pod.t create mode 100644 xt/boilerplate.t git-client> git status On branch master nothing to commit, working tree clean
With the above we committed everything. The status is now working tree clean
what better news for a lumberjack used to examine daily tons of dirty logs? ;)
Now we link the local copy and the remote one on github: all examples you'll find, and even what github proposes to you, says git remote add origin https://github.com/...
where origin
is not a keyword but just a label, a name. I found this misleading and I use my github name in this place or something
that tells me the meaning, like MyFriendRepo
. So from now on we will use YourGithubLogin
there.
Add the remote and show it verbosely ( with -v
):
git-client> git remote add YourGithubLogin https://github.com/YourGithubLogin/Range-Validator.git git-client> git remote -v YourGithubLogin https://github.com/YourGithubLogin/Range-Validator.git (fetch) YourGithubLogin https://github.com/YourGithubLogin/Range-Validator.git (push)
The verify operation gives us two hints: for the remote repository that we call YourGithubLogin
we can do fetch
(import all changes you don't have locally from the remote repository) or push
(export your local copy to the remote repository).
Since on github there is nothing and locally we have the whole code base, we definitively want to push
and we can do that if and only if, we have permission to push in the remote repository. It's our own repository, so no problem (git will ask for the github password). The push
wants to know which branch to push; we only have master
so:
git-client> git push YourGithubLogin master fatal: HttpRequestException encountered. Username for 'https://github.com': YourGithubLogin Password for 'https://[email protected]': *********** Counting objects: 17, done. Delta compression using up to 4 threads. Compressing objects: 100% (14/14), done. Writing objects: 100% (17/17), 5.33 KiB | 303.00 KiB/s, done. Total 17 (delta 1), reused 0 (delta 0) remote: Resolving deltas: 100% (1/1), done. remote: remote: Create a pull request for 'master' on GitHub by visiting: remote: https://github.com/YourGithubLogin/Range-Validator/pull/new/master remote: To https://github.com/YourGithubLogin/Range-Validator.git * [new branch] master -> master
Go to the github website to see what happened: the whole code base is in the online repository too, updated to our last commit (a.k.a. our first and only commit for the moment). From now on we can work on our code on any machine that has a git
client. To do so we must be diligent and commit and push our changes when it is time, to keep the online repository up to date. Clean yard, happy master mason.
A whole day has passed, well.. two days, and we did not write a single line of perl code yet. We are starting the right way! Time to go to sleep with a well prepared playground.
Well first of all some cleaning: open you local copy of the module /path/to/Range-Validator/lib/Range/Validator.pm
in your text editor or IDE. Personally I like the POD documentation to be all together after the __DATA__
token rather interleaved with the code. Inside the code I only like to have comments. POD documentation is for the user, comments are for you! After a week or month you'll never remember what your code is doing: comment it explaining what is passing.
So go to the end of the module where the line is the final 1;
( remember all modules must have a true return value as last statement) and place, in a new line the __DATA__
token. Move all POD after the token. Also cancel the POD and the code relative to function2
Then rename function1
into validate
and change accordingly the name of the POD section too.
Modify the POD part =head1 NAME
with a more humble and meaning description: Range::Validator - a simple module to verify array and list ranges
Change the =head1 SYNOPSIS
part too, removing unneeded text and changing code lines ( see below ): we do not do an object oriented module, so no new
method for us. You plan to accept both real ranges and strings representing ranges.
So, if you followed me, the module must look like:
package Range::Validator; use 5.006; use strict; use warnings; our $VERSION = '0.01'; sub validate { } 1; __DATA__ =head1 NAME Range::Validator - a simple module to verify array and list ranges =head1 VERSION Version 0.01 =cut =head1 SYNOPSIS use Range::Validator; my @range = Range::Validator->validate(0..3); # a valid range my @range = Range::Validator->validate(0..3,2); # a overlapping range my @range = Range::Validator->validate('1,3,7'); # a valid range passed as a string my @range = Range::Validator->validate('1,XXX,3'); # an invalid range passed as a string # more POD ...
Ok? Let's check our new POD is correct: open the shell in the directory created yesterday /path/to/Range-Validator
and run the following command: perldoc ./lib/Range/Validator.pm
Review the POD. It must be ok.
Now we test if the module syntax is correct. The first simple method is a short one liner using the perl option -I
to include ./lib
in @INC
and -MRange::Validator
to use our module( see
perlrun and
perlvar ):
shell> perl -I ./lib -MRange::Validator -e 1 shell>
No errors: good! the module can be used and has no syntax errors. But.. one moment: we want to try out all our features, and we plan to add many, using one liners? Are we mad?! No; we will use tests.
Tests are wonderful in perl and planning good tests (a test suite) will save a lot of time in the future and makes your code maintainable. The time you invest writing tests while coding will save a lot of time in the future when you modify the code base. I'm not a theoric of software writing nor an orthodox of test driven development, but to write tests while you code is a very good practice. You can even write tests before coding ie: you write something that test a wanted behaviour, you run it expecting a failure, then you write the code that make the test happy. This is up to you.
What is not a choice is having no test suite or writing all tests at the end of code development. No.
In the day one we used module-starter
to produce a skeleton of our module. module-starter
was so kind to write a bounch of tests for us in the standard directory /t
(ie tests). Tests are run normally during the installation (sorted by their names) of the module but, as we already said, they are the main source of serenity for us as developers. So let's see what module-starter
wrote inside /t/00-load.t
#!perl -T use 5.006; use strict; use warnings; use Test::More; plan tests => 1; BEGIN { use_ok( 'Range::Validator' ) || print "Bail out!\n"; } diag( "Testing Range::Validator $Range::Validator::VERSION, Perl $], $^X" );
This perl program use strict and warnings (you already know they are friends, do you?) then load the core module Test::More which generally
requires that you declare how many tests you intend to run ( plan tests => 1
) then inside the BEGIN
block use its method use_ok
that loads our own module and in case of failure print "Bail out!\n" aka "everything went wrong, leave the boat".
If the above succeeded Test::More calls diag
that emits a note with the text specified, useful to have while reviewing test output. The module also has the note
method that I prefer. Go to the module documentation to have an idea of Test::More
So, instead of the one liner we can safely call this test:
shell> perl -I ./lib ./t/00-load.t "-T" is on the #! line, it must also be used on the command line at ./t/00-load.t line 1.
The test crash because of the -T
that turns taint mode on. Taint mode is base of the security in perl, but for the moment we do not need it enabled, so we remove from the shebang line which will result in #!perl
(read about taint mode in the official perl documentation: perlsec).
(Note that removing -T
switch is not the best thing to do: perl -T -I ./lib ./t/00-load.t
is by far a better solution).
After this change the test will run as expected:
shell> perl -I ./lib ./t/00-load.t ok 1 - use Range::Validator; 1..1 # Testing Range::Validator 0.01, Perl 5.026000, /path/to/my/perl
Wow! we run our first test! ..yes, but in the wrong way. Well not exactly the wrong way but not the way tests are run during installation. Test are run through a TAP harness (TAP stands for Test Anything Protocol and is present in perl since ever: perl born the right way ;).
With your perl distribution you have the prove command (see its documentation) that run tests through a TAP harness. So we can use it.
We can call prove the very same way we called perl: prove -I ./lib ./t/00-load.t
but we are lazy and we spot prove -l
which has the same effect of prove -I ./lib
ie include ./lib
in @INC
Run the very same test through prove instead that perl and you will see a slightly different output:
shell> prove -l ./t/00-load.t ./t/00-load.t .. 1/? # Testing Range::Validator 0.01, Perl 5.026000, /path/to/my/perl ./t/00-load.t .. ok All tests successful. Files=1, Tests=1, 0 wallclock secs ( 0.01 usr + 0.02 sys = 0.03 CPU) Result: PASS
Basically the output includes some statistics and the count of test files processed and the overall number of tests. Also note that the message emitted by diag
is in another place: diagnostics by Test::More goes to STDERR
(which is buffered differently in respect of STDOUT
but this is another story..) and TAP aggregates tests results and prints them to STDOUT
Finally we have the developer gratification: Result: PASS
indicating all went well.
The prove
program promotes laziness and without argument (as a test file in the previous example) runs automatically every test file found under /t
folder: this is the same behaviour you will have during an effective module installation:
shell> prove -l t\00-load.t ....... 1/? # Testing Range::Validator 0.01, Perl 5.026000, /path/to/my/perl t\00-load.t ....... ok t\manifest.t ...... skipped: Author tests not required for installation t\pod-coverage.t .. skipped: Author tests not required for installation t\pod.t ........... skipped: Author tests not required for installation All tests successful. Files=4, Tests=1, 1 wallclock secs ( 0.06 usr + 0.02 sys = 0.08 CPU) Result: PASS
Ok we have done some change to the code base, small ones but changes. Wich changes? I'm lazy and I do not remember all files we modified. No problem git
will tell us. At least I remember which command I need to review the code base status: git status
Go to the git shell and run it:
git-client> git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: lib/Range/Validator.pm modified: t/00-load.t no changes added to commit (use "git add" and/or "git commit -a")
Ah yes, we modified two files: not only the module also the t/00-load.t
removing the -T
from shebang line, thanks git
and you are also so kind to give me two hints about what to do next: use "git add" and/or "git commit -a"
Go for the shorter path: we commit adding all files with git commit -a
ie: we commit all files that are already tracked and eventually we remove from tracked list all files deleted in the code base. But we remember that committing needs to include a message as label of the commit: git commit -m "message"
so putting all together and checking the status:
git-client> git commit -a -m "moved POD, removed -T" [master 49a0690] moved POD, removed -T 2 files changed, 20 insertions(+), 23 deletions(-) git-client> git status On branch master nothing to commit, working tree clean
Ok we submitted, well committed, all changes made. What's next? We have to synchronize the online repository that we named YourGithubLogin
so check it and push modified content to it:
git-client>git remote -v YourGithubLogin https://github.com/YourGithubLogin/Range-Validator (fetch) YourGithubLogin https://github.com/YourGithubLogin/Range-Validator (push) git-client> git push YourGithubLogin master fatal: HttpRequestException encountered. Username for 'https://github.com': YourGithubLogin Password for 'https://[email protected]': Counting objects: 7, done. Delta compression using up to 4 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (7/7), 870 bytes | 435.00 KiB/s, done. Total 7 (delta 3), reused 0 (delta 0) remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To https://github.com/YourGithubLogin/Range-Validator 1788c12..49a0690 master -> master
Go to the browser and open the online repository to see what happened after the git push
: in the main page, where files are listed we spot our two modified files with a new timestamp and with the message we used when committing. Under the Insight tab and then under Network in the right menu, we can see two points connected by a line segment: this is the visual history of our repository and each commit we have done: here you will find also eventual branches, but this is another story.
Well, another day is passed without writing a single line of perl code! At least for the moment our code is 100% bug free ;) I vaguely recall a chinese motto: "when you start something, start from the opposite" or something like that. To write a robust perl module start writing no perl code, for two days!
It's time to put some code inside our validate
subroutine. We plan to accept both a string like '1..3,5'
and a pure range like 1..5,6
but let's start with string form assuming only one element will be passed to our sub via @_
Remember what said in the foreword: this tutorial is not about coding, so be merciful with following examples.
sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; } # otherwise we received a list else{ ... } return @range; }
The above is straightforward (if ugly): we get something in via @_
(a string or a list) and we return something via return @range
To accomplish this we initialize $range
to hold our string.
A good principle in loops is "put exit conditions early" and following this principle we put our die conditions as soon as possible, ie after the if/else check.
But we dont want to die with an ugly message like Died at ../Range/Validator.pm line x
ie from the module perspective: we want to inform the user where his code provoked our module to die.
The core module Carp provides this kind of behaviour and we use its function croak
that dies from perspective of the caller.
So we add the line needed to load the module, a first croak
call if the string passed in contains forbidden characters and some other line too:
package Range::Validator; use 5.006; use strict; use warnings; use Carp; # --- new line our $VERSION = '0.01'; sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; } # otherwise we received a list else{ ... } # remove any space from string $range =~ s/\s+//g; # --- new line # die if invalid characters croak "invalid character passed in string [$range]!" if $range =~ /[^\s,.\d]/; # --- new line @range = eval ($range); # --- new line return @range; } 1;
How to see if all works as expected? Obviously with a test. Not 00-load.t
but a new one dedicated to the validate
sub. So go into the t
folder and create a new file 01-validate.t
and open it to edit the content.
Let's populate it with a basic content plus some new stuff (01-validate.t
):
#!perl use 5.006; use strict; use warnings; use Test::More qw(no_plan); use Test::Exception; use_ok( 'Range::Validator' ); ok (scalar Range::Validator::validate('0..2') == 3, 'ok valid string produces correct number of elements' ); note ("starting test of forbidden characters in the string form"); dies_ok { Range::Validator::validate('xxxinvalidstringxxx') } "expected to die with invalid character";
First of all we used a different notation for Test::More ie. use Test::More qw(no_plan)
We are telling to the module we (still) have not a plan about how many tests will be in this file. This is a handy feature.
The Test::More core module offers us ok
use_ok
and note
methods: view the module doc for more info about them.
But we also used in the above test the dies_ok
function: this one comes from the CPAN module Test::Exception
and we need to add this module in to our dependencies list.
Dependencies list? What is that? Where we spoke about this? Never, until now.
Infact the program module-starter
used in day one created a file called Makefile.PL
with the following default content:
use 5.006; use strict; use warnings; use ExtUtils::MakeMaker; WriteMakefile( NAME => 'Range::Validator', AUTHOR => q{MyName <[email protected]>}, VERSION_FROM => 'lib/Range/Validator.pm', ABSTRACT_FROM => 'lib/Range/Validator.pm', LICENSE => 'artistic_2', PL_FILES => {}, MIN_PERL_VERSION => '5.006', CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => '0', }, BUILD_REQUIRES => { 'Test::More' => '0', }, PREREQ_PM => { #'ABC' => '1.6', #'Foo::Bar::Module' => '5.0401', }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Range-Validator-*' }, );
This file is run on the target system trying to install your module. It's vaste matter and you can find many, many useful informations in the core documentation of ExtUtils::MakeMaker and in the ExtUtils::MakeMaker::Tutorial and, as always in perl, there many ways to do it.
In our simple case we only need to know few facts about BUILD_REQUIRES
and PREREQ_PM
fields.
The first one lists into a hash all modules and their version needed to build up our module, building includes testing, so if you need some module during tests it is the place where insert dependencies. The module-starter
program added 'Test::More' => '0'
entry for us. This is the right place to state that we intend to use Test::Exception CPAN module during tests.
By other hand PREREQ_PM
lists modules and their minimal versions needed to run your module. As you can see it's a different thing: to run Range::Validator
you never need Test::Exception
but, for example you 'll need Carp
Even if Carp
it's a core module is a good practice to include it into PREREQ_PM
Read a very good post about dependencies Re: How to specify tests dependencies with Makefile.PL?
Cleaning example lines and given all the above, we will modify Makefile.PL
as follow:
use 5.006; use strict; use warnings; use ExtUtils::MakeMaker; WriteMakefile( NAME => 'Range::Validator', AUTHOR => q{MyName <[email protected]>}, VERSION_FROM => 'lib/Range/Validator.pm', ABSTRACT_FROM => 'lib/Range/Validator.pm', LICENSE => 'artistic_2', PL_FILES => {}, MIN_PERL_VERSION => '5.006', CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => '0', }, BUILD_REQUIRES => { 'Test::More' => '0', 'Test::Exception' => '0', # --- new line }, PREREQ_PM => { 'Carp' => '0', # --- new line }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Range-Validator-*' }, );
So the moral is: when you add a dependency needed to run your module or to test it remember to update Makefile.PL
correspondent part.
prove -l
but specifying also -v
to be verbose and the filename of our new test (now we dont want all test run, just the one we are working on):
shell> prove -l -v ./t/01-validate.t ./t/01-validate.t .. ok 1 - use Range::Validator; ok 2 - ok valid string produces correct number of elements # starting test of forbidden characters in the string form ok 3 - expected to die with invalid character 1..3 ok All tests successful. Files=1, Tests=3, 0 wallclock secs ( 0.05 usr + 0.01 sys = 0.06 CPU) Result: PASS
What we need more from our first day of coding? To check our status and to synchronize our online repository (pay attention to the following commands because we have a new, untracked file!):
git-client> git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: Makefile.PL modified: lib/Range/Validator.pm Untracked files: (use "git add <file>..." to include in what will be committed) t/01-validate.t no changes added to commit (use "git add" and/or "git commit -a") git-client> git commit -a -m "some code into validate and modified Makefile.PL" [master 580f628] some code into validate and modified Makefile.PL 2 files changed, 23 insertions(+), 3 deletions(-)
We committed before adding the new file! shame on us! Add the new file and issue another commit:
git-client> git add t/01-validate.t git-client> git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: t/01-validate.t git-client> git commit -a -m "added 01-validate.t" [master 5083ec3] added 01-validate.t 1 file changed, 16 insertions(+) create mode 100644 t/01-validate.t git-client> git status On branch master nothing to commit, working tree clean
What more? Ah! pushing to the online repository:
git-client> git remote -v YourGithubLogin https://github.com/YourGithubLogin/Range-Validator (fetch) YourGithubLogin https://github.com/YourGithubLogin/Range-Validator (push) git-client> git push YourGithubLogin master fatal: HttpRequestException encountered. Username for 'https://github.com': YourGithubLogin Password for 'https://[email protected]': ********* Counting objects: 10, done. Delta compression using up to 4 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (10/10), 1.31 KiB | 447.00 KiB/s, done. Total 10 (delta 5), reused 0 (delta 0) remote: Resolving deltas: 100% (5/5), completed with 4 local objects. To https://github.com/YourGithubLogin/Range-Validator 49a0690..5083ec3 master -> master
What a day! We added six lines of code and an entire test file! Are we programming too much? Probably no but we are doing it in a robust way and we discovered it can be hard work. In perl hard work is justified only by (future) laziness and we are doing all these work because we are lazy and we do not want to waste our time when, in a month or a year, we need to take this code base again to enhance it or to debug it. So now it's time for the bed and for deserved colorful dreams.
We get up in the morning and we suddenly realize yesterday we forgot something very important: documentation! Good documentation is like an educated people, while poor documentation is like a boor one: who do you prefer to meet?
The same is for your module users: they hope and expect to find a good documentation and to write it is our duty. Dot.
Documentation content, in my little experience, can be impacted a lot for even small changes in the code or interface so, generally I write the larger part of the docs when the implementation or interface is well shaped. But, by other hand, a good approach is to put in the docs every little statement that will be true since the very beginning of your module development. At the moment we can state our validate
sub accepts both strings and ranges and always returns an array.
At the moment the relevant part of the POD documentation is:
=head1 EXPORT A list of functions that can be exported. You can delete this section if you don't export anything, such as for a purely object-oriented module. =head1 SUBROUTINES/METHODS =head2 validate =cut
We do not plan to export functions: our sub must be called via its fully qualified name, as we do in the test we created: Range::Validator::validate()
so we can delete the EXPORT part and add something in the subroutines part:
=head1 SUBROUTINES =head2 validate This function accepts a string or a list (range) and returns an array. In the string form the accepted characters are: positive integers, dots, commas and spaces. Every space will be removed.
We do not need =cut
anymore because we do not have POD in blocks interleaved with the code, but an unique POD block in the __DATA__
section.
Since we are now very fast with git
commands, let's commit
this little change; the push
to the remote repository can be left for the end of work session. So status
(check it frequently!) and commit
git-client> git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: lib/Range/Validator.pm no changes added to commit (use "git add" and/or "git commit -a") git-client> git commit -a -m "initial POD to document validate function" [master a6dc557] initial POD to document validate function 1 file changed, 5 insertions(+), 6 deletions(-)
Now it's time to add more checks for the incoming string: we do not accept a lone dot between non dots, nor even more than two dots consecutively:
# not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /[^.]+\.{1}[^.]+/; # not allowed more than 2 . croak "invalid range [$range] (more than 2 .)!" if $range =~ /[^.]+\.{3}/;
The whole sub now look like:
sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; } # otherwise we received a list else{ ... } # remove any space from string $range =~ s/\s+//g; # die if invalid characters croak "invalid character passed in string [$range]!" if $range =~ /[^\s,.\d]/; # not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /[^.]+\.{1}[^.]+/; # not allowed more than 2 . croak "invalid range [$range] (more than 2 .)!" if $range =~ /[^.]+\.{3}/; @range = eval ($range); return @range; }
Now it is time to test this behaviour. Go edit ./t/01-validate.t
adding (append the following code to the end of the test file) some dies_ok
statements preceded by a note
:
note ("start checks about incorrect dots in string"); dies_ok { Range::Validator::validate('1.2') } "expected to die with a lone dot"; dies_ok { Range::Validator::validate('0..2,5.6,8') } "expected to die with a lone dot";
Run the test via prove
shell> prove -l -v ./t/01-validate.t ./t/01-validate.t .. ok 1 - use Range::Validator; ok 2 - ok valid string produces correct number of elements # starting test of forbidden characters in the string form ok 3 - expected to die with invalid character # start checks about incorrect dots in string ok 4 - expected to die with a lone dot ok 5 - expected to die with a lone dot 1..5 ok All tests successful. Files=1, Tests=5, 0 wallclock secs ( 0.06 usr + 0.03 sys = 0.09 CPU) Result: PASS
Fine! But.. to much repetitions in the test code. Are not we expected to be DRY (Dont Repeat Yourself)? Yes we are and since we have been so lazy to put use Test::More qw(no_plan)
we can add a good loop of tests (replace the last two dies_ok
with the following code in the test file):
foreach my $string ( '1.2', '0..2,5.6,8', '1,2,.,3', '.' ){ dies_ok { Range::Validator::validate( $string ) } "expected to die with a lone dot in range [$string]"; }
Run the test again:
shell> prove -l -v ./t/01-validate.t ./t/01-validate.t .. ok 1 - use Range::Validator; ok 2 - ok valid string produces correct number of elements # Failed test 'expected to die with a lone dot in range [.]' # starting test of forbidden characters in the string form # at ./t/01-validate.t line 22. ok 3 - expected to die with invalid character # Looks like you failed 1 test of 7. # start checks about incorrect dots in string ok 4 - expected to die with a lone dot in range [1.2] ok 5 - expected to die with a lone dot in range [0..2,5.6,8] ok 6 - expected to die with a lone dot in range [1,2,.,3] not ok 7 - expected to die with a lone dot in range [.] 1..7 Dubious, test returned 1 (wstat 256, 0x100) Failed 1/7 subtests Test Summary Report ------------------- ./t/01-validate.t (Wstat: 256 Tests: 7 Failed: 1) Failed test: 7 Non-zero exit status: 1 Files=1, Tests=7, 0 wallclock secs ( 0.02 usr + 0.03 sys = 0.05 CPU) Result: FAIL
FAIL? Fortunately we shown the current range passed in the text generated by the test, so go examine it:
not ok 7 - expected to die with a lone dot in range [.]
Spot why it fails (ie it does not die as expected)? Our regex to check a lone dot is:
/[^.]+.{1}[^.]+/
And it reads (as per the YAPE::Regex::Explain
output): any character except: '.' (1 or more times (matching the most amount possible)) followed by '.' (1 times) followed by any character except: '.' (1 or more times (matching the most amount possible))
Which is simply not true for the given string '.'
So we try changing both plus signs with question mark quantifiers in the regex: it does not help. As a wise friend explains, we need lookaround: /(?<!\.)\.(?!\.)/
will work! So we change the check in the module like follow:
# not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /(?<!\.)\.(?!\.)/;
As we spot this edge case we add two similar ones to the test:
foreach my $string ( '1.2', '0..2,5.6,8', '1,2,.,3', '.', '1.', '.1' ){ dies_ok { Range::Validator::validate( $string ) } "expected to die with a lone dot in range [$string]"; }
Also the next regex (aimed to search for three dots) in the module is not working for the very same reason; change it from /[^.]+\.{3}/
to simply /\.{3}/
The moral? Tests are your friends! We spot, by hazard, an edge case and our code must be able to deal with it, so free as much your fantasy writing your tests. Cockroaches come from box corners.. ops, no I mean: bugs come from edge case.
Now we add some test to spot, and die, if three dots are found, with the new simpler regex /\.{3}/
so we change the code adding the following code to the test:
foreach my $newstring ( '1...3', '1,3...5','...', '1...', '...2' ){ dies_ok { Range::Validator::validate( $newstring ) } "expected to die with three dots in range [$newstring]"; }
We run the test:
shell> prove -l -v ./t/01-validate.t ./t/01-validate.t .. ok 1 - use Range::Validator; ok 2 - ok valid string produces correct number of elements # starting test of forbidden characters in the string form ok 3 - expected to die with invalid character # start checks about incorrect dots in string ok 4 - expected to die with a lone dot in range [1.2] ok 5 - expected to die with a lone dot in range [0..2,5.6,8] ok 6 - expected to die with a lone dot in range [1,2,.,3] ok 7 - expected to die with a lone dot in range [.] ok 8 - expected to die with a lone dot in range [1.] ok 9 - expected to die with a lone dot in range [.1] ok 10 - expected to die with three dots in range [1...3] ok 11 - expected to die with three dots in range [1,3...5] ok 12 - expected to die with three dots in range [...] ok 13 - expected to die with three dots in range [1...] ok 14 - expected to die with three dots in range [...2] 1..14 ok All tests successful. Files=1, Tests=14, 1 wallclock secs ( 0.03 usr + 0.03 sys = 0.06 CPU) Result: PASS
So now your sub is:
sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; } # otherwise we received a list else{ ... } # remove any space from string $range =~ s/\s+//g; # die if invalid characters croak "invalid character passed in string [$range]!" if $range =~ /[^\s,.\d]/; # not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /(?<!\.)\.(?!\.)/; # not allowed more than 2 . croak "invalid range [$range] (more than 2 .)!" if $range =~ /\.{3}/; @range = eval ($range); return @range; }
And our test file ./t/01-validate.t
as follow:
#!perl use 5.006; use strict; use warnings; use Test::More qw(no_plan); use Test::Exception; use_ok( 'Range::Validator' ); ok (scalar Range::Validator::validate('0..2') == 3, 'ok valid string produces correct number of elements' ); note ("starting test of forbidden characters in the string form"); dies_ok { Range::Validator::validate('xxxinvalidstringxxx') } "expected to die with invalid character"; note ("start checks about incorrect dots in string"); foreach my $string ( '1.2', '0..2,5.6,8', '1,2,.,3', '.', '1.', '.1' ){ dies_ok { Range::Validator::validate( $string ) } "expected to die with a lone dot in range [$string]"; } foreach my $newstring ( '1...3', '1,3...5','...', '1...', '...2' ){ dies_ok { Range::Validator::validate( $newstring ) } "expected to die with three dots in range [$newstring]"; }Time to review the status of the local repository, commit changes and push it online:
git-client> git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: lib/Range/Validator.pm modified: t/01-validate.t no changes added to commit (use "git add" and/or "git commit -a") git-client> git commit -a -m "changed regexes for 2 o lone dot and relative tests" [master 169809c] changed regexes for 2 o lone dot and relative tests 2 files changed, 17 insertions(+), 1 deletion(-) git-client> git push YourGithubLogin master fatal: HttpRequestException encountered. Username for 'https://github.com': YourGithubLogin Password for 'https://[email protected]': Counting objects: 12, done. Delta compression using up to 4 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (12/12), 1.50 KiB | 385.00 KiB/s, done. Total 12 (delta 5), reused 0 (delta 0) remote: Resolving deltas: 100% (5/5), completed with 3 local objects. To https://github.com/YourGithubLogin/Range-Validator 5083ec3..169809c master -> master
Today we committed twice, do you remember? first time just the POD we added for the sub and second time just few moments ago. We pushed just one time. What's really now in the online repository?
Go to the online repository, Insights, Network: the last two dots on the line segment are our two commits, pushed together in a single push. Handy, no? Click on the second-last dot and you will see the detail of the commit concerning the POD, with lines we removed in red and lines we added in green. Commits are free: committing small changes frequently is better than commit a lot of changes all together.
Today we plan to add two new validations in our sub: first one intended to be used against ranges passed in string form, the second to all ranges, before returning them.
The constraint for the string form is about reversed ranges, like 3..1
and it is added just after the last croak
we added yesterday:
# spot reverse ranges like 27..5 if ($range =~ /[^.]\.\.[^.]/){ foreach my $match ( $range=~/(\d+\.\.\d+)/g ){ $match=~/(\d+)\.\.(\d+)/; croak "$1 > $2 in range [$range]" if $1 > $2; } }
Now is important that we take the habit to commit on our own, whenever we add an atomic piece of code: so go to commit! From now on not every git operation will be shown with full output, only important ones (is this a git guide? No!).
The other one, applied before returning the range as array, is about overlapping ranges: (0..2,1)
that is equivalent to 0,1,1,2
with a nasty repetition terrible for the rest of the code outside the present module (this assumption is related to our current, fictional, scenario).
So, just before returning from the sub, we simply use a hash to have unique elements in the resulting array:
# eval the range @range = eval ($range); # remove duplicate elements using a hash my %single = map{ $_ => 1} @range; # -- new line # sort unique keys numerically @range = sort{ $a <=> $b } keys %single; # -- new line return @range;
As previously said, commit on your own, with a meaningful comment.
You end with:
sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; } # otherwise we received a list else{ ... } # remove any space from string $range =~ s/\s+//g; # die if invalid characters croak "invalid character passed in string [$range]!" if $range =~ /[^\s,.\d]/; # not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /(?<!\.)\.(?!\.)/; # not allowed more than 2 . croak "invalid range [$range] (more than 2 .)!" if $range =~ /\.{3}/; # spot reverse ranges like 27..5 if ($range =~ /[^.]\.\.[^.]/){ foreach my $match ( $range=~/(\d+\.\.\d+)/g ){ $match=~/(\d+)\.\.(\d+)/; croak "$1 > $2 in range [$range]" if $1 > $2; } } # eval the range @range = eval ($range); # remove duplicate elements using a hash my %single = map{ $_ => 1} @range; # sort unique keys numerically @range = sort{ $a <=> $b } keys %single; return @range; }
New features are worth to be pushed on the online repository: you know how can be done. Do it.
Did you follow my small advices about git committing and meaningful messages? If so it's time to see why is better to be diligent: with git log
(which man page is probably longer than this guide..) you can review a lot about previous activities:
git-client> git log HEAD --oneline bb952ee (HEAD -> master, YourGithubLogin/master) removing duplicates from overlapping ranges 15a5f63 check for reverse ranges in string form 169809c changed regexes for 2 o lone dot and relative tests a6dc557 initial POD to document validate function 5083ec3 added 01-validate.t 580f628 some code into validate, added 01-validate.t and modified Makefile.PL 49a0690 moved POD, removed -T 1788c12 module-starter created content
This is definitevely handy. HEAD
is where your activity is focused in this moment. Try to remove the --oneline
switch to see also all users and dates of each commit.
As you can understand git is a vaste world: explore it to suit your needs. This is not a git guide ;)
Until now we used a limited test arsenal: ok
from Test::Simple
use_ok
and note
from Test::More
dies_ok
from Test::Exception
As we added a croak
in our sub to prevent reversed ranges, we can use dies_ok
again to check such situations (append the following code to our test file):
foreach my $reversed ('3..1,7..9','1..4,7..5','3..4, 7..5','0..2,27..5'){ dies_ok { Range::Validator::validate( $reversed ) } "expected to die with reverse range [$reversed]"; }
Commit at your will.
Test::More
has a lot of useful testing facilities (review the module documentation to take inspiration) and now we will use is_deeply
to implement some positive test about expected and returned data structures.
This is useful, in our case, to test that overlapping ranges or unordered ones are returned corrected. To do this we can use a hash of inputs and their expected returned values (append the following code to our test file):
my %test = ( '1,1..3' => [(1,2,3)], '1,2..5,4' => [(1,2,3,4,5)], '1..5,3' => [(1,2,3,4,5)], '8,9,1..2' => [(1,2,8,9)], '1..3,3,5..7' => [(1,2,3,5,6,7)], '5..7,1..6' => [(1,2,3,4,5,6,7)], '0..5,3' => [(0,1,2,3,4,5)] ); # ranges, even if overlapped or unordered, return the correct array foreach my $range ( keys %test ){ my @res = Range::Validator::validate($range); is_deeply( $test{$range},\@res, "correct result for range [$range]" ); }
Last two tests we added will produce the following output:
ok 15 - expected to die with reverse range [3..1,7..9] ok 16 - expected to die with reverse range [1..4,7..5] ok 17 - expected to die with reverse range [3..4, 7..5] ok 18 - expected to die with reverse range [0..2,27..5] ok 19 - correct result for range [1,2..5,4] ok 20 - correct result for range [1,1..3] ok 21 - correct result for range [0..5,3] ok 22 - correct result for range [5..7,1..6] ok 23 - correct result for range [1..3,3,5..7] ok 24 - correct result for range [8,9,1..2] ok 25 - correct result for range [1..5,3]
Commit.
Just at glance: up to you to explore this topic. Look at the following git session of commands, two commands ( one that we never used until now) just before and reissued just after the push:
git-client> git log HEAD --oneline c3f8d5b (HEAD -> master) test for overlappped or unordered ranges f16789a test for reversed ranges bb952ee (YourGithubLogin/master) removing duplicates from overlapping ranges 15a5f63 check for reverse ranges in string form 169809c changed regexes for 2 o lone dot and relative tests a6dc557 initial POD to document validate function 5083ec3 added 01-validate.t 580f628 some code into validate, added 01-validate.t and modified Makefile.PL 49a0690 moved POD, removed -T 1788c12 module-starter created content git-client> git show-branch *master * [master] test for overlappped or unordered ranges ! [refs/remotes/YourGithubLogin/master] removing duplicates from overlapping ranges -- * [master] test for overlappped or unordered ranges * [master^] test for reversed ranges *+ [refs/remotes/YourGithubLogin/master] removing duplicates from overlapping ranges git-client> git push YourGithubLogin master fatal: HttpRequestException encountered. Username for 'https://github.com': YourGithubLogin Password for 'https://[email protected]': Counting objects: 8, done. Delta compression using up to 4 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (8/8), 1011 bytes | 505.00 KiB/s, done. Total 8 (delta 6), reused 0 (delta 0) remote: Resolving deltas: 100% (6/6), completed with 3 local objects. To https://github.com/YourGithubLogin/Range-Validator bb952ee..c3f8d5b master -> master git-client> git log HEAD --oneline c3f8d5b (HEAD -> master, YourGithubLogin/master) test for overlappped or unordered ranges f16789a test for reversed ranges bb952ee removing duplicates from overlapping ranges 15a5f63 check for reverse ranges in string form 169809c changed regexes for 2 o lone dot and relative tests a6dc557 initial POD to document validate function 5083ec3 added 01-validate.t 580f628 some code into validate, added 01-validate.t and modified Makefile.PL 49a0690 moved POD, removed -T 1788c12 module-starter created content git-client> git show-branch *master * [master] test for overlappped or unordered ranges ! [refs/remotes/YourGithubLogin/master] test for overlappped or unordered ranges -- *+ [master] test for overlappped or unordered ranges
We just need some small change and our module will be ready for production. We left some part of our code behind and precisely the else
part dedicated to incoming arrays:
# otherwise we received a list else{ ... }
and we can just fill it with @range = @_;
and commit the change.
But if the above is true, we have to move all string check inside the if ( @_ == 1) {...
block! Do it and commit the change. Now our sub is like the following one:
sub validate{ my $range; my @range; # assume we have a string if we receive only one argument if ( @_ == 1){ $range = $_[0]; # remove any space from string $range =~ s/\s+//g; # die if invalid characters croak "invalid character passed in string [$range]!" if $range =~ /[^\s,.\d]/; # not allowed a lone . croak "invalid range [$range] (single .)!" if $range =~ /(?<!\.)\.(?!\.)/; # not allowed more than 2 . croak "invalid range [$range] (more than 2 .)!" if $range =~ /\.{3}/; # spot reverse ranges like 27..5 if ($range =~ /[^.]\.\.[^.]/){ foreach my $match ( $range=~/(\d+\.\.\d+)/g ){ $match=~/(\d+)\.\.(\d+)/; croak "$1 > $2 in range [$range]" if $1 > $2; } } # eval the range @range = eval ($range); } # otherwise we received a list else{ @range = @_; } # remove duplicate elements using a hash my %single = map{ $_ => 1} @range; # sort unique keys numerically @range = sort{ $a <=> $b } keys %single; return @range; }
Is our change safe? Well we have a test suit: prove -l -v
will tell you if the change impacts the test suit (if tests are poor you can never be sure).
Now our module is ready for production. It just lacks of some good documentation. Not a big deal, but is our duty to document what the sub does effectively.
Add to the POD of our sub:
Every string with occurences of a lone dot or more than two dots will be rejected causing an exception in the calling program.
Reverse ranges like in '3..1'
passed as string will also cause an exception.
In both string and list form any duplicate element (overlapped range) will be silently removed. Any form of unordered list will be silently rerodered.
Check git status and commit. Use git log HEAD --oneline
to see that the local repository is three steps ahead of the remote one. Push the changes in the online repository. Use git log HEAD --oneline
again to see what happened.
Even if it is simpler we have to test the array form of our sub. We can use this time an array of tests each element being another array with two elements: first the list we pass to the sub, then the list we expect back from the sub. Again using is_deeply
is a good choice:
Add the following test to our file 01-validate.t
note ("starting test of list form"); my @test = ( # passed expected # correct ones [ [(0..3)], [(0,1,2,3)] ], [ [(0,1..3)], [(0,1,2,3)] ], [ [(0..3,5)], [(0,1,2,3,5)] ], # overlapped ones [ [(0..3,2)], [(0,1,2,3)] ], [ [(1,0..3)], [(0,1,2,3)] ], [ [(0..3,1..2)], [(0,1,2,3)] ], ); foreach my $list ( @test ){ my @res = Range::Validator::validate( @{$list->[0]} ); is_deeply( \@{$list->[1]},\@res, "correct result for list: @{$list->[0]}" ); }
Run the test: we reached the big number of 32 succesful test! Congratulations!
As always, commit the change with a meaningful comment and push this important set of changes to the online repository.
Our assumptions, in "day zero - the plan", were to accept only ordered, not overlapped lists or their string representations. Other software in the project (again: a fictional scenario), where our validation module is used, blindly pass what received from outside (many different sources) to our validate
sub. With the output produced by our sub many other subs or methods are called. All these software (out of our control) assume that, if an empty list is received then ALL elements are processed. This seemed the right thing to do.
After the advent of our module some example usage can be:
# all actions performed, no need to call our validate sub actions_to_activate_account(); # only reset password and send mail with new password my @valid_range = Range::Validator::validate(3,12); actions_to_activate_account( @valid_range ); # or in the string form: my $action_string = get_action_string_from_DB( actions => 'reset_pwd' ); # $action_string is '3,12' my @valid_range = Range::Validator::validate( $action_string ); actions_to_activate_account( @valid_range ); # or in the array form: my @actions = get_action_list_from_DB( actions => 'reset_pwd' ); # @actions is (3,12) my @valid_range = Range::Validator::validate( @actions ); actions_to_activate_account( @valid_range );
Right? The module goes in production and 98% of errors from the foreign part of the code base disappeared. Only 98%? Yes..
Miss A of department Z call your boss in a berserk state: not all their errors are gone away. They use the list form but Miss A and the developer B are sure no empty lists are passed to your validate sub. You call the developer B, a good fellow, who explain you that list are generated from a database field that cannot be empty (NOT NULL constraint in the database):
You - Listen B, if I emit a warning you'll be able to trap which list generated from the database provoked it?
B - Sure! Can you add this?
You - Yes, for sure. I can use a variable in Range::Validator namespace, let's name it warnings
and you'll set it to a true value and only you, and not the rest of the company, will see errors on STDERR. Ok?
B - Fantastic! I'll add the variable as soon as you tell me.
You - Ok, but then I want to know which list provoked the error, right? For a coffee?
B - Yeah man, for a coffee, as always.
So we add a line in the top of the module, just after VERSION: our $WARNINGS = 0;
to let dev B to trigger our warnings. We commit even this small change.
Then we add to the sub a carp
call triggered if $WARNINGS == 1
and if @_ == 0
and we add this as elsif
condition:
# assume we have a string if we receive only one argument if ( @_ == 1){ # STRING PART ... } elsif ( $WARNINGS == 1 and @_ == 0 ){ carp "Empty list passed in! We assume all element will be processed."; } # otherwise we received a list else{ # NON EMPTY LIST PART ... }
Git status, git commit on your own.
To grab STDERR
in test we have to add a dependency to Capture::Tiny
module which is able, with its method capture
to catch STDOUT
STDERR
and results emitted by an external command or a chunk of perl code. Handy and tiny module.
Do you remeber the place to specify a dependency? Bravo! Is in Makefile.PL
and we did the same in "day three step 3" when we added two modules to the BUILD_REQUIRES
hash. Now we add Capture::Tiny
to this part (remeber to specify module name in a quoted string):
BUILD_REQUIRES => { 'Test::More' => '0', 'Test::Exception' => '0', 'Capture::Tiny' => '0', # -- new line },
Commit this change.
Now in 01-validate.t
test file we first add the module with use Capture::Tiny qw(capture)
and then, at the end we add some test of the warning behaviour:
note ("test of warnings emitted"); { local $Range::Validator::WARNINGS; my ($stdout, $stderr, @result) = capture { Range::Validator::validate() }; unlike($stderr, qr/^Empty list passed in/, "no warning for empty list unless \$Range::Validator::WARNINGS"); $Range::Validator::WARNINGS = 1; ($stdout, $stderr, @result) = capture { Range::Validator::validate() }; like( $stderr, qr/^Empty list passed in/, "right warning for empty list if \$Range::Validator::WARNINGS"); }
Run the test suit, commit this change. Use git log HEAD --oneline
to visualize our progresses and push all recent commits to the online repository.
The new version goes in production. The good fellow calls you:
B - Ehy, we added your warnings..
You - And..?
B - We spotted our errors in the database..
You - And which kind of error?
B - Well.. do you know what perl sees when it spot 1, followed by FOUR dots, followed by 3?
You - Ahh aha ah ah.. unbelievable! Yes, I suppose it parses as: from 1 to .3 aka nothing, aka empty list..
B - Exactly! Can you imagine my boss face?
You - I dont want! A coffee is waiting for you. Thanks!
Add some POD,few lines are better than nothing, to the module documentation:
=head1 ENABLE WARNINGS If the$Range::Validator::WARNINGS
is set to a true value then an empty list passed tovalidate
will provoke a warning from the caller perspective.
Commit this change and update the online repository.
As stated in "day zero - the plan" sharing early is a good principle: can be worth to ask in a forum dedicated to Perl (like perlmonks.org) posting a RFC post (Request For Comments) or using the dedicated website http://prepan.org/ to collect suggestions about your module idea and implementation.
Your module is ready to be used and it is already used, but is not installable by a CPAN client nor can be indexed by a CPAN indexer at the moment. Read the short but complete description of possible files at What are the files in a CPAN distribution?
To create a properly structured CPAN distribution you must run perl Makefile.PL
then make dist
, this will create a tarball in the current directory that you can share and install using a CPAN client, or even upload to CPAN.
Following tests are not needed to install or use your module but to help you spotting what can be wrong in your distribution.
In the the "day one - preparing the ground" we used module-starter
to create the base of our module. Under the /t
folder the program put three test we did not seen until now: manifest.t
pod-coverage.t
and pod.t
These three tests are here for us and they will help us to check our module distribution is complete. Let's start from the first
shell> prove ./t/manifest.t ./t/manifest.t .. skipped: Author tests not required for installation Files=1, Tests=0, 0 wallclock secs ( 0.03 usr + 0.01 sys = 0.05 CPU) Result: NOTESTS
Ok, no test run, just skipped. Go to view what is inside the test: it skips all actions unless RELEASE_TESTING
environment variable is set. It also will complain unless a minimal version of Test::CheckManifest
is installed. So set this variable in the shell (how to do this depends on your operating system: linux users probably need export RELEASE_TESTING=1
while windows ones will use set RELEASE_TESTING=1
) and use your CPAN client to install the required module (normally cpan Test::CheckManifest
is all you need) and rerun the test again:
shell> prove ./t/manifest.t ./t/manifest.t .. # Failed test at ./t/manifest.t line 15. ./t/manifest.t .. 1/1 # got: 0 # expected: 1 # The following files are not named in the MANIFEST file: .... # MANY LINES MORE..
Omg?! What is all that output? The test complains about a lot of files that are present in the filesystem, in our module folder but are not specified in the MANIFEST
file. This file contains a list (one per line) of files contained within the tarball of your module.
In the above output we have seen a lot, if not all, files under the .git
directory. Obviously we do not want them included in our module distribution. How can we skip them? Using MANIFEST.SKIP
file that basically contains regular expressions describing which files should be excluded from the distribution.
So go create this file in the main folder of the module and add inside it a line with a regex saying we do not want the .git
directory:
^.git/
and add this file with git add MANIFEST.SKIP
and commit this important change.
Rerun the test (added some newlines for readability):
shell> prove ./t/manifest.t ./t/manifest.t .. 1/1 # Failed test at ./t/manifest.t line 15. # got: 0 # expected: 1 # The following files are not named in the MANIFEST file: /path/to/your/module/ignore.txt, /path/to/your/module/MANIFEST.SKIP, /path/to/your/module/t/01-validate.t, /path/to/your/module/xt/boilerplate.t ...
By far better: the test points us to two files we for sure need to include in MANIFEST
and precisely: MANIFEST.SKIP
and t/01-validate.t
Use the make manifest
command to add them automatically (after running perl Makefile.PL
if necessary), then commit the change. A note for strawberry perl users: pay attention to your flavour of make
infact you probably have dmake
shipped with your distribution. If you rerun the above test you'll see files added to MANIFEST
are no more present in the failure output.
Let's examine the remaining two files. What is ignore.txt
? It was created as default ignore list by module-starter
and it contains many lines of regexes. If we want module-starter
to create MANIFEST.SKIP
instead, next time we'll use it specify --ignores='manifest'
For the moment we can delete it. Commit.
If you rerun the test you now see only /xt/boilerplate.t
and, if you open it, you'll see that is just checking if you left some default text in our module, texts put by module-starter
Ah! useful test: let's run it:
shell> prove ./xt/boilerplate.t ./xt/boilerplate.t .. ok All tests successful. Test Summary Report ------------------- ./xt/boilerplate.t (Wstat: 0 Tests: 3 Failed: 0) TODO passed: 3 Files=1, Tests=3, 1 wallclock secs ( 0.03 usr + 0.03 sys = 0.06 CPU) Result: PASS
Ok no boilerplate garbage left. We can delete this test file and commit the change.
Now, finally:
shell> prove ./t/manifest.t ./t/manifest.t .. ok All tests successful. Files=1, Tests=1, 1 wallclock secs ( 0.02 usr + 0.03 sys = 0.05 CPU) Result: PASS
Push recent changes into the online repository.
In our /t
folder we still have two tests we did not run: shame! module-starter
created for us pod.t
and pod-coverage.t
The first one checks every POD in our distribution has no errors and the second ensures that all relevant files in your distribution are appropriately documented in POD documentation. Thanks for this. Run them (after installing the necessary modules Test::Pod
and Test::Pod::Coverage
):
shell> prove -l -v ./t/pod.t ./t/pod.t .. 1..1 ok 1 - POD test for lib/Range/Validator.pm ok All tests successful. Files=1, Tests=1, 0 wallclock secs ( 0.03 usr + 0.02 sys = 0.05 CPU) Result: PASS shell> prove -l -v ./t/pod-coverage.t ./t/pod-coverage.t .. 1..1 ok 1 - Pod coverage on Range::Validator ok All tests successful. Files=1, Tests=1, 0 wallclock secs ( 0.03 usr + 0.02 sys = 0.05 CPU) Result: PASS
The README
must contain some general information about the module. Users can read this file via cpan
client so put a minimal description in it. Gihub website use it as default page, so it is useful have some meningful text. Someone generates the text from the POD section of the module. Put a short description, maybe the sysnopsis and commit the change. Push it online.
Now we can proudly look at our commits history in a --reverse
order:
git-client> git log HEAD --oneline --reverse 1788c12 module-starter created content 49a0690 moved POD, removed -T 580f628 some code into validate, added 01-validate.t and modified Makefile.PL 5083ec3 added 01-validate.t a6dc557 initial POD to document validate function 169809c changed regexes for 2 o lone dot and relative tests 15a5f63 check for reverse ranges in string form bb952ee removing duplicates from overlapping ranges f16789a test for reversed ranges c3f8d5b test for overlappped or unordered ranges 89174fe in the else block @range = @_ 58dbb12 moved string checks into the if (@_ == 1) block 8697b87 added POD for all string and list cecks e4f8eb1 added tests for lists 3efd7ce added $WARNING = 0 a46d6fc elsif block to catch empty @_ and carping under request 3e5993d Capture::Tiny in Makefile.pl ac22e82 test for warnings emitted 9667e22 POD for warnings 13f53eb MANIFEST.SKIP first line 99ab999 MANIFEST added MANIFEST.SKIP and 01-validate.t 61b7c9f removed ignore.txt e3feb61 removed xt/boilerplate.t 3c0da4f (HEAD -> master, YourGithubLogin/master) modified README
A good glance of two dozens of commits! We have done a good job, even if with some errors: committing multiple changes in different part of the project (like in our third commit) is not wise: better atomical commits. We have also some typo in commit messages..
It's now time to see if our module can be installaed by a cpan client. Nothing easier: if you are in the distribution folder extracted from a tarball created by make dist
, just run cpan .
and enjoy the output (note that this command will modify the content of the directory!).
Until now we discussed the very minimum you have to do to be polite with other people, making your module. There is much more you can do to have a quality distribution. Look at wonderful checklist proposed by Release::Checklist being part of the official perl toolchain workgroup. Just an example: install Module::CPANTS::Analyse and its frontend App::CPANTS::Lint to check the overall quality, well Kwalitee, of your distribution.
This is option we choosed for the above example and, even if it is the less favorable one, we used this form for the extreme easy. The module is just a container of subs and all subs are available in the program that uses our module but only using their fully qualified name, ie including the name space where they are defined: Range::Validator::validate
was the syntax we used all over the tutorial.
Nothing bad if the above behaviour is all you need.
If you need more control over what to be available to the end user of your module Exporter
CORE module will be a better approach.
Go read the module documentation to have an idea of its usage.
You can leverage what to export into the program using your module, so no more fully qualified package name will be needed. I suggest you to no export nothing by default (ie leaving @EXPORT
empty) using instead @EXPORT_OK
to let the end user to import sub from your module on, explicit, request.
With the use of Exporter
you can also export variables into the program using your module, not only subs. It's up to you to decide if this is the right thing to do. Pay attention with names you use and the risk of name collision: what will happen if two module export two function with the same name?
Perl is not restrictive in any meaning of the word: nothing will prevent the end user of your module to call Your::Module::not_exported_at_all_sub()
and access its functionality. A fully qualified name will be always available. The end user is breaking the API you provide, API where not_exported_at_all_sub
is not even mentioned.
Preferred by many is the Object Oriented (OO) way. OO it's not better nor worst: it's a matter of aptitude or a matter of needs. See the relevant section on the core documentation: To-OO-or-not-OO? about the choice.
An object is just a little data structure that knows the class (the package) it belongs to. Nothing more complex than this. The data structure is generally a hash and its consciouness of its class (package) is provided by the bless
core function.
Your API will just provide a constructor (conventionally new
) and a serie of methods this object can use.
Again: nothing prevents end user to call one of your function by its fully qualified name as in Your::Module::_not_exported_at_all_sub()
it's just matter of being polite.
The core documentation include some tutorial about objects:
Many perl authors nowadays use specialized modules to build up OO projects: Moose
module or the lighter flavor one Moo
notably.
An OO module has many advantages in particular situations but is generally a bit slower than other module techniques.
Until now we modified the BUILD_REQUIRES
to specify dependencies needed while testing our module and PREREQ_PM
to include modules needed by our module to effectively run.
The file format is described in the documentation of ExtUtils::MakeMaker
where is stated that, since version 6.64 it is available another field:
defined as: "A hash of modules that are needed to test your module but not run or build it". This is exactly what we need, but this force us to specify, also in TEST_REQUIRES
Makefile.PL
that we need 'ExtUtils::MakeMaker' => '6.64'
in the CONFIGURE_REQUIRES
hash.
The 6.64 version of ExtUtils::MakeMaker
was released in 2012 but you cannot be sure end users have some modern perl, so we can safely use BUILD_REQUIRES
as always or use some logic to fallback to "older" functionality if ExtUtils::MakeMaker
is too old. You can use WriteMakefile1
sub used in the Makefile.PL
of App::EUMM::Upgrade
In the current tutorial we used Test::Exception
to test failures: consider also Test::Fatal
Overkill for simple test case but useful for complex one is the module Test::Class
Other modules worth to see are in the Task::Kensho list.
If in your tests you have the risk of code repetition (against the DRY - Dont Repeat Yourself principle) you can find handy to have a module only used in your tests, a module under the /t
folder.
You need some precautions, though.
Let's assume you plan a test helper module named testhelper
contained in the /t/testhelper.pm
file used by many of your test files.
You dont want CPAN to index your testhelper
module and, to do so, you can put a no_index
directive into your main META.yml
or META.json
file (or both). You can also use a trick in your package definition inserting a newline between package
declaration and the name of the package. Like in:
package # hide from CPAN indexer testhelper;
In all tests you want to use that helper module you must:
use lib '.'; use t::testhelper;
- https://perldoc.perl.org/perlmod.html
- https://perldoc.perl.org/perlnewmod.html
- https://perldoc.perl.org/perlmodstyle.html
- http://perldoc.perl.org/perlobj.html
- http://perldoc.perl.org/perlootut.html
- https://perldoc.perl.org/Test.html
- https://perldoc.perl.org/Test/Simple.html
- https://perldoc.perl.org/Test/More.html
- https://perldoc.perl.org/prove.html
- other CORE modules under the Test:: namespace
- modules under the TAP:: namespace
- https://www.perlmonks.org/index.pl?node_id=102347 - Simple Module Tutorial
- https://www.perlmonks.org/index.pl?node_id=431702 - José's Guide for creating Perl modules
- https://www.perlmonks.org/index.pl?node_id=418891 - Writing Solid CPAN Modules
- https://www.perlmonks.org/index.pl?node_id=553487 - On Interfaces and APIs
- https://www.perlmonks.org/index.pl?node_id=158999 - How to make a CPAN Module Distribution
- https://www.perlmonks.org/index.pl?node_id=1009586 - What are the files in a CPAN distribution?
- http://tynovsky.github.io/cpan-tutorials/
- http://modernperlbooks.com/books/modern_perl_2016/07-object-oriented-perl.html
- https://metacpan.org/pod/Module%3A%3AStarter
- https://www.perlmonks.org/index.pl?node_id=11106010 a quick and well done introduction
- http://modernperlbooks.com/books/modern_perl_2016/09-managing-real-programs.html
- http://shop.oreilly.com/product/9780596100926.do
- https://qa.perl.org/testing/
- http://petdance.com/perl/automated-testing/
- https://www.perlmonks.org/index.pl?node_id=1195817 - Test Driven Development, for software and for pancakes
As all my works the present tutorial would be not possible without the help of the perlmonks.org community. Not being an exhaustive list I want to thanks: Corion, choroba, Tux, 1nickt, marto, hippo, haukex, Eily, eyepopslikeamosquito, davido and kschwab (to be updated ;)