Meddling with Test::Builder (helloooo Test::Wrapper)
Jul 23, 2010 / By Yanick Champoux
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.

Recent Comments