diff options
Diffstat (limited to '')
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() |