File Coverage

blib/lib/Test/Expander.pm
Criterion Covered Total %
statement 271 271 100.0
branch 97 98 100.0
condition 25 25 100.0
subroutine 51 51 100.0
pod 0 7 0.0
total 444 452 98.4


line stmt bran cond sub pod time code
1             ## no critic ( ProhibitStringyEval ProhibitSubroutinePrototypes RequireLocalizedPunctuationVars)
2             package Test::Expander;
3              
4             # The versioning is conform with https://semver.org
5             our $VERSION = '2.2.0'; ## no critic (RequireUseStrict, RequireUseWarnings)
6              
7 20     20   1520735 use strict;
  20         129  
  20         797  
8             use warnings
9 20         941 FATAL => qw( all ),
10 20     20   112 NONFATAL => qw( deprecated exec internal malloc newline portable recursion );
  20         40  
11              
12 20     20   7577 use Const::Fast;
  20         42191  
  20         186  
13 20     20   9847 use File::chdir;
  20         59380  
  20         2108  
14 20     20   14175 use File::Temp qw( tempdir tempfile );
  20         388876  
  20         1359  
15 20     20   14583 use Getopt::Long qw( GetOptions :config posix_default );
  20         203928  
  20         138  
16 20     20   15231 use Importer;
  20         104483  
  20         205  
17 20     20   17689 use Path::Tiny qw( cwd path );
  20         233021  
  20         1389  
18 20     20   7341 use Scalar::Readonly qw( readonly_on );
  20         9460  
  20         1190  
19 20     20   3770 use Test2::API qw( context );
  20         435559  
  20         1292  
20 20     20   7000 use Test2::Tools::Basic;
  20         17458  
  20         1702  
21 20     20   8199 use Test2::Tools::Explain;
  20         6846  
  20         1182  
22 20     20   7243 use Test2::Tools::Subtest;
  20         9177  
  20         1490  
23              
24 20         9718 use Test::Expander::Constants qw(
25             $DIE $FALSE
26             $FMT_INVALID_DIRECTORY $FMT_INVALID_ENV_ENTRY $FMT_INVALID_VALUE $FMT_INVALID_SUBTEST_NUMBER $FMT_KEEP_ENV_VAR
27             $FMT_NEW_FAILED $FMT_NEW_SUCCEEDED $FMT_REPLACEMENT $FMT_REQUIRE_DESCRIPTION $FMT_REQUIRE_IMPLEMENTATION
28             $FMT_SEARCH_PATTERN $FMT_SET_ENV_VAR $FMT_SET_TO $FMT_SKIP_ENV_VAR $FMT_UNKNOWN_OPTION $FMT_USE_DESCRIPTION
29             $FMT_USE_IMPLEMENTATION $MSG_ERROR_WAS $MSG_UNEXPECTED_EXCEPTION
30             $NOTE
31             $REGEX_ANY_EXTENSION $REGEX_CLASS_HIERARCHY_LEVEL $REGEX_TOP_DIR_IN_PATH $REGEX_VERSION_NUMBER
32             $TRUE
33             %MOST_CONSTANTS_TO_EXPORT %REST_CONSTANTS_TO_EXPORT
34 20     20   8060 );
  20         58  
35              
36             my ( @subtest_names, @subtest_numbers );
37              
38             sub _subtest_selection {
39 26     26   5101 my $error;
40             GetOptions(
41             'subtest_name|subtest=s' => sub {
42 4     4   857 ( undef, my $opt_value ) = @_;
43 4 100       15 push( @subtest_names, eval { qr/$opt_value/ } ? $opt_value : "\Q$opt_value\E" );
  4         82  
44             },
45             'subtest_number=s' => sub {
46 4     4   1285 ( undef, my $opt_value ) = @_;
47 4 100       38 $error = sprintf( $FMT_INVALID_SUBTEST_NUMBER, $opt_value ) if $opt_value !~ m{^ \d+ (?: / \d+ )* $}x;
48 4         61 push( @subtest_numbers, $opt_value );
49             },
50 26         331 );
51 26 100       5895 die( $error) if $error;
52              
53 25         69 my $subtest_buffered_orig = \&Test2::Tools::Subtest::subtest_buffered;
54 25         93 my $subtest_streamed_orig = \&Test2::Tools::Subtest::subtest_streamed;
55 20     20   167 no warnings qw( redefine );
  20         50  
  20         2524  
56 25     12   159 *Test2::Tools::Subtest::subtest_buffered = sub { _subtest_conditional( $subtest_buffered_orig, @_ ) };
  12         2201  
57 25     2   168 *Test2::Tools::Subtest::subtest_streamed = sub { _subtest_conditional( $subtest_streamed_orig, @_ ) };
  2         7523  
58              
59 25         659 return;
60             }
61              
62 20     20   150 BEGIN { _subtest_selection() }
63              
64 20     20   8152 use Test2::V0 qw();
  20         2492349  
  20         28224  
65              
66             readonly_on( $VERSION );
67              
68             our ( $CLASS, $METHOD, $METHOD_REF, $TEMP_DIR, $TEMP_FILE, $TEST_FILE );
69             our @EXPORT = (
70             @{ Const::Fast::EXPORT },
71             @{ Test2::Tools::Explain::EXPORT },
72             @{ Test2::V0::EXPORT },
73             qw( tempdir tempfile ),
74             qw( cwd path ),
75             qw( BAIL_OUT dies_ok is_deeply lives_ok new_ok require_ok throws_ok use_ok ),
76             );
77              
78             *BAIL_OUT = \&bail_out; # Explicit "sub BAIL_OUT" would be untestable
79              
80             sub dies_ok ( &;$ ) {
81 2     2 0 2106 my ( $coderef, $description ) = @_;
82              
83 2         7 eval { $coderef->() };
  2         5  
84              
85 2         64 return ok( $@, $description );
86             }
87              
88             sub import {
89 33     33   28999 my ( $class, @exports ) = @_;
90              
91 33         69 my $frame_index = 0;
92 33         130 my $test_file;
93 33         339 while( my @current_frame = caller( $frame_index++ ) ) {
94 105         5109 $test_file = path( $current_frame[ 1 ] ) =~ s{^/}{}r;
95             }
96 33         1274 my $options = _parse_options( \@exports, $test_file );
97              
98 23         109 _export_most_symbols( $options, $test_file );
99 23         124 _set_env( $options->{ -target }, $test_file );
100 23 100 100     226 _mock_builtins( $options ) if defined( $CLASS ) && exists( $options->{ -builtins } );
101              
102 23         258 Test2::V0->import( %$options );
103              
104 23         30645 _export_rest_symbols();
105 23         365 Importer->import_into( $class, scalar( caller ), () );
106              
107 23         1877332 return;
108             }
109              
110             sub is_deeply ( $$;$@ ) {
111 2     2 0 2042 my ( $got, $expected, $title ) = @_;
112              
113 2         55 return is( $got, $expected, $title );
114             }
115              
116             sub lives_ok ( &;$ ) {
117 6     6 0 6801 my ( $coderef, $description ) = @_;
118              
119 6         14 eval { $coderef->() };
  6         94  
120 5 100       52 diag( $MSG_UNEXPECTED_EXCEPTION . $@ ) if $@;
121              
122 5         590 return ok( !$@, $description );
123             }
124              
125             sub new_ok {
126 2     3 0 7437 my ( $class, $args ) = @_;
127              
128 2   100     11 $args ||= [];
129 2         4 my $obj = eval { $class->new( @$args ) };
  2         15  
130 2         82 ok( !$@, _new_test_message( $class ) );
131              
132 2         596 return $obj;
133             }
134              
135             sub require_ok {
136 2     3 0 5952 my ( $module ) = @_;
137              
138 2         7 my $package = caller;
139 2         144 my $require_result = eval( sprintf( $FMT_REQUIRE_IMPLEMENTATION, $package, $module ) );
140 2         1713 ok( $require_result, sprintf( $FMT_REQUIRE_DESCRIPTION, $module, _error() ) );
141              
142 2         671 return $require_result;
143             }
144              
145             sub throws_ok ( &$;$ ) {
146 12     13 0 392503 my ( $coderef, $expecting, $description ) = @_;
147              
148 12         29 eval { $coderef->() };
  12         30  
149 12         50 my $exception = $@;
150 12         25 my $expected_type = ref( $expecting );
151              
152 12 100       57 return $expected_type eq 'Regexp' ? like ( $exception, $expecting, $description )
153             : isa_ok( $exception, [ $expecting ], $description );
154             }
155              
156             sub use_ok ( $;@ ) {
157 1     1 0 2072 my ( $module, @imports ) = @_;
158              
159 1         6 my ( $package, $filename, $line ) = caller( 0 );
160 1         4 $filename =~ y/\n\r/_/; # taken over from Test::More
161              
162 1         5 my $require_result = eval( sprintf( $FMT_USE_IMPLEMENTATION, $package, $module, _use_imports( \@imports ) ) );
163 1         20 ok(
164             $require_result,
165             sprintf(
166             $FMT_USE_DESCRIPTION, $module, _error( $FMT_SEARCH_PATTERN, sprintf( $FMT_REPLACEMENT, $filename, $line ) )
167             )
168             );
169              
170 1         350 return $require_result;
171             }
172              
173             sub _determine_testee {
174 23     23   64 my ( $options, $test_file ) = @_;
175              
176 23 100       85 if ( $options->{ -lib } ) {
177 3         4 foreach my $directory ( @{ $options->{ -lib } } ) {
  3         8  
178 3 100       11 $DIE->( $FMT_INVALID_DIRECTORY, $directory, 'invalid type' ) if ref( $directory );
179 2         123 my $inc_entry = eval( $directory );
180 2 100       94 $DIE->( $FMT_INVALID_DIRECTORY, $directory, $@ ) if $@;
181 1         9 unshift( @INC, $inc_entry );
182             }
183 1         3 delete( $options->{ -lib } );
184             }
185              
186 21 100       72 if ( exists( $options->{ -method } ) ) {
187 4         10 delete( $options->{ -method } );
188             }
189             else {
190 17         64 $METHOD = path( $test_file )->basename( $REGEX_ANY_EXTENSION );
191             }
192              
193 21 100       1793 unless ( exists( $options->{ -target } ) ) { # Try to determine class / module autmatically
194 15         106 my ( $test_root ) = $test_file =~ $REGEX_TOP_DIR_IN_PATH;
195 15         150 my $testee = path( $test_file )->relative( $test_root )->parent;
196 15 100       8586 $options->{ -target } = "$testee" =~ s{/}{::}gr if grep { path( $_ )->child( $testee . '.pm' )->is_file } @INC;
  166         13033  
197             }
198 21 100       1379 if ( defined( $options->{ -target } ) ) {
199 17         58 $CLASS = $options->{ -target };
200             }
201             else {
202 4         12 delete( $options->{ -target } );
203             }
204              
205 21         59 return $options;
206             }
207              
208             sub _error {
209 7     7   7825 my ( $search_string, $replacement_string ) = @_;
210              
211 7 100       54 return '' if $@ eq '';
212              
213 3         17 my $error = $MSG_ERROR_WAS . $@ =~ s/\n$//mr;
214 3 100       26 $error =~ s/$search_string/$replacement_string/m if defined( $search_string );
215 3         28 return $error;
216             }
217              
218             sub _export_most_symbols {
219 22     22   54 my ( $options, $test_file ) = @_;
220              
221 22 100       73 $TEST_FILE = path( $test_file )->absolute->stringify if path( $test_file )->exists;
222              
223 22         2853 return _export_symbols( %MOST_CONSTANTS_TO_EXPORT );
224             }
225              
226             sub _export_rest_symbols {
227             # Further export if class and method are known
228 21 100 100 21   375 return _export_symbols( %REST_CONSTANTS_TO_EXPORT ) if $CLASS && $METHOD && ( $METHOD_REF = $CLASS->can( $METHOD ) );
      100        
229              
230 6         13 $METHOD = undef;
231 6         48 return;
232             }
233              
234             sub _export_symbols {
235 35     35   164 my %constants = @_;
236              
237 35         205 foreach my $name ( sort keys( %constants ) ) { # Export defined constants
238 20     20   269 no strict qw( refs ); ## no critic (ProhibitProlongedStrictureOverride)
  20         109  
  20         4028  
239 112 100       49378 my $value = eval( "${ \$name }" ) or next;
  112         5808  
240 73         249 readonly_on( ${ __PACKAGE__ . '::' . $name =~ s/^.//r } );
  73         543  
241 73         234 push( @EXPORT, $name );
242 73         515 $NOTE->( $FMT_SET_TO, $name, $constants{ $name }->( $value, $CLASS ) );
243             }
244              
245 35         28847 return;
246             }
247              
248             sub _mock_builtins {
249 1     1   3 my ( $options ) = @_;
250              
251 1         1 while ( my ( $sub_name, $sub_ref ) = each( %{ $options->{ -builtins } } ) ) {
  2         10  
252 1         3 my $sub_full_name = $CLASS . '::' . $sub_name;
253 20     20   178 no strict qw( refs );
  20         121  
  20         13156  
254 1         26 *${ sub_full_name } = $sub_ref;
255             }
256 1         2 delete( $options->{ -builtins } );
257              
258 1         2 return;
259             }
260              
261             sub _new_test_message {
262 4     4   4789 my ( $class ) = @_;
263              
264 4 100       51 return $@ ? sprintf( $FMT_NEW_FAILED, $class, _error() ) : sprintf( $FMT_NEW_SUCCEEDED, $class, $class );
265             }
266              
267             sub _parse_options {
268 31     31   93 my ( $exports, $test_file ) = @_;
269              
270 31         58 my $options = {};
271 31         106 while ( my $option_name = shift( @$exports ) ) {
272 26 100 100     1559 $DIE->( $FMT_UNKNOWN_OPTION, $option_name, shift( @$exports ) // '' ) if $option_name !~ /^-\w/;
273              
274 24         44 my $option_value = shift( @$exports );
275 24 100       158 if ( $option_name eq '-builtins' ) { ## no critic (ProhibitCascadingIfElse)
    100          
    100          
    100          
    100          
    100          
276 3 100       16 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
277 2         19 while ( my ( $sub_name, $sub_ref ) = each( %$option_value ) ) {
278 2 100       15 $DIE->( $FMT_INVALID_VALUE, $option_name . "->{ $sub_name }", $sub_ref ) if ref( $sub_ref ) ne 'CODE';
279             }
280 1         4 $options->{ $option_name } = $option_value;
281             }
282             elsif ( $option_name eq '-lib' ) {
283 4 100       16 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'ARRAY';
284 3         13 $options->{ $option_name } = $option_value;
285             }
286             elsif ( $option_name eq '-method' ) {
287 5 100       18 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value );
288 4         21 $METHOD = $options->{ $option_name } = $option_value;
289             }
290             elsif ( $option_name eq '-target' ) {
291 6         23 $options->{ $option_name } = $option_value;
292             }
293             elsif ( $option_name eq '-tempdir' ) {
294 3 100       22 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
295 2         25 $TEMP_DIR = tempdir( CLEANUP => 1, %$option_value );
296             }
297             elsif ( $option_name eq '-tempfile' ) {
298 2 100       13 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
299 1         2 my $file_handle;
300 1         7 ( $file_handle, $TEMP_FILE ) = tempfile( UNLINK => 1, %$option_value );
301             }
302             else {
303 1         5 $options->{ $option_name } = $option_value;
304             }
305             }
306              
307 23         494 return _determine_testee( $options, $test_file );
308             }
309              
310             sub _read_env_file {
311 9     9   19 my ( $env_file ) = @_;
312              
313 9         23 my @lines = path( $env_file )->lines( { chomp => 1 } );
314 9         2401 my %env;
315 9         52 while ( my ( $index, $line ) = each( @lines ) ) {
316             ## no critic (ProhibitUnusedCapture)
317 14 100       457 next unless $line =~ /^ (? \w+) \s* (?: = \s* (? \S .*) | $ )/x;
318 20     20   9175 my ( $name, $value ) = @+{ qw( name value ) };
  20         8558  
  20         16217  
  13         125  
319 13 100       100 if ( exists( $+{ value } ) ) {
    100          
320 11         23 my $stricture = q{
321             use strict;
322             use warnings
323             FATAL => qw( all ),
324             NONFATAL => qw( deprecated exec internal malloc newline once portable redefine recursion uninitialized );
325             };
326 11         22 $value = eval {
327 3     3   15 eval( $stricture . '$value' );
  3     3   33  
  3     1   100  
  3         15  
  3         90  
  3         122  
  11         782  
  1         479  
  1         1728  
  1         31  
328 11 50       39 die( $@ ) if $@; # uncoverable branch true
329 3     3   15 $value = eval( $stricture . $value );
  3     3   39  
  3         128  
  3         17  
  3         46  
  3         98  
  11         718  
330 11 100       513 die( $@ ) if $@;
331 9         42 $value;
332             };
333 11 100       64 $DIE->( $FMT_INVALID_ENV_ENTRY, $index, $env_file, $line, $@ =~ s/\n//gr =~ s/ at \(eval .+//ir ) if $@;
334 9 100       25 if ( defined( $value ) ) {
335 8         30 $NOTE->( $FMT_SET_ENV_VAR, $name, $value, $env_file );
336 8         3325 $ENV{ $name } = $env{ $name } = $value;
337             }
338             else {
339 1         5 $NOTE->( $FMT_SKIP_ENV_VAR, $name, $env_file );
340             }
341             }
342             elsif ( exists( $ENV{ $+{ name } } ) ) {
343 1         14 $env{ $name } = $ENV{ $name };
344 1         5 $NOTE->( $FMT_KEEP_ENV_VAR, $name, $ENV{ $name }, $env_file );
345             }
346             }
347              
348 7         479 return \%env;
349             }
350              
351             sub _set_env {
352 30     30   17216 my ( $class, $test_file ) = @_;
353              
354 30 100       126 return unless path( $test_file )->exists;
355              
356 29         1324 my $env_found = $FALSE;
357 29         73 my $new_env = {};
358             {
359 29         56 local $CWD = $test_file =~ s{/.*}{}r; ## no critic (ProhibitLocalVars)
  29         168  
360 29         2051 ( $env_found, $new_env ) = _set_env_hierarchically( $class, $env_found, $new_env );
361             }
362              
363 29         1923 my $env_file = $test_file =~ s/$REGEX_ANY_EXTENSION/.env/r;
364              
365 29 100       644 if ( path( $env_file )->is_file ) {
366 7 100       400 $env_found = $TRUE unless $env_found;
367 7         24 my $method_env = _read_env_file( $env_file );
368 5         25 @$new_env{ keys( %$method_env ) } = values( %$method_env );
369             }
370              
371 27 100       1707 %ENV = %$new_env if $env_found;
372              
373 27         119 return;
374             }
375              
376             sub _set_env_hierarchically {
377 75     75   450 my ( $class, $env_found, $new_env ) = @_;
378              
379 75 100       284 return ( $env_found, $new_env ) unless $class;
380              
381 48         74 my $class_top_level;
382 48         450 ( $class_top_level, $class ) = $class =~ $REGEX_CLASS_HIERARCHY_LEVEL;
383              
384 48 100       182 return ( $FALSE, {} ) unless path( $class_top_level )->is_dir;
385              
386 46         2534 my $env_file = $class_top_level . '.env';
387 46 100       199 if ( path( $env_file )->is_file ) {
388 2 100       114 $env_found = $TRUE unless $env_found;
389 2         7 $new_env = { %$new_env, %{ _read_env_file( $env_file ) } };
  2         5  
390             }
391              
392 46         2109 local $CWD = $class_top_level; ## no critic (ProhibitLocalVars)
393 46         1869 return _set_env_hierarchically( $class, $env_found, $new_env );
394             }
395              
396             sub _subtest_conditional {
397 15     15   75 my ( $orig_subtest, $name, @rest ) = @_;
398              
399 15         38 my $ctx = context();
400 15         1268 my $number = join( '/', map { $_->count } @{ $ctx->stack } );
  18         97  
  15         79  
401 15 100 100     182 if (
      100        
      100        
402             !@subtest_names && !@subtest_numbers ||
403 3         29 ( grep { $name =~ /$_/ } @subtest_names ) ||
404 3         47 ( grep { /^$number/ } @subtest_numbers )
405             ) {
406 14         49 $orig_subtest->( $name, @rest );
407 14         23552 $ctx->release;
408             }
409             else {
410 1         17 $ctx->skip( 'forced by ' . __PACKAGE__ );
411 1         341 $ctx->release;
412             }
413              
414 15         375 return;
415             }
416              
417             sub _use_imports {
418 4     4   7870 my ( $imports ) = @_;
419              
420 4 100 100     147 return @$imports == 1 && $imports->[ 0 ] =~ $REGEX_VERSION_NUMBER ? ' ' . $imports->[ 0 ] : '';
421             }
422              
423             1;