1 package Test2::Formatter::TAP;
5 our $VERSION = '1.302198';
7 use Test2::Util qw/clone_io/;
9 use Test2::Util::HashBase qw{
10 no_numbers handles _encoding _last_fh
17 BEGIN { require Test2::Formatter; our @ISA = qw(Test2::Formatter) }
21 if (!defined $supports_tables) {
22 local $SIG{__DIE__} = 'DEFAULT';
25 = ($INC{'Term/Table.pm'} && $INC{'Term/Table/Util.pm'})
26 || eval { require Term::Table; require Term::Table::Util; 1 }
29 return $supports_tables;
34 my $old_fh = select $fh;
42 sub hide_buffered { 1 }
47 $self->{+HANDLES} ||= $self->_open_handles;
48 if(my $enc = delete $self->{encoding}) {
49 $self->encoding($enc);
57 my $out = clone_io(Test2::API::test2_stdout());
58 my $err = clone_io(Test2::API::test2_stderr());
69 if ($] ge "5.007003" and @_) {
71 my $handles = $self->{+HANDLES};
73 # https://rt.perl.org/Public/Bug/Display.html?id=31923
74 # If utf8 is requested we use ':utf8' instead of ':encoding(utf8)' in
75 # order to avoid the thread segfault.
76 if ($enc =~ m/^utf-?8$/i) {
77 binmode($_, ":utf8") for @$handles;
80 binmode($_, ":encoding($enc)") for @$handles;
82 $self->{+_ENCODING} = $enc;
85 return $self->{+_ENCODING};
89 no warnings 'redefine';
93 my ($self, $e, $num, $f) = @_;
95 # The most common case, a pass event with no amnesty and a normal name.
96 return if $self->print_optimal_pass($e, $num);
98 $f ||= $e->facet_data;
100 $self->encoding($f->{control}->{encoding}) if $f->{control}->{encoding};
102 my @tap = $self->event_tap($f, $num) or return;
104 $self->{+MADE_ASSERTION} = 1 if $f->{assert};
106 my $nesting = $f->{trace}->{nested} || 0;
107 my $handles = $self->{+HANDLES};
108 my $indent = ' ' x $nesting;
110 # Local is expensive! Only do it if we really need to.
111 local($\, $,) = (undef, '') if $\ || $,;
113 no warnings 'uninitialized';
114 my ($hid, $msg) = @$set;
116 my $io = $handles->[$hid] or next;
119 if $ENV{HARNESS_ACTIVE}
121 && $self->{+_LAST_FH} != $io
122 && $msg =~ m/^#\s*Failed( \(TODO\))? test /;
124 $msg =~ s/^/$indent/mg if $nesting;
126 $self->{+_LAST_FH} = $io;
130 sub print_optimal_pass {
131 my ($self, $e, $num) = @_;
135 # Only optimal if this is a Pass or a passing Ok
136 return unless $type eq 'Test2::Event::Pass' || ($type eq 'Test2::Event::Ok' && $e->{pass});
138 # Amnesty requires further processing (todo is a form of amnesty)
139 return if ($e->{amnesty} && @{$e->{amnesty}}) || defined($e->{todo});
141 # A name with a newline or hash symbol needs extra processing
142 return if defined($e->{name}) && (-1 != index($e->{name}, "\n") || -1 != index($e->{name}, '#'));
145 $ok .= " $num" if $num && !$self->{+NO_NUMBERS};
146 $ok .= defined($e->{name}) ? " - $e->{name}\n" : "\n";
148 if (my $nesting = $e->{trace}->{nested}) {
149 my $indent = ' ' x $nesting;
153 my $io = $self->{+HANDLES}->[OUT_STD];
155 local($\, $,) = (undef, '') if $\ || $,;
157 $self->{+_LAST_FH} = $io;
163 my ($self, $f, $num) = @_;
167 # If this IS the first event the plan should come first
168 # (plan must be before or after assertions, not in the middle)
169 push @tap => $self->plan_tap($f) if $f->{plan} && !$self->{+MADE_ASSERTION};
171 # The assertion is most important, if present.
173 push @tap => $self->assert_tap($f, $num);
174 push @tap => $self->debug_tap($f, $num) unless $f->{assert}->{no_debug} || $f->{assert}->{pass};
177 # Almost as important as an assertion
178 push @tap => $self->error_tap($f) if $f->{errors};
180 # Now lets see the diagnostics messages
181 push @tap => $self->info_tap($f) if $f->{info};
183 # If this IS NOT the first event the plan should come last
184 # (plan must be before or after assertions, not in the middle)
185 push @tap => $self->plan_tap($f) if $self->{+MADE_ASSERTION} && $f->{plan};
188 push @tap => $self->halt_tap($f) if $f->{control}->{halt};
191 return @tap if $f->{control}->{halt};
192 return @tap if grep { $f->{$_} } qw/assert plan info errors/;
194 # Use the summary as a fallback if nothing else is usable.
195 return $self->summary_tap($f, $num);
202 my $IO = ($f->{amnesty} && @{$f->{amnesty}}) ? OUT_STD : OUT_ERR;
205 my $details = $_->{details};
209 require Data::Dumper;
210 my $dumper = Data::Dumper->new([$details])->Indent(2)->Terse(1)->Pad('# ')->Useqq(1)->Sortkeys(1);
211 chomp($msg = $dumper->Dump);
214 chomp($msg = $details);
226 my $plan = $f->{plan} or return;
228 return if $plan->{none};
231 my $reason = $plan->{details} or return [OUT_STD, "1..0 # SKIP\n"];
233 return [OUT_STD, '1..0 # SKIP ' . $reason . "\n"];
236 return [OUT_STD, "1.." . $plan->{count} . "\n"];
239 sub no_subtest_space { 0 }
244 my $assert = $f->{assert} or return;
245 my $pass = $assert->{pass};
246 my $name = $assert->{details};
248 my $ok = $pass ? 'ok' : 'not ok';
249 $ok .= " $num" if $num && !$self->{+NO_NUMBERS};
251 # The regex form is ~250ms, the index form is ~50ms
254 (index($name, "\n") != -1 && (($name, @extra) = split(/\n\r?/, $name, -1))),
255 ((index($name, "#" ) != -1 || substr($name, -1) eq '\\') && (($name =~ s|\\|\\\\|g), ($name =~ s|#|\\#|g)))
258 my $extra_space = @extra ? ' ' x (length($ok) + 2) : '';
259 my $extra_indent = '';
261 my ($directives, $reason, $is_skip);
265 for my $am (@{$f->{amnesty}}) {
266 next if $am->{inherited};
267 my $tag = $am->{tag} or next;
268 $is_skip = 1 if $tag eq 'skip';
270 $directives{$tag} ||= $am->{details};
275 # Sort so that TODO comes before skip even on systems where lc sorts
276 # before uc, as other code depends on that ordering.
277 my @order = grep { !$seen{$_}++ } sort { lc $b cmp lc $a } keys %directives;
279 $directives = ' # ' . join ' & ' => @order;
281 for my $tag ('skip', @order) {
282 next unless defined($directives{$tag}) && length($directives{$tag});
283 $reason = $directives{$tag};
288 $ok .= " - $name" if defined $name && !($is_skip && !$name);
291 if ($f->{parent} && $f->{parent}->{buffered}) {
294 # In a verbose harness we indent the extra since they will appear
295 # inside the subtest braces. This helps readability. In a non-verbose
296 # harness we do not do this because it is less readable.
297 if ($ENV{HARNESS_IS_VERBOSE} || !$ENV{HARNESS_ACTIVE}) {
302 # Render the sub-events, we use our own counter for these.
307 # Bump the count for any event that should bump it.
308 $count++ if $f2->{assert};
310 # This indents all output lines generated for the sub-events.
311 # index 0 is the filehandle, index 1 is the message we want to indent.
312 map { $_->[1] =~ s/^(.*\S.*)$/ $1/mg; $_ } $self->event_tap($f2, $count);
313 } @{$f->{parent}->{children}};
315 push @subtap => [OUT_STD, "}\n"];
319 $directives = ' # TODO & SKIP' if $directives eq ' # TODO & skip';
321 $ok .= " $reason" if defined($reason);
324 $extra_space = ' ' if $self->no_subtest_space;
326 my @out = ([OUT_STD, "$ok\n"]);
327 push @out => map {[OUT_STD, "${extra_indent}#${extra_space}$_\n"]} @extra if @extra;
328 push @out => @subtap;
334 my ($self, $f, $num) = @_;
336 # Figure out the debug info, this is typically the file name and line
337 # number, but can also be a custom message. If no trace object is provided
338 # then we have nothing useful to display.
339 my $name = $f->{assert}->{details};
340 my $trace = $f->{trace};
342 my $debug = "[No trace info available]";
343 if ($trace->{details}) {
344 $debug = $trace->{details};
346 elsif ($trace->{frame}) {
347 my ($pkg, $file, $line) = @{$trace->{frame}};
348 $debug = "at $file line $line." if $file && $line;
351 my $amnesty = $f->{amnesty} && @{$f->{amnesty}}
355 # Create the initial diagnostics. If the test has a name we put the debug
356 # info on a second line, this behavior is inherited from Test::Builder.
357 my $msg = defined($name)
358 ? qq[# Failed test${amnesty} '$name'\n# $debug\n]
359 : qq[# Failed test${amnesty} $debug\n];
361 my $IO = $f->{amnesty} && @{$f->{amnesty}} ? OUT_STD : OUT_ERR;
369 return if $f->{trace}->{nested} && !$f->{trace}->{buffered};
370 my $details = $f->{control}->{details};
372 return [OUT_STD, "Bail out!\n"] unless defined($details) && length($details);
373 return [OUT_STD, "Bail out! $details\n"];
380 my $details = $_->{details};
381 my $table = $_->{table};
383 my $IO = $_->{debug} && !($f->{amnesty} && @{$f->{amnesty}}) ? OUT_ERR : OUT_STD;
386 if ($table && $self->supports_tables) {
387 $msg = join "\n" => map { "# $_" } Term::Table->new(
388 header => $table->{header},
389 rows => $table->{rows},
390 collapse => $table->{collapse},
391 no_collapse => $table->{no_collapse},
394 max_width => $self->calc_table_size($f),
397 elsif (ref($details)) {
398 require Data::Dumper;
399 my $dumper = Data::Dumper->new([$details])->Indent(2)->Terse(1)->Pad('# ')->Useqq(1)->Sortkeys(1);
400 chomp($msg = $dumper->Dump);
403 chomp($msg = $details);
413 my ($self, $f, $num) = @_;
415 return if $f->{about}->{no_display};
417 my $summary = $f->{about}->{details} or return;
419 $summary =~ s/^/# /smg;
421 return [OUT_STD, "$summary\n"];
424 sub calc_table_size {
428 my $term = Term::Table::Util::term_size();
429 my $nesting = 2 + (($f->{trace}->{nested} || 0) * 4); # 4 spaces per level, also '# ' prefix
430 my $total = $term - $nesting;
432 # Sane minimum width, any smaller and we are asking for pain
433 return 50 if $total < 50;
448 Test2::Formatter::TAP - Standard TAP formatter
452 This is what takes events and turns them into TAP.
456 use Test2::Formatter::TAP;
457 my $tap = Test2::Formatter::TAP->new();
460 $tap->encoding('utf8');
462 $tap->write($event, $number); # Output an event
468 =item $bool = $tap->no_numbers
470 =item $tap->set_no_numbers($bool)
472 Use to turn numbers on and off.
474 =item $arrayref = $tap->handles
476 =item $tap->set_handles(\@handles);
478 Can be used to get/set the filehandles. Indexes are identified by the
479 C<OUT_STD> and C<OUT_ERR> constants.
481 =item $encoding = $tap->encoding
483 =item $tap->encoding($encoding)
485 Get or set the encoding. By default no encoding is set, the original settings
486 of STDOUT and STDERR are used.
488 This directly modifies the stored filehandles, it does not create new ones.
490 =item $tap->write($e, $num)
492 Write an event to the console.
498 The source code repository for Test2 can be found at
499 F<http://github.com/Test-More/test-more/>.
505 =item Chad Granum E<lt>exodist@cpan.orgE<gt>
513 =item Chad Granum E<lt>exodist@cpan.orgE<gt>
515 =item Kent Fredric E<lt>kentnl@cpan.orgE<gt>
521 Copyright 2020 Chad Granum E<lt>exodist@cpan.orgE<gt>.
523 This program is free software; you can redistribute it and/or
524 modify it under the same terms as Perl itself.
526 See F<http://dev.perl.org/licenses/>