My test suite at work was starting to get me down.  It was taking
forever to run.  Not just the whole suite mind you, but individual
scripts were taking several seconds just to start up.

As my application has grown, so has its library (including a lot of
interconnected Class::DBI modules), and this has lead to the long
startup times.  The work-around (at least on the web side) has been to
run the app in a persistent environment such as mod_perl or
PersistentPerl.

After some playing around I've now also managed to get my test suite
running under PersistentPerl.  This has dramatically increased the
performance of my test scripts.

For instance, in a typical directory containing 16 test scripts
(about 2000 lines of test code), here are the different execution
timings:

    - without perperl                 ~ 70   seconds
    - with perperl (first run)        ~ 11   seconds
    - with perperl (subsequent runs)  ~  4.5 seconds

And on a single script (about 50 lines of code):
    - without perperl                 ~  5   seconds
    - with perperl (first run)        ~  5   seconds
    - with perperl (subsequent runs)  ~  1   second


To "restart" PersistentPerl, you have to kill its backend processes,
e.g.:

    $ killall perperl_backend

You have to do this if any code outside of the test script itself has
changed.  However you *don't* have to restart PersistentPerl if only the
test script has changed.

So for the common case of fixing bugs in your test script and rerunning
it, you still get the fast startup time.  But if you change something in
a module somewhere (such as to fix a bug that your test revealed), you
have to restart PersistentPerl again.

But where this system saves the most time is when you've finished with
your current round of test-driven development and you want to run the
entire suite to make sure that you haven't broken anything elsewhere. My
test suite used to take about 10 minutes to run.  Now it takes about 2
minutes.

Because Test::Harness forks a new perl interpreter for each test script,
and because it bases its caching system on the name of the script,
there is a bit of scaffolding involved in this setup.

First I created a wrapper script which uses PersistentPerl to run a
single test script, but which always re-executes the script (via 'do'):

    $ cat perperl-runscript
    #!/usr/bin/perperl -- -M1

    use strict;
    use Test::PerPerlHelper;  # see below

    my $script = shift;
    do $script or die $@;


(The '-M1' switch on the shbang line tells perperl to only spawn one
backend process.)

Next, I created a wrapper around prove to tell it to use
perperl-runscript as its perl interpreter:

    $ cat perperl-prove
    #!/bin/sh

    export HARNESS_PERL=perperl-runscript

    prove $*

There is also some code that has to be added to each test script in
order to get it to work properly under PersistentPerl:

   * have to reset Test::Builder counter at the beginning of the script

   * for scripts with no_plan, have to register a PersistentPerl cleanup
     handler to display the final 1..X line

   * need to prevent Test::Builder from duplicating STDOUT and STDERR,
     since this seems to be incompatible with PersistentPerl


I've made a little module to do this:

    use strict;
    use warnings;

    package Test::PerPerlHelper;

    use Test::More ();
    use Test::Builder ();

    sub import {
        my $class = shift;
        $class->plan(@_);
    }

    sub plan {
        my $class = shift;

        if (@_) {

            my $Test = Test::Builder->new;

            if (eval {require PersistentPerl} && PersistentPerl->i_am_perperl) {

                # Ugly hack to prevent Test::Builder
                # calling $self->_dup_stdhandles
                {
                    local $^C = 1;
                    $Test->reset;
                }

                Test::Builder::_autoflush(\*STDOUT);
                Test::Builder::_autoflush(\*STDERR);

                $Test->output(\*STDOUT);
                $Test->failure_output(\*STDERR);
                $Test->todo_output(\*STDOUT);

                $Test->no_ending(1);
                my $pp   = PersistentPerl->new;
                $pp->register_cleanup(sub {
                     $Test->_ending;
                });
            }
            $Test->plan(@_);
        }
    }

    1;



In a test script, you use it like this:

    $ cat 01-test.t

    use Test::More;
    use Test::PerPerlHelper 'no_plan';

    ok(1);
    ok(2);
    ok(3);


This should be compatible with regular (non-PersistentPerl) use as well.

The bit in Test::PerPerlHelper that sets $^C to prevent Test::Builder
from duplicating its filehandles is obviously ugly and wrong.  But
PersistentPerl doesn't seem to like STDOUT and STDERR being redirected
(it hangs).  Maybe Test::Builder could offer an option to disable this
feature?  Or is there another solution?

Limitations and Caveats with the system:

 * Scripts that muck about with STDIN, STDOUT or STDERR will probably
   have problems.

 * The usual persistent environment caveats apply:  be careful with
   redefined subs, global vars; 'require'd code only gets loaded on the
   first request, etc.

 * Test scripts have to end in a true value.


If there's interest, I'll try to package all this up into a CPAN module.


Michael


---
Michael Graham <[EMAIL PROTECTED]>


Reply via email to