File Coverage

blib/lib/Test/Prereq/Meta.pm
Criterion Covered Total %
statement 242 249 97.1
branch 70 90 77.7
condition 35 58 60.3
subroutine 41 41 100.0
pod 5 5 100.0
total 393 443 88.7


line stmt bran cond sub pod time code
1             package Test::Prereq::Meta;
2              
3 6     6   1025133 use 5.010; # because Module::Extract::Use has this.
  6         21  
4              
5 6     6   30 use strict;
  6         11  
  6         182  
6 6     6   25 use warnings;
  6         17  
  6         302  
7              
8 6     6   38 use Carp;
  6         18  
  6         466  
9 6     6   1625 use CPAN::Meta;
  6         98888  
  6         5446  
10 6     6   52 use Exporter qw{ import };
  6         11  
  6         178  
11 6     6   3348 use ExtUtils::Manifest ();
  6         87178  
  6         190  
12 6     6   48 use File::Find ();
  6         18  
  6         127  
13 6     6   22 use File::Glob ();
  6         14  
  6         81  
14 6     6   19 use File::Spec;
  6         9  
  6         81  
15 6     6   2784 use Module::Extract::Use;
  6         7942  
  6         212  
16 6     6   26508 use Module::CoreList 2.13;
  6         1187781  
  6         73  
17 6     6   15137 use Module::Metadata;
  6         38883  
  6         237  
18 6     6   43 use Scalar::Util ();
  6         10  
  6         128  
19 6     6   59 use Test::More 0.88;
  6         140  
  6         45  
20              
21             our $VERSION = '0.004';
22              
23             our @EXPORT_OK = qw{ all_prereq_ok file_prereq_ok prereq_ok };
24             our %EXPORT_TAGS = (
25             all => \@EXPORT_OK,
26             );
27              
28             # Hash lifted verbatim from File::Spec 3.78 published 2018-08-29
29             use constant DEFAULT_PATH_TYPE => {
30             MSWin32 => 'Win32',
31             os2 => 'OS2',
32             VMS => 'VMS',
33             NetWare => 'Win32', # Yes, File::Spec::Win32 works on NetWare.
34             symbian => 'Win32', # Yes, File::Spec::Win32 works on symbian.
35             dos => 'OS2', # Yes, File::Spec::OS2 works on DJGPP.
36             cygwin => 'Cygwin',
37             amigaos => 'AmigaOS',
38 6   50 6   2066 }->{$^O} || 'Unix';
  6         9  
  6         648  
39              
40 6     6   37 use constant REF_ARRAY => ref [];
  6         9  
  6         11850  
41              
42             sub new {
43 16     16 1 312306 my ( $class, %arg ) = @_;
44              
45 16   50     144 $arg{file_error} //= 'Failed to analyze %f: %e';
46 16   100     75 $arg{name} //= 'Prereq test: %f uses %m';
47             # NOTE that {path_type} is unsupported, and may change or be
48             # retracted without warning. I thought I needed it to support
49             # argument {prune}, which is itself experimental.
50 16   50     92 $arg{path_type} //= DEFAULT_PATH_TYPE;
51 16   50     90 $arg{per_file_note} //= '%f';
52 16   100     89 $arg{perl_version} //= 'none';
53 16   50     94 $arg{skip_name} //= 'Prereq test: %f does not use any modules';
54              
55             state $default = {
56             accept => [],
57             meta_file => [ qw{
58             MYMETA.json MYMETA.yml META.json META.yml } ],
59             prune => [],
60             uses => [],
61             verbose => (
62 16 50       47 scalar grep { -d } qw{ .bzr .cdv .git .hg .svn CVS } ) ? 1 : 0,
  30         460  
63             };
64 16         32 foreach my $name ( keys %{ $default } ) {
  16         66  
65 80   66     379 $arg{$name} //= $default->{$name};
66             my $code = __PACKAGE__->can( "__validate_$name" ) ||
67             __PACKAGE__->can( '__validate_' . ref $default->{$name} ) ||
68 80   66 16   907 sub {};
69 80         291 $code->( $name, \%arg );
70             }
71              
72             my $core_modules = _get_corelist_version( $arg{perl_version} )
73 16 50       87 or croak( "Unknown 'perl_version' $arg{perl_version}" );
74              
75             # The below is pretty much verbatim from the CPAN::Meta synopsis
76              
77 16         38 my $meta_data = $arg{_meta_file};
78              
79 16         30 my %requires;
80              
81 16         83 my $prereqs = $meta_data->effective_prereqs();
82 16         16480 foreach my $phase ( qw{ configure build test runtime } ) {
83 64         235 my $reqs = $prereqs->requirements_for( $phase, 'requires' );
84 64         2095 foreach my $module ( $reqs->required_modules() ) {
85 169         504 $requires{$module} = {};
86             }
87             }
88              
89             # The above is pretty much verbatim from the CPAN::Meta synopsis
90              
91             # NOTE that if we actually need the Perl version, we need to nab it
92             # before here.
93 16         40 delete $requires{perl};
94              
95 16         65 my $provides = _provides();
96              
97 2537         5742 my %has = map { $_ => 1 }
98 16         53 @{ $arg{accept} },
99 16         848 keys %{ $core_modules },
100 16         35 keys %{ $provides },
  16         151  
101             keys %requires,
102             ;
103              
104 16         379 $arg{uses} = { map { $_ => 1 } @{ $arg{uses} } };
  2         8  
  16         76  
105              
106 16 100       81 if ( $arg{verbose} ) {
107 2         4 my @dup;
108 2 100       3 @dup = grep { $requires{$_} } @{ $arg{accept} }
  1         5  
  2         6  
109             and diag "The following @{[
110 1 50       12 @dup == 1 ? 'module appears' : 'modules appear'
111             ]} in both the prerequisites and\nthe 'accept' argument: ",
112             join ', ', sort @dup;
113 2 100       644 @dup = grep { $arg{uses}{$_} } @{ $arg{accept} }
  1         9  
  2         8  
114             and diag "The following @{[
115 1 50       11 @dup == 1 ? 'module appears' : 'modules appear'
116             ]} in both the 'accept' argument and\nthe 'uses' argument: ",
117             join ', ', sort @dup;
118             }
119              
120 16         427 delete $arg{accept};
121 16         75 delete $arg{_meta_file};
122 16         40 delete $arg{path_type};
123              
124             my $self = bless {
125             # accept => $arg{accept},
126             # core_modules => $core_modules,
127             file_error => delete $arg{file_error},
128             has => \%has,
129             meta_file => delete $arg{meta_file},
130             meta_data => $meta_data,
131             name => delete $arg{name},
132             per_file_note => delete $arg{per_file_note},
133             perl_version => delete $arg{perl_version},
134             prune => delete $arg{prune},
135             # provides => $provides,
136             skip_name => delete $arg{skip_name},
137             uses => delete $arg{uses},
138             verbose => delete $arg{verbose},
139             _both_tools => ( -e 'Makefile.PL' && -e 'Build.PL' ),
140             _normalize_path => delete $arg{_normalize_path},
141 16   33     1055 _requires => \%requires,
      33        
142             }, ref $class || $class;
143              
144 16 100       84 if ( my $num = keys %arg ) {
145 1 50       12 croak "Unknown argument@{[ $num > 1 ? 's' : '' ]} ", join ', ',
146 1         3 map { "'$_'" } sort keys %arg;
  1         218  
147             }
148              
149 15         546 return $self;
150             }
151              
152             sub all_prereq_ok {
153 10     10 1 60 my ( $self, @file ) = _unpack_args( @_ );
154              
155 10 100       32 unless( @file ) {
156             @file = (
157 8         2468 ( grep { -d } qw{ blib/arch blib/lib blib/script t } ),
158 2         6 ( map { File::Spec->abs2rel( $_ ) }
  4         459  
159             File::Glob::bsd_glob( '*.PL' ) ),
160             );
161             }
162              
163 10         24 my $need_skip = 1;
164 10         18 my $ok = 1;
165              
166             File::Find::find(
167             {
168             wanted => sub {
169 63 100   63   331 if ( $self->{_normalize_path} ) {
170 1         4 $self->{_normalize_path}->();
171 1 50       4 if ( $self->{prune}{$_} ) {
172 1         2 $File::Find::prune = 1;
173 1         9 return;
174             }
175             }
176 62 100       202 _is_perl( $_ )
177             or return;
178             # The following is because File::Find tends to give us
179             # './fubar' if 'fubar' is in the current directory.
180 29         3198 $_ = File::Spec->abs2rel( $_ );
181 29         80 $need_skip = 0;
182 29 100       150 $self->file_prereq_ok( $_ )
183             or $ok = 0;
184 29         1285 return;
185             },
186             no_chdir => 1,
187 23     23   1166 preprocess => sub { return( sort @_ ) },
188             },
189 10         1197 @file,
190             );
191              
192 10 100       138 if ( $need_skip ) {
193 1         9 state $TEST = Test::More->builder();
194 1         18 local $Test::Builder::Level = _nest_depth();
195             # $TEST->skip( "$file does not use any modules" );
196 1         6 $TEST->skip( 'No Perl files found' );
197             }
198              
199 10         736 return $ok;
200             }
201              
202             sub all_prereqs_used {
203 3     3 1 509 my ( $self ) = @_;
204              
205 3         20 state $TEST = Test::More->builder();
206 3         27 local $Test::Builder::Level = _nest_depth();
207              
208 3         13 $TEST->note( '' );
209              
210             my @unused = sort
211 19   100     70 grep { ! $self->{uses}{$_} && ! $self->{_requires}{$_}{file} }
212 3         1576 keys %{ $self->{_requires} };
  3         17  
213 3 100       38 my $rslt = $TEST->ok( ! @unused, 'All required modules are used' )
214             or $TEST->diag( "The following @{[
215 1 50       1673 @unused == 1 ? 'prerequisite is' : 'prerequisites are'
216             ]} unused: ", join ', ', @unused );
217              
218 3 100 66     1097 if ( $self->{verbose} and
219 1 50       9 my @dup = grep { $self->{_requires}{$_}{file} && $self->{uses}{$_} }
220 1         3 keys %{ $self->{_requires} }
221             ) {
222 1         2 $TEST->diag( "The following @{[
223 1 50       9 @dup == 1 ? 'module appears' : 'modules appear'
224             ]} in both 'use' statements and\nthe 'uses' argument: ",
225             join ', ', sort @dup );
226             }
227              
228 3         344 return $rslt;
229             }
230              
231             sub file_prereq_ok {
232 35     35 1 200 my ( $self, $file, @arg ) = _unpack_args( @_ );
233             @arg
234 35 50       107 and confess(
235             'Usage: $tpm->file_prereq_ok( $file ) or file_prereq_ok( $file )' );
236              
237             # Because this gets us a pre-built object I use $Test::Builder::Level
238             # (localized) to get tests reported relative to the correct file and
239             # line, rather than setting the 'level' attribute.
240 35         121 state $TEST = Test::More->builder();
241 35         191 local $Test::Builder::Level = _nest_depth();
242              
243 35 50       193 if ( $self->{per_file_note} ne '' ) {
244             # We are not interested in the actual test number, but we need
245             # to know how many digits it is so that the note can be indented
246             # properly.
247 35         225 $TEST->note( '' );
248             $TEST->note(
249             ' ' x ( 4 + length( $TEST->current_test() + 1 ) ),
250             _format(
251             $self->{per_file_note},
252             {
253 35         27561 e => '',
254             f => $file,
255             m => '',
256             }
257             ),
258             );
259             }
260              
261 35         22104 my $need_skip = 1;
262 35         91 my $ok = 1;
263 35         65 my %module_found;
264              
265 35         100 state $extor = Module::Extract::Use->new();
266              
267 35         244 my $modules = $extor->get_modules_with_details( $file );
268 35 100       13889301 if ( my $err = $extor->error() ) {
269             $TEST->ok( 0,
270             _format(
271             $self->{file_error},
272             {
273 1         14 e => $err,
274             f => $file,
275             m => '',
276             },
277             )
278             );
279 1         1733 return 0;
280             }
281              
282 34         288 foreach my $usage (
283 215         447 sort { $a->{module} cmp $b->{module} }
284 34         206 @{ $modules }
285             ) {
286 136         43791 my $module = $usage->{module};
287              
288             # The following is needed because Module::Extract::Use tries too
289             # hard to find return() statements embedded in other statements.
290 136 50       992 $module =~ m/ \A [\w:]+ \z /smx
291             or next;
292              
293             # The following is needed because Module::Extract::Use returns
294             # duplicate 'require' statements because it finds them both in
295             # the scan for PPI::Statement::Include objects and in the scan
296             # for PPI::Token::Word 'require' objects.
297 136 100       611 $module_found{$module}++
298             and next;
299              
300             $self->{_requires}{$module}
301 134 100 100     552 and push @{ $self->{_requires}{$module}{file} ||= [] }, $file;
  107         593  
302              
303 134         354 state $toolchain = {
304             'Makefile.PL' => {
305             'ExtUtils::MakeMaker' => 1,
306             'inc::Module::Install' => 1,
307             },
308             'Build.PL' => {
309             'Module::Build' => 1,
310             'Module::Build::Tiny' => 1,
311             },
312             };
313              
314 134         196 $need_skip = 0;
315             $TEST->ok(
316             $self->{has}{$module} ||
317             $self->{_both_tools} && $toolchain->{$file}{$module} ||
318             0,
319             _format(
320             $self->{name},
321             {
322 134 100 100     1198 e => '',
323             f => $file,
324             m => $module,
325             },
326             ),
327             ) or $ok = 0;
328              
329             }
330              
331 34 100       13048 if ( $need_skip ) {
332 4         18 local $Test::Builder::Level = _nest_depth();
333             # $TEST->skip( "$file does not use any modules" );
334             $TEST->skip( _format(
335             $self->{skip_name},
336             {
337 4         36 e => '',
338             f => $file,
339             m => '',
340             },
341             ),
342             );
343             }
344              
345 34         4306 return $ok;
346             }
347              
348             sub _format {
349 174     174   6908 my ( $tplt, $sub ) = @_;
350 174   33     996 $tplt =~ s| % ( . ) | $sub->{$1} // $1 |smxge;
  307         1577  
351 174         957 return $tplt;
352             }
353              
354             sub prereq_ok {
355 1     1 1 120594 my ( $perl_version, $name, $accept ) = @_;
356 1   33     17 my $self = __PACKAGE__->new(
357             accept => $accept,
358             name => $name,
359             perl_version => $perl_version // $],
360             );
361 1         7 return $self->all_prereq_ok();
362             }
363              
364             sub _get_corelist_version {
365 16     16   40 my ( $perl_ver ) = @_;
366              
367 16 100       90 $perl_ver eq 'none'
368             and return {};
369              
370 3 100       14 $perl_ver eq 'this'
371             and $perl_ver = $];
372              
373 3         7 my $data;
374 3 50       21 $data = $Module::CoreList::version{$perl_ver}
375             and return $data;
376              
377             # The following is needed under very old versions of
378             # Module::CoreList -- pre-2.14 I think.
379 0         0 $perl_ver =~ s/ 0+ \z //smx;
380 0         0 return $Module::CoreList::version{$perl_ver};
381             }
382              
383             sub _is_perl {
384 62     62   147 my ( $file ) = @_;
385 62 100       6589 -T $file
386             or return 0;
387 39 100       450 $file =~ m/ [.] (?: (?i: pl ) | pm | t ) \z /smx
388             and return 1;
389 10 50       495 open my $fh, '<', $file
390             or return 0;
391 10         131 local $_ = <$fh>;
392 10         163 close $fh;
393             defined
394 10 50       35 or return 0;
395 10         393 return m/ \A [#]! .* perl /smx;
396             }
397              
398             {
399             my %ignore;
400             BEGIN {
401 6     6   36 %ignore = map { $_ => 1 } __PACKAGE__, qw{ DB File::Find };
  18         5147  
402             }
403              
404             sub _nest_depth {
405 43     43   85 my $nest = 0;
406 43   50     587 $nest++ while $ignore{ caller( $nest ) || '' };
407 43         121 return $nest;
408             }
409             }
410              
411             # All the __normalize_path_* subroutines operate on $_. They take no
412             # arguments and return nothing relevant. The names are File::Spec::
413             # OS-specific class names, and the intent is that anything supported by
414             # File::Spec should appear here.
415              
416       1     sub __normalize_path_AmigaOS {} # Assumed based on File::Spec::AmigaOS
417              
418       1     sub __normalize_path_Cygwin {} # I believe.
419              
420 1     1   926 sub __normalize_path_OS2 { s| \\ |/|smxg; } ## no critic (RequireFinalReturn)
421              
422       3     sub __normalize_path_Unix {}
423              
424             sub __normalize_path_VMS {
425 1     1   1106 croak( 'Can not normalize VMS paths' );
426             }
427              
428 1     1   838 sub __normalize_path_Win32 { s| \\ |/|smxg; } ## no critic (RequireFinalReturn)
429              
430             # We don't use Module::Metadata->provides(), because it filters out
431             # private packages. While we're at it, we just process every .pm we find.
432             sub _provides {
433 16     16   30 my %provides;
434 16         86 my $manifest = ExtUtils::Manifest::maniread();
435 16         8684 foreach my $file ( keys %{ $manifest } ) {
  16         131  
436 640 100       1404 $file =~ m/ [.] pm \z /smx
437             or next;
438 64 50       399 my $info = Module::Metadata->new_from_file( $file )
439             or next;
440 64         264845 foreach my $module ( $info->packages_inside() ) {
441 64         389 state $ignore = { map { $_ => 1 } qw{ main DB } };
  10         48  
442 64 100       269 $ignore->{$module}
443             and next;
444 48         392 $provides{$module} = 1;
445             }
446             }
447 16         404 return \%provides;
448             }
449              
450             sub _unpack_args {
451 45     45   142 my @arg = @_;
452 45 100 66     604 my $self = ( ref( $arg[0] ) && ref( $arg[0] )->isa( __PACKAGE__ ) ) ?
453             shift @arg :
454             __PACKAGE__->new();
455 45         189 return ( $self, @arg );
456             }
457              
458             sub __validate_meta_file {
459 16     16   61 my ( $name, $arg ) = @_;
460 16 100 66     85 if ( Scalar::Util::blessed( $arg->{$name} ) &&
461             $arg->{$name}->isa( 'CPAN::Meta' )
462             ) {
463 1         6 $arg->{"_$name"} = $arg->{$name};
464 1         5 return;
465             }
466 15         43 __validate_ARRAY( $name, $arg );
467 15 50       22 @{ $arg->{$name} }
  15         50  
468             or croak( "'$name' must specify at least one file" );
469 15         27 foreach my $fn ( @{ $arg->{$name} } ) {
  15         36  
470 16         228 local $@ = undef;
471 16 100       38 eval {
472 16         125 $arg->{"_$name"} = CPAN::Meta->load_file( $fn );
473             } or next;
474 15         254478 $arg->{$name} = $fn;
475 15         66 return;
476             }
477 0 0       0 1 == @{ $arg }
  0         0  
478             and croak( "$arg->{$name}[0] not readable" );
479 0         0 local $" = ', ';
480 0         0 croak( "None of @{ $arg->{$name} } readable" );
  0         0  
481             }
482              
483             sub __validate_prune {
484 16     16   61 my ( $name, $arg ) = @_;
485 16         59 __validate_ARRAY( $name, $arg );
486 16         37 my %rslt;
487 16         28 foreach ( @{ $arg->{$name} } ) {
  16         99  
488 1   33     17 $arg->{_normalize_path} ||= __PACKAGE__->can(
      33        
489             "__normalize_path_$arg->{path_type}" )
490             || croak( "Invalid path type '$arg->{path_type}'" );
491 1         5 $arg->{_normalize_path}->();
492 1         4 $rslt{$_} = 1;
493             }
494 16   100     104 $arg->{_normalize_path} ||= undef;
495 16         51 $arg->{$name} = \%rslt;
496 16         36 return;
497             }
498              
499             sub __validate_ARRAY {
500 63     63   115 my ( $name, $arg ) = @_;
501             ref $arg->{$name}
502 63 100       157 or $arg->{$name} = [ $arg->{$name} ];
503 63 50       147 REF_ARRAY eq ref $arg->{$name}
504             or croak( "'$name' must be a SCALAR or an ARRAY reference" );
505 63         116 return;
506             }
507              
508             1;
509              
510             __END__