File Coverage

blib/lib/Dist/Zilla/Plugin/VerifyPhases.pm
Criterion Covered Total %
statement 131 132 99.2
branch 32 36 88.8
condition 15 24 62.5
subroutine 21 21 100.0
pod 0 7 0.0
total 199 220 90.4


line stmt bran cond sub pod time code
1 6     6   2851065 use strict;
  6         7  
  6         177  
2 6     6   21 use warnings;
  6         7  
  6         313  
3             package Dist::Zilla::Plugin::VerifyPhases; # git description: v0.013-6-g5690f92
4             # ABSTRACT: Compare data and files at different phases of the distribution build process
5             # KEYWORDS: plugin distribution configuration phase verification validation
6             # vim: set ts=8 sts=4 sw=4 tw=115 et :
7              
8             our $VERSION = '0.014';
9              
10 6     6   21 use Moose;
  6         9  
  6         43  
11             with
12             'Dist::Zilla::Role::BeforeBuild',
13             'Dist::Zilla::Role::FileGatherer',
14             'Dist::Zilla::Role::EncodingProvider',
15             'Dist::Zilla::Role::FilePruner',
16             'Dist::Zilla::Role::FileMunger',
17             'Dist::Zilla::Role::AfterBuild';
18 6     6   25384 use Moose::Util 'find_meta';
  6         9  
  6         36  
19 6     6   829 use Digest::MD5 'md5_hex';
  6         8  
  6         436  
20 6     6   27 use List::Util 1.33 qw(none any);
  6         160  
  6         342  
21 6     6   1108 use Term::ANSIColor 3.00 'colored';
  6         9000  
  6         790  
22 6     6   3469 use Storable 'dclone';
  6         13344  
  6         343  
23 6     6   2268 use Test::Deep::NoTest qw(cmp_details deep_diag);
  6         757  
  6         21  
24 6     6   983 use namespace::autoclean;
  6         7  
  6         51  
25              
26             # filename => [ { object => $file_object, content => $checksummed_content } ]
27             my %all_files;
28              
29             # returns the filename and index under which the provided file can be found
30             sub _search_all_files
31             {
32 22     22   25 my ($self, $file) = @_;
33              
34 22         54 for my $filename (keys %all_files)
35             {
36 44         37 foreach my $index (0 .. $#{$all_files{$filename}})
  44         81  
37             {
38 33 100       103 return ($filename, $index) if $all_files{$filename}[$index]{object} == $file;
39             }
40             }
41             }
42              
43             #sub mvp_multivalue_args { qw(skip) }
44             has skip_file => (
45             isa => 'ArrayRef[Str]',
46             traits => [ 'Array' ],
47             handles => { skip_file => 'elements' },
48             init_arg => undef, # do not allow in configs just yet
49             lazy => 1,
50             default => sub { [ qw(Makefile.PL Build.PL) ] },
51             );
52              
53             has skip_distmeta => (
54             isa => 'ArrayRef[Str]',
55             traits => [ 'Array' ],
56             handles => { skip_distmeta => 'elements' },
57             init_arg => undef, # do not allow in configs just yet
58             lazy => 1,
59             default => sub { [ qw(x_static_install) ] },
60             );
61              
62             my %zilla_constructor_args;
63              
64             sub BUILD
65             {
66 6     6 0 14943 my $self = shift;
67 6         170 my $zilla = $self->zilla;
68 6         50 my $meta = find_meta($zilla);
69              
70             # no phases have been run yet, so we can effectively capture the initial
71             # state of the zilla object (and determine its construction args)
72             %zilla_constructor_args = map {
73 6         52 my $attr = $meta->find_attribute_by_name($_);
  60         2967  
74 60 100 66     2292 $attr && $attr->has_value($zilla) ? ( $_ => $attr->get_value($zilla) ) : ()
75             } qw(name version release_status abstract main_module authors distmeta _license_class _copyright_holder _copyright_year);
76             }
77              
78             # no reason to include configs - this plugin does not alter the build output
79             around dump_config => sub
80             {
81             my ($orig, $self) = @_;
82             my $config = $self->$orig;
83              
84             my $data = {
85             blessed($self) ne __PACKAGE__ ? ( version => $VERSION ) : (),
86             };
87             $config->{+__PACKAGE__} = $data if keys %$data;
88              
89             return $config;
90             };
91              
92             sub before_build
93             {
94 6     6 0 293593 my $self = shift;
95              
96             # adjust plugin order so that we are always last!
97 6         169 my $plugins = $self->zilla->plugins;
98 6         147 @$plugins = ((grep { $_ != $self } @$plugins), $self);
  74         95  
99             }
100              
101             sub gather_files
102             {
103 6     6 0 58452 my $self = shift;
104              
105 6         143 my $zilla = $self->zilla;
106 6         50 my $meta = find_meta($zilla);
107              
108 6         63 foreach my $attr_name (qw(name version release_status abstract main_module authors distmeta _share_dir_map))
109             {
110 48 100       1927 next if exists $zilla_constructor_args{$attr_name};
111 33         78 my $attr = $meta->find_attribute_by_name($attr_name);
112 33 100 66     1461 $self->_alert($attr_name . ' has already been calculated by end of file gathering phase')
113             if $attr and $attr->has_value($zilla);
114             }
115              
116             # license is created from some private attrs, which may have been provided
117             # at construction time
118             $self->_alert('license has already been calculated by end of file gathering phase')
119             if any {
120 16 100   16   50 not exists $zilla_constructor_args{$_}
121             and $meta->find_attribute_by_name($_)->has_value($zilla)
122 6 100       308 } qw(_license_class _copyright_holder _copyright_year);
123              
124             # all files should have been added by now. save their filenames/objects
125 6         477 foreach my $file (@{$zilla->files})
  6         150  
126             {
127 9         159 push @{ $all_files{$file->name} }, {
  9         25  
128             object => $file,
129             # encoding can change; don't bother capturing it yet
130             # content can change; don't bother capturing it yet
131             };
132             }
133             }
134              
135             # since last phase,
136             # new files added: no
137             # files removed: no
138             # files renamed: no
139             # encoding changed: ok to now; no from now on
140             # contents: ignore
141             sub set_file_encodings
142             {
143 6     6 0 6231 my $self = shift;
144              
145             # since the encoding attribute is SetOnce, if we force all the builders to
146             # fire now, we can guarantee they won't change later
147 6         10 foreach my $file (@{$self->zilla->files})
  6         140  
148             {
149 9         146 foreach my $entry (@{ $all_files{$file->name} })
  9         24  
150             {
151 11 100       519 $entry->{encoding} = $file->encoding if $entry->{object} eq $file;
152             }
153             }
154             }
155              
156             # since last phase,
157             # new files added: no
158             # files removed: ok to now; no from now on
159             # files renamed: no
160             # encoding changed: no
161             # contents: ignore
162             sub prune_files
163             {
164 6     6 0 6572 my $self = shift;
165              
166             # remove all still-existing files from our tracking list
167 6         8 foreach my $file (@{$self->zilla->files})
  6         138  
168             {
169 8         126 my ($filename, $index) = $self->_search_all_files($file);
170 8 50 33     43 if ($filename and defined $index)
171             {
172             # file has been renamed - an odd time to do this
173 8 50       29 $self->_alert('file has been renamed after file gathering phase: \'' . $file->name
174             . "' (originally '$filename', " . $file->added_by . ')')
175             if $filename ne $file->name;
176              
177 8         281 splice @{ $all_files{$filename} }, $index, 1;
  8         16  
178 8         20 next;
179             }
180              
181 0         0 $self->_alert('file has been added after file gathering phase: \'' . $file->name
182             . '\' (' . $file->added_by . ')');
183             }
184              
185             # anything left over has been removed, but this is okay by a file pruner
186              
187             # capture full file list all over again.
188 6         58 %all_files = ();
189 6         15 foreach my $file (@{$self->zilla->files})
  6         136  
190             {
191 8         269 push @{ $all_files{$file->name} }, {
  8         18  
192             object => $file,
193             encoding => $file->encoding,
194             content => undef, # content can change; don't bother capturing it yet
195             };
196             }
197             }
198              
199             my $distmeta;
200              
201             # since last phase,
202             # new files added: no
203             # files removed: no
204             # files renamed: allowed
205             # encoding changed: no
206             # record contents: ok to now; no from now on
207             # distmeta changed: ok to now; no from now on
208             # no prerequisites have been added yet
209             sub munge_files
210             {
211 5     5 0 12357 my $self = shift;
212              
213             # remove all still-existing files from our tracking list
214 5         9 foreach my $file (@{$self->zilla->files})
  5         121  
215             {
216 7         101 my ($filename, $index) = $self->_search_all_files($file);
217 7 100 66     35 if ($filename and defined $index)
218             {
219             # the file may have been renamed - but this is okay by a file munger
220 6         5 splice @{ $all_files{$filename} }, $index, 1;
  6         10  
221 6         13 next;
222             }
223              
224             # this is a new file we haven't seen before.
225 1         4 $self->_alert('file has been added after file gathering phase: \'' . $file->name
226             . '\' (' . $file->added_by . ')');
227             }
228              
229             # now report on any files added earlier that were removed.
230 5         278 foreach my $filename (keys %all_files)
231             {
232             $self->_alert('file has been removed after file pruning phase: \'' . $filename
233             . '\' (' . $_->{object}->added_by . ')')
234 7         215 foreach @{ $all_files{$filename} };
  7         49  
235             }
236              
237             # capture full file list all over again, recording contents now.
238 5         43 %all_files = ();
239 5         15 foreach my $file (@{$self->zilla->files})
  5         112  
240             {
241             # don't force FromCode files to calculate early; it might fire some
242             # lazy attributes prematurely
243 7 50       1032 push @{ $all_files{$file->name} }, {
  7         21  
244             object => $file,
245             encoding => $file->encoding,
246             content => ( $file->isa('Dist::Zilla::File::FromCode')
247             ? 'content ignored'
248             : md5_hex($file->encoded_content) ),
249             };
250             }
251              
252             # verify that nothing has tried to read the prerequisite data yet
253             # (only possible when the attribute is lazily built)
254 5         1798 my $prereq_attr = find_meta($self->zilla)->find_attribute_by_name('prereqs');
255 5 100 66     511 $self->_alert('prereqs have already been read from after munging phase!')
256             if Dist::Zilla->VERSION >= 5.024 and $prereq_attr->has_value($self->zilla);
257              
258             # verify no prerequisites have been provided yet
259             # (it would be highly unlikely for distmeta not to be populated yet, but
260             # force it anwyay so we have something to compare to later)
261 5         450 $distmeta = dclone($self->zilla->distmeta);
262 5 100       97059 if (exists $distmeta->{prereqs})
263             {
264 1         671 require Data::Dumper;
265             $self->_alert('prereqs have been improperly included with distribution metadata:',
266 1         4257 Data::Dumper->new([ $distmeta->{prereqs} ])->Indent(2)->Terse(1)->Sortkeys(1)->Dump,
267             );
268 1         267 delete $distmeta->{prereqs};
269             }
270             }
271              
272             # since last phase,
273             # new files added: no
274             # files removed: no
275             # files renamed: no
276             # change contents: no
277             # distmeta has not changed
278             sub after_build
279             {
280 5     5 0 50718 my $self = shift;
281              
282 5         10 foreach my $file (@{$self->zilla->files})
  5         125  
283             {
284 7         174 my ($filename, $index) = $self->_search_all_files($file);
285 7 100 66     44 if (not $filename or not defined $index)
286             {
287 1         3 $self->_alert('file has been added after file gathering phase: \'' . $file->name
288             . '\' (' . $file->added_by . ')');
289 1         215 next;
290             }
291              
292 6 100       20 if ($filename ne $file->name)
293             {
294 1         54 $self->_alert('file has been renamed after munging phase: \'' . $file->name
295             . "' (originally '$filename', " . $file->added_by . ')');
296 1         246 splice @{ $all_files{$filename} }, $index, 1;
  1         3  
297 1         3 next;
298             }
299              
300             # we give FromCode files a bye, since there is a good reason why their
301             # content at file munging time is incomplete
302             $self->_alert('content has changed after munging phase: \'' . $file->name
303             # this looks suspicious; we ought to have separate added_by,
304             # changed_by attributes
305             . '\' (' . $file->added_by . ')')
306             if not $file->isa('Dist::Zilla::File::FromCode')
307 9     9   145 and none { $file->name eq $_ } $self->skip_file
308 5 50 66     431 and $all_files{$file->name}[$index]{content} ne md5_hex($file->encoded_content);
      66        
309              
310 5         529 delete $all_files{$file->name};
311             }
312              
313 5         134 foreach my $filename (keys %all_files)
314             {
315             $self->_alert('file has been removed after file pruning phase: \'' . $filename
316             . '\' (' . $_->{object}->added_by . ')')
317 2         208 foreach @{ $all_files{$filename} };
  2         37  
318             }
319              
320             # check distmeta, minus prereqs
321 5         115 my $new_distmeta = dclone($self->zilla->distmeta);
322 5         380 delete $new_distmeta->{prereqs};
323 5         201 foreach my $ignore_key ($self->skip_distmeta)
324             {
325 5         24 $distmeta->{$ignore_key} = Test::Deep::ignore;
326 5 100       6160 delete $distmeta->{$ignore_key} if not exists $new_distmeta->{$ignore_key};
327             }
328 5         23 my ($ok, $stack) = cmp_details($new_distmeta, $distmeta);
329 5 100       56714 if (not $ok)
330             {
331 1         4 chomp(my $error = deep_diag($stack));
332 1         83 $self->_alert('distribution metadata has been altered after munging phase!', $error);
333             }
334             }
335              
336             sub _alert
337             {
338 17     17   761 my $self = shift;
339 17         56 $self->log(colored(join(' ', @_), 'bright_red'));
340             }
341              
342             1;
343              
344             __END__
345              
346             =pod
347              
348             =encoding UTF-8
349              
350             =head1 NAME
351              
352             Dist::Zilla::Plugin::VerifyPhases - Compare data and files at different phases of the distribution build process
353              
354             =head1 VERSION
355              
356             version 0.014
357              
358             =head1 SYNOPSIS
359              
360             In your F<dist.ini>:
361              
362             [VerifyPhases]
363              
364             =head1 DESCRIPTION
365              
366             This plugin runs in multiple L<Dist::Zilla> phases to check what actions have
367             taken place so far. Its intent is to find any plugins that are performing
368             actions outside the appropriate phase, so they can be fixed.
369              
370             Running at the end of the C<-FileGatherer> phase, it verifies that the
371             following distribution properties have not yet been populated/calculated, as
372             they usually depend on having the full complement of files added to the
373             distribution, with known encodings:
374              
375             =over 4
376              
377             =item *
378              
379             name
380              
381             =item *
382              
383             version
384              
385             =item *
386              
387             release_status
388              
389             =item *
390              
391             abstract
392              
393             =item *
394              
395             main_module
396              
397             =item *
398              
399             license
400              
401             =item *
402              
403             authors
404              
405             =item *
406              
407             metadata
408              
409             =item *
410              
411             _share_dir_map
412              
413             =back
414              
415             Running at the end of the C<-EncodingProvider> phase, it forces all encodings
416             to be built (by calling their lazy builders), to use their C<SetOnce> property
417             to ensure that no subsequent phase attempts to alter a file encoding.
418              
419             Running at the end of the C<-FilePruner> phase, it verifies that no additional
420             files have been added to the distribution, nor renamed, since the
421             C<-FileGatherer> phase.
422              
423             Running at the end of the C<-FileMunger> phase, it verifies that no additional
424             files have been added to nor removed from the distribution, nor renamed, since
425             the C<-FilePruner> phase; and that no prerequisites have yet been provided.
426             Additionally, it verifies that the prerequisite list has not yet been read
427             from, when possible.
428              
429             Running at the end of the C<-AfterBuild> phase, the full state of all files
430             are checked: files may not be added, removed, renamed nor had their content
431             change. Additionally, it verifies that no distribution metadata (with the
432             exception of prerequisites) has changed since the end of the C<-FileMunger>
433             phase.
434              
435             =for stopwords FromCode
436              
437             Currently, L<FromCode|Dist::Zilla::File::FromCode> files are not checked for
438             content, as interesting side effects can occur if their content subs are run
439             before all content is available (for example, other lazy builders can run too
440             early, resulting in incomplete or missing data).
441              
442             =for Pod::Coverage BUILD before_build gather_files set_file_encodings prune_files munge_files after_build
443              
444             =head1 SEE ALSO
445              
446             =over 4
447              
448             =item *
449              
450             L<Dist::Zilla::Plugin::ReportPhase>
451              
452             =item *
453              
454             L<Dist::Zilla::App::Command::dumpphases>
455              
456             =back
457              
458             =head1 SUPPORT
459              
460             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-VerifyPhases>
461             (or L<bug-Dist-Zilla-Plugin-VerifyPhases@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-VerifyPhases@rt.cpan.org>).
462              
463             There is also a mailing list available for users of this distribution, at
464             L<http://dzil.org/#mailing-list>.
465              
466             There is also an irc channel available for users of this distribution, at
467             L<C<#distzilla> on C<irc.perl.org>|irc://irc.perl.org/#distzilla>.
468              
469             I am also usually active on irc, as 'ether' at C<irc.perl.org>.
470              
471             =head1 AUTHOR
472              
473             Karen Etheridge <ether@cpan.org>
474              
475             =head1 COPYRIGHT AND LICENCE
476              
477             This software is copyright (c) 2014 by Karen Etheridge.
478              
479             This is free software; you can redistribute it and/or modify it under
480             the same terms as the Perl 5 programming language system itself.
481              
482             =cut