blob: b416dd2f80ec909fe1a2a62a769188901fc9ee82 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to generate code coverage report for iOS.
The generated code coverage report excludes test files, and test files are
identified by postfixes: ['unittest.cc', 'unittest.mm', 'egtest.mm'].
NOTE: This script must be called from the root of checkout. It may take up to
a few minutes to generate a report for targets that depend on Chrome,
such as ios_chrome_unittests. To simply play with this tool, you are
suggested to start with 'url_unittests'.
Example usages:
ios/tools/coverage/coverage.py url_unittests -t url/ -o out/html
# Generate code coverage report for url_unittests for directory url/ and all
# generated artifacts are stored in out/html
ios/tools/coverage/coverage.py url_unittests -t url/ -i url/mojo -o out/html
# Only include files under url/mojo.
ios/tools/coverage/coverage.py url_unittests -t url/ -i url/mojo -o out/html
-r coverage.profdata
# Skip running tests and reuse the specified profile data file.
For more options, please refer to ios/tools/coverage/coverage.py -h
"""
import sys
import argparse
import ConfigParser
import os
import subprocess
import webbrowser
sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir,
os.path.pardir, os.path.pardir, 'third_party'))
import jinja2
BUILD_DIRECTORY = 'out/Coverage-iphonesimulator'
DEFAULT_GOMA_JOBS = 50
# Name of the final profdata file, and this file needs to be passed to
# "llvm-cov" command in order to call "llvm-cov show" to inspect the
# line-by-line coverage of specific files.
PROFDATA_FILE_NAME = 'coverage.profdata'
# The code coverage profraw data file is generated by running the tests with
# coverage configuration, and the path to the file is part of the log that can
# be identified with the following identifier.
PROFRAW_FILE_LOG_IDENTIFIER = 'Coverage data at '
# Only test targets with the following postfixes are considered to be valid.
VALID_TEST_TARGET_POSTFIXES = ['unittests', 'inttests', 'egtests']
# Used to determine if a test target is an earl grey test.
EARL_GREY_TEST_TARGET_POSTFIX = 'egtests'
# Used to determine if a file is a test file. The coverage of test files should
# be excluded from code coverage report.
# TODO(crbug.com/763957): Make test file identifiers configurable.
TEST_FILES_POSTFIXES = ['unittest.mm', 'unittest.cc', 'egtest.mm']
# The default name of the html coverage report for a directory.
DIRECTORY_COVERAGE_HTML_REPORT_NAME = 'coverage.html'
class _DirectoryCoverageReportHtmlGenerator(object):
"""Excapsulates code coverage html report generation for a directory.
The generated html has a table that contains all the code coverage of its
sub-directories and files. Please refer to ./example.html for an example of
the generated html file.
"""
def __init__(self, css_path):
"""Initializes _DirectoryCoverageReportHtmlGenerator object.
This class assumes that the css file generated by 'llvm-cov show' is reused.
Args:
css_path: A path to the css file.
"""
assert os.path.exists(css_path), ('css file: {} doesn\'t exist.'
.format(css_path))
self._css_path = css_path
self._table_entries = []
template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'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')
def AddTableEntry(self, html_report_path, name, total_lines, executed_lines):
"""Adds a file or directory entry to the directory html coverage report.
Args:
html_report_path: A relative path to the file or directory's html report.
name: Base name of the file or directory.
total_lines: total number of lines.
executed_lines: executed number of lines.
"""
coverage = float(executed_lines) / total_lines
if coverage < 0.8:
color_class = 'column-entry-red'
elif coverage < 1:
color_class = 'column-entry-yellow'
elif coverage == 1:
color_class = 'column-entry-green'
else:
assert False, ('coverage cannot be greater than 100%, however, {} has '
'coverage: {}').format(html_report_path, coverage)
percentage_coverage = round(coverage * 100, 2)
self._table_entries.append({'href': html_report_path,
'name': name,
'color_class': color_class,
'total_lines': total_lines,
'executed_lines': executed_lines,
'percentage_coverage': percentage_coverage})
def WriteHtmlCoverageReport(self, output_path):
"""Write html coverage report for the directory.
In the report, sub-directories are displayed before files and within each
category, entries are sorted by coverage in ascending order.
Args:
output_path: A path to the html report.
"""
dir_entries = [entry for entry in self._table_entries
if os.path.basename(entry['href']) ==
DIRECTORY_COVERAGE_HTML_REPORT_NAME]
file_entries = [entry for entry in self._table_entries
if entry not in dir_entries]
file_entries.sort(
key=lambda entry: float(entry['executed_lines']) / entry['total_lines'])
dir_entries.sort(
key=lambda entry: float(entry['executed_lines']) / entry['total_lines'])
html_header = self._header_template.render(
css_path=os.path.relpath(self._css_path, os.path.dirname(output_path)))
html_table = self._table_template.render(dir_entries=dir_entries,
file_entries=file_entries)
html_footer = self._footer_template.render()
with open(output_path, 'w') as html_file:
html_file.write(html_header + html_table + html_footer)
class _FileLineCoverageReport(object):
"""Encapsulates coverage calculations for files."""
def __init__(self):
"""Initializes FileLineCoverageReport object."""
self._coverage = {}
def AddFile(self, path, total_lines, executed_lines):
"""Adds a new file entry.
Args:
path: path to the file.
total_lines: Total number of lines.
executed_lines: Total number of executed lines.
"""
summary = {
'total': total_lines,
'executed': executed_lines
}
self._coverage[path] = summary
def ContainsFile(self, path):
"""Returns True if the path is in the report.
Args:
path: path to the file.
Returns:
True if the path is in the report.
"""
return path in self._coverage
def GetCoverageForFile(self, path):
"""Returns tuple representing coverage for a file.
Args:
path: path to the file.
Returns:
tuple with two integers (total number of lines, number of executed lines.)
"""
assert path in self._coverage, '{} is not in the report.'.format(path)
return self._coverage[path]['total'], self._coverage[path]['executed']
def GetListOfFiles(self):
"""Returns a list of files in the report.
Returns:
A list of files.
"""
return self._coverage.keys()
def FilterFiles(self, include_paths, exclude_paths):
"""Filter files in the report.
Only includes files that is under at least one of the paths in
|include_paths|, but none of them in |exclude_paths|.
Args:
include_paths: A list of directories and files.
exclude_paths: A list of directories and files.
"""
files_to_delete = []
for path in self._coverage:
should_include = (any(os.path.abspath(path).startswith(
os.path.abspath(include_path))
for include_path in include_paths))
should_exclude = (any(os.path.abspath(path).startswith(
os.path.abspath(exclude_path))
for exclude_path in exclude_paths))
if not should_include or should_exclude:
files_to_delete.append(path)
for path in files_to_delete:
del self._coverage[path]
def ExcludeTestFiles(self):
"""Exclude test files from the report.
Test files are identified by |TEST_FILES_POSTFIXES|.
"""
files_to_delete = []
for path in self._coverage:
if any(path.endswith(postfix) for postfix in TEST_FILES_POSTFIXES):
files_to_delete.append(path)
for path in files_to_delete:
del self._coverage[path]
class _DirectoryLineCoverageReport(object):
"""Encapsulates coverage calculations for directories."""
def __init__(self, file_line_coverage_report, top_level_dir):
"""Initializes DirectoryLineCoverageReport object."""
self._coverage = {}
self._CalculateCoverageForDirectory(top_level_dir, self._coverage,
file_line_coverage_report)
def _CalculateCoverageForDirectory(self, path, line_coverage_result,
file_line_coverage_report):
"""Recursively calculates the line coverage for a directory.
Only directories that have non-zero number of total lines are included in
the report.
Args:
path: path to the directory.
line_coverage_result: per directory line coverage result with format:
dict => A dictionary containing line coverage data.
-- dir_path: dict => Line coverage summary.
---- total: int => total number of lines.
---- executed: int => executed number of lines.
file_line_coverage_report: a FileLineCoverageReport object.
"""
if path in line_coverage_result:
return
sum_total_lines = 0
sum_executed_lines = 0
for sub_name in os.listdir(path):
sub_path = os.path.normpath(os.path.join(path, sub_name))
if os.path.isdir(sub_path):
# Calculate coverage for sub-directories recursively.
self._CalculateCoverageForDirectory(sub_path, line_coverage_result,
file_line_coverage_report)
if sub_path in line_coverage_result:
sum_total_lines += line_coverage_result[sub_path]['total']
sum_executed_lines += line_coverage_result[sub_path]['executed']
elif file_line_coverage_report.ContainsFile(sub_path):
total_lines, executed_lines = (
file_line_coverage_report.GetCoverageForFile(sub_path))
sum_total_lines += total_lines
sum_executed_lines += executed_lines
if sum_total_lines != 0:
line_coverage_result[path] = {'total': sum_total_lines,
'executed': sum_executed_lines}
def GetListOfDirectories(self):
"""Returns a list of directories in the report.
Returns:
A list of directories.
"""
return self._coverage.keys()
def ContainsDirectory(self, path):
"""Returns True if the path is in the report.
Args:
path: path to the directory.
Returns:
True if the path is in the report.
"""
return path in self._coverage
def GetCoverageForDirectory(self, path):
"""Returns tuple representing coverage for a directory.
Args:
path: path to the directory.
Returns:
tuple with two integers (total number of lines, number of executed lines.)
"""
assert path in self._coverage, '{} is not in the report.'.format(path)
return self._coverage[path]['total'], self._coverage[path]['executed']
def _GenerateLineByLineFileCoverageInHtml(
targets, profdata_file_path, file_line_coverage_report, output_dir):
"""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:
output_dir/coverage/a/b/x.cc.html. An index html file is also generated as:
output_dir/index.html.
Args:
targets: A list of targets to be tested.
profdata_file_path: An absoluate path to the profdata file.
file_line_coverage_report: A FileLineCoverageReport object.
output_dir: output directory for generated html files, the path needs to be
an absolute path.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
binary_paths = [os.path.join(_GetApplicationBundlePath(target), target)
for target in targets]
# llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
# [[-object BIN]] [SOURCES]
# NOTE: For object files, the first one is specified as a positional argument,
# and the rest are specified as keyword argument.
cmd = ['xcrun', 'llvm-cov', 'show', '-arch=x86_64', '-format=html',
'-show-expansions', '-output-dir=' + output_dir,
'-instr-profile=' + profdata_file_path, binary_paths[0]]
cmd.extend(['-object=' + binary_path for binary_path in binary_paths[1:]])
cmd.extend(file_line_coverage_report.GetListOfFiles())
subprocess.check_call(cmd)
def _GeneratePerDirectoryCoverageHtmlReport(dir_line_coverage_report,
file_line_coverage_report,
output_dir):
"""Generates per directory coverage report in html.
For each directory, all its files or sub-directories that has non-zero number
of total lines are displayed.
Args:
dir_line_coverage_report: A DirectoryLineCoverageReport object.
file_line_coverage_report: A FileLineCoverageReport object.
output_dir: output directory for generated html files, the path needs to be
an absolute path.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
for dir_path in dir_line_coverage_report.GetListOfDirectories():
_GenerateCoverageHtmlReportForDirectory(dir_path, dir_line_coverage_report,
file_line_coverage_report,
output_dir)
def _GenerateCoverageHtmlReportForDirectory(dir_path, dir_line_coverage_report,
file_line_coverage_report,
output_dir):
"""Generates coverage report for directory in html.
Args:
dir_path: A path to the directory.
dir_line_coverage_report: A DirectoryLineCoverageReport object.
file_line_coverage_report: A FileLineCoverageReport object.
output_dir: output directory for generated html files, the path needs to be
an absolute path.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
css_path = os.path.join(output_dir, 'style.css')
html_generator = _DirectoryCoverageReportHtmlGenerator(css_path)
for sub_name in os.listdir(dir_path):
sub_path = os.path.normpath(os.path.join(dir_path, sub_name))
sub_path_html_report_path = _GetCoverageHtmlReportPath(
sub_path, output_dir)
relative_sub_path_html_report_path = os.path.relpath(
sub_path_html_report_path,
os.path.dirname(_GetCoverageHtmlReportPath(dir_path, output_dir)))
if dir_line_coverage_report.ContainsDirectory(sub_path):
total_lines, executed_lines = (
dir_line_coverage_report.GetCoverageForDirectory(sub_path))
html_generator.AddTableEntry(relative_sub_path_html_report_path,
os.path.basename(sub_path),
total_lines,
executed_lines)
elif file_line_coverage_report.ContainsFile(sub_path):
total_lines, executed_lines = (
file_line_coverage_report.GetCoverageForFile(sub_path))
html_generator.AddTableEntry(relative_sub_path_html_report_path,
os.path.basename(sub_path),
total_lines,
executed_lines)
html_generator.WriteHtmlCoverageReport(
_GetCoverageHtmlReportPath(dir_path, output_dir))
def _GetCoverageHtmlReportPath(file_or_dir_path, output_dir):
"""Given a file or directory, returns the corresponding html report path.
Args:
file_or_dir_path: os path to a file or directory.
output_dir: output directory for generated html files, the path needs to be
an absolute path.
Returns:
A path to the corresponding coverage html report.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute 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 _OverwriteHtmlReportsIndexFile(top_level_dir, dir_line_coverage_report,
output_dir):
"""Overwrites the index file to link to the report of the top level directory.
Args:
top_level_dir: The top level directory to generate code coverage for.
dir_line_coverage_report: A DirectoryLineCoverageReport object.
output_dir: output directory for generated html files, the path needs to be
an absolute path.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
css_path = os.path.join(output_dir, 'style.css')
index_html_generator = _DirectoryCoverageReportHtmlGenerator(css_path)
html_report_path = _GetCoverageHtmlReportPath(top_level_dir,
output_dir)
relative_html_report_path = os.path.relpath(html_report_path, output_dir)
total_lines, executed_lines = (
dir_line_coverage_report.GetCoverageForDirectory(top_level_dir))
index_html_generator.AddTableEntry(
relative_html_report_path, os.path.basename(top_level_dir), total_lines,
executed_lines)
index_file_path = os.path.join(output_dir, 'index.html')
index_html_generator.WriteHtmlCoverageReport(index_file_path)
def _CreateCoverageProfileDataForTargets(targets, output_dir, jobs_count=None,
gtest_filter=None):
"""Builds and runs target to generate the coverage profile data.
Args:
targets: A list of targets to be tested.
output_dir: A directory to store created coverage profile data file, the
path needs to be an absolute path.
jobs_count: Number of jobs to run in parallel for building. If None, a
default value is derived based on CPUs availability.
gtest_filter: If present, only run unit tests whose full name matches the
filter.
Returns:
An absolute path to the generated profdata file.
"""
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
_BuildTargetsWithCoverageConfiguration(targets, jobs_count)
profraw_file_paths = _GetProfileRawDataPathsByRunningTargets(targets,
gtest_filter)
profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
profraw_file_paths, output_dir)
print 'Code coverage profile data is created as: ' + profdata_file_path
return os.path.abspath(profdata_file_path)
def _GeneratePerFileLineCoverageReport(targets, profdata_file_path):
"""Generate per file code coverage report using llvm-cov report.
The officially suggested command to export code coverage data is to use
"llvm-cov export", which returns comprehensive code coverage data in a json
object, however, due to the large size and complicated dependencies of
Chrome, "llvm-cov export" takes too long to run, and for example, it takes 5
minutes for ios_chrome_unittests. Therefore, this script gets code coverage
data by calling "llvm-cov report", which is significantly faster and provides
the same data.
The raw code coverage report returned from "llvm-cov report" has the following
format:
Filename\tRegions\tMissed Regions\tCover\tFunctions\tMissed Functions\t
Executed\tInstantiations\tMissed Insts.\tLines\tMissed Lines\tCover
------------------------------------------------------------------------------
------------------------------------------------------------------------------
base/at_exit.cc\t89\t85\t4.49%\t7\t6\t14.29%\t7\t6\t14.29%\t107\t99\t7.48%
url/pathurl.cc\t89\t85\t4.49%\t7\t6\t14.29%\t7\t6\t14.29%\t107\t99\t7.48%
------------------------------------------------------------------------------
------------------------------------------------------------------------------
In Total\t89\t85\t4.49%\t7\t6\t14.29%\t7\t6\t14.29%\t107\t99\t7.48%
NOTE: This method only includes files with non-zero total number of lines to
the report.
Args:
targets: A list of targets to be tested.
profdata_file_path: An absolute path to the profdata file.
Returns:
A FileLineCoverageReport object.
"""
binary_paths = [os.path.join(_GetApplicationBundlePath(target), target)
for target in targets]
# llvm-cov report [options] -instr-profile PROFILE BIN [-object BIN,...]
# [[-object BIN]] [SOURCES].
# NOTE: For object files, the first one is specified as a positional argument,
# and the rest are specified as keyword argument.
cmd = ['xcrun', 'llvm-cov', 'report', '-arch=x86_64',
'-instr-profile=' + profdata_file_path, binary_paths[0]]
cmd.extend(['-object=' + binary_path for binary_path in binary_paths[1:]])
std_out = subprocess.check_output(cmd)
std_out_by_lines = std_out.split('\n')
# Strip out the unrelated lines. The 1st line is the header and the 2nd line
# is a '-' separator line. The last line is an empty line break, the second
# to last line is the in total coverage and the third to last line is a '-'
# separator line.
coverage_content_by_files = std_out_by_lines[2: -3]
# The 3rd to last column contains the total number of lines.
total_lines_index = -3
# The 2nd to last column contains the missed number of lines.
missed_lines_index = -2
file_line_coverage_report = _FileLineCoverageReport()
for coverage_content in coverage_content_by_files:
coverage_data = coverage_content.split()
file_name = coverage_data[0]
# TODO(crbug.com/765818): llvm-cov has a bug that proceduces invalid data in
# the report, and the following hack works it around. Remove the hack once
# the bug is fixed.
try:
total_lines = int(coverage_data[total_lines_index])
missed_lines = int(coverage_data[missed_lines_index])
except ValueError:
continue
if total_lines > 0:
executed_lines = total_lines - missed_lines
file_line_coverage_report.AddFile(file_name, total_lines, executed_lines)
return file_line_coverage_report
def _PrintLineCoverageStats(total, executed):
"""Print line coverage statistics.
The format is as following:
Total Lines: 20 Executed Lines: 2 Missed lines: 18 Coverage: 10%
Args:
total: total number of lines.
executed: number of lines that are executed.
"""
missed = total - executed
coverage = float(executed) / total if total > 0 else None
percentage_coverage = ('{}%'.format(int(coverage * 100))
if coverage is not None else 'NA')
output = ('Total Lines: {}\tExecuted Lines: {}\tMissed Lines: {}\t'
'Coverage: {}\n')
print output.format(total, executed, missed, percentage_coverage)
def _BuildTargetsWithCoverageConfiguration(targets, jobs_count):
"""Builds target with coverage configuration.
This function requires current working directory to be the root of checkout.
Args:
targets: A list of targets to be tested.
jobs_count: Number of jobs to run in parallel for compilation. If None, a
default value is derived based on CPUs availability.
"""
print 'Building ' + str(targets)
src_root = _GetSrcRootPath()
build_dir_path = os.path.join(src_root, BUILD_DIRECTORY)
cmd = ['ninja', '-C', build_dir_path]
if jobs_count:
cmd.append('-j' + str(jobs_count))
cmd.extend(targets)
subprocess.check_call(cmd)
def _GetProfileRawDataPathsByRunningTargets(targets, gtest_filter=None):
"""Runs target and returns the path to the generated profraw data file.
The output log of running the test target has no format, but it is guaranteed
to have a single line containing the path to the generated profraw data file.
Args:
targets: A list of targets to be tested.
gtest_filter: If present, only run unit tests whose full name matches the
filter.
Returns:
A list of absolute paths to the generated profraw data files.
"""
profraw_file_paths = []
logs = _RunTestTargetsWithCoverageConfiguration(targets, gtest_filter)
for log in logs:
is_profraw_file_found_in_log = False
for line in log:
if PROFRAW_FILE_LOG_IDENTIFIER in line:
is_profraw_file_found_in_log = True
profraw_file_path = line.split(PROFRAW_FILE_LOG_IDENTIFIER)[1][:-1]
profraw_file_paths.append(profraw_file_path)
break
if not is_profraw_file_found_in_log:
assert False, ('No profraw data file is generated, did you call '
'coverage_util::ConfigureCoverageReportPath() in test '
'setup? Please refer to base/test/test_support_ios.mm for '
'example.')
return profraw_file_paths
def _RunTestTargetsWithCoverageConfiguration(targets, gtest_filter=None):
"""Runs tests to generate the profraw data file.
This function requires current working directory to be the root of checkout.
Args:
targets: A list of targets to be tested.
gtest_filter: If present, only run unit tests whose full name matches the
filter.
Returns:
A list that contains the log output (after breaking by lines) for each
target. The logs has no format, but it is guaranteed to have a single line
containing the path to the generated profraw data file.
"""
logs = []
iossim_path = _GetIOSSimPath()
for target in targets:
cmd = [iossim_path]
# For iossim arguments, please refer to src/testing/iossim/iossim.mm.
if gtest_filter and not _TargetIsEarlGreyTest(target):
cmd.append('-c --gtest_filter=' + gtest_filter)
application_path = _GetApplicationBundlePath(target)
cmd.append(application_path)
if _TargetIsEarlGreyTest(target):
cmd.append(_GetXCTestBundlePath(target))
print 'Running {} with command: {}'.format(target, ' '.join(cmd))
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
log_chracters, _ = process.communicate()
logs.append(''.join(log_chracters).split('\n'))
return logs
def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths, output_dir):
"""Returns the path to the profdata file by merging profraw data file.
Args:
profraw_file_paths: A list of absolute paths to the profraw data files that
are to be merged.
output_dir: A directory to store created coverage profile data file, the
path needs to be an absolute path.
Returns:
An absolute path to the generated profdata file.
Raises:
CalledProcessError: An error occurred merging profraw data files.
"""
print 'Creating the profile data file'
assert os.path.isabs(output_dir), 'output_dir must be an absolute path.'
if not os.path.exists(output_dir):
os.makedirs(output_dir)
profdata_file_path = os.path.join(output_dir, PROFDATA_FILE_NAME)
try:
cmd = ['xcrun', 'llvm-profdata', 'merge', '-o', profdata_file_path]
cmd.extend(profraw_file_paths)
subprocess.check_call(cmd)
except subprocess.CalledProcessError as error:
print 'Failed to merge profraw to create profdata.'
raise error
return profdata_file_path
def _GetSrcRootPath():
"""Returns the absolute path to the root of checkout.
Returns:
A string representing the absolute path to the root of checkout.
"""
return os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir,
os.path.pardir, os.path.pardir))
def _GetApplicationBundlePath(target):
"""Returns the path to the generated application bundle after building.
Args:
target: The target to be tested.
Returns:
A path to the generated application bundles.
"""
src_root = _GetSrcRootPath()
application_bundle_name = target + '.app'
return os.path.join(src_root, BUILD_DIRECTORY, application_bundle_name)
def _GetXCTestBundlePath(target):
"""Returns the path to the xctest bundle after building.
Args:
target: A string representing the name of the target to be tested.
Returns:
A string representing the path to the generated xctest bundle.
"""
application_path = _GetApplicationBundlePath(target)
xctest_bundle_name = target + '_module.xctest'
return os.path.join(application_path, 'PlugIns', xctest_bundle_name)
def _GetIOSSimPath():
"""Returns the path to the iossim executable file after building.
Returns:
A string representing the path to the iossim executable file.
"""
src_root = _GetSrcRootPath()
iossim_path = os.path.join(src_root, BUILD_DIRECTORY, 'iossim')
return iossim_path
def _IsGomaConfigured():
"""Returns True if goma is enabled in the gn build settings.
Returns:
A boolean indicates whether goma is configured for building or not.
"""
# Load configuration.
settings = ConfigParser.SafeConfigParser()
settings.read(os.path.expanduser('~/.setup-gn'))
return settings.getboolean('goma', 'enabled')
def _TargetIsEarlGreyTest(target):
"""Returns true if the target is an earl grey test.
Args:
target: A string representing the name of the target to be tested.
Returns:
A boolean indicates whether the target is an earl grey test or not.
"""
return target.endswith(EARL_GREY_TEST_TARGET_POSTFIX)
def _AssertTargetsAreValidTestTargets(targets):
"""Asserts that the targets have a valid postfix.
The list of valid target name postfixes are defined in
VALID_TEST_TARGET_POSTFIXES.
Args:
targets: A list of targets to be tested.
"""
for target in targets:
if not (any(target.endswith(postfix) for postfix in
VALID_TEST_TARGET_POSTFIXES)):
assert False, ('target: {} is detected, however, only target name with '
'the following postfixes are supported: {}'.
format(target, VALID_TEST_TARGET_POSTFIXES))
def _AssertCoverageBuildDirectoryExists():
"""Asserts that the build directory with converage configuration exists."""
src_root = _GetSrcRootPath()
build_dir_path = os.path.join(src_root, BUILD_DIRECTORY)
assert os.path.exists(build_dir_path), (build_dir_path + " doesn't exist."
'Hint: run gclient runhooks or '
'ios/build/tools/setup-gn.py.')
def _AssertPathsExist(paths):
"""Asserts that the paths specified in |paths| exist.
Args:
paths: A list of files or directories.
"""
src_root = _GetSrcRootPath()
for path in paths:
abspath = os.path.join(src_root, path)
assert os.path.exists(abspath), (('Path: {} doesn\'t exist.\nA valid '
'path must exist and be relative to the '
'root of source, which is {}. For '
'example, \'ios/\' is a valid path.').
format(abspath, src_root))
def _ParseCommandArguments():
"""Add and parse relevant arguments for tool commands.
Returns:
A dictionanry representing the arguments.
"""
arg_parser = argparse.ArgumentParser()
arg_parser.usage = __doc__
arg_parser.add_argument('-t', '--top-level-dir', type=str, required=True,
help='The top level directory to show code coverage '
'report, the path needs to be relative to the '
'root of the checkout.')
arg_parser.add_argument('-i', '--include', action='append',
help='Directories to get code coverage for, and the '
'paths need to be relative to the root of the '
'checkout and all files under them are included '
'recursively.')
arg_parser.add_argument('-e', '--exclude', action='append',
help='Directories to get code coverage for, and the '
'paths need to be relative to the root of the '
'checkout and all files under them are excluded '
'recursively.')
arg_parser.add_argument('-j', '--jobs', type=int, default=None,
help='Run N jobs to build in parallel. If not '
'specified, a default value will be derived '
'based on CPUs availability. Please refer to '
'\'ninja -h\' for more details.')
arg_parser.add_argument('-r', '--reuse-profdata', type=str,
help='Skip building test target and running tests '
'and re-use the specified profile data file, '
'the path needs to be absolute or relative to '
'the root of checkout.')
arg_parser.add_argument('-o', '--output-dir', type=str, required=True,
help='Output directory for generated artifacts, the'
'path needs to be absolute or relative to the '
'root of checkout.')
arg_parser.add_argument('--gtest_filter', type=str,
help='Only run unit tests whose full name matches '
'the filter.')
arg_parser.add_argument('targets', nargs='+',
help='The names of the test targets to run.')
args = arg_parser.parse_args()
return args
def Main():
"""Executes tool commands."""
args = _ParseCommandArguments()
_AssertTargetsAreValidTestTargets(args.targets)
jobs = args.jobs
if not jobs and _IsGomaConfigured():
jobs = DEFAULT_GOMA_JOBS
_AssertCoverageBuildDirectoryExists()
_AssertPathsExist([args.top_level_dir])
include_paths = args.include or []
exclude_paths = args.exclude or []
if include_paths:
_AssertPathsExist(include_paths)
if exclude_paths:
_AssertPathsExist(exclude_paths)
if not include_paths:
include_paths.append(args.top_level_dir)
output_dir_abspath = os.path.abspath(args.output_dir)
profdata_file_path = args.reuse_profdata
if profdata_file_path:
assert os.path.exists(profdata_file_path), (('The provided profile data '
'file: {} doesn\'t exist.')
.format(profdata_file_path))
else:
profdata_file_path = _CreateCoverageProfileDataForTargets(
args.targets, output_dir_abspath, jobs, args.gtest_filter)
print 'Generating code coverge report'
file_line_coverage_report = _GeneratePerFileLineCoverageReport(
args.targets, profdata_file_path)
file_line_coverage_report.FilterFiles(include_paths, exclude_paths)
file_line_coverage_report.ExcludeTestFiles()
# ios/chrome and ios/chrome/ refer to the same directory.
top_level_dir = os.path.normpath(args.top_level_dir)
dir_line_coverage_report = _DirectoryLineCoverageReport(
file_line_coverage_report, top_level_dir)
print '\nLine Coverage Report for: ' + top_level_dir
total, executed = dir_line_coverage_report.GetCoverageForDirectory(
top_level_dir)
_PrintLineCoverageStats(total, executed)
_GenerateLineByLineFileCoverageInHtml(args.targets, profdata_file_path,
file_line_coverage_report,
output_dir_abspath)
print 'Generating per directory code coverage breakdown in html'
_GeneratePerDirectoryCoverageHtmlReport(
dir_line_coverage_report, file_line_coverage_report, output_dir_abspath)
# The default index file is generated only for the list of source files, needs
# to overwrite it to display per directory code coverage breakdown.
_OverwriteHtmlReportsIndexFile(top_level_dir, dir_line_coverage_report,
output_dir_abspath)
html_index_file_path = 'file://' + os.path.abspath(
os.path.join(args.output_dir, 'index.html'))
print 'index file for html report is generated as: {}'.format(
html_index_file_path)
webbrowser.open(html_index_file_path)
if __name__ == '__main__':
sys.exit(Main())