File Coverage

blib/lib/Dist/Zilla/Plugin/Test/ReportPrereqs.pm
Criterion Covered Total %
statement 63 64 98.4
branch 4 8 50.0
condition 1 3 33.3
subroutine 17 17 100.0
pod 0 3 0.0
total 85 95 89.4


line stmt bran cond sub pod time code
1 1     1   1189749 use 5.006;
  1         4  
2 1     1   6 use strict;
  1         3  
  1         23  
3 1     1   6 use warnings;
  1         2  
  1         65  
4              
5             package Dist::Zilla::Plugin::Test::ReportPrereqs;
6             # ABSTRACT: Report on prerequisite versions during automated testing
7              
8             our $VERSION = '0.028';
9              
10 1     1   6 use Dist::Zilla 4 ();
  1         20  
  1         25  
11              
12 1     1   7 use Moose;
  1         1  
  1         6  
13             with 'Dist::Zilla::Role::FileGatherer', 'Dist::Zilla::Role::PrereqSource';
14              
15 1     1   5433 use Sub::Exporter::ForMethods;
  1         2  
  1         11  
16             use Data::Section 0.200002 # encoding and bytes
17 1     1   191 { installer => Sub::Exporter::ForMethods::method_installer }, '-setup';
  1         21  
  1         6  
18              
19 1     1   1292 use Data::Dumper;
  1         5491  
  1         68  
20 1     1   6 use Moose::Util::TypeConstraints 'enum';
  1         2  
  1         9  
21              
22             sub mvp_multivalue_args {
23 1     1 0 169 return qw( include exclude );
24             }
25              
26             foreach my $attr (qw( include exclude )) {
27             has "${attr}s" => (
28             init_arg => $attr,
29             is => 'ro',
30             traits => ['Array'],
31             default => sub { [] },
32             handles => { "${attr}d_modules" => 'elements', },
33             );
34             }
35              
36             has verify_prereqs => (
37             is => 'ro',
38             isa => 'Bool',
39             default => 1,
40             );
41              
42             has version_extractor => (
43             is => 'ro',
44             isa => enum( [qw(ExtUtils::MakeMaker Module::Metadata)] ),
45             default => 'ExtUtils::MakeMaker',
46             );
47              
48             sub register_prereqs {
49 1     1 0 460698 my $self = shift;
50              
51 1         30 $self->zilla->register_prereqs(
52             {
53             phase => 'test',
54             type => 'requires',
55             },
56             'Test::More' => 0,
57             $self->version_extractor => 0,
58             'File::Spec' => 0,
59             );
60              
61 1         231 $self->zilla->register_prereqs(
62             {
63             phase => 'test',
64             type => 'recommends',
65             },
66             'CPAN::Meta' => '2.120900',
67             );
68             }
69              
70             sub gather_files {
71 1     1 0 62840 my $self = shift;
72              
73 1         6 my $data = $self->merged_section_data;
74 1 50 33     7913 return unless $data and %$data;
75              
76 1         11 require Dist::Zilla::File::InMemory;
77              
78 1         5 for my $filename ( keys %$data ) {
79             $self->add_file(
80             Dist::Zilla::File::InMemory->new(
81             {
82             name => $filename,
83 1         2 content => $self->_munge_test( ${ $data->{$filename} } ),
  1         5  
84             }
85             )
86             );
87             }
88              
89 1         1114 require Dist::Zilla::File::FromCode;
90             $self->add_file(
91             Dist::Zilla::File::FromCode->new(
92             {
93             name => $self->_dump_filename,
94 2     2   229398 code => sub { $self->_dump_prereqs },
95             }
96             )
97 1         58788 );
98              
99 1         516 return;
100             }
101              
102             sub _munge_test {
103 1     1   12 my ( $self, $guts ) = @_;
104 1 50       34 $guts =~ s{INSERT_VERSION_HERE}{$self->VERSION || '<self>'}e;
  1         36  
105 1         34 $guts =~ s{INSERT_DD_FILENAME_HERE}{'./' . $self->_dump_filename}e;
  1         5  
106 1         32 $guts =~ s{INSERT_INCLUDED_MODULES_HERE}{_format_list($self->included_modules)}e;
  1         49  
107 1         31 $guts =~ s{INSERT_EXCLUDED_MODULES_HERE}{_format_list($self->excluded_modules)}e;
  1         70  
108 1 50       32 $guts =~ s{INSERT_VERIFY_PREREQS_CONFIG}{$self->verify_prereqs ? 1 : 0}e;
  1         37  
109 1         31 $guts =~ s{VERSION_EXTRACTOR_MODULE}{$self->version_extractor}e;
  1         36  
110 1 50       35 if ( $self->version_extractor eq 'ExtUtils::MakeMaker' ) {
111 1         42 $guts
112             =~ s'VERSION_EXTRACTION'MM->parse_version( File::Spec->catfile($prefix, $file) )';
113              
114             }
115             else { # Module::Metadata
116 0         0 $guts
117             =~ s'VERSION_EXTRACTION'Module::Metadata->new_from_file( File::Spec->catfile($prefix, $file) )->version';
118             }
119              
120 1         32 return $guts;
121             }
122              
123 2     2   45 sub _dump_filename { 't/00-report-prereqs.dd' }
124              
125             sub _format_list {
126 2     2   5 return join( "\n", map { " $_" } @_ );
  3         26  
127             }
128              
129             sub _dump_prereqs {
130 2     2   6 my $self = shift;
131 2         47 my $prereqs = $self->zilla->prereqs->as_string_hash;
132 2         1372 return ("do { my "
133             . Data::Dumper->new( [$prereqs], ['x'] )->Purity(1)->Sortkeys(1)->Terse(0)->Dump()
134             . ' $x;'
135             . "\n }" );
136             }
137              
138             __PACKAGE__->meta->make_immutable;
139              
140             1;
141              
142             #pod =for Pod::Coverage
143             #pod gather_files
144             #pod mvp_multivalue_args
145             #pod register_prereqs
146             #pod
147             #pod =head1 SYNOPSIS
148             #pod
149             #pod # in dist.ini
150             #pod [Test::ReportPrereqs]
151             #pod include = Acme::FYI
152             #pod exclude = Acme::Dont::Care
153             #pod
154             #pod =head1 DESCRIPTION
155             #pod
156             #pod This L<Dist::Zilla> plugin adds a F<t/00-report-prereqs.t> test file and an accompanying
157             #pod F<t/00-report-prereqs.dd> data file. It reports
158             #pod the version of all modules listed in the distribution metadata prerequisites
159             #pod (including 'recommends', 'suggests', etc.). However, any 'develop' prereqs
160             #pod are not reported (unless they show up in another category).
161             #pod
162             #pod If a F<MYMETA.json> file exists and L<CPAN::Meta> is installed on the testing
163             #pod machine, F<MYMETA.json> will be examined for prerequisites in addition, as it
164             #pod would include any dynamic prerequisites not set in the distribution metadata.
165             #pod
166             #pod Versions are reported based on the result of C<parse_version> from
167             #pod L<ExtUtils::MakeMaker>, which means prerequisite modules are not actually
168             #pod loaded (which avoids various edge cases with certain modules). Parse errors are
169             #pod reported as "undef". If a module is not installed, "missing" is reported
170             #pod instead of a version string.
171             #pod
172             #pod Additionally, if L<CPAN::Meta> is installed, unfulfilled required prerequisites
173             #pod are reported after the list of all versions based on either F<MYMETA>
174             #pod (preferably) or F<META> (fallback).
175             #pod
176             #pod =head1 CONFIGURATION
177             #pod
178             #pod =head2 include
179             #pod
180             #pod An C<include> attribute can be specified (multiple times) to add modules
181             #pod to the report. This can be useful if there is a module in the dependency
182             #pod chain that is problematic but is not directly required by this project.
183             #pod These modules will be listed in an "Other Modules" section at the end of
184             #pod the report.
185             #pod
186             #pod =head2 exclude
187             #pod
188             #pod An C<exclude> attribute can be specified (multiple times) to remove
189             #pod modules from the report (if you had a reason to do so).
190             #pod
191             #pod =head2 verify_prereqs
192             #pod
193             #pod When set, installed versions of all 'requires' prerequisites are verified
194             #pod against those specified. Defaults to true, but requires CPAN::Meta to be installed.
195             #pod
196             #pod =head2 version_extractor
197             #pod
198             #pod Specifies the module to use to extract each installed prerequisite's
199             #pod version. Defaults to L<ExtUtils::MakeMaker>, which is recommended for most
200             #pod situations. It can also be specified as L<Module::Metadata>, which can be
201             #pod useful if L<ExtUtils::MakeMaker>'s mechanism is too naive, or if
202             #pod L<ExtUtils::MakeMaker> is not already a prerequisite of the distribution.
203             #pod
204             #pod =head1 SEE ALSO
205             #pod
206             #pod Other Dist::Zilla::Plugins do similar things in slightly different ways that didn't
207             #pod suit my style and needs.
208             #pod
209             #pod =for :list
210             #pod * L<Dist::Zilla::Plugin::Test::PrereqsFromMeta> -- requires prereqs to be satisfied
211             #pod * L<Dist::Zilla::Plugin::Test::ReportVersions> -- bundles a copy of YAML::Tiny, reads prereqs only from META.yml, and attempts to load them with C<require>
212             #pod * L<Dist::Zilla::Plugin::ReportVersions::Tiny> -- static list only, loads modules with C<require>
213             #pod
214             #pod =cut
215              
216             =pod
217              
218             =encoding UTF-8
219              
220             =head1 NAME
221              
222             Dist::Zilla::Plugin::Test::ReportPrereqs - Report on prerequisite versions during automated testing
223              
224             =head1 VERSION
225              
226             version 0.028
227              
228             =head1 SYNOPSIS
229              
230             # in dist.ini
231             [Test::ReportPrereqs]
232             include = Acme::FYI
233             exclude = Acme::Dont::Care
234              
235             =head1 DESCRIPTION
236              
237             This L<Dist::Zilla> plugin adds a F<t/00-report-prereqs.t> test file and an accompanying
238             F<t/00-report-prereqs.dd> data file. It reports
239             the version of all modules listed in the distribution metadata prerequisites
240             (including 'recommends', 'suggests', etc.). However, any 'develop' prereqs
241             are not reported (unless they show up in another category).
242              
243             If a F<MYMETA.json> file exists and L<CPAN::Meta> is installed on the testing
244             machine, F<MYMETA.json> will be examined for prerequisites in addition, as it
245             would include any dynamic prerequisites not set in the distribution metadata.
246              
247             Versions are reported based on the result of C<parse_version> from
248             L<ExtUtils::MakeMaker>, which means prerequisite modules are not actually
249             loaded (which avoids various edge cases with certain modules). Parse errors are
250             reported as "undef". If a module is not installed, "missing" is reported
251             instead of a version string.
252              
253             Additionally, if L<CPAN::Meta> is installed, unfulfilled required prerequisites
254             are reported after the list of all versions based on either F<MYMETA>
255             (preferably) or F<META> (fallback).
256              
257             =for Pod::Coverage gather_files
258             mvp_multivalue_args
259             register_prereqs
260              
261             =head1 CONFIGURATION
262              
263             =head2 include
264              
265             An C<include> attribute can be specified (multiple times) to add modules
266             to the report. This can be useful if there is a module in the dependency
267             chain that is problematic but is not directly required by this project.
268             These modules will be listed in an "Other Modules" section at the end of
269             the report.
270              
271             =head2 exclude
272              
273             An C<exclude> attribute can be specified (multiple times) to remove
274             modules from the report (if you had a reason to do so).
275              
276             =head2 verify_prereqs
277              
278             When set, installed versions of all 'requires' prerequisites are verified
279             against those specified. Defaults to true, but requires CPAN::Meta to be installed.
280              
281             =head2 version_extractor
282              
283             Specifies the module to use to extract each installed prerequisite's
284             version. Defaults to L<ExtUtils::MakeMaker>, which is recommended for most
285             situations. It can also be specified as L<Module::Metadata>, which can be
286             useful if L<ExtUtils::MakeMaker>'s mechanism is too naive, or if
287             L<ExtUtils::MakeMaker> is not already a prerequisite of the distribution.
288              
289             =head1 SEE ALSO
290              
291             Other Dist::Zilla::Plugins do similar things in slightly different ways that didn't
292             suit my style and needs.
293              
294             =over 4
295              
296             =item *
297              
298             L<Dist::Zilla::Plugin::Test::PrereqsFromMeta> -- requires prereqs to be satisfied
299              
300             =item *
301              
302             L<Dist::Zilla::Plugin::Test::ReportVersions> -- bundles a copy of YAML::Tiny, reads prereqs only from META.yml, and attempts to load them with C<require>
303              
304             =item *
305              
306             L<Dist::Zilla::Plugin::ReportVersions::Tiny> -- static list only, loads modules with C<require>
307              
308             =back
309              
310             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
311              
312             =head1 SUPPORT
313              
314             =head2 Bugs / Feature Requests
315              
316             Please report any bugs or feature requests through the issue tracker
317             at L<https://github.com/dagolden/Dist-Zilla-Plugin-Test-ReportPrereqs/issues>.
318             You will be notified automatically of any progress on your issue.
319              
320             =head2 Source Code
321              
322             This is open source software. The code repository is available for
323             public review and contribution under the terms of the license.
324              
325             L<https://github.com/dagolden/Dist-Zilla-Plugin-Test-ReportPrereqs>
326              
327             git clone https://github.com/dagolden/Dist-Zilla-Plugin-Test-ReportPrereqs.git
328              
329             =head1 AUTHOR
330              
331             David Golden <dagolden@cpan.org>
332              
333             =head1 CONTRIBUTORS
334              
335             =for stopwords Brendan Byrd Dave Rolsky Karen Etheridge Kent Fredric Randy Stauner Yanick Champoux
336              
337             =over 4
338              
339             =item *
340              
341             Brendan Byrd <Perl@ResonatorSoft.org>
342              
343             =item *
344              
345             Dave Rolsky <autarch@urth.org>
346              
347             =item *
348              
349             Karen Etheridge <ether@cpan.org>
350              
351             =item *
352              
353             Kent Fredric <kentfredric@gmail.com>
354              
355             =item *
356              
357             Randy Stauner <randy@magnificent-tears.com>
358              
359             =item *
360              
361             Yanick Champoux <yanick@babyl.dyndns.org>
362              
363             =back
364              
365             =head1 COPYRIGHT AND LICENSE
366              
367             This software is Copyright (c) 2012 by David Golden.
368              
369             This is free software, licensed under:
370              
371             The Apache License, Version 2.0, January 2004
372              
373             =cut
374              
375             __DATA__
376             ___[ t/00-report-prereqs.t ]___
377             #!perl
378              
379             use strict;
380             use warnings;
381              
382             # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs INSERT_VERSION_HERE
383              
384             use Test::More tests => 1;
385              
386             use VERSION_EXTRACTOR_MODULE;
387             use File::Spec;
388              
389             # from $version::LAX
390             my $lax_version_re =
391             qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )?
392             |
393             (?:\.[0-9]+) (?:_[0-9]+)?
394             ) | (?:
395             v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )?
396             |
397             (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)?
398             )
399             )/x;
400              
401             # hide optional CPAN::Meta modules from prereq scanner
402             # and check if they are available
403             my $cpan_meta = "CPAN::Meta";
404             my $cpan_meta_pre = "CPAN::Meta::Prereqs";
405             my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic
406              
407             # Verify requirements?
408             my $DO_VERIFY_PREREQS = INSERT_VERIFY_PREREQS_CONFIG;
409              
410             sub _max {
411             my $max = shift;
412             $max = ( $_ > $max ) ? $_ : $max for @_;
413             return $max;
414             }
415              
416             sub _merge_prereqs {
417             my ($collector, $prereqs) = @_;
418              
419             # CPAN::Meta::Prereqs object
420             if (ref $collector eq $cpan_meta_pre) {
421             return $collector->with_merged_prereqs(
422             CPAN::Meta::Prereqs->new( $prereqs )
423             );
424             }
425              
426             # Raw hashrefs
427             for my $phase ( keys %$prereqs ) {
428             for my $type ( keys %{ $prereqs->{$phase} } ) {
429             for my $module ( keys %{ $prereqs->{$phase}{$type} } ) {
430             $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module};
431             }
432             }
433             }
434              
435             return $collector;
436             }
437              
438             my @include = qw(
439             INSERT_INCLUDED_MODULES_HERE
440             );
441              
442             my @exclude = qw(
443             INSERT_EXCLUDED_MODULES_HERE
444             );
445              
446             # Add static prereqs to the included modules list
447             my $static_prereqs = do 'INSERT_DD_FILENAME_HERE';
448              
449             # Merge all prereqs (either with ::Prereqs or a hashref)
450             my $full_prereqs = _merge_prereqs(
451             ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ),
452             $static_prereqs
453             );
454              
455             # Add dynamic prereqs to the included modules list (if we can)
456             my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
457             my $cpan_meta_error;
458             if ( $source && $HAS_CPAN_META
459             && (my $meta = eval { CPAN::Meta->load_file($source) } )
460             ) {
461             $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs);
462             }
463             else {
464             $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source)
465             $source = 'static metadata';
466             }
467              
468             my @full_reports;
469             my @dep_errors;
470             my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs;
471              
472             # Add static includes into a fake section
473             for my $mod (@include) {
474             $req_hash->{other}{modules}{$mod} = 0;
475             }
476              
477             for my $phase ( qw(configure build test runtime develop other) ) {
478             next unless $req_hash->{$phase};
479             next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING});
480              
481             for my $type ( qw(requires recommends suggests conflicts modules) ) {
482             next unless $req_hash->{$phase}{$type};
483              
484             my $title = ucfirst($phase).' '.ucfirst($type);
485             my @reports = [qw/Module Want Have/];
486              
487             for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) {
488             next if $mod eq 'perl';
489             next if grep { $_ eq $mod } @exclude;
490              
491             my $file = $mod;
492             $file =~ s{::}{/}g;
493             $file .= ".pm";
494             my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC;
495              
496             my $want = $req_hash->{$phase}{$type}{$mod};
497             $want = "undef" unless defined $want;
498             $want = "any" if !$want && $want == 0;
499              
500             my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";
501              
502             if ($prefix) {
503             my $have = VERSION_EXTRACTION;
504             $have = "undef" unless defined $have;
505             push @reports, [$mod, $want, $have];
506              
507             if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) {
508             if ( $have !~ /\A$lax_version_re\z/ ) {
509             push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)";
510             }
511             elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) {
512             push @dep_errors, "$mod version '$have' is not in required range '$want'";
513             }
514             }
515             }
516             else {
517             push @reports, [$mod, $want, "missing"];
518              
519             if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) {
520             push @dep_errors, "$mod is not installed ($req_string)";
521             }
522             }
523             }
524              
525             if ( @reports ) {
526             push @full_reports, "=== $title ===\n\n";
527              
528             my $ml = _max( map { length $_->[0] } @reports );
529             my $wl = _max( map { length $_->[1] } @reports );
530             my $hl = _max( map { length $_->[2] } @reports );
531              
532             if ($type eq 'modules') {
533             splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl];
534             push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports;
535             }
536             else {
537             splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl];
538             push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports;
539             }
540              
541             push @full_reports, "\n";
542             }
543             }
544             }
545              
546             if ( @full_reports ) {
547             diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports;
548             }
549              
550             if ( $cpan_meta_error || @dep_errors ) {
551             diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n";
552             }
553              
554             if ( $cpan_meta_error ) {
555             my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
556             diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n";
557             }
558              
559             if ( @dep_errors ) {
560             diag join("\n",
561             "\nThe following REQUIRED prerequisites were not satisfied:\n",
562             @dep_errors,
563             "\n"
564             );
565             }
566              
567             pass('Reported prereqs');
568              
569             # vim: ts=4 sts=4 sw=4 et: