package TAP::Formatter::Console; use strict; use TAP::Base (); use POSIX qw(strftime); use vars qw($VERSION @ISA); @ISA = qw(TAP::Base); my $MAX_ERRORS = 5; my %VALIDATION_FOR; BEGIN { %VALIDATION_FOR = ( directives => sub { shift; shift }, verbosity => sub { shift; shift }, timer => sub { shift; shift }, failures => sub { shift; shift }, errors => sub { shift; shift }, color => sub { shift; shift }, jobs => sub { shift; shift }, stdout => sub { my ( $self, $ref ) = @_; $self->_croak("option 'stdout' needs a filehandle") unless ( ref $ref || '' ) eq 'GLOB' or eval { $ref->can('print') }; return $ref; }, ); my @getter_setters = qw( _longest _tests_without_extensions _printed_summary_header _colorizer ); for my $method ( @getter_setters, keys %VALIDATION_FOR ) { no strict 'refs'; *$method = sub { my $self = shift; return $self->{$method} unless @_; $self->{$method} = shift; }; } } =head1 NAME TAP::Formatter::Console - Harness output delegate for default console output =head1 VERSION Version 3.05 =cut $VERSION = '3.05'; =head1 DESCRIPTION This provides console orientated output formatting for TAP::Harness. =head1 SYNOPSIS use TAP::Formatter::Console; my $harness = TAP::Formatter::Console->new( \%args ); =cut sub _initialize { my ( $self, $arg_for ) = @_; $arg_for ||= {}; $self->SUPER::_initialize($arg_for); my %arg_for = %$arg_for; # force a shallow copy $self->verbosity(0); for my $name ( keys %VALIDATION_FOR ) { my $property = delete $arg_for{$name}; if ( defined $property ) { my $validate = $VALIDATION_FOR{$name}; $self->$name( $self->$validate($property) ); } } if ( my @props = keys %arg_for ) { $self->_croak( "Unknown arguments to " . __PACKAGE__ . "::new (@props)" ); } $self->stdout( \*STDOUT ) unless $self->stdout; if ( $self->color ) { require TAP::Formatter::Color; $self->_colorizer( TAP::Formatter::Color->new ); } return $self; } sub verbose { shift->verbosity >= 1 } sub quiet { shift->verbosity <= -1 } sub really_quiet { shift->verbosity <= -2 } sub silent { shift->verbosity <= -3 } =head1 METHODS =head2 Class Methods =head3 C my %args = ( verbose => 1, ) my $harness = TAP::Formatter::Console->new( \%args ); The constructor returns a new C object. If a L is created with no C a C is automatically created. If any of the following options were given to TAP::Harness->new they well be passed to this constructor which accepts an optional hashref whose allowed keys are: =over 4 =item * C Set the verbosity level. =item * C Printing individual test results to STDOUT. =item * C Append run time for each test to output. Uses L if available. =item * C Only show test failures (this is a no-op if C is selected). =item * C Suppressing some test output (mostly failures while tests are running). =item * C Suppressing everything but the tests summary. =item * C Suppressing all output. =item * C If parse errors are found in the TAP output, a note of this will be made in the summary report. To see all of the parse errors, set this argument to true: errors => 1 =item * C If set to a true value, only test results with directives will be displayed. This overrides other settings such as C or C. =item * C A filehandle for catching standard output. =item * C If defined specifies whether color output is desired. If C is not defined it will default to color output if color support is available on the current platform and output is not being redirected. =item * C The number of concurrent jobs this formatter will handle. =back Any keys for which the value is C will be ignored. =cut # new supplied by TAP::Base =head3 C Called by Test::Harness before any test output is generated. =cut sub prepare { my ( $self, @tests ) = @_; my $longest = 0; my $tests_without_extensions = 0; foreach my $test (@tests) { $longest = length $test if length $test > $longest; if ( $test !~ /\.\w+$/ ) { # TODO: Coverage? $tests_without_extensions = 1; } } $self->_tests_without_extensions($tests_without_extensions); $self->_longest($longest); } sub _format_now { strftime "[%H:%M:%S]", localtime } sub _format_name { my ( $self, $test ) = @_; my $name = $test; my $extra = 0; unless ( $self->_tests_without_extensions ) { $name =~ s/(\.\w+)$//; # strip the .t or .pm $extra = length $1; } my $periods = '.' x ( $self->_longest + $extra + 4 - length $test ); if ( $self->timer ) { my $stamp = $self->_format_now(); return "$stamp $name$periods"; } else { return "$name$periods"; } } =head3 C Called to create a new test session. A test session looks like this: my $session = $formatter->open_test( $test, $parser ); while ( defined( my $result = $parser->next ) ) { $session->result($result); exit 1 if $result->is_bailout; } $session->close_test; =cut sub open_test { my ( $self, $test, $parser ) = @_; my $class = $self->jobs > 1 ? 'TAP::Formatter::Console::ParallelSession' : 'TAP::Formatter::Console::Session'; eval "require $class"; $self->_croak($@) if $@; my $session = $class->new( { name => $test, formatter => $self, parser => $parser } ); $session->header; return $session; } =head3 C $harness->summary( $aggregate ); C prints the summary report after all tests are run. The argument is an aggregate. =cut sub summary { my ( $self, $aggregate ) = @_; return if $self->silent; my @t = $aggregate->descriptions; my $tests = \@t; my $runtime = $aggregate->elapsed_timestr; my $total = $aggregate->total; my $passed = $aggregate->passed; if ( $self->timer ) { $self->_output( $self->_format_now(), "\n" ); } # TODO: Check this condition still works when all subtests pass but # the exit status is nonzero if ( $aggregate->all_passed ) { $self->_output("All tests successful.\n"); } # ~TODO option where $aggregate->skipped generates reports if ( $total != $passed or $aggregate->has_problems ) { $self->_output("\nTest Summary Report"); $self->_output("\n-------------------\n"); foreach my $test (@$tests) { $self->_printed_summary_header(0); my ($parser) = $aggregate->parsers($test); $self->_output_summary_failure( 'failed', " Failed test number(s): ", $test, $parser ); $self->_output_summary_failure( 'todo_passed', " TODO passed: ", $test, $parser ); # ~TODO this cannot be the default #$self->_output_summary_failure( 'skipped', " Tests skipped: " ); if ( my $exit = $parser->exit ) { $self->_summary_test_header( $test, $parser ); $self->_failure_output(" Non-zero exit status: $exit\n"); } if ( my @errors = $parser->parse_errors ) { my $explain; if ( @errors > $MAX_ERRORS && !$self->errors ) { $explain = "Displayed the first $MAX_ERRORS of " . scalar(@errors) . " TAP syntax errors.\n" . "Re-run prove with the -p option to see them all.\n"; splice @errors, $MAX_ERRORS; } $self->_summary_test_header( $test, $parser ); $self->_failure_output( sprintf " Parse errors: %s\n", shift @errors ); foreach my $error (@errors) { my $spaces = ' ' x 16; $self->_failure_output("$spaces$error\n"); } $self->_failure_output($explain) if $explain; } } } my $files = @$tests; $self->_output("Files=$files, Tests=$total, $runtime\n"); my $status = $aggregate->get_status; $self->_output("Result: $status\n"); } sub _output_summary_failure { my ( $self, $method, $name, $test, $parser ) = @_; # ugly hack. Must rethink this :( my $output = $method eq 'failed' ? '_failure_output' : '_output'; if ( $parser->$method() ) { $self->_summary_test_header( $test, $parser ); $self->$output($name); my @results = $self->_balanced_range( 40, $parser->$method() ); $self->$output( sprintf "%s\n" => shift @results ); my $spaces = ' ' x 16; while (@results) { $self->$output( sprintf "$spaces%s\n" => shift @results ); } } } sub _summary_test_header { my ( $self, $test, $parser ) = @_; return if $self->_printed_summary_header; my $spaces = ' ' x ( $self->_longest - length $test ); $spaces = ' ' unless $spaces; my $output = $self->_get_output_method($parser); $self->$output( sprintf "$test$spaces(Wstat: %d Tests: %d Failed: %d)\n", $parser->wait, $parser->tests_run, scalar $parser->failed ); $self->_printed_summary_header(1); } sub _output { my $self = shift; print { $self->stdout } @_; } # Use _colorizer delegate to set output color. NOP if we have no delegate sub _set_colors { my ( $self, @colors ) = @_; if ( my $colorizer = $self->_colorizer ) { my $output_func = $self->{_output_func} ||= sub { $self->_output(@_); }; $colorizer->set_color( $output_func, $_ ) for @colors; } } sub _failure_output { my $self = shift; $self->_set_colors('red'); my $out = join '', @_; my $has_newline = chomp $out; $self->_output($out); $self->_set_colors('reset'); $self->_output($/) if $has_newline; } sub _balanced_range { my ( $self, $limit, @range ) = @_; @range = $self->_range(@range); my $line = ""; my @lines; my $curr = 0; while (@range) { if ( $curr < $limit ) { my $range = ( shift @range ) . ", "; $line .= $range; $curr += length $range; } elsif (@range) { $line =~ s/, $//; push @lines => $line; $line = ''; $curr = 0; } } if ($line) { $line =~ s/, $//; push @lines => $line; } return @lines; } sub _range { my ( $self, @numbers ) = @_; # shouldn't be needed, but subclasses might call this @numbers = sort { $a <=> $b } @numbers; my ( $min, @range ); foreach my $i ( 0 .. $#numbers ) { my $num = $numbers[$i]; my $next = $numbers[ $i + 1 ]; if ( defined $next && $next == $num + 1 ) { if ( !defined $min ) { $min = $num; } } elsif ( defined $min ) { push @range => "$min-$num"; undef $min; } else { push @range => $num; } } return @range; } sub _get_output_method { my ( $self, $parser ) = @_; return $parser->has_problems ? '_failure_output' : '_output'; } 1;