File Coverage

blib/lib/Test2/Aggregate.pm
Criterion Covered Total %
statement 145 145 100.0
branch 82 82 100.0
condition 18 18 100.0
subroutine 19 19 100.0
pod 1 1 100.0
total 265 265 100.0


line stmt bran cond sub pod time code
1             package Test2::Aggregate;
2              
3 16     16   3071788 use strict;
  16         96  
  16         421  
4 16     16   67 use warnings;
  16         27  
  16         358  
5              
6 16     16   67 use File::Find;
  16         31  
  16         842  
7 16     16   83 use File::Path;
  16         32  
  16         866  
8 16     16   7372 use File::Slurp;
  16         329022  
  16         935  
9              
10 16     16   533 use Test2::V0 'subtest';
  16         126717  
  16         110  
11              
12             =head1 NAME
13              
14             Test2::Aggregate - Aggregate tests for increased speed
15              
16             =head1 SYNOPSIS
17              
18             use Test2::Aggregate;
19             use Test2::V0; # Or 'use Test::More' etc if your suite uses an other framework
20              
21             Test2::Aggregate::run_tests(
22             dirs => \@test_dirs
23             );
24              
25             done_testing();
26              
27             =head1 VERSION
28              
29             Version 0.15
30              
31             =cut
32              
33             our $VERSION = '0.15';
34              
35             =head1 DESCRIPTION
36              
37             Aggregates all tests specified with C (which can even be individual tests)
38             to avoid forking, reloading etc that can help with performance (dramatically if
39             you have numerous small tests) and also facilitate group profiling. It is quite
40             common to have tests that take over a second of startup time for milliseconds of
41             actual runtime - L removes that overhead.
42             Test files are expected to end in B<.t> and are run as subtests of a single
43             aggregate test.
44              
45             A bit similar (mainly in intent) to L, but no inspiration was
46             drawn from the specific module, so simpler in concept and execution, which
47             makes it much more likely to work with your test suite (especially if you use modern
48             tools like L). It does not even try to package each test by default
49             (there is an option), which may be good or bad, depending on your requirements.
50              
51             Generally, the way to use this module is to try to aggregate sets of quick tests
52             (e.g. unit tests). Try to iterativelly add tests to the aggregator, using the C
53             option, so you can easily edit and remove those that do not work. Trying an entire,
54             large, suite in one go is not a good idea, as an incompatible test can break the
55             run making the subsequent tests fail (especially when doing things like globally
56             redefining built-ins etc) - see the module usage notes for help.
57              
58             The module can work with L / L suites, but you will
59             have less issues with L (see notes).
60              
61             =head1 METHODS
62            
63             =head2 C
64              
65             my $stats = Test2::Aggregate::run_tests(
66             dirs => \@dirs, # optional if lists defined
67             lists => \@lists, # optional if dirs defined
68             exclude => qr/exclude_regex/, # optional
69             include => qr/include_regex/, # optional
70             root => '/testroot/', # optional
71             load_modules => \@modules, # optional
72             package => 0, # optional
73             shuffle => 0, # optional
74             sort => 0, # optional
75             reverse => 0, # optional
76             unique => 1, # optional
77             repeat => 1, # optional, requires Test2::Plugin::BailOnFail for < 0
78             slow => 0, # optional
79             override => \%override, # optional, requires Sub::Override
80             stats_output => $stats_output_path, # optional
81             extend_stats => 0, # optional
82             test_warnings => 0, # optional
83             allow_errors => 0, # optional
84             pre_eval => $code_to_eval, # optional
85             dry_run => 0 # optional
86             );
87              
88             Runs the aggregate tests. Returns a hashref with stats like this:
89              
90             $stats = {
91             'test.t' => {
92             'test_no' => 1, # numbering starts at 1
93             'pass_perc' => 100, # for single runs pass/fail is 100/0
94             'timestamp' => '20190705T145043', # start of test
95             'time' => '0.1732', # seconds - only with stats_output
96             'warnings' => $STDERR # only with test_warnings on non empty STDERR
97             }
98             };
99              
100             The parameters to pass:
101              
102             =over 4
103            
104             =item * C (either this or C is required)
105              
106             An arrayref containing directories which will be searched recursively, or even
107             individual tests. The directories (unless C or C are true)
108             will be processed and tests run in order specified. Test files are expected to
109             end in C<.t>.
110              
111             =item * C (either this or C is required)
112              
113             Arrayref of flat files from which each line will be pushed to C (so they
114             have a lower precedence - note C still applies, don't include it in the
115             paths inside the list files). If the path does not exist, it will currently be
116             silently ignored, however the "official" way to skip a line without checking it
117             as a path is to start with a C<#> to denote a comment.
118              
119             This option is nicely combined with the C<--exclude-list> option of C (the
120             L) to skip the individual runs of the tests you aggregated.
121              
122             =item * C (optional)
123              
124             A regex to filter out tests that you want excluded.
125              
126             =item * C (optional)
127              
128             A regex which the tests have to match in order to be included in the test run.
129             Applied after C.
130              
131             =item * C (optional)
132              
133             If defined, must be a valid root directory that will prefix all C and
134             C items. You may want to set it to C<'./'> if you want dirs relative
135             to the current directory and the dot is not in your C<@INC>.
136              
137             =item * C (optional)
138              
139             Arrayref with modules to be loaded (with C) at the start of the
140             test. Useful for testing modules with special namespace requirements.
141              
142             =item * C (optional)
143              
144             Will package each test in its own namespace. While it may help avoid things like
145             redefine warnings, from experience, it can break some tests, so it is disabled
146             by default.
147              
148             =item * C (optional)
149              
150             Pass L compatible key/values as a hashref.
151              
152             =item * C (optional)
153              
154             Number of times to repeat the test(s) (default is 1 for a single run). If
155             C is negative, L is required, as the tests
156             will repeat until they bail on a failure. It can be combined with C
157             in which case a warning will also cause the test run to end.
158              
159             =item * C (optional)
160              
161             From v0.11, duplicate tests are by default removed from the running list as that
162             could mess up the stats output. You can still define it as false to allow duplicate
163             tests in the list.
164              
165             =item * C (optional)
166              
167             Sort tests alphabetically if set to true. Provides a way to fix the test order
168             across systems.
169              
170             =item * C (optional)
171              
172             Random order of tests if set to true. Will override C.
173              
174             =item * C (optional)
175              
176             Reverse order of tests if set to true.
177              
178             =item * C (optional)
179              
180             When true, tests will be skipped if the environment variable C is set.
181              
182             =item * C (optional)
183              
184             Tests for warnings over all the tests if set to true - this is added as a final
185             test which expects zero as the number of tests which had STDERR output.
186             The STDERR output of each test will be printed at the end of the test run (and
187             included in the test run result hash), so if you want to see warnings the moment
188             they are generated leave this option disabled.
189              
190             =item * C (optional)
191              
192             If enabled, it will allow errors that exit tests prematurely (so they may return
193             a pass if one of their subtests had passed). The option is available to enable
194             old behaviour (version <= 0.12), before the module stopped allowing this.
195              
196             =item * C (optional)
197              
198             Instead of running the tests, will do C for each one. Otherwise,
199             test order, stats files etc. will be produced (as if all tests passed).
200              
201             =item * C (optional)
202              
203             String with code to run with eval before each test. You might be inclined to do
204             this for example:
205              
206             pre_eval => "no warnings 'redefine';"
207              
208             You might expect it to silence redefine warnings (when you have similarly named
209             subs on many tests), but even if you don't set warnings explicitly in your tests,
210             most test bundles will set warnings automatically for you (e.g. for L
211             you'd have to do C 1;> to avoid it).
212              
213             =item * C (optional)
214              
215             C specifies a path where a file will be created to print out
216             running time per test (average if multiple iterations) and passing percentage.
217             Output is sorted from slowest test to fastest. On negative C the stats
218             of each successful run will be written separately instead of the averages.
219             The name of the file is C.
220             If C<'-'> is passed instead of a path, then the output will be written to STDOUT.
221             The timing stats are useful because the test harness doesn't normally measure
222             time per subtest (remember, your individual aggregated tests become subtests).
223             If you prefer to capture the hash output of the function and use that for your
224             reports, you still need to define C to enable timing (just send
225             the output to C, C etc).
226              
227             =item * C (optional)
228              
229             This option exist to make the default output format of C be fixed,
230             but still allow additions in future versions that will only be written with the
231             C option enabled.
232             Additions with C as of the current version:
233              
234             =over 4
235              
236             - starting date/time in ISO_8601.
237              
238             =back
239              
240             =back
241              
242             =cut
243              
244             sub run_tests {
245 37     37 1 67862 my %args = @_;
246             Test2::V0::plan skip_all => 'Skipping slow tests.'
247 37 100 100     137 if $args{slow} && $ENV{SKIP_SLOW};
248              
249 36         68 eval "use $_;" foreach @{$args{load_modules}};
  36         169  
250 36         965 local $ENV{AGGREGATE_TESTS} = 1;
251              
252 36 100       122 my $override = $args{override} ? _override($args{override}) : undef;
253 36         73 my @dirs = ();
254 36   100     185 my $root = $args{root} || '';
255 36         63 my @tests;
256              
257 36 100       126 @dirs = @{$args{dirs}} if $args{dirs};
  27         68  
258 36 100 100     128 $root .= '/' unless !$root || $root =~ m#/$#;
259              
260 36 100 100     196 if ($root && ! -e $root) {
261 2         26 warn "Root '$root' does not exist, no tests are loaded."
262             } else {
263 34         62 foreach my $file (@{$args{lists}}) {
  34         116  
264             push @dirs,
265 11 100       71 map { /^\s*#/ ? () : $_ }
  33         1487  
266             split( /\r?\n/, read_file("$root$file") );
267             }
268              
269             find(
270 112 100   112   3559 sub {push @tests, $File::Find::name if /\.t$/},
271 34 100       291 grep {-e} map {$root . $_} @dirs
  47         3174  
  47         163  
272             )
273             if @dirs;
274             }
275              
276 36 100       276 $args{unique} = 1 unless defined $args{unique};
277 36   100     196 $args{repeat} ||= 1;
278              
279 36         152 _process_run_order(\@tests, \%args);
280              
281 36         125 my @stack = caller();
282 36   100     137 $args{caller} = $stack[1] || 'aggregate';
283 36         422 $args{caller} =~ s#^.*?([^/]+)$#$1#;
284              
285 36         76 my $warnings = [];
286 36 100       132 if ($args{repeat} < 0) {
    100          
287 3     4   212 eval 'use Test2::Plugin::BailOnFail';
  4         29  
  4         8  
  4         21  
288 3         39 my $iter = 0;
289 3         14 while (!@$warnings) {
290 4         6 $iter++;
291 4         119 print "Test suite iteration $iter\n";
292 4 100       23 if ($args{test_warnings}) {
293             $warnings = _process_warnings(
294 3     3   40 Test2::V0::warnings{_run_tests(\@tests, \%args)},
295 3         20 \%args
296             );
297             } else {
298 1         3 _run_tests(\@tests, \%args);
299             }
300             }
301             } elsif ($args{test_warnings}) {
302             $warnings = _process_warnings(
303 5     5   65 Test2::V0::warnings { _run_tests(\@tests, \%args) },
304 5         30 \%args
305             );
306 5         39 Test2::V0::is(
307             @$warnings,
308             0,
309             'No warnings in the aggregate tests.'
310             );
311             } else {
312 28         93 _run_tests(\@tests, \%args);
313             }
314              
315 34 100       5865 warn "Test warning output:\n".join("\n", @$warnings)."\n"
316             if @$warnings;
317              
318 34         699 return $args{stats};
319             }
320              
321             sub _process_run_order {
322 36     36   63 my $tests = shift;
323 36         61 my $args = shift;
324              
325 36 100       135 @$tests = grep(!/$args->{exclude}/, @$tests) if $args->{exclude};
326 36 100       118 @$tests = grep(/$args->{include}/, @$tests) if $args->{include};
327              
328 36 100       151 @$tests = _uniq(@$tests) if $args->{unique};
329 36 100       112 @$tests = reverse @$tests if $args->{reverse};
330              
331 36 100       131 if ($args->{shuffle}) {
    100          
332 1         6 require List::Util;
333 1         6 @$tests = List::Util::shuffle @$tests;
334             } elsif ($args->{sort}) {
335 3         11 @$tests = sort @$tests;
336             }
337             }
338              
339             sub _process_warnings {
340 8     8   124 my $warnings = shift;
341 8         14 my $args = shift;
342 8         87 my @warnings = split(/<-Test2::Aggregate\n/, join('',@$warnings));
343 8         20 my @clean = ();
344              
345 8         17 foreach my $warn (@warnings) {
346 22 100       66 if ($warn =~ m/(.*)->Test2::Aggregate\n(.*\S.*)/s) {
347 4         19 push @clean, "<$1>\n$2";
348 4         13 $args->{stats}->{$1}->{warnings} = $2;
349 4         21 $args->{stats}->{$1}->{pass_perc} = 0;
350             }
351             }
352 8         35 return \@clean;
353             }
354              
355             sub _run_tests {
356 36     36   56 my $tests = shift;
357 36         63 my $args = shift;
358              
359 36         65 my $repeat = $args->{repeat};
360 36 100       100 $repeat = 1 if $repeat < 0;
361 36         66 my (%stats, $start);
362              
363 36 100       596 require Time::HiRes if $args->{stats_output};
364              
365 36         1246 for my $i (1 .. $repeat) {
366 40 100       107 my $iter = $repeat > 1 ? "Iter: $i/$repeat - " : '';
367 40         62 my $count = 1;
368 40         79 foreach my $test (@$tests) {
369              
370 64 100       242 warn "$test->Test2::Aggregate\n" if $args->{test_warnings};
371              
372 64 100       307 $stats{$test}{test_no} = $count unless $stats{$test}{test_no};
373 64 100       163 $start = Time::HiRes::time() if $args->{stats_output};
374 64         145 $stats{$test}{timestamp} = _timestamp();
375              
376 64         117 my $exec_error;
377             my $result = subtest $iter. "Running test $test" => sub {
378 64 100   64   36684 eval $args->{pre_eval} if $args->{pre_eval};
379              
380 64 100       178 if ($args->{dry_run}) {
381 2         6 Test2::V0::ok($test);
382             } else {
383             $args->{package}
384 62 100       12137 ? eval "package Test::$i" . '::' . "$count; do '$test';"
385             : do $test;
386 61         152021 $exec_error = $@;
387             }
388             Test2::V0::is($exec_error, '', 'Execution should not fail/warn')
389 63 100 100     1023 if !$args->{allow_errors} && $exec_error;
390 64         489 };
391              
392 63 100       63861 warn "<-Test2::Aggregate\n" if $args->{test_warnings};
393              
394             $stats{$test}{time} += (Time::HiRes::time() - $start)/$repeat
395 63 100       279 if $args->{stats_output};
396 63 100       253 $stats{$test}{pass_perc} += $result ? 100/$repeat : 0;
397 63         146 $count++;
398             }
399             }
400              
401 35 100       119 _print_stats(\%stats, $args) if $args->{stats_output};
402 34         150 $args->{stats} = \%stats;
403             }
404              
405             sub _override {
406 1     1   2 my $replace = shift;
407              
408 1         6 require Sub::Override;
409              
410 1         7 my $override = Sub::Override->new;
411 1         8 $override->replace($_, $replace->{$_}) for (keys %{$replace});
  1         6  
412              
413 1         40 return $override;
414             }
415              
416             sub _print_stats {
417 7     7   20 my ($stats, $args) = @_;
418              
419 7 100       178 unless (-e $args->{stats_output}) {
420 4         497 my @create = mkpath($args->{stats_output});
421 4 100       168 unless (scalar @create) {
422 1         14 warn "Could not create ".$args->{stats_output};
423 1         7 return;
424             }
425             }
426              
427 6         37 my $fh;
428 6 100       28 if ($args->{stats_output} =~ /^-$/) {
429 2         6 $fh = *STDOUT
430             } else {
431 4         25 my $file = $args->{stats_output}."/".$args->{caller}."-"._timestamp().".txt";
432 4 100       204 open($fh, '>', $file) or die "Can't open > $file: $!";
433             }
434              
435 5         14 my $total = 0;
436 5 100       17 my $extra = $args->{extend_stats} ? ' TIMESTAMP' : '';
437 5         120 print $fh "TIME PASS%$extra TEST\n";
438              
439 5         37 foreach my $test (sort {$stats->{$b}->{time}<=>$stats->{$a}->{time}} keys %$stats) {
  5         28  
440 10 100       26 $extra = ' '.$stats->{$test}->{timestamp} if $args->{extend_stats};
441 10         18 $total += $stats->{$test}->{time};
442             printf $fh "%.2f %d$extra $test\n",
443 10         124 $stats->{$test}->{time}, $stats->{$test}->{pass_perc};
444             }
445              
446 5         39 printf $fh "TOTAL TIME: %.1f sec\n", $total;
447 5 100       84 close $fh unless $args->{stats_output} =~ /^-$/;
448             }
449              
450             sub _uniq {
451 35     35   50 my %seen;
452 35         178 grep !$seen{$_}++, @_;
453             }
454              
455             sub _timestamp {
456 68     68   1788 my ($s, $m, $h, $D, $M, $Y) = localtime(time);
457 68         633 return sprintf "%04d%02d%02dT%02d%02d%02d", $Y+1900, $M+1, $D, $h, $m, $s;
458             }
459              
460             =head1 USAGE NOTES
461              
462             Not all tests can be modified to run under the aggregator, it is not intended
463             for tests that require an isolated environment, do overrides etc. For other tests
464             which can potentially run under the aggregator, sometimes very simple changes may be
465             needed like giving unique names to subs (or not warning for redefines, or trying the
466             package option), replacing things that complain, restoring the environment at
467             the end of the test etc.
468              
469             Unit tests are usually great for aggregating. You could use the hash that C
470             returns in a script that tries to add more tests automatically to an aggregate list
471             to see which added tests passed and keep them, dropping failures. See later in the
472             notes for a detailed example.
473              
474             Trying to aggregate too many tests into a single one can be counter-intuitive as
475             you would ideally want to parallelize your test suite (so a super-long aggregated
476             test continuing after the rest are done will slow down the suite). And in general
477             more tests will run aggregated if they are grouped so that tests that can't be
478             aggregated together are in different groups.
479              
480             In general you can call C multiple times in a test and
481             even load C with tests that already contain another C, the
482             only real issue with multiple calls is that if you use C on a call,
483             L is loaded so any subsequent failure, on any following
484             C call will trigger a Bail.
485              
486             =head2 Test::More
487              
488             If you haven't switched to the L you are generally advised to do so
489             for a number of reasons, compatibility with this module being only a very minor
490             one. If you are stuck with a L suite, L can still
491             probably help you more than the similarly-named C modules.
492              
493             Although the module tries to load C with minimal imports to not interfere,
494             it is generally better to do C in your aggregating test (i.e.
495             alongside with C).
496              
497             =head2 BEGIN / END Blocks
498              
499             C / C blocks will run at the start/end of each test and any overrides
500             etc you might have set will apply to the rest of the tests, so if you use them you
501             probably need to make changes for aggregation. An example of such a change is when
502             you have a C<*GLOBAL::CORE::exit> override to test scripts that can call C.
503             A solution is to use something like L:
504              
505             BEGIN {
506             unless ($Test::Trap::VERSION) { # Avoid warnings for multiple loads in aggregation
507             require Test::Trap;
508             Test::Trap->import();
509             }
510             }
511              
512             =head2 Test::Class
513              
514             L is sort of an aggregator itself. You make your tests into modules
515             and then load them on the same C<.t> file, so ideally you will not end up with many
516             C<.t> files that would require further aggregation. If you do, due to the L
517             implementation specifics, those C<.t> files won't run under L.
518              
519             =head2 $ENV{AGGREGATE_TESTS}
520              
521             The environment variable C will be set while the tests are running
522             for your convenience. Example usage is making a test you know cannot run under the
523             aggregator check and croak if it was run under it, or a module that can only be loaded
524             once, so you load it on the aggregated test file and then use something like this in
525             the individual test files:
526              
527             eval 'use My::Module' unless $ENV{AGGREGATE_TESTS};
528              
529             If you have a custom test bundle, you could use the variable to do things like
530             disable warnings on redefines only for tests that run aggregated:
531              
532             use Import::Into;
533              
534             sub import {
535             ...
536             'warnings'->unimport::out_of($package, 'redefine')
537             if $ENV{AGGREGATE_TESTS};
538             }
539              
540             Another idea is to make the test die when it is run under the aggregator, if, at
541             design time, you know it is not supposed to run aggregated.
542              
543             =head2 Example aggregating strategy
544              
545             There are many approaches you could do to use C with an existing
546             test suite, so for example you can start by making a list of the test files you
547             are trying to aggregate:
548              
549             find t -name '*.t' > all.lst
550              
551             If you have a substantial test suite, perhaps try with a portion of it (a subdir?)
552             instead of the entire suite. In any case, try running them aggregated like this:
553              
554             use Test2::Aggregate;
555             use Test2::V0; # Or Test::More;
556              
557             my $stats = Test2::Aggregate::run_tests(
558             lists => ['all.lst'],
559             );
560              
561             open OUT, ">pass.lst";
562             foreach my $test (sort {$stats->{$a}->{test_no} <=> $stats->{$b}->{test_no}} keys %$stats) {
563             print OUT "$test\n" if $stats->{$test}->{pass_perc};
564             }
565             close OUT;
566              
567             done_testing();
568              
569             Run the above with C or C in verbose mode, so that in case the run
570             hangs (it can happen), you can see where it did so and edit C removing
571             the offending test.
572              
573             If the run completes, you have a "starting point" - i.e. a list that can run under
574             the aggregator in C.
575             You can try adding back some of the failed tests - test failures can be cascading,
576             so some might be passing if added back, or have small issues you can address.
577              
578             Try adding C 1> to C to fix warnings as well, unless
579             it is common for your tests to have C output.
580              
581             To have your entire suite run aggregated tests together once and not repeat them
582             along with the other, non-aggregated, tests, it is a good idea to use the
583             C<--exclude-list> option of the C.
584              
585             Hopefully your tests can run in parallel (C), in which case you
586             would split your aggregated tests into multiple lists to have them run in parallel.
587             Here is an example of a wrapper around C, to easily handle multiple lists:
588              
589             BEGIN {
590             my @args = ();
591             foreach (@ARGV) {
592             if (/--exclude-lists=(\S+)/) {
593             my $all = 't/aggregate/aggregated.tests';
594             `awk '{print "t/"\$0}' $1 > $all`;
595             push @args, "--exclude-list=$all";
596             } else { push @args, $_ if $_; }
597             }
598             push @args, qw(-P...) # Preload module list (useful for non-aggregated tests)
599             unless grep {/--cover/} @args;
600             @ARGV = @args;
601             }
602             exec ('yath', @ARGV);
603              
604             You would call it with something like C<--exclude-lists=t/aggregate/*.lst>, and
605             the tests listed will be excluded (you will have them running aggregated through
606             their own C<.t> files using L).
607              
608             =head1 AUTHOR
609              
610             Dimitrios Kechagias, C<< >>
611            
612             =head1 BUGS
613              
614             Please report any bugs or feature requests to C,
615             or through the web interface at L.
616             I will be notified, and then you'll automatically be notified of progress on your
617             bug as I make changes. You could also submit issues or even pull requests to the
618             github repo (see below).
619              
620             =head1 GIT
621              
622             L
623            
624             =head1 COPYRIGHT & LICENSE
625              
626             Copyright (C) 2019, SpareRoom.com
627              
628             This program is free software; you can redistribute
629             it and/or modify it under the same terms as Perl itself.
630              
631             =cut
632              
633             1;