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   293655 use 5.010; # because Module::Extract::Use has this.
  6         55  
4              
5 6     6   28 use strict;
  6         12  
  6         127  
6 6     6   29 use warnings;
  6         10  
  6         162  
7              
8 6     6   30 use Carp;
  6         16  
  6         351  
9 6     6   2492 use CPAN::Meta;
  6         134237  
  6         238  
10 6     6   59 use Exporter qw{ import };
  6         11  
  6         246  
11 6     6   3618 use ExtUtils::Manifest ();
  6         55731  
  6         192  
12 6     6   61 use File::Find ();
  6         12  
  6         81  
13 6     6   29 use File::Glob ();
  6         12  
  6         81  
14 6     6   24 use File::Spec;
  6         10  
  6         92  
15 6     6   3282 use Module::Extract::Use;
  6         10476  
  6         212  
16 6     6   15491 use Module::CoreList;
  6         557548  
  6         613  
17 6     6   9358 use Module::Metadata;
  6         32052  
  6         268  
18 6     6   64 use Scalar::Util ();
  6         10  
  6         126  
19 6     6   30 use Test::More 0.88;
  6         198  
  6         66  
20              
21             our $VERSION = '0.002';
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   2133 }->{$^O} || 'Unix';
  6         29  
  6         748  
39              
40 6     6   40 use constant REF_ARRAY => ref [];
  6         9  
  6         10762  
41              
42             sub new {
43 16     16 1 19677 my ( $class, %arg ) = @_;
44              
45 16   50     151 $arg{file_error} //= 'Failed to analyze %f: %e';
46 16   100     83 $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     110 $arg{path_type} //= DEFAULT_PATH_TYPE;
51 16   50     115 $arg{per_file_note} //= '%f';
52 16   100     84 $arg{perl_version} //= 'none';
53 16   50     96 $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       57 scalar grep { -d } qw{ .bzr .cdv .git .hg .svn CVS } ) ? 1 : 0,
  30         339  
63             };
64 16         40 foreach my $name ( keys %{ $default } ) {
  16         79  
65 80   100     406 $arg{$name} //= $default->{$name};
66             my $code = __PACKAGE__->can( "__validate_$name" ) ||
67             __PACKAGE__->can( '__validate_' . ref $default->{$name} ) ||
68 80   100 16   941 sub {};
69 80         257 $code->( $name, \%arg );
70             }
71              
72 16         54 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         33 local $Module::CoreList::version{none} = {};
  16         90  
78             local $Module::CoreList::version{this} =
79 16         88 $Module::CoreList::version{$]};
80             $core_modules = $Module::CoreList::version{$arg{perl_version}}
81 16 50       170 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         51 my $meta_data = $arg{_meta_file};
87              
88 16         31 my %requires;
89              
90 16         88 my $prereqs = $meta_data->effective_prereqs();
91 16         14518 foreach my $phase ( qw{ configure build test runtime } ) {
92 64         281 my $reqs = $prereqs->requirements_for( $phase, 'requires' );
93 64         2218 foreach my $module ( $reqs->required_modules() ) {
94 169         458 $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         45 delete $requires{perl};
103              
104 16         71 my $provides = _provides();
105              
106 2159         3887 my %has = map { $_ => 1 }
107 16         77 @{ $arg{accept} },
108 16         665 keys %{ $core_modules },
109 16         99 keys %{ $provides },
  16         151  
110             keys %requires,
111             ;
112              
113 16         187 $arg{uses} = { map { $_ => 1 } @{ $arg{uses} } };
  2         9  
  16         84  
114              
115 16 100       97 if ( $arg{verbose} ) {
116 2         4 my @dup;
117 2 100       17 @dup = grep { $requires{$_} } @{ $arg{accept} }
  1         7  
  2         7  
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       379 @dup = grep { $arg{uses}{$_} } @{ $arg{accept} }
  1         8  
  2         8  
123             and diag "The following @{[
124 1 50       11 @dup == 1 ? 'module appears' : 'modules appear'
125             ]} in both the 'accept' argument and\nthe 'uses' argument: ",
126             join ', ', sort @dup;
127             }
128              
129 16         291 delete $arg{accept};
130 16         109 delete $arg{_meta_file};
131 16         52 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     1088 _requires => \%requires,
      33        
151             }, ref $class || $class;
152              
153 16 100       106 if ( my $num = keys %arg ) {
154 1 50       11 croak "Unknown argument@{[ $num > 1 ? 's' : '' ]} ", join ', ',
155 1         3 map { "'$_'" } sort keys %arg;
  1         184  
156             }
157              
158 15         562 return $self;
159             }
160              
161             sub all_prereq_ok {
162 10     10 1 119 my ( $self, @file ) = _unpack_args( @_ );
163              
164 10 100       42 unless( @file ) {
165             @file = (
166 8         355 ( grep { -d } qw{ blib/arch blib/lib blib/script t } ),
167 2         20 ( map { File::Spec->abs2rel( $_ ) }
  4         326  
168             File::Glob::bsd_glob( '*.PL' ) ),
169             );
170             }
171              
172 10         35 my $need_skip = 1;
173 10         19 my $ok = 1;
174              
175             File::Find::find(
176             {
177             wanted => sub {
178 63 100   63   301 if ( $self->{_normalize_path} ) {
179 1         17 $self->{_normalize_path}->();
180 1 50       6 if ( $self->{prune}{$_} ) {
181 1         3 $File::Find::prune = 1;
182 1         13 return;
183             }
184             }
185 62 100       196 _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         3137 $_ = File::Spec->abs2rel( $_ );
190 29         97 $need_skip = 0;
191 29 100       117 $self->file_prereq_ok( $_ )
192             or $ok = 0;
193 29         964 return;
194             },
195             no_chdir => 1,
196 23     23   927 preprocess => sub { return( sort @_ ) },
197             },
198 10         1333 @file,
199             );
200              
201 10 100       154 if ( $need_skip ) {
202 1         15 state $TEST = Test::More->builder();
203 1         25 local $Test::Builder::Level = _nest_depth();
204             # $TEST->skip( "$file does not use any modules" );
205 1         7 $TEST->skip( 'No Perl files found' );
206             }
207              
208 10         806 return $ok;
209             }
210              
211             sub all_prereqs_used {
212 3     3 1 378 my ( $self ) = @_;
213              
214 3         20 state $TEST = Test::More->builder();
215 3         33 local $Test::Builder::Level = _nest_depth();
216              
217 3         14 $TEST->note( '' );
218              
219             my @unused = sort
220 19   100     69 grep { ! $self->{uses}{$_} && ! $self->{_requires}{$_}{file} }
221 3         743 keys %{ $self->{_requires} };
  3         17  
222 3 100       17 my $rslt = $TEST->ok( ! @unused, 'All required modules are used' )
223             or $TEST->diag( "The following @{[
224 1 50       1009 @unused == 1 ? 'prerequisite is' : 'prerequisites are'
225             ]} unused: ", join ', ', @unused );
226              
227 3 100 66     787 if ( $self->{verbose} and
228 1 50       10 my @dup = grep { $self->{_requires}{$_}{file} && $self->{uses}{$_} }
229 1         4 keys %{ $self->{_requires} }
230             ) {
231 1         3 $TEST->diag( "The following @{[
232 1 50       10 @dup == 1 ? 'module appears' : 'modules appear'
233             ]} in both 'use' statements and\nthe 'uses' argument: ",
234             join ', ', sort @dup );
235             }
236              
237 3         290 return $rslt;
238             }
239              
240             sub file_prereq_ok {
241 35     35 1 153 my ( $self, $file, @arg ) = _unpack_args( @_ );
242             @arg
243 35 50       121 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         114 state $TEST = Test::More->builder();
250 35         191 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         258 $TEST->note( '' );
257             $TEST->note(
258             ' ' x ( 4 + length( $TEST->current_test() + 1 ) ),
259             _format(
260             $self->{per_file_note},
261             {
262 35         12453 e => '',
263             f => $file,
264             m => '',
265             }
266             ),
267             );
268             }
269              
270 35         8522 my $need_skip = 1;
271 35         66 my $ok = 1;
272 35         82 my %module_found;
273              
274 35         95 state $extor = Module::Extract::Use->new();
275              
276 35         232 my $modules = $extor->get_modules_with_details( $file );
277 35 100       2897800 if ( my $err = $extor->error() ) {
278             $TEST->ok( 0,
279             _format(
280             $self->{file_error},
281             {
282 1         11 e => $err,
283             f => $file,
284             m => '',
285             },
286             )
287             );
288 1         1181 return 0;
289             }
290              
291 34         268 foreach my $usage (
292 215         392 sort { $a->{module} cmp $b->{module} }
293 34         214 @{ $modules }
294             ) {
295 137         28176 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       834 $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       507 $module_found{$module}++
307             and next;
308              
309             $self->{_requires}{$module}
310 134 100 100     413 and push @{ $self->{_requires}{$module}{file} ||= [] }, $file;
  107         505  
311              
312 134         251 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         177 $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     856 e => '',
332             f => $file,
333             m => $module,
334             },
335             ),
336             ) or $ok = 0;
337              
338             }
339              
340 34 100       9742 if ( $need_skip ) {
341 4         24 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         33 e => '',
347             f => $file,
348             m => '',
349             },
350             ),
351             );
352             }
353              
354 34         2349 return $ok;
355             }
356              
357             sub _format {
358 174     174   4503 my ( $tplt, $sub ) = @_;
359 174   33     890 $tplt =~ s| % ( . ) | $sub->{$1} // $1 |smxge;
  307         1255  
360 174         743 return $tplt;
361             }
362              
363             sub prereq_ok {
364 1     1 1 76 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   128 my ( $file ) = @_;
375 62 100       6932 -T $file
376             or return 0;
377 39 100       428 $file =~ m/ [.] (?: (?i: pl ) | pm | t ) \z /smx
378             and return 1;
379 10 50       282 open my $fh, '<', $file
380             or return 0;
381 10         106 local $_ = <$fh>;
382 10         106 close $fh;
383             defined
384 10 50       29 or return 0;
385 10         245 return m/ \A [#]! .* perl /smx;
386             }
387              
388             {
389             my %ignore;
390             BEGIN {
391 6     6   44 %ignore = map { $_ => 1 } __PACKAGE__, qw{ DB File::Find };
  18         4724  
392             }
393              
394             sub _nest_depth {
395 43     43   79 my $nest = 0;
396 43   50     470 $nest++ while $ignore{ caller( $nest ) || '' };
397 43         106 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   418 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   604 croak( 'Can not normalize VMS paths' );
416             }
417              
418 1     1   528 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   34 my %provides;
424 16         101 my $manifest = ExtUtils::Manifest::maniread();
425 16         9594 foreach my $file ( keys %{ $manifest } ) {
  16         151  
426 608 100       1491 $file =~ m/ [.] pm \z /smx
427             or next;
428 64 50       544 my $info = Module::Metadata->new_from_file( $file )
429             or next;
430 64         256163 foreach my $module ( $info->packages_inside() ) {
431 64         480 state $ignore = { map { $_ => 1 } qw{ main DB } };
  10         39  
432 64 100       303 $ignore->{$module}
433             and next;
434 48         440 $provides{$module} = 1;
435             }
436             }
437 16         537 return \%provides;
438             }
439              
440             sub _unpack_args {
441 45     45   139 my @arg = @_;
442 45 100 66     536 my $self = ( ref( $arg[0] ) && ref( $arg[0] )->isa( __PACKAGE__ ) ) ?
443             shift @arg :
444             __PACKAGE__->new();
445 45         170 return ( $self, @arg );
446             }
447              
448             sub __validate_meta_file {
449 16     16   51 my ( $name, $arg ) = @_;
450 16 100 66     114 if ( Scalar::Util::blessed( $arg->{$name} ) &&
451             $arg->{$name}->isa( 'CPAN::Meta' )
452             ) {
453 1         6 $arg->{"_$name"} = $arg->{$name};
454 1         4 return;
455             }
456 15         52 __validate_ARRAY( $name, $arg );
457 15 50       26 @{ $arg->{$name} }
  15         61  
458             or croak( "'$name' must specify at least one file" );
459 15         31 foreach my $fn ( @{ $arg->{$name} } ) {
  15         49  
460 16 100       464 -r $fn
461             or next;
462 15         60 $arg->{$name} = $fn;
463 15         180 $arg->{"_$name"} = CPAN::Meta->load_file( $fn );
464 15         355599 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   54 my ( $name, $arg ) = @_;
474 16         80 __validate_ARRAY( $name, $arg );
475 16         28 my %rslt;
476 16         34 foreach ( @{ $arg->{$name} } ) {
  16         59  
477 1   33     14 $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     102 $arg->{_normalize_path} ||= undef;
484 16         40 $arg->{$name} = \%rslt;
485 16         45 return;
486             }
487              
488             sub __validate_ARRAY {
489 63     63   160 my ( $name, $arg ) = @_;
490             ref $arg->{$name}
491 63 100       188 or $arg->{$name} = [ $arg->{$name} ];
492 63 50       183 REF_ARRAY eq ref $arg->{$name}
493             or croak( "'$name' must be a SCALAR or an ARRAY reference" );
494 63         123 return;
495             }
496              
497             1;
498              
499             __END__