mcov revision 5821806d5e7f356e8fa4b058a389a808ea183019
1#!/usr/bin/perl -w
2# 
3# mcov: script to convert gcov data to lcov format on Mac.
4#
5# Based on lcov (http://ltp.sourceforge.net/coverage/lcov.php)
6# Written by ajeya at google dot com.
7#
8# usage:
9# mcov --directory <base directory> --output <output file> --verbose <level>
10#
11 
12use strict;
13
14use Cwd;
15use File::Basename;
16use File::Find;
17use File::Spec::Functions;
18use Getopt::Long;
19
20# function prototypes
21sub process_dafile(@);
22sub canonical_path(@);
23sub split_filename(@);
24sub read_gcov_header(@);
25sub read_gcov_file(@);
26
27# scalars with default values
28my $directory = Cwd::abs_path(Cwd::getcwd);
29my $data_file_extension = ".gcda";
30my $output_filename = "output.lcov";
31my $gcov_tool  = "/usr/bin/gcov";
32my $verbosity = 0;
33
34# TODO(ajeya): GetOptions doesn't handle case where the script is called with
35# no arguments. This needs to be fixed.
36my $result = GetOptions("directory|d=s" => \$directory,
37                         "output|o=s" => \$output_filename,
38                         "verbose" => \$verbosity);                        
39if (!$result) {
40  print "Usage: $0  --directory <base directory> --output <output file>";
41  print " [--verbose <level>]\n";
42  exit(1);
43}
44
45# convert the directory path to absolute path.
46$directory = Cwd::abs_path($directory);
47
48# convert the output file path to absolute path.
49$output_filename = Cwd::abs_path($output_filename);
50
51# Output expected args for buildbot debugging assistance.
52my $cwd = getcwd();
53print "mcov: after abs_pathing\n";
54print "mcov: getcwd() = $cwd\n";
55print "mcov: directory for data files is $directory\n";
56print "mcov: output filename is $output_filename\n";
57
58# Sanity check; die if path is wrong.
59# We don't check for output_filename because... we create it.
60if (! -d $directory) {
61  print "mcov: Bad args passed; exiting with error.\n";
62  exit(1);
63}
64
65# open file for output
66open(INFO_HANDLE, ">$output_filename");
67
68my @file_list;  # scalar to hold the list of all gcda files.
69if (-d $directory) {
70  printf("Scanning $directory for $data_file_extension files ...\n");
71  find(sub {
72         my $file = $_;
73         if ($file =~ m/\Q$data_file_extension\E$/i) {
74           push(@file_list, Cwd::abs_path($file));
75         }},
76       $directory);
77  printf("Found %d data files in %s\n", $#file_list + 1, $directory);
78}
79
80# Process all files in list
81foreach my $file (@file_list) {
82  process_dafile($file);
83}
84close(INFO_HANDLE);
85
86# Remove the misc gcov files that are created.
87my @gcov_list = glob("*.gcov");
88foreach my $gcov_file (@gcov_list) {
89  unlink($gcov_file);
90}
91
92exit(0);
93
94# end of script
95
96# process_dafile:
97# argument(s): a file path with gcda extension
98# returns: void 
99# This method calls gcov to generate the coverage data and write the output in
100# lcov format to the output file.
101sub process_dafile(@) {
102  my ($filename) = @_;
103  print("Processing $filename ...\n");
104
105  my $da_filename;   # Name of data file to process
106  my $base_name;     # data filename without ".da/.gcda" extension
107  my $gcov_error;    # Error code of gcov tool
108  my $object_dir;    # Directory containing all object files
109  my $gcov_file;     # Name of a .gcov file
110  my @gcov_data;     # Contents of a .gcov file
111  my @gcov_list;     # List of generated .gcov files
112  my $base_dir;      # Base directory for current da file
113  local *OLD_STDOUT; # Handle to store STDOUT temporarily
114  
115  # Get directory and basename of data file
116  ($base_dir, $base_name) = split_filename(canonical_path($filename));
117
118  # Check for writable $base_dir (gcov will try to write files there)
119  if (!-w $base_dir) {
120    print("ERROR: cannot write to directory $base_dir\n");
121    return;
122  }
123
124  # Construct name of graph file
125  $da_filename = File::Spec::Functions::catfile($base_dir,
126                                                join(".", $base_name, "gcno"));
127
128  # Ignore empty graph file (e.g. source file with no statement)
129  if (-z $da_filename) {
130    warn("WARNING: empty $da_filename (skipped)\n");
131    return;
132  }
133    
134  # Set $object_dir to real location of object files. This may differ
135  # from $base_dir if the graph file is just a link to the "real" object
136  # file location.
137  $object_dir = dirname($da_filename);
138  
139  # Save the current STDOUT to OLD_STDOUT and set STDOUT to /dev/null to mute
140  # standard output.
141  if (!$verbosity) {
142    open(OLD_STDOUT, ">>&STDOUT");
143    open(STDOUT, ">/dev/null");
144  }
145  
146  # run gcov utility with the supplied gcno file and object directory.
147  $gcov_error = system($gcov_tool, $da_filename, "-o", $object_dir);
148  
149  # Restore STDOUT if we changed it before.
150  if (!$verbosity) {
151    open(STDOUT, ">>&OLD_STDOUT");
152  }
153
154  if ($gcov_error) {
155    warn("WARNING: GCOV failed for $da_filename!\n");
156    return;
157  }
158
159  # Collect data from resulting .gcov files and create .info file
160  @gcov_list = glob("*.gcov");
161  # Check for files
162  if (!scalar(@gcov_list)) {
163    warn("WARNING: gcov did not create any files for $da_filename!\n");
164  }
165  
166  foreach $gcov_file (@gcov_list) {
167    my $source_filename = read_gcov_header($gcov_file);
168    
169    if (!defined($source_filename)) {
170      next;
171    }
172    
173    $source_filename = canonical_path($source_filename);
174    
175    # Read in contents of gcov file
176    @gcov_data = read_gcov_file($gcov_file);
177    
178    # Skip empty files
179    if (!scalar(@gcov_data)) {
180      warn("WARNING: skipping empty file $gcov_file\n");
181      unlink($gcov_file);
182      next;
183    }
184        
185    print(INFO_HANDLE "SF:", Cwd::abs_path($source_filename), "\n");
186
187    # Write coverage information for each instrumented line
188    # Note: @gcov_content contains a list of (flag, count, source)
189    # tuple for each source code line
190    while (@gcov_data) {
191      # Check for instrumented line 
192      if ($gcov_data[0]) {
193        print(INFO_HANDLE "DA:", $gcov_data[3], ",", $gcov_data[1], "\n");
194      }
195      # Remove already processed data from array
196      splice(@gcov_data,0,4);  
197    }
198    print(INFO_HANDLE "end_of_record\n");
199    
200    # Remove .gcov file after processing
201    unlink($gcov_file);
202  } #end for_each
203}
204
205# canonical_path:
206# argument(s): any file path
207# returns: the file path as a string
208# 
209# clean up the file path being passed.
210sub canonical_path(@) {
211  my ($filename) = @_;
212  return (File::Spec::Functions::canonpath($filename));
213}
214
215# split_filename:
216# argument(s): any file path
217# returns: an array with the path components
218# 
219# splits the file path into path and filename (with no extension).
220sub split_filename(@){
221  my ($filename) = @_;
222  my ($base, $path, $ext) = File::Basename::fileparse($filename, '\.[^\.]*');
223  return ($path, $base);
224}
225
226# read_gcov_header:
227# argument(s): path to gcov file
228# returns: an array the contens of the gcov header.
229# 
230# reads the gcov file and returns the parsed contents of a gcov header as an 
231# array.
232sub read_gcov_header(@) {
233  my ($filename) = @_;
234  my $source;
235  local *INPUT;
236
237  if (!open(INPUT, $filename)) {
238    warn("WARNING: cannot read $filename!\n");
239    return (undef,undef);
240  }
241  
242  my @lines = <INPUT>;
243  foreach my $line (@lines) {
244    chomp($line);
245    # check for lines with source string. 
246    if ($line =~ /^\s+-:\s+0:Source:(.*)$/) {
247      # Source: header entry
248      $source = $1;
249    } else {
250      last;
251    }
252  }
253  close(INPUT);
254  return $source;
255}
256
257# read_gcov_file:
258# argument(s): path to gcov file
259# returns: an array with the contents of the gcov file.
260# 
261# reads the gcov file and returns the parsed contents of a gcov file
262# as an array.
263sub read_gcov_file(@) {
264  my ($filename) = @_;
265  my @result = ();
266  my $number;
267  local *INPUT;
268
269  if (!open(INPUT, $filename)) {
270    warn("WARNING: cannot read $filename!\n");
271    return @result;
272  }
273  
274  # Parse gcov output and populate the array
275  my @lines = <INPUT>;
276  foreach my $line (@lines) {
277    chomp($line);
278    if ($line =~ /^\s*([^:]+):\s*(\d+?):(.*)$/) {
279      # <exec count>:<line number>:<source code>
280      
281      if ($1 eq "-") {
282        # Uninstrumented line
283        push(@result, 0);
284        push(@result, 0);
285        push(@result, $3);
286        push(@result, $2);
287      } elsif ($2 eq "0") {
288        #ignore comments and other header info
289      } else {
290        # Source code execution data
291        $number = $1;
292        # Check for zero count
293        if ($number eq "#####") {
294          $number = 0;
295        }
296        push(@result, 1);
297        push(@result, $number);
298        push(@result, $3);
299        push(@result, $2);
300      }  
301    }
302  }
303  close(INPUT);
304  return @result;
305}
306