File Coverage

blib/lib/Test/Inspector.pm
Criterion Covered Total %
statement 88 91 96.7
branch 25 28 89.2
condition 9 10 90.0
subroutine 12 13 92.3
pod 3 3 100.0
total 137 145 94.4


line stmt bran cond sub pod time code
1             package Test::Inspector;
2              
3             =head1 NAME
4              
5             Test::Inspector - are you testing everything?
6              
7             =head1 SYNOPSIS
8              
9             my $inspector = Test::Inspector->setup({
10             modules => [ 'Foo::Bar', 'Bar::Baz', ... ],
11             dirs => [ '/path/to/test/dir1', '/path/to/test/dir2', ... ],
12             ignore => [ 'import_from_elsewhere_method1', 'also_imported', ... ],
13             private => 1, # tests *all* methods, don't ignore ones that start _
14             });
15              
16             print $inspector->inspect;
17              
18             =head1 DESCRIPTION
19              
20             Ever been asked to write tests for an unknown codebase? A large codebase,
21             that may, or may not, have tests associated with it? How do you know if you
22             need to test a method? Is it already tested?
23              
24             This doesn't answer those questions per se. It tries to make a first best
25             stab at it for you.
26              
27             Supply a list of modules, supply a list of test directories, and we see if
28             the methods in the modules are called anywhere in those directories. It
29             doesn't mean that the tests are good, but it might help you in where to add a
30             new test, or which tests you should be running.
31              
32             If you import methods into a module, you may not want to know if they are
33             tested by your good self. That should be up to the exporting module's test
34             suite, right? Using the 'ignore' key to the hashref or args, you can say you
35             don't care about those methods. Like, say, in this itself, I use File::Find,
36             but don't really want to be worrying about if I have tested 'find' or
37             'finddepth'. Y'see?
38              
39             =head1 METHODS
40              
41             =cut
42              
43 1     1   660 use strict;
  1         2  
  1         31  
44 1     1   4 use warnings;
  1         3  
  1         42  
45              
46             our $VERSION = '0.03';
47              
48 1     1   4 use File::Find;
  1         1  
  1         65  
49 1     1   807 use lib '/Users/mkerr/code';
  1         622  
  1         5  
50              
51             =head2 setup
52              
53             my $inspector = Test::Inspector->setup({
54             modules => [ 'Foo::Bar', 'Bar::Baz', ... ],
55             dirs => [ '/path/to/test/dir1', '/path/to/test/dir2', ... ],
56             ignore => [ 'import_from_elsewhere_method1', 'also_imported', ... ],
57             private => 1, # tests *all* methods, don't ignore ones that start _
58             });
59              
60             Set the Inspector up with some modules and directories. Both passed in as
61             listrefs in the keys of the hashref.
62              
63             =cut
64              
65             sub _module_methods {
66 2     2   6 my ($class, $private, @modules) = @_;
67 2         2 my %stuff;
68 1     1   111 no strict 'refs';
  1         1  
  1         839  
69 2         4 for my $module (@modules) {
70 4         222 eval "require $module";
71 4         21 $module->import();
72 4         5 for my $what (%{*{"$module\::"}}) {
  4         5  
  4         21  
73 78 100       257 next unless $what =~ m/$module/;
74 39         267 (my $meth = $what) =~ s/^\*$module\:\://;
75 39 100       192 next unless $module->can($meth);
76 29 100 100     89 next if $meth =~ /^_/ && $private;
77 25         63 $stuff{$module}{$meth}++;
78             }
79             }
80 2         26 return %stuff;
81             }
82              
83             sub _find_tests {
84 2     2   4 my ($class, @dirs) = @_;
85 2         2 my @test_files;
86             my $wanted = sub {
87 16 100   16   550 push @test_files, $File::Find::name if $_ =~ m/\.(pl|pm|t)$/
88 2         10 };
89 2         191 find($wanted, @dirs);
90 2         16 return 'test_files', [ @test_files ];
91             }
92              
93             sub setup {
94 2     2 1 373 my ($class, $info) = @_;
95 2 50       12 die "Incorrect args" unless ref $info eq 'HASH';
96 2         8 bless {
97 2         11 $class->_find_tests(@{ $info->{dirs} }),
98 2 100       3 $class->_module_methods($info->{private} ? 1 : 0, @{ $info->{modules} }),
99             %$info,
100             }, $class;
101             }
102              
103             =head2 inspect
104              
105             my %report = $inspector->inspect;
106              
107             This will inspect the tests to see if all the methods in the module were
108             referenced in any way.
109              
110             =cut
111              
112             sub _check {
113 20     20   39 my ($self, $mod, $file) = @_;
114 20         27 my $methods = join '|', keys %{ $self->{$mod} };
  20         108  
115 20         35 my (%results, $use);
116 20 50       728 open FILE, '<', $file or return;
117 20         459 for my $line () {
118 384 100       2213 $results{'__is_used__'}++ if $line =~ m/(use|use_ok|require|require_ok).*$mod/;
119 384         431 for my $meth (keys %{ $self->{$mod} }) {
  384         1066  
120 2400   100     6539 $results{$meth} ||= 0;
121 2400 100       16954 next unless $line =~ m/$meth/;
122 46         81 $results{$meth}++;
123             }
124             }
125 20         312 close FILE;
126 20         222 return %results;
127             }
128              
129 0     0   0 sub _results { %{ shift->{results} } }
  0         0  
130              
131             sub inspect {
132 2     2 1 3 my $self = shift;
133 2 50       12 return %{ $self->{results} } if exists $self->{results};
  0         0  
134 2         3 my %results;
135 2         4 for my $test_script (@{ $self->{test_files} }) {
  2         5  
136 10         15 for my $module (@{ $self->{modules} }) {
  10         25  
137 20         61 $results{$module}{$test_script} = { $self->_check($module, $test_script) };
138             }
139             }
140 2         11 $self->{results} = { %results };
141 2         13 return %results;
142             }
143              
144             =head2 pretty_report
145              
146             print $inspector->pretty_report;
147              
148             As it says, this is pretty report. The output looks like:
149              
150             Module::Name
151             test_script_name
152             method_name1 => FOUND
153             method_name2 => NOT FOUND
154             ...
155              
156             OK, so it is a report, not that pretty. If you want to know how much time I
157             spent on this in total...wait! come back!
158              
159             =cut
160              
161             sub pretty_report {
162 2     2 1 3 my $self = shift;
163 2         7 my %results = $self->inspect;
164 2 100       5 my $ignore = join "|", @{ $self->{ignore} || [] }, '__is_used__';
  2         14  
165 2         13 for my $module (sort keys %results) {
166 4         48 print "$module\n";
167 4         6 for my $test_script (sort keys %{ $results{$module} }) {
  4         19  
168 20         116 print "\t$test_script\n";
169 20         37 my ($found, $not, $status) = (0, 0, '');
170 20         21 for my $method (sort keys %{ $results{$module}{$test_script} }) {
  20         102  
171 81 100       197 do {
172 12         55 print "\t\t$module not used in this script\n";
173 12         15 last;
174             } unless exists $results{$module}{$test_script}{'__is_used__'};
175 69 100 66     876 next if $ignore && $method =~ m/$ignore/;
176 55 100       116 if ($results{$module}{$test_script}{$method}) {
177 20         23 $status = 'FOUND'; $found++;
  20         21  
178             } else {
179 35         44 $status = 'NOT FOUND'; $not++;
  35         41  
180             }
181 55         266 print "\t\t$method $status\n";
182             }
183 20   100     72 my $denom = ($found + $not) || 1;
184 20         348 printf "\t\tfound: %d not found %d (%.2f%%)\n\n",
185             $found, $not, ($found / $denom) * 100;
186             }
187             }
188             }
189              
190             =head1 NOTES
191              
192             Look, you might have worked out, this is a first-pass attempt, it is dumb,
193             could probably be done better using other modules, yadayadayada. It isn't
194             meant to be the One True Answer for checking to make sure you have tested all
195             your methods. It is a crude tool used to aid you somewhat. It might do, it
196             might not, if it doesn't, then use something else!
197              
198             =head1 BUGS
199              
200             When you use the script itself to try and self-test, it all gets a bit
201             self-referential doesn't it? Probably not the best code I have ever written,
202             but probably more useful that all the other stuff. Who am I kidding?
203              
204             =head1 TODO
205              
206             o Stuff, no doubt. This did what I wanted it to do, in a crude way.
207              
208             =head1 AUTHOR
209              
210             (c) Stray Toaster 2007.
211              
212             =cut
213              
214             return qw/The light bulbs burn and her fingers will learn/;