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