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   14422455 use strict;
  6         18  
  6         208  
2 6     6   38 use warnings;
  6         13  
  6         400  
3             package Dist::Zilla::Plugin::VerifyPhases; # git description: v0.014-6-ge0c81aa
4             # vim: set ts=8 sts=4 sw=4 tw=115 et :
5             # ABSTRACT: Compare data and files at different phases of the distribution build process
6             # KEYWORDS: plugin distribution configuration phase verification validation
7              
8             our $VERSION = '0.015';
9              
10 6     6   35 use Moose;
  6         13  
  6         127  
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   39700 use Moose::Util 'find_meta';
  6         15  
  6         48  
19 6     6   1260 use Digest::MD5 'md5_hex';
  6         14  
  6         445  
20 6     6   40 use List::Util 1.33 qw(none any);
  6         157  
  6         376  
21 6     6   1302 use Term::ANSIColor 3.00 'colored';
  6         11962  
  6         1084  
22 6     6   48 use Storable 'dclone';
  6         14  
  6         292  
23 6     6   3064 use Test::Deep::NoTest qw(cmp_details deep_diag);
  6         1039  
  6         28  
24 6     6   1200 use namespace::autoclean;
  6         16  
  6         65  
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   54 my ($self, $file) = @_;
33              
34 22         69 for my $filename (keys %all_files)
35             {
36 43         73 foreach my $index (0 .. $#{$all_files{$filename}})
  43         116  
37             {
38 32 100       156 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 18971 my $self = shift;
67 6         171 my $zilla = $self->zilla;
68 6         72 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         80 my $attr = $meta->find_attribute_by_name($_);
  60         4810  
74 60 100 66     4224 $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 435957 my $self = shift;
95              
96             # adjust plugin order so that we are always last!
97 6         173 my $plugins = $self->zilla->plugins;
98 6         174 @$plugins = ((grep { $_ != $self } @$plugins), $self);
  74         159  
99             }
100              
101             sub gather_files
102             {
103 6     6 0 64139 my $self = shift;
104              
105 6         151 my $zilla = $self->zilla;
106 6         71 my $meta = find_meta($zilla);
107              
108 6         116 foreach my $attr_name (qw(name version release_status abstract main_module authors distmeta))
109             {
110 42 100       2603 next if exists $zilla_constructor_args{$attr_name};
111 27         175 my $attr = $meta->find_attribute_by_name($attr_name);
112 27 100 66     2041 $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   83 not exists $zilla_constructor_args{$_}
121             and $meta->find_attribute_by_name($_)->has_value($zilla)
122 6 100       491 } qw(_license_class _copyright_holder _copyright_year);
123              
124             # all files should have been added by now. save their filenames/objects
125 6         825 foreach my $file (@{$zilla->files})
  6         170  
126             {
127 9         217 push @{ $all_files{$file->name} }, {
  9         36  
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: not ok
137             # files removed: not ok
138             # files renamed: not ok
139             # encoding changed: ok to now; no from now on
140             # contents: ignore
141             sub set_file_encodings
142             {
143 6     6 0 9482 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         17 foreach my $file (@{$self->zilla->files})
  6         169  
148             {
149 9         242 foreach my $entry (@{ $all_files{$file->name} })
  9         35  
150             {
151 11 100       627 $entry->{encoding} = $file->encoding if $entry->{object} eq $file;
152             }
153             }
154             }
155              
156             # since last phase,
157             # new files added: not ok
158             # files removed: ok to now; not ok from now on
159             # files renamed: not ok
160             # encoding changed: not ok
161             # contents: ignore
162             sub prune_files
163             {
164 6     6 0 9711 my $self = shift;
165              
166             # remove all still-existing files from our tracking list
167 6         18 foreach my $file (@{$self->zilla->files})
  6         149  
168             {
169 8         159 my ($filename, $index) = $self->_search_all_files($file);
170 8 50 33     60 if ($filename and defined $index)
171             {
172             # file has been renamed - an odd time to do this
173 8 50       32 $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         345 splice @{ $all_files{$filename} }, $index, 1;
  8         28  
178 8         28 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         73 %all_files = ();
189 6         24 foreach my $file (@{$self->zilla->files})
  6         137  
190             {
191 8         383 push @{ $all_files{$file->name} }, {
  8         30  
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: not ok
203             # files removed: not ok
204             # files renamed: allowed
205             # encoding changed: not ok
206             # record contents: ok to now; not ok from now on
207             # distmeta changed: ok to now; not ok from now on
208             # no prerequisites have been added yet
209             sub munge_files
210             {
211 5     5 0 17973 my $self = shift;
212              
213             # remove all still-existing files from our tracking list
214 5         13 foreach my $file (@{$self->zilla->files})
  5         122  
215             {
216 7         130 my ($filename, $index) = $self->_search_all_files($file);
217 7 100 66     49 if ($filename and defined $index)
218             {
219             # the file may have been renamed - but this is okay by a file munger
220 6         14 splice @{ $all_files{$filename} }, $index, 1;
  6         17  
221 6         21 next;
222             }
223              
224             # this is a new file we haven't seen before.
225 1         5 $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         404 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         339 foreach @{ $all_files{$filename} };
  7         50  
235             }
236              
237             # capture full file list all over again, recording contents now.
238 5         44 %all_files = ();
239 5         24 foreach my $file (@{$self->zilla->files})
  5         125  
240             {
241             # don't force FromCode files to calculate early; it might fire some
242             # lazy attributes prematurely
243 7 50       1356 push @{ $all_files{$file->name} }, {
  7         30  
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         2443 my $prereq_attr = find_meta($self->zilla)->find_attribute_by_name('prereqs');
255 5 100 66     746 $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         708 $distmeta = dclone($self->zilla->distmeta);
262 5 100       143112 if (exists $distmeta->{prereqs})
263             {
264 1         741 require Data::Dumper;
265             $self->_alert('prereqs have been improperly included with distribution metadata:',
266 1         4553 Data::Dumper->new([ $distmeta->{prereqs} ])->Indent(2)->Terse(1)->Sortkeys(1)->Dump,
267             );
268 1         419 delete $distmeta->{prereqs};
269             }
270             }
271              
272             # since last phase,
273             # new files added: not ok
274             # files removed: not ok
275             # files renamed: not ok
276             # change contents: not ok
277             # distmeta has not changed
278             sub after_build
279             {
280 5     5 0 76649 my $self = shift;
281              
282 5         16 foreach my $file (@{$self->zilla->files})
  5         151  
283             {
284 7         245 my ($filename, $index) = $self->_search_all_files($file);
285 7 100 66     50 if (not $filename or not defined $index)
286             {
287 1         5 $self->_alert('file has been added after file gathering phase: \'' . $file->name
288             . '\' (' . $file->added_by . ')');
289 1         337 next;
290             }
291              
292 6 100       31 if ($filename ne $file->name)
293             {
294 1         41 $self->_alert('file has been renamed after munging phase: \'' . $file->name
295             . "' (originally '$filename', " . $file->added_by . ')');
296 1         488 splice @{ $all_files{$filename} }, $index, 1;
  1         5  
297 1         5 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   219 and none { $file->name eq $_ } $self->skip_file
308 5 50 66     543 and $all_files{$file->name}[$index]{content} ne md5_hex($file->encoded_content);
      66        
309              
310 5         751 delete $all_files{$file->name};
311             }
312              
313 5         198 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         4 foreach @{ $all_files{$filename} };
  2         40  
318             }
319              
320             # check distmeta, minus prereqs
321 5         529 my $new_distmeta = dclone($self->zilla->distmeta);
322 5         558 delete $new_distmeta->{prereqs};
323 5         240 foreach my $ignore_key ($self->skip_distmeta)
324             {
325 5         35 $distmeta->{$ignore_key} = Test::Deep::ignore;
326 5 100       7390 delete $distmeta->{$ignore_key} if not exists $new_distmeta->{$ignore_key};
327             }
328 5         29 my ($ok, $stack) = cmp_details($new_distmeta, $distmeta);
329 5 100       79223 if (not $ok)
330             {
331 1         6 chomp(my $error = deep_diag($stack));
332 1         136 $self->_alert('distribution metadata has been altered after munging phase!', $error);
333             }
334             }
335              
336             sub _alert
337             {
338 16     16   977 my $self = shift;
339 16         86 $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.015
357              
358             =head1 SYNOPSIS
359              
360             At the end of 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             =back
410              
411             Running at the end of the C<-EncodingProvider> phase, it forces all encodings
412             to be built (by calling their lazy builders), to use their C<SetOnce> property
413             to ensure that no subsequent phase attempts to alter a file encoding.
414              
415             Running at the end of the C<-FilePruner> phase, it verifies that no additional
416             files have been added to the distribution, nor renamed, since the
417             C<-FileGatherer> phase.
418              
419             Running at the end of the C<-FileMunger> phase, it verifies that no additional
420             files have been added to nor removed from the distribution, nor renamed, since
421             the C<-FilePruner> phase; and that no prerequisites have yet been provided.
422             Additionally, it verifies that the prerequisite list has not yet been read
423             from, when possible.
424              
425             Running at the end of the C<-AfterBuild> phase, the full state of all files
426             are checked: files may not be added, removed, renamed nor had their content
427             change. Additionally, it verifies that no distribution metadata (with the
428             exception of prerequisites) has changed since the end of the C<-FileMunger>
429             phase.
430              
431             =for stopwords FromCode
432              
433             Currently, L<FromCode|Dist::Zilla::File::FromCode> files are not checked for
434             content, as interesting side effects can occur if their content subs are run
435             before all content is available (for example, other lazy builders can run too
436             early, resulting in incomplete or missing data).
437              
438             =for Pod::Coverage BUILD before_build gather_files set_file_encodings prune_files munge_files after_build
439              
440             =head1 SEE ALSO
441              
442             =over 4
443              
444             =item *
445              
446             L<Dist::Zilla::Plugin::ReportPhase>
447              
448             =item *
449              
450             L<Dist::Zilla::App::Command::dumpphases>
451              
452             =back
453              
454             =head1 SUPPORT
455              
456             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-VerifyPhases>
457             (or L<bug-Dist-Zilla-Plugin-VerifyPhases@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-VerifyPhases@rt.cpan.org>).
458              
459             There is also a mailing list available for users of this distribution, at
460             L<http://dzil.org/#mailing-list>.
461              
462             There is also an irc channel available for users of this distribution, at
463             L<C<#distzilla> on C<irc.perl.org>|irc://irc.perl.org/#distzilla>.
464              
465             I am also usually active on irc, as 'ether' at C<irc.perl.org>.
466              
467             =head1 AUTHOR
468              
469             Karen Etheridge <ether@cpan.org>
470              
471             =head1 COPYRIGHT AND LICENCE
472              
473             This software is copyright (c) 2014 by Karen Etheridge.
474              
475             This is free software; you can redistribute it and/or modify it under
476             the same terms as the Perl 5 programming language system itself.
477              
478             =cut