File Coverage

blib/lib/Test/Expander.pm
Criterion Covered Total %
statement 228 228 100.0
branch 85 86 100.0
condition 16 16 100.0
subroutine 40 40 100.0
pod 0 7 0.0
total 369 377 98.1


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.1.4'; ## no critic (RequireUseStrict, RequireUseWarnings)
6              
7 18     18   1295680 use strict;
  18         111  
  18         760  
8             use warnings
9 18         856 FATAL => qw( all ),
10 18     18   106 NONFATAL => qw( deprecated exec internal malloc newline portable recursion );
  18         32  
11              
12 18     18   6862 use Const::Fast;
  18         35914  
  18         163  
13 18     18   8553 use File::chdir;
  18         52043  
  18         1850  
14 18     18   11703 use File::Temp qw( tempdir tempfile );
  18         338528  
  18         1173  
15 18     18   8955 use Importer;
  18         87641  
  18         151  
16 18     18   14460 use Path::Tiny qw( cwd path );
  18         204972  
  18         1261  
17 18     18   6551 use Scalar::Readonly qw( readonly_on );
  18         8107  
  18         1025  
18 18     18   6581 use Test2::Tools::Basic;
  18         382115  
  18         1460  
19 18     18   7597 use Test2::Tools::Explain;
  18         6034  
  18         960  
20 18     18   7265 use Test2::V0 qw();
  18         2347309  
  18         1036  
21              
22 18         28991 use Test::Expander::Constants qw(
23             $DIE $FALSE
24             $FMT_INVALID_DIRECTORY $FMT_INVALID_ENV_ENTRY $FMT_INVALID_VALUE $FMT_KEEP_ENV_VAR $FMT_NEW_FAILED
25             $FMT_NEW_SUCCEEDED $FMT_REPLACEMENT $FMT_REQUIRE_DESCRIPTION $FMT_REQUIRE_IMPLEMENTATION $FMT_SEARCH_PATTERN
26             $FMT_SET_ENV_VAR $FMT_SET_TO $FMT_SKIP_ENV_VAR $FMT_UNKNOWN_OPTION $FMT_USE_DESCRIPTION $FMT_USE_IMPLEMENTATION
27             $MSG_ERROR_WAS $MSG_UNEXPECTED_EXCEPTION
28             $NOTE
29             $REGEX_ANY_EXTENSION $REGEX_CLASS_HIERARCHY_LEVEL $REGEX_TOP_DIR_IN_PATH $REGEX_VERSION_NUMBER
30             $TRUE
31             %MOST_CONSTANTS_TO_EXPORT %REST_CONSTANTS_TO_EXPORT
32 18     18   7177 );
  18         53  
33              
34             readonly_on( $VERSION );
35              
36             our ( $CLASS, $METHOD, $METHOD_REF, $TEMP_DIR, $TEMP_FILE, $TEST_FILE );
37             our @EXPORT = (
38             @{ Const::Fast::EXPORT },
39             @{ Test2::Tools::Explain::EXPORT },
40             @{ Test2::V0::EXPORT },
41             qw( tempdir tempfile ),
42             qw( cwd path ),
43             qw( BAIL_OUT dies_ok is_deeply lives_ok new_ok require_ok throws_ok use_ok ),
44             );
45              
46             *BAIL_OUT = \&bail_out; # Explicit "sub BAIL_OUT" would be untestable
47              
48             sub dies_ok ( &;$ ) {
49 2     2 0 1932 my ( $coderef, $description ) = @_;
50              
51 2         7 eval { $coderef->() };
  2         33  
52              
53 2         19 return ok( $@, $description );
54             }
55              
56             sub import {
57 31     31   20460 my ( $class, @exports ) = @_;
58              
59 31         102 my $frame_index = 0;
60 31         64 my $test_file;
61 31         244 while( my @current_frame = caller( $frame_index++ ) ) {
62 101         4855 $test_file = path( $current_frame[ 1 ] ) =~ s{^/}{}r;
63             }
64 31         1104 my $options = _parse_options( \@exports, $test_file );
65              
66 21         74 _export_most_symbols( $options, $test_file );
67 21         135 _set_env( $options->{ -target }, $test_file );
68 21 100 100     147 _mock_builtins( $options ) if defined( $CLASS ) && exists( $options->{ -builtins } );
69              
70 21         185 Test2::V0->import( %$options );
71              
72 21         27293 _export_rest_symbols();
73 21         245 Importer->import_into( $class, scalar( caller ), () );
74              
75 21         1634403 return;
76             }
77              
78             sub is_deeply ( $$;$@ ) {
79 2     2 0 2059 my ( $got, $expected, $title ) = @_;
80              
81 2         12 return is( $got, $expected, $title );
82             }
83              
84             sub lives_ok ( &;$ ) {
85 3     3 0 5759 my ( $coderef, $description ) = @_;
86              
87 3         38 eval { $coderef->() };
  3         12  
88 3 100       19 diag( $MSG_UNEXPECTED_EXCEPTION . $@ ) if $@;
89              
90 3         618 return ok( !$@, $description );
91             }
92              
93             sub new_ok {
94 3     3 0 7202 my ( $class, $args ) = @_;
95              
96 3   100     12 $args ||= [];
97 3         36 my $obj = eval { $class->new( @$args ) };
  3         16  
98 3         82 ok( !$@, _new_test_message( $class ) );
99              
100 3         658 return $obj;
101             }
102              
103             sub require_ok {
104 3     3 0 5740 my ( $module ) = @_;
105              
106 3         7 my $package = caller;
107 3         171 my $require_result = eval( sprintf( $FMT_REQUIRE_IMPLEMENTATION, $package, $module ) );
108 3         1667 ok( $require_result, sprintf( $FMT_REQUIRE_DESCRIPTION, $module, _error() ) );
109              
110 3         688 return $require_result;
111             }
112              
113             sub throws_ok ( &$;$ ) {
114 13     13 0 365812 my ( $coderef, $expecting, $description ) = @_;
115              
116 13         65 eval { $coderef->() };
  13         36  
117 13         107 my $exception = $@;
118 13         31 my $expected_type = ref( $expecting );
119              
120 13 100       61 return $expected_type eq 'Regexp' ? like ( $exception, $expecting, $description )
121             : isa_ok( $exception, [ $expecting ], $description );
122             }
123              
124             sub use_ok ( $;@ ) {
125 2     2 0 2044 my ( $module, @imports ) = @_;
126              
127 2         12 my ( $package, $filename, $line ) = caller( 0 );
128 2         7 $filename =~ y/\n\r/_/; # taken over from Test::More
129              
130 2         43 my $require_result = eval( sprintf( $FMT_USE_IMPLEMENTATION, $package, $module, _use_imports( \@imports ) ) );
131 2         18 ok(
132             $require_result,
133             sprintf(
134             $FMT_USE_DESCRIPTION, $module, _error( $FMT_SEARCH_PATTERN, sprintf( $FMT_REPLACEMENT, $filename, $line ) )
135             )
136             );
137              
138 2         359 return $require_result;
139             }
140              
141             sub _determine_testee {
142 23     23   96 my ( $options, $test_file ) = @_;
143              
144 23 100       80 if ( $options->{ -lib } ) {
145 4         9 foreach my $directory ( @{ $options->{ -lib } } ) {
  4         75  
146 3 100       17 $DIE->( $FMT_INVALID_DIRECTORY, $directory, 'invalid type' ) if ref( $directory );
147 2         138 my $inc_entry = eval( $directory );
148 2 100       94 $DIE->( $FMT_INVALID_DIRECTORY, $directory, $@ ) if $@;
149 1         4 unshift( @INC, $inc_entry );
150             }
151 1         4 delete( $options->{ -lib } );
152             }
153              
154 20 100       63 if ( exists( $options->{ -method } ) ) {
155 4         11 delete( $options->{ -method } );
156             }
157             else {
158 16         51 $METHOD = path( $test_file )->basename( $REGEX_ANY_EXTENSION );
159             }
160              
161 20 100       1613 unless ( exists( $options->{ -target } ) ) { # Try to determine class / module autmatically
162 14         110 my ( $test_root ) = $test_file =~ $REGEX_TOP_DIR_IN_PATH;
163 14         131 my $testee = path( $test_file )->relative( $test_root )->parent;
164 14 100       7302 $options->{ -target } = $testee =~ s{/}{::}gr if grep { path( $_ )->child( $testee . '.pm' )->is_file } @INC;
  155         12137  
165             }
166 20 100       1335 if ( defined( $options->{ -target } ) ) {
167 16         43 $CLASS = $options->{ -target };
168             }
169             else {
170 4         8 delete( $options->{ -target } );
171             }
172              
173 20         57 return $options;
174             }
175              
176             sub _error {
177 7     8   7288 my ( $search_string, $replacement_string ) = @_;
178              
179 7 100       58 return '' if $@ eq '';
180              
181 3         14 my $error = $MSG_ERROR_WAS . $@ =~ s/\n$//mr;
182 3 100       22 $error =~ s/$search_string/$replacement_string/m if defined( $search_string );
183 3         78 return $error;
184             }
185              
186             sub _export_most_symbols {
187 20     21   45 my ( $options, $test_file ) = @_;
188              
189 20         67 $TEST_FILE = path( $test_file )->absolute->stringify;
190              
191 20         1786 return _export_symbols( %MOST_CONSTANTS_TO_EXPORT );
192             }
193              
194             sub _export_rest_symbols {
195             # Further export if class and method are known
196 20 100 100 21   368 return _export_symbols( %REST_CONSTANTS_TO_EXPORT ) if $CLASS && $METHOD && ( $METHOD_REF = $CLASS->can( $METHOD ) );
      100        
197              
198 6         18 $METHOD = undef;
199 6         10 return;
200             }
201              
202             sub _export_symbols {
203 32     33   123 my %constants = @_;
204              
205 32         203 foreach my $name ( sort keys( %constants ) ) { # Export defined constants
206 18     18   164 no strict qw( refs ); ## no critic (ProhibitProlongedStrictureOverride)
  18         38  
  18         3659  
207 102 100       40522 my $value = eval( "${ \$name }" ) or next;
  102         5698  
208 69         232 readonly_on( ${ __PACKAGE__ . '::' . $name =~ s/^.//r } );
  69         472  
209 69         179 push( @EXPORT, $name );
210 69         273 $NOTE->( $FMT_SET_TO, $name, $constants{ $name }->( $value, $CLASS ) );
211             }
212              
213 32         28465 return;
214             }
215              
216             sub _mock_builtins {
217 1     2   2 my ( $options ) = @_;
218              
219 1         2 while ( my ( $sub_name, $sub_ref ) = each( %{ $options->{ -builtins } } ) ) {
  2         8  
220 1         3 my $sub_full_name = $CLASS . '::' . $sub_name;
221 18     18   161 no strict qw( refs );
  18         58  
  18         11445  
222 1         14 *${ sub_full_name } = $sub_ref;
223             }
224 1         3 delete( $options->{ -builtins } );
225              
226 1         11 return;
227             }
228              
229             sub _new_test_message {
230 4     5   4761 my ( $class ) = @_;
231              
232 4 100       57 return $@ ? sprintf( $FMT_NEW_FAILED, $class, _error() ) : sprintf( $FMT_NEW_SUCCEEDED, $class, $class );
233             }
234              
235             sub _parse_options {
236 30     31   75 my ( $exports, $test_file ) = @_;
237              
238 30         55 my $options = {};
239 30         133 while ( my $option_name = shift( @$exports ) ) {
240 26 100 100     1541 $DIE->( $FMT_UNKNOWN_OPTION, $option_name, shift( @$exports ) // '' ) if $option_name !~ /^-\w/;
241              
242 24         52 my $option_value = shift( @$exports );
243 24 100       106 if ( $option_name eq '-builtins' ) { ## no critic (ProhibitCascadingIfElse)
    100          
    100          
    100          
    100          
    100          
244 3 100       17 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
245 2         14 while ( my ( $sub_name, $sub_ref ) = each( %$option_value ) ) {
246 2 100       13 $DIE->( $FMT_INVALID_VALUE, $option_name . "->{ $sub_name }", $sub_ref ) if ref( $sub_ref ) ne 'CODE';
247             }
248 1         4 $options->{ $option_name } = $option_value;
249             }
250             elsif ( $option_name eq '-lib' ) {
251 4 100       18 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'ARRAY';
252 3         12 $options->{ $option_name } = $option_value;
253             }
254             elsif ( $option_name eq '-method' ) {
255 5 100       19 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value );
256 4         16 $METHOD = $options->{ $option_name } = $option_value;
257             }
258             elsif ( $option_name eq '-target' ) {
259 6         25 $options->{ $option_name } = $option_value;
260             }
261             elsif ( $option_name eq '-tempdir' ) {
262 3 100       16 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
263 2         12 $TEMP_DIR = tempdir( CLEANUP => 1, %$option_value );
264             }
265             elsif ( $option_name eq '-tempfile' ) {
266 2 100       10 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
267 1         2 my $file_handle;
268 1         7 ( $file_handle, $TEMP_FILE ) = tempfile( UNLINK => 1, %$option_value );
269             }
270             else {
271 1         4 $options->{ $option_name } = $option_value;
272             }
273             }
274              
275 22         480 return _determine_testee( $options, $test_file );
276             }
277              
278             sub _read_env_file {
279 9     10   20 my ( $env_file ) = @_;
280              
281 9         22 my @lines = path( $env_file )->lines( { chomp => 1 } );
282 9         2410 my %env;
283 9         50 while ( my ( $index, $line ) = each( @lines ) ) {
284             ## no critic (ProhibitUnusedCapture)
285 14 100       452 next unless $line =~ /^ (? \w+) \s* (?: = \s* (? \S .*) | $ )/x;
286 18     18   8122 my ( $name, $value ) = @+{ qw( name value ) };
  18         7061  
  18         11631  
  13         122  
287 13 100       92 if ( exists( $+{ value } ) ) {
    100          
288 11         23 my $stricture = q{
289             use strict;
290             use warnings
291             FATAL => qw( all ),
292             NONFATAL => qw( deprecated exec internal malloc newline once portable redefine recursion uninitialized );
293             };
294 11         36 $value = eval {
295 3     3   20 eval( $stricture . '$value' );
  3     3   6  
  3     1   117  
  3         17  
  3         13  
  3         133  
  11         732  
  1         492  
  1         1681  
  1         32  
296 11 50       37 die( $@ ) if $@; # uncoverable branch true
297 3     3   21 $value = eval( $stricture . $value );
  3     3   7  
  3         125  
  3         18  
  3         6  
  3         121  
  11         683  
298 11 100       443 die( $@ ) if $@;
299 9         19 $value;
300             };
301 11 100       47 $DIE->( $FMT_INVALID_ENV_ENTRY, $index, $env_file, $line, $@ =~ s/\n//gr =~ s/ at \(eval .+//ir ) if $@;
302 9 100       25 if ( defined( $value ) ) {
303 8         31 $NOTE->( $FMT_SET_ENV_VAR, $name, $value, $env_file );
304 8         3248 $ENV{ $name } = $env{ $name } = $value;
305             }
306             else {
307 1         3 $NOTE->( $FMT_SKIP_ENV_VAR, $name, $env_file );
308             }
309             }
310             elsif ( exists( $ENV{ $+{ name } } ) ) {
311 1         4 $env{ $name } = $ENV{ $name };
312 1         4 $NOTE->( $FMT_KEEP_ENV_VAR, $name, $ENV{ $name }, $env_file );
313             }
314             }
315              
316 7         492 return \%env;
317             }
318              
319             sub _set_env {
320 28     28   29148 my ( $class, $test_file ) = @_;
321              
322 28         58 my $env_found = $FALSE;
323 28         56 my $new_env = {};
324             {
325 28         55 local $CWD = $test_file =~ s{/.*}{}r; ## no critic (ProhibitLocalVars)
  28         141  
326 28         2030 ( $env_found, $new_env ) = _set_env_hierarchically( $class, $env_found, $new_env );
327             }
328              
329 28         1773 my $env_file = $test_file =~ s/$REGEX_ANY_EXTENSION/.env/r;
330              
331 28 100       351 if ( path( $env_file )->is_file ) {
332 7 100       410 $env_found = $TRUE unless $env_found;
333 7         18 my $method_env = _read_env_file( $env_file );
334 5         22 @$new_env{ keys( %$method_env ) } = values( %$method_env );
335             }
336              
337 26 100       1498 %ENV = %$new_env if $env_found;
338              
339 26         94 return;
340             }
341              
342             sub _set_env_hierarchically {
343 72     72   187 my ( $class, $env_found, $new_env ) = @_;
344              
345 72 100       266 return ( $env_found, $new_env ) unless $class;
346              
347 46         71 my $class_top_level;
348 46         345 ( $class_top_level, $class ) = $class =~ $REGEX_CLASS_HIERARCHY_LEVEL;
349              
350 46 100       186 return ( $FALSE, {} ) unless path( $class_top_level )->is_dir;
351              
352 44         2465 my $env_file = $class_top_level . '.env';
353 44 100       129 if ( path( $env_file )->is_file ) {
354 2 100       93 $env_found = $TRUE unless $env_found;
355 2         8 $new_env = { %$new_env, %{ _read_env_file( $env_file ) } };
  2         6  
356             }
357              
358 44         2050 local $CWD = $class_top_level; ## no critic (ProhibitLocalVars)
359 44         1862 return _set_env_hierarchically( $class, $env_found, $new_env );
360             }
361              
362             sub _use_imports {
363 4     4   7553 my ( $imports ) = @_;
364              
365 4 100 100     113 return @$imports == 1 && $imports->[ 0 ] =~ $REGEX_VERSION_NUMBER ? ' ' . $imports->[ 0 ] : '';
366             }
367              
368             1;