File Coverage

blib/lib/Test2/Harness/Renderer/EventStream.pm
Criterion Covered Total %
statement 328 341 96.1
branch 185 212 87.2
condition 81 99 81.8
subroutine 33 33 100.0
pod 0 8 0.0
total 627 693 90.4


line stmt bran cond sub pod time code
1             package Test2::Harness::Renderer::EventStream;
2 23     23   135152 use strict;
  23         25  
  23         552  
3 23     23   90 use warnings;
  23         23  
  23         1017  
4              
5             our $VERSION = '0.000013';
6              
7 23     23   92 use Test2::Util::HashBase qw/color verbose jobs slots parallel clear out_std watch colors graph_colors counter/;
  23         23  
  23         157  
8 23     23   18793 use Term::ANSIColor();
  23         112669  
  23         621  
9 23     23   116 use List::Util qw/first shuffle/;
  23         24  
  23         1844  
10 23     23   93 use Scalar::Util qw/blessed/;
  23         23  
  23         846  
11 23     23   531 use Time::HiRes qw/sleep/;
  23         1001  
  23         220  
12 23     23   10879 use Test2::Util::Term qw/term_size/;
  23         531148  
  23         137  
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   3359 for my $sig (qw/INT TERM/) {
47             my $old = $SIG{$sig} || sub {
48             $SIG{$sig} = 'DEFAULT';
49             kill $sig, $$;
50 46   50     439 };
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         59917 };
57             }
58             }
59              
60             END {
61 23 50   23   4776527 print STDOUT Term::ANSIColor::color('reset') if -t STDOUT;
62 23 50       430 print STDERR Term::ANSIColor::color('reset') if -t STDERR;
63             }
64              
65             sub init {
66 100     100 0 206690 my $self = shift;
67 100         255 $self->{+JOBS} = {};
68 100         220 $self->{+SLOTS} = [];
69 100         119 $self->{+CLEAR} = 0;
70              
71 100   66     337 my $fh = $self->{+OUT_STD} ||= do {
72 84 50       17956 open( my $out, '>&', STDOUT ) or die "Can't dup STDOUT: $!";
73              
74 84         372 my $old = select $out;
75 84         237 $| = 1;
76 84         200 select $old;
77              
78 84         325 $out;
79             };
80              
81 100         192 $self->{+COUNTER} = 0;
82              
83 100         261 my $is_term = -t $fh;
84 100 50       270 $self->{+COLOR} = $is_term ? 1 : 0 unless defined $self->{+COLOR};
    100          
85 100 50       386 $self->{+WATCH} = $is_term ? 1 : 0 unless defined $self->{+WATCH};
    100          
86 100 50 66     737 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       295 : $self->{+COLOR} ? \%DEFAULT_COLORS
    50          
93             : {};
94              
95 100 100       219 my $graph_colors = $self->{+COLOR} ? [@DEFAULT_GRAPH_COLORS] : [];
96              
97 100   50     453 $self->{+COLORS} ||= {map { $_ => eval { Term::ANSIColor::color($colors->{$_}) } || '' } grep {$colors->{$_}} keys %$colors, 'reset'};
  34   100     430  
  126         361  
98 100 50 100     586 $self->{+GRAPH_COLORS} ||= [map { eval { Term::ANSIColor::color($_) } || '' } grep {$_} @$graph_colors];
  16         129  
  16         22  
  16         15  
99             }
100              
101             sub paint {
102 918     918   19987 my $self = shift;
103 918         1918 my $string = "";
104              
105 918         1871 my $colors = $self->{+COLORS};
106 918         1394 my $graph = $self->{+GRAPH_COLORS};
107 918         1341 my $jobs = $self->{+JOBS};
108              
109 918 100       2127 if ($self->{+CLEAR}) {
110 2         5 $string .= "\e[K";
111 2         4 $self->{+CLEAR}--;
112             }
113              
114 918         2423 for my $i (@_) {
115 23162 100       27918 unless (ref($i)) {
116 8730         6042 $string .= $i;
117 8730         7206 next;
118             }
119              
120 14432         17219 my ($c, $s, $r) = @$i;
121 14432 100       21469 $r = 1 if @$i < 3;
122 14432 100       22022 if ($c =~ m/^\d+$/) {
123 2172 100 50     3591 $string .= $graph->[$jobs->{$c}->{slot} % @$graph] || '' if @$graph
124             }
125             else {
126 12260   100     32005 $string .= $colors->{lc($c)} || '';
127             }
128 14432         9889 $string .= $s;
129 14432 100 100     42615 $string .= $colors->{reset} || '' if $r;
130             }
131              
132 918         2336 my $fh = $self->{+OUT_STD};
133              
134 918         199955 print $fh $string;
135             }
136              
137             sub encoding {
138 323     323   1958 my $self = shift;
139 323         668 my ($enc) = @_;
140              
141 323         687 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       2374 if ($enc =~ m/^utf-?8$/i) {
146 323         1904 binmode($fh, ":utf8");
147             }
148             else {
149 0         0 binmode($fh, ":encoding($enc)");
150             }
151             }
152              
153             sub summary {
154 26     26 0 3625 my $self = shift;
155 26         115 my ($results) = @_;
156              
157 26         93 my @fail = grep {!$_->passed} @$results;
  434         1440  
158              
159 26 100       217 if (@fail) {
160 2         8 $self->paint("\n", ['failed', "=== FAILURE SUMMARY ===\n", 0]);
161 2         4 $self->paint(map { " * " . $_->name . "\n" } @fail);
  4         202  
162             }
163             else {
164 24         230 $self->paint("\n", ['passed', "=== ALL TESTS SUCCEEDED ===\n", 0]);
165             }
166              
167 26         251 $self->paint(['reset', '', 0], "\n");
168             }
169              
170             sub listen {
171 54     54 0 106 my $self = shift;
172 54     19785   332 sub { $self->process(@_) };
  19785         35263  
173             }
174              
175             sub init_job {
176 611     611 0 2970 my $self = shift;
177 611         698 my ($j) = @_;
178              
179 611         1159 my $jobs = $self->{+JOBS};
180 611         712 my $slots = $self->{+SLOTS};
181              
182 611         628 my $slot;
183 611         3602 for my $s (0 .. @$slots) {
184 613 100       1807 $slot = $s unless defined $slots->[$s];
185 613 100       1511 last if defined $slot;
186             }
187              
188 611         1744 $slots->[$slot] = $j;
189              
190 611         4459 return $jobs->{$j} = {slot => $slot};
191             }
192              
193             sub end_job {
194 605     605   5162 my $self = shift;
195 605         1261 my ($j) = @_;
196              
197 605         3422 my $job = delete $self->{+JOBS}->{$j};
198 605         7236 $self->{+SLOTS}->[$job->{slot}] = undef;
199             }
200              
201             sub update_state {
202 19787     19787   15397 my $self = shift;
203 19787         14697 my ($j, $event) = @_;
204              
205 19787         18785 $self->{+COUNTER}++;
206              
207 19787         21732 my $jobs = $self->{+JOBS};
208 19787   66     43562 my $job = $jobs->{$j} ||= $self->init_job($j);
209 19787         18859 $job->{counter}++;
210              
211 19787 100       77923 $self->encoding($event->encoding) if $event->isa('Test2::Event::Encoding');
212             }
213              
214             sub pick_renderer {
215 19246     19246   116336 my $self = shift;
216 19246         14775 my ($event) = @_;
217              
218 19246   100     29449 my $n = $event->nested || 0;
219              
220 19246 100       75744 return 'render' if $n < 0;
221              
222 19230 100       25337 if ($n == 0) {
223 4973 100       10382 return 'render' unless $event->subtest_id;
224 2002 100       8249 return 'render_subtest' unless $event->in_subtest;
225             }
226              
227 14265 100       20586 return 'render_orphan' unless $event->in_subtest;
228 14257 100       47144 return 'preview' if $self->{+WATCH};
229              
230 14249         27727 return;
231             }
232              
233             sub process {
234 19791     19791   17861 my $self = shift;
235 19791         15200 my ($j, $event) = @_;
236              
237 19791         33731 my $job_id = $j->id;
238 19791         55668 $self->update_state($job_id, $event);
239              
240 19791         19349 my $job = $self->{+JOBS}->{$job_id};
241              
242 19791         46521 my $is_end = $event->isa('Test2::Event::ProcessFinish');
243              
244 19791 100 100     61904 if ($event->isa('Test2::Event::ProcessStart') && !$self->{+VERBOSE}) {
245 603         1808 $job->{start} = $event;
246             }
247             else {
248 19188         29256 my @to_print = $self->_process($job_id, $event, $is_end);
249 19188 100       36760 $self->paint(@to_print) if @to_print;
250             }
251              
252 19791         29460 $self->do_watch;
253              
254 19791 100       56733 $self->end_job($job_id) if $is_end;
255             }
256              
257             sub _process {
258 19190     19190   28166 my $self = shift;
259 19190         20477 my ($j, $event, $is_end) = @_;
260 19190         17977 my $job = $self->{+JOBS}->{$j};
261              
262 19190 100       21773 my $meth = $self->pick_renderer($event) or return;
263 4949 100       32281 my @to_print = $self->$meth($j, $event) or return;
264 864 100       7038 my @start = $job->{start} ? $self->render($j, delete $job->{start}) : ();
265              
266 864 100       7841 return (@start, @to_print) unless $is_end;
267              
268 605 100       4295 my @errors = $self->_plan_errors($event->result->events, 0) or return @to_print;
269 2         444 my @tree = $self->tree($j, $event);
270              
271 2         11 @errors = map {(
272 4         47 ['tag', '['], ['fail',' PLAN '], ['tag', ']'],
273             ' ', @tree, ' ',
274             ['fail', $_],
275             "\n",
276             )} @errors;
277              
278 2         47 return (@start, @errors, @to_print);
279             }
280              
281             sub do_watch {
282 19791     19791   15883 my $self = shift;
283 19791 100       29707 return unless $self->{+WATCH};
284 4 100       12 return if $self->{+VERBOSE};
285              
286 2         4 my $jobs = $self->{+JOBS};
287              
288 2         5 my $size = length($self->{+COUNTER});
289              
290 2         9 $self->paint(" Events Seen: ", $self->{+COUNTER}, "\r");
291 2         4 $self->{+CLEAR} = 1;
292             }
293              
294             sub _tag {
295 21368     21368   22280 my $self = shift;
296 21368         18927 my ($event) = @_;
297              
298 21368 100       36223 return if $event->no_display;
299              
300 21364 100       90358 return ("LAUNCH", 'file')
301             if $event->isa('Test2::Event::ProcessStart');
302              
303 20158 100       45873 if ($event->isa('Test2::Event::ParserSelect')) {
304 605 100       2509 return unless $self->{+VERBOSE};
305 2         14 return ('PARSER', 'parser_select');
306             }
307              
308 19553 100       42828 if ($event->isa('Test2::Event::Subtest')) {
309 2061 100       4665 return ("FAILED", 'failed') if $event->causes_fail;
310              
311 2057   100     7950 my $n = $event->nested || 0;
312 2057 100 100     15362 return unless $self->{+VERBOSE} || $n < 0;
313              
314 18         21 my ($plan) = (grep { $_->isa('Test2::Event::Plan') } @{$event->subevents})[0];
  34         118  
  18         38  
315 18 100 66     66 if ($plan && $plan->directive && $plan->directive eq 'SKIP') {
      66        
316 4         71 return ("SKIP!!", 'skipall');
317             }
318              
319 14         105 return ("PASSED", 'passed');
320             }
321              
322 17492 100       37299 if ($event->isa('Test2::Event::ProcessFinish')) {
323 1202 100       2688 return ("NOTEST", "skipall") unless $event->result->ran_tests;
324 1126 50       1878 return ("PASSED", 'passed') if $event->result->passed;
325 0         0 return ("FAILED", 'failed');
326             }
327              
328 16290 100       31995 if ($event->isa('Test2::Event::Plan')) {
329 2692 100       5958 if ($event->directive eq 'SKIP') {
330 80         693 return ("SKIP!!", 'skipall');
331             }
332 2612 100       12232 return unless $self->{+VERBOSE};
333 10         34 return (" PLAN ", 'plan');
334             }
335              
336 13598 100       27967 if ($event->isa('Test2::Event::Encoding')) {
337 325 100       916 return unless $self->{+VERBOSE};
338 2         10 return ('ENCODE', 'encoding');
339             }
340              
341 13273 100 100     69898 if ($event->isa('Test2::Event::UnknownStdout') || $event->isa('Test2::Event::UnknownStderr')) {
342 8 50       21 return unless defined $event->output;
343              
344 8 100       64 return ("STDERR", 'stderr') if $event->isa('Test2::Event::UnknownStderr');
345 4 50       14 return (" DIAG ", 'diag') if $event->diagnostics;
346              
347 4 100       27 return unless $self->{+VERBOSE};
348 2         12 return ("STDOUT", 'stdout');
349             }
350              
351 13265 100 100     65597 if ($event->isa('Test2::Event::UnexpectedProcessExit') || $event->isa('Test2::Event::TimeoutReset')) {
352 132         321 return ("PARSER", 'parser');
353             }
354              
355 13133 100       20311 if ($event->increments_count) {
356 12403 100       36056 if ($self->{+VERBOSE}) {
357 24 100 66     108 return (" OK ", 'skip') if $event->can('reason') && defined $event->reason;
358 22 100 100     129 return ("NOT OK", 'todo') if $event->can('todo') && defined $event->todo;
359             # The event is a failure but something overrode that - this would
360             # be a failure inside a subtest marked as todo.
361 20 50 66     126 return ("NOT OK", 'todo') if !$event->pass && $event->effective_pass;
362 20 100       691 return (" OK ", 'pass') unless $event->causes_fail;
363             }
364              
365 12383 100       16411 return ("NOT OK", 'fail') if $event->causes_fail;
366 12375         36391 return;
367             }
368              
369 730 100       3674 if ($event->can('message')) {
370 710 100       1297 return (" DIAG ", 'diag') if $event->diagnostics;
371 404 100       1672 return unless $self->{+VERBOSE};
372 2         11 return (" NOTE ", 'note');
373             }
374              
375 20 100 100     101 return unless $self->{+VERBOSE} || $event->diagnostics;
376 14 100       73 return ("PARSER", 'parser') if $event->isa('Test2::Event::ParseError');
377              
378 10 100 66     24 return unless defined $event->summary && $event->summary =~ /\S/;
379              
380 8         115 return (" ???? ", 'unknown');
381             }
382              
383             sub tag {
384 19805     19805   21723 my $self = shift;
385 19805         13892 my ($event) = @_;
386              
387 19805         23386 my ($val, $color) = $self->_tag($event);
388              
389 19805 100       69440 return unless $val;
390             return (
391 1477         9199 ['tag', '['],
392             [$color, $val],
393             ['tag', ']'],
394             );
395             }
396              
397             sub tree {
398 3524     3524   4398 my $self = shift;
399 3524         3839 my ($j, $event) = @_;
400              
401             # Get mark
402 3524         4302 my $mark = '+';
403 3524 100       5227 if (!$event) {
404 2         4 $mark = '|';
405             }
406             else {
407 3522   100     8057 my $n = $event->nested || 0;
408 3522 100       22036 $mark = '_' if $event->isa('Test2::Event::ProcessStart');
409 3522 100 100     11222 $mark = '=' if $event->isa('Test2::Event::Subtest') && $n < 0;
410 3522 100       9933 $mark = '=' if $event->isa('Test2::Event::ProcessFinish');
411             }
412              
413 3524         3872 my $jobs = $self->{+JOBS};
414 3524         4132 my $slots = $self->{+SLOTS};
415              
416 3524         2938 my @marks;
417 3524         7602 for my $s (@$slots) {
418 3542 100       6328 if (!defined($s)) {
419 12         13 push @marks => (' ', ' ');
420 12         15 next;
421             }
422              
423 3530 100 100     9160 unless ($jobs->{$s}->{counter} > 1 || $j == $s) {
424 10         19 push @marks => ([$s, ':'], ' ');
425 10         12 next;
426             }
427              
428 3520 100 100     16365 if ($s == $j && $mark ne '|') {
429 3504 100       11384 push @marks => ([$mark eq '+' ? $s : 'mark', $mark], ' ');
430             }
431             else {
432 16         30 push @marks => ([$s, '|'], ' ');
433             }
434             }
435 3524         3283 pop @marks;
436 3524         6538 return @marks;
437             }
438              
439             sub painted_length {
440 1479     1479 0 5780 my $self = shift;
441 1479 100       2276 my $str = join '' => map { ref($_) ? $_->[1] : $_ } @_;
  8836         14115  
442 1479         3041 return length($str);
443             }
444              
445             sub event_summary {
446 1477     1477   1797 my $self = shift;
447 1477         1505 my ($event, $start) = @_;
448              
449 1477         2864 my ($val, $color) = $self->_tag($event);
450              
451 1477         7161 my $summary = $event->summary;
452              
453 1477         7343 $summary =~ s/^[\n\r]+//g;
454 1477 50       6191 my @lines = grep {defined $_ && length $_} split /[\n\r]+/, $summary;
  3317         12179  
455 1477 50       3004 @lines = ('') unless @lines;
456              
457 1477         3226 my $len = $self->painted_length(@$start) + 1;
458 1477         7582 my $term_size = term_size();
459              
460 1477         7162323 my @blob;
461 1477 100       2907 if (grep { $term_size <= $len + length($_) } @lines) {
  3317         7485  
462 51         305 @lines = ( ['blob', '----- START -----'] );
463 51         314 @blob = (
464             [$color, $summary],
465             "\n",
466             @$start,
467             ['blob', '------ END ------'],
468             "\n",
469             );
470             }
471             else {
472 1426         2150 @lines = map { [$color, $_] } @lines;
  3266         10251  
473             }
474              
475 1477         11608 return (\@lines, \@blob);
476             }
477              
478             sub render {
479 19803     19803   16554 my $self = shift;
480 19803         21519 my ($j, $event, @nest) = @_;
481              
482             # If there is no tag then we do not render it.
483 19803 100       25477 my @tag = $self->tag($event) or return;
484 1475         3193 my @tree = $self->tree($j, $event);
485 1475         3881 my @start = (@tag, ' ', @tree, ' ', @nest);
486              
487 1475         3730 my ($summary, $blob) = $self->event_summary($event, \@start);
488              
489 1475         1669 my @out;
490 1475         10943 push @out => (@start, $_, "\n") for @$summary;
491 1475 100       4115 push @out => @$blob if @$blob;
492              
493 1475         13192 return @out;
494             }
495              
496             sub render_orphan {
497 6     6 0 1749 my $self = shift;
498 6         9 my ($j, $event) = @_;
499              
500             # If there is no tag then we do not render it.
501 6 100       17 my @tag = $self->tag($event) or return;
502 4         22 my @tree = $self->tree($j, $event);
503 4         108 my @start = (@tag, ' ', @tree, ' ', [$j, ("> " x $event->nested)]);
504              
505 4         199 my ($summary, $blob) = $self->event_summary($event, \@start);
506              
507 4         12 my @out;
508 4         17 push @out => (@start, $_, "\n") for @$summary;
509 4 100       11 push @out => @$blob if @$blob;
510              
511 4         36 return @out;
512             }
513              
514             sub preview {
515 6     6 0 1784 my $self = shift;
516 6         8 my ($j, $event) = @_;
517              
518             # If there is no tag then we do not render it.
519 6 100       17 my @tag = $self->tag($event) or return;
520 4         21 my @tree = $self->tree($j, $event);
521 4         27 my @start = (@tag, ' ', @tree, ' ', [$j, ("> " x $event->nested)]);
522              
523 4         208 my ($summary) = $self->event_summary($event, \@start);
524              
525 4         15 $self->{+CLEAR} = 2;
526 4         33 return (@start, $summary->[-1], "\r");
527             }
528              
529             sub render_subtest {
530 1988     1988 0 2226 my $self = shift;
531 1988         1921 my ($j, $event) = @_;
532              
533 1988         3490 my @out = $self->render($j, $event);
534              
535 1988         1651 my @todo = @{$event->subevents};
  1988         4013  
536 1988         6415 my @stack = ($event);
537              
538 1988         3950 while (my $e = shift @todo) {
539 14253         11402 my $nest = "";
540              
541 14253 100       19336 if ($e->subtest_id) {
542 53         256 unshift @todo => @{$e->subevents};
  53         216  
543 53         215 push @stack => $e;
544              
545 53         163 $nest = '| ' x ($e->nested - 1);
546 53         247 $nest .= "+-";
547             }
548             else {
549 14200         37352 $nest = '| ' x $e->nested;
550             }
551              
552 14253 100 50     51830 if (!@todo || (($todo[0]->in_subtest || '') ne ($e->in_subtest || '') && !$e->subtest_id)) {
      50        
      100        
      66        
553 2041         4567 push @out => $self->render($j, $e, [$j, $nest]);
554              
555 2041         4376 my @tree = $self->tree($j, $e);
556              
557 2041 50       3864 if (my $st = pop @stack) {
558             push @out => (
559             ['tag', '['], ['fail', ' PLAN '], ['tag', ']'],
560             ' ', @tree, ' ',
561             [$j, $nest],
562             ['fail', $_],
563             "\n",
564 2041   100     3946 ) for $self->_plan_errors($st->subevents, ($st->nested || 0) + 1);
565             }
566              
567 2041 50 66     7657 if (@out && $self->{+VERBOSE}) {
568 4         15 my $n2 = '| ' x ($e->nested - 1);
569 4         56 push @out => (
570             " ", @tree, " ",
571             [$j, "$n2^"],
572             "\n",
573             );
574             }
575             }
576             else {
577 12212         79294 push @out => $self->render($j, $e, [$j, $nest]);
578             }
579             }
580              
581 1988         5455 return @out;
582             }
583              
584             sub _plan_errors {
585 2642     2642   20949 my $self = shift;
586 2642         2605 my $events = shift;
587 2642         1806 my $nested = shift;
588              
589 2642         2710 my @errors;
590              
591 2642 50 66     5797 unless ($nested || grep { $_->isa('Test2::Event::ProcessStart') } @{$events}) {
  19182         43525  
  601         1823  
592 0         0 push @errors => 'No process start event was seen!';
593 0         0 return;
594             }
595              
596 2642 100 100     2249 my @plans = grep { ($_->nested || 0) == $nested && $_->isa('Test2::Event::Plan') } @{$events};
  33435         152310  
  2642         3136  
597              
598 2642 50       17036 unless (@plans) {
599 0         0 push @errors => 'No plan was ever set.';
600 0         0 return;
601             }
602              
603 2642 50       4401 push @errors => 'Multiple plans were set.'
604             if @plans > 1;
605              
606 2642 100 66     11844 push @errors => 'Plan must come before or after all testing, not in the middle.'
607             unless $plans[0] == $events->[0] || $plans[0] == $events->[-1];
608              
609 2642         9542 my $max = ($plans[0]->sets_plan)[0];
610              
611 2642 100 100     12067 my $total = grep { ($_->nested || 0) == $nested && $_->increments_count } @{$events};
  33435         148088  
  2642         3084  
612 2642 50       28103 return if $max == $total;
613 0           push @errors => "Planned to run $max test(s) but ran $total.";
614              
615 0           return @errors;
616             }
617              
618             1;
619              
620             __END__