File Coverage

blib/lib/Test/Kwalitee/Extra.pm
Criterion Covered Total %
statement 222 260 85.3
branch 98 152 64.4
condition 19 54 35.1
subroutine 26 27 96.3
pod n/a
total 365 493 74.0


line stmt bran cond sub pod time code
1             package Test::Kwalitee::Extra;
2              
3 8     8   374668 use strict;
  8         20  
  8         204  
4 8     8   41 use warnings;
  8         17  
  8         330  
5              
6             # ABSTRACT: Run Kwalitee tests including optional indicators, especially, prereq_matches_use
7             our $VERSION = 'v0.4.0'; # VERSION
8              
9 8     8   2282 use version 0.77;
  8         9612  
  8         55  
10 8     8   512 use Cwd;
  8         18  
  8         514  
11 8     8   50 use Carp;
  8         17  
  8         394  
12 8     8   40 use File::Find;
  8         20  
  8         331  
13 8     8   41 use File::Spec;
  8         18  
  8         148  
14 8     8   39 use Test::Builder;
  8         17  
  8         134  
15 8     8   3311 use MetaCPAN::Client;
  8         2418095  
  8         280  
16 8     8   2464 use Module::CPANTS::Analyse 0.87;
  8         807749  
  8         51  
17 8     8   55100 use Module::CPANTS::Kwalitee::Prereq;
  8         57515  
  8         215  
18 8     8   16927 use Module::CoreList;
  8         323875  
  8         89  
19 8     8   8241 use Module::Extract::Namespaces;
  8         712033  
  8         16351  
20              
21             sub _exclude_proper_libs
22             {
23 8     8   167 my $target_ver = version->parse($Module::CPANTS::Analyse::VERSION);
24 8   33     53048 return $target_ver == version->parse('0.88') || $target_ver > version->parse('0.89');
25             }
26              
27             sub _init
28             {
29             return {
30 8 50   8   203599 builder => Test::Builder->new,
31             exclude => {
32             # can not apply already unpacked dist
33             extractable => 1,
34             extracts_nicely => 1,
35             has_version => 1,
36             has_proper_version => 1,
37             _exclude_proper_libs() ? (proper_libs => 1) : (),
38              
39             # already dirty in test phase
40             no_generated_files => 1,
41             manifest_matches_dist => 1,
42              
43             },
44             include => {},
45             core => 1,
46             optional => 1,
47             experimental => 0,
48             analyser => Module::CPANTS::Analyse->new({
49             distdir => cwd(),
50             dist => cwd(),
51             }),
52             retry => 5,
53             };
54             }
55              
56             sub _pmu_error_desc
57             {
58 6     6   15 my ($error, $remedy, $berror, $bremedy);
59              
60 6         51 my $ref = Module::CPANTS::Kwalitee::Prereq->kwalitee_indicators;
61 6         37 foreach my $val (@$ref) {
62 0 0       0 ($error, $remedy) = @{$val}{qw(error remedy)} if $val->{name} eq 'prereq_matches_use';
  0         0  
63 0 0       0 ($berror, $bremedy) = @{$val}{qw(error remedy)} if $val->{name} eq 'build_prereq_matches_use';
  0         0  
64             }
65 6   50     79 $error ||= q{This distribution uses a module or a dist that's not listed as a prerequisite.};
66 6   50     53 $remedy ||= q{List all used modules in META.yml requires};
67 6   50     40 $berror ||= q{This distribution uses a module or a dist in its test suite that's not listed as a build prerequisite.};
68 6   50     39 $bremedy ||= q{List all modules used in the test suite in META.yml build_requires};
69              
70 6         25 return ($error, $remedy, $berror, $bremedy);
71             }
72              
73             sub _check_ind
74             {
75 285     285   514 my ($env, $ind) = @_;
76 285 100       854 return 1 if $env->{include}{$ind->{name}};
77 275 100       797 return 0 if $env->{exclude}{$ind->{name}};
78 257 100       653 if($ind->{is_experimental}) { # experimental
    100          
79 33         3558 return $env->{experimental};
80             } elsif($ind->{is_extra}) { # optional
81 72         718 return $env->{optional};
82             } else { # core
83 152         426 return $env->{core};
84             }
85             }
86              
87             sub _is_core
88             {
89 176     176   777 my ($module, $minperlver) = @_;
90 176 50       1375 return 0 if defined Module::CoreList->removed_from($module);
91 176         4839103 my $fr = Module::CoreList->first_release($module);
92 176 100       4452458 return 0 if ! defined $fr;
93 116 100       4191 return 1 if version->parse($minperlver) >= version->parse($fr);
94 45         537 return 0;
95             }
96              
97             sub _do_test_one
98             {
99 46     46   9392 local $Test::Builder::Level = $Test::Builder::Level + 1;
100              
101 46         159 my ($test, $ok, $name, $error, $remedy, $more) = @_;
102              
103 46         248 $test->ok($ok, $name);
104 46 100       20325 if(!$ok) {
105 2         15 $test->diag(' Detail: ', $error);
106 2 50       254 $test->diag(' Detail: ', ref($more) ? join(', ', @$more) : $more) if defined $more;
    50          
107 2         250 $test->diag(' Remedy: ', $remedy);
108             }
109             }
110              
111             sub _is_missing_check_for_old
112             {
113 0     0   0 my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
114              
115 0         0 while(my ($key, $val) = each %$uses) {
116 0 0       0 next if version::is_lax($key); # perl version
117             # Skip packages provided by the distribution but not indexed by CPAN.
118 0 0       0 next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
  0         0  
119 0 0       0 next if _is_core($key, $minperlver);
120 0 0       0 next if $key =~ m'[$@%*&]'; # ignore entry including sigil
121 0         0 my $result = eval { $mcpan->module($key) };
  0         0  
122 0 0 0     0 if($@ || ! $result->distribution) {
123 0         0 $qerror->{$key} = 1;
124 0         0 next;
125             }
126 0         0 my $dist = $result->distribution;
127 0 0 0     0 push @$missing, $key.' in '.$dist if $val->{in_code} && $val->{in_code} != ($val->{evals_in_code} || 0) && ! exists $prereq->{$dist};
      0        
      0        
128 0 0 0     0 push @$bmissing, $key.' in '.$dist if $val->{in_tests} && $val->{in_tests} != ($val->{evals_in_tests} || 0) && ! exists $build_prereq->{$dist};
      0        
      0        
129             }
130             }
131              
132             my %uses_keys = (
133             used_in_code => '',
134             required_in_code => '',
135             used_in_tests => 'build',
136             required_in_tests => 'build'
137             );
138             sub _is_missing_check_for_new
139             {
140 4     4   103 my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
141              
142 4         44 foreach my $uses_keys (keys %uses_keys) {
143 16         53 while(my ($key, $val) = each %{$uses->{$uses_keys}}) {
  136         1350  
144 120 100       942 next if version::is_lax($key); # perl version
145             # Skip packages provided by the distribution but not indexed by CPAN.
146 116 50       3722 next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
  0         0  
147 116 100       665 next if _is_core($key, $minperlver);
148 64 50       578 next if $key =~ m'[$@%*&]'; # ignore entry including sigil
149 64         247 my $result = eval { $mcpan->module($key) };
  64         710  
150 64 50 33     39688066 if($@ || ! $result->distribution) {
151 0         0 $qerror->{$key} = 1;
152 0         0 next;
153             }
154 64         2953 my $dist = $result->distribution;
155 64 100       874 if($uses_keys{$uses_keys} ne 'build') {
156 29 50       401 push @$missing, $key.' in '.$dist if ! exists $prereq->{$dist};
157             } else { # build
158 35 100       539 push @$bmissing, $key.' in '.$dist if ! exists $build_prereq->{$dist};
159             }
160             }
161             }
162             }
163              
164             sub _do_test_pmu
165             {
166 6     6   22 local $Test::Builder::Level = $Test::Builder::Level + 1;
167              
168 6         22 my ($env) = @_;
169 6         27 my ($error, $remedy, $berror, $bremedy) = _pmu_error_desc();
170 6         16 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  6         24  
171 6 100 100     29 return if ! _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 }) &&
172             ! _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
173              
174 4         14 my $minperlver;
175 4 100       19 if(exists $env->{minperlver}) {
176 1         3 $minperlver = $env->{minperlver};
177             } else {
178 3         9 $minperlver = $];
179 3         7 for my $val (@{$analyser->d->{prereq}}) {
  3         46  
180 9 100       61 if($val->{requires} eq 'perl') {
181 3         8 $minperlver = $val->{version};
182 3         8 last;
183             }
184             }
185             }
186 4         79 my $mcpan = MetaCPAN::Client->new;
187              
188 4         14032 my %qerror;
189 4         11 my (%build_prereq, %prereq);
190             # NOTE: prereq part is kept in new stash layout of Module::CPANTS::Analyse since 0.93_01
191 4         9 foreach my $val (@{$analyser->d->{prereq}}) {
  4         21  
192 60 100       479 next if _is_core($val->{requires}, $minperlver);
193 41         150 my $retry = 0;
194 41         124 my $result;
195 41         298 while($retry < $env->{retry}) {
196 41         161 $result = eval { $mcpan->module($val->{requires}) };
  41         518  
197 41 50 33     25895307 if($@ || ! $result->distribution) {
198 0         0 ++$retry;
199             } else {
200 41         1128 last;
201             }
202             }
203 41 50       531 if($retry == $env->{retry}) {
204 0         0 $qerror{$val->{requires}} = 1;
205 0         0 next;
206             }
207 41 100 66     914 $prereq{$result->distribution} = 1 if $val->{is_prereq} || $val->{is_optional_prereq};
208 41 100 100     1004 $build_prereq{$result->{distribution}} = 1 if $val->{is_prereq} || $val->{is_build_prereq} || $val->{is_optional_prereq};
      66        
209             }
210              
211             # NOTE: uses part is changed in new stash layout of Module::CPANTS::Analyse since 0.93_01
212 4         18 my $is_old = grep { exists $analyser->d->{uses}{$_}{module} } keys %{$analyser->d->{uses}};
  20         335  
  4         53  
213              
214             # Look at META.yml to determine if the author specified modules provided
215             # by the distribution that should not be indexed by CPAN.
216 4         62 my $packages_not_indexed = _get_packages_not_indexed(
217             d => $analyser->d,
218             distdir => $analyser->distdir,
219             is_old => $is_old,
220             );
221              
222 4         16 my (@missing, @bmissing);
223 4 50       21 if($is_old) {
224 0         0 _is_missing_check_for_old($analyser->d->{uses}, \%prereq, \%build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, \@missing, \@bmissing, \%qerror);
225             } else {
226 4         47 _is_missing_check_for_new($analyser->d->{uses}, \%prereq, \%build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, \@missing, \@bmissing, \%qerror);
227             }
228              
229 4 50       36 if(%qerror) {
230 0         0 $remedy = $bremedy = 'Fix query error(s) to MetaCPAN.';
231             }
232 4 50 33     42 _do_test_one($test, ! %qerror && @missing == 0, 'prereq_matches_use by '.__PACKAGE__, $error, $remedy,
    100          
233             ! %qerror ? 'Missing: '.join(', ', sort @missing) : 'Query error: '.join(' ', sort keys %qerror))
234             if _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 });
235 4 50 33     34 _do_test_one($test, ! %qerror && @bmissing == 0, 'build_prereq_matches_use by '.__PACKAGE__, $berror, $bremedy,
    100          
236             ! %qerror ? 'Missing: '.join(', ', sort @bmissing) : 'Query error: '.join(' ', sort keys %qerror))
237             if _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
238             }
239              
240             # Look at META.yml to determine if the author specified modules provided
241             # by the distribution that should not be indexed by CPAN.
242             sub _get_packages_not_indexed
243             {
244 12     12   6379 my (%args) = @_;
245 12         42 my $d = delete $args{'d'};
246 12         33 my $distdir = delete $args{'distdir'};
247 12         31 my $is_old = delete $args{'is_old'};
248              
249             # Check if no_index exists in META.yml
250 12         37 my $meta_yml = $d->{'meta_yml'};
251 12 50       50 return [] if !defined $meta_yml;
252 12         32 my $no_index = $meta_yml->{'no_index'};
253 12 50       37 return [] if !defined $no_index;
254              
255             # Get the uses, to determine which ones are no-index internals.
256 12         26 my $uses = $d->{'uses'};
257 12 50       42 return [] if !defined $uses;
258             # NOTE: uses part is changed in new stash layout of Module::CPANTS::Analyse since 0.93_01
259 12 100       37 if(!$is_old) {
260 8         18 my @uses;
261 8         28 push @uses, keys %{$uses->{$_}} for qw[used_in_code required_in_code used_in_tests required_in_tests];
  32         221  
262 8         24 $uses = { map { ($_ => undef) } @uses };
  132         328  
263             }
264              
265 12         41 my $packages_not_indexed = {};
266              
267             # Find all the files corresponding to the 'file' and 'directory'
268             # sections of 'no_index'.
269 12         32 my @files = ();
270              
271 12 100       57 if (defined $no_index->{'file'}) {
272 2         12 push @files, map { File::Spec->catdir($distdir, $_) } @{$no_index->{'file'}};
  2         27  
  2         5  
273             }
274              
275 12 100       47 if (defined $no_index->{'directory'}) {
276             my $filter_pm_files = sub {
277 20 100   20   957 return if $File::Find::name !~ /\.pm$/;
278 10         277 push(@files, $File::Find::name);
279 6         69 };
280              
281 6         18 foreach my $directory (@{$no_index->{'directory'}}) {
  6         30  
282 6         107 my $no_meta_directory = File::Spec->catdir($distdir, $directory);
283 6 50       268 if(-d $no_meta_directory) {
284 6         619 File::Find::find(
285             $filter_pm_files,
286             $no_meta_directory,
287             );
288             }
289             }
290             }
291              
292             # Extract the namespaces from those files.
293 12         79 foreach my $file (@files) {
294 12         133 my @namespaces = Module::Extract::Namespaces->from_file($file);
295 12         43777 foreach my $namespace (@namespaces) {
296 12 100       72 next if !exists $uses->{$namespace};
297 4         15 $packages_not_indexed->{$namespace} = undef;
298             }
299             }
300              
301             # 'package' section of no_index.
302 12 100       53 if (defined $no_index->{'package'}) {
303 2         3 foreach my $package (@{$no_index->{'package'}}) {
  2         6  
304 2 50       8 next if !exists $uses->{$package};
305 2         6 $packages_not_indexed->{$package} = undef;
306             }
307             }
308              
309             # 'namespace' section of no_index.
310 12 100       40 if (defined $no_index->{'namespace'}) {
311 2         7 foreach my $use (keys %$uses) {
312 6         11 foreach my $namespace (@{$no_index->{'namespace'}}) {
  6         13  
313 6 100       50 next if $use !~ /^\Q$namespace\E(?:::|$)/;
314 4         11 $packages_not_indexed->{$use} = undef;
315             }
316             }
317             }
318              
319 12         107 return [sort keys %$packages_not_indexed];
320             }
321              
322             sub _count_tests
323             {
324 3     3   12 my ($env) = @_;
325 3         7 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  3         23  
326 3         7 my $count = 0;
327 3         7 foreach my $mod (@{$analyser->mck->generators}) {
  3         13  
328 48         180 foreach my $ind (@{$mod->kwalitee_indicators}) {
  48         314  
329 87 50       729 next if $ind->{needs_db};
330 87 100       161 next if ! _check_ind($env, $ind);
331 24         84 ++$count;
332             }
333             }
334             # overrides needs_db
335 3 50       29 ++$count if _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 });
336 3 100       15 ++$count if _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
337 3         29 return $count;
338             }
339              
340             sub _do_test
341             {
342 6     6   23 local $Test::Builder::Level = $Test::Builder::Level + 1;
343 6         33 my ($env) = @_;
344 6         27 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  6         21  
345              
346 6 100       33 if(! $env->{no_plan}) {
347 2         8 $test->plan(tests => _count_tests(@_));
348             }
349 6         601 foreach my $mod (@{$analyser->mck->generators}) {
  6         29  
350 96         1074 $mod->analyse($analyser);
351 96         2579106 foreach my $ind (@{$mod->kwalitee_indicators}) {
  96         541  
352 174 50       1776 next if $ind->{needs_db};
353 174 100       418 next if ! _check_ind($env, $ind);
354             _do_test_one(
355             $test,
356             $ind->{code}($analyser->d, $ind),
357             $ind->{name}.' by '.$mod,
358             $ind->{error},
359             $ind->{remedy},
360             $analyser->d->{error}{$ind->{name}}
361 42         146 );
362             }
363             }
364 6         66 _do_test_pmu($env);
365             }
366              
367             my %class = ( core => 1, optional => 1, experimental => 1 );
368              
369             sub import
370             {
371 7     7   9325 my ($pkg, @arg) = @_;
372 7         26 my $env = _init();
373 7         1321871 my $ind_seen = 0;
374 7         57 while(my $arg = shift @arg) {
375 24 100       173 if($arg eq ':no_plan') {
    100          
    50          
    100          
    50          
    100          
376 5         51 $env->{no_plan} = 1;
377             } elsif($arg eq ':minperlver') {
378 1         7 $env->{minperlver} = shift @arg;
379             } elsif($arg eq ':retry') {
380 0         0 $env->{retry} = shift @arg;
381             } elsif($arg =~ /^!:/) {
382 11 50       59 warn "Tag $arg appears after indicator" if $ind_seen;
383 11         44 $arg =~ s/^!://;
384 11 50       67 if($arg eq 'all') {
    50          
    50          
385 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 1;
386             } elsif($arg eq 'none') {
387 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 0;
388             } elsif($class{$arg}) {
389 11         49 $env->{$arg} = 0;
390             } else {
391 0         0 warn "Unknown tag :$arg is used";
392             }
393             } elsif($arg =~ /^:/) {
394 0 0       0 warn "Tag $arg appears after indicator" if $ind_seen;
395 0         0 $arg =~ s/^://;
396 0 0       0 if($arg eq 'all') {
    0          
    0          
397 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 0;
398             } elsif($arg eq 'none') {
399 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 1;
400             } elsif($class{$arg}) {
401 0         0 $env->{$arg} = 1;
402             } else {
403 0         0 warn "Unknown tag :$arg is used";
404             }
405             } elsif($arg =~ /^!/) {
406 1         25 $ind_seen = 1;
407 1         6 $arg =~ s/^!//;
408 1         5 $env->{exclude}{$arg} = 1;
409 1         6 delete $env->{include}{$arg};
410             } else {
411 6         14 $ind_seen = 1;
412 6         21 $env->{include}{$arg} = 1;
413 6         27 delete $env->{exclude}{$arg};
414             }
415             }
416 7         38 _do_test($env);
417             }
418              
419             1;
420              
421             __END__