[Coverage] Implements components view in code coverage report.
This CL implements a feature to display code coverage by components.
The high-level structure of this change contains three different views:
- directory_view: organizing by top level directories under src/.
- component_view: organizing by components.
- file_view: organizing by each single file.
The directory_view is the default view to which index.html redirects to,
and each html report contains links to toggle between the three views.
Bug: 799632
Change-Id: I3df09fef2dfa0c8da2535f03d02d478b9c51bc8c
Reviewed-on: https://chromium-review.googlesource.com/894992
Commit-Queue: Yuke Liao <[email protected]>
Reviewed-by: Abhishek Arya <[email protected]>
Cr-Commit-Position: refs/heads/master@{#533896}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index 29d03b7..f1797b9 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -105,12 +105,25 @@
# 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.
+DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
+COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
+FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
+
+# Used to extract a mapping between directories and components.
+COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
+
class _CoverageSummary(object):
"""Encapsulates coverage summary representation."""
- def __init__(self, regions_total, regions_covered, functions_total,
- functions_covered, lines_total, lines_covered):
+ 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': {
@@ -139,16 +152,20 @@
'covered']
-class _DirectoryCoverageReportHtmlGenerator(object):
- """Encapsulates coverage html report generation for a directory.
+class _CoverageReportHtmlGenerator(object):
+ """Encapsulates coverage html report generation.
- The generated html has a table that contains links to the coverage report of
- its sub-directories and files. Please refer to ./directory_example_report.html
- for an example of the generated html file.
+ The generated html has a table that contains links to other coverage reports.
"""
- def __init__(self):
- """Initializes _DirectoryCoverageReportHtmlGenerator object."""
+ 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.abspath(os.path.join(OUTPUT_DIR, css_file_name))
assert os.path.exists(css_absolute_path), (
@@ -157,6 +174,9 @@
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 = {}
template_dir = os.path.join(
@@ -173,8 +193,13 @@
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_path, name,
+ summary, html_report_relative_path, name,
os.path.basename(html_report_path) ==
DIRECTORY_COVERAGE_HTML_REPORT_NAME)
self._table_entries.append(table_entry)
@@ -189,11 +214,27 @@
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:
- percentage = round((float(summary_dict[feature]['covered']
- ) / summary_dict[feature]['total']) * 100, 2)
+ if summary_dict[feature]['total'] == 0:
+ percentage = 0.0
+ else:
+ percentage = round((float(summary_dict[feature]['covered']
+ ) / summary_dict[feature]['total']) * 100, 2)
color_class = self._GetColorClass(percentage)
entry[feature] = {
'total': summary_dict[feature]['total'],
@@ -202,13 +243,6 @@
'color_class': color_class
}
- if href != None:
- entry['href'] = href
- if name != None:
- entry['name'] = name
- if is_dir != None:
- entry['is_dir'] = is_dir
-
return entry
def _GetColorClass(self, percentage):
@@ -222,14 +256,11 @@
assert False, 'Invalid coverage percentage: "%d"' % percentage
- def WriteHtmlCoverageReport(self, output_path):
- """Write html coverage report for the directory.
+ def WriteHtmlCoverageReport(self):
+ """Writes html coverage report.
In the report, sub-directories are displayed before files and within each
category, entries are sorted alphabetically.
-
- Args:
- output_path: A path to the html report.
"""
def EntryCmp(left, right):
@@ -237,18 +268,31 @@
if left['is_dir'] != right['is_dir']:
return -1 if left['is_dir'] == True else 1
- return left['name'] < right['name']
+ 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 = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
+ component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
+ file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
+
html_header = self._header_template.render(
- css_path=os.path.relpath(css_path, os.path.dirname(output_path)))
+ css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
+ directory_view_href=_GetRelativePathToDirectoryOfFile(
+ directory_view_path, self._output_path),
+ component_view_href=_GetRelativePathToDirectoryOfFile(
+ component_view_path, self._output_path),
+ file_view_href=_GetRelativePathToDirectoryOfFile(
+ file_view_path, self._output_path))
+
html_table = self._table_template.render(
- entries=self._table_entries, total_entry=self._total_entry)
+ entries=self._table_entries,
+ total_entry=self._total_entry,
+ table_entry_type=self._table_entry_type)
html_footer = self._footer_template.render()
- with open(output_path, 'w') as html_file:
+ with open(self._output_path, 'w') as html_file:
html_file.write(html_header + html_table + html_footer)
@@ -337,8 +381,8 @@
'Failed to download coverage tools: %s.' % coverage_tools_url)
-def _GenerateLineByLineFileCoverageInHtml(binary_paths, profdata_file_path,
- filters):
+def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
+ filters):
"""Generates per file line-by-line coverage in html using 'llvm-cov show'.
For a file with absolute path /a/b/x.cc, a html report is generated as:
@@ -368,16 +412,33 @@
logging.debug('Finished running "llvm-cov show" command')
-def _GeneratePerDirectoryCoverageInHtml(binary_paths, profdata_file_path,
- filters):
- """Generates coverage breakdown per directory."""
- logging.debug('Calculating and writing per-directory coverage reports')
- per_file_coverage_summary = _GeneratePerFileCoverageSummary(
- binary_paths, profdata_file_path, filters)
- per_directory_coverage_summary = defaultdict(
- lambda: _CoverageSummary(0, 0, 0, 0, 0, 0))
+def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
+ """Generates html index file for file view."""
+ file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
+ 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()
- # Calculate per directory code coverage summaries.
+ 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()
+ 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)
@@ -388,49 +449,173 @@
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):
+ """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)
- logging.debug(
- 'Finished calculating and writing per-directory coverage reports')
+ logging.debug('Finished writing per-directory coverage html reports')
def _GenerateCoverageInHtmlForDirectory(
dir_path, per_directory_coverage_summary, per_file_coverage_summary):
"""Generates coverage html report for a single directory."""
- html_generator = _DirectoryCoverageReportHtmlGenerator()
+ 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))
- entry_html_report_path = _GetCoverageHtmlReportPath(entry_path)
- # Use relative paths instead of absolute paths to make the generated
- # reports portable.
- html_report_path = _GetCoverageHtmlReportPath(dir_path)
- entry_html_report_relative_path = os.path.relpath(
- entry_html_report_path, os.path.dirname(html_report_path))
+ 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:
+ continue
- if entry_path in per_directory_coverage_summary:
- html_generator.AddLinkToAnotherReport(
- entry_html_report_relative_path, os.path.basename(entry_path),
- per_directory_coverage_summary[entry_path])
- elif entry_path in per_file_coverage_summary:
- html_generator.AddLinkToAnotherReport(
- entry_html_report_relative_path, os.path.basename(entry_path),
- per_file_coverage_summary[entry_path])
+ 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(html_report_path)
+ html_generator.WriteHtmlCoverageReport()
+
+
+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 = os.path.join(OUTPUT_DIR,
+ DIRECTORY_VIEW_INDEX_FILE)
+ 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 = os.path.abspath(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 directory_to_component:
+ component = directory_to_component[directory]
+ component_to_directories[component].append(directory)
+
+ return component_to_directories
+
+
+def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
+ component_to_directories,
+ 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:
+ _GenerateCoverageInHtmlForComponent(
+ component, per_component_coverage_summary, component_to_directories,
+ per_directory_coverage_summary)
+
+ logging.debug('Finished writing per-component coverage html reports.')
+
+
+def _GenerateCoverageInHtmlForComponent(
+ component_name, per_component_coverage_summary, component_to_directories,
+ per_directory_coverage_summary):
+ """Generates coverage html report for a component."""
+ component_html_report_path = _GetCoverageHtmlReportPathForComponent(
+ component_name)
+ component_html_report_dirname = os.path.dirname(component_html_report_path)
+ if not os.path.exists(component_html_report_dirname):
+ os.makedirs(component_html_report_dirname)
+
+ html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
+ 'Path')
+
+ for dir_path in component_to_directories[component_name]:
+ dir_absolute_path = os.path.abspath(dir_path)
+ if dir_absolute_path not in per_directory_coverage_summary:
+ 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()
+
+
+def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
+ """Generates the html index file for component view."""
+ component_view_index_file_path = os.path.join(OUTPUT_DIR,
+ COMPONENT_VIEW_INDEX_FILE)
+ logging.debug('Generating component view html index file as: "%s".',
+ component_view_index_file_path)
+ html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
+ 'Component')
+ totals_coverage_summary = _CoverageSummary()
+
+ for component in per_component_coverage_summary:
+ totals_coverage_summary.AddSummary(
+ per_component_coverage_summary[component])
+
+ html_generator.AddLinkToAnotherReport(
+ _GetCoverageHtmlReportPathForComponent(component), component,
+ per_component_coverage_summary[component])
+
+ html_generator.CreateTotalsEntry(totals_coverage_summary)
+ html_generator.WriteHtmlCoverageReport()
+ logging.debug('Generating component view html index file.')
def _OverwriteHtmlReportsIndexFile():
- """Overwrites the index file to link to the source root directory report."""
+ """Overwrites the root index file to redirect to the default view."""
html_index_file_path = os.path.join(OUTPUT_DIR,
os.extsep.join(['index', 'html']))
- src_root_html_report_path = _GetCoverageHtmlReportPath(SRC_ROOT_PATH)
- src_root_html_report_relative_path = os.path.relpath(
- src_root_html_report_path, os.path.dirname(html_index_file_path))
+ directory_view_index_file_path = os.path.join(OUTPUT_DIR,
+ DIRECTORY_VIEW_INDEX_FILE)
+ _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>
@@ -438,20 +623,43 @@
<!-- HTML meta refresh URL redirection -->
<meta http-equiv="refresh" content="0; url=%s">
</head>
- </html>""" % src_root_html_report_relative_path)
- with open(html_index_file_path, 'w') as f:
+ </html>""" % to_html_relative_path)
+ with open(from_html_path, 'w') as f:
f.write(content)
-def _GetCoverageHtmlReportPath(file_or_dir_path):
- """Given a file or directory, returns the corresponding html report path."""
- html_path = (
- os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage') +
- os.path.abspath(file_or_dir_path))
- if os.path.isdir(file_or_dir_path):
- return os.path.join(html_path, DIRECTORY_COVERAGE_HTML_REPORT_NAME)
- else:
- return os.extsep.join([html_path, 'html'])
+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([os.path.abspath(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.
+ 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(
+ os.path.abspath(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.
+ 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(os.path.abspath(OUTPUT_DIR), 'coverage')
def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
@@ -579,9 +787,7 @@
logging.info('Running command: "%s", the output is redirected to "%s"',
command, output_file_path)
output = subprocess.check_output(
- command.split(), env={
- 'LLVM_PROFILE_FILE': expected_profraw_file_path
- })
+ command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
with open(output_file_path, 'w') as output_file:
output_file.write(output)
@@ -742,6 +948,19 @@
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 directly, please call os.path.relpath directly.' %
+ base_path)
+ base_dirname = os.path.dirname(base_path)
+ return os.path.relpath(target_path, base_dirname)
+
+
def _ParseCommandArguments():
"""Adds and parses relevant arguments for tool comands.
@@ -810,8 +1029,9 @@
def Main():
"""Execute tool commands."""
- assert _GetPlatform() in ['linux', 'mac'], (
- 'Coverage is only supported on linux and mac platforms.')
+ assert _GetPlatform() in [
+ 'linux', 'mac'
+ ], ('Coverage is only supported on linux and mac platforms.')
assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
'called from the root '
'of checkout.')
@@ -850,13 +1070,28 @@
logging.info('Generating code coverage report in html (this can take a while '
'depending on size of target!)')
- _GenerateLineByLineFileCoverageInHtml(binary_paths, profdata_file_path,
- absolute_filter_paths)
- _GeneratePerDirectoryCoverageInHtml(binary_paths, profdata_file_path,
- absolute_filter_paths)
+ per_file_coverage_summary = _GeneratePerFileCoverageSummary(
+ binary_paths, profdata_file_path, absolute_filter_paths)
+ _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
+ absolute_filter_paths)
+ _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
+
+ per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
+ per_file_coverage_summary)
+ _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
+ per_file_coverage_summary)
+ _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)
+ _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 code coverage breakdown.
+ # to overwrite it to display per directory coverage view by default.
_OverwriteHtmlReportsIndexFile()
html_index_file_path = 'file://' + os.path.abspath(