File Coverage

blib/lib/App/Tangerine.pm
Criterion Covered Total %
statement 48 183 26.2
branch 0 80 0.0
condition 0 32 0.0
subroutine 16 29 55.1
pod 0 12 0.0
total 64 336 19.0


line stmt bran cond sub pod time code
1             package App::Tangerine;
2             $App::Tangerine::VERSION = '0.21';
3             # ABSTRACT: Perl dependency metadata tool
4 1     1   865 use 5.010;
  1         3  
  1         35  
5 1     1   5 use strict;
  1         3  
  1         35  
6 1     1   13 use warnings;
  1         2  
  1         30  
7 1     1   339 use App::Tangerine::Metadata;
  1         1  
  1         21  
8              
9 1     1   719 use Archive::Extract;
  1         200337  
  1         69  
10 1     1   15 use Cwd;
  1         1  
  1         105  
11 1     1   876 use File::Find::Rule;
  1         7721  
  1         10  
12 1     1   774 use File::Find::Rule::Perl;
  1         6755  
  1         17  
13 1     1   911 use File::Temp;
  1         14066  
  1         137  
14 1     1   11 use File::Spec;
  1         2  
  1         24  
15 1     1   989 use Getopt::Long;
  1         13228  
  1         10  
16 1     1   1394 use List::Compare;
  1         20729  
  1         44  
17 1     1   616 use MCE::Map;
  1         37870  
  1         7  
18 1     1   1080 use Pod::Usage;
  1         39723  
  1         154  
19 1     1   545 use Tangerine;
  1         107284  
  1         1519  
20              
21             my %flags = (
22             jobs => 'auto',
23             mode => 'all',
24             );
25              
26             sub init {
27 0     0 0   GetOptions
28             all => \$flags{all},
29             compact => \$flags{compact},
30             diff => \$flags{diff},
31             files => \$flags{files},
32             'jobs=i' => \$flags{jobs},
33             help => \$flags{help},
34             'mode=s' => \$flags{mode},
35             verbose => \$flags{verbose};
36 0           my %p2uargs = (
37             -sections => 'SYNOPSIS|OPTIONS|EXAMPLES',
38             -verbose => 99);
39 0 0         unless (scalar(@ARGV)) {
40 0           pod2usage(-message => "Nothing to examine.\n",
41             -exitval => 1,
42             %p2uargs)
43             }
44 0 0 0       if ($flags{diff} && scalar(@ARGV) != 2) {
45 0           pod2usage(-message => "The diff option requires two arguments.\n",
46             -exitval => 2,
47             %p2uargs)
48             }
49 0 0 0       if ($flags{diff} && !(-e $ARGV[0] && -e $ARGV[1])) {
      0        
50 0           pod2usage(-message => "Cannot compute difference: No such file or directory.\n",
51             -exitval => 3,
52             %p2uargs)
53             }
54 0 0 0       if ($flags{mode} &&
55             $flags{mode} !~ m/^(compile|u(se)?|runtime|r(eq)?|package|p(rov)?|a(ll)?)$/) {
56 0           pod2usage(-message => "Incorrect mode specified.\n",
57             -exitval => 4,
58             %p2uargs)
59             }
60 0 0 0       if ($flags{jobs} && $flags{jobs} !~ m/^(auto|\d+)$/) {
61 0           pod2usage(-message => "The number of jobs must be a positive numeric value.\n",
62             -exitval => 5,
63             %p2uargs)
64             }
65 0 0         if ($flags{help}) {
66 0           pod2usage(%p2uargs);
67             }
68 0 0 0       if ($flags{compact} && $flags{mode} ne 'all') {
69 0           print { *STDERR }
  0            
70             "Compact mode enabled. Setting mode to `all'...\n";
71 0           $flags{mode} = 'all'
72             }
73 0 0 0       if ($flags{compact} && $flags{files}) {
74 0           print { *STDERR }
  0            
75             "Compact and files modes are incompatible. Ignoring files...\n";
76 0           $flags{files} = undef
77             }
78             MCE::Map::init {
79 0   0       max_workers => ($flags{jobs} // 'auto'),
80             chunk_size => 1
81             };
82 0 0         adjustmode() if $flags{mode};
83             }
84              
85             sub finish {
86 0     0 0   MCE::Map::finish;
87             }
88              
89             sub adjustmode {
90 0 0   0 0   $flags{mode} = 'u' if $flags{mode} =~ m/^(compile|u(se)?)$/;
91 0 0         $flags{mode} = 'r' if $flags{mode} =~ m/^(runtime|r(eq)?)$/;
92 0 0         $flags{mode} = 'p' if $flags{mode} =~ m/^(package|p(rov)?)$/;
93             }
94              
95            
96             sub analyze {
97             mce_map {
98 0     0     my @meta;
99 0           my $file = $_;
100 0           my $scanner = Tangerine->new(file => $file, mode => $flags{mode});
101 0           $scanner->run;
102 0           my %metameta = (
103             p => $scanner->provides,
104             c => $scanner->uses,
105             r => $scanner->requires
106             );
107 0           for my $metatype (keys %metameta) {
108 0           for my $mod (keys %{$metameta{$metatype}}) {
  0            
109 0           for my $occurence (@{$metameta{$metatype}->{$mod}}) {
  0            
110 0           push @meta, App::Tangerine::Metadata->new(
111             name => $mod,
112             type => $metatype,
113             file => $file,
114             line => $occurence->line,
115             version => $occurence->version
116             );
117             }
118             }
119             }
120             @meta
121 0           } @_
122 0     0 0   }
123              
124             sub gatherfiles {
125 0     0 0   my @files;
126 0 0         my $findrule = $flags{all} ?
127             File::Find::Rule->file:
128             File::Find::Rule->perl_file;
129 0           for my $arg (@_) {
130 0 0         if (-d $arg) {
    0          
131 0           push @files, $findrule->in($arg);
132             } elsif (-f $arg) {
133 0           push @files, $arg
134             } else {
135 0           print { *STDERR } "Cannot access `$arg': No such file or directory\n"
  0            
136             }
137             }
138             @files
139 0           }
140              
141             sub extract {
142 0     0 0   my ($archive, $destination) = @_;
143 0           my $ae = Archive::Extract->new(archive => $archive);
144 0           eval {
145 0           $ae->extract(to => $destination);
146             };
147 0 0         if ($@) {
148 0           print { *STDERR } "Failed to extract `$archive' to `$destination'.";
  0            
149 0           return;
150             }
151 0           return $ae->files;
152             }
153              
154             sub analyzedir {
155 0     0 0   my $dir = shift;
156 0           my $olddir = getcwd();
157 0           chdir $dir;
158 0           my @meta = analyze(gatherfiles(File::Spec->canonpath('./')));
159 0           chdir $olddir;
160             return @meta
161 0           }
162              
163             sub analyzearchive {
164 0     0 0   my $archive = shift;
165 0           my $olddir = getcwd();
166 0           my $tmpdir = File::Temp->newdir('tangerine-XXXXXX',
167             DIR => File::Spec->tmpdir());
168 0 0         my $files = extract($archive, $tmpdir->dirname) or exit 100;
169 0           my $newdir = File::Spec->catdir($tmpdir->dirname, $files->[0]);
170 0 0         $newdir = (File::Spec->splitpath($newdir))[1] if -f $newdir;
171 0           chdir $newdir;
172 0           my @meta = analyze(gatherfiles(File::Spec->canonpath('./')));
173 0           chdir $olddir;
174             return @meta
175 0           }
176              
177             sub run {
178 0     0 0   init();
179 0 0         if ($flags{diff}) {
180 0           my (@m1, @m2);
181 0 0         @m1 = -d $ARGV[0] ? analyzedir($ARGV[0]) : analyzearchive($ARGV[0]);
182 0 0         @m2 = -d $ARGV[1] ? analyzedir($ARGV[1]) : analyzearchive($ARGV[1]);
183 0           my $lc = List::Compare->new(\@m1, \@m2);
184 0           @m1 = map { assemblemd($_) } $lc->get_unique;
  0            
185 0           @m2 = map { assemblemd($_) } $lc->get_complement;
  0            
186 0           my @files;
187             {
188 0           my %tmpfiles;
  0            
189 0           $tmpfiles{$_->file} = 1 for (@m1, @m2);
190 0           @files = keys %tmpfiles
191             }
192 0           for my $file (sort @files) {
193 0           print $file."\n";
194 0           for (sortmd(undef, @m1)) {
195 0 0         print "\t- ".
    0          
196             formattype($_->type).
197             ' '.$_->name.
198             ($_->version ? ' ['.$_->version.']' : '').
199             "\n"
200             if $_->file eq $file
201             }
202 0           for (sortmd(undef, @m2)) {
203 0 0         print "\t+ ".
    0          
204             formattype($_->type).
205             ' '.$_->name.
206             ($_->version ? ' ['.$_->version.']' : '').
207             "\n"
208             if ($_->file eq $file);
209             }
210             }
211             } else {
212 0           my @meta = analyze(gatherfiles(@ARGV));
213 0 0         if ($flags{files}) {
214 0           my $lastfile = '';
215 0           for my $md (sortmd('file', @meta)) {
216 0 0         if ($md->file ne $lastfile) {
217 0           print $md->file."\n";
218 0           $lastfile = $md->file
219             }
220 0 0         print "\t".
221             formattype($md->type).
222             ' '.$md->name.
223             ' [#'.$md->line.']'.
224             ($md->version ? ' [v'.$md->version.']' : '').
225             "\n";
226             }
227             } else {
228 0           my $lastname = '';
229 0           my $skip = '';
230 0           for my $md (sortmd('name', @meta)) {
231 0 0         next if $md->name eq $skip;
232 0 0         if ($md->name ne $lastname) {
233 0           $lastname = $md->name;
234 0 0 0       if ($flags{compact} && $md->type eq 'p') {
235 0           $skip = $md->name;
236             next
237 0           }
238 0           print $md->name."\n";
239             }
240 0 0         print "\t".
241             formattype($md->type).
242             ' '.$md->file.
243             ':'.$md->line.
244             ($md->version ? ' [v'.$md->version.']' : '').
245             "\n";
246             }
247             }
248             }
249 0           finish();
250             }
251              
252             sub formattype {
253 0     0 0   my $type = shift;
254 0 0         return 'PACKAGE' if $type eq 'p';
255 0 0         return 'COMPILE' if $type eq 'c';
256 0 0         return 'RUNTIME' if $type eq 'r';
257             }
258              
259             sub assemblemd {
260 0     0 0   my ($t, $n, $v, $f) = split /\0/, shift;
261 0           App::Tangerine::Metadata->new(type => $t, name => $n, version => $v, file => $f)
262             }
263              
264             sub sortmd {
265 0     0 0   my $by = shift;
266 1     1   12 no warnings 'uninitialized';
  1         2  
  1         252  
267 0           sort {
268 0           my (@first, @second);
269 0 0         if ($by eq 'name') {
    0          
270 0           $first[0] = $a->name;
271 0           $first[1] = $b->name;
272 0           $second[0] = $a->file;
273 0           $second[1] = $b->file
274             } elsif ($by eq 'file') {
275 0           $first[0] = $a->file;
276 0           $first[1] = $b->file;
277 0           $second[0] = $a->name;
278 0           $second[1] = $b->name
279             }
280 0 0 0       $first[0] cmp $first[1] ||
    0 0        
    0          
281             ($a->type eq 'p' ? -1 :
282             ($b->type eq 'p' ? 1 :
283             $a->type cmp $b->type)) ||
284             $second[0] cmp $second[1] ||
285             $a->line <=> $b->line
286             } @_
287             }
288              
289             1;
290              
291             __END__