0

[Fuchsia] Allow the Fuchsia test-runner scripts to use AEMU

Adding an aemu_target.py that supports running headless AEMU with swiftshader.
Adds AEMU checkout to the build (with aemu_checkout set to True).
Add support for AEMU in layout tests.

R=marshallk@google.com

Bug: 1000906
Change-Id: Ie4a9e7957c5a33822dd44e8892c0820307ea5b02
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1789849
Reviewed-by: Kevin Marshall <kmarshall@chromium.org>
Reviewed-by: Nico Weber <thakis@chromium.org>
Commit-Queue: Chong Gu <chonggu@google.com>
Cr-Commit-Position: refs/heads/master@{#700484}
This commit is contained in:
Chong Gu
2019-09-26 23:24:02 +00:00
committed by Commit Bot
parent 2842a95740
commit 9ad904d8e0
9 changed files with 316 additions and 131 deletions

27
DEPS

@ -121,6 +121,11 @@ vars = {
# Wildcards are supported (e.g. "qemu.*").
'checkout_fuchsia_boot_images': "qemu.x64,qemu.arm64",
# By Default, do not checkout AEMU, as it is too big. This can be overridden
# e.g. with custom_vars.
# TODO(chonggu): Delete once AEMU package is small enough.
'checkout_aemu': False,
# Default to the empty board. Desktop Chrome OS builds don't need cros SDK
# dependencies. Other Chrome OS builds should always define this explicitly.
'cros_board': '',
@ -1327,6 +1332,28 @@ deps = {
'dep_type': 'cipd',
},
'src/third_party/aemu-linux-x64': {
'packages': [
{
'package': 'fuchsia/third_party/aemu/linux-amd64',
'version': 'IzRqaHDMNtw9FjGgpntL65P_3dvQRLIuzxBkSUpoG1UC'
},
],
'condition': 'host_os == "linux" and checkout_aemu',
'dep_type': 'cipd',
},
'src/third_party/aemu-mac-x64': {
'packages': [
{
'package': 'fuchsia/third_party/aemu/mac-amd64',
'version': 'T9bWxf8aUC5TwCFgPxpuW29Mfy-7Z9xCfXB9QO8MfU0C'
},
],
'condition': 'host_os == "mac" and checkout_aemu',
'dep_type': 'cipd',
},
'src/third_party/re2/src':
Var('chromium_git') + '/external/github.com/google/re2.git' + '@' + '67bce690decdb507b13e14050661f8b9ebfcfe6c',

@ -0,0 +1,72 @@
# Copyright 2019 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.
"""Implements commands for running and interacting with Fuchsia on AEMU."""
import os
import platform
import qemu_target
import logging
from common import GetEmuRootForPlatform
class AemuTarget(qemu_target.QemuTarget):
def __init__(self, output_dir, target_cpu, system_log_file, emu_type,
cpu_cores, require_kvm, ram_size_mb):
super(AemuTarget, self).__init__(output_dir, target_cpu, system_log_file,
emu_type, cpu_cores, require_kvm,
ram_size_mb)
# TODO(crbug.com/1000907): Enable AEMU for arm64.
if platform.machine() == 'aarch64':
raise Exception('AEMU does not support arm64 hosts.')
# TODO(bugs.fuchsia.dev/p/fuchsia/issues/detail?id=37301): Remove
# once aemu is part of default fuchsia build
def _EnsureEmulatorExists(self, path):
assert os.path.exists(path), \
'This checkout is missing %s. To check out the files, add this\n' \
'entry to the "custon_vars" section of your .gclient file:\n\n' \
' "checkout_aemu": True\n\n' % (self._emu_type)
def _BuildCommand(self):
aemu_exec = 'emulator-headless'
aemu_folder = GetEmuRootForPlatform(self._emu_type)
self._EnsureEmulatorExists(aemu_folder)
aemu_path = os.path.join(aemu_folder, aemu_exec)
# `VirtioInput` is needed for touch input device support on Fuchsia.
# `RefCountPipe` is needed for proper cleanup of resources when a process
# that uses Vulkan dies inside the guest
aemu_features = 'VirtioInput,RefCountPipe'
# Configure the CPU to emulate.
# On Linux, we can enable lightweight virtualization (KVM) if the host and
# guest architectures are the same.
if self._IsKvmEnabled():
aemu_features += ',KVM,GLDirectMem,Vulkan'
else:
if self._target_cpu != 'arm64':
aemu_features += ',-GLDirectMem'
# All args after -fuchsia flag gets passed to QEMU
aemu_command = [aemu_path,
'-feature', aemu_features,
'-window-size', '1024x600',
'-gpu', 'swiftshader_indirect',
'-fuchsia'
]
aemu_command.extend(self._BuildQemuConfig())
aemu_command.extend([
'-vga', 'none',
'-device', 'isa-debug-exit,iobase=0xf4,iosize=0x04',
'-device', 'virtio-keyboard-pci',
'-device', 'virtio_input_multi_touch_pci_1',
'-device', 'ich9-ahci,id=ahci'])
logging.info(' '.join(aemu_command))
return aemu_command

@ -40,9 +40,9 @@ def GetHostArchFromPlatform():
return 'arm64'
raise Exception('Unsupported host architecture: %s' % host_arch)
def GetQemuRootForPlatform():
def GetEmuRootForPlatform(emulator):
return os.path.join(DIR_SOURCE_ROOT, 'third_party',
'qemu-' + GetHostOsFromPlatform() + '-' +
emulator + '-' + GetHostOsFromPlatform() + '-' +
GetHostArchFromPlatform())
def ConnectPortForwardingTask(target, local_port, remote_port = 0):

@ -6,10 +6,10 @@ import logging
import os
import sys
from aemu_target import AemuTarget
from device_target import DeviceTarget
from qemu_target import QemuTarget
def AddCommonArgs(arg_parser):
"""Adds command line arguments to |arg_parser| for options which are shared
across test and executable target types."""
@ -34,8 +34,13 @@ def AddCommonArgs(arg_parser):
common_args.add_argument('--target-staging-path',
help='target path under which to stage packages '
'during deployment.', default='/data')
common_args.add_argument('--device', '-d', action='store_true', default=False,
help='Run on hardware device instead of QEMU.')
common_args.add_argument('--device', default=None,
choices=['aemu','qemu','device'],
help='Choose to run on aemu|qemu|device. ' +
'By default, Fuchsia will run in QEMU.')
common_args.add_argument('-d', action='store_const', dest='device',
const='device',
help='Run on device instead of emulator.')
common_args.add_argument('--host', help='The IP of the target device. ' +
'Optional.')
common_args.add_argument('--node-name',
@ -64,7 +69,11 @@ def AddCommonArgs(arg_parser):
help='Enable debug-level logging.')
common_args.add_argument('--qemu-cpu-cores', type=int, default=4,
help='Sets the number of CPU cores to provide if '
'launching in a VM with QEMU.'),
'launching in a VM.'),
common_args.add_argument('--memory', type=int, default=2048,
help='Sets the RAM size (MB) if launching in a VM'),
common_args.add_argument('--no-kvm', action='store_true', default=False,
help='Disable KVM virtualization'),
common_args.add_argument(
'--os_check', choices=['check', 'update', 'ignore'],
default='update',
@ -94,7 +103,6 @@ def ConfigureLogging(args):
def GetDeploymentTargetForArgs(args, require_kvm=False):
"""Constructs a deployment target object using parameters taken from
command line arguments."""
if args.system_log_file == '-':
system_log_file = sys.stdout
elif args.system_log_file:
@ -102,19 +110,27 @@ def GetDeploymentTargetForArgs(args, require_kvm=False):
else:
system_log_file = None
# Allow fuchsia to run on qemu if device not explicitly chosen.
if not args.device:
return QemuTarget(output_dir=args.output_directory,
target_cpu=args.target_cpu,
cpu_cores=args.qemu_cpu_cores,
system_log_file=system_log_file,
require_kvm=require_kvm)
args.device = 'qemu'
target_args = { 'output_dir':args.output_directory,
'target_cpu':args.target_cpu,
'system_log_file':system_log_file }
if args.device == 'device':
target_args.update({ 'host':args.host,
'node_name':args.node_name,
'port':args.port,
'ssh_config':args.ssh_config,
'fuchsia_out_dir':args.fuchsia_out_dir,
'os_check':args.os_check })
return DeviceTarget(**target_args)
else:
return DeviceTarget(output_dir=args.output_directory,
target_cpu=args.target_cpu,
host=args.host,
node_name=args.node_name,
port=args.port,
ssh_config=args.ssh_config,
fuchsia_out_dir=args.fuchsia_out_dir,
system_log_file=system_log_file,
os_check=args.os_check)
target_args.update({ 'cpu_cores':args.qemu_cpu_cores,
'require_kvm':not args.no_kvm,
'emu_type':args.device,
'ram_size_mb':args.memory })
if args.device == 'qemu':
return QemuTarget(**target_args)
else:
return AemuTarget(**target_args)

@ -0,0 +1,92 @@
# Copyright 2019 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.
"""Implements commands for running/interacting with Fuchsia on an emulator."""
import boot_data
import logging
import os
import subprocess
import sys
import target
import tempfile
class EmuTarget(target.Target):
def __init__(self, output_dir, target_cpu, system_log_file):
"""output_dir: The directory which will contain the files that are
generated to support the emulator deployment.
target_cpu: The emulated target CPU architecture.
Can be 'x64' or 'arm64'."""
super(EmuTarget, self).__init__(output_dir, target_cpu)
self._emu_process = None
self._system_log_file = system_log_file
def __enter__(self):
return self
def _GetEmulatorName(self):
pass
def _BuildCommand(self):
"""Build the command that will be run to start Fuchsia in the emulator."""
pass
# Used by the context manager to ensure that the emulator is killed when
# the Python process exits.
def __exit__(self, exc_type, exc_val, exc_tb):
self.Shutdown();
def Start(self):
emu_command = self._BuildCommand()
# We pass a separate stdin stream. Sharing stdin across processes
# leads to flakiness due to the OS prematurely killing the stream and the
# Python script panicking and aborting.
# The precise root cause is still nebulous, but this fix works.
# See crbug.com/741194.
logging.debug('Launching %s.' % (self._GetEmulatorName()))
logging.debug(' '.join(emu_command))
# Zircon sends debug logs to serial port (see kernel.serial=legacy flag
# above). Serial port is redirected to a file through emulator stdout.
# Unless a |_system_log_file| is explicitly set, we output the kernel serial
# log to a temporary file, and print that out if we are unable to connect to
# the emulator guest, to make it easier to diagnose connectivity issues.
temporary_system_log_file = None
if self._system_log_file:
stdout = self._system_log_file
stderr = subprocess.STDOUT
else:
temporary_system_log_file = tempfile.NamedTemporaryFile('w')
stdout = temporary_system_log_file
stderr = sys.stderr
self._emu_process = subprocess.Popen(emu_command, stdin=open(os.devnull),
stdout=stdout, stderr=stderr)
try:
self._WaitUntilReady();
except target.FuchsiaTargetException:
if temporary_system_log_file:
logging.info('Kernel logs:\n' +
open(temporary_system_log_file.name, 'r').read())
raise
def Shutdown(self):
if self._IsEmuStillRunning():
logging.info('Shutting down %s' % (self._GetEmulatorName()))
self._emu_process.kill()
def _IsEmuStillRunning(self):
if not self._emu_process:
return False
return os.waitpid(self._emu_process.pid, os.WNOHANG)[0] == 0
def _GetEndpoint(self):
if not self._IsEmuStillRunning():
raise Exception('%s quit unexpectedly.' % (self._GetEmulatorName()))
return ('localhost', self._host_ssh_port)
def _GetSshConfigPath(self):
return boot_data.GetSSHConfigPath(self._output_dir)

@ -6,19 +6,17 @@
import boot_data
import common
import emu_target
import logging
import md5
import os
import platform
import shutil
import socket
import subprocess
import sys
import target
import tempfile
import time
from common import GetQemuRootForPlatform, EnsurePathExists
from common import GetEmuRootForPlatform, EnsurePathExists
# Virtual networking configuration data for QEMU.
@ -31,46 +29,33 @@ GUEST_MAC_ADDRESS = '52:54:00:63:5e:7b'
EXTENDED_BLOBSTORE_SIZE = 1073741824 # 1GB
class QemuTarget(target.Target):
def __init__(self, output_dir, target_cpu, cpu_cores, system_log_file,
require_kvm, ram_size_mb=2048):
"""output_dir: The directory which will contain the files that are
generated to support the QEMU deployment.
target_cpu: The emulated target CPU architecture.
Can be 'x64' or 'arm64'."""
super(QemuTarget, self).__init__(output_dir, target_cpu)
self._qemu_process = None
self._ram_size_mb = ram_size_mb
self._system_log_file = system_log_file
self._cpu_cores = cpu_cores
self._require_kvm = require_kvm
class QemuTarget(emu_target.EmuTarget):
def __init__(self, output_dir, target_cpu, system_log_file,
emu_type, cpu_cores, require_kvm, ram_size_mb):
super(QemuTarget, self).__init__(output_dir, target_cpu,
system_log_file)
self._emu_type=emu_type
self._cpu_cores=cpu_cores
self._require_kvm=require_kvm
self._ram_size_mb=ram_size_mb
def __enter__(self):
return self
def _GetEmulatorName(self):
return self._emu_type
# Used by the context manager to ensure that QEMU is killed when the Python
# process exits.
def __exit__(self, exc_type, exc_val, exc_tb):
self.Shutdown();
def _IsKvmEnabled(self):
if self._require_kvm:
if (sys.platform.startswith('linux') and
os.access('/dev/kvm', os.R_OK | os.W_OK)):
if self._target_cpu == 'arm64' and platform.machine() == 'aarch64':
return True
if self._target_cpu == 'x64' and platform.machine() == 'x86_64':
return True
return False
def Start(self):
def _BuildQemuConfig(self):
boot_data.AssertBootImagesExist(self._GetTargetSdkArch(), 'qemu')
qemu_path = os.path.join(GetQemuRootForPlatform(), 'bin',
'qemu-system-' + self._GetTargetSdkLegacyArch())
kernel_args = boot_data.GetKernelArgs(self._output_dir)
# TERM=dumb tells the guest OS to not emit ANSI commands that trigger
# noisy ANSI spew from the user's terminal emulator.
kernel_args.append('TERM=dumb')
# Enable logging to the serial port. This is a temporary fix to investigate
# the root cause for https://crbug.com/869753 .
kernel_args.append('kernel.serial=legacy')
qemu_command = [qemu_path,
'-m', str(self._ram_size_mb),
'-nographic',
emu_command = [
'-kernel', EnsurePathExists(
boot_data.GetTargetFile('qemu-kernel.kernel',
self._GetTargetSdkArch(),
@ -78,6 +63,7 @@ class QemuTarget(target.Target):
'-initrd', EnsurePathExists(
boot_data.GetBootImage(self._output_dir, self._GetTargetSdkArch(),
boot_data.TARGET_TYPE_QEMU)),
'-m', str(self._ram_size_mb),
'-smp', str(self._cpu_cores),
# Attach the blobstore and data volumes. Use snapshot mode to discard
@ -92,38 +78,20 @@ class QemuTarget(target.Target):
# monitor.
'-serial', 'stdio',
'-monitor', 'none',
'-append', ' '.join(kernel_args)
]
# Configure the machine to emulate, based on the target architecture.
if self._target_cpu == 'arm64':
qemu_command.extend([
emu_command.extend([
'-machine','virt',
])
netdev_type = 'virtio-net-pci'
else:
qemu_command.extend([
emu_command.extend([
'-machine', 'q35',
])
netdev_type = 'e1000'
# Configure the CPU to emulate.
# On Linux, we can enable lightweight virtualization (KVM) if the host and
# guest architectures are the same.
enable_kvm = self._require_kvm or (sys.platform.startswith('linux') and (
(self._target_cpu == 'arm64' and platform.machine() == 'aarch64') or
(self._target_cpu == 'x64' and platform.machine() == 'x86_64')) and
os.access('/dev/kvm', os.R_OK | os.W_OK))
if enable_kvm:
qemu_command.extend(['-enable-kvm', '-cpu', 'host,migratable=no'])
else:
logging.warning('Unable to launch QEMU with KVM acceleration.')
if self._target_cpu == 'arm64':
qemu_command.extend(['-cpu', 'cortex-a53'])
else:
qemu_command.extend(['-cpu', 'Haswell,+smap,-check,-fsgsbase'])
# Configure virtual network. It is used in the tests to connect to
# testserver running on the host.
netdev_config = 'user,id=net0,net=%s,dhcpstart=%s,host=%s' % \
@ -131,61 +99,50 @@ class QemuTarget(target.Target):
self._host_ssh_port = common.GetAvailableTcpPort()
netdev_config += ",hostfwd=tcp::%s-:22" % self._host_ssh_port
qemu_command.extend([
emu_command.extend([
'-netdev', netdev_config,
'-device', '%s,netdev=net0,mac=%s' % (netdev_type, GUEST_MAC_ADDRESS),
])
# We pass a separate stdin stream to qemu. Sharing stdin across processes
# leads to flakiness due to the OS prematurely killing the stream and the
# Python script panicking and aborting.
# The precise root cause is still nebulous, but this fix works.
# See crbug.com/741194.
logging.debug('Launching QEMU.')
logging.debug(' '.join(qemu_command))
# Zircon sends debug logs to serial port (see kernel.serial=legacy flag
# above). Serial port is redirected to a file through QEMU stdout.
# Unless a |_system_log_file| is explicitly set, we output the kernel serial
# log to a temporary file, and print that out if we are unable to connect to
# the QEMU guest, to make it easier to diagnose connectivity issues.
temporary_system_log_file = None
if self._system_log_file:
stdout = self._system_log_file
stderr = subprocess.STDOUT
# Configure the CPU to emulate.
# On Linux, we can enable lightweight virtualization (KVM) if the host and
# guest architectures are the same.
if self._IsKvmEnabled():
kvm_command = ['-enable-kvm', '-cpu', 'host,migratable=no']
else:
temporary_system_log_file = tempfile.NamedTemporaryFile('w')
stdout = temporary_system_log_file
stderr = sys.stderr
logging.warning('Unable to launch %s with KVM acceleration.'
% (self._emu_type) +
'The guest VM will be slow.')
if self._target_cpu == 'arm64':
kvm_command = ['-cpu', 'cortex-a53']
else:
kvm_command = ['-cpu', 'Haswell,+smap,-check,-fsgsbase']
self._qemu_process = subprocess.Popen(qemu_command, stdin=open(os.devnull),
stdout=stdout, stderr=stderr)
try:
self._WaitUntilReady();
except target.FuchsiaTargetException:
if temporary_system_log_file:
logging.info("Kernel logs:\n" +
open(temporary_system_log_file.name, 'r').read())
raise
emu_command.extend(kvm_command)
def Shutdown(self):
if self._IsQemuStillRunning():
logging.info('Shutting down QEMU.')
self._qemu_process.kill()
kernel_args = boot_data.GetKernelArgs(self._output_dir)
def _IsQemuStillRunning(self):
if not self._qemu_process:
return False
return os.waitpid(self._qemu_process.pid, os.WNOHANG)[0] == 0
# TERM=dumb tells the guest OS to not emit ANSI commands that trigger
# noisy ANSI spew from the user's terminal emulator.
kernel_args.append('TERM=dumb')
def _GetEndpoint(self):
if not self._IsQemuStillRunning():
raise Exception('QEMU quit unexpectedly.')
return ('localhost', self._host_ssh_port)
# Construct kernel cmd line
kernel_args.append('kernel.serial=legacy')
def _GetSshConfigPath(self):
return boot_data.GetSSHConfigPath(self._output_dir)
# Don't 'reboot' the emulator if the kernel crashes
kernel_args.append('kernel.halt-on-panic=true')
emu_command.extend(['-append', ' '.join(kernel_args)])
return emu_command
def _BuildCommand(self):
qemu_exec = 'qemu-system-'+self._GetTargetSdkLegacyArch()
qemu_command = [os.path.join(GetEmuRootForPlatform(self._emu_type), 'bin',
qemu_exec)]
qemu_command.extend(self._BuildQemuConfig())
qemu_command.append('-nographic')
return qemu_command
def _ComputeFileHash(filename):
hasher = md5.new()
@ -202,7 +159,8 @@ def _EnsureBlobstoreQcowAndReturnPath(output_dir, target_arch):
"""Returns a file containing the Fuchsia blobstore in a QCOW format,
with extra buffer space added for growth."""
qimg_tool = os.path.join(common.GetQemuRootForPlatform(), 'bin', 'qemu-img')
qimg_tool = os.path.join(common.GetEmuRootForPlatform('qemu'),
'bin', 'qemu-img')
fvm_tool = os.path.join(common.SDK_ROOT, 'tools', 'fvm')
blobstore_path = boot_data.GetTargetFile('storage-full.blk', target_arch,
'qemu')

@ -193,7 +193,7 @@ group.
### Running test suites
Building test suites generate a launcher script to run them on a QEMU instance
Building test suites generate a launcher script to run them on an emulator
or a physical device. These scripts are generated at `out/fuchsia/bin`. For
instance,to run the `base_unittests` target, launch:
@ -204,6 +204,10 @@ $ out/fuchsia/bin/run_base_unittests
Common gtest arguments such as `--gtest_filter=...` are supported by the run
script. The launcher script also symbolizes backtraces.
The test suite, by default, will run on QEMU. AEMU can be used for running
tests that interact with Fuchsia's window manager, Scenic. To change the device
that Fuchsia will run on, use `--device={aemu|qemu|device}`.
To run a test suite on an *unprovisioned device* in a zedboot state, simply add
`-d` to the test runner script arguments. Subsequent runs of the test runner
script will be able to pick up the same device.

@ -60,6 +60,8 @@ def _import_fuchsia_runner():
# pylint: disable=import-error
# pylint: disable=invalid-name
# pylint: disable=redefined-outer-name
global aemu_target
import aemu_target
global fuchsia_target
import target as fuchsia_target
global qemu_target
@ -123,12 +125,20 @@ class SubprocessOutputLogger(object):
self._process.kill()
class _TargetHost(object):
def __init__(self, build_path, ports_to_forward):
def __init__(self, build_path, ports_to_forward, target_device):
try:
self._target = None
self._target = qemu_target.QemuTarget(
build_path, 'x64', cpu_cores=CPU_CORES, system_log_file=None,
require_kvm=True, ram_size_mb=8192)
target_args = { 'output_dir':build_path,
'target_cpu':'x64',
'system_log_file':None,
'cpu_cores':CPU_CORES,
'require_kvm':True,
'emu_type':target_device,
'ram_size_mb':8192}
if target_device == 'qemu':
self._target = qemu_target.QemuTarget(**target_args)
else:
self._target = aemu_target.AemuTarget(**target_args)
self._target.Start()
self._setup_target(build_path, ports_to_forward)
except:
@ -185,6 +195,7 @@ class FuchsiaPort(base.Port):
self._operating_system = 'fuchsia'
self._version = 'fuchsia'
self._target_device = self.get_option('device')
# TODO(sergeyu): Add support for arm64.
self._architecture = 'x86_64'
@ -212,7 +223,7 @@ class FuchsiaPort(base.Port):
super(FuchsiaPort, self).setup_test_run()
try:
self._target_host = _TargetHost(
self._build_path(), self.SERVER_PORTS)
self._build_path(), self.SERVER_PORTS, self._target_device)
if self.get_option('zircon_logging'):
self._zircon_logger = SubprocessOutputLogger(

@ -143,6 +143,11 @@ def parse_args(args):
action='store_false',
default=True,
help=('Do not log Zircon debug messages.')),
optparse.make_option(
'--device',
choices=['aemu','qemu'],
default='qemu',
help=('Choose device to launch Fuchsia with.')),
]))
option_group_definitions.append(