1package TAP::Formatter::Base; 2 3use strict; 4use warnings; 5use base 'TAP::Base'; 6use POSIX qw(strftime); 7 8my $MAX_ERRORS = 5; 9my %VALIDATION_FOR; 10 11BEGIN { 12 %VALIDATION_FOR = ( 13 directives => sub { shift; shift }, 14 verbosity => sub { shift; shift }, 15 normalize => sub { shift; shift }, 16 timer => sub { shift; shift }, 17 failures => sub { shift; shift }, 18 comments => sub { shift; shift }, 19 errors => sub { shift; shift }, 20 color => sub { shift; shift }, 21 jobs => sub { shift; shift }, 22 show_count => sub { shift; shift }, 23 stdout => sub { 24 my ( $self, $ref ) = @_; 25 26 $self->_croak("option 'stdout' needs a filehandle") 27 unless $self->_is_filehandle($ref); 28 29 return $ref; 30 }, 31 ); 32 33 sub _is_filehandle { 34 my ( $self, $ref ) = @_; 35 36 return 0 if !defined $ref; 37 38 return 1 if ref $ref eq 'GLOB'; # lexical filehandle 39 return 1 if !ref $ref && ref \$ref eq 'GLOB'; # bare glob like *STDOUT 40 41 return 1 if eval { $ref->can('print') }; 42 43 return 0; 44 } 45 46 my @getter_setters = qw( 47 _longest 48 _printed_summary_header 49 _colorizer 50 ); 51 52 __PACKAGE__->mk_methods( @getter_setters, keys %VALIDATION_FOR ); 53} 54 55=head1 NAME 56 57TAP::Formatter::Base - Base class for harness output delegates 58 59=head1 VERSION 60 61Version 3.43 62 63=cut 64 65our $VERSION = '3.43'; 66 67=head1 DESCRIPTION 68 69This provides console orientated output formatting for TAP::Harness. 70 71=head1 SYNOPSIS 72 73 use TAP::Formatter::Console; 74 my $harness = TAP::Formatter::Console->new( \%args ); 75 76=cut 77 78sub _initialize { 79 my ( $self, $arg_for ) = @_; 80 $arg_for ||= {}; 81 82 $self->SUPER::_initialize($arg_for); 83 my %arg_for = %$arg_for; # force a shallow copy 84 85 $self->verbosity(0); 86 87 for my $name ( keys %VALIDATION_FOR ) { 88 my $property = delete $arg_for{$name}; 89 if ( defined $property ) { 90 my $validate = $VALIDATION_FOR{$name}; 91 $self->$name( $self->$validate($property) ); 92 } 93 } 94 95 if ( my @props = keys %arg_for ) { 96 $self->_croak( 97 "Unknown arguments to " . __PACKAGE__ . "::new (@props)" ); 98 } 99 100 $self->stdout( \*STDOUT ) unless $self->stdout; 101 102 if ( $self->color ) { 103 require TAP::Formatter::Color; 104 $self->_colorizer( TAP::Formatter::Color->new ); 105 } 106 107 return $self; 108} 109 110sub verbose { shift->verbosity >= 1 } 111sub quiet { shift->verbosity <= -1 } 112sub really_quiet { shift->verbosity <= -2 } 113sub silent { shift->verbosity <= -3 } 114 115=head1 METHODS 116 117=head2 Class Methods 118 119=head3 C<new> 120 121 my %args = ( 122 verbose => 1, 123 ) 124 my $harness = TAP::Formatter::Console->new( \%args ); 125 126The constructor returns a new C<TAP::Formatter::Console> object. If 127a L<TAP::Harness> is created with no C<formatter> a 128C<TAP::Formatter::Console> is automatically created. If any of the 129following options were given to TAP::Harness->new they well be passed to 130this constructor which accepts an optional hashref whose allowed keys are: 131 132=over 4 133 134=item * C<verbosity> 135 136Set the verbosity level. 137 138=item * C<verbose> 139 140Printing individual test results to STDOUT. 141 142=item * C<timer> 143 144Append run time for each test to output. Uses L<Time::HiRes> if available. 145 146=item * C<failures> 147 148Show test failures (this is a no-op if C<verbose> is selected). 149 150=item * C<comments> 151 152Show test comments (this is a no-op if C<verbose> is selected). 153 154=item * C<quiet> 155 156Suppressing some test output (mostly failures while tests are running). 157 158=item * C<really_quiet> 159 160Suppressing everything but the tests summary. 161 162=item * C<silent> 163 164Suppressing all output. 165 166=item * C<errors> 167 168If parse errors are found in the TAP output, a note of this will be made 169in the summary report. To see all of the parse errors, set this argument to 170true: 171 172 errors => 1 173 174=item * C<directives> 175 176If set to a true value, only test results with directives will be displayed. 177This overrides other settings such as C<verbose>, C<failures>, or C<comments>. 178 179=item * C<stdout> 180 181A filehandle for catching standard output. 182 183=item * C<color> 184 185If defined specifies whether color output is desired. If C<color> is not 186defined it will default to color output if color support is available on 187the current platform and output is not being redirected. 188 189=item * C<jobs> 190 191The number of concurrent jobs this formatter will handle. 192 193=item * C<show_count> 194 195Boolean value. If false, disables the C<X/Y> test count which shows up while 196tests are running. 197 198=back 199 200Any keys for which the value is C<undef> will be ignored. 201 202=cut 203 204# new supplied by TAP::Base 205 206=head3 C<prepare> 207 208Called by Test::Harness before any test output is generated. 209 210This is an advisory and may not be called in the case where tests are 211being supplied to Test::Harness by an iterator. 212 213=cut 214 215sub prepare { 216 my ( $self, @tests ) = @_; 217 218 my $longest = 0; 219 220 for my $test (@tests) { 221 $longest = length $test if length $test > $longest; 222 } 223 224 $self->_longest($longest); 225} 226 227sub _format_now { strftime "[%H:%M:%S]", localtime } 228 229sub _format_name { 230 my ( $self, $test ) = @_; 231 my $name = $test; 232 my $periods = '.' x ( $self->_longest + 2 - length $test ); 233 $periods = " $periods "; 234 235 if ( $self->timer ) { 236 my $stamp = $self->_format_now(); 237 return "$stamp $name$periods"; 238 } 239 else { 240 return "$name$periods"; 241 } 242 243} 244 245=head3 C<open_test> 246 247Called to create a new test session. A test session looks like this: 248 249 my $session = $formatter->open_test( $test, $parser ); 250 while ( defined( my $result = $parser->next ) ) { 251 $session->result($result); 252 exit 1 if $result->is_bailout; 253 } 254 $session->close_test; 255 256=cut 257 258sub open_test { 259 die "Unimplemented."; 260} 261 262sub _output_success { 263 my ( $self, $msg ) = @_; 264 $self->_output($msg); 265} 266 267=head3 C<summary> 268 269 $harness->summary( $aggregate ); 270 271C<summary> prints the summary report after all tests are run. The first 272argument is an aggregate to summarise. An optional second argument may 273be set to a true value to indicate that the summary is being output as a 274result of an interrupted test run. 275 276=cut 277 278sub summary { 279 my ( $self, $aggregate, $interrupted ) = @_; 280 281 return if $self->silent; 282 283 my @t = $aggregate->descriptions; 284 my $tests = \@t; 285 286 my $runtime = $aggregate->elapsed_timestr; 287 288 my $total = $aggregate->total; 289 my $passed = $aggregate->passed; 290 291 if ( $self->timer ) { 292 $self->_output( $self->_format_now(), "\n" ); 293 } 294 295 $self->_failure_output("Test run interrupted!\n") 296 if $interrupted; 297 298 # TODO: Check this condition still works when all subtests pass but 299 # the exit status is nonzero 300 301 if ( $aggregate->all_passed ) { 302 $self->_output_success("All tests successful.\n"); 303 } 304 305 # ~TODO option where $aggregate->skipped generates reports 306 if ( $total != $passed or $aggregate->has_problems ) { 307 $self->_output("\nTest Summary Report"); 308 $self->_output("\n-------------------\n"); 309 for my $test (@$tests) { 310 $self->_printed_summary_header(0); 311 my ($parser) = $aggregate->parsers($test); 312 $self->_output_summary_failure( 313 'failed', 314 [ ' Failed test: ', ' Failed tests: ' ], 315 $test, $parser 316 ); 317 $self->_output_summary_failure( 318 'todo_passed', 319 " TODO passed: ", $test, $parser 320 ); 321 322 # ~TODO this cannot be the default 323 #$self->_output_summary_failure( 'skipped', " Tests skipped: " ); 324 325 if ( my $exit = $parser->exit ) { 326 $self->_summary_test_header( $test, $parser ); 327 $self->_failure_output(" Non-zero exit status: $exit\n"); 328 } 329 elsif ( my $wait = $parser->wait ) { 330 $self->_summary_test_header( $test, $parser ); 331 $self->_failure_output(" Non-zero wait status: $wait\n"); 332 } 333 334 if ( my @errors = $parser->parse_errors ) { 335 my $explain; 336 if ( @errors > $MAX_ERRORS && !$self->errors ) { 337 $explain 338 = "Displayed the first $MAX_ERRORS of " 339 . scalar(@errors) 340 . " TAP syntax errors.\n" 341 . "Re-run prove with the -p option to see them all.\n"; 342 splice @errors, $MAX_ERRORS; 343 } 344 $self->_summary_test_header( $test, $parser ); 345 $self->_failure_output( 346 sprintf " Parse errors: %s\n", 347 shift @errors 348 ); 349 for my $error (@errors) { 350 my $spaces = ' ' x 16; 351 $self->_failure_output("$spaces$error\n"); 352 } 353 $self->_failure_output($explain) if $explain; 354 } 355 } 356 } 357 my $files = @$tests; 358 $self->_output("Files=$files, Tests=$total, $runtime\n"); 359 my $status = $aggregate->get_status; 360 $self->_output("Result: $status\n"); 361} 362 363sub _output_summary_failure { 364 my ( $self, $method, $name, $test, $parser ) = @_; 365 366 # ugly hack. Must rethink this :( 367 my $output = $method eq 'failed' ? '_failure_output' : '_output'; 368 369 if ( my @r = $parser->$method() ) { 370 $self->_summary_test_header( $test, $parser ); 371 my ( $singular, $plural ) 372 = 'ARRAY' eq ref $name ? @$name : ( $name, $name ); 373 $self->$output( @r == 1 ? $singular : $plural ); 374 my @results = $self->_balanced_range( 40, @r ); 375 $self->$output( sprintf "%s\n" => shift @results ); 376 my $spaces = ' ' x 16; 377 while (@results) { 378 $self->$output( sprintf "$spaces%s\n" => shift @results ); 379 } 380 } 381} 382 383sub _summary_test_header { 384 my ( $self, $test, $parser ) = @_; 385 return if $self->_printed_summary_header; 386 my $spaces = ' ' x ( $self->_longest - length $test ); 387 $spaces = ' ' unless $spaces; 388 my $output = $self->_get_output_method($parser); 389 my $wait = $parser->wait; 390 defined $wait or $wait = '(none)'; 391 $self->$output( 392 sprintf "$test$spaces(Wstat: %s Tests: %d Failed: %d)\n", 393 $wait, $parser->tests_run, scalar $parser->failed 394 ); 395 $self->_printed_summary_header(1); 396} 397 398sub _output { 399 my $self = shift; 400 401 print { $self->stdout } @_; 402} 403 404sub _failure_output { 405 my $self = shift; 406 407 $self->_output(@_); 408} 409 410sub _balanced_range { 411 my ( $self, $limit, @range ) = @_; 412 @range = $self->_range(@range); 413 my $line = ""; 414 my @lines; 415 my $curr = 0; 416 while (@range) { 417 if ( $curr < $limit ) { 418 my $range = ( shift @range ) . ", "; 419 $line .= $range; 420 $curr += length $range; 421 } 422 elsif (@range) { 423 $line =~ s/, $//; 424 push @lines => $line; 425 $line = ''; 426 $curr = 0; 427 } 428 } 429 if ($line) { 430 $line =~ s/, $//; 431 push @lines => $line; 432 } 433 return @lines; 434} 435 436sub _range { 437 my ( $self, @numbers ) = @_; 438 439 # shouldn't be needed, but subclasses might call this 440 @numbers = sort { $a <=> $b } @numbers; 441 my ( $min, @range ); 442 443 for my $i ( 0 .. $#numbers ) { 444 my $num = $numbers[$i]; 445 my $next = $numbers[ $i + 1 ]; 446 if ( defined $next && $next == $num + 1 ) { 447 if ( !defined $min ) { 448 $min = $num; 449 } 450 } 451 elsif ( defined $min ) { 452 push @range => "$min-$num"; 453 undef $min; 454 } 455 else { 456 push @range => $num; 457 } 458 } 459 return @range; 460} 461 462sub _get_output_method { 463 my ( $self, $parser ) = @_; 464 return $parser->has_problems ? '_failure_output' : '_output'; 465} 466 4671; 468