#!/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 --output --verbose
#
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 --output ";
print " [--verbose ]\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 = ;
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 = ;
foreach my $line (@lines) {
chomp($line);
if ($line =~ /^\s*([^:]+):\s*(\d+?):(.*)$/) {
# ::
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;
}