blob: 2c0395e8b60fb20cf7ea9eb737b4805347ed0681 [file] [log] [blame]
#!/usr/bin/env 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.
"""Packages a user.bootfs for a Fuchsia QEMU image, pulling in the runtime
dependencies of a test binary, and then uses QEMU from the Fuchsia SDK to run
it. Does not yet implement running on real hardware."""
import argparse
import os
import re
import subprocess
import sys
import tempfile
DIR_SOURCE_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
SDK_ROOT = os.path.join(DIR_SOURCE_ROOT, 'third_party', 'fuchsia-sdk')
def RunAndCheck(dry_run, args):
if dry_run:
print 'Run:', args
else:
subprocess.check_call(args)
def DumpFile(dry_run, name, description):
"""Prints out the contents of |name| if |dry_run|."""
if not dry_run:
return
print
print 'Contents of %s (for %s)' % (name, description)
print '-' * 80
with open(name) as f:
sys.stdout.write(f.read())
print '-' * 80
def MakeTargetImageName(common_prefix, output_directory, location):
"""Generates the relative path name to be used in the file system image.
common_prefix: a prefix of both output_directory and location that
be removed.
output_directory: an optional prefix on location that will also be removed.
location: the file path to relativize.
.so files will be stored into the lib subdirectory to be able to be found by
default by the loader.
Examples:
>>> MakeTargetImageName(common_prefix='/work/cr/src/',
... output_directory='/work/cr/src/out/fuch',
... location='/work/cr/src/base/test/data/xyz.json')
'base/test/data/xyz.json'
>>> MakeTargetImageName(common_prefix='/work/cr/src/',
... output_directory='/work/cr/src/out/fuch',
... location='/work/cr/src/out/fuch/icudtl.dat')
'icudtl.dat'
>>> MakeTargetImageName(common_prefix='/work/cr/src/',
... output_directory='/work/cr/src/out/fuch',
... location='/work/cr/src/out/fuch/libbase.so')
'lib/libbase.so'
"""
assert output_directory.startswith(common_prefix)
output_dir_no_common_prefix = output_directory[len(common_prefix):]
assert location.startswith(common_prefix)
loc = location[len(common_prefix):]
if loc.startswith(output_dir_no_common_prefix):
loc = loc[len(output_dir_no_common_prefix)+1:]
# TODO(fuchsia): The requirements for finding/loading .so are in flux, so this
# ought to be reconsidered at some point. See https://crbug.com/732897.
if location.endswith('.so'):
loc = 'lib/' + loc
return loc
def AddToManifest(manifest_file, target_name, source, mapper):
"""Appends |source| to the given |manifest_file| (a file object) in a format
suitable for consumption by mkbootfs.
If |source| is a file it's directly added. If |source| is a directory, its
contents are recursively added.
|source| must exist on disk at the time this function is called.
"""
if os.path.isdir(source):
files = [os.path.join(dp, f) for dp, dn, fn in os.walk(source) for f in fn]
for f in files:
# We pass None as the mapper because this should never recurse a 2nd time.
AddToManifest(manifest_file, mapper(f), f, None)
elif os.path.exists(source):
manifest_file.write('%s=%s\n' % (target_name, source))
else:
raise Exception('%s does not exist' % source)
def BuildBootfs(output_directory, runtime_deps_path, test_name, gtest_filter,
gtest_repeat, test_launcher_batch_limit,
test_launcher_filter_file, test_launcher_jobs,
single_process_tests, dry_run):
with open(runtime_deps_path) as f:
lines = f.readlines()
locations_to_add = [os.path.abspath(os.path.join(output_directory, x.strip()))
for x in lines]
locations_to_add.append(
os.path.abspath(os.path.join(output_directory, test_name)))
common_prefix = os.path.commonprefix(locations_to_add)
target_source_pairs = zip(
[MakeTargetImageName(common_prefix, output_directory, loc)
for loc in locations_to_add],
locations_to_add)
# Add extra .so's that are required for running to system/lib
sysroot_libs = [
'libc++abi.so.1',
'libc++.so.2',
'libunwind.so.1',
]
sysroot_lib_path = os.path.join(SDK_ROOT, 'sysroot', 'x86_64-fuchsia', 'lib')
for lib in sysroot_libs:
target_source_pairs.append(
('lib/' + lib, os.path.join(sysroot_lib_path, lib)))
# Generate a little script that runs the test binaries and then shuts down
# QEMU.
autorun_file = tempfile.NamedTemporaryFile()
autorun_file.write('#!/bin/sh\n')
autorun_file.write('/system/' + os.path.basename(test_name))
autorun_file.write(' --test-launcher-retry-limit=0')
if int(os.environ.get('CHROME_HEADLESS', 0)) != 0:
# When running on bots (without KVM) execution is quite slow. The test
# launcher times out a subprocess after 45s which can be too short. Make the
# timeout twice as long.
autorun_file.write(' --test-launcher-timeout=90000')
if single_process_tests:
autorun_file.write(' --single-process-tests')
if test_launcher_filter_file:
test_launcher_filter_file = os.path.normpath(
os.path.join(output_directory, test_launcher_filter_file))
filter_file_on_device = MakeTargetImageName(
common_prefix, output_directory, test_launcher_filter_file)
autorun_file.write(' --test-launcher-filter-file=/system/' +
filter_file_on_device)
target_source_pairs.append(
[filter_file_on_device, test_launcher_filter_file])
if test_launcher_batch_limit:
autorun_file.write(' --test-launcher-batch-limit=%d' %
test_launcher_batch_limit)
if test_launcher_jobs:
autorun_file.write(' --test-launcher-jobs=%d' %
test_launcher_jobs)
if gtest_filter:
autorun_file.write(' --gtest_filter=' + gtest_filter)
if gtest_repeat:
autorun_file.write(' --gtest_repeat=' + gtest_repeat)
autorun_file.write('\n')
# If shutdown happens too soon after the test completion, log statements from
# the end of the run will be lost, so sleep for a bit before shutting down.
autorun_file.write('msleep 3000\n')
autorun_file.write('dm poweroff\n')
autorun_file.flush()
os.chmod(autorun_file.name, 0750)
DumpFile(dry_run, autorun_file.name, 'autorun')
target_source_pairs.append(('autorun', autorun_file.name))
# Generate an initial.config for application_manager that tells it to run
# our autorun script with sh.
initial_config_file = tempfile.NamedTemporaryFile()
initial_config_file.write('''{
"initial-apps": [
[ "file:///boot/bin/sh", "/system/autorun" ]
]
}
''')
initial_config_file.flush()
DumpFile(dry_run, initial_config_file.name, 'initial.config')
target_source_pairs.append(('data/appmgr/initial.config',
initial_config_file.name))
manifest_file = tempfile.NamedTemporaryFile()
bootfs_name = runtime_deps_path + '.bootfs'
for target, source in target_source_pairs:
AddToManifest(manifest_file.file, target, source,
lambda x: MakeTargetImageName(
common_prefix, output_directory, x))
mkbootfs_path = os.path.join(SDK_ROOT, 'tools', 'mkbootfs')
manifest_file.flush()
DumpFile(dry_run, manifest_file.name, 'manifest')
RunAndCheck(dry_run,
[mkbootfs_path, '-o', bootfs_name,
'--target=boot', os.path.join(SDK_ROOT, 'bootdata.bin'),
'--target=system', manifest_file.name,
])
return bootfs_name
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--dry-run', '-n', action='store_true', default=False,
help='Just print commands, don\'t execute them.')
parser.add_argument('--output-directory',
type=os.path.realpath,
help=('Path to the directory in which build files are'
' located (must include build type).'))
parser.add_argument('--runtime-deps-path',
type=os.path.realpath,
help='Runtime data dependency file from GN.')
parser.add_argument('--test-name',
type=os.path.realpath,
help='Name of the the test')
parser.add_argument('--gtest_filter',
help='GTest filter to use in place of any default.')
parser.add_argument('--gtest_repeat',
help='GTest repeat value to use.')
parser.add_argument('--single-process-tests', action='store_true',
default=False,
help='Runs the tests and the launcher in the same '
'process. Useful for debugging.')
parser.add_argument('--test-launcher-batch-limit',
type=int,
help='Sets the limit of test batch to run in a single '
'process.')
# --test-launcher-filter-file is specified relative to --output-directory,
# so specifying type=os.path.* will break it.
parser.add_argument('--test-launcher-filter-file',
help='Pass filter file through to target process.')
parser.add_argument('--test-launcher-jobs',
type=int,
help='Sets the number of parallel test jobs.')
parser.add_argument('--test_launcher_summary_output',
help='Currently ignored for 2-sided roll.')
args = parser.parse_args()
bootfs = BuildBootfs(args.output_directory, args.runtime_deps_path,
args.test_name, args.gtest_filter, args.gtest_repeat,
args.test_launcher_batch_limit,
args.test_launcher_filter_file, args.test_launcher_jobs,
args.single_process_tests, args.dry_run)
qemu_path = os.path.join(SDK_ROOT, 'qemu', 'bin', 'qemu-system-x86_64')
qemu_command = [qemu_path,
'-m', '2048',
'-nographic',
'-net', 'none',
'-smp', '4',
'-machine', 'q35',
'-kernel', os.path.join(SDK_ROOT, 'kernel', 'magenta.bin'),
'-initrd', bootfs,
'-append', 'TERM=xterm-256color kernel.halt_on_panic=true']
if int(os.environ.get('CHROME_HEADLESS', 0)) == 0:
qemu_command += ['-enable-kvm', '-cpu', 'host,migratable=no']
else:
qemu_command += ['-cpu', 'Haswell,+smap,-check']
if args.dry_run:
print 'Run:', qemu_command
else:
prefix = r'^.*> '
bt_with_offset_re = re.compile(prefix +
'bt#(\d+): pc 0x[0-9a-f]+ sp (0x[0-9a-f]+) \((\S+),(0x[0-9a-f]+)\)$')
bt_end_re = re.compile(prefix + 'bt#(\d+): end')
qemu_popen = subprocess.Popen(qemu_command, stdout=subprocess.PIPE)
processed_lines = []
success = False
while True:
line = qemu_popen.stdout.readline()
if not line:
break
print line,
if 'SUCCESS: all tests passed.' in line:
success = True
if bt_end_re.match(line.strip()):
if processed_lines:
print '----- start symbolized stack'
for processed in processed_lines:
print processed
print '----- end symbolized stack'
processed_lines = []
else:
m = bt_with_offset_re.match(line.strip())
if m:
addr2line_output = subprocess.check_output(
['addr2line', '-Cipf', '--exe=' + args.test_name, m.group(4)])
prefix = '#%s: ' % m.group(1)
# addr2line outputs a second line for inlining information, offset
# that to align it properly after the frame index.
addr2line_filtered = addr2line_output.strip().replace(
'(inlined', ' ' * len(prefix) + '(inlined')
processed_lines.append('%s%s' % (prefix, addr2line_filtered))
qemu_popen.wait()
return 0 if success else 1
return 0
if __name__ == '__main__':
sys.exit(main())