Commit 1de68d73 authored by Max Moroz's avatar Max Moroz Committed by Commit Bot

[Code Coverage] Refactor the script logic into Py2/Py3 compatible module.

- Move all report post processing logic to a separate coverage_utils file.
- Add switch for disabling component view.
- Fix shared libraries regexp to match versioned libraries.
- Move HTML post processing methods into a class (to reduce passing a bunch
  of parameters back and forth).
- Add _Main() to coverage_utils.py that can be used outside of Chromium.

For example, to use that script on OSS-Fuzz we would need to do the following:
1) git clone https://chromium.googlesource.com/chromium/src/tools/code_coverage somewhere
2) echo -en '#!/bin/bash\npython3 somewhere/coverage_utils.py $@\n' \
       > /usr/local/bin/coverage_helper

Bug: 817798
Change-Id: I8de8cec2d118f022eb43634dd85b96e4126f5e63
Reviewed-on: https://chromium-review.googlesource.com/1180364
Commit-Queue: Max Moroz <mmoroz@chromium.org>
Reviewed-by: default avatarOliver Chang <ochang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#584726}
parent 921cd240
......@@ -91,9 +91,10 @@ sys.path.append(
os.path.join(
os.path.dirname(__file__), os.path.pardir, os.path.pardir,
'third_party'))
import jinja2
from collections import defaultdict
import coverage_utils
# Absolute path to the code coverage tools binary. These paths can be
# overwritten by user specified coverage tool paths.
LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
......@@ -128,15 +129,6 @@ SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
# Build arg required for generating code coverage data.
CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
# The default name of the html coverage report for a directory.
DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
# Name of the html index files for different views.
COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
LOGS_DIR_NAME = 'logs'
# Used to extract a mapping between directories and components.
......@@ -161,214 +153,10 @@ FILE_BUG_MESSAGE = (
LLVM_PROFILE_FILE_PATH_SUBSTITUTION = '<llvm_profile_file_path>'
class _CoverageSummary(object):
"""Encapsulates coverage summary representation."""
def __init__(self,
regions_total=0,
regions_covered=0,
functions_total=0,
functions_covered=0,
lines_total=0,
lines_covered=0):
"""Initializes _CoverageSummary object."""
self._summary = {
'regions': {
'total': regions_total,
'covered': regions_covered
},
'functions': {
'total': functions_total,
'covered': functions_covered
},
'lines': {
'total': lines_total,
'covered': lines_covered
}
}
def Get(self):
"""Returns summary as a dictionary."""
return self._summary
def AddSummary(self, other_summary):
"""Adds another summary to this one element-wise."""
for feature in self._summary:
self._summary[feature]['total'] += other_summary.Get()[feature]['total']
self._summary[feature]['covered'] += other_summary.Get()[feature][
'covered']
class _CoverageReportHtmlGenerator(object):
"""Encapsulates coverage html report generation.
The generated html has a table that contains links to other coverage reports.
"""
def __init__(self, output_path, table_entry_type):
"""Initializes _CoverageReportHtmlGenerator object.
Args:
output_path: Path to the html report that will be generated.
table_entry_type: Type of the table entries to be displayed in the table
header. For example: 'Path', 'Component'.
"""
css_file_name = os.extsep.join(['style', 'css'])
css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
assert os.path.exists(css_absolute_path), (
'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
'is called first, and the css file is generated at: "%s".' %
css_absolute_path)
self._css_absolute_path = css_absolute_path
self._output_path = output_path
self._table_entry_type = table_entry_type
self._table_entries = []
self._total_entry = {}
source_dir = os.path.dirname(os.path.realpath(__file__))
template_dir = os.path.join(source_dir, 'html_templates')
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
self._header_template = jinja_env.get_template('header.html')
self._table_template = jinja_env.get_template('table.html')
self._footer_template = jinja_env.get_template('footer.html')
self._style_overrides = open(
os.path.join(source_dir, 'static', 'css', 'style.css')).read()
def AddLinkToAnotherReport(self, html_report_path, name, summary):
"""Adds a link to another html report in this report.
The link to be added is assumed to be an entry in this directory.
"""
# Use relative paths instead of absolute paths to make the generated reports
# portable.
html_report_relative_path = _GetRelativePathToDirectoryOfFile(
html_report_path, self._output_path)
table_entry = self._CreateTableEntryFromCoverageSummary(
summary, html_report_relative_path, name,
os.path.basename(html_report_path) ==
DIRECTORY_COVERAGE_HTML_REPORT_NAME)
self._table_entries.append(table_entry)
def CreateTotalsEntry(self, summary):
"""Creates an entry corresponds to the 'Totals' row in the html report."""
self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
def _CreateTableEntryFromCoverageSummary(self,
summary,
href=None,
name=None,
is_dir=None):
"""Creates an entry to display in the html report."""
assert (href is None and name is None and is_dir is None) or (
href is not None and name is not None and is_dir is not None), (
'The only scenario when href or name or is_dir can be None is when '
'creating an entry for the Totals row, and in that case, all three '
'attributes must be None.')
entry = {}
if href is not None:
entry['href'] = href
if name is not None:
entry['name'] = name
if is_dir is not None:
entry['is_dir'] = is_dir
summary_dict = summary.Get()
for feature in summary_dict:
if summary_dict[feature]['total'] == 0:
percentage = 0.0
else:
percentage = float(summary_dict[feature]
['covered']) / summary_dict[feature]['total'] * 100
color_class = self._GetColorClass(percentage)
entry[feature] = {
'total': summary_dict[feature]['total'],
'covered': summary_dict[feature]['covered'],
'percentage': '{:6.2f}'.format(percentage),
'color_class': color_class
}
return entry
def _GetColorClass(self, percentage):
"""Returns the css color class based on coverage percentage."""
if percentage >= 0 and percentage < 80:
return 'red'
if percentage >= 80 and percentage < 100:
return 'yellow'
if percentage == 100:
return 'green'
assert False, 'Invalid coverage percentage: "%d".' % percentage
def WriteHtmlCoverageReport(self, no_file_view):
"""Writes html coverage report.
In the report, sub-directories are displayed before files and within each
category, entries are sorted alphabetically.
"""
def EntryCmp(left, right):
"""Compare function for table entries."""
if left['is_dir'] != right['is_dir']:
return -1 if left['is_dir'] == True else 1
return -1 if left['name'] < right['name'] else 1
self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
directory_view_path = _GetDirectoryViewPath()
directory_view_href = _GetRelativePathToDirectoryOfFile(
directory_view_path, self._output_path)
component_view_path = _GetComponentViewPath()
component_view_href = _GetRelativePathToDirectoryOfFile(
component_view_path, self._output_path)
# File view is optional in the report.
file_view_href = None
if not no_file_view:
file_view_path = _GetFileViewPath()
file_view_href = _GetRelativePathToDirectoryOfFile(
file_view_path, self._output_path)
html_header = self._header_template.render(
css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
directory_view_href=directory_view_href,
component_view_href=component_view_href,
file_view_href=file_view_href,
style_overrides=self._style_overrides)
html_table = self._table_template.render(
entries=self._table_entries,
total_entry=self._total_entry,
table_entry_type=self._table_entry_type)
html_footer = self._footer_template.render()
with open(self._output_path, 'w') as html_file:
html_file.write(html_header + html_table + html_footer)
def _ConfigureLogging(args):
"""Configures logging settings for later use."""
log_level = logging.DEBUG if args.verbose else logging.INFO
log_format = '[%(asctime)s %(levelname)s] %(message)s'
log_file = args.log_file if args.log_file else None
logging.basicConfig(filename=log_file, level=log_level, format=log_format)
def _ConfigureLLVMCoverageTools(args):
"""Configures llvm coverage tools."""
if args.coverage_tools_dir:
llvm_bin_dir = _GetFullPath(args.coverage_tools_dir)
llvm_bin_dir = coverage_utils.GetFullPath(args.coverage_tools_dir)
global LLVM_COV_PATH
global LLVM_PROFDATA_PATH
LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
......@@ -383,71 +171,6 @@ def _ConfigureLLVMCoverageTools(args):
LLVM_COV_PATH, LLVM_PROFDATA_PATH)
def _GetSharedLibraries(binary_paths):
"""Returns list of shared libraries used by specified binaries."""
logging.info('Finding shared libraries for targets (if any).')
shared_libraries = []
cmd = []
shared_library_re = None
if sys.platform.startswith('linux'):
cmd.extend(['ldd'])
shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
r'.*\.so)\s.*')
elif sys.platform.startswith('darwin'):
cmd.extend(['otool', '-L'])
shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
else:
assert False, 'Cannot detect shared libraries used by the given targets.'
assert shared_library_re is not None
cmd.extend(binary_paths)
output = subprocess.check_output(cmd)
for line in output.splitlines():
m = shared_library_re.match(line)
if not m:
continue
shared_library_path = m.group(1)
if sys.platform.startswith('darwin'):
# otool outputs "@rpath" macro instead of the dirname of the given binary.
shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
if shared_library_path in shared_libraries:
continue
assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
'the given target(s) does not '
'exist.' % shared_library_path)
with open(shared_library_path) as f:
data = f.read()
# Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
if '__llvm_cov' in data:
shared_libraries.append(shared_library_path)
logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
shared_libraries)
logging.info('Finished finding shared libraries for targets.')
return shared_libraries
def _GetHostPlatform():
"""Returns the host platform.
This is separate from the target platform/os that coverage is running for.
"""
if sys.platform == 'win32' or sys.platform == 'cygwin':
return 'win'
if sys.platform.startswith('linux'):
return 'linux'
else:
assert sys.platform == 'darwin'
return 'mac'
def _GetPathWithLLVMSymbolizerDir():
"""Add llvm-symbolizer directory to path for symbolized stacks."""
path = os.getenv('PATH')
......@@ -500,7 +223,7 @@ def DownloadCoverageToolsIfNeeded():
clang_revision_str, clang_sub_revision_str = package_version.split('-')
return int(clang_revision_str), int(clang_sub_revision_str)
host_platform = _GetHostPlatform()
host_platform = coverage_utils.GetHostPlatform()
clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
clang_update.STAMP_FILE)
......@@ -568,7 +291,7 @@ def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]])
_AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
if _GetHostPlatform() in ['linux', 'mac']:
if coverage_utils.GetHostPlatform() in ['linux', 'mac']:
subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
subprocess_cmd.extend(filters)
if ignore_filename_regex:
......@@ -576,333 +299,27 @@ def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
subprocess.check_call(subprocess_cmd)
# llvm-cov creates "coverage" subdir in the output dir. We would like to use
# the platform name instead, as it simplifies the report dir structure when
# the same report is generated for different platforms.
default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
platform_report_subdir_path = _GetCoverageReportRootDirPath()
_MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
logging.debug('Finished running "llvm-cov show" command.')
def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
"""Generates html index file for file view."""
file_view_index_file_path = _GetFileViewPath()
logging.debug('Generating file view html index file as: "%s".',
file_view_index_file_path)
html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
'Path')
totals_coverage_summary = _CoverageSummary()
for file_path in per_file_coverage_summary:
totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
html_generator.AddLinkToAnotherReport(
_GetCoverageHtmlReportPathForFile(file_path),
os.path.relpath(file_path, SRC_ROOT_PATH),
per_file_coverage_summary[file_path])
html_generator.CreateTotalsEntry(totals_coverage_summary)
html_generator.WriteHtmlCoverageReport(no_file_view=False)
logging.debug('Finished generating file view html index file.')
def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
"""Calculates per directory coverage summary."""
logging.debug('Calculating per-directory coverage summary.')
per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
for file_path in per_file_coverage_summary:
summary = per_file_coverage_summary[file_path]
parent_dir = os.path.dirname(file_path)
while True:
per_directory_coverage_summary[parent_dir].AddSummary(summary)
if parent_dir == SRC_ROOT_PATH:
break
parent_dir = os.path.dirname(parent_dir)
logging.debug('Finished calculating per-directory coverage summary.')
return per_directory_coverage_summary
def _GeneratePerDirectoryCoverageInHtml(
per_directory_coverage_summary, per_file_coverage_summary, no_file_view):
"""Generates per directory coverage breakdown in html."""
logging.debug('Writing per-directory coverage html reports.')
for dir_path in per_directory_coverage_summary:
_GenerateCoverageInHtmlForDirectory(dir_path,
per_directory_coverage_summary,
per_file_coverage_summary, no_file_view)
logging.debug('Finished writing per-directory coverage html reports.')
def _GenerateCoverageInHtmlForDirectory(
dir_path, per_directory_coverage_summary, per_file_coverage_summary,
no_file_view):
"""Generates coverage html report for a single directory."""
html_generator = _CoverageReportHtmlGenerator(
_GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
for entry_name in os.listdir(dir_path):
entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
if entry_path in per_file_coverage_summary:
entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
entry_coverage_summary = per_file_coverage_summary[entry_path]
elif entry_path in per_directory_coverage_summary:
entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
entry_path)
entry_coverage_summary = per_directory_coverage_summary[entry_path]
else:
# Any file without executable lines shouldn't be included into the report.
# For example, OWNER and README.md files.
continue
html_generator.AddLinkToAnotherReport(entry_html_report_path,
os.path.basename(entry_path),
entry_coverage_summary)
html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
html_generator.WriteHtmlCoverageReport(no_file_view)
def _GenerateDirectoryViewHtmlIndexFile():
"""Generates the html index file for directory view.
Note that the index file is already generated under SRC_ROOT_PATH, so this
file simply redirects to it, and the reason of this extra layer is for
structural consistency with other views.
"""
directory_view_index_file_path = _GetDirectoryViewPath()
logging.debug('Generating directory view html index file as: "%s".',
directory_view_index_file_path)
src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
SRC_ROOT_PATH)
_WriteRedirectHtmlFile(directory_view_index_file_path,
src_root_html_report_path)
logging.debug('Finished generating directory view html index file.')
def _CalculatePerComponentCoverageSummary(component_to_directories,
per_directory_coverage_summary):
"""Calculates per component coverage summary."""
logging.debug('Calculating per-component coverage summary.')
per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
for component in component_to_directories:
for directory in component_to_directories[component]:
absolute_directory_path = _GetFullPath(directory)
if absolute_directory_path in per_directory_coverage_summary:
per_component_coverage_summary[component].AddSummary(
per_directory_coverage_summary[absolute_directory_path])
logging.debug('Finished calculating per-component coverage summary.')
return per_component_coverage_summary
def _ExtractComponentToDirectoriesMapping():
"""Returns a mapping from components to directories."""
component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
directory_to_component = component_mappings['dir-to-component']
component_to_directories = defaultdict(list)
for directory in sorted(directory_to_component):
component = directory_to_component[directory]
# Check if we already added the parent directory of this directory. If yes,
# skip this sub-directory to avoid double-counting.
found_parent_directory = False
for component_directory in component_to_directories[component]:
if directory.startswith(component_directory + '/'):
found_parent_directory = True
break
if not found_parent_directory:
component_to_directories[component].append(directory)
return component_to_directories
def _GeneratePerComponentCoverageInHtml(
per_component_coverage_summary, component_to_directories,
per_directory_coverage_summary, no_file_view):
"""Generates per-component coverage reports in html."""
logging.debug('Writing per-component coverage html reports.')
for component in per_component_coverage_summary:
_GenerateCoverageInHtmlForComponent(
component, per_component_coverage_summary, component_to_directories,
per_directory_coverage_summary, no_file_view)
logging.debug('Finished writing per-component coverage html reports.')
def _GenerateCoverageInHtmlForComponent(
component_name, per_component_coverage_summary, component_to_directories,
per_directory_coverage_summary, no_file_view):
"""Generates coverage html report for a component."""
component_html_report_path = _GetCoverageHtmlReportPathForComponent(
component_name)
component_html_report_dir = os.path.dirname(component_html_report_path)
if not os.path.exists(component_html_report_dir):
os.makedirs(component_html_report_dir)
html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
'Path')
for dir_path in component_to_directories[component_name]:
dir_absolute_path = _GetFullPath(dir_path)
if dir_absolute_path not in per_directory_coverage_summary:
# Any directory without an excercised file shouldn't be included into the
# report.
continue
html_generator.AddLinkToAnotherReport(
_GetCoverageHtmlReportPathForDirectory(dir_path),
os.path.relpath(dir_path, SRC_ROOT_PATH),
per_directory_coverage_summary[dir_absolute_path])
html_generator.CreateTotalsEntry(
per_component_coverage_summary[component_name])
html_generator.WriteHtmlCoverageReport(no_file_view)
def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
no_file_view):
"""Generates the html index file for component view."""
component_view_index_file_path = _GetComponentViewPath()
logging.debug('Generating component view html index file as: "%s".',
component_view_index_file_path)
html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
'Component')
for component in per_component_coverage_summary:
html_generator.AddLinkToAnotherReport(
_GetCoverageHtmlReportPathForComponent(component), component,
per_component_coverage_summary[component])
# Do not create a totals row for the component view as the value is incorrect
# due to failure to account for UNKNOWN component and some paths belonging to
# multiple components.
html_generator.WriteHtmlCoverageReport(no_file_view)
logging.debug('Finished generating component view html index file.')
def _MergeTwoDirectories(src_path, dst_path):
"""Merge src_path directory into dst_path directory."""
for filename in os.listdir(src_path):
dst_path = os.path.join(dst_path, filename)
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
os.rename(os.path.join(src_path, filename), dst_path)
shutil.rmtree(src_path)
def _OverwriteHtmlReportsIndexFile():
"""Overwrites the root index file to redirect to the default view."""
html_index_file_path = _GetHtmlIndexPath()
directory_view_index_file_path = _GetDirectoryViewPath()
_WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
def _WriteRedirectHtmlFile(from_html_path, to_html_path):
"""Writes a html file that redirects to another html file."""
to_html_relative_path = _GetRelativePathToDirectoryOfFile(
to_html_path, from_html_path)
content = ("""
<!DOCTYPE html>
<html>
<head>
<!-- HTML meta refresh URL redirection -->
<meta http-equiv="refresh" content="0; url=%s">
</head>
</html>""" % to_html_relative_path)
with open(from_html_path, 'w') as f:
f.write(content)
def _CleanUpOutputDir():
"""Perform a cleanup of the output dir."""
# Remove the default index.html file produced by llvm-cov.
index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
if os.path.exists(index_path):
os.remove(index_path)
def _GetCoverageHtmlReportPathForFile(file_path):
"""Given a file path, returns the corresponding html report path."""
assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
html_report_path = os.extsep.join([_GetFullPath(file_path), 'html'])
# '+' is used instead of os.path.join because both of them are absolute paths
# and os.path.join ignores the first path.
# TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
return _GetCoverageReportRootDirPath() + html_report_path
def _GetCoverageHtmlReportPathForDirectory(dir_path):
"""Given a directory path, returns the corresponding html report path."""
assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
html_report_path = os.path.join(
_GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
# '+' is used instead of os.path.join because both of them are absolute paths
# and os.path.join ignores the first path.
# TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
return _GetCoverageReportRootDirPath() + html_report_path
def _GetCoverageHtmlReportPathForComponent(component_name):
"""Given a component, returns the corresponding html report path."""
component_file_name = component_name.lower().replace('>', '-')
html_report_name = os.extsep.join([component_file_name, 'html'])
return os.path.join(_GetCoverageReportRootDirPath(), 'components',
html_report_name)
def _GetCoverageReportRootDirPath():
"""The root directory that contains all generated coverage html reports."""
return os.path.join(OUTPUT_DIR, _GetHostPlatform())
def _GetComponentViewPath():
"""Path to the HTML file for the component view."""
return os.path.join(_GetCoverageReportRootDirPath(),
COMPONENT_VIEW_INDEX_FILE)
def _GetDirectoryViewPath():
"""Path to the HTML file for the directory view."""
return os.path.join(_GetCoverageReportRootDirPath(),
DIRECTORY_VIEW_INDEX_FILE)
def _GetFileViewPath():
"""Path to the HTML file for the file view."""
return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
def _GetLogsDirectoryPath():
"""Path to the logs directory."""
return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
def _GetHtmlIndexPath():
"""Path to the main HTML index file."""
return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR), LOGS_DIR_NAME)
def _GetProfdataFilePath():
"""Path to the resulting .profdata file."""
return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
PROFDATA_FILE_NAME)
def _GetSummaryFilePath():
"""The JSON file that contains coverage summary written by llvm-cov export."""
return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
SUMMARY_FILE_NAME)
def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
......@@ -976,9 +393,10 @@ def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
logging.debug('Executing the test commands.')
# Remove existing profraw data files.
for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
report_root_dir = coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR)
for file_or_dir in os.listdir(report_root_dir):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
os.remove(os.path.join(report_root_dir, file_or_dir))
# Ensure that logs directory exists.
if not os.path.exists(_GetLogsDirectoryPath()):
......@@ -1009,10 +427,10 @@ def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
if _IsIOS():
profraw_file_paths = [_GetProfrawDataFileByParsingOutput(output)]
else:
for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
for file_or_dir in os.listdir(report_root_dir):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
profraw_file_paths.append(
os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
os.path.join(report_root_dir, file_or_dir))
assert profraw_file_paths, (
'Running target "%s" failed to generate any profraw data file, '
......@@ -1079,7 +497,8 @@ def _ExecuteCommand(target, command, output_file_path):
profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
expected_profraw_file_name = os.extsep.join(
[target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
expected_profraw_file_path = os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
expected_profraw_file_name)
command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
expected_profraw_file_path)
......@@ -1186,7 +605,7 @@ def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
subprocess_cmd.extend(profdata_file_paths)
output = subprocess.check_output(subprocess_cmd)
logging.debug('Merge output: %s' % output)
logging.debug('Merge output: %s', output)
except subprocess.CalledProcessError as error:
logging.error(
'Failed to merge target profdata files to create coverage profdata. %s',
......@@ -1224,7 +643,7 @@ def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
subprocess_cmd.extend(profraw_file_paths)
output = subprocess.check_output(subprocess_cmd)
logging.debug('Merge output: %s' % output)
logging.debug('Merge output: %s', output)
except subprocess.CalledProcessError as error:
logging.error(
'Failed to merge target profraw files to create target profdata.')
......@@ -1262,31 +681,7 @@ def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
with open(_GetSummaryFilePath(), 'w') as f:
f.write(export_output)
json_output = json.loads(export_output)
assert len(json_output['data']) == 1
files_coverage_data = json_output['data'][0]['files']
per_file_coverage_summary = {}
for file_coverage_data in files_coverage_data:
file_path = file_coverage_data['filename']
assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
'File path "%s" in coverage summary is outside source checkout.' %
file_path)
summary = file_coverage_data['summary']
if summary['lines']['count'] == 0:
continue
per_file_coverage_summary[file_path] = _CoverageSummary(
regions_total=summary['regions']['count'],
regions_covered=summary['regions']['covered'],
functions_total=summary['functions']['count'],
functions_covered=summary['functions']['covered'],
lines_total=summary['lines']['count'],
lines_covered=summary['lines']['covered'])
logging.debug('Finished generating per-file code coverage summary.')
return per_file_coverage_summary
return export_output
def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
......@@ -1350,7 +745,7 @@ def _VerifyTargetExecutablesAreInBuildDirectory(commands):
the given build directory."""
for command in commands:
binary_path = _GetBinaryPath(command)
binary_absolute_path = _GetFullPath(binary_path)
binary_absolute_path = coverage_utils.GetFullPath(binary_path)
assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
'Target executable "%s" in command: "%s" is outside of '
'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
......@@ -1372,7 +767,7 @@ def _ValidateCurrentPlatformIsSupported():
if target_os:
current_platform = target_os
else:
current_platform = _GetHostPlatform()
current_platform = coverage_utils.GetHostPlatform()
assert current_platform in [
'linux', 'mac', 'chromeos', 'ios'
......@@ -1429,19 +824,6 @@ def _VerifyPathsAndReturnAbsolutes(paths):
return absolute_paths
def _GetRelativePathToDirectoryOfFile(target_path, base_path):
"""Returns a target path relative to the directory of base_path.
This method requires base_path to be a file, otherwise, one should call
os.path.relpath directly.
"""
assert os.path.dirname(base_path) != base_path, (
'Base path: "%s" is a directory, please call os.path.relpath directly.' %
base_path)
base_dir = os.path.dirname(base_path)
return os.path.relpath(target_path, base_dir)
def _GetBinaryPathsFromTargets(targets, build_dir):
"""Return binary paths from target names."""
# FIXME: Derive output binary from target build definitions rather than
......@@ -1449,7 +831,7 @@ def _GetBinaryPathsFromTargets(targets, build_dir):
binary_paths = []
for target in targets:
binary_path = os.path.join(build_dir, target)
if _GetHostPlatform() == 'win':
if coverage_utils.GetHostPlatform() == 'win':
binary_path += '.exe'
if os.path.exists(binary_path):
......@@ -1462,12 +844,6 @@ def _GetBinaryPathsFromTargets(targets, build_dir):
return binary_paths
def _GetFullPath(path):
"""Return full absolute path."""
return (os.path.abspath(
os.path.realpath(os.path.expandvars(os.path.expanduser(path)))))
def _GetCommandForWebTests(arguments):
"""Return command to run for blink web tests."""
command_list = [
......@@ -1475,8 +851,7 @@ def _GetCommandForWebTests(arguments):
'third_party/blink/tools/run_web_tests.py',
'--additional-driver-flag=--no-sandbox',
'--additional-env-var=LLVM_PROFILE_FILE=%s' %
LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
'--batch-size=1',
LLVM_PROFILE_FILE_PATH_SUBSTITUTION, '--batch-size=1',
'--child-processes=%d' % max(1, int(multiprocessing.cpu_count() / 2)),
'--disable-breakpad', '--no-show-results', '--skip-failing-tests',
'--target=%s' % os.path.basename(BUILD_DIR), '--time-out-ms=30000'
......@@ -1488,7 +863,7 @@ def _GetCommandForWebTests(arguments):
def _GetBinaryPathForWebTests():
"""Return binary path used to run blink web tests."""
host_platform = _GetHostPlatform()
host_platform = coverage_utils.GetHostPlatform()
if host_platform == 'win':
return os.path.join(BUILD_DIR, 'content_shell.exe')
elif host_platform == 'linux':
......@@ -1506,7 +881,7 @@ def _SetupOutputDir():
shutil.rmtree(OUTPUT_DIR)
# Creates |OUTPUT_DIR| and its platform sub-directory.
os.makedirs(_GetCoverageReportRootDirPath())
os.makedirs(coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR))
def _ParseCommandArguments():
......@@ -1585,6 +960,11 @@ def _ParseCommandArguments():
'are large number of html files, the file view becomes heavy and may '
'cause the browser to freeze, and this argument comes handy.')
arg_parser.add_argument(
'--no-component-view',
action='store_true',
help='Don\'t generate the component view in the coverage report.')
arg_parser.add_argument(
'--coverage-tools-dir',
type=str,
......@@ -1633,19 +1013,19 @@ def Main():
# Change directory to source root to aid in relative paths calculations.
global SRC_ROOT_PATH
SRC_ROOT_PATH = _GetFullPath(
SRC_ROOT_PATH = coverage_utils.GetFullPath(
os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
os.chdir(SRC_ROOT_PATH)
args = _ParseCommandArguments()
_ConfigureLogging(args)
coverage_utils.ConfigureLogging(verbose=args.verbose, log_file=args.log_file)
_ConfigureLLVMCoverageTools(args)
global BUILD_DIR
BUILD_DIR = _GetFullPath(args.build_dir)
BUILD_DIR = coverage_utils.GetFullPath(args.build_dir)
global OUTPUT_DIR
OUTPUT_DIR = _GetFullPath(args.output_dir)
OUTPUT_DIR = coverage_utils.GetFullPath(args.output_dir)
assert args.web_tests or args.command or args.profdata_file, (
'Need to either provide commands to run using -c/--command option OR '
......@@ -1691,43 +1071,31 @@ def Main():
profdata_file_path = args.profdata_file
binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
binary_paths.extend(_GetSharedLibraries(binary_paths))
binary_paths.extend(
coverage_utils.GetSharedLibraries(binary_paths, BUILD_DIR))
logging.info('Generating code coverage report in html (this can take a while '
'depending on size of target!).')
per_file_coverage_summary = _GeneratePerFileCoverageSummary(
per_file_summary_data = _GeneratePerFileCoverageSummary(
binary_paths, profdata_file_path, absolute_filter_paths,
args.ignore_filename_regex)
_GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
absolute_filter_paths,
args.ignore_filename_regex)
if not args.no_file_view:
_GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
per_file_coverage_summary)
_GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
per_file_coverage_summary,
args.no_file_view)
_GenerateDirectoryViewHtmlIndexFile()
component_to_directories = _ExtractComponentToDirectoriesMapping()
per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
component_to_directories, per_directory_coverage_summary)
_GeneratePerComponentCoverageInHtml(
per_component_coverage_summary, component_to_directories,
per_directory_coverage_summary, args.no_file_view)
_GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
args.no_file_view)
# The default index file is generated only for the list of source files, needs
# to overwrite it to display per directory coverage view by default.
_OverwriteHtmlReportsIndexFile()
_CleanUpOutputDir()
html_index_file_path = 'file://' + _GetFullPath(_GetHtmlIndexPath())
logging.info('Index file for html report is generated as: "%s".',
html_index_file_path)
component_mappings = None
if not args.no_component_view:
component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
# Call prepare here.
processor = coverage_utils.CoverageReportPostProcessor(
OUTPUT_DIR,
SRC_ROOT_PATH,
per_file_summary_data,
no_component_view=args.no_component_view,
no_file_view=args.no_file_view,
component_mappings=component_mappings)
processor.PrepareHtmlReport()
if __name__ == '__main__':
......
# Copyright 2018 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.
#
# The script intentionally does not have a shebang, as it is Py2/Py3 compatible.
import argparse
from collections import defaultdict
import functools
import jinja2
import json
import logging
import os
import re
import shutil
import subprocess
import sys
# The default name of the html coverage report for a directory.
DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
# Name of the html index files for different views.
COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
class CoverageSummary(object):
"""Encapsulates coverage summary representation."""
def __init__(self,
regions_total=0,
regions_covered=0,
functions_total=0,
functions_covered=0,
lines_total=0,
lines_covered=0):
"""Initializes CoverageSummary object."""
self._summary = {
'regions': {
'total': regions_total,
'covered': regions_covered
},
'functions': {
'total': functions_total,
'covered': functions_covered
},
'lines': {
'total': lines_total,
'covered': lines_covered
}
}
def Get(self):
"""Returns summary as a dictionary."""
return self._summary
def AddSummary(self, other_summary):
"""Adds another summary to this one element-wise."""
for feature in self._summary:
self._summary[feature]['total'] += other_summary.Get()[feature]['total']
self._summary[feature]['covered'] += other_summary.Get()[feature][
'covered']
class CoverageReportHtmlGenerator(object):
"""Encapsulates coverage html report generation.
The generated html has a table that contains links to other coverage reports.
"""
def __init__(self, output_dir, output_path, table_entry_type):
"""Initializes _CoverageReportHtmlGenerator object.
Args:
output_dir: Path to the dir for writing coverage report to.
output_path: Path to the html report that will be generated.
table_entry_type: Type of the table entries to be displayed in the table
header. For example: 'Path', 'Component'.
"""
css_file_name = os.extsep.join(['style', 'css'])
css_absolute_path = os.path.join(output_dir, css_file_name)
assert os.path.exists(css_absolute_path), (
'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
'is called first, and the css file is generated at: "%s".' %
css_absolute_path)
self._css_absolute_path = css_absolute_path
self._output_dir = output_dir
self._output_path = output_path
self._table_entry_type = table_entry_type
self._table_entries = []
self._total_entry = {}
source_dir = os.path.dirname(os.path.realpath(__file__))
template_dir = os.path.join(source_dir, 'html_templates')
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
self._header_template = jinja_env.get_template('header.html')
self._table_template = jinja_env.get_template('table.html')
self._footer_template = jinja_env.get_template('footer.html')
self._style_overrides = open(
os.path.join(source_dir, 'static', 'css', 'style.css')).read()
def AddLinkToAnotherReport(self, html_report_path, name, summary):
"""Adds a link to another html report in this report.
The link to be added is assumed to be an entry in this directory.
"""
# Use relative paths instead of absolute paths to make the generated reports
# portable.
html_report_relative_path = GetRelativePathToDirectoryOfFile(
html_report_path, self._output_path)
table_entry = self._CreateTableEntryFromCoverageSummary(
summary, html_report_relative_path, name,
os.path.basename(html_report_path) ==
DIRECTORY_COVERAGE_HTML_REPORT_NAME)
self._table_entries.append(table_entry)
def CreateTotalsEntry(self, summary):
"""Creates an entry corresponds to the 'Totals' row in the html report."""
self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
def _CreateTableEntryFromCoverageSummary(self,
summary,
href=None,
name=None,
is_dir=None):
"""Creates an entry to display in the html report."""
assert (href is None and name is None and is_dir is None) or (
href is not None and name is not None and is_dir is not None), (
'The only scenario when href or name or is_dir can be None is when '
'creating an entry for the Totals row, and in that case, all three '
'attributes must be None.')
entry = {}
if href is not None:
entry['href'] = href
if name is not None:
entry['name'] = name
if is_dir is not None:
entry['is_dir'] = is_dir
summary_dict = summary.Get()
for feature in summary_dict:
if summary_dict[feature]['total'] == 0:
percentage = 0.0
else:
percentage = float(summary_dict[feature]
['covered']) / summary_dict[feature]['total'] * 100
color_class = self._GetColorClass(percentage)
entry[feature] = {
'total': summary_dict[feature]['total'],
'covered': summary_dict[feature]['covered'],
'percentage': '{:6.2f}'.format(percentage),
'color_class': color_class
}
return entry
def _GetColorClass(self, percentage):
"""Returns the css color class based on coverage percentage."""
if percentage >= 0 and percentage < 80:
return 'red'
if percentage >= 80 and percentage < 100:
return 'yellow'
if percentage == 100:
return 'green'
assert False, 'Invalid coverage percentage: "%d".' % percentage
def WriteHtmlCoverageReport(self, no_component_view, no_file_view):
"""Writes html coverage report.
In the report, sub-directories are displayed before files and within each
category, entries are sorted alphabetically.
"""
def EntryCmp(left, right):
"""Compare function for table entries."""
if left['is_dir'] != right['is_dir']:
return -1 if left['is_dir'] == True else 1
return -1 if left['name'] < right['name'] else 1
self._table_entries = sorted(
self._table_entries, key=functools.cmp_to_key(EntryCmp))
css_path = os.path.join(self._output_dir, os.extsep.join(['style', 'css']))
directory_view_path = GetDirectoryViewPath(self._output_dir)
directory_view_href = GetRelativePathToDirectoryOfFile(
directory_view_path, self._output_path)
component_view_href = None
if not no_component_view:
component_view_path = GetComponentViewPath(self._output_dir)
component_view_href = GetRelativePathToDirectoryOfFile(
component_view_path, self._output_path)
# File view is optional in the report.
file_view_href = None
if not no_file_view:
file_view_path = GetFileViewPath(self._output_dir)
file_view_href = GetRelativePathToDirectoryOfFile(file_view_path,
self._output_path)
html_header = self._header_template.render(
css_path=GetRelativePathToDirectoryOfFile(css_path, self._output_path),
directory_view_href=directory_view_href,
component_view_href=component_view_href,
file_view_href=file_view_href,
style_overrides=self._style_overrides)
html_table = self._table_template.render(
entries=self._table_entries,
total_entry=self._total_entry,
table_entry_type=self._table_entry_type)
html_footer = self._footer_template.render()
with open(self._output_path, 'w') as html_file:
html_file.write(html_header + html_table + html_footer)
class CoverageReportPostProcessor(object):
"""Post processing of code coverage reports produced by llvm-cov."""
def __init__(self,
output_dir,
src_root_dir,
summary_data,
no_component_view,
no_file_view,
component_mappings={}):
"""Initializes CoverageReportPostProcessor object."""
# Caller provided parameters.
self.output_dir = output_dir
self.src_root_dir = src_root_dir
self.summary_data = json.loads(summary_data)
assert len(self.summary_data['data']) == 1
self.no_component_view = no_component_view
self.no_file_view = no_file_view
# Mapping from components to directories
self.component_to_directories = None
if component_mappings:
self._ExtractComponentToDirectoriesMapping(component_mappings)
# The root directory that contains all generated coverage html reports.
self.report_root_dir = GetCoverageReportRootDirPath(self.output_dir)
# Path to the HTML file for the component view.
self.component_view_path = GetComponentViewPath(self.output_dir)
# Path to the HTML file for the directory view.
self.directory_view_path = GetDirectoryViewPath(self.output_dir)
# Path to the HTML file for the file view.
self.file_view_path = GetFileViewPath(self.output_dir)
# Path to the main HTML index file.
self.html_index_path = GetHtmlIndexPath(self.output_dir)
def _ExtractComponentToDirectoriesMapping(self, component_mappings):
"""Initializes a mapping from components to directories."""
directory_to_component = component_mappings['dir-to-component']
self.component_to_directories = defaultdict(list)
for directory in sorted(directory_to_component):
component = directory_to_component[directory]
# Check if we already added the parent directory of this directory. If
# yes,skip this sub-directory to avoid double-counting.
found_parent_directory = False
for component_directory in self.component_to_directories[component]:
if directory.startswith(component_directory + '/'):
found_parent_directory = True
break
if not found_parent_directory:
self.component_to_directories[component].append(directory)
def CalculatePerDirectoryCoverageSummary(self, per_file_coverage_summary):
"""Calculates per directory coverage summary."""
logging.debug('Calculating per-directory coverage summary.')
per_directory_coverage_summary = defaultdict(lambda: CoverageSummary())
for file_path in per_file_coverage_summary:
summary = per_file_coverage_summary[file_path]
parent_dir = os.path.dirname(file_path)
while True:
per_directory_coverage_summary[parent_dir].AddSummary(summary)
if parent_dir == self.src_root_dir:
break
parent_dir = os.path.dirname(parent_dir)
logging.debug('Finished calculating per-directory coverage summary.')
return per_directory_coverage_summary
def CalculatePerComponentCoverageSummary(self,
per_directory_coverage_summary):
"""Calculates per component coverage summary."""
logging.debug('Calculating per-component coverage summary.')
per_component_coverage_summary = defaultdict(lambda: CoverageSummary())
for component in self.component_to_directories:
for directory in self.component_to_directories[component]:
absolute_directory_path = GetFullPath(directory)
if absolute_directory_path in per_directory_coverage_summary:
per_component_coverage_summary[component].AddSummary(
per_directory_coverage_summary[absolute_directory_path])
logging.debug('Finished calculating per-component coverage summary.')
return per_component_coverage_summary
def GeneratePerComponentCoverageInHtml(self, per_component_coverage_summary,
per_directory_coverage_summary):
"""Generates per-component coverage reports in html."""
logging.debug('Writing per-component coverage html reports.')
for component in per_component_coverage_summary:
self.GenerateCoverageInHtmlForComponent(component,
per_component_coverage_summary,
per_directory_coverage_summary)
logging.debug('Finished writing per-component coverage html reports.')
def GenerateComponentViewHtmlIndexFile(self, per_component_coverage_summary):
"""Generates the html index file for component view."""
component_view_index_file_path = self.component_view_path
logging.debug('Generating component view html index file as: "%s".',
component_view_index_file_path)
html_generator = CoverageReportHtmlGenerator(
self.output_dir, component_view_index_file_path, 'Component')
for component in per_component_coverage_summary:
html_generator.AddLinkToAnotherReport(
self.GetCoverageHtmlReportPathForComponent(component), component,
per_component_coverage_summary[component])
# Do not create a totals row for the component view as the value is
# incorrect due to failure to account for UNKNOWN component and some paths
# belonging to multiple components.
html_generator.WriteHtmlCoverageReport(self.no_component_view,
self.no_file_view)
logging.debug('Finished generating component view html index file.')
def GenerateCoverageInHtmlForComponent(self, component_name,
per_component_coverage_summary,
per_directory_coverage_summary):
"""Generates coverage html report for a component."""
component_html_report_path = self.GetCoverageHtmlReportPathForComponent(
component_name)
component_html_report_dir = os.path.dirname(component_html_report_path)
if not os.path.exists(component_html_report_dir):
os.makedirs(component_html_report_dir)
html_generator = CoverageReportHtmlGenerator(
self.output_dir, component_html_report_path, 'Path')
for dir_path in self.component_to_directories[component_name]:
dir_absolute_path = GetFullPath(dir_path)
if dir_absolute_path not in per_directory_coverage_summary:
# Any directory without an exercised file shouldn't be included into
# the report.
continue
html_generator.AddLinkToAnotherReport(
self.GetCoverageHtmlReportPathForDirectory(dir_path),
os.path.relpath(dir_path, self.src_root_dir),
per_directory_coverage_summary[dir_absolute_path])
html_generator.CreateTotalsEntry(
per_component_coverage_summary[component_name])
html_generator.WriteHtmlCoverageReport(self.no_component_view,
self.no_file_view)
def GetCoverageHtmlReportPathForComponent(self, component_name):
"""Given a component, returns the corresponding html report path."""
component_file_name = component_name.lower().replace('>', '-')
html_report_name = os.extsep.join([component_file_name, 'html'])
return os.path.join(self.report_root_dir, 'components', html_report_name)
def GetCoverageHtmlReportPathForDirectory(self, dir_path):
"""Given a directory path, returns the corresponding html report path."""
assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
html_report_path = os.path.join(
GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
# '+' is used instead of os.path.join because both of them are absolute
# paths and os.path.join ignores the first path.
# TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
return self.report_root_dir + html_report_path
def GetCoverageHtmlReportPathForFile(self, file_path):
"""Given a file path, returns the corresponding html report path."""
assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
html_report_path = os.extsep.join([GetFullPath(file_path), 'html'])
# '+' is used instead of os.path.join because both of them are absolute
# paths and os.path.join ignores the first path.
# TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
return self.report_root_dir + html_report_path
def GenerateFileViewHtmlIndexFile(self, per_file_coverage_summary,
file_view_index_file_path):
"""Generates html index file for file view."""
logging.debug('Generating file view html index file as: "%s".',
file_view_index_file_path)
html_generator = CoverageReportHtmlGenerator(
self.output_dir, file_view_index_file_path, 'Path')
totals_coverage_summary = CoverageSummary()
for file_path in per_file_coverage_summary:
totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
html_generator.AddLinkToAnotherReport(
self.GetCoverageHtmlReportPathForFile(file_path),
os.path.relpath(file_path, self.src_root_dir),
per_file_coverage_summary[file_path])
html_generator.CreateTotalsEntry(totals_coverage_summary)
html_generator.WriteHtmlCoverageReport(self.no_component_view,
self.no_file_view)
logging.debug('Finished generating file view html index file.')
def GeneratePerFileCoverageSummary(self):
"""Generate per file coverage summary using coverage data in JSON format."""
files_coverage_data = self.summary_data['data'][0]['files']
per_file_coverage_summary = {}
for file_coverage_data in files_coverage_data:
file_path = file_coverage_data['filename']
assert file_path.startswith(self.src_root_dir + os.sep), (
'File path "%s" in coverage summary is outside source checkout.' %
file_path)
summary = file_coverage_data['summary']
if summary['lines']['count'] == 0:
continue
per_file_coverage_summary[file_path] = CoverageSummary(
regions_total=summary['regions']['count'],
regions_covered=summary['regions']['covered'],
functions_total=summary['functions']['count'],
functions_covered=summary['functions']['covered'],
lines_total=summary['lines']['count'],
lines_covered=summary['lines']['covered'])
logging.debug('Finished generating per-file code coverage summary.')
return per_file_coverage_summary
def GeneratePerDirectoryCoverageInHtml(self, per_directory_coverage_summary,
per_file_coverage_summary):
"""Generates per directory coverage breakdown in html."""
logging.debug('Writing per-directory coverage html reports.')
for dir_path in per_directory_coverage_summary:
self.GenerateCoverageInHtmlForDirectory(
dir_path, per_directory_coverage_summary, per_file_coverage_summary)
logging.debug('Finished writing per-directory coverage html reports.')
def GenerateCoverageInHtmlForDirectory(self, dir_path,
per_directory_coverage_summary,
per_file_coverage_summary):
"""Generates coverage html report for a single directory."""
html_generator = CoverageReportHtmlGenerator(
self.output_dir, self.GetCoverageHtmlReportPathForDirectory(dir_path),
'Path')
for entry_name in os.listdir(dir_path):
entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
if entry_path in per_file_coverage_summary:
entry_html_report_path = self.GetCoverageHtmlReportPathForFile(
entry_path)
entry_coverage_summary = per_file_coverage_summary[entry_path]
elif entry_path in per_directory_coverage_summary:
entry_html_report_path = self.GetCoverageHtmlReportPathForDirectory(
entry_path)
entry_coverage_summary = per_directory_coverage_summary[entry_path]
else:
# Any file without executable lines shouldn't be included into the
# report. For example, OWNER and README.md files.
continue
html_generator.AddLinkToAnotherReport(entry_html_report_path,
os.path.basename(entry_path),
entry_coverage_summary)
html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
html_generator.WriteHtmlCoverageReport(self.no_component_view,
self.no_file_view)
def GenerateDirectoryViewHtmlIndexFile(self):
"""Generates the html index file for directory view.
Note that the index file is already generated under src_root_dir, so this
file simply redirects to it, and the reason of this extra layer is for
structural consistency with other views.
"""
directory_view_index_file_path = self.directory_view_path
logging.debug('Generating directory view html index file as: "%s".',
directory_view_index_file_path)
src_root_html_report_path = self.GetCoverageHtmlReportPathForDirectory(
self.src_root_dir)
WriteRedirectHtmlFile(directory_view_index_file_path,
src_root_html_report_path)
logging.debug('Finished generating directory view html index file.')
def RenameDefaultCoverageDirectory(self):
"""Rename the default coverage directory into platform specific name."""
# llvm-cov creates "coverage" subdir in the output dir. We would like to use
# the platform name instead, as it simplifies the report dir structure when
# the same report is generated for different platforms.
default_report_subdir_path = os.path.join(self.output_dir, 'coverage')
if not os.path.exists(default_report_subdir_path):
logging.error('Default coverage report dir does not exist: %s.',
default_report_subdir_path)
if not os.path.exists(self.report_root_dir):
os.mkdir(self.report_root_dir)
MergeTwoDirectories(default_report_subdir_path, self.report_root_dir)
def OverwriteHtmlReportsIndexFile(self):
"""Overwrites the root index file to redirect to the default view."""
html_index_file_path = self.html_index_path
directory_view_index_file_path = self.directory_view_path
WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
def CleanUpOutputDir(self):
"""Perform a cleanup of the output dir."""
# Remove the default index.html file produced by llvm-cov.
index_path = os.path.join(self.output_dir, INDEX_HTML_FILE)
if os.path.exists(index_path):
os.remove(index_path)
def PrepareHtmlReport(self):
self.RenameDefaultCoverageDirectory()
per_file_coverage_summary = self.GeneratePerFileCoverageSummary()
if not self.no_file_view:
self.GenerateFileViewHtmlIndexFile(per_file_coverage_summary,
self.file_view_path)
per_directory_coverage_summary = self.CalculatePerDirectoryCoverageSummary(
per_file_coverage_summary)
self.GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
per_file_coverage_summary)
self.GenerateDirectoryViewHtmlIndexFile()
if not self.no_component_view:
per_component_coverage_summary = (
self.CalculatePerComponentCoverageSummary(
per_directory_coverage_summary))
self.GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
per_directory_coverage_summary)
self.GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
# The default index file is generated only for the list of source files,
# needs to overwrite it to display per directory coverage view by default.
self.OverwriteHtmlReportsIndexFile()
self.CleanUpOutputDir()
html_index_file_path = 'file://' + GetFullPath(self.html_index_path)
logging.info('Index file for html report is generated as: "%s".',
html_index_file_path)
def ConfigureLogging(verbose=False, log_file=None):
"""Configures logging settings for later use."""
log_level = logging.DEBUG if verbose else logging.INFO
log_format = '[%(asctime)s %(levelname)s] %(message)s'
logging.basicConfig(filename=log_file, level=log_level, format=log_format)
def GetComponentViewPath(output_dir):
"""Path to the HTML file for the component view."""
return os.path.join(
GetCoverageReportRootDirPath(output_dir), COMPONENT_VIEW_INDEX_FILE)
def GetCoverageReportRootDirPath(output_dir):
"""The root directory that contains all generated coverage html reports."""
return os.path.join(output_dir, GetHostPlatform())
def GetDirectoryViewPath(output_dir):
"""Path to the HTML file for the directory view."""
return os.path.join(
GetCoverageReportRootDirPath(output_dir), DIRECTORY_VIEW_INDEX_FILE)
def GetFileViewPath(output_dir):
"""Path to the HTML file for the file view."""
return os.path.join(
GetCoverageReportRootDirPath(output_dir), FILE_VIEW_INDEX_FILE)
def GetHtmlIndexPath(output_dir):
"""Path to the main HTML index file."""
return os.path.join(GetCoverageReportRootDirPath(output_dir), INDEX_HTML_FILE)
def GetFullPath(path):
"""Return full absolute path."""
return (os.path.abspath(
os.path.realpath(os.path.expandvars(os.path.expanduser(path)))))
def GetHostPlatform():
"""Returns the host platform.
This is separate from the target platform/os that coverage is running for.
"""
if sys.platform == 'win32' or sys.platform == 'cygwin':
return 'win'
if sys.platform.startswith('linux'):
return 'linux'
else:
assert sys.platform == 'darwin'
return 'mac'
def GetRelativePathToDirectoryOfFile(target_path, base_path):
"""Returns a target path relative to the directory of base_path.
This method requires base_path to be a file, otherwise, one should call
os.path.relpath directly.
"""
assert os.path.dirname(base_path) != base_path, (
'Base path: "%s" is a directory, please call os.path.relpath directly.' %
base_path)
base_dir = os.path.dirname(base_path)
return os.path.relpath(target_path, base_dir)
def GetSharedLibraries(binary_paths, build_dir):
"""Returns list of shared libraries used by specified binaries."""
logging.info('Finding shared libraries for targets (if any).')
shared_libraries = []
cmd = []
shared_library_re = None
if sys.platform.startswith('linux'):
cmd.extend(['ldd'])
shared_library_re = re.compile(r'.*\.so[.0-9]*\s=>\s(.*' + build_dir +
r'.*\.so[.0-9]*)\s.*')
elif sys.platform.startswith('darwin'):
cmd.extend(['otool', '-L'])
shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
else:
assert False, 'Cannot detect shared libraries used by the given targets.'
assert shared_library_re is not None
cmd.extend(binary_paths)
output = subprocess.check_output(cmd).decode('utf-8', 'ignore')
for line in output.splitlines():
m = shared_library_re.match(line)
if not m:
continue
shared_library_path = m.group(1)
if sys.platform.startswith('darwin'):
# otool outputs "@rpath" macro instead of the dirname of the given binary.
shared_library_path = shared_library_path.replace('@rpath', build_dir)
if shared_library_path in shared_libraries:
continue
assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
'the given target(s) does not '
'exist.' % shared_library_path)
with open(shared_library_path, 'rb') as f:
data = f.read()
# Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
if b'__llvm_cov' in data:
shared_libraries.append(shared_library_path)
logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
shared_libraries)
logging.info('Finished finding shared libraries for targets.')
return shared_libraries
def MergeTwoDirectories(src_dir_path, dst_dir_path):
"""Merge src_dir_path directory into dst_path directory."""
for filename in os.listdir(src_dir_path):
dst_path = os.path.join(dst_dir_path, filename)
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
os.rename(os.path.join(src_dir_path, filename), dst_path)
shutil.rmtree(src_dir_path)
def WriteRedirectHtmlFile(from_html_path, to_html_path):
"""Writes a html file that redirects to another html file."""
to_html_relative_path = GetRelativePathToDirectoryOfFile(
to_html_path, from_html_path)
content = ("""
<!DOCTYPE html>
<html>
<head>
<!-- HTML meta refresh URL redirection -->
<meta http-equiv="refresh" content="0; url=%s">
</head>
</html>""" % to_html_relative_path)
with open(from_html_path, 'w') as f:
f.write(content)
def _CmdSharedLibraries(args):
"""Handles 'shared_libs' command."""
if not args.object:
logging.error('No binaries are specified.')
return 1
paths = GetSharedLibraries(args.object, args.build_dir)
if not paths:
return 0
# Print output in the format that can be passed to llvm-cov tool.
output = ' '.join(['-object=%s' % path for path in paths])
print(output)
return 0
def _CmdPostProcess(args):
"""Handles 'post_process' command."""
with open(args.summary_file) as f:
summary_data = f.read()
processor = CoverageReportPostProcessor(
args.output_dir,
args.src_root_dir,
summary_data,
no_component_view=True,
no_file_view=False)
processor.PrepareHtmlReport()
def Main():
parser = argparse.ArgumentParser(
'coverage_utils', description='Code coverage utils.')
parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='Prints additional debug output.')
subparsers = parser.add_subparsers(dest='command')
shared_libs_parser = subparsers.add_parser(
'shared_libs', help='Detect shared libraries.')
shared_libs_parser.add_argument('-build-dir', help='Path to the build dir.')
shared_libs_parser.add_argument(
'-object', action='append', help='Path to the binary using shared libs.')
post_processing_parser = subparsers.add_parser(
'post_process', help='Post process a report.')
post_processing_parser.add_argument(
'-output-dir', help='Path to the report dir.')
post_processing_parser.add_argument(
'-src-root-dir', help='Path to the src root dir.')
post_processing_parser.add_argument(
'-summary-file', help='Path to the summary file.')
args = parser.parse_args()
ConfigureLogging(args.verbose)
if args.command == 'shared_libs':
return _CmdSharedLibraries(args)
elif args.command == 'post_process':
return _CmdPostProcess(args)
if __name__ == '__main__':
sys.exit(Main())
......@@ -12,7 +12,11 @@
<body>
<h2>Coverage Report</h2>
<p>
View results by: <a href='{{ component_view_href }}'>Components</a> | <a href='{{ directory_view_href }}'>Directories</a>
View results by:
{% if component_view_href %}
<a href='{{ component_view_href }}'>Components</a> |
{% endif %}
<a href='{{ directory_view_href }}'>Directories</a>
{% if file_view_href %}
| <a href='{{ file_view_href }}'>Files</a>
{% endif %}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment