Porting Test::Builder to Perl 6
by chromatic
|
Pages: 1, 2, 3
Final (Ha!) Version
Shortly after I checked in the example code, Stevan Little began work on a test suite (using Test.pm). I knew that Pugs didn't support many of the necessary language constructs, but this allowed Pugs hackers to identify necessary features and me to identify legitimate bugs and mistakes in the code. (It's tricky to bootstrap test-driven development.)
After filling out the test suite, fixing all of the known bugs in my code, talking other Pugs hackers into adding features I needed, and implementing those I couldn't pawn off on others, Test::Builder works completely in Pugs right now. There is one remaining nice feature: splatty args in method calls. But I'm ready to port Test.pm to the new back end and then write many, many more useful testing modules--starting with a port of Mark Fowler's Test::Builder::Tester written the night before this article went public!
The singleton creation in Test::Builder now looks like:
class Test::Builder-0.2.0;
use Test::Builder::Test;
use Test::Builder::Output;
use Test::Builder::TestPlan;
my Test::Builder $:singleton;
has Test::Builder::Output $.output handles 'diag';
has Test::Builder::TestPlan $.testplan;
has @:results;
method new ( Test::Builder $Class: ?$plan, ?$output )
{
return $:singleton //= $Class.SUPER::new(
testplan => $plan, output => $output
);
}
method create ( Test::Builder $Class: ?$plan, ?$output )
{
return $Class.new( testplan => $plan, output => $output );
}
submethod BUILD
(
Test::Builder::TestPlan ?$.testplan,
Test::Builder::Output ?$.output = Test::Builder::Output.new()
)
{}
Those test modules that want to use the default $Test
object directly can call Test::Builder::new() to return the
singleton, creating it if necessary. Test modules that need different
output or plan objects should call Test::Builder::create().
(The test suite actually does this.)
Having removed the Test::Builder code from
Test::Builder::Test, I revised the latter, as well:
class Test::Builder::Test-0.2.0
{
method new (
$number,
?$passed = 1,
?$skip = 0,
?$todo = 0,
?$reason = '',
?$description = '',
)
{
return ::Test::Builder::Test::TODO.new(
description => $description, passed => $passed, reason => $reason
) if $todo;
return ::Test::Builder::Test::Skip.new(
description => $description, passed => 1, reason => $reason
) if $skip;
return ::Test::Builder::Test::Pass.new(
description => $description, passed => 1,
) if $passed;
return ::Test::Builder::Test::Fail.new(
description => $description, passed => 0,
);
}
}
That's it. I moved the object attributes into roles.
Test::Builder::Test::Base is the basis for all tests,
encapsulating all of the attributes that tests share and providing the
important methods:
role Test::Builder::Test::Base
{
has Bool $.passed;
has Int $.number;
has Str $.diagnostic;
has Str $.description;
submethod BUILD (
$.description,
$.passed,
?$.number = 0,
?$.diagnostic = '???',
) {}
method status returns Hash
{
return
{
passed => $.passed,
description => $.description,
};
}
method report returns Str
{
my $ok = $.passed ?? 'ok' :: 'not ok';
my $description = "- $.description";
return join( ' ', $ok, $.number, $description );
}
}
class Test::Builder::Test::Pass does Test::Builder::Test::Base {}
class Test::Builder::Test::Fail does Test::Builder::Test::Base {}
Test::Builder::Test::WithReason forms the basis for TODO
and SKIP tests, adding the reason why the developer marked the test as
either:
role Test::Builder::Test::WithReason does Test::Builder::Test::Base
{
has Str $.reason;
submethod BUILD ( $.reason ) {}
method status returns Hash ( $self: )
{
my $status = $self.SUPER::status();
$status{"reason"} = $.reason;
return $status;
}
}
class Test::Builder::Test::Skip does Test::Builder::Test::WithReason { ... }
class Test::Builder::Test::TODO does Test::Builder::Test::WithReason { ... }
What's Hard
The two greatest difficulties I encountered in this porting effort were in mapping my design to the new Perl 6 way of thinking and in working around Pugs bugs and unsupported features. The former is interesting; it may suggest places where other people will run into difficulties.
One of the trickiest parts of Perl 6's OO model to understand is the
interaction of the new(), BUILD(), and
BUILDALL() methods. Perl 5 provides very little in the way of
object support beyond bless. Though having finer-grained
control over object creation, initialization, and initializer dispatch will
be very useful, remembering the purposes of each method is very important,
lest you override the wrong one and end up with an infinite loop or
partially initialized object.
From rereading the design documents, experimenting, picking the brains of other @Larry members, and thinking hard, my rules are:
Leave
new()alone.This method creates the opaque object. Override it when you don't want to return a new object of this class every time. Don't do initialization here. Don't forget to call
SUPER::new()if you actually want an object.Override
BUILD()to add initialize attributes for objects of this class.Think of this as an initializer, not a constructor.
Override
BUILDALL()when you want to change the order of initialization.I haven't needed this yet and don't expect to.
Pugs-wise, find a good Haskell tutorial, find a really fast machine that
can run GHC 6.4, and look for lambdacamel mentors on #pugs. (My productivity
increased when Autrijus told me about Haskell's trace function.
He called it a refreshing desert in the oasis of referential
transparency.)
What's Easy
Was this exercise valuable? Absolutely! It reinforced my belief that Perl 6 is not only Perlish, but that it's a fantastic revolution of Perl 5 in several ways:
- The object system is much better. Attributes and accessors require almost no syntax, and that only in their declarations. Using attributes feels Perlish, even if it's not manipulating hash keys.
- Function signatures eliminate a lot of code. My initializers do a lot of work, but they don't take much code. Some even have empty method bodies. This is a big win, except for the poor souls who had to implement the underlying binding code in Pugs. (That took a while.)
- Roles are fantastic. Sure, I believed in them already, but being able to use them without the hacks required in Perl 5 was even better.
Final Thoughts
Schwern and I did put a lot of thought into the Perl 5 redesign we never
really did, and my code here really benefits from the lessons I learned from
the previous version. Still, even though I wrote code to a moving project that
didn't yet support all of the features I wanted, it was a great exercise.
Test::Builder is simpler, shorter, cleaner, and more flexible;
it's ready for everything the Perl 6 QA group can throw at it.
Test::Builder isn't the only Perl 5 module being ported to
Perl 6. Other modules include ports of HTTP::Server::Simple,
Net::IRC, LWP, and CGI. There are even ports
underway for Catalyst and Maypole.
Perl 6 isn't ready yet, but it's closer every day. Now's a great time to port some of your code to see how Perl 6 is still Perlish, but a revolutionary step in refreshing new directions.
You must be logged in to the O'Reilly Network to post a talkback.


