Home | History | Annotate | Download | only in bin
      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