blob: df86b9099e32dad2e32d94df0314856b4efe2037 [file] [log] [blame]
# Copyright (C) 2018 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import os
import select
import socket
import subprocess
import sys
import threading
from blinkpy.common import exit_codes
from blinkpy.common.path_finder import WEB_TESTS_LAST_COMPONENT
from blinkpy.common.path_finder import get_chromium_src_dir
from blinkpy.web_tests.port import base
from blinkpy.web_tests.port import driver
from blinkpy.web_tests.port import factory
from blinkpy.web_tests.port import linux
from blinkpy.web_tests.port import server_process
# Modules loaded dynamically in _import_fuchsia_runner().
# pylint: disable=invalid-name
fuchsia_target = None
qemu_target = None
symbolizer = None
# pylint: enable=invalid-name
# Imports Fuchsia runner modules. This is done dynamically only when FuchsiaPort
# is instantiated to avoid dependency on Fuchsia runner on other platforms.
def _import_fuchsia_runner():
sys.path.insert(0, os.path.join(get_chromium_src_dir(), 'build/fuchsia'))
# pylint: disable=import-error
# pylint: disable=invalid-name
# pylint: disable=redefined-outer-name
global aemu_target
import aemu_target
global device_target
import device_target
global fuchsia_target
import target as fuchsia_target
global qemu_target
import qemu_target
global symbolizer
import symbolizer
# pylint: enable=import-error
# pylint: enable=invalid-name
# pylint: disable=redefined-outer-name
# Path to the content shell package relative to the build directory.
CONTENT_SHELL_PACKAGE_PATH = 'gen/content/shell/content_shell/content_shell.far'
# HTTP path prefixes for the HTTP server.
# WEB_TEST_PATH_PREFIX should be matched to the local directory name of
# web_tests because some tests and test_runner find test root directory
# with it.
WEB_TESTS_PATH_PREFIX = '/third_party/blink/' + WEB_TESTS_LAST_COMPONENT
# Paths to the directory where the fonts are copied to. Must match the path in
# content/shell/app/blink_test_platform_support_fuchsia.cc .
FONTS_DEVICE_PATH = '/system/fonts'
# Number of CPU cores in qemu.
CPU_CORES = 4
# Number of content_shell instances to run in parallel. 1 per CPU core.
MAX_WORKERS = CPU_CORES
PROCESS_START_TIMEOUT = 20
_log = logging.getLogger(__name__)
def _subprocess_log_thread(pipe, prefix):
try:
while True:
line = pipe.readline()
if not line:
return
_log.error('%s: %s', prefix, line)
finally:
pipe.close()
class SubprocessOutputLogger(object):
def __init__(self, process, prefix):
self._process = process
self._thread = threading.Thread(
target=_subprocess_log_thread, args=(process.stdout, prefix))
self._thread.daemon = True
self._thread.start()
def __del__(self):
self.close()
def close(self):
self._process.kill()
class _TargetHost(object):
def __init__(self, build_path, build_ids_path, ports_to_forward, target,
results_directory):
try:
self._amber_repo = None
self._target = target
self._target.Start()
self._setup_target(build_path, build_ids_path, ports_to_forward,
results_directory)
except:
self.cleanup()
raise
def _setup_target(self, build_path, build_ids_path, ports_to_forward,
results_directory):
# Tell SSH to forward all server ports from the Fuchsia device to
# the host.
forwarding_flags = [
'-O',
'forward', # Send SSH mux control signal.
'-N', # Don't execute command
'-T' # Don't allocate terminal.
]
for port in ports_to_forward:
forwarding_flags += ['-R', '%d:localhost:%d' % (port, port)]
self._proxy = self._target.RunCommandPiped([],
ssh_args=forwarding_flags,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
self._listener = self._target.RunCommandPiped(['log_listener'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
listener_log_path = os.path.join(results_directory, 'system_log')
listener_log = open(listener_log_path, 'w')
self.symbolizer = symbolizer.RunSymbolizer(
self._listener.stdout, listener_log, [build_ids_path])
self._amber_repo = self._target.GetAmberRepo()
self._amber_repo.__enter__()
package_path = os.path.join(build_path, CONTENT_SHELL_PACKAGE_PATH)
self._target.InstallPackage([package_path])
# Process will be forked for each worker, which may make QemuTarget
# unusable (e.g. waitpid() for qemu process returns ECHILD after
# fork() ). Save command runner before fork()ing, to use it later to
# connect to the target.
self.target_command_runner = self._target.GetCommandRunner()
def run_command(self, command):
return self.target_command_runner.RunCommandPiped(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def cleanup(self):
if self._amber_repo:
self._amber_repo.__exit__(None, None, None)
if self._target:
# Emulator targets will be shutdown during cleanup.
# TODO(sergeyu): Currently __init__() always starts Qemu, so we can
# just shutdown it. Update this logic when reusing target devices
# for multiple test runs.
if not isinstance(self._target, device_target.DeviceTarget):
self._target.Shutdown()
self._target = None
class FuchsiaPort(base.Port):
port_name = 'fuchsia'
SUPPORTED_VERSIONS = ('fuchsia', )
FALLBACK_PATHS = {
'fuchsia':
['fuchsia'] + linux.LinuxPort.latest_platform_fallback_path()
}
def __init__(self, host, port_name, **kwargs):
super(FuchsiaPort, self).__init__(host, port_name, **kwargs)
self._operating_system = 'fuchsia'
self._version = 'fuchsia'
self._target_device = self.get_option('device')
# TODO(sergeyu): Add support for arm64.
self._architecture = 'x86_64'
self.server_process_constructor = FuchsiaServerProcess
# Used to implement methods that depend on the host platform.
self._host_port = factory.PortFactory(host).get(**kwargs)
self._target_host = self.get_option('fuchsia_target')
self._zircon_logger = None
self._host_ip = self.get_option('fuchsia_host_ip')
_import_fuchsia_runner()
def _driver_class(self):
return ChromiumFuchsiaDriver
def _path_to_driver(self, target=None):
return self._build_path_with_target(target, CONTENT_SHELL_PACKAGE_PATH)
def __del__(self):
if self._zircon_logger:
self._zircon_logger.close()
def setup_test_run(self):
super(FuchsiaPort, self).setup_test_run()
try:
target_args = {
'out_dir': self._build_path(),
'system_log_file': None,
'fuchsia_out_dir': self.get_option('fuchsia_out_dir')
}
if self._target_device == 'device':
additional_args = {
'target_cpu': self.get_option('fuchsia_target_cpu'),
'ssh_config': self.get_option('fuchsia_ssh_config'),
'os_check': 'ignore',
'host': self.get_option('fuchsia_host'),
'port': self.get_option('fuchsia_port'),
'node_name': self.get_option('fuchsia_node_name')
}
target_args.update(additional_args)
target = device_target.DeviceTarget(**target_args)
else:
additional_args = {
'target_cpu': 'x64',
'cpu_cores': CPU_CORES,
'require_kvm': True,
'ram_size_mb': 8192
}
if self._target_device == 'qemu':
target_args.update(additional_args)
target = qemu_target.QemuTarget(**target_args)
else:
additional_args.update({
'enable_graphics': False,
'hardware_gpu': False
})
target_args.update(additional_args)
target = aemu_target.AemuTarget(**target_args)
self._target_host = _TargetHost(self._build_path(),
self.get_build_ids_path(),
self.SERVER_PORTS, target,
self.results_directory())
if self.get_option('zircon_logging'):
self._zircon_logger = SubprocessOutputLogger(
self._target_host.run_command(['dlog', '-f']), 'Zircon')
# Save fuchsia_target in _options, so it can be shared with other
# workers.
self._options.fuchsia_target = self._target_host
except fuchsia_target.FuchsiaTargetException as e:
_log.error('Failed to start qemu: %s.', str(e))
return exit_codes.NO_DEVICES_EXIT_STATUS
def clean_up_test_run(self):
if self._target_host:
self._target_host.cleanup()
self._target_host = None
def num_workers(self, requested_num_workers):
# Run a single qemu instance.
return min(MAX_WORKERS, requested_num_workers)
def _default_timeout_ms(self):
# Use 20s timeout instead of the default 6s. This is necessary because
# the tests are executed in qemu, so they run slower compared to other
# platforms.
return 20000
def requires_http_server(self):
"""HTTP server is always required to avoid copying the tests to the VM.
"""
return True
def start_http_server(self, additional_dirs, number_of_drivers):
additional_dirs['/third_party/blink/PerformanceTests'] = \
self._perf_tests_dir()
additional_dirs[WEB_TESTS_PATH_PREFIX] = self.web_tests_dir()
additional_dirs['/gen'] = self.generated_sources_directory()
additional_dirs['/third_party/blink'] = \
self._path_from_chromium_base('third_party', 'blink')
super(FuchsiaPort, self).start_http_server(additional_dirs,
number_of_drivers)
def path_to_apache(self):
return self._host_port.path_to_apache()
def path_to_apache_config_file(self):
return self._host_port.path_to_apache_config_file()
def default_smoke_test_only(self):
return True
def get_target_host(self):
return self._target_host
def get_build_ids_path(self):
package_path = self._path_to_driver()
return os.path.join(os.path.dirname(package_path), 'ids.txt')
class ChromiumFuchsiaDriver(driver.Driver):
def __init__(self, port, worker_number, no_timeout=False):
super(ChromiumFuchsiaDriver, self).__init__(port, worker_number,
no_timeout)
def _initialize_server_process(self, server_name, cmd_line, environment):
self._server_process = self._port.server_process_constructor(
self._port,
server_name,
cmd_line,
environment,
more_logging=self._port.get_option('driver_logging'),
host_ip=self._port._host_ip)
def _base_cmd_line(self):
cmd = [
'run',
'fuchsia-pkg://fuchsia.com/content_shell#meta/content_shell.cmx'
]
if self._port._target_device == 'qemu':
cmd.append('--ozone-platform=headless')
# Use Scenic on AEMU
else:
cmd.extend([
'--ozone-platform=scenic', '--enable-oop-rasterization',
'--use-vulkan', '--enable-gpu-rasterization',
'--force-device-scale-factor=1', '--use-gl=stub',
'--enable-features=UseSkiaRenderer,Vulkan'
])
return cmd
def _command_from_driver_input(self, driver_input):
command = super(ChromiumFuchsiaDriver,
self)._command_from_driver_input(driver_input)
if command.startswith('/'):
relative_test_filename = \
os.path.relpath(command, self._port.web_tests_dir())
command = 'http://127.0.0.1:8000' + WEB_TESTS_PATH_PREFIX + \
'/' + relative_test_filename
return command
# Custom version of ServerProcess that runs processes on a remote device.
class FuchsiaServerProcess(server_process.ServerProcess):
def __init__(self,
port_obj,
name,
cmd,
env=None,
treat_no_data_as_crash=False,
more_logging=False,
host_ip=None):
super(FuchsiaServerProcess, self).__init__(
port_obj, name, cmd, env, treat_no_data_as_crash, more_logging)
self._symbolizer_proc = None
self._host_ip = host_ip or qemu_target.HOST_IP_ADDRESS
def _start(self):
if self._proc:
raise ValueError('%s already running' % self._name)
self._reset()
# Fuchsia doesn't support stdin stream for packaged applications, so the
# stdin stream for content_shell is routed through a separate TCP
# socket. Open a local socket and then pass the address with the port as
# --stdin-redirect parameter. content_shell will connect to this address
# and will use that connection as its stdin stream.
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind(('127.0.0.1', 0))
listen_socket.listen(1)
stdin_port = listen_socket.getsockname()[1]
command = ['%s=%s' % (k, v) for k, v in self._env.items()] + \
self._cmd + \
['--no-sandbox', '--stdin-redirect=%s:%s' %
(self._host_ip, stdin_port)]
proc = self._port.get_target_host().run_command(command)
# Wait for incoming connection from content_shell.
fd = listen_socket.fileno()
read_fds, _, _ = select.select([fd], [], [], PROCESS_START_TIMEOUT)
if fd not in read_fds:
listen_socket.close()
proc.kill()
raise driver.DeviceFailure(
'Timed out waiting connection from content_shell.')
# Python's interfaces for sockets and pipes are different. To masquerade
# the socket as a pipe dup() the file descriptor and pass it to
# os.fdopen().
stdin_socket, _ = listen_socket.accept()
fd = stdin_socket.fileno() # pylint: disable=no-member
stdin_pipe = os.fdopen(os.dup(fd), "w", 0)
stdin_socket.close()
proc.stdin.close()
proc.stdin = stdin_pipe
# Run symbolizer to filter the stderr stream.
self._symbolizer_proc = symbolizer.RunSymbolizer(
proc.stderr, subprocess.PIPE, [self._port.get_build_ids_path()])
proc.stderr = self._symbolizer_proc.stdout
self._set_proc(proc)
def stop(self, timeout_secs=0.0, kill_tree=False):
result = super(FuchsiaServerProcess, self).stop(
timeout_secs, kill_tree)
if self._symbolizer_proc:
self._symbolizer_proc.kill()
return result