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 12 use strict; 13 14 use Cwd; 15 use File::Basename; 16 use File::Find; 17 use File::Spec::Functions; 18 use Getopt::Long; 19 20 # function prototypes 21 sub process_dafile(@); 22 sub canonical_path(@); 23 sub split_filename(@); 24 sub read_gcov_header(@); 25 sub read_gcov_file(@); 26 27 # scalars with default values 28 my $directory = Cwd::abs_path(Cwd::getcwd); 29 my $data_file_extension = ".gcda"; 30 my $output_filename = "output.lcov"; 31 my $gcov_tool = "/usr/bin/gcov"; 32 my $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. 36 my $result = GetOptions("directory|d=s" => \$directory, 37 "output|o=s" => \$output_filename, 38 "verbose" => \$verbosity); 39 if (!$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. 52 my $cwd = getcwd(); 53 print "mcov: after abs_pathing\n"; 54 print "mcov: getcwd() = $cwd\n"; 55 print "mcov: directory for data files is $directory\n"; 56 print "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. 60 if (! -d $directory) { 61 print "mcov: Bad args passed; exiting with error.\n"; 62 exit(1); 63 } 64 65 # open file for output 66 open(INFO_HANDLE, ">$output_filename"); 67 68 my @file_list; # scalar to hold the list of all gcda files. 69 if (-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 81 foreach my $file (@file_list) { 82 process_dafile($file); 83 } 84 close(INFO_HANDLE); 85 86 # Remove the misc gcov files that are created. 87 my @gcov_list = glob("*.gcov"); 88 foreach my $gcov_file (@gcov_list) { 89 unlink($gcov_file); 90 } 91 92 exit(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. 101 sub 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. 210 sub 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). 220 sub 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. 232 sub 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. 263 sub 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