| #!/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; |
| } |