File Coverage

blib/lib/Test/CircularDependencies.pm
Criterion Covered Total %
statement 103 114 90.3
branch 31 52 59.6
condition 2 6 33.3
subroutine 15 15 100.0
pod 0 5 0.0
total 151 192 78.6


line stmt bran cond sub pod time code
1             package Test::CircularDependencies;
2 2     2   38887 use 5.010;
  2         6  
3 2     2   11 use strict;
  2         4  
  2         55  
4 2     2   10 use warnings;
  2         7  
  2         122  
5              
6             our $VERSION = '0.01';
7              
8             =head1 NAME
9              
10             Test::CircularDependencies - make sure non of the modules depend on themselves
11              
12             =head1 SYNOPSIS
13              
14              
15             perl -Ilib script/find-circular-dependencies.pl t/circular_dependency/my_exe.pl --dir t/circular_dependency/
16              
17             =head1 DESCRIPTION
18              
19             Given one or more scripts, modules, or directories containing those, create a data structure that represents the dependencies.
20             Allow the user to restrict the recursion to files found specific directories.
21              
22             So let's say we have several application in our company and I'd like to make sure there are no circular dependencies.
23              
24             projectA/
25             lib/A.pm
26             bin/exe.pl
27             projectB/
28             lib/
29             B.pm
30             Module/
31             C.pm
32             D.pm
33              
34             but for histoical reasons while C.pm holds 'package Module::C;' D.pm holds 'package D;' so
35             when we use this we need to
36              
37             use lib 'projectA/lib';
38             use lib 'projectB/lib';
39             use lib 'projectB/lib/Module';
40              
41             See als L<circular::require>
42              
43             =head1 AUTHOR
44              
45             L<Gabor Szabo|http://szabgab.com/>
46              
47             =head1 COPYRIGHT
48              
49             Copyright 2015 Gabor Szabo, All Rights Reserved.
50              
51             You may use, modify, and distribute this package under the
52             same terms as Perl itself.
53              
54             =cut
55              
56 2     2   17 use Carp qw(croak);
  2         4  
  2         120  
57 2     2   1818 use Data::Dumper qw(Dumper);
  2         20639  
  2         166  
58 2     2   13 use Exporter qw(import);
  2         4  
  2         58  
59 2     2   7111 use Module::CoreList ();
  2         103370  
  2         797  
60             #use Module::Path qw(module_path);
61 2     2   2331 use Perl::PrereqScanner;
  2         1305193  
  2         139  
62              
63             our @EXPORT_OK = qw(find_dependencies test_loops);
64              
65             my %depends;
66             my @loops;
67              
68             ### From here copy of functions from patched version of Module::Path
69             ### https://github.com/neilbowers/Module-Path/issues/17
70             ### remove these if that patch gets applied.
71 2     2   17 use Cwd qw/ abs_path /;
  2         6  
  2         273  
72             my $SEPARATOR;
73              
74             BEGIN {
75 2 50   2   29 if ($^O =~ /^(dos|os2)/i) {
    50          
76 0         0 $SEPARATOR = '\\';
77             } elsif ($^O =~ /^MacOS/i) {
78 0         0 $SEPARATOR = ':';
79             } else {
80 2         1741 $SEPARATOR = '/';
81             }
82             }
83              
84             sub module_path
85             {
86 6     6 0 15 my ($module, $args) = @_;
87 6         11 my $relpath;
88             my $fullpath;
89              
90 6         72 ($relpath = $module) =~ s/::/$SEPARATOR/g;
91 6 50       39 $relpath .= '.pm' unless $relpath =~ m!\.pm$!;
92              
93 6 50       28 my @inc = $args->{dirs} ? @{$args->{dirs}} : @INC;
  6         26  
94              
95             DIRECTORY:
96 6         19 foreach my $dir (@inc) {
97 6 50       19 next DIRECTORY if not defined($dir);
98              
99             # see 'perldoc -f require' on why you might find
100             # a reference in @INC
101 6 50       20 next DIRECTORY if ref($dir);
102              
103 6 50 33     283 next unless -d $dir && -x $dir;
104              
105             # The directory path might have a symlink somewhere in it,
106             # so we get an absolute path (ie resolve any symlinks).
107             # The previous attempt at this only dealt with the case
108             # where the final directory in the path was a symlink,
109             # now we're trying to deal with symlinks anywhere in the path.
110 6         12 my $abs_dir = $dir;
111 6         13 eval { $abs_dir = abs_path($abs_dir); };
  6         326  
112 6 50 33     47 next DIRECTORY if $@ || !defined($abs_dir);
113              
114 6         19 $fullpath = $abs_dir.$SEPARATOR.$relpath;
115 6 50       144 return $fullpath if -f $fullpath;
116             }
117              
118 0         0 return undef;
119             }
120             ### end of Module::Path code.
121              
122              
123             sub test_loops {
124 2     2 0 594 my ($input, $dirs, $text) = @_;
125 2         10 my @loops = find_dependencies($input, $dirs);
126              
127 2         1389 require Test::Builder;
128             # TODO check if there is a plan already and croak if there is none? or plan if there is none? $Test->plan(@_);
129 2         28 my $Test = Test::Builder->new;
130 2         25 $Test->ok( !scalar(@loops), $text );
131 2 50       1159 if (@loops) {
132 2         6 foreach my $loop (@loops) {
133 2         19 $Test->diag( "Loop found: @$loop" );
134             }
135             }
136 2         195 return not scalar @loops;
137             }
138              
139             sub find_loop {
140 22     22 0 28 my ($elem) = @_;
141 22         25 state @tree;
142 22         23 state %in_tree;
143              
144 22 100       46 if ($in_tree{$elem}) {
145 2         9 push @loops, [@tree, $elem];
146 2         6 return;
147             } else {
148 20         27 push @tree, $elem;
149 20         28 $in_tree{$elem} = 1;
150 20         22 foreach my $dep (sort keys %{$depends{$elem} }) {
  20         49  
151 14         36 find_loop($dep);
152             }
153 20         25 pop @tree;
154 20         42 delete $in_tree{$elem};
155             }
156             }
157              
158              
159             sub find_dependencies {
160 2     2 0 4 my ($inputs, $dirs, $verbose) = @_;
161              
162 2         5 @loops = ();
163 2         6 %depends = ();
164              
165 2         4 my @queue;
166              
167 2 50       7 croak "Requires at least one input.\n" if not @$inputs;
168 2         6 foreach my $inp (@$inputs) {
169 2 50       50 if (-f $inp) {
170 2         4 push @queue, $inp;
171             next
172 2         6 }
173 0 0       0 if (-d $inp) {
174 0         0 croak "Cannot handle directories yet '$inp'.\n";
175             }
176 0         0 croak "Invalid argument '$inp' (not file and not directory).\n";
177             }
178 2         47 my $scanner = Perl::PrereqScanner->new;
179 2         115633 while (@queue) {
180 10         14951 my $module = shift @queue;
181 10 100       47 next if $depends{$module};
182 8         31 $depends{$module} = {};
183 8 100       363 my $path = -f $module ? $module : module_path($module, { dirs => $dirs });
184 8 50       39 if (not $path) {
185 0         0 croak "Can't find $module\n";
186 0         0 next;
187             }
188              
189             # Huge files (eg currently Perl::Tidy) will cause PPI to barf
190             # So we need to catch those, keep calm, and carry on
191 8         16 my $prereqs = eval { $scanner->scan_file($path); };
  8         64  
192 8 50       116263 if ($@) {
193 0         0 warn $@;
194 0         0 next;
195             }
196 8         46 my $depsref = $prereqs->as_string_hash();
197 8         383 foreach my $dep (keys %{ $depsref }) {
  8         30  
198 32 100       75322 next if is_core($dep);
199 16 100       543902 next if $dep eq 'perl';
200 8 50       30 say $dep if $verbose;
201 8 50       33 die "Self dependency for '$module'?" if $module eq $dep;
202 8         41 $depends{$module}{$dep} = 1;
203 8         47 push(@queue, $dep);
204             }
205             }
206              
207             #print Dumper \%depends;
208 2         19 foreach my $root (sort keys %depends) {
209 8         23 find_loop($root);
210 8         17 delete $depends{$root}; # so we won't find the same loop multiple times.
211             }
212 2         51 return @loops;
213             }
214              
215             sub is_core
216             {
217 32     32 0 88 my $module = shift;
218 32 50       94 my $version = @_ > 0 ? shift : $^V;
219              
220 32 100       97 return 0 unless defined(my $first_release = Module::CoreList::first_release($module));
221 16 50       81543 return 0 unless $version >= $first_release;
222 16 50       87 return 1 if !defined(my $final_release = Module::CoreList::removed_from($module));
223 0           return $version <= $final_release;
224             }
225              
226              
227