blob: 978db9159ffba3d9187a7a2d0838f9a82d6b6d5d [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 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, ports_to_forward):
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)
self._target.Start()
self._setup_target(build_path, ports_to_forward)
except:
self.cleanup()
raise
def _setup_target(self, build_path, ports_to_forward):
# 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,
stderr=subprocess.PIPE)
package_path = os.path.join(build_path, CONTENT_SHELL_PACKAGE_PATH)
self._target.InstallPackage(package_path, "content_shell",
package_deps=[])
# 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._target:
# TODO(sergeyu): Currently __init__() always starts Qemu, so we can
# just shutdown it. Update this logic when reusing target devices
# for multiple test runs.
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'
# 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
_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:
self._target_host = _TargetHost(
self._build_path(), self.SERVER_PORTS)
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 20 * 1000
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 _base_cmd_line(self):
return ['run',
'fuchsia-pkg://fuchsia.com/content_shell#meta/content_shell.cmx',
'--ozone-platform=headless']
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):
super(FuchsiaServerProcess, self).__init__(
port_obj, name, cmd, env, treat_no_data_as_crash, more_logging)
self._symbolizer_proc = None
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' %
(qemu_target.HOST_IP_ADDRESS, 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, [self._port.get_build_ids_path()]);
proc.stderr = self._symbolizer_proc.stdout
self._set_proc(proc)
def stop(self, timeout_secs=0.0):
result = super(FuchsiaServerProcess, self).stop(timeout_secs)
if self._symbolizer_proc:
self._symbolizer_proc.kill()
return result