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>