diff --git a/cassandane/Cassandane/Unit/RunnerPretty.pm b/cassandane/Cassandane/Unit/FormatPretty.pm similarity index 90% rename from cassandane/Cassandane/Unit/RunnerPretty.pm rename to cassandane/Cassandane/Unit/FormatPretty.pm index 94a20f4590..1c7f9c8c8b 100644 --- a/cassandane/Cassandane/Unit/RunnerPretty.pm +++ b/cassandane/Cassandane/Unit/FormatPretty.pm @@ -37,12 +37,12 @@ # OF THIS SOFTWARE. # -package Cassandane::Unit::RunnerPretty; +package Cassandane::Unit::FormatPretty; use strict; use warnings; use lib '.'; -use base qw(Cassandane::Unit::Runner); +use base qw(Cassandane::Unit::Formatter); sub new { @@ -66,7 +66,7 @@ sub new sub ansi { my ($self, $codes, @args) = @_; - my $isatty = -t $self->print_stream; + my $isatty = -t $self->{fh}; my $ansi; @@ -77,13 +77,6 @@ sub ansi return $ansi; } -sub start_test -{ - my $self = shift; - my $test = shift; - # prevent the default action which is to print "." -} - sub add_pass { my $self = shift; @@ -100,8 +93,6 @@ sub add_error my $self = shift; my $test = shift; - $self->record_failed($test); - my $line = sprintf "%s %s\n", $self->ansi([31], '[ERROR ]'), _getname($test); @@ -113,7 +104,6 @@ sub add_failure my $self = shift; my $test = shift; - $self->record_failed($test); my $line = sprintf "%s %s\n", $self->ansi([33], '[FAILED]'), _getname($test); @@ -149,8 +139,8 @@ sub print_errors my $saved_output_stream; if ($self->{_quiet}) { if ($self->{_quiet_report_fh}) { - $saved_output_stream = $self->{_Print_stream}; - $self->{_Print_stream} = $self->{_quiet_report_fh}; + $saved_output_stream = $self->{fh}; + $self->{fh} = $self->{_quiet_report_fh}; } else { return; @@ -178,7 +168,7 @@ sub print_errors } if ($saved_output_stream) { - $self->{_Print_stream} = $saved_output_stream; + $self->{fh} = $saved_output_stream; } } @@ -189,8 +179,8 @@ sub print_failures my $saved_output_stream; if ($self->{_quiet}) { if ($self->{_quiet_report_fh}) { - $saved_output_stream = $self->{_Print_stream}; - $self->{_Print_stream} = $self->{_quiet_report_fh}; + $saved_output_stream = $self->{fh}; + $self->{fh} = $self->{_quiet_report_fh}; } else { return; @@ -218,7 +208,7 @@ sub print_failures } if ($saved_output_stream) { - $self->{_Print_stream} = $saved_output_stream; + $self->{fh} = $saved_output_stream; } } diff --git a/cassandane/Cassandane/Unit/FormatTAP.pm b/cassandane/Cassandane/Unit/FormatTAP.pm new file mode 100644 index 0000000000..c6d9b220a7 --- /dev/null +++ b/cassandane/Cassandane/Unit/FormatTAP.pm @@ -0,0 +1,73 @@ +#!/usr/bin/perl +# +# Copyright (c) 2011-2017 FastMail Pty Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# 3. The name "Fastmail Pty Ltd" must not be used to +# endorse or promote products derived from this software without +# prior written permission. For permission or any legal +# details, please contact +# FastMail Pty Ltd +# PO Box 234 +# Collins St West 8007 +# Victoria +# Australia +# +# 4. Redistributions of any form whatsoever must retain the following +# acknowledgment: +# "This product includes software developed by Fastmail Pty. Ltd." +# +# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +package Cassandane::Unit::FormatTAP; +use strict; +use warnings; +use Data::Dumper; +use IO::File; + +use lib '.'; +use base qw(Cassandane::Unit::Formatter); + +sub new +{ + my ($class, $fh) = @_; + return $class->SUPER::new($fh); +} + +sub start_test +{ + my ($self, $test) = @_; + $self->_print('.'); +} + +sub add_error +{ + my ($self, $test, $exception) = @_; + $self->_print('E'); +} + +sub add_failure +{ + my ($self, $test, $exception) = @_; + $self->_print('F'); +} + +1; diff --git a/cassandane/Cassandane/Unit/RunnerXML.pm b/cassandane/Cassandane/Unit/FormatXML.pm similarity index 71% rename from cassandane/Cassandane/Unit/RunnerXML.pm rename to cassandane/Cassandane/Unit/FormatXML.pm index c2b54eb496..0770661cb2 100644 --- a/cassandane/Cassandane/Unit/RunnerXML.pm +++ b/cassandane/Cassandane/Unit/FormatXML.pm @@ -37,47 +37,34 @@ # OF THIS SOFTWARE. # -package Cassandane::Unit::RunnerXML; +package Cassandane::Unit::FormatXML; +use strict; +use warnings; +use vars qw($VERSION); use XML::Generator; use Time::HiRes qw(time); use Sys::Hostname; use POSIX qw(strftime); -use strict; -use warnings; -use vars qw($VERSION); -# XXX should this inherit from our own Cassandane::Unit::Runner? -use base qw(Test::Unit::Runner); +use lib '.'; +use base qw(Cassandane::Unit::Formatter); -# $Id: XML.pm 27 2004-08-24 11:22:24Z andrew $ $VERSION = '0.1'; sub new { - my ($class, $directory, $generator) = @_; + my ($class, $params, @args) = @_; - $generator ||= XML::Generator->new(escape => 'always', pretty => 2); + my $self = $class->SUPER::new(@args); - return bless({directory => $directory, gen => $generator, - all_tests_passed => 1, - classrecs => {}}, - $class); -} + $params->{generator} ||= XML::Generator->new(escape => 'always', + pretty => 2); -sub all_tests_passed { - my ($self) = @_; - - return $self->{all_tests_passed}; -} + $self->{directory} = $params->{directory}; + $self->{gen} = $params->{generator}; + $self->{classrecs} = {}; -sub start { - my ($self, $suite) = @_; - - my $result = $self->create_test_result(); - $result->add_listener($self); - my $start_time = time(); - $suite->run($result, $self); - $self->_emit_xml(); + return $self; } sub _classrec { @@ -98,8 +85,6 @@ sub _testrec { { start_time => 0, node => undef, child_nodes => [] }; } -sub add_pass {} - sub _extype { my ($exception) = @_; @@ -115,7 +100,6 @@ sub add_failure { my $cr = $self->_classrec($test); my $tr = $self->_testrec($test); $cr->{failures}++; - $self->{all_tests_passed} = 0; push(@{$tr->{child_nodes}}, $self->{gen}->failure({type => _extype($exception), message => $exception->get_message()}, @@ -128,7 +112,6 @@ sub add_error { my $cr = $self->_classrec($test); my $tr = $self->_testrec($test); $cr->{errors}++; - $self->{all_tests_passed} = 0; push(@{$tr->{child_nodes}}, $self->{gen}->error({type => _extype($exception), message => $exception->get_message()}, @@ -197,6 +180,16 @@ sub _emit_xml { } } +sub finished +{ + my ($self, $result, $start_time, $end_time) = @_; + + # XXX This class does all its own accounting, which is probably + # XXX redundant since it doesn't report anything that it couldn't + # XXX just get from the usual $result/$start_time/$end_time args. + $self->_emit_xml(); +} + sub _xml_filename { my ($self, $class) = @_; @@ -205,72 +198,3 @@ sub _xml_filename { } 1; - -__END__ - - -=head1 NAME - -Test::Unit::Runner::XML - Generate XML reports from unit test results - -=head1 SYNOPSIS - - use Test::Unit::Runner::XML; - - mkdir("test_reports"); - my $runner = Test::Unit::Runner::XML->new("test-reports"); - $runner->start($test); - exit(!$runner->all_tests_passed()); - -=head1 DESCRIPTION - -Test::Unit::Runner::XML generates XML reports from unit test results. The -reports are in the same format as those produced by Ant's JUnit task, -allowing them to be used with Java continuous integration and reporting tools. - -=head1 CONSTRUCTOR - - Test::Unit::Runner::XML->new($directory) - -Construct a new runner that will write XML reports into $directory - -=head1 METHODS - -=head2 start - - $runner->start($test); - -Run the L $test and generate XML reports from the results. - -=head2 all_tests_passed - - exit(!$runner->all_tests_passed()); - -Return true if all tests executed by $runner since it was constructed passed. - -=head1 AUTHOR - -Copyright (c) 2004 Andrew Eland, Eandrew@andreweland.orgE. - -All rights reserved. This program is free software; you can -redistribute it and/or modify it under the same terms as Perl itself. - -=head1 SEE ALSO - -=over 4 - -=item * - -L - -=item * - -L - -=item * - -The Ant JUnit task, http://ant.apache.org/ - -=cut - - diff --git a/cassandane/Cassandane/Unit/Formatter.pm b/cassandane/Cassandane/Unit/Formatter.pm new file mode 100644 index 0000000000..f892c122be --- /dev/null +++ b/cassandane/Cassandane/Unit/Formatter.pm @@ -0,0 +1,185 @@ +#!/usr/bin/perl +# +# Copyright (c) 2011-2017 FastMail Pty Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# 3. The name "Fastmail Pty Ltd" must not be used to +# endorse or promote products derived from this software without +# prior written permission. For permission or any legal +# details, please contact +# FastMail Pty Ltd +# PO Box 234 +# Collins St West 8007 +# Victoria +# Australia +# +# 4. Redistributions of any form whatsoever must retain the following +# acknowledgment: +# "This product includes software developed by Fastmail Pty. Ltd." +# +# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +package Cassandane::Unit::Formatter; +use strict; +use warnings; + +use base 'Test::Unit::Listener'; +use Benchmark; +use IO::Handle; + +sub new +{ + my ($class, $fh) = @_; + + $fh //= \*STDOUT; + $fh->autoflush(1); + + return bless { + remove_me_in_cassandane_child => 1, + fh => $fh, + }, $class; +} + +sub _print +{ + my ($self, @args) = @_; + $self->{fh}->print(@args); +} + +# No-op implementations of Listener interface. To create a new output +# format, subclass from this and override the appropriate event handlers + +sub start_suite +{ + my ($self, $suite) = @_; +} + +sub start_test +{ + my ($self, $test) = @_; +} + +sub add_pass +{ + my ($self, $test) = @_; +} + +sub add_error +{ + my ($self, $test, $exception) = @_; +} + +sub add_failure +{ + my ($self, $test, $exception) = @_; +} + +sub end_test +{ + my ($self, $test) = @_; +} + +# Override this with your output format's end-of-tests handling. The +# default is to print a summary. +sub finished +{ + my ($self, $result, $start_time, $end_time) = @_; + $self->print_summary($result, $start_time, $end_time); +} + +# Override this, and/or subs print_header, print_errors, print_failures +# to change how the summary is presented. +sub print_summary +{ + my ($self, $result, $start_time, $end_time) = @_; + + my $run_time = timediff($end_time, $start_time); + print "\n", "Time: ", timestr($run_time), "\n"; + + $self->print_header($result); + $self->print_errors($result); + $self->print_failures($result); +} + +sub print_header +{ + my ($self, $result) = @_; + + if ($result->was_successful()) { + $self->_print("\n", "OK", " (", $result->run_count(), " tests)\n"); + } + else { + $self->_print("\n", "!!!FAILURES!!!", "\n", + "Test Results:\n", + "Run: ", $result->run_count(), + ", Failures: ", $result->failure_count(), + ", Errors: ", $result->error_count(), + "\n"); + } +} + +sub print_errors +{ + my ($self, $result) = @_; + + return unless my $error_count = $result->error_count(); + + my $msg = "\nThere " . + ($error_count == 1 ? + "was 1 error" + : "were $error_count errors") . + ":\n"; + $self->_print($msg); + + my $i = 0; + for my $e (@{$result->errors()}) { + chomp(my $e_to_str = $e); + $i++; + $self->_print("$i) $e_to_str\n"); + $self->_print("\nAnnotations:\n", $e->object->annotations()) + if $e->object->annotations(); + } +} + +sub print_failures +{ + my ($self, $result) = @_; + + return unless my $failure_count = $result->failure_count; + + my $msg = "\nThere " . + ($failure_count == 1 ? + "was 1 failure" + : "were $failure_count failures") . + ":\n"; + $self->_print($msg); + + my $i = 0; + for my $f (@{$result->failures()}) { + chomp(my $f_to_str = $f); + $self->_print("\n") if $i++; + $self->_print("$i) $f_to_str\n"); + $self->_print("\nAnnotations:\n", $f->object->annotations()) + if $f->object->annotations(); + } +} + +1; diff --git a/cassandane/Cassandane/Unit/Runner.pm b/cassandane/Cassandane/Unit/Runner.pm index 0ae79d11c0..65522a2d82 100644 --- a/cassandane/Cassandane/Unit/Runner.pm +++ b/cassandane/Cassandane/Unit/Runner.pm @@ -40,8 +40,9 @@ package Cassandane::Unit::Runner; use strict; use warnings; -use base qw(Test::Unit::TestRunner); +use base qw(Test::Unit::Runner); use Test::Unit::Result; +use Benchmark; use IO::File; use lib '.'; @@ -49,17 +50,18 @@ use Cassandane::Cassini; sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - $self->{remove_me_in_cassandane_child} = 1; + my ($class) = @_; my $cassini = Cassandane::Cassini->instance(); my $rootdir = $cassini->val('cassandane', 'rootdir', '/var/tmp/cass'); my $failed_file = "$rootdir/failed"; - $self->{failed_fh} = IO::File->new($failed_file, 'w'); # if we can't write there, we just won't record failed tests! - return $self; + return bless { + remove_me_in_cassandane_child => 1, + formatters => [], + failed_fh => IO::File->new($failed_file, 'w'), + }, $class; } sub create_test_result @@ -69,6 +71,55 @@ sub create_test_result return $self->{_result}; } +sub add_formatter +{ + my ($self, $formatter) = @_; + + push @{$self->{formatters}}, $formatter; +} + +# this is very similar to Test::Unit::Result's tell_listeners(), except +# without the annoying crash when the listener doesn't care about the event +sub tell_formatters +{ + my ($self, $method, @args) = @_; + + foreach my $formatter (@{$self->{formatters}}) { + if ($formatter->can($method)) { + $formatter->$method(@args); + } + } +} + +sub do_run +{ + my ($self, $suite) = @_; + my $result = $self->create_test_result(); + + $result->add_listener($self); + foreach my $f (@{$self->{formatters}}) { + $result->add_listener($f); + } + + my $start_time = new Benchmark(); + $suite->run($result, $self); + my $end_time = new Benchmark(); + + foreach my $f (@{$self->{formatters}}) { + $f->finished($result, $start_time, $end_time); + } + + return $result->was_successful; +} + +sub start_suite { } + +sub start_test { } + +sub end_test { } + +sub add_pass { } + sub record_failed { my ($self, $test) = @_; @@ -87,14 +138,12 @@ sub add_error { my ($self, $test) = @_; $self->record_failed($test); - $self->SUPER::add_error($test); } sub add_failure { my ($self, $test) = @_; $self->record_failed($test); - $self->SUPER::add_failure($test); } 1; diff --git a/cassandane/Cassandane/Unit/TestPlan.pm b/cassandane/Cassandane/Unit/TestPlan.pm index 2837769b86..b2837a7989 100644 --- a/cassandane/Cassandane/Unit/TestPlan.pm +++ b/cassandane/Cassandane/Unit/TestPlan.pm @@ -871,10 +871,20 @@ sub _finish_workitem my ($self, $witem, $result, $runner) = @_; my ($suite, $test) = $self->_get_suite_and_test($witem); + # The test was actually started earlier by _run_workitem, but its + # start_test event wasn't sent. It might have got swallowed due to + # the output format listeners being removed in the workitem handling. + # Send the event again now, to make sure the formatters actually get + # it... $result->start_test($test); - if ($runner->can('fake_start_time')) + # But! If they're computing their own start time based on this event + # they'll get it wrong. We know the real start time, so tell the + # formatter to use that instead. + if ($runner->can('tell_formatters')) { - $runner->fake_start_time($test, $witem->{start_time}); + $runner->tell_formatters('fake_start_time', + $test, + $witem->{start_time}); } $test->annotate_from_file($witem->{logfile}); diff --git a/cassandane/testrunner.pl b/cassandane/testrunner.pl index 6b39233e11..716b1bc9ce 100755 --- a/cassandane/testrunner.pl +++ b/cassandane/testrunner.pl @@ -45,8 +45,10 @@ use lib '.'; use Cassandane::Util::Setup; +use Cassandane::Unit::FormatPretty; +use Cassandane::Unit::FormatTAP; +use Cassandane::Unit::FormatXML; use Cassandane::Unit::Runner; -use Cassandane::Unit::RunnerPretty; use Cassandane::Unit::TestPlan; use Cassandane::Util::Log; use Cassandane::Cassini; @@ -58,7 +60,7 @@ $Data::Dumper::Sortkeys = 1; $Data::Dumper::Trailingcomma = 1; -my $format = 'prettier'; +my %want_formats = (); my $output_dir = 'reports'; my $do_list = 0; # The default really should be --no-keep-going like make @@ -137,88 +139,56 @@ }; }; -my %runners = -( - tap => sub - { - my ($plan, $fh) = @_; - local *__ANON__ = "runner_tap"; - my $runner = Cassandane::Unit::Runner->new($fh); - my @filters = qw(x skip_version skip_missing_features - skip_runtime_check - enable_wanted_properties); - push @filters, 'skip_slow' if $plan->{skip_slow}; - push @filters, 'slow_only' if $plan->{slow_only}; - $runner->filter(@filters); - return $runner->do_run($plan, 0); +my %formatters = ( + tap => { + writes_to_stdout => 1, + formatter => sub { + my ($fh) = @_; + return Cassandane::Unit::FormatTAP->new($fh); + }, }, - pretty => sub - { - my ($plan, $fh) = @_; - local *__ANON__ = "runner_pretty"; - my $runner = Cassandane::Unit::RunnerPretty->new({}, $fh); - my @filters = qw(x skip_version skip_missing_features - skip_runtime_check - enable_wanted_properties); - push @filters, 'skip_slow' if $plan->{skip_slow}; - push @filters, 'slow_only' if $plan->{slow_only}; - $runner->filter(@filters); - return $runner->do_run($plan, 0); + pretty => { + writes_to_stdout => 1, + formatter => sub { + my ($fh) = @_; + return Cassandane::Unit::FormatPretty->new({}, $fh); + }, }, - prettier => sub - { - my ($plan, $fh) = @_; - local *__ANON__ = "runner_prettier"; - my $runner = Cassandane::Unit::RunnerPretty->new({quiet=>1}, $fh); - my @filters = qw(x skip_version skip_missing_features - skip_runtime_check - enable_wanted_properties); - push @filters, 'skip_slow' if $plan->{skip_slow}; - push @filters, 'slow_only' if $plan->{slow_only}; - $runner->filter(@filters); - return $runner->do_run($plan, 0); + prettier => { + writes_to_stdout => 1, + formatter => sub { + my ($fh) = @_; + return Cassandane::Unit::FormatPretty->new({quiet=>1}, $fh); + }, + }, + xml => { + writes_to_stdout => 0, + formatter => sub { + my ($fh) = @_; + return Cassandane::Unit::FormatXML->new({ + directory => $output_dir + }); + }, }, ); become_cyrus(); -eval -{ - require Cassandane::Unit::RunnerXML; - - if ( ! -d $output_dir ) - { +eval { + if ( ! -d $output_dir ) { mkdir($output_dir) or die "Cannot make output directory \"$output_dir\": $!\n"; } - if (! -w $output_dir ) - { + if (! -w $output_dir ) { die "Cannot write to output directory \"$output_dir\"\n"; } - - $runners{xml} = sub - { - my ($plan, $fh) = @_; - local *__ANON__ = "runner_xml"; - - my $runner = Cassandane::Unit::RunnerXML->new($output_dir); - my @filters = qw(x skip_version skip_missing_features - skip_runtime_check - enable_wanted_properties); - push @filters, 'skip_slow' if $plan->{skip_slow}; - push @filters, 'slow_only' if $plan->{slow_only}; - $runner->filter(@filters); - $runner->start($plan); - return $runner->all_tests_passed(); - }; }; if ($@) { my $eval_err = $@; - $runners{xml} = sub - { - print STDERR "Sorry, XML output format not available due to:\n=> $eval_err"; - return 0; + $formatters{xml}->{formatter} = sub { + die "Sorry, XML output format not available due to:\n", + "=> $eval_err"; }; } @@ -248,13 +218,15 @@ sub usage } elsif ($a eq '-f') { - $format = shift; - usage unless defined $runners{$format}; + my $format = shift; + usage unless defined $formatters{$format}; + $want_formats{$format} = 1; } elsif ($a =~ m/^-f(\w+)$/) { - $format = $1; - usage unless defined $runners{$format}; + my $format = $1; + usage unless defined $formatters{$format}; + $want_formats{$format} = 1; } elsif ($a eq '-v' || $a eq '--verbose') { @@ -383,10 +355,30 @@ sub usage { # Build the schedule per commandline $plan->schedule(@names); + # Run the schedule - open my $fh, '>&', \*STDOUT - or die "Cannot save STDOUT as a runner print stream: $!"; - exit(! $runners{$format}->($plan, $fh)); + $want_formats{prettier} = 1 if not scalar keys %want_formats; + my @writes_to_stdout = grep { + $formatters{$_}->{writes_to_stdout} + } keys %want_formats; + if (scalar @writes_to_stdout > 1) { + my $joined = join ', ', map { "'$_'" } @writes_to_stdout; + die "$joined formatters all want to write to stdout\n"; + } + + my @filters = qw(x skip_version skip_missing_features + skip_runtime_check + enable_wanted_properties); + push @filters, 'skip_slow' if $plan->{skip_slow}; + push @filters, 'slow_only' if $plan->{slow_only}; + + my $runner = Cassandane::Unit::Runner->new(); + foreach my $f (keys %want_formats) { + $runner->add_formatter($formatters{$f}->{formatter}->()); + } + $runner->filter(@filters); + + exit !$runner->do_run($plan); } sub _listitem {