Testing scripts in your distribution, portably

This is a summary of the things I had to do, to add a simple test case for a script that I added to one of my distributions. It's taken over 2 elapsed days, 3 developer releases, and two sessions on IRC, to get to the end. I'm writing this up (a) so I don't forget, and (b) in case it's useful to someone else, and (c) in case the peanut gallery can point out any further gotchas I've missed.

My module Module::Path just takes a module name and returns the path to the .pm file, which it finds by scanning @INC.

The testsuite includes the following simple test (simplified slightly here):

  use strict;
  use Module::path qw(module_path);
  use Test::More ...;

  ok(module_path('strict') eq $INC{'strict.pm'});

Because the test use's strict, then we know that the path where it was found will be in $INC{'strict.pm'} (see perldoc -f require if you weren't aware of that). I deliberately picked a module without '::' in the name, so I don't have to worry about directory separators.

I just added a script, mpath, which just calls module_path():

  % mpath MetaCPAN::API
  /usr/local/lib/perl5/site_perl/5.16.0/MetaCPAN/API.pm

In the past when I've included a script in a dist, I haven't bothered with tests. Times change, I guess. I should be able to map the test above to something like:

  chomp($path = `mpath strict`);
  ok($path eq $INC{'strict.pm'});

Now the test is in the t/ directory, and mpath is in bin/, so I wondered if I could get away with:

  chomp($path = `../bin/mpath strict`);
  ok($path eq $INC{'strict.pm'});

I'm using Dist::Zilla for this module (my second try with DZ), and both dzil test and prove -lr t failed with this, but a manual "perl Makefile.PL; make; make test worked, so I wondered if it would work in a release. I uploaded a dev release, 0.04_1 (see the Developer Releases section in 04pause.html for more on developer releases). The first result from CPANtesters was a fail. Not really a huge surprise.

I tried google for a while, looking for something that explained how to portably run scripts in tests like this, but didn't turn up anything helpful. So I turned to IRC (I'm a recent convert, for Perl purposes). kentnl and leont were very helpful, pointing out that I could use FindBin together with Path::Class or File::Spec to make a portable path. Leon pointed out that he uses File::Spec, as it's a core module.

FindBin locates the path to the directory where a script is running from. So this will give me the path to t/, and then I need to portably do ../bin. File::Spec::Functions provides catfile(), which will concatenate paths for your OS, and updir does the portable "up one directory". So now I ended up with:

  use File::Spec::Functions;
  my $MPATH = catfile( $FindBin::Bin, updir(), qw(bin mpath) );
  
  chomp($path = `$MPATH strict`);

(There's an OO interface as well: File::Spec).

Great, tests fine on my machine. Even so, another developer release, 0.04_2, because you never know...

And yep, the first CPANtester result was a fail. Fortunately I'd actually written the test as:

  ok($? == 0 && defined($path) && $path eq $INC{'strict.pm'},
     "check 'mpath strict' matches \%INC") || do {
      warn "\n",
           "    \%INC        : $INC{'strict.pm'}\n",
           "    module_path : $path\n",
           "    \$^O         : $^O\n";
  };

So I could see that %INC and module_path were giving me paths associated with different versions of perl:

    %INC        : .../perl-5.10.1/lib/5.10.1/strict.pm
    module_path : /usr/local/lib/perl5/5.12.4/strict.pm

Huh?

I realised this was probably caused by the hashbang line in the mpath script:

  #!/usr/bin/env perl

The mpath script was being run by a different perl than the test. I need them to run with the same version of perl. I vaguely remembered that there's a system variable which gives the path to the perl used to run you: $^X. So now my line to run mpath was:

  chomp($path = `$^X $MPATH strict 2>&1`);

By now I'm just assuming there will be something else, so back to IRC I went. I think it was leont (again!) who pointed out that $^X might contain spaces. Really? REALLY?! Consensus (thanks leont, ether, haarg) seemed to be quoting it should be enough ("anyone who uses any kind of shell character in their paths should be hit with a cluebat anyway" — leont). Graham Knop (haarg) mentioned that "the full story with Windows argument quoting is a lot nastier, but at least for filenames, wrapping in double quotes is usually adequate". So now we've got:

  chomp($path = `"$^X" $MPATH strict 2>&1`);

And finally my developer release, 0.04_3, started coming back with green from CPAN testers. It probably took me 10 minutes to write the script, including the pod, but two days elapsed time to get the test working portably.

And Graham also pointed out that the path to where the dist has been installed might contain spaces, so I should be quoting that path as well:

  chomp($path = `"$^X" "$MPATH" strict 2>&1`);

Thank you CPAN testers, leont, kentnl, ether, and haarg for helping me get here. Is there somewhere I could have gone to learn this in one place? If so there are probably other things there I should read!

9 Comments

I've been told that $^X can also be a relative path, so it might cause problem if you chdir around. Never encountered this though.

I was about to comment saying that you should test the script in "blib/script/" because ExtUtils::MakeMaker will have mangled that to add the correct local path to Perl in the shebang line, but it turns out that that doesn't happen until "make install".

That is probably a bug?

Yeah. I extracted Devel::FindPerl from Module::Build to deal with such edge-cases. If D::FP can't find your perl, you're probably screwed anyway.

All (known) CPAN clients run tests from the root directory of the distribution. Using FindBin is only necessary if the author likes to run tests from an other directory (such as from t/ itself).

About $^X, have a look to Probe::Perl->find_perl_interpreter that is used in Test::Script.

Keep in mind that $FindBin::Bin can also contain spaces, so you should be quoting $MPATH as well. As I had mentioned in IRC, the full story with Windows argument quoting is a lot nastier, but at least for filenames, wrapping in double quotes is usually adequate.

I'm biased, but I would recommend Capture::Tiny instead of qx(). That also means you can use system() and stop worrying about spaces in paths. (Mostly stop worrying).

I would use "bin/yourprogram" or "script/yourprogram" (depending on where you put it) and leave out FindBin.

See https://gist.github.com/3795436 for what that looks like. I think that winds up much more portable.

Probe::Perl instead of $^X if you're *really* paranoid, but I've largely stopped bothering with it.

I know it's been a while since you wrote this, but thanks for doing so. I referenced it when I was trying to be crazy-thorough. I have a write-up of my tries on SO: http://stackoverflow.com/questions/14544674/testing-scripts-in-distribution-without-building/20339123

Leave a comment

About Neil Bowers

user-pic Perl hacker since 1992.