File Coverage

blib/lib/Test2/Harness/Renderer/EventStream.pm
Criterion Covered Total %
statement 325 338 96.1
branch 181 208 87.0
condition 81 99 81.8
subroutine 33 33 100.0
pod 0 8 0.0
total 620 686 90.3


line stmt bran cond sub pod time code
1             package Test2::Harness::Renderer::EventStream;
2 23     23   113947 use strict;
  23         46  
  23         601  
3 23     23   72 use warnings;
  23         24  
  23         1027  
4              
5             our $VERSION = '0.000012';
6              
7 23     23   69 use Test2::Util::HashBase qw/color verbose jobs slots parallel clear out_std watch colors graph_colors counter/;
  23         24  
  23         178  
8 23     23   18011 use Term::ANSIColor();
  23         117923  
  23         704  
9 23     23   178 use List::Util qw/first shuffle/;
  23         24  
  23         2134  
10 23     23   90 use Scalar::Util qw/blessed/;
  23         45  
  23         922  
11 23     23   481 use Time::HiRes qw/sleep/;
  23         935  
  23         179  
12 23     23   11638 use Test2::Util::Term qw/term_size/;
  23         105024  
  23         141  
13              
14             my @DEFAULT_GRAPH_COLORS = qw{
15             blue yellow cyan magenta
16             bright_blue bright_yellow bright_cyan bright_magenta
17             };
18              
19             my %DEFAULT_COLORS = (
20             blob => 'bold bright_black on_white',
21             tag => 'bold bright_white',
22             mark => 'bold bright_white',
23             diag => 'yellow',
24             stderr => 'yellow',
25             fail => 'bold red',
26             failed => 'bold red',
27             parser => 'magenta',
28             unknown => 'magenta',
29             pass => 'green',
30             passed => 'bold green',
31             reset => 'reset',
32             skip => 'bold white on_blue',
33             skipall => 'bold white on_blue',
34             todo => 'bold black on_bright_yellow',
35             file => 'bold bright_white',
36             );
37              
38             my %EXTENDED_COLORS = (
39             %DEFAULT_COLORS,
40             plan => 'cyan',
41             note => 'blue',
42             stdout => 'blue',
43             );
44              
45             BEGIN {
46 23     23   3086 for my $sig (qw/INT TERM/) {
47             my $old = $SIG{$sig} || sub {
48             $SIG{$sig} = 'DEFAULT';
49             kill $sig, $$;
50 46   50     498 };
51              
52             $SIG{$sig} = sub {
53 0 0       0 print STDOUT Term::ANSIColor::color('reset') if -t STDOUT;
54 0 0       0 print STDERR Term::ANSIColor::color('reset') if -t STDERR;
55 0         0 $old->();
56 46         54946 };
57             }
58             }
59              
60             END {
61 23 50   23   4718180 print STDOUT Term::ANSIColor::color('reset') if -t STDOUT;
62 23 50       401 print STDERR Term::ANSIColor::color('reset') if -t STDERR;
63             }
64              
65             sub init {
66 100     100 0 179679 my $self = shift;
67 100         296 $self->{+JOBS} = {};
68 100         186 $self->{+SLOTS} = [];
69 100         193 $self->{+CLEAR} = 0;
70              
71 100   66     358 my $fh = $self->{+OUT_STD} ||= do {
72 84 50       23027 open( my $out, '>&', STDOUT ) or die "Can't dup STDOUT: $!";
73              
74 84         380 my $old = select $out;
75 84         268 $| = 1;
76 84         233 select $old;
77              
78 84         306 $out;
79             };
80              
81 100         250 $self->{+COUNTER} = 0;
82              
83 100         241 my $is_term = -t $fh;
84 100 50       322 $self->{+COLOR} = $is_term ? 1 : 0 unless defined $self->{+COLOR};
    100          
85 100 50       409 $self->{+WATCH} = $is_term ? 1 : 0 unless defined $self->{+WATCH};
    100          
86 100 50 66     735 if (($is_term || $self->{+COLOR} || $self->{+WATCH}) && $^O eq 'MSWin32') {
      66        
87 0 0       0 eval { require Win32::Console::ANSI } and Win32::Console::ANSI->import;
  0         0  
88             }
89              
90             my $colors =
91             $self->{+COLOR} > 1 ? \%EXTENDED_COLORS
92 100 100       364 : $self->{+COLOR} ? \%DEFAULT_COLORS
    50          
93             : {};
94              
95 100 100       254 my $graph_colors = $self->{+COLOR} ? [@DEFAULT_GRAPH_COLORS] : [];
96              
97 100   50     570 $self->{+COLORS} ||= {map { $_ => eval { Term::ANSIColor::color($colors->{$_}) } || '' } grep {$colors->{$_}} keys %$colors, 'reset'};
  34   100     399  
  126         471  
98 100 50 100     642 $self->{+GRAPH_COLORS} ||= [map { eval { Term::ANSIColor::color($_) } || '' } grep {$_} @$graph_colors];
  16         128  
  16         20  
  16         12  
99             }
100              
101             sub paint {
102 880     880   24632 my $self = shift;
103 880         1974 my $string = "";
104              
105 880         1598 my $colors = $self->{+COLORS};
106 880         1736 my $graph = $self->{+GRAPH_COLORS};
107 880         1407 my $jobs = $self->{+JOBS};
108              
109 880 100       1890 if ($self->{+CLEAR}) {
110 2         5 $string .= "\e[K";
111 2         5 $self->{+CLEAR}--;
112             }
113              
114 880         3143 for my $i (@_) {
115 22554 100       27674 unless (ref($i)) {
116 8502         5611 $string .= $i;
117 8502         7519 next;
118             }
119              
120 14052         16441 my ($c, $s, $r) = @$i;
121 14052 100       25409 $r = 1 if @$i < 3;
122 14052 100       23024 if ($c =~ m/^\d+$/) {
123 2134 100 50     2972 $string .= $graph->[$jobs->{$c}->{slot} % @$graph] || '' if @$graph
124             }
125             else {
126 11918   100     28775 $string .= $colors->{lc($c)} || '';
127             }
128 14052         10079 $string .= $s;
129 14052 100 100     40148 $string .= $colors->{reset} || '' if $r;
130             }
131              
132 880         1235 my $fh = $self->{+OUT_STD};
133              
134 880         249521 print $fh $string;
135             }
136              
137             sub encoding {
138 323     323   2070 my $self = shift;
139 323         541 my ($enc) = @_;
140              
141 323         624 my $fh = $self->{+OUT_STD};
142             # https://rt.perl.org/Public/Bug/Display.html?id=31923
143             # If utf8 is requested we use ':utf8' instead of ':encoding(utf8)' in
144             # order to avoid the thread segfault.
145 323 50       2367 if ($enc =~ m/^utf-?8$/i) {
146 323         1694 binmode($fh, ":utf8");
147             }
148             else {
149 0         0 binmode($fh, ":encoding($enc)");
150             }
151             }
152              
153             sub summary {
154 26     26 0 3221 my $self = shift;
155 26         70 my ($results) = @_;
156              
157 26         92 my @fail = grep {!$_->passed} @$results;
  434         1399  
158              
159 26 100       128 if (@fail) {
160 2         12 $self->paint("\n", ['failed', "=== FAILURE SUMMARY ===\n", 0]);
161 2         3 $self->paint(map { " * " . $_->name . "\n" } @fail);
  4         167  
162             }
163             else {
164 24         166 $self->paint("\n", ['passed', "=== ALL TESTS SUCCEEDED ===\n", 0]);
165             }
166              
167 26         245 $self->paint(['reset', '', 0], "\n");
168             }
169              
170             sub listen {
171 54     54 0 87 my $self = shift;
172 19731     19731   40186 sub { $self->process(@_) }
173 54         463 };
174              
175             sub init_job {
176 611     611 0 2811 my $self = shift;
177 611         844 my ($j) = @_;
178              
179 611         1061 my $jobs = $self->{+JOBS};
180 611         930 my $slots = $self->{+SLOTS};
181              
182 611         1660 my $slot;
183 611         3487 for my $s (0 .. @$slots) {
184 613 100       2033 $slot = $s unless defined $slots->[$s];
185 613 100       1843 last if defined $slot;
186             }
187              
188 611         1386 $slots->[$slot] = $j;
189              
190 611         3818 return $jobs->{$j} = {slot => $slot};
191             }
192              
193             sub end_job {
194 605     605   4010 my $self = shift;
195 605         1271 my ($j) = @_;
196              
197 605         1618 my $job = delete $self->{+JOBS}->{$j};
198 605         4150 $self->{+SLOTS}->[$job->{slot}] = undef;
199             }
200              
201             sub update_state {
202 19733     19733   15427 my $self = shift;
203 19733         16262 my ($j, $event) = @_;
204              
205 19733         18476 $self->{+COUNTER}++;
206              
207 19733         17235 my $jobs = $self->{+JOBS};
208 19733   66     40951 my $job = $jobs->{$j} ||= $self->init_job($j);
209 19733         20668 $job->{counter}++;
210              
211 19733 100       83273 $self->encoding($event->encoding) if $event->isa('Test2::Event::Encoding');
212             }
213              
214             sub pick_renderer {
215 19192     19192   115637 my $self = shift;
216 19192         14184 my ($event) = @_;
217              
218 19192   100     34331 my $n = $event->nested || 0;
219              
220 19192 100       77964 return 'render' if $n < 0;
221              
222 19176 100       28034 if ($n == 0) {
223 4973 100       10851 return 'render' unless $event->subtest_id;
224 2002 100       9019 return 'render_subtest' unless $event->in_subtest;
225             }
226              
227 14211 100       21615 return 'render_orphan' unless $event->in_subtest;
228 14203 100       48123 return 'preview' if $self->{+WATCH};
229              
230 14195         31716 return;
231             }
232              
233             sub process {
234 19737     19737   16985 my $self = shift;
235 19737         16402 my ($j, $event) = @_;
236              
237 19737         36314 my $job_id = $j->id;
238 19737         61812 $self->update_state($job_id, $event);
239              
240 19737         20447 my $job = $self->{+JOBS}->{$job_id};
241              
242 19737         47607 my $is_end = $event->isa('Test2::Event::ProcessFinish');
243              
244 19737 100 100     66452 if ($event->isa('Test2::Event::ProcessStart') && !$self->{+VERBOSE}) {
245 603         2720 $job->{start} = $event;
246             }
247             else {
248 19134         28743 my @to_print = $self->_process($job_id, $event, $is_end);
249 19134 100       33765 $self->paint(@to_print) if @to_print;
250             }
251              
252 19737         27877 $self->do_watch;
253              
254 19737 100       61900 $self->end_job($job_id) if $is_end;
255             }
256              
257             sub _process {
258 19136     19136   25905 my $self = shift;
259 19136         18176 my ($j, $event, $is_end) = @_;
260 19136         17043 my $job = $self->{+JOBS}->{$j};
261              
262 19136 100       24405 my $meth = $self->pick_renderer($event) or return;
263 4949 100       36001 my @to_print = $self->$meth($j, $event) or return;
264 826 100       4059 my @start = $job->{start} ? $self->render($j, delete $job->{start}) : ();
265              
266 826 100       11292 return (@start, @to_print) unless $is_end;
267              
268 605 100       2743 my @errors = $self->_plan_errors($event->result->events, 0) or return @to_print;
269 2         321 my @tree = $self->tree($j, $event);
270              
271 2         9 @errors = map {(
272 4         32 ['tag', '['], ['fail',' PLAN '], ['tag', ']'],
273             ' ', @tree, ' ',
274             ['fail', $_],
275             "\n",
276             )} @errors;
277              
278 2         28 return (@start, @errors, @to_print);
279             }
280              
281             sub do_watch {
282 19737     19737   17115 my $self = shift;
283 19737 100       32979 return unless $self->{+WATCH};
284 4 100       14 return if $self->{+VERBOSE};
285              
286 2         4 my $jobs = $self->{+JOBS};
287              
288 2         5 my $size = length($self->{+COUNTER});
289              
290 2         8 $self->paint(" Events Seen: ", $self->{+COUNTER}, "\r");
291 2         4 $self->{+CLEAR} = 1;
292             }
293              
294             sub _tag {
295 21272     21272   20957 my $self = shift;
296 21272         14947 my ($event) = @_;
297              
298 21272 100       37538 return if $event->no_display;
299              
300 21268 100       93079 return ("LAUNCH", 'file')
301             if $event->isa('Test2::Event::ProcessStart');
302              
303 20062 100       49724 if ($event->isa('Test2::Event::ParserSelect')) {
304 605 100       2617 return unless $self->{+VERBOSE};
305 2         12 return ('PARSER', 'parser_select');
306             }
307              
308 19457 100       46153 if ($event->isa('Test2::Event::Subtest')) {
309 2061 100       4637 return ("FAILED", 'failed') if $event->causes_fail;
310              
311 2057   100     8820 my $n = $event->nested || 0;
312 2057 100 100     16429 return unless $self->{+VERBOSE} || $n < 0;
313              
314 18         19 my ($plan) = (grep { $_->isa('Test2::Event::Plan') } @{$event->subevents})[0];
  34         99  
  18         29  
315 18 100 66     57 if ($plan && $plan->directive && $plan->directive eq 'SKIP') {
      66        
316 4         60 return ("SKIP!!", 'skipall');
317             }
318              
319 14         85 return ("PASSED", 'passed');
320             }
321              
322 17396 100       39618 if ($event->isa('Test2::Event::ProcessFinish')) {
323 1202 50       4322 return ("PASSED", 'passed') if $event->result->passed;
324 0         0 return ("FAILED", 'failed');
325             }
326              
327 16194 100       32580 if ($event->isa('Test2::Event::Plan')) {
328 2650 100       5909 return unless $self->{+VERBOSE};
329 10         22 return (" PLAN ", 'plan');
330             }
331              
332 13544 100       27607 if ($event->isa('Test2::Event::Encoding')) {
333 325 100       1319 return unless $self->{+VERBOSE};
334 2         15 return ('ENCODE', 'encoding');
335             }
336              
337 13219 100 100     70601 if ($event->isa('Test2::Event::UnknownStdout') || $event->isa('Test2::Event::UnknownStderr')) {
338 8 50       20 return unless defined $event->output;
339              
340 8 100       57 return ("STDERR", 'stderr') if $event->isa('Test2::Event::UnknownStderr');
341 4 50       13 return (" DIAG ", 'diag') if $event->diagnostics;
342              
343 4 100       23 return unless $self->{+VERBOSE};
344 2         16 return ("STDOUT", 'stdout');
345             }
346              
347 13211 100 100     66907 if ($event->isa('Test2::Event::UnexpectedProcessExit') || $event->isa('Test2::Event::TimeoutReset')) {
348 132         792 return ("PARSER", 'parser');
349             }
350              
351 13079 100       22372 if ($event->increments_count) {
352 12349 100       37214 if ($self->{+VERBOSE}) {
353 24 100 66     75 return (" OK ", 'skip') if $event->can('reason') && defined $event->reason;
354 22 100 100     104 return ("NOT OK", 'todo') if $event->can('todo') && defined $event->todo;
355             # The event is a failure but something overrode that - this would
356             # be a failure inside a subtest marked as todo.
357 20 50 66     111 return ("NOT OK", 'todo') if !$event->pass && $event->effective_pass;
358 20 100       674 return (" OK ", 'pass') unless $event->causes_fail;
359             }
360              
361 12329 100       17004 return ("NOT OK", 'fail') if $event->causes_fail;
362 12321         38386 return;
363             }
364              
365 730 100       3737 if ($event->can('message')) {
366 710 100       1544 return (" DIAG ", 'diag') if $event->diagnostics;
367 404 100       2109 return unless $self->{+VERBOSE};
368 2         12 return (" NOTE ", 'note');
369             }
370              
371 20 100 100     99 return unless $self->{+VERBOSE} || $event->diagnostics;
372 14 100       62 return ("PARSER", 'parser') if $event->isa('Test2::Event::ParseError');
373              
374 10 100 66     23 return unless defined $event->summary && $event->summary =~ /\S/;
375              
376 8         101 return (" ???? ", 'unknown');
377             }
378              
379             sub tag {
380 19751     19751   25604 my $self = shift;
381 19751         13846 my ($event) = @_;
382              
383 19751         21852 my ($val, $color) = $self->_tag($event);
384              
385 19751 100       74968 return unless $val;
386             return (
387 1439         9739 ['tag', '['],
388             [$color, $val],
389             ['tag', ']'],
390             );
391             }
392              
393             sub tree {
394 3486     3486   5270 my $self = shift;
395 3486         4016 my ($j, $event) = @_;
396              
397             # Get mark
398 3486         4116 my $mark = '+';
399 3486 100       6294 if (!$event) {
400 2         5 $mark = '|';
401             }
402             else {
403 3484   100     7958 my $n = $event->nested || 0;
404 3484 100       19746 $mark = '_' if $event->isa('Test2::Event::ProcessStart');
405 3484 100 100     11426 $mark = '=' if $event->isa('Test2::Event::Subtest') && $n < 0;
406 3484 100       9415 $mark = '=' if $event->isa('Test2::Event::ProcessFinish');
407             }
408              
409 3486         4677 my $jobs = $self->{+JOBS};
410 3486         3827 my $slots = $self->{+SLOTS};
411              
412 3486         3660 my @marks;
413 3486         7196 for my $s (@$slots) {
414 3504 100       6301 if (!defined($s)) {
415 12         16 push @marks => (' ', ' ');
416 12         15 next;
417             }
418              
419 3492 100 100     8328 unless ($jobs->{$s}->{counter} > 1 || $j == $s) {
420 10         17 push @marks => ([$s, ':'], ' ');
421 10         9 next;
422             }
423              
424 3482 100 100     15109 if ($s == $j && $mark ne '|') {
425 3466 100       11838 push @marks => ([$mark eq '+' ? $s : 'mark', $mark], ' ');
426             }
427             else {
428 16         32 push @marks => ([$s, '|'], ' ');
429             }
430             }
431 3486         3862 pop @marks;
432 3486         7311 return @marks;
433             }
434              
435             sub painted_length {
436 1441     1441 0 5066 my $self = shift;
437 1441 100       2378 my $str = join '' => map { ref($_) ? $_->[1] : $_ } @_;
  8608         15136  
438 1441         3086 return length($str);
439             }
440              
441             sub event_summary {
442 1439     1439   1745 my $self = shift;
443 1439         1469 my ($event, $start) = @_;
444              
445 1439         2269 my ($val, $color) = $self->_tag($event);
446              
447 1439         6707 my $summary = $event->summary;
448              
449 1439         6084 $summary =~ s/^[\n\r]+//g;
450 1439 50       6505 my @lines = grep {defined $_ && length $_} split /[\n\r]+/, $summary;
  3279         10981  
451 1439 50       3260 @lines = ('') unless @lines;
452              
453 1439         4581 my $len = $self->painted_length(@$start) + 1;
454 1439         6254 my $term_size = term_size();
455              
456 1439         6412 my @blob;
457 1439 100       2145 if (grep { $term_size <= $len + length($_) } @lines) {
  3279         6103  
458 51         196 @lines = ( ['blob', '----- START -----'] );
459 51         231 @blob = (
460             [$color, $summary],
461             "\n",
462             @$start,
463             ['blob', '------ END ------'],
464             "\n",
465             );
466             }
467             else {
468 1388         1518 @lines = map { [$color, $_] } @lines;
  3228         5568  
469             }
470              
471 1439         4341 return (\@lines, \@blob);
472             }
473              
474             sub render {
475 19749     19749   17643 my $self = shift;
476 19749         20647 my ($j, $event, @nest) = @_;
477              
478             # If there is no tag then we do not render it.
479 19749 100       25182 my @tag = $self->tag($event) or return;
480 1437         3411 my @tree = $self->tree($j, $event);
481 1437         5175 my @start = (@tag, ' ', @tree, ' ', @nest);
482              
483 1437         4060 my ($summary, $blob) = $self->event_summary($event, \@start);
484              
485 1437         1663 my @out;
486 1437         7962 push @out => (@start, $_, "\n") for @$summary;
487 1437 100       6342 push @out => @$blob if @$blob;
488              
489 1437         10057 return @out;
490             }
491              
492             sub render_orphan {
493 6     6 0 1781 my $self = shift;
494 6         8 my ($j, $event) = @_;
495              
496             # If there is no tag then we do not render it.
497 6 100       14 my @tag = $self->tag($event) or return;
498 4         19 my @tree = $self->tree($j, $event);
499 4         26 my @start = (@tag, ' ', @tree, ' ', [$j, ("> " x $event->nested)]);
500              
501 4         198 my ($summary, $blob) = $self->event_summary($event, \@start);
502              
503 4         10 my @out;
504 4         15 push @out => (@start, $_, "\n") for @$summary;
505 4 100       11 push @out => @$blob if @$blob;
506              
507 4         28 return @out;
508             }
509              
510             sub preview {
511 6     6 0 1806 my $self = shift;
512 6         8 my ($j, $event) = @_;
513              
514             # If there is no tag then we do not render it.
515 6 100       15 my @tag = $self->tag($event) or return;
516 4         23 my @tree = $self->tree($j, $event);
517 4         29 my @start = (@tag, ' ', @tree, ' ', [$j, ("> " x $event->nested)]);
518              
519 4         200 my ($summary) = $self->event_summary($event, \@start);
520              
521 4         23 $self->{+CLEAR} = 2;
522 4         37 return (@start, $summary->[-1], "\r");
523             }
524              
525             sub render_subtest {
526 1988     1988 0 2213 my $self = shift;
527 1988         2434 my ($j, $event) = @_;
528              
529 1988         3874 my @out = $self->render($j, $event);
530              
531 1988         1920 my @todo = @{$event->subevents};
  1988         4425  
532 1988         7671 my @stack = ($event);
533              
534 1988         5133 while (my $e = shift @todo) {
535 14199         10384 my $nest = "";
536              
537 14199 100       20743 if ($e->subtest_id) {
538 53         289 unshift @todo => @{$e->subevents};
  53         268  
539 53         252 push @stack => $e;
540              
541 53         195 $nest = '| ' x ($e->nested - 1);
542 53         331 $nest .= "+-";
543             }
544             else {
545 14146         39878 $nest = '| ' x $e->nested;
546             }
547              
548 14199 100 50     52749 if (!@todo || (($todo[0]->in_subtest || '') ne ($e->in_subtest || '') && !$e->subtest_id)) {
      50        
      100        
      66        
549 2041         4790 push @out => $self->render($j, $e, [$j, $nest]);
550              
551 2041         4435 my @tree = $self->tree($j, $e);
552              
553 2041 50       4156 if (my $st = pop @stack) {
554             push @out => (
555             ['tag', '['], ['fail', ' PLAN '], ['tag', ']'],
556             ' ', @tree, ' ',
557             [$j, $nest],
558             ['fail', $_],
559             "\n",
560 2041   100     4167 ) for $self->_plan_errors($st->subevents, ($st->nested || 0) + 1);
561             }
562              
563 2041 50 66     8199 if (@out && $self->{+VERBOSE}) {
564 4         7 my $n2 = '| ' x ($e->nested - 1);
565 4         30 push @out => (
566             " ", @tree, " ",
567             [$j, "$n2^"],
568             "\n",
569             );
570             }
571             }
572             else {
573 12158         86788 push @out => $self->render($j, $e, [$j, $nest]);
574             }
575             }
576              
577 1988         6653 return @out;
578             }
579              
580             sub _plan_errors {
581 2642     2642   17897 my $self = shift;
582 2642         2615 my $events = shift;
583 2642         1730 my $nested = shift;
584              
585 2642         2160 my @errors;
586              
587 2642 50 66     5680 unless ($nested || grep { $_->isa('Test2::Event::ProcessStart') } @{$events}) {
  19128         44604  
  601         1617  
588 0         0 push @errors => 'No process start event was seen!';
589 0         0 return;
590             }
591              
592 2642 100 100     2299 my @plans = grep { ($_->nested || 0) == $nested && $_->isa('Test2::Event::Plan') } @{$events};
  33327         164642  
  2642         3327  
593              
594 2642 50       18444 unless (@plans) {
595 0         0 push @errors => 'No plan was ever set.';
596 0         0 return;
597             }
598              
599 2642 50       4873 push @errors => 'Multiple plans were set.'
600             if @plans > 1;
601              
602 2642 100 66     12601 push @errors => 'Plan must come before or after all testing, not in the middle.'
603             unless $plans[0] == $events->[0] || $plans[0] == $events->[-1];
604              
605 2642         7484 my $max = ($plans[0]->sets_plan)[0];
606              
607 2642 100 100     11028 my $total = grep { ($_->nested || 0) == $nested && $_->increments_count } @{$events};
  33327         164472  
  2642         3592  
608 2642 50       27752 return if $max == $total;
609 0           push @errors => "Planned to run $max test(s) but ran $total.";
610              
611 0           return @errors;
612             }
613              
614             1;
615              
616             __END__