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