File Coverage

blib/lib/Test/Dependencies.pm
Criterion Covered Total %
statement 136 141 96.4
branch 34 48 70.8
condition 30 47 63.8
subroutine 13 13 100.0
pod 1 1 100.0
total 214 250 85.6


line stmt bran cond sub pod time code
1             package Test::Dependencies;
2              
3 6     6   882791 use warnings;
  6         16  
  6         385  
4 6     6   36 use strict;
  6         14  
  6         152  
5              
6 5     5   32 use Carp;
  5         10  
  5         374  
7 5     5   22662 use Module::CoreList;
  5         1252323  
  5         75  
8 5     5   15092 use Pod::Strip;
  5         282305  
  5         291  
9              
10 5     5   623 use parent 'Test::Builder::Module';
  5         342  
  5         43  
11              
12             =head1 NAME
13              
14             Test::Dependencies - Ensure that the dependency listing is complete
15              
16             =head1 VERSION
17              
18             Version 0.34
19              
20             =cut
21              
22             our $VERSION = '0.34';
23              
24             =head1 SYNOPSIS
25              
26             In your t/00-dependencies.t:
27              
28             use CPAN::Meta; # or CPAN::Meta::cpanfile
29             use File::Find::Rule::Perl;
30              
31             use Test::More;
32             use Test::Dependencies '0.28' forward_compatible => 1;
33              
34             my $meta = CPAN::Meta->load_file('META.json'); # or META.yml
35             plan skip_all => 'No META.json' if ! $meta;
36              
37             my @files = File::Find::Rule::Perl->perl_files->in('./lib', './bin');
38             ok_dependencies($meta, \@files, [qw/runtime configure build test/],
39             undef, # all features in the cpanfile
40             ignores => [ qw/ Your::Namespace Some::Other::Namespace / ]
41             );
42              
43             done_testing;
44              
45             =head1 DESCRIPTION
46              
47             Makes sure that all of the modules that are 'use'd are listed in the
48             Makefile.PL as dependencies.
49              
50             =head1 OPTIONS
51              
52             B You can pass options to the module via the 'use' line.
53             These options will be moved to the ok_dependencies() function.
54             The available options are:
55              
56             =over 4
57              
58             =item forward_compatible
59              
60             When specified and true, stops the module from outputting a plan, which
61             is the default mode of operation when the module becomes 1.0.
62              
63             =item exclude
64              
65             Specifies the list of namespaces for which it is ok not to have
66             specified dependencies.
67              
68             =item style
69              
70             B
71              
72             There used to be the option of specifying a style; the heavy style
73             depended on B::PerlReq. This module stopped working somewhere around
74             Perl 5.20. Specifying a style no longer has any effect.
75              
76             =back
77              
78             =cut
79              
80             our @EXPORT = qw/ok_dependencies/;
81              
82             our $exclude_re;
83              
84             sub import {
85 4     4   76 my $package = shift;
86 4         15 my %args = @_;
87 4         11 my $callerpack = caller;
88 4         42 my $tb = __PACKAGE__->builder;
89 4         98 $tb->exported_to($callerpack);
90              
91 4 50       71 unless ($args{forward_compatible}) {
92 4         20 $tb->no_plan;
93              
94 4 100       4362 if (defined $args{exclude}) {
95 3         6 foreach my $namespace (@{$args{exclude}}) {
  3         11  
96 14 50       84 croak "$namespace is not a valid namespace"
97             unless $namespace =~ m/^(?:(?:\w+::)|)+\w+$/;
98             }
99 3         8 $exclude_re = join '|', map { "^$_(\$|::)" } @{$args{exclude}};
  14         59  
  3         8  
100             }
101             else {
102 1         5 $exclude_re = qr/^$/;
103             }
104             }
105              
106 4         4423 $package->export_to_level(1, '', qw/ok_dependencies/);
107             }
108              
109              
110             sub _get_modules_used_in_file {
111 12     12   25 my $file = shift;
112 12         35 my ($fh, $code);
113 12         0 my %used;
114              
115 12         45 local $/;
116 12 50       645 open $fh, $file or return undef;
117 12         331 my $data = <$fh>;
118 12         123 close $fh;
119 12         121 my $p = Pod::Strip->new;
120 12         756 $p->output_string(\$code);
121 12         827 $p->parse_string_document($data);
122 12 50       117425 $code =~ m/^(__DATA__|__END__)$.*/m
123             and $code = $`; # strip data and end sections ($`==$PREMATCH)
124 12         543 $used{$2}++ while $code =~ /^\s*(use|with|extends)\s+['"]?([\w:.]+)['"]?/gm;
125 12         134 while ($code =~ m{^\s*use\s+(?:base|parent)
126             \s+(?:qw.|(?:(?:['"]|q.|qq.)))([\w\s:]+)}gmx) {
127 2         572 $used{$_}++ for split ' ', $1;
128             }
129              
130 12         325 return [keys %used];
131             }
132              
133             sub _get_modules_used {
134 3     3   42 my ($files) = @_;
135 3         11 my @modules;
136              
137 3         21 foreach my $file (sort @$files) {
138 12         37 my $ret = _get_modules_used_in_file($file);
139 12 50       57 if (! defined $ret) {
140 0         0 die "Could not determine modules used in '$file'";
141             }
142 12         53 push @modules, @$ret;
143             }
144 3         20 return @modules;
145             }
146              
147             sub _legacy_ok_dependencies {
148 4     4   10 my ($missing_dep);
149 4         61 my $tb = __PACKAGE__->builder;
150             {
151 4         65 local $@;
  4         9  
152              
153 4     3   364 eval "use CPAN::Meta;";
  3         89  
  3         9  
  3         79  
154 4     3   302 eval "use File::Find::Rule::Perl;";
  3         39  
  3         24  
  3         73  
155              
156 4         180 $missing_dep = $@;
157             }
158 4 50       18 die $missing_dep if $missing_dep;
159              
160 4         9 my $meta;
161 4         11 for my $file (qw/ META.json META.yml /) {
162 6 100       286 if (-r $file) {
163 3         31 $tb->ok(1, "$file is present and readable");
164 3         1636 $meta = CPAN::Meta->load_file($file);
165 3         95902 last;
166             }
167             }
168              
169 4 100       29 if (! $meta) {
170 1         7 $tb->level(2);
171 1         15 $tb->ok(0, "Missing META.{yml,json} file for dependency checking");
172 1         1429 $tb->diag("Use the non-legacy invocation to provide the info");
173 1         702 return;
174             }
175              
176             my @run = File::Find::Rule::Perl->perl_file->in(
177 3         74 grep { -e $_ } ('./bin', './lib', './t'));
  9         3888  
178              
179 3         8443 ok_dependencies($meta, \@run, ignores => [ 'ExtUtils::MakeMaker']);
180             }
181              
182              
183             =head1 EXPORTED FUNCTIONS
184              
185             =head2 ok_dependencies($meta, $files, $phases, $features, %options)
186              
187             $meta is a CPAN::Meta object
188             $files is an arrayref with files to be scanned
189              
190             =head3 %options keys
191              
192             =over
193              
194             =item phases
195              
196             This is an arrayref holding one or more names of phases
197             as defined by L, or undef for all
198              
199             =item features
200              
201             This is an arrayref holding zero or more names of features, or undef for all
202              
203             =item ignores
204              
205             This is a arrayref listing the names of modules (and their sub-namespaces)
206             for which no errors are to be reported.
207              
208             =back
209              
210             =head2 ok_dependencies()
211              
212             B Legacy invocation to be removed. In previous versions,
213             this function would scan the I bin/, lib/ and t/ subtrees, with the
214             exception of a few sub-directories known to be used by version control
215             systems.
216              
217             This behaviour has been changed: as of 0.20, Find::File::Rule::Perl
218             is being used to find Perl files (*.pl, *.pm, *.t and those starting with
219             a shebang line referring to perl).
220              
221             =cut
222              
223             sub ok_dependencies {
224              
225 7 100   7 1 411297 return _legacy_ok_dependencies
226             unless @_;
227              
228 3         14 my ($meta, $files, %options) = @_;
229 3         10 my $phases = $options{phases};
230 3         8 my $features = $options{features};
231             my $ignores_re = '^(?:' . join('|',
232             # create regex for sub-namespaces
233 3         17 map { "$_(?:::.*)?" }
234 3   50     8 @{$options{ignores} // []})
  3         16  
235             . ')$';
236 3         74 $ignores_re = qr/$ignores_re/;
237              
238 3   33     35 $features //= $meta->features;
239 3 50       1205 $features = [ $features ] unless ref $features;
240 3   50     24 $phases //= [ 'runtime', 'configure', 'build', 'test', 'develop' ];
241 3 50       16 $phases = [ $phases ] unless ref $phases;
242              
243 3         40 my $tb = __PACKAGE__->builder;
244 3         83 my %used = map { $_ => 1 } _get_modules_used($files);
  34         59  
245              
246 3         29 my @meta_features = map { $_->identifier } $meta->features;
  2         1189  
247 3         57 my $prereqs = $meta->effective_prereqs(\@meta_features);
248 3         14019 my $reqs = [];
249              
250             push @$reqs, $prereqs->requirements_for($_, 'requires')
251 3         22 for (@$phases);
252              
253 3         4717 my $min_perl_ver;
254             my $minimum_perl;
255 3         9 for (map { $_->requirements_for_module('perl') } @$reqs) {
  15         201  
256 2 50       21 next if ! defined $_;
257              
258 2         25 my $ver = version->parse($_)->numify;
259 2 50 33     15 $minimum_perl = (defined $min_perl_ver and $min_perl_ver < $ver)
260             ? $minimum_perl : $_;
261 2 50 33     10 $min_perl_ver = (defined $min_perl_ver and $min_perl_ver < $ver)
262             ? $min_perl_ver : $ver;
263             }
264 3   100     25 $minimum_perl //= "v5.0.0";
265 3   100     13 $min_perl_ver //= 5.0;
266              
267 3         9 for my $req (@$reqs) {
268 15         71 for my $mod (sort $req->required_modules) {
269 28 100 100     2374 next if ($mod eq 'perl'
      66        
      100        
270             or $mod =~ $ignores_re
271             or ($exclude_re and $mod =~ $exclude_re));
272              
273             # if the module is/was deprecated from CORE,
274             # it makes sense to require it, if the dependency exists
275 8 50 33     48 next if (Module::CoreList->deprecated_in($mod)
276             or Module::CoreList->removed_from($mod));
277              
278 8         31029 my $req_version = $req->requirements_for_module($mod);
279 8         541 my $first_in = Module::CoreList->first_release($mod, $req_version);
280 8 100       8990 my $verstr = ($req_version) ? '(' . $req_version . ')' : '';
281 8         111 my $corestr = version->parse($first_in)->normal;
282 8 100       157 $tb->ok($first_in > $min_perl_ver,
283             "Required module '$mod'$verstr "
284             . "in core (since $corestr) after minimum perl "
285             . $minimum_perl )
286             if defined $first_in;
287             }
288             }
289              
290 3         14 my %required;
291 3         9 for my $req (@$reqs) {
292             $required{$_} = $req->requirements_for_module($_)
293 15         918 for $req->required_modules;
294             }
295 3         218 delete $required{perl};
296              
297 3         20 foreach my $mod (sort keys %required) {
298 18 100 66     2578 $tb->ok(exists $used{$mod}, "Declared dependency $mod used")
      100        
299             unless ($mod =~ $ignores_re
300             or ($exclude_re and $mod =~ $exclude_re));
301             }
302              
303 3         798 foreach my $mod (sort keys %used) {
304 22 100 33     7772 next if ($mod =~ $ignores_re
      66        
      66        
305             or $mod =~ m/^v?\d+[.]\d+/ # minimum perl version requirement
306             or ($exclude_re and $mod =~ $exclude_re));
307              
308 20         153 my $first_in = Module::CoreList->first_release($mod, $required{$mod});
309 20         47304 my $v;
310 20 50       116 if ($v = Module::CoreList->removed_from($mod)) {
    50          
    100          
311 0         0 $v = version->parse($v)->normal;
312 0         0 $tb->ok(exists $required{$mod},
313             "Removed-from-CORE (in $v) module '$mod' "
314             . 'explicitly required');
315             }
316             elsif ($v = Module::CoreList->deprecated_in($mod)) {
317 0         0 $v = version->parse($v)->normal;
318 0         0 $tb->ok(exists $required{$mod},
319             "Deprecated-from-CORE (in $v) module '$mod' explicitly "
320             . 'required to anticipate removal');
321             }
322             elsif (defined $first_in) {
323 16         107318 $v = version->parse($first_in)->normal;
324 16   66     243 $tb->ok($first_in <= $min_perl_ver or exists $required{$mod},
325             "Used CORE module '$mod' in core before "
326             . "Perl $minimum_perl (since $v) "
327             . "or explicitly required");
328             }
329             else {
330 4         4626 $tb->ok(exists $required{$mod},
331             "Used non-CORE module '$mod' in requirements listing");
332             }
333             }
334             }
335              
336             =head1 AUTHORS
337              
338             =over 4
339              
340             =item * Jesse Vincent C<< >>
341              
342             =item * Alex Vandiver C<< >>
343              
344             =item * Zev Benjamin C<< >>
345              
346             =item * Erik Huelsmann C<< >>
347              
348             =back
349              
350             =head1 BUGS
351              
352             =over 4
353              
354             =item * Test::Dependencies does not track module version requirements.
355              
356             =back
357              
358             Please report your bugs on GitHub:
359              
360             L
361              
362             =head1 SUPPORT
363              
364             You can find documentation for this module with the perldoc command.
365              
366             perldoc Test::Dependencies
367              
368             You can also look for information at:
369              
370             =over 4
371              
372             =item * CPAN Ratings
373              
374             L
375              
376             =item * Search CPAN
377              
378             L
379              
380             =back
381              
382             =head1 LICENCE AND COPYRIGHT
383              
384             Copyright (c) 2016-2023, Erik Huelsmann. All rights reserved.
385             Copyright (c) 2007, Best Practical Solutions, LLC. All rights reserved.
386              
387             This module is free software; you can redistribute it and/or modify it
388             under the same terms as Perl itself. See perlartistic.
389              
390             DISCLAIMER OF WARRANTY
391              
392             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
393             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
394             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
395             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
396             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
397             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
398             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
399             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
400             NECESSARY SERVICING, REPAIR, OR CORRECTION.
401              
402             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
403             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
404             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE
405             TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
406             CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
407             SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
408             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
409             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
410             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
411             DAMAGES.
412              
413             =cut
414              
415             1;