File Coverage

blib/lib/CPAN/Mini/Visit/Simple.pm
Criterion Covered Total %
statement 66 177 37.2
branch 8 88 9.0
condition 1 18 5.5
subroutine 19 29 65.5
pod 10 10 100.0
total 104 322 32.3


line stmt bran cond sub pod time code
1             package CPAN::Mini::Visit::Simple;
2 6     6   30556 use 5.010;
  6         14  
3 6     6   20 use strict;
  6         7  
  6         122  
4 6     6   24 use warnings;
  6         11  
  6         271  
5              
6             our $VERSION = '0.015';
7             $VERSION = eval $VERSION; ## no critic
8              
9 6     6   3385 use Archive::Extract;
  6         774017  
  6         208  
10 6     6   39 use Carp;
  6         9  
  6         344  
11 6     6   2858 use CPAN::Mini ();
  6         530465  
  6         140  
12 6     6   43 use Cwd;
  6         8  
  6         399  
13 6     6   29 use File::Basename qw( dirname basename );
  6         7  
  6         263  
14 6     6   24 use File::Find;
  6         6  
  6         239  
15 6     6   66 use File::Spec;
  6         6  
  6         112  
16 6     6   21 use File::Temp;
  6         7  
  6         371  
17 6     6   2710 use Path::Class;
  6         73857  
  6         310  
18 6     6   34 use Scalar::Util qw( reftype );
  6         7  
  6         244  
19 6         6093 use CPAN::Mini::Visit::Simple::Auxiliary qw(
20             $ARCHIVE_REGEX
21             dedupe_superseded
22             get_lookup_table
23             normalize_version_number
24 6     6   2740 );
  6         9  
25              
26             sub new {
27 5     5 1 6112 my ($class, $args) = @_;
28 5         9 my %data = ();
29 5 100       12 if ( ! $args->{minicpan} ) {
30             # Work around a bug in CPAN::Mini:
31             # https://rt.cpan.org/Public/Bug/Display.html?id=55272
32 2         12 my $config_file = CPAN::Mini->config_file({});
33 2 50 33     57 croak "CPAN::Mini config file not located: $!"
34             unless ( defined $config_file and -e $config_file );
35 2         8 my %config = CPAN::Mini->read_config;
36 2 50       264 if ( $config{local} ) {
37 2         6 $data{minicpan} = $config{local};
38             }
39             }
40             else {
41 3         6 $data{minicpan} = $args->{minicpan};
42             }
43             croak "Directory $data{minicpan} not found"
44 5 100       255 unless (-d $data{minicpan});
45              
46 4         26 my $id_dir = File::Spec->catdir($data{minicpan}, qw( authors id ));
47 4 100       122 croak "Absence of $id_dir implies no valid minicpan"
48             unless -d $id_dir;
49 3         6 $data{id_dir} = $id_dir;
50              
51 3         7 my $self = bless \%data, $class;
52 3         7 return $self;
53             }
54              
55             sub get_minicpan {
56 1     1 1 206 my $self = shift;
57 1         9 return $self->{minicpan};
58             }
59              
60             sub get_id_dir {
61 1     1 1 175 my $self = shift;
62 1         2 return $self->{id_dir};
63             }
64             sub identify_distros {
65 0     0 1   my ($self, $args) = @_;
66              
67             croak "Bad argument 'list' provided to identify_distros()"
68 0 0         if exists $args->{list};
69              
70 0 0         if ( defined $args->{start_dir} ) {
71             croak "Directory $args->{start_dir} not found"
72 0 0         unless (-d $args->{start_dir} );
73             croak "Directory $args->{start_dir} must be subdirectory of $self->{id_dir}"
74 0 0         unless ( $args->{start_dir} =~ m/\Q$self->{id_dir}\E/ );
75 0           $self->{start_dir} = $args->{start_dir};
76             }
77             else {
78 0           $self->{start_dir} = $self->{id_dir};
79             }
80              
81 0 0         if ( defined $args->{pattern} ) {
82             croak "'pattern' is a regex, which means it must be a REGEXP ref"
83 0 0         unless (reftype($args->{pattern}) eq 'REGEXP');
84             }
85              
86 0           my $found_ref = $self->_search_from_start_dir( $args );
87 0           $self->{list} = dedupe_superseded( $found_ref );
88 0           return 1;
89             }
90              
91             sub identify_distros_from_derived_list {
92 0     0 1   my ($self, $args) = @_;
93             croak "Bad argument 'start_dir' provided to identify_distros_from_derived_list()"
94 0 0         if exists $args->{start_dir};
95             croak "Bad argument 'pattern' provided to identify_distros_from_derived_list()"
96 0 0         if exists $args->{pattern};
97             croak "identify_distros_from_derived_list() needs 'list' element"
98 0 0         unless exists $args->{list};
99             croak "Value of 'list' must be array reference"
100 0 0         unless reftype($args->{list}) eq 'ARRAY';
101             croak "Value of 'list' must be non-empty"
102 0 0         unless scalar(@{$args->{list}});
  0            
103 0           $self->{list} = dedupe_superseded( $args->{list} );
104 0           return 1;
105             }
106              
107             sub _search_from_start_dir {
108 0     0     my ($self, $args) = @_;
109 0           my @found = ();
110             find(
111             {
112             follow => 0,
113             no_chdir => 1,
114 0     0     preprocess => sub { my @files = sort @_; return @files },
  0            
115             wanted => sub {
116 0 0   0     return unless /$ARCHIVE_REGEX/;
117 0 0         if ( defined $args->{pattern} ) {
118 0 0         return unless $_ =~ m/$args->{pattern}/;
119             }
120 0           push @found, File::Spec->canonpath($File::Find::name);
121             },
122             },
123             $self->{start_dir},
124 0           );
125 0           return \@found;
126             }
127              
128             sub say_list {
129 0     0 1   my ($self, $args) = @_;
130 0 0         if (not defined $args) {
131 0           say $_ for @{$self->{list}};
  0            
132             }
133             else {
134 0 0         croak "Argument must be hashref" unless reftype($args) eq 'HASH';
135 0 0         croak "Need 'file' element in hashref" unless exists $args->{file};
136             open my $FH, '>', $args->{file}
137 0 0         or croak "Unable to open handle to $args->{file} for writing";
138 0           say $FH $_ for @{$self->{list}};
  0            
139 0 0         close $FH
140             or croak "Unable to close handle to $args->{file} after writing";
141             }
142             }
143              
144             sub get_list {
145 0     0 1   my ($self) = @_;
146 0 0         return unless defined $self->{list};
147 0           return @{$self->{list}};
  0            
148             }
149              
150             sub get_list_ref {
151 0     0 1   my ($self) = @_;
152 0 0         return unless defined $self->{list};
153 0           return $self->{list};
154             }
155              
156             sub refresh_list {
157 0     0 1   my ($self, $args) = @_;
158             croak "Need 'derived_list' whose value is list of distributions needing refreshment"
159 0 0         unless exists $args->{derived_list};
160             croak "Value of 'derived_list' must be array reference"
161 0 0         unless reftype( $args->{derived_list} ) eq 'ARRAY';
162              
163             # Call identify_distros() with all arguments except 'derived_list',
164             # i.e., with 'start_dir' and/or 'pattern'.
165 0           my %reduced_args = map { $_ => 1 } @{ $args->{derived_list} };
  0            
  0            
166 0           delete $reduced_args{derived_list};
167 0           my $rv = $self->identify_distros( \%reduced_args );
168              
169             # So now we have an updated primary list ($self->{list}).
170             # We will need to make a hash out of that where they key is the stem of
171             # the distribution name and the value is the version.
172             # We will make a similar hash from the derived list.
173              
174 0           my $primary = get_lookup_table( $self->get_list_ref() );
175 0           my $derived = get_lookup_table( $args->{derived_list} );
176              
177 0           foreach my $stem ( keys %{$derived} ) {
  0            
178 0 0         if ( not exists $primary->{$stem} ) {
    0          
179 0           delete $derived->{$stem};
180             }
181             elsif ( $primary->{$stem}{version} > $derived->{$stem}{version} ) {
182 0           $derived->{$stem}{version} = $primary->{$stem}{version};
183 0           $derived->{$stem}{distro} = $primary->{$stem}{distro};
184             }
185             else {
186             # nothing to do
187             }
188             }
189              
190 0           return [ sort map { $derived->{$_}{distro} } keys %{$derived} ];
  0            
  0            
191             }
192              
193             sub visit {
194 0     0 1   my ($self, $args) = @_;
195 6     6   32 no warnings 'once';
  6         9  
  6         264  
196 0           local $Archive::Extract::PREFER_BIN = 1;
197 6     6   21 use warnings 'once';
  6         6  
  6         2454  
198 0 0         local $Archive::Extract::WARN = $args->{quiet} ? 0 : 1;
199             croak "Must have a list of distributions on which to take action"
200 0 0         unless defined $self->{list};
201             croak "'visit()' method requires 'action' subroutine reference"
202             unless (
203             ( defined ($args->{action}) )
204             and
205             ( defined reftype($args->{action}) )
206             and
207 0 0 0       ( reftype($args->{action}) eq 'CODE' )
      0        
208             );
209 0           my @action_args = ();
210 0 0         if ( defined $args->{action_args} ) {
211             croak "'action_args' must be array reference"
212             unless (
213             ( defined reftype($args->{action_args}) )
214             and
215 0 0 0       ( reftype($args->{action_args}) eq 'ARRAY' )
216             );
217 0           @action_args = @{ $args->{action_args} };
  0            
218             }
219 0           my $here = cwd();
220 0           LIST: foreach my $distro ( @{$self->{list}} ) {
  0            
221 0           my $proper_distro = q{};
222 0           my $real_id_dir = $self->get_id_dir();
223 0 0         if ( $distro =~ m|\Q$real_id_dir\E| ) {
224 0           $proper_distro = basename($distro);
225             }
226              
227 0           my $olderr;
228             # stderr > /dev/null if quiet
229 0 0         if ( not $Archive::Extract::WARN ) {
230 0           open $olderr, ">&STDERR";
231 0           open STDERR, ">", File::Spec->devnull;
232             }
233 0           my $tdir = File::Temp->newdir();
234 0 0         chdir $tdir or croak "Unable to change to temporary directory";
235 0           my $ae = Archive::Extract->new( archive => $distro );
236 0 0         my $extract_ok = $ae->extract( to => $tdir ) or do {
237 0           warn "Unable to extract $distro; skipping";
238 0 0         if ( not $Archive::Extract::WARN ) {
239 0           open STDERR, ">&", $olderr;
240 0           close $olderr;
241             }
242 0           next LIST;
243             };
244              
245             # restore stderr if quiet
246 0 0         if ( not $Archive::Extract::WARN ) {
247 0           open STDERR, ">&", $olderr;
248 0           close $olderr;
249             }
250             # Note: It's not clear what would cause $extract_ok to be false.
251             # Things that are not valid archives appear to be caught by
252             # Archive::Extract::new() and rendered as fatal. So following block
253             # is unlikely to be covered by test suite.
254 0 0 0       if ( ( not $extract_ok ) and $Archive::Extract::WARN ) {
255 0           carp "Couldn't extract '$distro'";
256 0           return;
257             }
258             # most distributions unpack a single directory that we must enter
259             # but some behave poorly and unpack to the current directory
260 0           my $dir = Path::Class::Dir->new();
261 0           my @children = $dir->children;
262 0 0 0       if ( ( @children == 1 ) and ( -d $children[0] ) ) {
263 0           chdir $children[0];
264             }
265              
266 0           &{$args->{action}}($proper_distro, @action_args);# execute command
  0            
267 0 0         chdir $here or croak "Unable to change back to starting point";
268             }
269 0           return 1;
270             }
271              
272             1;