File Coverage

blib/lib/Test2/Aggregate.pm
Criterion Covered Total %
statement 149 149 100.0
branch 84 84 100.0
condition 18 18 100.0
subroutine 20 20 100.0
pod 1 2 50.0
total 272 273 99.6


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