blob: e7f20ac45a4ef5b8120f715d6a03eb4e17b172dc [file] [log] [blame]
# 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={},
path_equivalence=None):
"""Initializes CoverageReportPostProcessor object."""
# Caller provided parameters.
self.output_dir = output_dir
self.src_root_dir = os.path.normpath(GetFullPath(src_root_dir))
if not self.src_root_dir.endswith(os.sep):
self.src_root_dir += os.sep
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)
self.path_map = None
if path_equivalence:
def _PreparePath(path):
path = os.path.normpath(path)
if not path.endswith(os.sep):
# A normalized path does not end with '/', unless it is a root dir.
path += os.sep
return path
self.path_map = [_PreparePath(p) for p in path_equivalence.split(',')]
assert len(self.path_map) == 2, 'Path equivalence argument is incorrect.'
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 _MapToLocal(self, path):
"""Maps a path from the coverage data to a local path."""
if not self.path_map:
return path
return path.replace(self.path_map[0], self.path_map[1], 1)
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 os.path.normpath(parent_dir) == os.path.normpath(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(
self._MapToLocal(dir_path)), '"%s" is not a directory.' % dir_path
html_report_path = os.path.join(
GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
return self.CombineAbsolutePaths(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(
self._MapToLocal(file_path)), '"%s" is not a file.' % file_path
html_report_path = os.extsep.join([GetFullPath(file_path), 'html'])
return self.CombineAbsolutePaths(self.report_root_dir, html_report_path)
def CombineAbsolutePaths(self, path1, path2):
if GetHostPlatform() == 'win':
# Absolute paths in Windows may start with a drive letter and colon.
# Remove them from the second path before appending to the first.
_, path2 = os.path.splitdrive(path2)
# '+' is used instead of os.path.join because both of them are absolute
# paths and os.path.join ignores the first path.
return path1 + path2
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), (
'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(self._MapToLocal(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.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, otool_path):
"""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'):
otool = otool_path if otool_path else 'otool'
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
library_paths = GetSharedLibraries(args.object, args.build_dir, None)
if not library_paths:
return 0
# Print output in the format that can be passed to llvm-cov tool.
output = ' '.join(
'-object=%s' % os.path.normpath(path) for path in library_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,
path_equivalence=args.path_equivalence)
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.', required=True)
shared_libs_parser.add_argument(
'-object',
action='append',
help='Path to the binary using shared libs.',
required=True)
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.', required=True)
post_processing_parser.add_argument(
'-src-root-dir', help='Path to the src root dir.', required=True)
post_processing_parser.add_argument(
'-summary-file', help='Path to the summary file.', required=True)
post_processing_parser.add_argument(
'-path-equivalence',
help='Map the paths in the coverage data to local '
'source files path (=<from>,<to>)')
args = parser.parse_args()
ConfigureLogging(args.verbose)
if args.command == 'shared_libs':
return _CmdSharedLibraries(args)
elif args.command == 'post_process':
return _CmdPostProcess(args)
else:
parser.print_help(sys.stderr)
if __name__ == '__main__':
sys.exit(Main())