package Badger::Test::Manager; use Badger::Class version => 0.01, debug => 1, base => 'Badger::Prototype', import => 'class'; use Badger::Rainbow 'ANSI_colours ANSI_escape'; eval "use Algorithm::Diff qw( diff )"; our $CAN_DIFF = $@ ? 0 : 1; our $ESCAPES = qr/\e\[(.*?)m/; # remove ANSI escapes our $REASON = 'No reason given'; our $MESSAGES = { no_plan => "You haven't called plan() yet!\n", dup_plan => "You called plan() twice!\n", plan => "1..%s\n", skip_all => "1..0 # skip %s\n", skip_one => "ok %s # skip %s\n", name => "test %s at %s line %s", ok => "ok %s - %s\n", not_ok => "not ok %s - %s\n%s", not_eq => "# expect: [%s]\n# result: [%s]\n", not_ne => "# unexpected match: [%s]\n", not_like => "# expect: /%s/\n# result: [%s]\n", not_unlike => "# expect: ! /%s/\n# result: [%s]\n", too_few => "# Looks like you planned %s tests but only ran %s.\n", too_many => "# Looks like you planned only %s tests but ran %s.\n", no_result => "# result is undefined\n", pass => "# PASS: All %d tests passed\n", fail => "# FAIL: %d tests failed\n", mess => "# FAIL: Inconsistent test results\n", summary => "# %d/%d tests run, %d passed, %d failed, %d skipped\n", hunk => "# -- diffs %s of %s --\n", delta => "# %s %3d %s\n", }; our $SCHEME = { green => 'ok pass', red => 'not_ok too_few too_many fail mess', cyan => 'skip_one skip_all hunk delta', yellow => 'plan not_eq not_ne not_like not_unlike summary', }; # Sorry, English and American/Spanish only, no couleur, colori, farbe, etc. *color = \&colour; # for nice cleanup in END block our $INSTANCES = { }; #----------------------------------------------------------------------- # constructor method #----------------------------------------------------------------------- sub init { my ($self, $config) = @_; $self->{ plan } = $config->{ plan } || 0; $self->{ count } = $config->{ count } || 1; $self->{ results } = $config->{ results } || [ ]; $self->{ summary } = $config->{ summary } || 0; $self->{ reason } = $config->{ reason } || $REASON; $self->{ colour } = $config->{ colour } || $config->{ color } || 0; $INSTANCES->{ $self } = $self; return $self; } #------------------------------------------------------------------------ # plan($n) # # Declare how many (more) tests are expected to come. If ok() is called # before plan() then the results are cached instead of being printed up # front. When plan() is called, the total number of tests (including any # cached) is known and the "1..$n" line can be printed along with any # cached results. After that, calls to ok() generated output immediately. #------------------------------------------------------------------------ sub plan ($$;$) { my $self = shift->prototype; my ($tests, $reason) = @_; # calling plan() twice would be ambiguous return $self->error_msg('dup_plan') if $self->{ plan }; # if $tests == 0 then skip all return $self->skip_all($reason) unless $tests; # update the plan to account for any tests that have already been run my $results = $self->{ results }; $tests += @$results; $self->test_msg( plan => $tests ); $self->{ plan } = $tests; $self->{ tested } = 0; $self->{ passed } = 0; $self->{ failed } = 0; $self->{ skipped } = 0; # now flush any cached test results while (@$results) { my $test = shift @$results; $self->result(@$test); } } sub ok ($$;$$) { my $self = shift->prototype; my ($ok, $name, $detail) = @_; $detail ||= ''; $name ||= $self->test_name; if ($self->{ plan }) { $self->result($ok, $self->{ count }, $name, $detail); } else { # cache results if plan() not yet called push(@{ $self->{ results } }, [ $ok, $self->{ count }, $name, $detail ]); } $self->{ count }++; $self->{ tested }++; return $ok; } sub pass ($;$) { shift->ok(1, @_); } sub fail ($;$) { shift->ok(0, @_); } sub is ($$$;$) { my $self = shift->prototype; my ($result, $expect, $msg) = @_; $msg ||= $self->test_name(); if (! defined $result) { return $self->fail($msg, $self->message('no_result')); } # force stringification of $result to avoid 'no eq method' overload errors $result = "$result" if ref $result; # if we have coloured output enabled then the result might not match # the expected because of embedded ANSI escapes, so we strip them out my ($r, $e) = map { s/$ESCAPES//g if $self->{ colour }; $_ } ($result, $expect); if ($r eq $e) { return $self->pass($msg); } else { return $self->fail($msg, $self->different($expect, $result)); } } sub isnt ($$$;$) { my $self = shift->prototype; my ($result, $expect, $msg) = @_; $msg ||= $self->test_name(); # force stringification of $result to avoid 'no eq method' overload errors $result = "$result" if ref $result; # if we have coloured output enabled then the result might not match # the expected because of embedded ANSI escapes, so we strip them out my ($r, $e) = map { s/$ESCAPES//g if $self->{ colour }; $_ } ($result, $expect); if ($r ne $e) { return $self->pass($msg); } else { for ($expect, $result) { s/\n/\n |/g; } return $self->fail($msg, $self->message( not_eq => $expect, $result )); } } sub like ($$$;$) { my $self = shift->prototype; my ($result, $expect, $name) = @_; $name ||= $self->test_name(); # strip ANSI escapes if necessary my $r = $result; $r =~ s/$ESCAPES//g if $self->{ colour }; if ($r =~ $expect) { $self->pass($name); } else { return $self->fail($name, $self->message( not_like => $expect, $result )); } } sub unlike ($$$;$) { my $self = shift->prototype; my ($result, $expect, $name) = @_; $name ||= $self->test_name(); # strip ANSI escapes if necessary my $r = $result; $r =~ s/$ESCAPES//g if $self->{ colour }; if ($r !~ $expect) { $self->pass($name); } else { return $self->fail($name, $self->message( not_unlike => $expect, $result )); } } sub skip ($;$) { my $self = shift->prototype; my $msg = shift || $self->test_name; return $self->error_msg('no_plan') unless $self->{ plan }; $self->{ tested }++; $self->{ skipped }++; return $self->test_msg( skip_one => $self->{ count }++, $msg ); } sub skip_some { my ($self, $n, $msg) = @_; $n = int $n; return unless $n > 0; while ($n--) { $self->skip($msg); } } sub skip_rest { my $self = shift->prototype; my $msg = shift; my $plan = $self->{ plan }; while ($self->{ tested } < $plan) { $self->skip($msg); } exit; } sub skip_all ($;$) { my $self = shift->prototype; $self->test_msg( skip_all => shift || $self->{ reason } ); exit; } sub result { my $self = shift->prototype; my $ok = shift; return $self->error_msg('no_plan') unless $self->{ plan }; if ($ok) { $self->{ passed }++; return $self->test_msg( ok => @_ ); } else { $self->{ failed }++; return $self->test_msg( not_ok => @_ ); } } sub test_msg { my $self = shift; print $self->message(@_); } sub test_name ($) { my $self = shift->prototype; my ($pkg, $file, $line) = caller(2); $self->message( name => $self->{ count }, $file, $line ); } sub different { my ($self, $expect, $result) = @_; my ($pad_exp, $pad_res) = ($expect, $result); for ($pad_exp, $pad_res) { s/\n/\n# |/g; } my $msg = $self->message( not_eq => $pad_exp, $pad_res ); return $msg unless $CAN_DIFF; my $diffs = diff( map { [ split(/\n/) ] } $expect, $result ); my $n = 0; my $m = scalar @$diffs; foreach my $hunk (@$diffs) { $msg .= $self->message( hunk => ++$n, $m ); foreach my $delta (@$hunk) { $msg .= $self->message( delta => @$delta ); } # $msg .= "\n"; } return $msg; } sub colour { my $self = shift->prototype; my $ansi = ANSI_colours; # enable colour mode by inserting ANSI escapes into $MESSAGES if (@_ && ($self->{ colour } = shift)) { foreach my $col (keys %$SCHEME) { my $code = $ansi->{ $col } || $self->error("Invalid colour name in \$SCHEME: $col\n"); $MESSAGES->{ $_ } = ANSI_escape($code, $MESSAGES->{ $_ }) for split(/\s+/, $SCHEME->{ $col }); } Badger::Debug->enable_colour; } return $self->{ colour }; } sub flush { my $self = shift->prototype; my $results = shift || $self->{ results }; return unless @$results; $self->{ plan } ||= @$results; while (@$results) { my $test = shift @$results; $self->result(@$test); } } sub summary { my $self = shift->prototype; return @_ ? ($self->{ summary } = shift) : $self->{ summary }; } sub finish { my $self = shift->prototype; $self->flush; # output any cached results my ($plan, $ran, $pass, $fail, $skip) = @$self{ qw( plan tested passed failed skipped ) }; return unless $plan; # mandatory warnings about too many/too few if ($ran < $plan) { $self->test_msg( too_few => $plan, $ran ); } elsif ($ran > $plan) { $self->test_msg( too_many => $plan, $ran ); } # optional summary follows for those who want it return unless $self->{ summary }; my $good = $pass + $skip; if ($fail) { $self->test_msg( fail => $fail ); } elsif ($good == $plan) { $self->test_msg( pass => $plan ); } else { $self->test_msg('mess'); } $self->test_msg( summary => $ran, $plan, $pass, $fail, $skip ); # remove ourselves from the index delete $INSTANCES->{ $self }; } END { # Cleanup test managers so they can report errors using test_msg(). If # we leave it until global destruction (e.g. by using a DESTROY method to # call finish() then there's a chance that the Badger::Class object that # perform the $MESSAGE lookup will have already been cleaned up. $_->finish for values %$INSTANCES; } 1; __END__ =head1 NAME Badger::Test::Manager - test manager module =head1 SYNOPSIS use Badger::Test::Manager; # object methods my $manager = Badger::Test::Manager->new( plan => 7 ); $manager->ok($bool, 'This is a test'); $manager->pass('This is ok'); $manager->fail('This is not ok'); $manager->is($this, $this, 'This and that are equal'); $manager->isnt($this, $this, 'This and that are not equal'); $manager->like($this, qr/that/, 'This is matched by that'); $manager->unlike($this, qr/that/, 'This is not matched by that'); # class methods Badger::Test::Manager->plan(7); Badger::Test::Manager->ok($bool, 'This is a test'); Badger::Test::Manager->pass('This is ok'); Badger::Test::Manager->fail('This is not ok'); # ... plus is(), isnt(), like() unlike() methods, as above =head1 DESCRIPTION This module implements a simple test manager for L. =head1 METHODS All methods can be called as class methods or object methods. In the case of class methods, they are called against a prototype object returned by the L method inherited from L. =head2 plan($tests) How many tests you plan to run. An error will be thrown if you try to call this method twice. =head2 result($flag,@args) Low-level method to generate a test result. =head2 ok($flag, $name) Report on the success or failure of a test: $manager->ok(1, 'This is good'); $manager->ok(0, 'This is bad'); =head2 is($this, $that, $name) Test if the first two arguments are equal. $manager->is($this, $that, "This and that are equal"); =head2 isnt($this, $that, $name) Test if the first two arguments are not equal. $manager->isnt($this, $that, "This and that are equal"); =head2 like($text, qr/regex/, $name) Test if the first argument is matched by the regex passed as the second argument. $manager->like($this, qr/like that/i, "This and that are alike"); =head2 unlike($text, qr/regex/, $name) Test if the first argument is not matched by the regex passed as the second argument. $manager->unlike($this, qr/like that/i, "This and that are unalike"); =head2 pass($name) Pass a test. $manager->pass('Module Loaded'); =head2 fail($name) Fail a test. $manager->fail('Stonehenge crushed by a dwarf'); =head2 skip($reason) Skip a single test. $manager->skip("We don't have that piece of scenery any more"); =head2 skip_some($number,$reason) Skip a number of tests. $manager->skip_some(11, "We don't have that piece of scenery any more"); =head2 skip_rest(,$reason) Skip any remaining tests. $manager->skip_rest("We don't have that piece of scenery any more"); =head2 skip_all($reason) Skip all tests. This should be called instead of L $manager->skip_all("We don't have that piece of scenery any more"); =head2 colour($flag) Method to enable or disable colour mode. =head2 color($flag) An alias for L. =head1 INTERNAL METHODS =head2 finish() This method is called automatically when the C object is destroyed. It flushes any pending tests, performs any final sanity checks and prints a summary if requested. =head2 flush() This methods flushes any cached test results. You don't need to worry about it. =head2 summary() This method generates a final summary of the tests =head2 test_msg() Used to generate the test messages displayed via the L L method. The message formats are defined in the C<$MESSAGES> package variable. =head2 test_name() Use to generate a name for a test if one isn't explicitly provided. =head2 different($expect,$result) This method is call when a test find a result that doesn't match the expected value. If C is installed on your machine, it will generate a message showing how the output and expected values differ. Otherwise it will generate a regular message reporting the mismatch. =head1 AUTHOR Andy Wardley L =head1 COPYRIGHT Copyright (C) 1996-2009 Andy Wardley. All Rights Reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L =cut # Local Variables: # mode: perl # perl-indent-level: 4 # indent-tabs-mode: nil # End: # # vim: expandtab shiftwidth=4: