File Coverage

blib/lib/Test/CircularDependencies.pm
Criterion Covered Total %
statement 104 121 85.9
branch 33 58 56.9
condition 2 6 33.3
subroutine 15 15 100.0
pod 0 5 0.0
total 154 205 75.1


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