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 1 100.0
total 272 272 100.0


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