#!/usr/bin/perl -w # # mcov: script to convert gcov data to lcov format on Mac. # # Based on lcov (http://ltp.sourceforge.net/coverage/lcov.php) # Written by ajeya at google dot com. # # usage: # mcov --directory <base directory> --output <output file> --verbose <level> # use strict; use Cwd; use File::Basename; use File::Find; use File::Spec::Functions; use Getopt::Long; # function prototypes sub process_dafile(@); sub canonical_path(@); sub split_filename(@); sub read_gcov_header(@); sub read_gcov_file(@); # scalars with default values my $directory = Cwd::abs_path(Cwd::getcwd); my $data_file_extension = ".gcda"; my $output_filename = "output.lcov"; my $gcov_tool = "/usr/bin/gcov"; my $verbosity = 0; # TODO(ajeya): GetOptions doesn't handle case where the script is called with # no arguments. This needs to be fixed. my $result = GetOptions("directory|d=s" => \$directory, "output|o=s" => \$output_filename, "verbose" => \$verbosity); if (!$result) { print "Usage: $0 --directory <base directory> --output <output file>"; print " [--verbose <level>]\n"; exit(1); } # convert the directory path to absolute path. $directory = Cwd::abs_path($directory); # convert the output file path to absolute path. $output_filename = Cwd::abs_path($output_filename); # Output expected args for buildbot debugging assistance. my $cwd = getcwd(); print "mcov: after abs_pathing\n"; print "mcov: getcwd() = $cwd\n"; print "mcov: directory for data files is $directory\n"; print "mcov: output filename is $output_filename\n"; # Sanity check; die if path is wrong. # We don't check for output_filename because... we create it. if (! -d $directory) { print "mcov: Bad args passed; exiting with error.\n"; exit(1); } # open file for output open(INFO_HANDLE, ">$output_filename"); my @file_list; # scalar to hold the list of all gcda files. if (-d $directory) { printf("Scanning $directory for $data_file_extension files ...\n"); find(sub { my $file = $_; if ($file =~ m/\Q$data_file_extension\E$/i) { push(@file_list, Cwd::abs_path($file)); }}, $directory); printf("Found %d data files in %s\n", $#file_list + 1, $directory); } # Process all files in list foreach my $file (@file_list) { process_dafile($file); } close(INFO_HANDLE); # Remove the misc gcov files that are created. my @gcov_list = glob("*.gcov"); foreach my $gcov_file (@gcov_list) { unlink($gcov_file); } exit(0); # end of script # process_dafile: # argument(s): a file path with gcda extension # returns: void # This method calls gcov to generate the coverage data and write the output in # lcov format to the output file. sub process_dafile(@) { my ($filename) = @_; print("Processing $filename ...\n"); my $da_filename; # Name of data file to process my $base_name; # data filename without ".da/.gcda" extension my $gcov_error; # Error code of gcov tool my $object_dir; # Directory containing all object files my $gcov_file; # Name of a .gcov file my @gcov_data; # Contents of a .gcov file my @gcov_list; # List of generated .gcov files my $base_dir; # Base directory for current da file local *OLD_STDOUT; # Handle to store STDOUT temporarily # Get directory and basename of data file ($base_dir, $base_name) = split_filename(canonical_path($filename)); # Check for writable $base_dir (gcov will try to write files there) if (!-w $base_dir) { print("ERROR: cannot write to directory $base_dir\n"); return; } # Construct name of graph file $da_filename = File::Spec::Functions::catfile($base_dir, join(".", $base_name, "gcno")); # Ignore empty graph file (e.g. source file with no statement) if (-z $da_filename) { warn("WARNING: empty $da_filename (skipped)\n"); return; } # Set $object_dir to real location of object files. This may differ # from $base_dir if the graph file is just a link to the "real" object # file location. $object_dir = dirname($da_filename); # Save the current STDOUT to OLD_STDOUT and set STDOUT to /dev/null to mute # standard output. if (!$verbosity) { open(OLD_STDOUT, ">>&STDOUT"); open(STDOUT, ">/dev/null"); } # run gcov utility with the supplied gcno file and object directory. $gcov_error = system($gcov_tool, $da_filename, "-o", $object_dir); # Restore STDOUT if we changed it before. if (!$verbosity) { open(STDOUT, ">>&OLD_STDOUT"); } if ($gcov_error) { warn("WARNING: GCOV failed for $da_filename!\n"); return; } # Collect data from resulting .gcov files and create .info file @gcov_list = glob("*.gcov"); # Check for files if (!scalar(@gcov_list)) { warn("WARNING: gcov did not create any files for $da_filename!\n"); } foreach $gcov_file (@gcov_list) { my $source_filename = read_gcov_header($gcov_file); if (!defined($source_filename)) { next; } $source_filename = canonical_path($source_filename); # Read in contents of gcov file @gcov_data = read_gcov_file($gcov_file); # Skip empty files if (!scalar(@gcov_data)) { warn("WARNING: skipping empty file $gcov_file\n"); unlink($gcov_file); next; } print(INFO_HANDLE "SF:", Cwd::abs_path($source_filename), "\n"); # Write coverage information for each instrumented line # Note: @gcov_content contains a list of (flag, count, source) # tuple for each source code line while (@gcov_data) { # Check for instrumented line if ($gcov_data[0]) { print(INFO_HANDLE "DA:", $gcov_data[3], ",", $gcov_data[1], "\n"); } # Remove already processed data from array splice(@gcov_data,0,4); } print(INFO_HANDLE "end_of_record\n"); # Remove .gcov file after processing unlink($gcov_file); } #end for_each } # canonical_path: # argument(s): any file path # returns: the file path as a string # # clean up the file path being passed. sub canonical_path(@) { my ($filename) = @_; return (File::Spec::Functions::canonpath($filename)); } # split_filename: # argument(s): any file path # returns: an array with the path components # # splits the file path into path and filename (with no extension). sub split_filename(@){ my ($filename) = @_; my ($base, $path, $ext) = File::Basename::fileparse($filename, '\.[^\.]*'); return ($path, $base); } # read_gcov_header: # argument(s): path to gcov file # returns: an array the contens of the gcov header. # # reads the gcov file and returns the parsed contents of a gcov header as an # array. sub read_gcov_header(@) { my ($filename) = @_; my $source; local *INPUT; if (!open(INPUT, $filename)) { warn("WARNING: cannot read $filename!\n"); return (undef,undef); } my @lines = <INPUT>; foreach my $line (@lines) { chomp($line); # check for lines with source string. if ($line =~ /^\s+-:\s+0:Source:(.*)$/) { # Source: header entry $source = $1; } else { last; } } close(INPUT); return $source; } # read_gcov_file: # argument(s): path to gcov file # returns: an array with the contents of the gcov file. # # reads the gcov file and returns the parsed contents of a gcov file # as an array. sub read_gcov_file(@) { my ($filename) = @_; my @result = (); my $number; local *INPUT; if (!open(INPUT, $filename)) { warn("WARNING: cannot read $filename!\n"); return @result; } # Parse gcov output and populate the array my @lines = <INPUT>; foreach my $line (@lines) { chomp($line); if ($line =~ /^\s*([^:]+):\s*(\d+?):(.*)$/) { # <exec count>:<line number>:<source code> if ($1 eq "-") { # Uninstrumented line push(@result, 0); push(@result, 0); push(@result, $3); push(@result, $2); } elsif ($2 eq "0") { #ignore comments and other header info } else { # Source code execution data $number = $1; # Check for zero count if ($number eq "#####") { $number = 0; } push(@result, 1); push(@result, $number); push(@result, $3); push(@result, $2); } } } close(INPUT); return @result; }