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   1200455 use 5.006;
  1         5  
2 1     1   5 use strict;
  1         8  
  1         28  
3 1     1   5 use warnings;
  1         2  
  1         90  
4              
5             package Dist::Zilla::Plugin::Test::ReportPrereqs;
6             # ABSTRACT: Report on prerequisite versions during automated testing
7              
8             our $VERSION = '0.027';
9              
10 1     1   6 use Dist::Zilla 4 ();
  1         21  
  1         18  
11              
12 1     1   5 use Moose;
  1         2  
  1         6  
13             with 'Dist::Zilla::Role::FileGatherer', 'Dist::Zilla::Role::PrereqSource';
14              
15 1     1   6339 use Sub::Exporter::ForMethods;
  1         3  
  1         11  
16             use Data::Section 0.200002 # encoding and bytes
17 1     1   192 { installer => Sub::Exporter::ForMethods::method_installer }, '-setup';
  1         21  
  1         7  
18              
19 1     1   1337 use Data::Dumper;
  1         4483  
  1         52  
20 1     1   8 use Moose::Util::TypeConstraints 'enum';
  1         2  
  1         10  
21              
22             sub mvp_multivalue_args {
23 1     1 0 182 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 473128 my $self = shift;
50              
51 1         31 $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         257 $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 67150 my $self = shift;
72              
73 1         10 my $data = $self->merged_section_data;
74 1 50 33     10355 return unless $data and %$data;
75              
76 1         10 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         4 content => $self->_munge_test( ${ $data->{$filename} } ),
  1         7  
84             }
85             )
86             );
87             }
88              
89 1         1226 require Dist::Zilla::File::FromCode;
90             $self->add_file(
91             Dist::Zilla::File::FromCode->new(
92             {
93             name => $self->_dump_filename,
94 2     2   231306 code => sub { $self->_dump_prereqs },
95             }
96             )
97 1         62343 );
98              
99 1         626 return;
100             }
101              
102             sub _munge_test {
103 1     1   25 my ( $self, $guts ) = @_;
104 1 50       52 $guts =~ s{INSERT_VERSION_HERE}{$self->VERSION || '<self>'}e;
  1         28  
105 1         32 $guts =~ s{INSERT_DD_FILENAME_HERE}{'./' . $self->_dump_filename}e;
  1         5  
106 1         31 $guts =~ s{INSERT_INCLUDED_MODULES_HERE}{_format_list($self->included_modules)}e;
  1         53  
107 1         32 $guts =~ s{INSERT_EXCLUDED_MODULES_HERE}{_format_list($self->excluded_modules)}e;
  1         42  
108 1 50       31 $guts =~ s{INSERT_VERIFY_PREREQS_CONFIG}{$self->verify_prereqs ? 1 : 0}e;
  1         36  
109 1         30 $guts =~ s{VERSION_EXTRACTOR_MODULE}{$self->version_extractor}e;
  1         36  
110 1 50       32 if ( $self->version_extractor eq 'ExtUtils::MakeMaker' ) {
111 1         36 $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         31 return $guts;
121             }
122              
123 2     2   45 sub _dump_filename { 't/00-report-prereqs.dd' }
124              
125             sub _format_list {
126 2     2   6 return join( "\n", map { " $_" } @_ );
  3         22  
127             }
128              
129             sub _dump_prereqs {
130 2     2   7 my $self = shift;
131 2         74 my $prereqs = $self->zilla->prereqs->as_string_hash;
132 2         3409 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.027
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 annocpan anno 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 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             Karen Etheridge <ether@cpan.org>
346              
347             =item *
348              
349             Kent Fredric <kentfredric@gmail.com>
350              
351             =item *
352              
353             Randy Stauner <randy@magnificent-tears.com>
354              
355             =item *
356              
357             Yanick Champoux <yanick@babyl.dyndns.org>
358              
359             =back
360              
361             =head1 COPYRIGHT AND LICENSE
362              
363             This software is Copyright (c) 2012 by David Golden.
364              
365             This is free software, licensed under:
366              
367             The Apache License, Version 2.0, January 2004
368              
369             =cut
370              
371             __DATA__
372             ___[ t/00-report-prereqs.t ]___
373             #!perl
374              
375             use strict;
376             use warnings;
377              
378             # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs INSERT_VERSION_HERE
379              
380             use Test::More tests => 1;
381              
382             use VERSION_EXTRACTOR_MODULE;
383             use File::Spec;
384              
385             # from $version::LAX
386             my $lax_version_re =
387             qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )?
388             |
389             (?:\.[0-9]+) (?:_[0-9]+)?
390             ) | (?:
391             v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )?
392             |
393             (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)?
394             )
395             )/x;
396              
397             # hide optional CPAN::Meta modules from prereq scanner
398             # and check if they are available
399             my $cpan_meta = "CPAN::Meta";
400             my $cpan_meta_pre = "CPAN::Meta::Prereqs";
401             my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic
402              
403             # Verify requirements?
404             my $DO_VERIFY_PREREQS = INSERT_VERIFY_PREREQS_CONFIG;
405              
406             sub _max {
407             my $max = shift;
408             $max = ( $_ > $max ) ? $_ : $max for @_;
409             return $max;
410             }
411              
412             sub _merge_prereqs {
413             my ($collector, $prereqs) = @_;
414              
415             # CPAN::Meta::Prereqs object
416             if (ref $collector eq $cpan_meta_pre) {
417             return $collector->with_merged_prereqs(
418             CPAN::Meta::Prereqs->new( $prereqs )
419             );
420             }
421              
422             # Raw hashrefs
423             for my $phase ( keys %$prereqs ) {
424             for my $type ( keys %{ $prereqs->{$phase} } ) {
425             for my $module ( keys %{ $prereqs->{$phase}{$type} } ) {
426             $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module};
427             }
428             }
429             }
430              
431             return $collector;
432             }
433              
434             my @include = qw(
435             INSERT_INCLUDED_MODULES_HERE
436             );
437              
438             my @exclude = qw(
439             INSERT_EXCLUDED_MODULES_HERE
440             );
441              
442             # Add static prereqs to the included modules list
443             my $static_prereqs = do 'INSERT_DD_FILENAME_HERE';
444              
445             # Merge all prereqs (either with ::Prereqs or a hashref)
446             my $full_prereqs = _merge_prereqs(
447             ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ),
448             $static_prereqs
449             );
450              
451             # Add dynamic prereqs to the included modules list (if we can)
452             my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
453             my $cpan_meta_error;
454             if ( $source && $HAS_CPAN_META
455             && (my $meta = eval { CPAN::Meta->load_file($source) } )
456             ) {
457             $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs);
458             }
459             else {
460             $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source)
461             $source = 'static metadata';
462             }
463              
464             my @full_reports;
465             my @dep_errors;
466             my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs;
467              
468             # Add static includes into a fake section
469             for my $mod (@include) {
470             $req_hash->{other}{modules}{$mod} = 0;
471             }
472              
473             for my $phase ( qw(configure build test runtime develop other) ) {
474             next unless $req_hash->{$phase};
475             next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING});
476              
477             for my $type ( qw(requires recommends suggests conflicts modules) ) {
478             next unless $req_hash->{$phase}{$type};
479              
480             my $title = ucfirst($phase).' '.ucfirst($type);
481             my @reports = [qw/Module Want Have/];
482              
483             for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) {
484             next if $mod eq 'perl';
485             next if grep { $_ eq $mod } @exclude;
486              
487             my $file = $mod;
488             $file =~ s{::}{/}g;
489             $file .= ".pm";
490             my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC;
491              
492             my $want = $req_hash->{$phase}{$type}{$mod};
493             $want = "undef" unless defined $want;
494             $want = "any" if !$want && $want == 0;
495              
496             my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";
497              
498             if ($prefix) {
499             my $have = VERSION_EXTRACTION;
500             $have = "undef" unless defined $have;
501             push @reports, [$mod, $want, $have];
502              
503             if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) {
504             if ( $have !~ /\A$lax_version_re\z/ ) {
505             push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)";
506             }
507             elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) {
508             push @dep_errors, "$mod version '$have' is not in required range '$want'";
509             }
510             }
511             }
512             else {
513             push @reports, [$mod, $want, "missing"];
514              
515             if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) {
516             push @dep_errors, "$mod is not installed ($req_string)";
517             }
518             }
519             }
520              
521             if ( @reports ) {
522             push @full_reports, "=== $title ===\n\n";
523              
524             my $ml = _max( map { length $_->[0] } @reports );
525             my $wl = _max( map { length $_->[1] } @reports );
526             my $hl = _max( map { length $_->[2] } @reports );
527              
528             if ($type eq 'modules') {
529             splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl];
530             push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports;
531             }
532             else {
533             splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl];
534             push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports;
535             }
536              
537             push @full_reports, "\n";
538             }
539             }
540             }
541              
542             if ( @full_reports ) {
543             diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports;
544             }
545              
546             if ( $cpan_meta_error || @dep_errors ) {
547             diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n";
548             }
549              
550             if ( $cpan_meta_error ) {
551             my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
552             diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n";
553             }
554              
555             if ( @dep_errors ) {
556             diag join("\n",
557             "\nThe following REQUIRED prerequisites were not satisfied:\n",
558             @dep_errors,
559             "\n"
560             );
561             }
562              
563             pass;
564              
565             # vim: ts=4 sts=4 sw=4 et: