Meddling with Test::Builder (helloooo Test::Wrapper)

Jul 23, 2010 / By Yanick Champoux

Tags: , ,

Following up on my threat of last week, I released Test::Wrapper on CPAN.

If you read my previous blog entry, you know that one of the big gotchas of the wrapping gymnastics I was doing was that it was utterly #@$%$# up Test::Builder’s internal states. Thus, at that point, it was either run TAP tests, or use Test::Wrapper, but don’t do both at the same time. Not the most God-awful limitation ever, perhaps, but still not very cool.

Since then, I’ve taken a second look at the problem, and realized that this limitation can not only be overcome, but in a surprisingly easy manner.

The trick is to know that Test::Builder states are kept in a global object, $Test::Builder::Test. Since all the information is kept there, for our meddling to become benign all we have to do is a classic “distract, switch, butcher & reinstate” maneuver:

# slightly simplified from Test::Wrapper's guts

# $original_test is the full name of the test.
#  E.g.  'Test::More::like'

my $wrapped = $original_test;

# get the original test's coderef
my $original_test_ref = eval '\&' . $original_test;

# get its prototype
my $proto = prototype $original_test_ref;
$proto &&= "($proto)";

# okay, let's wrap that test...
eval <<"END_EVAL";

sub $wrapped $proto {

    # magic! we make a local copy of $Test::Builder::Test
    # that we can mangle in every way we want

    local \$Test::Builder::Test = {
        %\$Test::Builder::Test
    };

    my \$builder = bless \$Test::Builder::Test, 'Test::Builder';

    # we change the testing plan to a single test (this one)
    \$builder->{Have_Plan}        = 1;
    \$builder->{Have_Output_Plan} = 1;
    \$builder->{Expected_Tests}   = 1;

    # we capture all the output channels
    my ( \$output, \$failure, \$todo );
    \$builder->output( \\\$output );
    \$builder->failure_output( \\\$failure);
    \$builder->todo_output( \\\$todo );

    # call the original test, which will interact with our
    # modified $Test::Builder::Test
    \$original_test_ref->( \@_ );

    # ... and harvest the output and populate an object with it
    return Test::Wrapper->new(
        output => \$output,
        diag => \$failure,
        todo => \$todo,
    );

    # that's it! leaving the subroutine will make our
    # $Test::Builder::Test get out of scope, which
    # will un-hide the original $Test::Builder::Test
    # unaltered by what happened here
}

END_EVAL

And that, boys and girls, is the crux of Test::Wrapper. The rest is just window-dressing and API goodness.

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>