Commit a0c8c2ff authored by Yuke Liao's avatar Yuke Liao Committed by Commit Bot

[Coverage] Support running code coverage tool on iOS platform

This CL supports running the code coverage tool on iOS platform and
removes the ios/tools/coverage.

Bug: 814608

Cq-Include-Trybots: master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: I135c9505a7f2aadc9cd71ca5f6b3893776aed116
Reviewed-on: https://chromium-review.googlesource.com/935195
Commit-Queue: Yuke Liao <liaoyuke@chromium.org>
Reviewed-by: default avatarNico Weber <thakis@chromium.org>
Reviewed-by: default avatarSylvain Defresne <sdefresne@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarAbhishek Arya <inferno@chromium.org>
Cr-Commit-Position: refs/heads/master@{#539940}
parent bc8d95c6
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import("//build/config/coverage/coverage.gni")
import("//build/config/ios/ios_sdk.gni") import("//build/config/ios/ios_sdk.gni")
import("//build/config/sysroot.gni") import("//build/config/sysroot.gni")
import("//build/toolchain/toolchain.gni") import("//build/toolchain/toolchain.gni")
...@@ -97,8 +98,8 @@ config("runtime_library") { ...@@ -97,8 +98,8 @@ config("runtime_library") {
cflags = common_flags cflags = common_flags
ldflags = common_flags ldflags = common_flags
if (ios_enable_coverage) { if (use_clang_coverage) {
configs = [ ":enable_coverage" ] configs = [ "//build/config/coverage:default_coverage" ]
} }
} }
...@@ -124,16 +125,6 @@ config("xctest_config") { ...@@ -124,16 +125,6 @@ config("xctest_config") {
] ]
} }
# This enables support for LLVM code coverage. See
# http://llvm.org/docs/CoverageMappingFormat.html.
config("enable_coverage") {
cflags = [
"-fprofile-instr-generate",
"-fcoverage-mapping",
]
ldflags = [ "-fprofile-instr-generate" ]
}
group("xctest") { group("xctest") {
public_configs = [ ":xctest_config" ] public_configs = [ ":xctest_config" ]
} }
...@@ -36,10 +36,6 @@ declare_args() { ...@@ -36,10 +36,6 @@ declare_args() {
# to avoid running out of certificates if using a free account. # to avoid running out of certificates if using a free account.
ios_automatically_manage_certs = true ios_automatically_manage_certs = true
# Enabling this option makes clang compile for profiling to gather code
# coverage metrics.
ios_enable_coverage = false
# If non-empty, this list must contain valid cpu architecture, and the final # If non-empty, this list must contain valid cpu architecture, and the final
# build will be a multi-architecture build (aka fat build) supporting the # build will be a multi-architecture build (aka fat build) supporting the
# main $target_cpu architecture and all of $additional_target_cpus. # main $target_cpu architecture and all of $additional_target_cpus.
......
...@@ -95,14 +95,9 @@ class GnGenerator(object): ...@@ -95,14 +95,9 @@ class GnGenerator(object):
args.append(('enable_stripping', 'enable_dsyms')) args.append(('enable_stripping', 'enable_dsyms'))
args.append(('is_official_build', self._config == 'Official')) args.append(('is_official_build', self._config == 'Official'))
args.append(('is_chrome_branded', 'is_official_build')) args.append(('is_chrome_branded', 'is_official_build'))
args.append(('ios_enable_coverage', self._config == 'Coverage')) args.append(('use_xcode_clang', 'is_official_build'))
args.append(('use_clang_coverage', self._config == 'Coverage'))
# TODO(crbug.com/75794): the version of llvm-cov used for code coverage is args.append(('is_component_build', False))
# tied to the version of clang used. As no copy of llvm-cov is shipped with
# the Chrome's clang, the version shipped with Xcode is used, thus code
# needs to be compiled with Xcode's clang when enabling code coverage.
# Remove this once llvm-cov is shipped with Chrome's clang.
args.append(('use_xcode_clang', 'is_official_build || ios_enable_coverage'))
if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1': if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1':
args.append(('use_system_xcode', False)) args.append(('use_system_xcode', False))
......
liaoyuke@chromium.org
# TEAM: ios-directory-owners@chromium.org
# OS: iOS
This diff is collapsed.
<!doctype html>
<html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta charset='UTF-8'>
<link rel='stylesheet' type='text/css' href='/Users/liaoyuke/bling/src/out/Coverage-iphonesimulator/html/style.css'>
</head>
<body>
<h2>Coverage Report</h2>
<p>Click <a href='http://clang.llvm.org/docs/SourceBasedCodeCoverage.html#interpreting-reports'>here</a> for information about interpreting this report.</p>
<div class='centered'>
<table>
<tr>
<td class='column-entry-left'>Directory Name</td>
<td class='column-entry'>Line Coverage</td>
</tr>
<tr class='light-row'>
<td>
<pre><a href='/Users/liaoyuke/bling/src/out/Coverage-iphonesimulator/html/coverage/Users/liaoyuke/bling/src/url/third_party/coverage.html'>third_party</a></pre>
</td>
<td class='column-entry-yellow'>
<pre> 90.31% (699/774)</pre>
</td>
</tr>
<tr>
<td class='column-entry-left'>File Name</td>
<td class='column-entry'>Line Coverage</td>
</tr>
<tr class='light-row'>
<td>
<pre><a href='/Users/liaoyuke/bling/src/out/Coverage-iphonesimulator/html/coverage/Users/liaoyuke/bling/src/url/url_canon_stdurl.cc.html'>url_canon_stdurl.cc</a></pre>
</td>
<td class='column-entry-yellow'>
<pre> 93.94% (124/132)</pre>
</td>
</tr>
</table>
</div>
</body>
</html>
\ No newline at end of file
</table>
</div>
</body>
</html>
\ No newline at end of file
<!doctype html>
<html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta charset='UTF-8'>
<link rel='stylesheet' type='text/css' href='{{ css_path }}'>
</head>
<body>
<h2>Coverage Report</h2>
<p>Click <a href='http://clang.llvm.org/docs/SourceBasedCodeCoverage.html#interpreting-reports'>here</a> for information about interpreting this report.</p>
<div class='centered'>
<table>
\ No newline at end of file
<table>
<tr>
<td class='column-entry-left'>Directory Name</td>
<td class='column-entry'>Line Coverage</td>
</tr>
{% for dir_entry in dir_entries %}
<tr class='light-row'>
<td>
<pre><a href='{{ dir_entry.href }}'>{{ dir_entry.name }}</a></pre>
</td>
<td class='{{ dir_entry.color_class }}'>
<pre> {{ dir_entry.percentage_coverage }}% ({{ dir_entry.executed_lines }}/{{ dir_entry.total_lines }})</pre>
</td>
</tr>
{% endfor %}
<tr>
<td class='column-entry-left'>File Name</td>
<td class='column-entry'>Line Coverage</td>
</tr>
{% for file_entry in file_entries %}
<tr class='light-row'>
<td>
<pre><a href='{{ file_entry.href }}'>{{ file_entry.name }}</a></pre>
</td>
<td class='{{ file_entry.color_class }}'>
<pre> {{ file_entry.percentage_coverage }}% ({{ file_entry.executed_lines }}/{{ file_entry.total_lines }})</pre>
</td>
</tr>
{% endfor %}
</table>
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import("//build_overrides/gtest.gni") import("//build_overrides/gtest.gni")
if (is_ios) { if (is_ios) {
import("//build/config/coverage/coverage.gni")
import("//build/config/ios/ios_sdk.gni") import("//build/config/ios/ios_sdk.gni")
import("//build/buildflag_header.gni") import("//build/buildflag_header.gni")
} }
...@@ -94,6 +95,6 @@ source_set("gtest_main") { ...@@ -94,6 +95,6 @@ source_set("gtest_main") {
if (is_ios) { if (is_ios) {
buildflag_header("ios_enable_coverage") { buildflag_header("ios_enable_coverage") {
header = "ios_enable_coverage.h" header = "ios_enable_coverage.h"
flags = [ "IOS_ENABLE_COVERAGE=$ios_enable_coverage" ] flags = [ "IOS_ENABLE_COVERAGE=$use_clang_coverage" ]
} }
} }
...@@ -316,6 +316,12 @@ def _GetPlatform(): ...@@ -316,6 +316,12 @@ def _GetPlatform():
return 'mac' return 'mac'
def _IsTargetOsIos():
"""Returns true if the target_os specified in args.gn file is ios"""
build_args = _ParseArgsGnFile()
return 'target_os' in build_args and build_args['target_os'] == '"ios"'
# TODO(crbug.com/759794): remove this function once tools get included to # TODO(crbug.com/759794): remove this function once tools get included to
# Clang bundle: # Clang bundle:
# https://chromium-review.googlesource.com/c/chromium/src/+/688221 # https://chromium-review.googlesource.com/c/chromium/src/+/688221
...@@ -342,13 +348,13 @@ def DownloadCoverageToolsIfNeeded(): ...@@ -342,13 +348,13 @@ def DownloadCoverageToolsIfNeeded():
package_version = stamp_file_line.rstrip() package_version = stamp_file_line.rstrip()
target_os = '' target_os = ''
if target_os and platform != target_os: if target_os and target_os != 'ios' and platform != target_os:
continue continue
clang_revision_str, clang_sub_revision_str = package_version.split('-') clang_revision_str, clang_sub_revision_str = package_version.split('-')
return int(clang_revision_str), int(clang_sub_revision_str) return int(clang_revision_str), int(clang_sub_revision_str)
assert False, 'Coverage is only supported on target_os - linux, mac.' assert False, 'Coverage is only supported on target_os - linux, mac and ios'
platform = _GetPlatform() platform = _GetPlatform()
clang_revision, clang_sub_revision = _GetRevisionFromStampFile( clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
...@@ -416,6 +422,11 @@ def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path, ...@@ -416,6 +422,11 @@ def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
] ]
subprocess_cmd.extend( subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]]) ['-object=' + binary_path for binary_path in binary_paths[1:]])
if _IsTargetOsIos():
# iOS binaries are universal binaries, and it requires specifying the
# architecture to use.
subprocess_cmd.append('-arch=x86_64')
subprocess_cmd.extend(filters) subprocess_cmd.extend(filters)
subprocess.check_call(subprocess_cmd) subprocess.check_call(subprocess_cmd)
logging.debug('Finished running "llvm-cov show" command') logging.debug('Finished running "llvm-cov show" command')
...@@ -751,13 +762,33 @@ def _GetProfileRawDataPathsByExecutingCommands(targets, commands): ...@@ -751,13 +762,33 @@ def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION): if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
os.remove(os.path.join(OUTPUT_DIR, file_or_dir)) os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
profraw_file_paths = []
# Run all test targets to generate profraw data files. # Run all test targets to generate profraw data files.
for target, command in zip(targets, commands): for target, command in zip(targets, commands):
_ExecuteCommand(target, command) output_file_name = os.extsep.join([target + '_output', 'txt'])
output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
logging.info('Running command: "%s", the output is redirected to "%s"',
command, output_file_path)
if _IsIosCommand(command):
# On iOS platform, due to lack of write permissions, profraw files are
# generated outside of the OUTPUT_DIR, and the exact paths are contained
# in the output of the command execution.
output = _ExecuteIosCommand(target, command)
profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
else:
# On other platforms, profraw files are generated inside the OUTPUT_DIR.
output = _ExecuteCommand(target, command)
with open(output_file_path, 'w') as output_file:
output_file.write(output)
logging.debug('Finished executing the test commands') logging.debug('Finished executing the test commands')
profraw_file_paths = [] if _IsTargetOsIos():
return profraw_file_paths
for file_or_dir in os.listdir(OUTPUT_DIR): for file_or_dir in os.listdir(OUTPUT_DIR):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION): if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir)) profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
...@@ -775,12 +806,7 @@ def _GetProfileRawDataPathsByExecutingCommands(targets, commands): ...@@ -775,12 +806,7 @@ def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
def _ExecuteCommand(target, command): def _ExecuteCommand(target, command):
"""Runs a single command and generates a profraw data file. """Runs a single command and generates a profraw data file."""
Args:
target: A target built with coverage instrumentation.
command: A command used to run the target.
"""
# Per Clang "Source-based Code Coverage" doc: # Per Clang "Source-based Code Coverage" doc:
# "%Nm" expands out to the instrumented binary's signature. When this pattern # "%Nm" expands out to the instrumented binary's signature. When this pattern
# is specified, the runtime creates a pool of N raw profiles which are used # is specified, the runtime creates a pool of N raw profiles which are used
...@@ -796,15 +822,58 @@ def _ExecuteCommand(target, command): ...@@ -796,15 +822,58 @@ def _ExecuteCommand(target, command):
[target, '%4m', PROFRAW_FILE_EXTENSION]) [target, '%4m', PROFRAW_FILE_EXTENSION])
expected_profraw_file_path = os.path.join(OUTPUT_DIR, expected_profraw_file_path = os.path.join(OUTPUT_DIR,
expected_profraw_file_name) expected_profraw_file_name)
output_file_name = os.extsep.join([target + '_output', 'txt'])
output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
logging.info('Running command: "%s", the output is redirected to "%s"', try:
command, output_file_path) output = subprocess.check_output(
output = subprocess.check_output( command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path}) except subprocess.CalledProcessError as e:
with open(output_file_path, 'w') as output_file: output = e.output
output_file.write(output) logging.warning('Command: "%s" exited with non-zero return code', command)
return output
def _ExecuteIosCommand(target, command):
"""Runs a single iOS command and generates a profraw data file.
iOS application doesn't have write access to folders outside of the app, so
it's impossible to instruct the app to flush the profraw data file to the
desired location. The profraw data file will be generated somewhere within the
application's Documents folder, and the full path can be obtained by parsing
the output.
"""
assert _IsIosCommand(command)
try:
output = subprocess.check_output(command.split())
except subprocess.CalledProcessError as e:
# iossim emits non-zero return code even if tests run successfully, so
# ignore the return code.
output = e.output
return output
def _GetProfrawDataFileByParsingOutput(output):
"""Returns the path to the profraw data file obtained by parsing the output.
The output 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.
NOTE: This should only be called when target os is iOS.
"""
assert _IsTargetOsIos()
output_by_lines = ''.join(output).split('\n')
profraw_file_identifier = 'Coverage data at '
for line in output_by_lines:
if profraw_file_identifier in line:
profraw_file_path = line.split(profraw_file_identifier)[1][:-1]
return profraw_file_path
assert False, ('No profraw data file was generated, did you call '
'coverage_util::ConfigureCoverageReportPath() in test setup? '
'Please refer to base/test/test_support_ios.mm for example.')
def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths): def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
...@@ -853,6 +922,11 @@ def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters): ...@@ -853,6 +922,11 @@ def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
] ]
subprocess_cmd.extend( subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]]) ['-object=' + binary_path for binary_path in binary_paths[1:]])
if _IsTargetOsIos():
# iOS binaries are universal binaries, and it requires specifying the
# architecture to use.
subprocess_cmd.append('-arch=x86_64')
subprocess_cmd.extend(filters) subprocess_cmd.extend(filters)
json_output = json.loads(subprocess.check_output(subprocess_cmd)) json_output = json.loads(subprocess.check_output(subprocess_cmd))
...@@ -887,6 +961,9 @@ def _GetBinaryPath(command): ...@@ -887,6 +961,9 @@ def _GetBinaryPath(command):
2. Use xvfb. 2. Use xvfb.
2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>" 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>" 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
3. Use iossim to run tests on iOS platform.
3.1. "out/Coverage-iphonesimulator/iossim
out/Coverage-iphonesimulator/url_unittests.app <arguments>"
Args: Args:
command: A command used to run a target. command: A command used to run a target.
...@@ -905,9 +982,21 @@ def _GetBinaryPath(command): ...@@ -905,9 +982,21 @@ def _GetBinaryPath(command):
if os.path.basename(command_parts[0]) == xvfb_script_name: if os.path.basename(command_parts[0]) == xvfb_script_name:
return command_parts[1] return command_parts[1]
if _IsIosCommand(command):
# For a given application bundle, the binary resides in the bundle and has
# the same name with the application without the .app extension.
app_path = command_parts[1]
app_name = os.path.splitext(os.path.basename(app_path))[0]
return os.path.join(app_path, app_name)
return command.split()[0] return command.split()[0]
def _IsIosCommand(command):
"""Returns true if command is used to run tests on iOS platform."""
return os.path.basename(command.split()[0]) == 'iossim'
def _VerifyTargetExecutablesAreInBuildDirectory(commands): def _VerifyTargetExecutablesAreInBuildDirectory(commands):
"""Verifies that the target executables specified in the commands are inside """Verifies that the target executables specified in the commands are inside
the given build directory.""" the given build directory."""
......
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