How Test::Aggregate does 'skip_all'

Ovid on 2008-09-16T08:03:16

Before I start, I just want to say how incredibly thankful I am that I did not take that job at Lehman Brothers. It was soooo tempting.

Test::Aggregate used to not allow 'skip_all' in plans. This was because you would "skip_all" 12,000 tests instead of just the 20 or so in the test program. Very disappointing. It even warned you if it thought you might be using skip_all. I mentioned I might fix it one day, but now I had to fix it (0.34_03 and above), but it turned out to not be easy.

Test::Aggregate uses an Apache::Registry trick to make defer loading some code. We take a small test program like this:

use strict;
use warnings;

use Test::More 'no_plan';

is 2+2, 4, 'We can add!';

And turn it into something similar to this (trimming some bits):

  1 {
  2 #################### beginning of aggtests/test.t ####################
  3     package aggteststestt;
  4
  5     sub run_the_tests {
  6         local $0 = 'aggtests/test.t';
  7
  8 # line 1 "aggtests/test.t"
  9
 10         use strict;
 11         use warnings;
 12
 13         use Test::More 'no_plan';
 14
 15         is 2 + 2, 4, 'We can add!';
 16     }
 17 #################### end of aggtests/test.t ####################
 18 }

Line 3 guarantees that we're in an encapsulated namespace. Line 6 solves a problem where many tests make assumptions about the contents of $0 and line 8 is a line directive which ensures that errors are reported with the original line number and filename. There's actually a lot more, but this is the core of what we're doing.

The problem was that a "skip_all" in Test::Builder figured out an easy way to halt the tests. It just calls exit and that doesn't work for me. Since these tests are now wrapped in subs, if I wanted to simulate "skip_all" I had to call "return", but as a stand-alone test program, that looks very strange. Nonetheless, I had this at the top of a number of test programs:

if ( $ENV{FAST_TESTS} ) {
    if ( $ENV{TEST_AGGREGATE} ) {
        Test::Builder->new->skip("Skipping $0 under aggregated fast tests");
        return;
    }
    else {
        plan skip_all => 'FAST_TESTS environment variable set';
    }
}
else {
    plan tests => $num_tests;
}

I couldn't factor that out into a subroutine as that "return" would have to return from an extra stack level. Ugh! (more than once I have failed to refactor code because I need to return or eval code at a different stack level from the current one). However, even using this would throw my "skip_all" warnings in Test::Aggregate.

I was washing dishes on Sunday and thinking "damn it. I wish I had a reliable source filter or Perl 6 grammars. That would make this problem trivial because I ... I ... I..."

Oh. I'm a dumbass.

I'm not using source filters, but I'm already rewriting the damned code! Now the above test script looks a bit like this when rewritten:

  1 {
  2 #################### beginning of aggtests/test.t ####################
  3     package aggteststestt;
  4
  5     sub run_the_tests {
  6         local $0 = 'aggtests/test.t';
  7
  8         AGGTESTBLOCK: {
  9             if ( my $reason
 10                 = $Test::Aggregate::Builder::SKIP_REASON_FOR{aggteststestt} )
 11             {
 12                 Test::Builder->new->skip($reason);
 13                 last AGGTESTBLOCK;
 14             }
 15 # line 1 "aggtests/test.t"
 16
 17             use strict;
 18             use warnings;
 19
 20             use Test::More 'no_plan';
 21
 22             is 2 + 2, 4, 'We can add!';
 23         }
 24     }
 25 #################### end of aggtests/test.t ####################
 26 }

Ugly, but you should never see this code anyway. Problem solved. (I wanted to put the "last AGGTESTBLOCK" in the Test::Builder::plan subroutine, but because it's called at compile-time, the AGGTESTBLOCK hasn't finished parsing, so &plan cannot "last" out of a block it can't see).

And that ugly if/else "FAST_TESTS" block above? Now it looks like this:

# If it's not in a BEGIN block, it will run too late for
# the &run_the_tests call to see it.
BEGIN {
    plan $ENV{FAST_TESTS}
        ? (skip_all => 'FAST_TESTS environment variable set')
        : (tests => $num_tests);
}

All things considered, I think this has been a good day's work.