File Coverage

blib/lib/Test/Kwalitee/Extra.pm
Criterion Covered Total %
statement 217 258 84.1
branch 93 150 62.0
condition 29 54 53.7
subroutine 26 27 96.3
pod n/a
total 365 489 74.6


line stmt bran cond sub pod time code
1             package Test::Kwalitee::Extra;
2              
3 8     8   545485 use strict;
  8         21  
  8         260  
4 8     8   37 use warnings;
  8         16  
  8         331  
5              
6             # ABSTRACT: Run Kwalitee tests including optional indicators, especially, prereq_matches_use
7             our $VERSION = 'v0.3.0'; # VERSION
8              
9 8     8   4572 use version 0.77;
  8         15613  
  8         57  
10 8     8   522 use Cwd;
  8         16  
  8         613  
11 8     8   43 use Carp;
  8         17  
  8         515  
12 8     8   41 use File::Find;
  8         11  
  8         453  
13 8     8   40 use File::Spec;
  8         15  
  8         180  
14 8     8   36 use Test::Builder;
  8         14  
  8         165  
15 8     8   8244 use MetaCPAN::API::Tiny;
  8         561797  
  8         282  
16 8     8   6041 use Module::CPANTS::Analyse 0.87;
  8         1459696  
  8         97  
17 8     8   6394 use Module::CPANTS::Kwalitee::Prereq;
  8         137812  
  8         283  
18 8     8   26986 use Module::CoreList;
  8         394013  
  8         115  
19 8     8   20232 use Module::Extract::Namespaces;
  8         1325572  
  8         25521  
20              
21             sub _exclude_proper_libs
22             {
23 8     8   181 my $target_ver = version->parse($Module::CPANTS::Analyse::VERSION);
24 8   33     145706 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   274267 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   12 my ($error, $remedy, $berror, $bremedy);
59              
60 6         66 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     124 $error ||= q{This distribution uses a module or a dist that's not listed as a prerequisite.};
66 6   50     51 $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 it's test suite that's not listed as a build prerequisite.};
68 6   50     64 $bremedy ||= q{List all modules used in the test suite in META.yml build_requires};
69              
70 6         27 return ($error, $remedy, $berror, $bremedy);
71             }
72              
73             sub _check_ind
74             {
75 240     240   314 my ($env, $ind) = @_;
76 240 100       735 return 1 if $env->{include}{$ind->{name}};
77 230 100       812 return 0 if $env->{exclude}{$ind->{name}};
78 212 100       530 if($ind->{is_experimental}) { # experimental
    100          
79 24         15648 return $env->{experimental};
80             } elsif($ind->{is_extra}) { # optional
81 63         886 return $env->{optional};
82             } else { # core
83 125         510 return $env->{core};
84             }
85             }
86              
87             sub _is_core
88             {
89 156     156   466 my ($module, $minperlver) = @_;
90 156 50       1255 return 0 if defined Module::CoreList->removed_from($module);
91 156         2022873 my $fr = Module::CoreList->first_release($module);
92 156 100       1948795 return 0 if ! defined $fr;
93 104 100       3686 return 1 if version->parse($minperlver) >= version->parse($fr);
94 39         298 return 0;
95             }
96              
97             sub _do_test_one
98             {
99 39     39   11697 local $Test::Builder::Level = $Test::Builder::Level + 1;
100              
101 39         136 my ($test, $ok, $name, $error, $remedy, $more) = @_;
102              
103 39         238 $test->ok($ok, $name);
104 39 100       22471 if(!$ok) {
105 2         9 $test->diag(' Detail: ', $error);
106 2 50       182 $test->diag(' Detail: ', ref($more) ? join(', ', @$more) : $more) if defined $more;
    50          
107 2         161 $test->diag(' Remedy: ', $remedy);
108             }
109             }
110              
111             sub _is_missing_check_for_old
112             {
113 4     4   92 my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
114              
115 4         28 while(my ($key, $val) = each %$uses) {
116 108 50       750 next if version::is_lax($key); # perl version
117             # Skip packages provided by the distribution but not indexed by CPAN.
118 108 50       3664 next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
  0         0  
119 108 100       442 next if _is_core($key, $minperlver);
120 59 50       466 next if $key =~ m'[$@%*&]'; # ignore entry including sigil
121 59         142 my $result = eval { $mcpan->module($key) };
  59         777  
122 59 50 33     5527237 if($@ || ! exists $result->{distribution}) {
123 0         0 $qerror->{$key} = 1;
124 0         0 next;
125             }
126 59         225 my $dist = $result->{distribution};
127 59 50 50     881 push @$missing, $key.' in '.$dist if $val->{in_code} && $val->{in_code} != ($val->{evals_in_code} || 0) && ! exists $prereq->{$dist};
      66        
      66        
128 59 100 100     2476 push @$bmissing, $key.' in '.$dist if $val->{in_tests} && $val->{in_tests} != ($val->{evals_in_tests} || 0) && ! exists $build_prereq->{$dist};
      100        
      100        
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 0     0   0 my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
141              
142 0         0 foreach my $uses_keys (keys %uses_keys) {
143 0         0 while(my ($key, $val) = each %{$uses->{$uses_keys}}) {
  0         0  
144 0 0       0 next if version::is_lax($key); # perl version
145             # Skip packages provided by the distribution but not indexed by CPAN.
146 0 0       0 next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
  0         0  
147 0 0       0 next if _is_core($key, $minperlver);
148 0 0       0 next if $key =~ m'[$@%*&]'; # ignore entry including sigil
149 0         0 my $result = eval { $mcpan->module($key) };
  0         0  
150 0 0 0     0 if($@ || ! exists $result->{distribution}) {
151 0         0 $qerror->{$key} = 1;
152 0         0 next;
153             }
154 0         0 my $dist = $result->{distribution};
155 0 0       0 if($uses_keys{$uses_keys} ne 'build') {
156 0 0       0 push @$missing, $key.' in '.$dist if ! exists $prereq->{$dist};
157             } else { # build
158 0 0       0 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         16 my ($env) = @_;
169 6         30 my ($error, $remedy, $berror, $bremedy) = _pmu_error_desc();
170 6         18 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  6         26  
171 6 100 66     46 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         38 my $minperlver;
175 4 100       19 if(exists $env->{minperlver}) {
176 1         3 $minperlver = $env->{minperlver};
177             } else {
178 3         10 $minperlver = $];
179 3         8 for my $val (@{$analyser->d->{prereq}}) {
  3         18  
180 27 100       102 if($val->{requires} eq 'perl') {
181 3         9 $minperlver = $val->{version};
182 3         7 last;
183             }
184             }
185             }
186 4         89 my $mcpan = MetaCPAN::API::Tiny->new;
187              
188 4         608 my %qerror;
189 4         9 my (%build_prereq, %prereq);
190             # NOTE: prereq part is kept in new stash layout of Module::CPANTS::Analyse since 0.93_01
191 4         7 foreach my $val (@{$analyser->d->{prereq}}) {
  4         18  
192 48 100       313 next if _is_core($val->{requires}, $minperlver);
193 32         91 my $retry = 0;
194 32         54 my $result;
195 32         182 while($retry < $env->{retry}) {
196 32         123 $result = eval { $mcpan->module($val->{requires}) };
  32         298  
197 32 50 33     3133970 if($@ || ! exists $result->{distribution}) {
198 0         0 ++$retry;
199             } else {
200 32         89 last;
201             }
202             }
203 32 50       195 if($retry == $env->{retry}) {
204 0         0 $qerror{$val->{requires}} = 1;
205 0         0 next;
206             }
207 32 100 66     330 $prereq{$result->{distribution}} = 1 if $val->{is_prereq} || $val->{is_optional_prereq};
208 32 50 66     1021 $build_prereq{$result->{distribution}} = 1 if $val->{is_prereq} || $val->{is_build_prereq} || $val->{is_optional_prereq};
      33        
209             }
210              
211             # NOTE: uses part is changed in new stash layout of Module::CPANTS::Analyse since 0.93_01
212 4         11 my $is_old = grep { exists $analyser->d->{uses}{$_}{module} } keys %{$analyser->d->{uses}};
  108         1107  
  4         42  
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         57 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         10 my (@missing, @bmissing);
223 4 50       23 if($is_old) {
224 4         28 _is_missing_check_for_old($analyser->d->{uses}, \%prereq, \%build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, \@missing, \@bmissing, \%qerror);
225             } else {
226 0         0 _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       31 if(%qerror) {
230 0         0 $remedy = $bremedy = 'Fix query error(s) to MetaCPAN.';
231             }
232 4 50 33     41 _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   5471 my (%args) = @_;
245 12         32 my $d = delete $args{'d'};
246 12         25 my $distdir = delete $args{'distdir'};
247 12         27 my $is_old = delete $args{'is_old'};
248              
249             # Check if no_index exists in META.yml
250 12         29 my $meta_yml = $d->{'meta_yml'};
251 12 50       35 return [] if !defined $meta_yml;
252 12         29 my $no_index = $meta_yml->{'no_index'};
253 12 50       30 return [] if !defined $no_index;
254              
255             # Get the uses, to determine which ones are no-index internals.
256 12         21 my $uses = $d->{'uses'};
257 12 50       34 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       27 if(!$is_old) {
260 4         5 my @uses;
261 4         9 push @uses, keys %{$uses->{$_}} for qw[used_in_code required_in_code used_in_tests required_in_tests];
  16         42  
262 4         9 $uses = { map { ($_ => undef) } @uses };
  12         40  
263             }
264              
265 12         24 my $packages_not_indexed = {};
266              
267             # Find all the files corresponding to the 'file' and 'directory'
268             # sections of 'no_index'.
269 12         21 my @files = ();
270              
271 12 100       37 if (defined $no_index->{'file'}) {
272 2         4 push @files, map { File::Spec->catdir($distdir, $_) } @{$no_index->{'file'}};
  2         30  
  2         4  
273             }
274              
275 12 100       40 if (defined $no_index->{'directory'}) {
276             my $filter_pm_files = sub {
277 20 100   20   907 return if $File::Find::name !~ /\.pm$/;
278 10         289 push(@files, $File::Find::name);
279 6         48 };
280              
281 6         14 foreach my $directory (@{$no_index->{'directory'}}) {
  6         28  
282 6         827 File::Find::find(
283             $filter_pm_files,
284             File::Spec->catdir($distdir, $directory),
285             );
286             }
287             }
288              
289             # Extract the namespaces from those files.
290 12         37 foreach my $file (@files) {
291 12         125 my @namespaces = Module::Extract::Namespaces->from_file($file);
292 12         41293 foreach my $namespace (@namespaces) {
293 12 100       69 next if !exists $uses->{$namespace};
294 4         14 $packages_not_indexed->{$namespace} = undef;
295             }
296             }
297              
298             # 'package' section of no_index.
299 12 100       44 if (defined $no_index->{'package'}) {
300 2         4 foreach my $package (@{$no_index->{'package'}}) {
  2         3  
301 2 50       6 next if !exists $uses->{$package};
302 2         6 $packages_not_indexed->{$package} = undef;
303             }
304             }
305              
306             # 'namespace' section of no_index.
307 12 100       37 if (defined $no_index->{'namespace'}) {
308 2         7 foreach my $use (keys %$uses) {
309 6         7 foreach my $namespace (@{$no_index->{'namespace'}}) {
  6         13  
310 6 100       56 next if $use !~ /^\Q$namespace\E(?:::|$)/;
311 4         11 $packages_not_indexed->{$use} = undef;
312             }
313             }
314             }
315              
316 12         82 return [sort keys %$packages_not_indexed];
317             }
318              
319             sub _count_tests
320             {
321 3     3   10 my ($env) = @_;
322 3         39 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  3         9  
323 3         7 my $count = 0;
324 3         5 foreach my $mod (@{$analyser->mck->generators}) {
  3         12  
325 48         156 foreach my $ind (@{$mod->kwalitee_indicators}) {
  48         375  
326 72 50       783 next if $ind->{needs_db};
327 72 100       111 next if ! _check_ind($env, $ind);
328 20         126 ++$count;
329             }
330             }
331             # overrides needs_db
332 3 50       31 ++$count if _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 });
333 3 100       15 ++$count if _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
334 3         78 return $count;
335             }
336              
337             sub _do_test
338             {
339 6     6   45 local $Test::Builder::Level = $Test::Builder::Level + 1;
340 6         100 my ($env) = @_;
341 6         15 my ($test, $analyser) = @{$env}{qw(builder analyser)};
  6         23  
342              
343 6 100       32 if(! $env->{no_plan}) {
344 2         9 $test->plan(tests => _count_tests(@_));
345             }
346 6         822 foreach my $mod (@{$analyser->mck->generators}) {
  6         29  
347 96         1293 $mod->analyse($analyser);
348 96         2328070 foreach my $ind (@{$mod->kwalitee_indicators}) {
  96         659  
349 144 50       1950 next if $ind->{needs_db};
350 144 100       341 next if ! _check_ind($env, $ind);
351 35         129 _do_test_one(
352             $test,
353             $ind->{code}($analyser->d, $ind),
354             $ind->{name}.' by '.$mod,
355             $ind->{error},
356             $ind->{remedy},
357             $analyser->d->{error}{$ind->{name}}
358             );
359             }
360             }
361 6         118 _do_test_pmu($env);
362             }
363              
364             my %class = ( core => 1, optional => 1, experimental => 1 );
365              
366             sub import
367             {
368 7     7   9871 my ($pkg, @arg) = @_;
369 7         34 my $env = _init();
370 7         2635615 my $ind_seen = 0;
371 7         104 while(my $arg = shift @arg) {
372 24 100       202 if($arg eq ':no_plan') {
    100          
    50          
    100          
    50          
    100          
373 5         61 $env->{no_plan} = 1;
374             } elsif($arg eq ':minperlver') {
375 1         11 $env->{minperlver} = shift @arg;
376             } elsif($arg eq ':retry') {
377 0         0 $env->{retry} = shift @arg;
378             } elsif($arg =~ /^!:/) {
379 11 50       91 warn "Tag $arg appears after indicator" if $ind_seen;
380 11         52 $arg =~ s/^!://;
381 11 50       79 if($arg eq 'all') {
    50          
    50          
382 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 1;
383             } elsif($arg eq 'none') {
384 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 0;
385             } elsif($class{$arg}) {
386 11         67 $env->{$arg} = 0;
387             } else {
388 0         0 warn "Unknown tag :$arg is used";
389             }
390             } elsif($arg =~ /^:/) {
391 0 0       0 warn "Tag $arg appears after indicator" if $ind_seen;
392 0         0 $arg =~ s/^://;
393 0 0       0 if($arg eq 'all') {
    0          
    0          
394 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 0;
395             } elsif($arg eq 'none') {
396 0         0 $env->{core} = $env->{optional} = $env->{experimental} = 1;
397             } elsif($class{$arg}) {
398 0         0 $env->{$arg} = 1;
399             } else {
400 0         0 warn "Unknown tag :$arg is used";
401             }
402             } elsif($arg =~ /^!/) {
403 1         2 $ind_seen = 1;
404 1         5 $arg =~ s/^!//;
405 1         5 $env->{exclude}{$arg} = 1;
406 1         5 delete $env->{include}{$arg};
407             } else {
408 6         13 $ind_seen = 1;
409 6         52 $env->{include}{$arg} = 1;
410 6         85 delete $env->{exclude}{$arg};
411             }
412             }
413 7         57 _do_test($env);
414             }
415              
416             1;
417              
418             __END__