blob: 8f6ccbf5a0a8d16feccca4126af3f812645f7dfe [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to clean the lcov files and convert it to HTML
TODO(niranjan): Add usage information here
"""
import optparse
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib2
# These are source files that were generated during compile time. We want to
# remove references to these files from the lcov file otherwise genhtml will
# throw an error.
win32_srcs_exclude = ['parse.y',
'xpathgrammar.cpp',
'cssgrammar.cpp',
'csspropertynames.gperf']
# Number of lines of a new coverage data set
# to send at a time to the dashboard.
POST_CHUNK_SIZE = 50
# Number of post request failures to allow before exiting.
MAX_FAILURES = 5
def CleanPathNames(dir):
"""Clean the pathnames of the HTML generated by genhtml.
This method is required only for code coverage on Win32. Due to a known issue
with reading from CIFS shares mounted on Linux, genhtml appends a ^M to every
file name it reads from the Windows share, causing corrupt filenames in
genhtml's output folder.
Args:
dir: Output folder of the genhtml output.
Returns:
None
"""
# Stip off the ^M characters that get appended to the file name
for dirpath, dirname, filenames in os.walk(dir):
for file in filenames:
file_clean = file.replace('\r', '')
if file_clean != file:
os.rename(file, file_clean)
def GenerateHtml(lcov_path, dash_root):
"""Runs genhtml to convert lcov data to human readable HTML.
This script expects the LCOV file name to be in the format:
chrome_<platform>_<revision#>.lcov.
This method parses the file name and then sets up the correct folder
hierarchy for the coverage data and then runs genhtml to get the actual HTML
formatted coverage data.
Args:
lcov_path: Path of the lcov data file.
dash_root: Root location of the dashboard.
Returns:
Code coverage percentage on sucess.
None on failure.
"""
# Parse the LCOV file name.
filename = os.path.basename(lcov_path).split('.')[0]
buffer = filename.split('_')
dash_root = dash_root.rstrip('/') # Remove trailing '/'
# Set up correct folder hierarchy in the dashboard root
# TODO(niranjan): Check the formatting using a regexp
if len(buffer) >= 3: # Check if filename has right formatting
platform = buffer[len(buffer) - 2]
revision = buffer[len(buffer) - 1]
if os.path.exists(os.path.join(dash_root, platform)) == False:
os.mkdir(os.path.join(dash_root, platform))
output_dir = os.path.join(dash_root, platform, revision)
os.mkdir(output_dir)
else:
# TODO(niranjan): Add failure logging here.
return None # File not formatted correctly
# Run genhtml
os.system('/usr/bin/genhtml -o %s %s' % (output_dir, lcov_path))
# TODO(niranjan): Check the exit status of the genhtml command.
# TODO(niranjan): Parse the stdout and return coverage percentage.
CleanPathNames(output_dir)
return 'dummy' # TODO(niranjan): Return actual percentage.
def CleanWin32Lcov(lcov_path, src_root):
"""Cleanup the lcov data generated on Windows.
This method fixes up the paths inside the lcov file from the Win32 specific
paths to the actual paths of the mounted CIFS share. The lcov files generated
on Windows have the following format:
SF:c:\chrome_src\src\skia\sgl\skscan_antihair.cpp
DA:97,0
DA:106,0
DA:107,0
DA:109,0
...
end_of_record
This method changes the source-file (SF) lines to a format compatible with
genhtml on Linux by fixing paths. This method also removes references to
certain dynamically generated files to be excluded from the code ceverage.
Args:
lcov_path: Path of the Win32 lcov file to be cleaned.
src_root: Location of the source and symbols dir.
Returns:
None
"""
strip_flag = False
lcov = open(lcov_path, 'r')
loc_csv_file = open(lcov_path + '.csv', 'w')
(tmpfile_id, tmpfile_name) = tempfile.mkstemp()
tmpfile = open(tmpfile_name, 'w')
src_root = src_root.rstrip('/') # Remove trailing '/'
for line in lcov:
if line.startswith('SF'):
# We want to exclude certain auto-generated files otherwise genhtml will
# fail to convert lcov to HTML.
for exp in win32_srcs_exclude:
if line.rfind(exp) != -1:
strip_flag = True # Indicates that we want to remove this section
# Now we normalize the paths
# e.g. Change SF:c:\foo\src\... to SF:/chrome_src/...
parse_buffer = line.split(':')
buffer = '%s:%s%s' % (parse_buffer[0],
src_root,
parse_buffer[2])
buffer = buffer.replace('\\', '/')
line = buffer.replace('\r', '')
# We want an accurate count of the lines of code in a given file so that
# we can estimate the code coverage perscentage accurately. We use a
# third party script cloc.pl which gives that count and then just parse
# its command line output to filter out the other unnecessary data.
# TODO(niranjan): Find out a better way of doing this.
buffer = buffer.lstrip('SF:')
file_for_loc = buffer.replace('\r\n', '')
# TODO(niranjan): Add a check to see if cloc is present on the machine.
command = ["perl",
"cloc.pl",
file_for_loc]
output = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT).communicate()[0]
if output.rfind('error:'):
return None
tmp_buf1 = output.split('=')
tmp_buf2 = tmp_buf1[len(tmp_buf1) - 2].split('x')[0].split(' ')
loc = tmp_buf2[len(tmp_buf2) - 2]
loc_csv_file.write('%s,%s\r\n' % (file_for_loc, loc))
# Write to the temp file if the section to write is valid
if strip_flag == False:
# Also write this to the 'clean' LCOV file
tmpfile.write('%s' % (line))
# Reset the strip flag
if line.endswith('end_of_record'):
strip_flag = False
# Close the files and replace the lcov file by the 'clean' tmpfile
tmpfile.close()
lcov.close()
loc_csv_file.close()
shutil.move(tmpfile_name, lcov_path)
def ParseCoverageDataForDashboard(lcov_path):
"""Parse code coverage data into coverage results per source node.
Use lcov and linecount data to create a map of source nodes to
corresponding total and tested line counts.
Args:
lcov_path: File path to lcov coverage data.
Returns:
List of strings with comma separated source node and coverage.
"""
results = {}
linecount_path = lcov_path + '.csv'
assert(os.path.exists(linecount_path),
'linecount csv does not exist at: %s' % linecount_path)
csv_file = open(linecount_path, 'r')
linecounts = csv_file.readlines()
csv_file.close()
lcov_file = open(lcov_path, 'r')
srcfile_index = 0
for line in lcov_file:
line = line.strip()
# Set the current srcfile name for a new src file declaration.
if line[:len('SF:')] == 'SF:':
instrumented_set = {}
executed_set = {}
srcfile_name = line[len('SF:'):]
# Mark coverage data points hashlist style for the current src file.
if line[:len('DA:')] == 'DA:':
line_info = line[len('DA:'):].split(',')
assert(len(line_info) == 2, 'DA: line format unexpected - %s' % line)
(line_num, line_was_executed) = line_info
instrumented_set[line_num] = True
# line_was_executed is '0' or '1'
if int(line_was_executed):
executed_set[line_num] = True
# Update results for the current src file at record end.
if line == 'end_of_record':
instrumented = len(instrumented_set.keys())
executed = len(executed_set.keys())
parent_directory = srcfile_name[:srcfile_name.rfind('/') + 1]
linecount_point = linecounts[srcfile_index].strip().split(',')
assert(len(linecount_point) == 2,
'lintcount format unexpected - %s' % linecounts[srcfile_index])
(linecount_path, linecount_count) = linecount_point
srcfile_index += 1
# Sanity check that path names in the lcov and linecount are lined up.
if linecount_path[-10:] != srcfile_name[-10:]:
print 'NAME MISMATCH: %s :: %s' % (srcfile_name, linecount_path)
if instrumented > int(linecount_count):
linecount_count = instrumented
# Keep counts the same way that it is done in the genhtml utility.
# Count the coverage of a file towards the file,
# the parent directory, and the source root.
AddResults(results, srcfile_name, int(linecount_count), executed)
AddResults(results, parent_directory, int(linecount_count), executed)
AddResults(results, '/', instrumented, executed)
lcov_file.close()
keys = results.keys()
keys.sort()
# The first key (sorted) will be the base directory '/'
# but its full path may be '/mnt/chrome_src/src/'
# using this offset will ignore the part '/mnt/chrome_src/src'.
# Offset is the last '/' that isn't the last character for the
# first directory name in results (position 1 in keys).
offset = len(keys[1][:keys[1][:-1].rfind('/')])
lines = []
for key in keys:
if len(key) > offset:
node_path = key[offset:]
else:
node_path = key
(total, covered) = results[key]
percent = float(covered) * 100 / total
lines.append('%s,%.2f' % (node_path, percent))
return lines
def AddResults(results, location, lines_total, lines_executed):
"""Add resulting line tallies to a location's total.
Args:
results: Map of node location to corresponding coverage data.
location: Source node string.
lines_total: Number of lines to add to the total count for this node.
lines_executed: Number of lines to add to the executed count for this node.
"""
if results.has_key(location):
(i, e) = results[location]
results[location] = (i + lines_total, e + lines_executed)
else:
results[location] = (lines_total, lines_executed)
def PostResultsToDashboard(lcov_path, results, post_url):
"""Post coverage results to coverage dashboard.
Args:
lcov_path: File path for lcov data in the expected format:
<project>_<platform>_<cl#>.coverage.lcov
results: string list in the appropriate posting format.
"""
project_platform_cl = lcov_path.split('.')[0].split('_')
assert(len(project_platform_cl) == 3,
'lcov_path not in expected format: %s' % lcov_path)
(project, platform, cl_string) = project_platform_cl
project_name = '%s-%s' % (project, platform)
url = '%s/newdata.do?project=%s&cl=%s' % (post_url, project_name, cl_string)
# Send POSTs of POST_CHUNK_SIZE lines of the result set until
# there is no more data and last_loop is set to True.
last_loop = False
cur_line = 0
while not last_loop:
body = '\n'.join(results[cur_line:cur_line + POST_CHUNK_SIZE])
cur_line += POST_CHUNK_SIZE
last_loop = (cur_line >= len(results))
req = urllib2.Request('%s&last=%s' % (url, str(last_loop)), body)
req.add_header('Content-Type', 'text/plain')
SendPost(req)
# Global counter for the current number of request failures.
num_fails = 0
def SendPost(req):
"""Execute a post request and retry for up to MAX_FAILURES.
Args:
req: A urllib2 request object.
Raises:
URLError: If urlopen throws after too many retries.
HTTPError: If urlopen throws after too many retries.
"""
global num_fails
try:
urllib2.urlopen(req)
# Reset failure count.
num_fails = 0
except (urllib2.URLError, urllib2.HTTPError):
num_fails += 1
if num_fails < MAX_FAILURES:
print 'fail, retrying (%d)' % num_fails
time.sleep(5)
SendPost(req)
else:
print 'POST request exceeded allowed retries.'
raise
def main():
if sys.platform[:5] != 'linux': # Run this only on Linux
print 'This script is supported only on Linux'
os.exit(1)
# Command line parsing
parser = optparse.OptionParser()
parser.add_option('-p',
'--platform',
dest='platform',
default=None,
help=('Platform that the locv file was generated on. Must'
'be one of {win32, linux2, linux3, macosx}'))
parser.add_option('-s',
'--source',
dest='src_dir',
default=None,
help='Path to the source code and symbols')
parser.add_option('-d',
'--dash_root',
dest='dash_root',
default=None,
help='Root directory for the dashboard')
parser.add_option('-l',
'--lcov',
dest='lcov_path',
default=None,
help='Location of the LCOV file to process')
parser.add_option('-u',
'--post_url',
dest='post_url',
default=None,
help='Base URL of the coverage dashboard')
(options, args) = parser.parse_args()
if options.platform == None:
parser.error('Platform not specified')
if options.lcov_path == None:
parser.error('lcov file path not specified')
if options.src_dir == None:
parser.error('Source directory not specified')
if options.dash_root == None:
parser.error('Dashboard root not specified')
if options.post_url == None:
parser.error('Post URL not specified')
if options.platform == 'win32':
CleanWin32Lcov(options.lcov_path, options.src_dir)
percent = GenerateHtml(options.lcov_path, options.dash_root)
if percent == None:
# TODO(niranjan): Add logging.
print 'Failed to generate code coverage'
os.exit(1)
else:
# TODO(niranjan): Do something with the code coverage numbers
pass
else:
print 'Unsupported platform'
os.exit(1)
# Prep coverage results for dashboard and post new set.
parsed_data = ParseCoverageDataForDashboard(options.lcov_path)
PostResultsToDashboard(options.lcov_path, parsed_data, options.post_url)
if __name__ == '__main__':
main()