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

Posted in: Technical Track

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.

email
Want to talk with an expert? Schedule a call with our team to get the conversation started.

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *