File Coverage

blib/lib/Test/Expander.pm
Criterion Covered Total %
statement 229 229 100.0
branch 89 90 100.0
condition 16 16 100.0
subroutine 40 40 100.0
pod 0 7 0.0
total 374 382 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.5'; ## no critic (RequireUseStrict, RequireUseWarnings)
6              
7 18     18   1313843 use strict;
  18         120  
  18         785  
8             use warnings
9 18         853 FATAL => qw( all ),
10 18     18   119 NONFATAL => qw( deprecated exec internal malloc newline portable recursion );
  18         36  
11              
12 18     18   6508 use Const::Fast;
  18         37633  
  18         169  
13 18     18   8747 use File::chdir;
  18         52590  
  18         1890  
14 18     18   12255 use File::Temp qw( tempdir tempfile );
  18         343574  
  18         1160  
15 18     18   9282 use Importer;
  18         87793  
  18         158  
16 18     18   14855 use Path::Tiny qw( cwd path );
  18         206708  
  18         1236  
17 18     18   6701 use Scalar::Readonly qw( readonly_on );
  18         8225  
  18         1021  
18 18     18   6743 use Test2::Tools::Basic;
  18         384804  
  18         1464  
19 18     18   7822 use Test2::Tools::Explain;
  18         6282  
  18         1095  
20 18     18   7400 use Test2::V0 qw();
  18         2363086  
  18         1061  
21              
22 18         29296 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   7323 );
  18         46  
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 1963 my ( $coderef, $description ) = @_;
50              
51 2         5 eval { $coderef->() };
  2         62  
52              
53 2         20 return ok( $@, $description );
54             }
55              
56             sub import {
57 32     32   29191 my ( $class, @exports ) = @_;
58              
59 32         107 my $frame_index = 0;
60 32         55 my $test_file;
61 32         279 while( my @current_frame = caller( $frame_index++ ) ) {
62 102         4967 $test_file = path( $current_frame[ 1 ] ) =~ s{^/}{}r;
63             }
64 32         1175 my $options = _parse_options( \@exports, $test_file );
65              
66 22         74 _export_most_symbols( $options, $test_file );
67 22         141 _set_env( $options->{ -target }, $test_file );
68 22 100 100     157 _mock_builtins( $options ) if defined( $CLASS ) && exists( $options->{ -builtins } );
69              
70 22         203 Test2::V0->import( %$options );
71              
72 22         27080 _export_rest_symbols();
73 22         258 Importer->import_into( $class, scalar( caller ), () );
74              
75 22         1679539 return;
76             }
77              
78             sub is_deeply ( $$;$@ ) {
79 2     2 0 2055 my ( $got, $expected, $title ) = @_;
80              
81 2         12 return is( $got, $expected, $title );
82             }
83              
84             sub lives_ok ( &;$ ) {
85 3     3 0 5768 my ( $coderef, $description ) = @_;
86              
87 3         41 eval { $coderef->() };
  3         12  
88 3 100       23 diag( $MSG_UNEXPECTED_EXCEPTION . $@ ) if $@;
89              
90 3         565 return ok( !$@, $description );
91             }
92              
93             sub new_ok {
94 3     3 0 9388 my ( $class, $args ) = @_;
95              
96 3   100     20 $args ||= [];
97 3         39 my $obj = eval { $class->new( @$args ) };
  3         23  
98 3         168 ok( !$@, _new_test_message( $class ) );
99              
100 3         1019 return $obj;
101             }
102              
103             sub require_ok {
104 3     3 0 6065 my ( $module ) = @_;
105              
106 3         12 my $package = caller;
107 3         176 my $require_result = eval( sprintf( $FMT_REQUIRE_IMPLEMENTATION, $package, $module ) );
108 3         1800 ok( $require_result, sprintf( $FMT_REQUIRE_DESCRIPTION, $module, _error() ) );
109              
110 3         639 return $require_result;
111             }
112              
113             sub throws_ok ( &$;$ ) {
114 13     13 0 376375 my ( $coderef, $expecting, $description ) = @_;
115              
116 13         31 eval { $coderef->() };
  13         38  
117 13         106 my $exception = $@;
118 13         32 my $expected_type = ref( $expecting );
119              
120 13 100       62 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 1937 my ( $module, @imports ) = @_;
126              
127 2         13 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         19 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         382 return $require_result;
139             }
140              
141             sub _determine_testee {
142 23     23   91 my ( $options, $test_file ) = @_;
143              
144 23 100       77 if ( $options->{ -lib } ) {
145 4         6 foreach my $directory ( @{ $options->{ -lib } } ) {
  4         68  
146 3 100       11 $DIE->( $FMT_INVALID_DIRECTORY, $directory, 'invalid type' ) if ref( $directory );
147 2         129 my $inc_entry = eval( $directory );
148 2 100       97 $DIE->( $FMT_INVALID_DIRECTORY, $directory, $@ ) if $@;
149 1         5 unshift( @INC, $inc_entry );
150             }
151 1         10 delete( $options->{ -lib } );
152             }
153              
154 20 100       67 if ( exists( $options->{ -method } ) ) {
155 4         10 delete( $options->{ -method } );
156             }
157             else {
158 16         51 $METHOD = path( $test_file )->basename( $REGEX_ANY_EXTENSION );
159             }
160              
161 20 100       1660 unless ( exists( $options->{ -target } ) ) { # Try to determine class / module autmatically
162 14         67 my ( $test_root ) = $test_file =~ $REGEX_TOP_DIR_IN_PATH;
163 14         125 my $testee = path( $test_file )->relative( $test_root )->parent;
164 14 100       7521 $options->{ -target } = $testee =~ s{/}{::}gr if grep { path( $_ )->child( $testee . '.pm' )->is_file } @INC;
  155         12403  
165             }
166 20 100       1342 if ( defined( $options->{ -target } ) ) {
167 16         41 $CLASS = $options->{ -target };
168             }
169             else {
170 4         9 delete( $options->{ -target } );
171             }
172              
173 20         56 return $options;
174             }
175              
176             sub _error {
177 7     8   7357 my ( $search_string, $replacement_string ) = @_;
178              
179 7 100       54 return '' if $@ eq '';
180              
181 3         13 my $error = $MSG_ERROR_WAS . $@ =~ s/\n$//mr;
182 3 100       23 $error =~ s/$search_string/$replacement_string/m if defined( $search_string );
183 3         32 return $error;
184             }
185              
186             sub _export_most_symbols {
187 21     22   51 my ( $options, $test_file ) = @_;
188              
189 21 100       61 $TEST_FILE = path( $test_file )->absolute->stringify if path( $test_file )->exists;
190              
191 21         2620 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   328 return _export_symbols( %REST_CONSTANTS_TO_EXPORT ) if $CLASS && $METHOD && ( $METHOD_REF = $CLASS->can( $METHOD ) );
      100        
197              
198 6         16 $METHOD = undef;
199 6         11 return;
200             }
201              
202             sub _export_symbols {
203 33     34   123 my %constants = @_;
204              
205 33         210 foreach my $name ( sort keys( %constants ) ) { # Export defined constants
206 18     18   193 no strict qw( refs ); ## no critic (ProhibitProlongedStrictureOverride)
  18         40  
  18         3720  
207 106 100       42817 my $value = eval( "${ \$name }" ) or next;
  106         5903  
208 69         235 readonly_on( ${ __PACKAGE__ . '::' . $name =~ s/^.//r } );
  69         470  
209 69         168 push( @EXPORT, $name );
210 69         261 $NOTE->( $FMT_SET_TO, $name, $constants{ $name }->( $value, $CLASS ) );
211             }
212              
213 33         29152 return;
214             }
215              
216             sub _mock_builtins {
217 1     2   3 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   146 no strict qw( refs );
  18         56  
  18         11938  
222 1         14 *${ sub_full_name } = $sub_ref;
223             }
224 1         3 delete( $options->{ -builtins } );
225              
226 1         2 return;
227             }
228              
229             sub _new_test_message {
230 4     5   4909 my ( $class ) = @_;
231              
232 4 100       65 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         56 my $options = {};
239 30         138 while ( my $option_name = shift( @$exports ) ) {
240 26 100 100     1587 $DIE->( $FMT_UNKNOWN_OPTION, $option_name, shift( @$exports ) // '' ) if $option_name !~ /^-\w/;
241              
242 24         47 my $option_value = shift( @$exports );
243 24 100       120 if ( $option_name eq '-builtins' ) { ## no critic (ProhibitCascadingIfElse)
    100          
    100          
    100          
    100          
    100          
244 3 100       18 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
245 2         11 while ( my ( $sub_name, $sub_ref ) = each( %$option_value ) ) {
246 2 100       17 $DIE->( $FMT_INVALID_VALUE, $option_name . "->{ $sub_name }", $sub_ref ) if ref( $sub_ref ) ne 'CODE';
247             }
248 1         3 $options->{ $option_name } = $option_value;
249             }
250             elsif ( $option_name eq '-lib' ) {
251 4 100       16 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'ARRAY';
252 3         13 $options->{ $option_name } = $option_value;
253             }
254             elsif ( $option_name eq '-method' ) {
255 5 100       18 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value );
256 4         17 $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       15 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
263 2         18 $TEMP_DIR = tempdir( CLEANUP => 1, %$option_value );
264             }
265             elsif ( $option_name eq '-tempfile' ) {
266 2 100       11 $DIE->( $FMT_INVALID_VALUE, $option_name, $option_value ) if ref( $option_value ) ne 'HASH';
267 1         2 my $file_handle;
268 1         6 ( $file_handle, $TEMP_FILE ) = tempfile( UNLINK => 1, %$option_value );
269             }
270             else {
271 1         5 $options->{ $option_name } = $option_value;
272             }
273             }
274              
275 22         476 return _determine_testee( $options, $test_file );
276             }
277              
278             sub _read_env_file {
279 9     10   19 my ( $env_file ) = @_;
280              
281 9         26 my @lines = path( $env_file )->lines( { chomp => 1 } );
282 9         2367 my %env;
283 9         57 while ( my ( $index, $line ) = each( @lines ) ) {
284             ## no critic (ProhibitUnusedCapture)
285 14 100       470 next unless $line =~ /^ (? \w+) \s* (?: = \s* (? \S .*) | $ )/x;
286 18     18   8240 my ( $name, $value ) = @+{ qw( name value ) };
  18         7095  
  18         11960  
  13         126  
287 13 100       89 if ( exists( $+{ value } ) ) {
    100          
288 11         26 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         19 $value = eval {
295 3     3   23 eval( $stricture . '$value' );
  3     3   6  
  3     1   124  
  3         22  
  3         7  
  3         134  
  11         830  
  1         538  
  1         1663  
  1         35  
296 11 50       38 die( $@ ) if $@; # uncoverable branch true
297 3     3   20 $value = eval( $stricture . $value );
  3     3   6  
  3         141  
  3         20  
  3         13  
  3         110  
  11         764  
298 11 100       474 die( $@ ) if $@;
299 9         21 $value;
300             };
301 11 100       48 $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         33 $NOTE->( $FMT_SET_ENV_VAR, $name, $value, $env_file );
304 8         3241 $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         3 $env{ $name } = $ENV{ $name };
312 1         6 $NOTE->( $FMT_KEEP_ENV_VAR, $name, $ENV{ $name }, $env_file );
313             }
314             }
315              
316 7         508 return \%env;
317             }
318              
319             sub _set_env {
320 29     29   30206 my ( $class, $test_file ) = @_;
321              
322 29 100       104 return unless path( $test_file )->exists;
323              
324 28         1272 my $env_found = $FALSE;
325 28         65 my $new_env = {};
326             {
327 28         55 local $CWD = $test_file =~ s{/.*}{}r; ## no critic (ProhibitLocalVars)
  28         141  
328 28         1929 ( $env_found, $new_env ) = _set_env_hierarchically( $class, $env_found, $new_env );
329             }
330              
331 28         1747 my $env_file = $test_file =~ s/$REGEX_ANY_EXTENSION/.env/r;
332              
333 28 100       337 if ( path( $env_file )->is_file ) {
334 7 100       359 $env_found = $TRUE unless $env_found;
335 7         21 my $method_env = _read_env_file( $env_file );
336 5         26 @$new_env{ keys( %$method_env ) } = values( %$method_env );
337             }
338              
339 26 100       1582 %ENV = %$new_env if $env_found;
340              
341 26         98 return;
342             }
343              
344             sub _set_env_hierarchically {
345 72     72   191 my ( $class, $env_found, $new_env ) = @_;
346              
347 72 100       250 return ( $env_found, $new_env ) unless $class;
348              
349 46         66 my $class_top_level;
350 46         358 ( $class_top_level, $class ) = $class =~ $REGEX_CLASS_HIERARCHY_LEVEL;
351              
352 46 100       152 return ( $FALSE, {} ) unless path( $class_top_level )->is_dir;
353              
354 44         2423 my $env_file = $class_top_level . '.env';
355 44 100       131 if ( path( $env_file )->is_file ) {
356 2 100       115 $env_found = $TRUE unless $env_found;
357 2         10 $new_env = { %$new_env, %{ _read_env_file( $env_file ) } };
  2         5  
358             }
359              
360 44         2024 local $CWD = $class_top_level; ## no critic (ProhibitLocalVars)
361 44         1823 return _set_env_hierarchically( $class, $env_found, $new_env );
362             }
363              
364             sub _use_imports {
365 4     4   7794 my ( $imports ) = @_;
366              
367 4 100 100     118 return @$imports == 1 && $imports->[ 0 ] =~ $REGEX_VERSION_NUMBER ? ' ' . $imports->[ 0 ] : '';
368             }
369              
370             1;