File Coverage

lib/UR/Namespace/Command/Test/Callcount.pm
Criterion Covered Total %
statement 15 79 18.9
branch 0 30 0.0
condition 0 14 0.0
subroutine 5 10 50.0
pod 0 3 0.0
total 20 136 14.7


line stmt bran cond sub pod time code
1             package UR::Namespace::Command::Test::Callcount;
2              
3 1     1   24 use warnings;
  1         2  
  1         38  
4 1     1   3 use strict;
  1         2  
  1         18  
5 1     1   3 use IO::File;
  1         2  
  1         181  
6 1     1   4 use File::Find;
  1         1  
  1         44  
7 1     1   4 use UR;
  1         2  
  1         6  
8             our $VERSION = "0.46"; # UR $VERSION;
9              
10             UR::Object::Type->define(
11             class_name => __PACKAGE__,
12             is => "UR::Namespace::Command::Base",
13             has => [
14             'sort' => { is => 'String', valid_values => ['count', 'sub'], default_value => 'count',
15             doc => 'The output file should be sorted by "count" (sub call counts) or "sub" (sub names)' },
16             ],
17             has_optional => [
18             input => { is => 'ARRAY', doc => 'list of input file pathnames' },
19             output => { is => 'String', doc => 'pathname of the output file' },
20             bare_args => {
21             is_many => 1,
22             shell_args_position => 1
23             }
24             ],
25             );
26              
27 0     0 0   sub help_brief { "Collect the data from a prior 'ur test run --callcount' run into a single output file" }
28              
29             sub help_synopsis {
30             return <
31             cd MyNamespace
32             ur test run --callcount # run tests and generate *.callcount files
33             ur test callcount --output all.callcount # collect all *.callcount info in the current tree
34              
35             # Collect results from only 2 files and print results to STDOUT
36             ur test callcount t/test_1.callcount t/test_2.callcount
37             EOS
38 0     0 0   }
39              
40             sub help_detail {
41             return <
42             This command collects the data in *.callcount files (generated when tests are
43             run with the 'ur test run --callcount' command), combines like data among
44             them, and writes a new callcount file with the collected data.
45              
46             Input files can be specified on the command line, and the default is to find
47             all *.callcount files in the current directory tree. The output file can
48             be specified with the --output option, or prints its results to STDOUT
49             by default.
50             EOS
51 0     0 0   }
52              
53             sub execute {
54              
55             #$DB::single = 1;
56 0     0     my $self = shift;
57              
58             # First, handle all the different ways input files/directories are
59             # handled
60 0           my @input;
61 0           my $inputs = $self->input;
62 0 0 0       if ($inputs and ref($inputs) eq 'ARRAY') {
    0 0        
    0          
63 0           @input = @$inputs;
64             } elsif ($inputs and $inputs =~ m/,/) {
65 0           @input = split(',',$inputs);
66             } elsif (!$inputs) {
67 0           @input = $self->bare_args;
68 0 0         @input = ('.') unless @input; # when no inputs at all are given, start with '.'
69             } else {
70 0           $self->error_message("Couldn't determine input files and directories");
71 0           return;
72             }
73              
74             # Now, flatten out everything in @input by searching in directories
75             # for *.callcount files
76 0           my(@directories, %input_files);
77 0           foreach (@input) {
78 0 0         if (-d $_) {
79 0           push @directories, $_;
80             } else {
81 0           $input_files{$_} = 1;
82             }
83             }
84 0 0         if (@directories) {
85             my $wanted = sub {
86 0 0   0     if ($File::Find::name =~ m/.callcount$/) {
87 0           $input_files{$File::Find::name} = 1;
88             }
89 0           };
90 0           File::Find::find($wanted, @directories);
91             }
92              
93 0           my $out_fh;
94 0 0 0       if ($self->output and $self->output eq '-') {
    0          
95 0           $out_fh = \*STDOUT;
96             } elsif ($self->output) {
97 0           my $output = $self->output;
98 0           $out_fh = IO::File->new($output, 'w');
99 0 0         unless ($out_fh) {
100 0           $self->error_message("Can't open $output for writing: $!");
101 0           return undef;
102             }
103             }
104              
105              
106 0           my %data;
107 0           foreach my $input_file ( keys %input_files ) {
108 0           my $in_fh = IO::File->new($input_file);
109 0 0         unless ($in_fh) {
110 0           $self->error_message("Can't open $input_file for reading: $!");
111 0           next;
112             }
113              
114 0           while(<$in_fh>) {
115 0           chomp;
116 0           my($count, $subname, $subloc, $callers) = split(/\t/, $_, 4);
117 0   0       $callers ||= '';
118              
119 0           my %callers;
120 0           foreach my $caller ( split(/\t/, $callers ) ) {
121 0           $callers{$caller} = 1;
122             }
123            
124 0 0         if (exists $data{$subname}) {
125 0           $data{$subname}->[0] += $count;
126 0           foreach my $caller ( keys %callers ) {
127 0           $data{$subname}->[3]->{$caller} = 1;
128             }
129             } else {
130 0           $data{$subname} = [ $count, $subname, $subloc, \%callers];
131             }
132             }
133 0           $in_fh->close();
134             }
135              
136 0           my @order;
137 0 0 0       if ($self->sort eq 'count') {
    0          
138 0           @order = sort { $a->[0] <=> $b->[0] } values %data;
  0            
139             } elsif ($self->sort eq 'sub' or $self->sort eq 'subs') {
140 0           @order = sort { $a->[1] cmp $b->[1] } values %data;
  0            
141             }
142              
143 0 0         if ($out_fh) {
144 0           foreach ( @order ) {
145 0           my $callers = join("\t", keys %{$_->[3]}); # convert the callers back into a \t sep string
  0            
146 0           $out_fh->print(join("\t",@{$_}[0..2], $callers), "\n");
  0            
147             }
148 0           $out_fh->close();
149             }
150              
151 0           return \@order;
152             }
153              
154            
155             1;
156              
157             =pod
158              
159             =head1 NAME
160              
161             B - collect callcount data from running tests into one file
162              
163             =head1 SYNOPSIS
164              
165             # run tests in a given namespace
166             cd my_sandbox/TheApp
167             ur test run --recurse --callcount
168              
169             ur test callcount --output all_tests.callcount
170              
171             =head1 DESCRIPTION
172              
173             Callcount data can be used to find unused subroutines in your code. When
174             the test suite is run with the C option, then for each *.t file
175             run by the test suite, a corresponding *.callcount file is created containing
176             information about how often all the defined subroutines were called.
177              
178             The callcount file is a plain text file with three columns:
179              
180             =over 4
181              
182             =item 1.
183              
184             The number of times this subroutine was called
185              
186             =item 2.
187              
188             The name of the subroutine
189              
190             =item 3.
191              
192             Where in the code this subroutine is defined
193              
194             =back
195              
196             After a test suite run with sufficient coverage, subroutines with 0 calls
197             are candidates for removal, and subs with high call counts are candidates
198             for optimization.
199              
200             =head1 OPTIONS
201              
202             =over 4
203              
204             =item --input
205              
206             Name the *.callcount input file(s). When run from the command line, it
207             accepts a list of files separated by ','s. Input files can also be given
208             as plain, unnamed command line arguments (C). When run as a
209             command module within another program, the C) property can be an
210             arrayref of pathanmes.
211              
212             After inputs are determined, any directories given are expanded by searching
213             them recursively for files ending in .callcount with L.
214              
215             If no inputs in any form are given, then it defaults to '.', the current
216             directory, which means all *.callcount files under the current directory
217             are used.
218              
219             =item --output
220              
221             The pathname to write the collected data to. The user may use '-' to print
222             the results to STDOUT.
223              
224             =item --sort
225              
226             How the collected results should be sorted before being reported. The
227             default is 'count', which sorts incrementally by call count (the first
228             column). 'sub' performs a string sort by subroutine name (column 2).
229              
230             =back
231              
232             =head1 execute()
233              
234             The C method returns an arrayref of data sorted in the appropriate
235             way. Each element is itself an arrayref of three items: count, sub name, and
236             sub location.
237              
238             =cut
239              
240