File Coverage

blib/lib/Test/Prereq/Meta.pm
Criterion Covered Total %
statement 240 245 97.9
branch 66 84 78.5
condition 36 57 63.1
subroutine 40 40 100.0
pod 5 5 100.0
total 387 431 89.7


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