| 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.10 |
| |
| =cut |
| |
| $VERSION = '3.10'; |
| |
| =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<new> |
| |
| my %args = ( |
| verbose => 1, |
| ) |
| my $harness = TAP::Formatter::Console->new( \%args ); |
| |
| The constructor returns a new C<TAP::Formatter::Console> object. If |
| a L<TAP::Harness> is created with no C<formatter> a |
| C<TAP::Formatter::Console> 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<verbosity> |
| |
| Set the verbosity level. |
| |
| =item * C<verbose> |
| |
| Printing individual test results to STDOUT. |
| |
| =item * C<timer> |
| |
| Append run time for each test to output. Uses L<Time::HiRes> if available. |
| |
| =item * C<failures> |
| |
| Only show test failures (this is a no-op if C<verbose> is selected). |
| |
| =item * C<quiet> |
| |
| Suppressing some test output (mostly failures while tests are running). |
| |
| =item * C<really_quiet> |
| |
| Suppressing everything but the tests summary. |
| |
| =item * C<silent> |
| |
| Suppressing all output. |
| |
| =item * C<errors> |
| |
| 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<directives> |
| |
| If set to a true value, only test results with directives will be displayed. |
| This overrides other settings such as C<verbose> or C<failures>. |
| |
| =item * C<stdout> |
| |
| A filehandle for catching standard output. |
| |
| =item * C<color> |
| |
| If defined specifies whether color output is desired. If C<color> 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<jobs> |
| |
| The number of concurrent jobs this formatter will handle. |
| |
| =back |
| |
| Any keys for which the value is C<undef> will be ignored. |
| |
| =cut |
| |
| # new supplied by TAP::Base |
| |
| =head3 C<prepare> |
| |
| 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<open_test> |
| |
| 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<summary> |
| |
| $harness->summary( $aggregate ); |
| |
| C<summary> 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: ', ' Failed tests: ' ], |
| $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 ( my @r = $parser->$method() ) { |
| $self->_summary_test_header( $test, $parser ); |
| my ( $singular, $plural ) |
| = 'ARRAY' eq ref $name ? @$name : ( $name, $name ); |
| $self->$output( @r == 1 ? $singular : $plural ); |
| my @results = $self->_balanced_range( 40, @r ); |
| $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; |