summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/lacros
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/build/lacros')
-rw-r--r--third_party/libwebrtc/build/lacros/BUILD.gn20
-rw-r--r--third_party/libwebrtc/build/lacros/OWNERS3
-rw-r--r--third_party/libwebrtc/build/lacros/PRESUBMIT.py25
-rw-r--r--third_party/libwebrtc/build/lacros/lacros_resource_sizes.gni21
-rwxr-xr-xthird_party/libwebrtc/build/lacros/lacros_resource_sizes.py350
-rw-r--r--third_party/libwebrtc/build/lacros/lacros_resource_sizes.pydeps12
-rw-r--r--third_party/libwebrtc/build/lacros/metadata.json.in6
-rwxr-xr-xthird_party/libwebrtc/build/lacros/mojo_connection_lacros_launcher.py160
-rwxr-xr-xthird_party/libwebrtc/build/lacros/test_runner.py581
-rwxr-xr-xthird_party/libwebrtc/build/lacros/test_runner_test.py267
10 files changed, 1445 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/lacros/BUILD.gn b/third_party/libwebrtc/build/lacros/BUILD.gn
new file mode 100644
index 0000000000..216010a42e
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/BUILD.gn
@@ -0,0 +1,20 @@
+# Copyright 2020 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.
+
+import("//build/config/python.gni")
+import("//build/util/process_version.gni")
+
+python_library("lacros_resource_sizes_py") {
+ pydeps_file = "lacros_resource_sizes.pydeps"
+ data = [ "//buildtools/third_party/eu-strip/bin/eu-strip" ]
+ data_deps = [ "//third_party/catapult/tracing:convert_chart_json" ]
+}
+
+process_version("lacros_version_metadata") {
+ sources = [ "//chrome/VERSION" ]
+
+ template_file = "metadata.json.in"
+ output = "$root_out_dir/metadata.json"
+ process_only = true
+}
diff --git a/third_party/libwebrtc/build/lacros/OWNERS b/third_party/libwebrtc/build/lacros/OWNERS
new file mode 100644
index 0000000000..aae4f73726
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/OWNERS
@@ -0,0 +1,3 @@
+svenzheng@chromium.org
+liaoyuke@chromium.org
+erikchen@chromium.org
diff --git a/third_party/libwebrtc/build/lacros/PRESUBMIT.py b/third_party/libwebrtc/build/lacros/PRESUBMIT.py
new file mode 100644
index 0000000000..1667f2ae39
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/PRESUBMIT.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2020 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.
+"""Presubmit script for changes affecting //build/lacros"""
+
+USE_PYTHON3 = True
+
+
+def _CommonChecks(input_api, output_api):
+ tests = input_api.canned_checks.GetUnitTestsInDirectory(
+ input_api,
+ output_api,
+ '.', [r'^.+_test\.py$'],
+ run_on_python2=False,
+ run_on_python3=True,
+ skip_shebang_check=True)
+ return input_api.RunTests(tests)
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
diff --git a/third_party/libwebrtc/build/lacros/lacros_resource_sizes.gni b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.gni
new file mode 100644
index 0000000000..0c50e0fe34
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.gni
@@ -0,0 +1,21 @@
+# Copyright 2020 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.
+
+import("//build/util/generate_wrapper.gni")
+
+# Generates a script in the bin directory that runs
+# //build/lacros/lacros_resource_sizes.py for the provided configuration.
+template("lacros_resource_sizes_test") {
+ generate_wrapper(target_name) {
+ forward_variables_from(invoker, [ "data_deps" ])
+ executable = "//build/lacros/lacros_resource_sizes.py"
+ wrapper_script = "$root_out_dir/bin/run_${target_name}"
+
+ deps = [ "//build/lacros:lacros_resource_sizes_py" ]
+ executable_args = [
+ "--chromium-output-directory",
+ "@WrappedPath(.)",
+ ]
+ }
+}
diff --git a/third_party/libwebrtc/build/lacros/lacros_resource_sizes.py b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.py
new file mode 100755
index 0000000000..f761662861
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.py
@@ -0,0 +1,350 @@
+#!/usr/bin/env python
+# Copyright 2020 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.
+"""Reports binary size metrics for LaCrOS build artifacts.
+
+More information at //docs/speed/binary_size/metrics.md.
+"""
+
+import argparse
+import collections
+import contextlib
+import json
+import logging
+import os
+import subprocess
+import sys
+import tempfile
+
+
+@contextlib.contextmanager
+def _SysPath(path):
+ """Library import context that temporarily appends |path| to |sys.path|."""
+ if path and path not in sys.path:
+ sys.path.insert(0, path)
+ else:
+ path = None # Indicates that |sys.path| is not modified.
+ try:
+ yield
+ finally:
+ if path:
+ sys.path.pop(0)
+
+
+DIR_SOURCE_ROOT = os.environ.get(
+ 'CHECKOUT_SOURCE_ROOT',
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)))
+
+BUILD_COMMON_PATH = os.path.join(DIR_SOURCE_ROOT, 'build', 'util', 'lib',
+ 'common')
+
+TRACING_PATH = os.path.join(DIR_SOURCE_ROOT, 'third_party', 'catapult',
+ 'tracing')
+
+EU_STRIP_PATH = os.path.join(DIR_SOURCE_ROOT, 'buildtools', 'third_party',
+ 'eu-strip', 'bin', 'eu-strip')
+
+with _SysPath(BUILD_COMMON_PATH):
+ import perf_tests_results_helper # pylint: disable=import-error
+
+with _SysPath(TRACING_PATH):
+ from tracing.value import convert_chart_json # pylint: disable=import-error
+
+_BASE_CHART = {
+ 'format_version': '0.1',
+ 'benchmark_name': 'resource_sizes',
+ 'benchmark_description': 'LaCrOS resource size information.',
+ 'trace_rerun_options': [],
+ 'charts': {}
+}
+
+_KEY_RAW = 'raw'
+_KEY_GZIPPED = 'gzipped'
+_KEY_STRIPPED = 'stripped'
+_KEY_STRIPPED_GZIPPED = 'stripped_then_gzipped'
+
+
+class _Group:
+ """A group of build artifacts whose file sizes are summed and tracked.
+
+ Build artifacts for size tracking fall under these categories:
+ * File: A single file.
+ * Group: A collection of files.
+ * Dir: All files under a directory.
+
+ Attributes:
+ paths: A list of files or directories to be tracked together.
+ title: The display name of the group.
+ track_stripped: Whether to also track summed stripped ELF sizes.
+ track_compressed: Whether to also track summed compressed sizes.
+ """
+
+ def __init__(self, paths, title, track_stripped=False,
+ track_compressed=False):
+ self.paths = paths
+ self.title = title
+ self.track_stripped = track_stripped
+ self.track_compressed = track_compressed
+
+
+# List of disjoint build artifact groups for size tracking. This list should be
+# synched with lacros-amd64-generic-binary-size-rel builder contents (specified
+# in # //infra/config/subprojects/chromium/ci.star) and
+# chromeos-amd64-generic-lacros-internal builder (specified in src-internal).
+_TRACKED_GROUPS = [
+ _Group(paths=['chrome'],
+ title='File: chrome',
+ track_stripped=True,
+ track_compressed=True),
+ _Group(paths=['chrome_crashpad_handler'],
+ title='File: chrome_crashpad_handler'),
+ _Group(paths=['icudtl.dat'], title='File: icudtl.dat'),
+ _Group(paths=['nacl_helper'], title='File: nacl_helper'),
+ _Group(paths=['nacl_irt_x86_64.nexe'], title='File: nacl_irt_x86_64.nexe'),
+ _Group(paths=['resources.pak'], title='File: resources.pak'),
+ _Group(paths=[
+ 'chrome_100_percent.pak', 'chrome_200_percent.pak', 'headless_lib.pak'
+ ],
+ title='Group: Other PAKs'),
+ _Group(paths=['snapshot_blob.bin'], title='Group: Misc'),
+ _Group(paths=['locales/'], title='Dir: locales'),
+ _Group(paths=['swiftshader/'], title='Dir: swiftshader'),
+ _Group(paths=['WidevineCdm/'], title='Dir: WidevineCdm'),
+]
+
+
+def _visit_paths(base_dir, paths):
+ """Itemizes files specified by a list of paths.
+
+ Args:
+ base_dir: Base directory for all elements in |paths|.
+ paths: A list of filenames or directory names to specify files whose sizes
+ to be counted. Directories are recursed. There's no de-duping effort.
+ Non-existing files or directories are ignored (with warning message).
+ """
+ for path in paths:
+ full_path = os.path.join(base_dir, path)
+ if os.path.exists(full_path):
+ if os.path.isdir(full_path):
+ for dirpath, _, filenames in os.walk(full_path):
+ for filename in filenames:
+ yield os.path.join(dirpath, filename)
+ else: # Assume is file.
+ yield full_path
+ else:
+ logging.critical('Not found: %s', path)
+
+
+def _is_probably_elf(filename):
+ """Heuristically decides whether |filename| is ELF via magic signature."""
+ with open(filename, 'rb') as fh:
+ return fh.read(4) == '\x7FELF'
+
+
+def _is_unstrippable_elf(filename):
+ """Identifies known-unstrippable ELF files to denoise the system."""
+ return filename.endswith('.nexe') or filename.endswith('libwidevinecdm.so')
+
+
+def _get_filesize(filename):
+ """Returns the size of a file, or 0 if file is not found."""
+ try:
+ return os.path.getsize(filename)
+ except OSError:
+ logging.critical('Failed to get size: %s', filename)
+ return 0
+
+
+def _get_gzipped_filesize(filename):
+ """Returns the gzipped size of a file, or 0 if file is not found."""
+ BUFFER_SIZE = 65536
+ if not os.path.isfile(filename):
+ return 0
+ try:
+ # Call gzip externally instead of using gzip package since it's > 2x faster.
+ cmd = ['gzip', '-c', filename]
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ # Manually counting bytes instead of using len(p.communicate()[0]) to avoid
+ # buffering the entire compressed data (can be ~100 MB).
+ ret = 0
+ while True:
+ chunk = len(p.stdout.read(BUFFER_SIZE))
+ if chunk == 0:
+ break
+ ret += chunk
+ return ret
+ except OSError:
+ logging.critical('Failed to get gzipped size: %s', filename)
+ return 0
+
+
+def _get_catagorized_filesizes(filename):
+ """Measures |filename| sizes under various transforms.
+
+ Returns: A Counter (keyed by _Key_* constants) that stores measured sizes.
+ """
+ sizes = collections.Counter()
+ sizes[_KEY_RAW] = _get_filesize(filename)
+ sizes[_KEY_GZIPPED] = _get_gzipped_filesize(filename)
+
+ # Pre-assign values for non-ELF, or in case of failure for ELF.
+ sizes[_KEY_STRIPPED] = sizes[_KEY_RAW]
+ sizes[_KEY_STRIPPED_GZIPPED] = sizes[_KEY_GZIPPED]
+
+ if _is_probably_elf(filename) and not _is_unstrippable_elf(filename):
+ try:
+ fd, temp_file = tempfile.mkstemp()
+ os.close(fd)
+ cmd = [EU_STRIP_PATH, filename, '-o', temp_file]
+ subprocess.check_output(cmd)
+ sizes[_KEY_STRIPPED] = _get_filesize(temp_file)
+ sizes[_KEY_STRIPPED_GZIPPED] = _get_gzipped_filesize(temp_file)
+ if sizes[_KEY_STRIPPED] > sizes[_KEY_RAW]:
+ # This weird case has been observed for libwidevinecdm.so.
+ logging.critical('Stripping made things worse for %s' % filename)
+ except subprocess.CalledProcessError:
+ logging.critical('Failed to strip file: %s' % filename)
+ finally:
+ os.unlink(temp_file)
+ return sizes
+
+
+def _dump_chart_json(output_dir, chartjson):
+ """Writes chart histogram to JSON files.
+
+ Output files:
+ results-chart.json contains the chart JSON.
+ perf_results.json contains histogram JSON for Catapult.
+
+ Args:
+ output_dir: Directory to place the JSON files.
+ chartjson: Source JSON data for output files.
+ """
+ results_path = os.path.join(output_dir, 'results-chart.json')
+ logging.critical('Dumping chartjson to %s', results_path)
+ with open(results_path, 'w') as json_file:
+ json.dump(chartjson, json_file, indent=2)
+
+ # We would ideally generate a histogram set directly instead of generating
+ # chartjson then converting. However, perf_tests_results_helper is in
+ # //build, which doesn't seem to have any precedent for depending on
+ # anything in Catapult. This can probably be fixed, but since this doesn't
+ # need to be super fast or anything, converting is a good enough solution
+ # for the time being.
+ histogram_result = convert_chart_json.ConvertChartJson(results_path)
+ if histogram_result.returncode != 0:
+ raise Exception('chartjson conversion failed with error: ' +
+ histogram_result.stdout)
+
+ histogram_path = os.path.join(output_dir, 'perf_results.json')
+ logging.critical('Dumping histograms to %s', histogram_path)
+ with open(histogram_path, 'w') as json_file:
+ json_file.write(histogram_result.stdout)
+
+
+def _run_resource_sizes(args):
+ """Main flow to extract and output size data."""
+ chartjson = _BASE_CHART.copy()
+ report_func = perf_tests_results_helper.ReportPerfResult
+ total_sizes = collections.Counter()
+
+ def report_sizes(sizes, title, track_stripped, track_compressed):
+ report_func(chart_data=chartjson,
+ graph_title=title,
+ trace_title='size',
+ value=sizes[_KEY_RAW],
+ units='bytes')
+
+ if track_stripped:
+ report_func(chart_data=chartjson,
+ graph_title=title + ' (Stripped)',
+ trace_title='size',
+ value=sizes[_KEY_STRIPPED],
+ units='bytes')
+
+ if track_compressed:
+ report_func(chart_data=chartjson,
+ graph_title=title + ' (Gzipped)',
+ trace_title='size',
+ value=sizes[_KEY_GZIPPED],
+ units='bytes')
+
+ if track_stripped and track_compressed:
+ report_func(chart_data=chartjson,
+ graph_title=title + ' (Stripped, Gzipped)',
+ trace_title='size',
+ value=sizes[_KEY_STRIPPED_GZIPPED],
+ units='bytes')
+
+ for g in _TRACKED_GROUPS:
+ sizes = sum(
+ map(_get_catagorized_filesizes, _visit_paths(args.out_dir, g.paths)),
+ collections.Counter())
+ report_sizes(sizes, g.title, g.track_stripped, g.track_compressed)
+
+ # Total compressed size is summed over individual compressed sizes, instead
+ # of concatanating first, then compress everything. This is done for
+ # simplicity. It also gives a conservative size estimate (assuming file
+ # metadata and overheads are negligible).
+ total_sizes += sizes
+
+ report_sizes(total_sizes, 'Total', True, True)
+
+ _dump_chart_json(args.output_dir, chartjson)
+
+
+def main():
+ """Parses arguments and runs high level flows."""
+ argparser = argparse.ArgumentParser(description='Writes LaCrOS size metrics.')
+
+ argparser.add_argument('--chromium-output-directory',
+ dest='out_dir',
+ required=True,
+ type=os.path.realpath,
+ help='Location of the build artifacts.')
+
+ output_group = argparser.add_mutually_exclusive_group()
+
+ output_group.add_argument('--output-dir',
+ default='.',
+ help='Directory to save chartjson to.')
+
+ # Accepted to conform to the isolated script interface, but ignored.
+ argparser.add_argument('--isolated-script-test-filter',
+ help=argparse.SUPPRESS)
+ argparser.add_argument('--isolated-script-test-perf-output',
+ type=os.path.realpath,
+ help=argparse.SUPPRESS)
+
+ output_group.add_argument(
+ '--isolated-script-test-output',
+ type=os.path.realpath,
+ help='File to which results will be written in the simplified JSON '
+ 'output format.')
+
+ args = argparser.parse_args()
+
+ isolated_script_output = {'valid': False, 'failures': []}
+ if args.isolated_script_test_output:
+ test_name = 'lacros_resource_sizes'
+ args.output_dir = os.path.join(
+ os.path.dirname(args.isolated_script_test_output), test_name)
+ if not os.path.exists(args.output_dir):
+ os.makedirs(args.output_dir)
+
+ try:
+ _run_resource_sizes(args)
+ isolated_script_output = {'valid': True, 'failures': []}
+ finally:
+ if args.isolated_script_test_output:
+ results_path = os.path.join(args.output_dir, 'test_results.json')
+ with open(results_path, 'w') as output_file:
+ json.dump(isolated_script_output, output_file)
+ with open(args.isolated_script_test_output, 'w') as output_file:
+ json.dump(isolated_script_output, output_file)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/third_party/libwebrtc/build/lacros/lacros_resource_sizes.pydeps b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.pydeps
new file mode 100644
index 0000000000..9c0afd3570
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/lacros_resource_sizes.pydeps
@@ -0,0 +1,12 @@
+# Generated by running:
+# build/print_python_deps.py --root build/lacros --output build/lacros/lacros_resource_sizes.pydeps build/lacros/lacros_resource_sizes.py
+../../third_party/catapult/third_party/six/six.py
+../../third_party/catapult/third_party/vinn/vinn/__init__.py
+../../third_party/catapult/third_party/vinn/vinn/_vinn.py
+../../third_party/catapult/tracing/tracing/__init__.py
+../../third_party/catapult/tracing/tracing/value/__init__.py
+../../third_party/catapult/tracing/tracing/value/convert_chart_json.py
+../../third_party/catapult/tracing/tracing_project.py
+../util/lib/common/perf_result_data_type.py
+../util/lib/common/perf_tests_results_helper.py
+lacros_resource_sizes.py
diff --git a/third_party/libwebrtc/build/lacros/metadata.json.in b/third_party/libwebrtc/build/lacros/metadata.json.in
new file mode 100644
index 0000000000..3fceff256c
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/metadata.json.in
@@ -0,0 +1,6 @@
+{
+ "content": {
+ "version": "@MAJOR@.@MINOR@.@BUILD@.@PATCH@"
+ },
+ "metadata_version": 1
+}
diff --git a/third_party/libwebrtc/build/lacros/mojo_connection_lacros_launcher.py b/third_party/libwebrtc/build/lacros/mojo_connection_lacros_launcher.py
new file mode 100755
index 0000000000..c537183448
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/mojo_connection_lacros_launcher.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env vpython3
+#
+# Copyright 2020 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.
+"""Helps launch lacros-chrome with mojo connection established on Linux
+ or Chrome OS. Use on Chrome OS is for dev purposes.
+
+ The main use case is to be able to launch lacros-chrome in a debugger.
+
+ Please first launch an ash-chrome in the background as usual except without
+ the '--lacros-chrome-path' argument and with an additional
+ '--lacros-mojo-socket-for-testing' argument pointing to a socket path:
+
+ XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime ./out/ash/chrome \\
+ --user-data-dir=/tmp/ash-chrome --enable-wayland-server \\
+ --no-startup-window --enable-features=LacrosSupport \\
+ --lacros-mojo-socket-for-testing=/tmp/lacros.sock
+
+ Then, run this script with '-s' pointing to the same socket path used to
+ launch ash-chrome, followed by a command one would use to launch lacros-chrome
+ inside a debugger:
+
+ EGL_PLATFORM=surfaceless XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime \\
+ ./build/lacros/mojo_connection_lacros_launcher.py -s /tmp/lacros.sock
+ gdb --args ./out/lacros-release/chrome --user-data-dir=/tmp/lacros-chrome
+"""
+
+import argparse
+import array
+import contextlib
+import os
+import pathlib
+import socket
+import sys
+import subprocess
+
+
+_NUM_FDS_MAX = 3
+
+
+# contextlib.nullcontext is introduced in 3.7, while Python version on
+# CrOS is still 3.6. This is for backward compatibility.
+class NullContext:
+ def __init__(self, enter_ret=None):
+ self.enter_ret = enter_ret
+
+ def __enter__(self):
+ return self.enter_ret
+
+ def __exit__(self, exc_type, exc_value, trace):
+ pass
+
+
+def _ReceiveFDs(sock):
+ """Receives FDs from ash-chrome that will be used to launch lacros-chrome.
+
+ Args:
+ sock: A connected unix domain socket.
+
+ Returns:
+ File objects for the mojo connection and maybe startup data file.
+ """
+ # This function is borrowed from with modifications:
+ # https://docs.python.org/3/library/socket.html#socket.socket.recvmsg
+ fds = array.array("i") # Array of ints
+ # Along with the file descriptor, ash-chrome also sends the version in the
+ # regular data.
+ version, ancdata, _, _ = sock.recvmsg(
+ 1, socket.CMSG_LEN(fds.itemsize * _NUM_FDS_MAX))
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
+ # There are three versions currently this script supports.
+ # The oldest one: ash-chrome returns one FD, the mojo connection of
+ # old bootstrap procedure (i.e., it will be BrowserService).
+ # The middle one: ash-chrome returns two FDs, the mojo connection of
+ # old bootstrap procedure, and the second for the start up data FD.
+ # The newest one: ash-chrome returns three FDs, the mojo connection of
+ # old bootstrap procedure, the second for the start up data FD, and
+ # the third for another mojo connection of new bootstrap procedure.
+ # TODO(crbug.com/1156033): Clean up the code to drop the support of
+ # oldest one after M91.
+ # TODO(crbug.com/1180712): Clean up the mojo procedure support of the
+ # the middle one after M92.
+ cmsg_len_candidates = [(i + 1) * fds.itemsize
+ for i in range(_NUM_FDS_MAX)]
+ assert len(cmsg_data) in cmsg_len_candidates, (
+ 'CMSG_LEN is unexpected: %d' % (len(cmsg_data), ))
+ fds.frombytes(cmsg_data[:])
+
+ if version == b'\x00':
+ assert len(fds) in (1, 2, 3), 'Expecting exactly 1, 2, or 3 FDs'
+ legacy_mojo_fd = os.fdopen(fds[0])
+ startup_fd = None if len(fds) < 2 else os.fdopen(fds[1])
+ mojo_fd = None if len(fds) < 3 else os.fdopen(fds[2])
+ elif version == b'\x01':
+ assert len(fds) == 2, 'Expecting exactly 2 FDs'
+ legacy_mojo_fd = None
+ startup_fd = os.fdopen(fds[0])
+ mojo_fd = os.fdopen(fds[1])
+ else:
+ raise AssertionError('Unknown version: \\x%s' % version.encode('hex'))
+ return legacy_mojo_fd, startup_fd, mojo_fd
+
+
+def _MaybeClosing(fileobj):
+ """Returns closing context manager, if given fileobj is not None.
+
+ If the given fileobj is none, return nullcontext.
+ """
+ return (contextlib.closing if fileobj else NullContext)(fileobj)
+
+
+def Main():
+ arg_parser = argparse.ArgumentParser()
+ arg_parser.usage = __doc__
+ arg_parser.add_argument(
+ '-s',
+ '--socket-path',
+ type=pathlib.Path,
+ required=True,
+ help='Absolute path to the socket that were used to start ash-chrome, '
+ 'for example: "/tmp/lacros.socket"')
+ flags, args = arg_parser.parse_known_args()
+
+ assert 'XDG_RUNTIME_DIR' in os.environ
+ assert os.environ.get('EGL_PLATFORM') == 'surfaceless'
+
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
+ sock.connect(flags.socket_path.as_posix())
+ legacy_mojo_connection, startup_connection, mojo_connection = (
+ _ReceiveFDs(sock))
+
+ with _MaybeClosing(legacy_mojo_connection), \
+ _MaybeClosing(startup_connection), \
+ _MaybeClosing(mojo_connection):
+ cmd = args[:]
+ pass_fds = []
+ if legacy_mojo_connection:
+ cmd.append('--mojo-platform-channel-handle=%d' %
+ legacy_mojo_connection.fileno())
+ pass_fds.append(legacy_mojo_connection.fileno())
+ else:
+ # TODO(crbug.com/1188020): This is for backward compatibility.
+ # We should remove this after M93 lacros is spread enough.
+ cmd.append('--mojo-platform-channel-handle=-1')
+ if startup_connection:
+ cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno())
+ pass_fds.append(startup_connection.fileno())
+ if mojo_connection:
+ cmd.append('--crosapi-mojo-platform-channel-handle=%d' %
+ mojo_connection.fileno())
+ pass_fds.append(mojo_connection.fileno())
+ proc = subprocess.Popen(cmd, pass_fds=pass_fds)
+
+ return proc.wait()
+
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/third_party/libwebrtc/build/lacros/test_runner.py b/third_party/libwebrtc/build/lacros/test_runner.py
new file mode 100755
index 0000000000..397076a287
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/test_runner.py
@@ -0,0 +1,581 @@
+#!/usr/bin/env vpython3
+#
+# Copyright 2020 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.
+"""This script facilitates running tests for lacros on Linux.
+
+ In order to run lacros tests on Linux, please first follow bit.ly/3juQVNJ
+ to setup build directory with the lacros-chrome-on-linux build configuration,
+ and corresponding test targets are built successfully.
+
+ * Example usages:
+
+ ./build/lacros/test_runner.py test out/lacros/url_unittests
+ ./build/lacros/test_runner.py test out/lacros/browser_tests
+
+ The commands above run url_unittests and browser_tests respecitively, and more
+ specifically, url_unitests is executed directly while browser_tests is
+ executed with the latest version of prebuilt ash-chrome, and the behavior is
+ controlled by |_TARGETS_REQUIRE_ASH_CHROME|, and it's worth noting that the
+ list is maintained manually, so if you see something is wrong, please upload a
+ CL to fix it.
+
+ ./build/lacros/test_runner.py test out/lacros/browser_tests \\
+ --gtest_filter=BrowserTest.Title
+
+ The above command only runs 'BrowserTest.Title', and any argument accepted by
+ the underlying test binary can be specified in the command.
+
+ ./build/lacros/test_runner.py test out/lacros/browser_tests \\
+ --ash-chrome-version=793554
+
+ The above command runs tests with a given version of ash-chrome, which is
+ useful to reproduce test failures, the version corresponds to the commit
+ position of commits on the master branch, and a list of prebuilt versions can
+ be found at: gs://ash-chromium-on-linux-prebuilts/x86_64.
+
+ ./testing/xvfb.py ./build/lacros/test_runner.py test out/lacros/browser_tests
+
+ The above command starts ash-chrome with xvfb instead of an X11 window, and
+ it's useful when running tests without a display attached, such as sshing.
+
+ For version skew testing when passing --ash-chrome-path-override, the runner
+ will try to find the ash major version and Lacros major version. If ash is
+ newer(major version larger), the runner will not run any tests and just
+ returns success.
+"""
+
+import argparse
+import json
+import os
+import logging
+import re
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+import zipfile
+
+_SRC_ROOT = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
+sys.path.append(os.path.join(_SRC_ROOT, 'third_party', 'depot_tools'))
+
+# Base GS URL to store prebuilt ash-chrome.
+_GS_URL_BASE = 'gs://ash-chromium-on-linux-prebuilts/x86_64'
+
+# Latest file version.
+_GS_URL_LATEST_FILE = _GS_URL_BASE + '/latest/ash-chromium.txt'
+
+# GS path to the zipped ash-chrome build with any given version.
+_GS_ASH_CHROME_PATH = 'ash-chromium.zip'
+
+# Directory to cache downloaded ash-chrome versions to avoid re-downloading.
+_PREBUILT_ASH_CHROME_DIR = os.path.join(os.path.dirname(__file__),
+ 'prebuilt_ash_chrome')
+
+# Number of seconds to wait for ash-chrome to start.
+ASH_CHROME_TIMEOUT_SECONDS = (
+ 300 if os.environ.get('ASH_WRAPPER', None) else 10)
+
+# List of targets that require ash-chrome as a Wayland server in order to run.
+_TARGETS_REQUIRE_ASH_CHROME = [
+ 'app_shell_unittests',
+ 'aura_unittests',
+ 'browser_tests',
+ 'components_unittests',
+ 'compositor_unittests',
+ 'content_unittests',
+ 'dbus_unittests',
+ 'extensions_unittests',
+ 'media_unittests',
+ 'message_center_unittests',
+ 'snapshot_unittests',
+ 'sync_integration_tests',
+ 'unit_tests',
+ 'views_unittests',
+ 'wm_unittests',
+
+ # regex patterns.
+ '.*_browsertests',
+ '.*interactive_ui_tests'
+]
+
+# List of targets that require ash-chrome to support crosapi mojo APIs.
+_TARGETS_REQUIRE_MOJO_CROSAPI = [
+ # TODO(jamescook): Add 'browser_tests' after multiple crosapi connections
+ # are allowed. For now we only enable crosapi in targets that run tests
+ # serially.
+ 'interactive_ui_tests',
+ 'lacros_chrome_browsertests',
+ 'lacros_chrome_browsertests_run_in_series'
+]
+
+
+def _GetAshChromeDirPath(version):
+ """Returns a path to the dir storing the downloaded version of ash-chrome."""
+ return os.path.join(_PREBUILT_ASH_CHROME_DIR, version)
+
+
+def _remove_unused_ash_chrome_versions(version_to_skip):
+ """Removes unused ash-chrome versions to save disk space.
+
+ Currently, when an ash-chrome zip is downloaded and unpacked, the atime/mtime
+ of the dir and the files are NOW instead of the time when they were built, but
+ there is no garanteen it will always be the behavior in the future, so avoid
+ removing the current version just in case.
+
+ Args:
+ version_to_skip (str): the version to skip removing regardless of its age.
+ """
+ days = 7
+ expiration_duration = 60 * 60 * 24 * days
+
+ for f in os.listdir(_PREBUILT_ASH_CHROME_DIR):
+ if f == version_to_skip:
+ continue
+
+ p = os.path.join(_PREBUILT_ASH_CHROME_DIR, f)
+ if os.path.isfile(p):
+ # The prebuilt ash-chrome dir is NOT supposed to contain any files, remove
+ # them to keep the directory clean.
+ os.remove(p)
+ continue
+ chrome_path = os.path.join(p, 'test_ash_chrome')
+ if not os.path.exists(chrome_path):
+ chrome_path = p
+ age = time.time() - os.path.getatime(chrome_path)
+ if age > expiration_duration:
+ logging.info(
+ 'Removing ash-chrome: "%s" as it hasn\'t been used in the '
+ 'past %d days', p, days)
+ shutil.rmtree(p)
+
+def _GsutilCopyWithRetry(gs_path, local_name, retry_times=3):
+ """Gsutil copy with retry.
+
+ Args:
+ gs_path: The gs path for remote location.
+ local_name: The local file name.
+ retry_times: The total try times if the gsutil call fails.
+
+ Raises:
+ RuntimeError: If failed to download the specified version, for example,
+ if the version is not present on gcs.
+ """
+ import download_from_google_storage
+ gsutil = download_from_google_storage.Gsutil(
+ download_from_google_storage.GSUTIL_DEFAULT_PATH)
+ exit_code = 1
+ retry = 0
+ while exit_code and retry < retry_times:
+ retry += 1
+ exit_code = gsutil.call('cp', gs_path, local_name)
+ if exit_code:
+ raise RuntimeError('Failed to download: "%s"' % gs_path)
+
+
+def _DownloadAshChromeIfNecessary(version):
+ """Download a given version of ash-chrome if not already exists.
+
+ Args:
+ version: A string representing the version, such as "793554".
+
+ Raises:
+ RuntimeError: If failed to download the specified version, for example,
+ if the version is not present on gcs.
+ """
+
+ def IsAshChromeDirValid(ash_chrome_dir):
+ # This function assumes that once 'chrome' is present, other dependencies
+ # will be present as well, it's not always true, for example, if the test
+ # runner process gets killed in the middle of unzipping (~2 seconds), but
+ # it's unlikely for the assumption to break in practice.
+ return os.path.isdir(ash_chrome_dir) and os.path.isfile(
+ os.path.join(ash_chrome_dir, 'test_ash_chrome'))
+
+ ash_chrome_dir = _GetAshChromeDirPath(version)
+ if IsAshChromeDirValid(ash_chrome_dir):
+ return
+
+ shutil.rmtree(ash_chrome_dir, ignore_errors=True)
+ os.makedirs(ash_chrome_dir)
+ with tempfile.NamedTemporaryFile() as tmp:
+ logging.info('Ash-chrome version: %s', version)
+ gs_path = _GS_URL_BASE + '/' + version + '/' + _GS_ASH_CHROME_PATH
+ _GsutilCopyWithRetry(gs_path, tmp.name)
+
+ # https://bugs.python.org/issue15795. ZipFile doesn't preserve permissions.
+ # And in order to workaround the issue, this function is created and used
+ # instead of ZipFile.extractall().
+ # The solution is copied from:
+ # https://stackoverflow.com/questions/42326428/zipfile-in-python-file-permission
+ def ExtractFile(zf, info, extract_dir):
+ zf.extract(info.filename, path=extract_dir)
+ perm = info.external_attr >> 16
+ os.chmod(os.path.join(extract_dir, info.filename), perm)
+
+ with zipfile.ZipFile(tmp.name, 'r') as zf:
+ # Extra all files instead of just 'chrome' binary because 'chrome' needs
+ # other resources and libraries to run.
+ for info in zf.infolist():
+ ExtractFile(zf, info, ash_chrome_dir)
+
+ _remove_unused_ash_chrome_versions(version)
+
+
+def _GetLatestVersionOfAshChrome():
+ """Returns the latest version of uploaded ash-chrome."""
+ with tempfile.NamedTemporaryFile() as tmp:
+ _GsutilCopyWithRetry(_GS_URL_LATEST_FILE, tmp.name)
+ with open(tmp.name, 'r') as f:
+ return f.read().strip()
+
+
+def _WaitForAshChromeToStart(tmp_xdg_dir, lacros_mojo_socket_file,
+ enable_mojo_crosapi):
+ """Waits for Ash-Chrome to be up and running and returns a boolean indicator.
+
+ Determine whether ash-chrome is up and running by checking whether two files
+ (lock file + socket) have been created in the |XDG_RUNTIME_DIR| and the lacros
+ mojo socket file has been created if enabling the mojo "crosapi" interface.
+ TODO(crbug.com/1107966): Figure out a more reliable hook to determine the
+ status of ash-chrome, likely through mojo connection.
+
+ Args:
+ tmp_xdg_dir (str): Path to the XDG_RUNTIME_DIR.
+ lacros_mojo_socket_file (str): Path to the lacros mojo socket file.
+ enable_mojo_crosapi (bool): Whether to bootstrap the crosapi mojo interface
+ between ash and the lacros test binary.
+
+ Returns:
+ A boolean indicating whether Ash-chrome is up and running.
+ """
+
+ def IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file,
+ enable_mojo_crosapi):
+ return (len(os.listdir(tmp_xdg_dir)) >= 2
+ and (not enable_mojo_crosapi
+ or os.path.exists(lacros_mojo_socket_file)))
+
+ time_counter = 0
+ while not IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file,
+ enable_mojo_crosapi):
+ time.sleep(0.5)
+ time_counter += 0.5
+ if time_counter > ASH_CHROME_TIMEOUT_SECONDS:
+ break
+
+ return IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file,
+ enable_mojo_crosapi)
+
+
+def _ExtractAshMajorVersion(file_path):
+ """Extract major version from file_path.
+
+ File path like this:
+ ../../lacros_version_skew_tests_v94.0.4588.0/test_ash_chrome
+
+ Returns:
+ int representing the major version. Or 0 if it can't extract
+ major version.
+ """
+ m = re.search(
+ 'lacros_version_skew_tests_v(?P<version>[0-9]+).[0-9]+.[0-9]+.[0-9]+/',
+ file_path)
+ if (m and 'version' in m.groupdict().keys()):
+ return int(m.group('version'))
+ logging.warning('Can not find the ash version in %s.' % file_path)
+ # Returns ash major version as 0, so we can still run tests.
+ # This is likely happen because user is running in local environments.
+ return 0
+
+
+def _FindLacrosMajorVersionFromMetadata():
+ # This handles the logic on bots. When running on bots,
+ # we don't copy source files to test machines. So we build a
+ # metadata.json file which contains version information.
+ if not os.path.exists('metadata.json'):
+ logging.error('Can not determine current version.')
+ # Returns 0 so it can't run any tests.
+ return 0
+ version = ''
+ with open('metadata.json', 'r') as file:
+ content = json.load(file)
+ version = content['content']['version']
+ return int(version[:version.find('.')])
+
+
+def _FindLacrosMajorVersion():
+ """Returns the major version in the current checkout.
+
+ It would try to read src/chrome/VERSION. If it's not available,
+ then try to read metadata.json.
+
+ Returns:
+ int representing the major version. Or 0 if it fails to
+ determine the version.
+ """
+ version_file = os.path.abspath(
+ os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ '../../chrome/VERSION'))
+ # This is mostly happens for local development where
+ # src/chrome/VERSION exists.
+ if os.path.exists(version_file):
+ lines = open(version_file, 'r').readlines()
+ return int(lines[0][lines[0].find('=') + 1:-1])
+ return _FindLacrosMajorVersionFromMetadata()
+
+
+def _ParseSummaryOutput(forward_args):
+ """Find the summary output file path.
+
+ Args:
+ forward_args (list): Args to be forwarded to the test command.
+
+ Returns:
+ None if not found, or str representing the output file path.
+ """
+ logging.warning(forward_args)
+ for arg in forward_args:
+ if arg.startswith('--test-launcher-summary-output='):
+ return arg[len('--test-launcher-summary-output='):]
+ return None
+
+
+def _RunTestWithAshChrome(args, forward_args):
+ """Runs tests with ash-chrome.
+
+ Args:
+ args (dict): Args for this script.
+ forward_args (list): Args to be forwarded to the test command.
+ """
+ if args.ash_chrome_path_override:
+ ash_chrome_file = args.ash_chrome_path_override
+ ash_major_version = _ExtractAshMajorVersion(ash_chrome_file)
+ lacros_major_version = _FindLacrosMajorVersion()
+ if ash_major_version > lacros_major_version:
+ logging.warning('''Not running any tests, because we do not \
+support version skew testing for Lacros M%s against ash M%s''' %
+ (lacros_major_version, ash_major_version))
+ # Create an empty output.json file so result adapter can read
+ # the file. Or else result adapter will report no file found
+ # and result infra failure.
+ output_json = _ParseSummaryOutput(forward_args)
+ if output_json:
+ with open(output_json, 'w') as f:
+ f.write("""{"all_tests":[],"disabled_tests":[],"global_tags":[],
+"per_iteration_data":[],"test_locations":{}}""")
+ # Although we don't run any tests, this is considered as success.
+ return 0
+ if not os.path.exists(ash_chrome_file):
+ logging.error("""Can not find ash chrome at %s. Did you download \
+the ash from CIPD? If you don't plan to build your own ash, you need \
+to download first. Example commandlines:
+ $ cipd auth-login
+ $ echo "chromium/testing/linux-ash-chromium/x86_64/ash.zip \
+version:92.0.4515.130" > /tmp/ensure-file.txt
+ $ cipd ensure -ensure-file /tmp/ensure-file.txt \
+-root lacros_version_skew_tests_v92.0.4515.130
+ Then you can use --ash-chrome-path-override=\
+lacros_version_skew_tests_v92.0.4515.130/test_ash_chrome
+""" % ash_chrome_file)
+ return 1
+ elif args.ash_chrome_path:
+ ash_chrome_file = args.ash_chrome_path
+ else:
+ ash_chrome_version = (args.ash_chrome_version
+ or _GetLatestVersionOfAshChrome())
+ _DownloadAshChromeIfNecessary(ash_chrome_version)
+ logging.info('Ash-chrome version: %s', ash_chrome_version)
+
+ ash_chrome_file = os.path.join(_GetAshChromeDirPath(ash_chrome_version),
+ 'test_ash_chrome')
+ try:
+ # Starts Ash-Chrome.
+ tmp_xdg_dir_name = tempfile.mkdtemp()
+ tmp_ash_data_dir_name = tempfile.mkdtemp()
+
+ # Please refer to below file for how mojo connection is set up in testing.
+ # //chrome/browser/ash/crosapi/test_mojo_connection_manager.h
+ lacros_mojo_socket_file = '%s/lacros.sock' % tmp_ash_data_dir_name
+ lacros_mojo_socket_arg = ('--lacros-mojo-socket-for-testing=%s' %
+ lacros_mojo_socket_file)
+ enable_mojo_crosapi = any(t == os.path.basename(args.command)
+ for t in _TARGETS_REQUIRE_MOJO_CROSAPI)
+
+ ash_process = None
+ ash_env = os.environ.copy()
+ ash_env['XDG_RUNTIME_DIR'] = tmp_xdg_dir_name
+ ash_cmd = [
+ ash_chrome_file,
+ '--user-data-dir=%s' % tmp_ash_data_dir_name,
+ '--enable-wayland-server',
+ '--no-startup-window',
+ ]
+ if enable_mojo_crosapi:
+ ash_cmd.append(lacros_mojo_socket_arg)
+
+ # Users can specify a wrapper for the ash binary to do things like
+ # attaching debuggers. For example, this will open a new terminal window
+ # and run GDB.
+ # $ export ASH_WRAPPER="gnome-terminal -- gdb --ex=r --args"
+ ash_wrapper = os.environ.get('ASH_WRAPPER', None)
+ if ash_wrapper:
+ logging.info('Running ash with "ASH_WRAPPER": %s', ash_wrapper)
+ ash_cmd = list(ash_wrapper.split()) + ash_cmd
+
+ ash_process_has_started = False
+ total_tries = 3
+ num_tries = 0
+ while not ash_process_has_started and num_tries < total_tries:
+ num_tries += 1
+ ash_process = subprocess.Popen(ash_cmd, env=ash_env)
+ ash_process_has_started = _WaitForAshChromeToStart(
+ tmp_xdg_dir_name, lacros_mojo_socket_file, enable_mojo_crosapi)
+ if ash_process_has_started:
+ break
+
+ logging.warning('Starting ash-chrome timed out after %ds',
+ ASH_CHROME_TIMEOUT_SECONDS)
+ logging.warning('Printing the output of "ps aux" for debugging:')
+ subprocess.call(['ps', 'aux'])
+ if ash_process and ash_process.poll() is None:
+ ash_process.kill()
+
+ if not ash_process_has_started:
+ raise RuntimeError('Timed out waiting for ash-chrome to start')
+
+ # Starts tests.
+ if enable_mojo_crosapi:
+ forward_args.append(lacros_mojo_socket_arg)
+
+ test_env = os.environ.copy()
+ test_env['EGL_PLATFORM'] = 'surfaceless'
+ test_env['XDG_RUNTIME_DIR'] = tmp_xdg_dir_name
+ test_process = subprocess.Popen([args.command] + forward_args, env=test_env)
+ return test_process.wait()
+
+ finally:
+ if ash_process and ash_process.poll() is None:
+ ash_process.terminate()
+ # Allow process to do cleanup and exit gracefully before killing.
+ time.sleep(0.5)
+ ash_process.kill()
+
+ shutil.rmtree(tmp_xdg_dir_name, ignore_errors=True)
+ shutil.rmtree(tmp_ash_data_dir_name, ignore_errors=True)
+
+
+def _RunTestDirectly(args, forward_args):
+ """Runs tests by invoking the test command directly.
+
+ args (dict): Args for this script.
+ forward_args (list): Args to be forwarded to the test command.
+ """
+ try:
+ p = None
+ p = subprocess.Popen([args.command] + forward_args)
+ return p.wait()
+ finally:
+ if p and p.poll() is None:
+ p.terminate()
+ time.sleep(0.5)
+ p.kill()
+
+
+def _HandleSignal(sig, _):
+ """Handles received signals to make sure spawned test process are killed.
+
+ sig (int): An integer representing the received signal, for example SIGTERM.
+ """
+ logging.warning('Received signal: %d, killing spawned processes', sig)
+
+ # Don't do any cleanup here, instead, leave it to the finally blocks.
+ # Assumption is based on https://docs.python.org/3/library/sys.html#sys.exit:
+ # cleanup actions specified by finally clauses of try statements are honored.
+
+ # https://tldp.org/LDP/abs/html/exitcodes.html:
+ # Exit code 128+n -> Fatal error signal "n".
+ sys.exit(128 + sig)
+
+
+def _RunTest(args, forward_args):
+ """Runs tests with given args.
+
+ args (dict): Args for this script.
+ forward_args (list): Args to be forwarded to the test command.
+
+ Raises:
+ RuntimeError: If the given test binary doesn't exist or the test runner
+ doesn't know how to run it.
+ """
+
+ if not os.path.isfile(args.command):
+ raise RuntimeError('Specified test command: "%s" doesn\'t exist' %
+ args.command)
+
+ # |_TARGETS_REQUIRE_ASH_CHROME| may not always be accurate as it is updated
+ # with a best effort only, therefore, allow the invoker to override the
+ # behavior with a specified ash-chrome version, which makes sure that
+ # automated CI/CQ builders would always work correctly.
+ requires_ash_chrome = any(
+ re.match(t, os.path.basename(args.command))
+ for t in _TARGETS_REQUIRE_ASH_CHROME)
+ if not requires_ash_chrome and not args.ash_chrome_version:
+ return _RunTestDirectly(args, forward_args)
+
+ return _RunTestWithAshChrome(args, forward_args)
+
+
+def Main():
+ for sig in (signal.SIGTERM, signal.SIGINT):
+ signal.signal(sig, _HandleSignal)
+
+ logging.basicConfig(level=logging.INFO)
+ arg_parser = argparse.ArgumentParser()
+ arg_parser.usage = __doc__
+
+ subparsers = arg_parser.add_subparsers()
+
+ test_parser = subparsers.add_parser('test', help='Run tests')
+ test_parser.set_defaults(func=_RunTest)
+
+ test_parser.add_argument(
+ 'command',
+ help='A single command to invoke the tests, for example: '
+ '"./url_unittests". Any argument unknown to this test runner script will '
+ 'be forwarded to the command, for example: "--gtest_filter=Suite.Test"')
+
+ version_group = test_parser.add_mutually_exclusive_group()
+ version_group.add_argument(
+ '--ash-chrome-version',
+ type=str,
+ help='Version of an prebuilt ash-chrome to use for testing, for example: '
+ '"793554", and the version corresponds to the commit position of commits '
+ 'on the main branch. If not specified, will use the latest version '
+ 'available')
+ version_group.add_argument(
+ '--ash-chrome-path',
+ type=str,
+ help='Path to an locally built ash-chrome to use for testing. '
+ 'In general you should build //chrome/test:test_ash_chrome.')
+
+ # This is for version skew testing. The current CI/CQ builder builds
+ # an ash chrome and pass it using --ash-chrome-path. In order to use the same
+ # builder for version skew testing, we use a new argument to override
+ # the ash chrome.
+ test_parser.add_argument(
+ '--ash-chrome-path-override',
+ type=str,
+ help='The same as --ash-chrome-path. But this will override '
+ '--ash-chrome-path or --ash-chrome-version if any of these '
+ 'arguments exist.')
+ args = arg_parser.parse_known_args()
+ return args[0].func(args[0], args[1])
+
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/third_party/libwebrtc/build/lacros/test_runner_test.py b/third_party/libwebrtc/build/lacros/test_runner_test.py
new file mode 100755
index 0000000000..9713dfc387
--- /dev/null
+++ b/third_party/libwebrtc/build/lacros/test_runner_test.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env vpython3
+# Copyright 2020 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.
+
+import logging
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+
+import mock
+from parameterized import parameterized
+
+import test_runner
+
+
+class TestRunnerTest(unittest.TestCase):
+ def setUp(self):
+ logging.disable(logging.CRITICAL)
+ time.sleep = mock.Mock()
+
+ def tearDown(self):
+ logging.disable(logging.NOTSET)
+
+ @parameterized.expand([
+ 'url_unittests',
+ './url_unittests',
+ 'out/release/url_unittests',
+ './out/release/url_unittests',
+ ])
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that the test runner doesn't attempt to download ash-chrome if not
+ # required.
+ def test_do_not_require_ash_chrome(self, command, mock_popen, mock_download,
+ _):
+ args = ['script_name', 'test', command]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ self.assertEqual(1, mock_popen.call_count)
+ mock_popen.assert_called_with([command])
+ self.assertFalse(mock_download.called)
+
+ @parameterized.expand([
+ 'browser_tests',
+ 'components_browsertests',
+ 'content_browsertests',
+ 'lacros_chrome_browsertests',
+ ])
+ @mock.patch.object(os,
+ 'listdir',
+ return_value=['wayland-0', 'wayland-0.lock'])
+ @mock.patch.object(tempfile,
+ 'mkdtemp',
+ side_effect=['/tmp/xdg', '/tmp/ash-data'])
+ @mock.patch.object(os.environ, 'copy', side_effect=[{}, {}])
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner,
+ '_GetLatestVersionOfAshChrome',
+ return_value='793554')
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that the test runner downloads and spawns ash-chrome if ash-chrome is
+ # required.
+ def test_require_ash_chrome(self, command, mock_popen, mock_download, *_):
+ args = ['script_name', 'test', command]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ mock_download.assert_called_with('793554')
+ self.assertEqual(2, mock_popen.call_count)
+
+ ash_chrome_args = mock_popen.call_args_list[0][0][0]
+ self.assertTrue(ash_chrome_args[0].endswith(
+ 'build/lacros/prebuilt_ash_chrome/793554/test_ash_chrome'))
+ expected_ash_chrome_args = [
+ '--user-data-dir=/tmp/ash-data',
+ '--enable-wayland-server',
+ '--no-startup-window',
+ ]
+ if command == 'lacros_chrome_browsertests':
+ expected_ash_chrome_args.append(
+ '--lacros-mojo-socket-for-testing=/tmp/ash-data/lacros.sock')
+ self.assertListEqual(expected_ash_chrome_args, ash_chrome_args[1:])
+ ash_chrome_env = mock_popen.call_args_list[0][1].get('env', {})
+ self.assertDictEqual({'XDG_RUNTIME_DIR': '/tmp/xdg'}, ash_chrome_env)
+
+ test_args = mock_popen.call_args_list[1][0][0]
+ if command == 'lacros_chrome_browsertests':
+ self.assertListEqual([
+ command,
+ '--lacros-mojo-socket-for-testing=/tmp/ash-data/lacros.sock'
+ ], test_args)
+ else:
+ self.assertListEqual([command], test_args)
+
+ test_env = mock_popen.call_args_list[1][1].get('env', {})
+ self.assertDictEqual(
+ {
+ 'XDG_RUNTIME_DIR': '/tmp/xdg',
+ 'EGL_PLATFORM': 'surfaceless'
+ }, test_env)
+
+
+ @mock.patch.object(os,
+ 'listdir',
+ return_value=['wayland-0', 'wayland-0.lock'])
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner,
+ '_GetLatestVersionOfAshChrome',
+ return_value='793554')
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that when a ash-chrome version is specified, that version is used
+ # instead of the latest one.
+ def test_specify_ash_chrome_version(self, mock_popen, mock_download, *_):
+ args = [
+ 'script_name', 'test', 'browser_tests', '--ash-chrome-version', '781122'
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ mock_download.assert_called_with('781122')
+
+ @mock.patch.object(os,
+ 'listdir',
+ return_value=['wayland-0', 'wayland-0.lock'])
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that if a ash-chrome version is specified, uses ash-chrome to run
+ # tests anyway even if |_TARGETS_REQUIRE_ASH_CHROME| indicates an ash-chrome
+ # is not required.
+ def test_overrides_do_not_require_ash_chrome(self, mock_popen, mock_download,
+ *_):
+ args = [
+ 'script_name', 'test', './url_unittests', '--ash-chrome-version',
+ '793554'
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ mock_download.assert_called_with('793554')
+ self.assertEqual(2, mock_popen.call_count)
+
+ @mock.patch.object(os,
+ 'listdir',
+ return_value=['wayland-0', 'wayland-0.lock'])
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner, '_GetLatestVersionOfAshChrome')
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that when an ash-chrome path is specified, the test runner doesn't try
+ # to download prebuilt ash-chrome.
+ def test_specify_ash_chrome_path(self, mock_popen, mock_download,
+ mock_get_latest_version, *_):
+ args = [
+ 'script_name',
+ 'test',
+ 'browser_tests',
+ '--ash-chrome-path',
+ '/ash/test_ash_chrome',
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ self.assertFalse(mock_get_latest_version.called)
+ self.assertFalse(mock_download.called)
+
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that arguments not known to the test runner are forwarded to the
+ # command that invokes tests.
+ def test_command_arguments(self, mock_popen, mock_download, _):
+ args = [
+ 'script_name', 'test', './url_unittests', '--gtest_filter=Suite.Test'
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ mock_popen.assert_called_with(
+ ['./url_unittests', '--gtest_filter=Suite.Test'])
+ self.assertFalse(mock_download.called)
+
+ @mock.patch.dict(os.environ, {'ASH_WRAPPER': 'gdb --args'}, clear=False)
+ @mock.patch.object(os,
+ 'listdir',
+ return_value=['wayland-0', 'wayland-0.lock'])
+ @mock.patch.object(tempfile,
+ 'mkdtemp',
+ side_effect=['/tmp/xdg', '/tmp/ash-data'])
+ @mock.patch.object(os.environ, 'copy', side_effect=[{}, {}])
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner,
+ '_GetLatestVersionOfAshChrome',
+ return_value='793554')
+ @mock.patch.object(test_runner, '_DownloadAshChromeIfNecessary')
+ @mock.patch.object(subprocess, 'Popen', return_value=mock.Mock())
+ # Tests that, when the ASH_WRAPPER environment variable is set, it forwards
+ # the commands to the invocation of ash.
+ def test_ash_wrapper(self, mock_popen, *_):
+ args = [
+ 'script_name', 'test', './browser_tests', '--gtest_filter=Suite.Test'
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ test_runner.Main()
+ ash_args = mock_popen.call_args_list[0][0][0]
+ self.assertTrue(ash_args[2].endswith('test_ash_chrome'))
+ self.assertEqual(['gdb', '--args'], ash_args[:2])
+
+
+ # Test when ash is newer, test runner skips running tests and returns 0.
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ @mock.patch.object(os.path, 'isfile', return_value=True)
+ @mock.patch.object(test_runner, '_FindLacrosMajorVersion', return_value=91)
+ def test_version_skew_ash_newer(self, *_):
+ args = [
+ 'script_name', 'test', './browser_tests', '--gtest_filter=Suite.Test',
+ '--ash-chrome-path-override=\
+lacros_version_skew_tests_v92.0.100.0/test_ash_chrome'
+ ]
+ with mock.patch.object(sys, 'argv', args):
+ self.assertEqual(test_runner.Main(), 0)
+
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ def test_lacros_version_from_chrome_version(self, *_):
+ version_data = '''\
+MAJOR=95
+MINOR=0
+BUILD=4615
+PATCH=0\
+'''
+ open_lib = '__builtin__.open'
+ if sys.version_info[0] >= 3:
+ open_lib = 'builtins.open'
+ with mock.patch(open_lib,
+ mock.mock_open(read_data=version_data)) as mock_file:
+ version = test_runner._FindLacrosMajorVersion()
+ self.assertEqual(95, version)
+
+ @mock.patch.object(os.path, 'exists', return_value=True)
+ def test_lacros_version_from_metadata(self, *_):
+ metadata_json = '''
+{
+ "content": {
+ "version": "92.1.4389.2"
+ },
+ "metadata_version": 1
+}
+ '''
+ open_lib = '__builtin__.open'
+ if sys.version_info[0] >= 3:
+ open_lib = 'builtins.open'
+ with mock.patch(open_lib,
+ mock.mock_open(read_data=metadata_json)) as mock_file:
+ version = test_runner._FindLacrosMajorVersionFromMetadata()
+ self.assertEqual(92, version)
+ mock_file.assert_called_with('metadata.json', 'r')
+
+
+if __name__ == '__main__':
+ unittest.main()