[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 <[email protected]>
Reviewed-by: Nico Weber <[email protected]>
Reviewed-by: Sylvain Defresne <[email protected]>
Reviewed-by: Eugene But <[email protected]>
Reviewed-by: Abhishek Arya <[email protected]>
Cr-Commit-Position: refs/heads/master@{#539940}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index 9aeac9b..69fc11e 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -316,6 +316,12 @@
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
# Clang bundle:
# https://chromium-review.googlesource.com/c/chromium/src/+/688221
@@ -342,13 +348,13 @@
package_version = stamp_file_line.rstrip()
target_os = ''
- if target_os and platform != target_os:
+ if target_os and target_os != 'ios' and platform != target_os:
continue
clang_revision_str, clang_sub_revision_str = package_version.split('-')
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()
clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
@@ -416,6 +422,11 @@
]
subprocess_cmd.extend(
['-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.check_call(subprocess_cmd)
logging.debug('Finished running "llvm-cov show" command')
@@ -751,13 +762,33 @@
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
+ profraw_file_paths = []
+
# Run all test targets to generate profraw data files.
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')
- profraw_file_paths = []
+ if _IsTargetOsIos():
+ return profraw_file_paths
+
for file_or_dir in os.listdir(OUTPUT_DIR):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
@@ -775,12 +806,7 @@
def _ExecuteCommand(target, command):
- """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.
- """
+ """Runs a single command and generates a profraw data file."""
# Per Clang "Source-based Code Coverage" doc:
# "%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
@@ -796,15 +822,58 @@
[target, '%4m', PROFRAW_FILE_EXTENSION])
expected_profraw_file_path = os.path.join(OUTPUT_DIR,
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"',
- command, output_file_path)
- output = subprocess.check_output(
- command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
- with open(output_file_path, 'w') as output_file:
- output_file.write(output)
+ try:
+ output = subprocess.check_output(
+ command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
+ except subprocess.CalledProcessError as e:
+ output = e.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):
@@ -853,6 +922,11 @@
]
subprocess_cmd.extend(
['-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)
json_output = json.loads(subprocess.check_output(subprocess_cmd))
@@ -887,6 +961,9 @@
2. Use xvfb.
2.1. "python 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:
command: A command used to run a target.
@@ -905,9 +982,21 @@
if os.path.basename(command_parts[0]) == xvfb_script_name:
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]
+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):
"""Verifies that the target executables specified in the commands are inside
the given build directory."""