| #!/usr/bin/env python3 |
| # |
| # Copyright 2020 The Chromium Authors |
| # 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. |
| |
| Interactively debugging tests |
| |
| Any of the previous examples accept the switches |
| --gdb |
| --lldb |
| to run the tests in the corresponding debugger. |
| """ |
| |
| 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')) |
| |
| |
| # The cipd path for prebuilt ash chrome. |
| _ASH_CIPD_PATH = 'chromium/testing/linux-ash-chromium/x86_64/ash.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') |
| |
| # File path to the asan symbolizer executable. |
| _ASAN_SYMBOLIZER_PATH = os.path.join(_SRC_ROOT, 'tools', 'valgrind', 'asan', |
| 'asan_symbolize.py') |
| |
| # 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' |
| ] |
| |
| # Default test filter file for each target. These filter files will be |
| # used by default if no other filter file get specified. |
| _DEFAULT_FILTER_FILES_MAPPING = { |
| 'browser_tests': 'linux-lacros.browser_tests.filter', |
| 'components_unittests': 'linux-lacros.components_unittests.filter', |
| 'content_browsertests': 'linux-lacros.content_browsertests.filter', |
| 'interactive_ui_tests': 'linux-lacros.interactive_ui_tests.filter', |
| 'lacros_chrome_browsertests': |
| 'linux-lacros.lacros_chrome_browsertests.filter', |
| 'sync_integration_tests': 'linux-lacros.sync_integration_tests.filter', |
| 'unit_tests': 'linux-lacros.unit_tests.filter', |
| } |
| |
| |
| 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 _GetLatestVersionOfAshChrome(): |
| '''Get the latest ash chrome version. |
| |
| Get the package version info with canary ref. |
| |
| Returns: |
| A string with the chrome version. |
| |
| Raises: |
| RuntimeError: if we can not get the version. |
| ''' |
| cp = subprocess.run( |
| ['cipd', 'describe', _ASH_CIPD_PATH, '-version', 'canary'], |
| capture_output=True) |
| assert (cp.returncode == 0) |
| groups = re.search(r'version:(?P<version>[\d\.]+)', str(cp.stdout)) |
| if not groups: |
| raise RuntimeError('Can not find the version. Error message: %s' % |
| cp.stdout) |
| return groups.group('version') |
| |
| |
| def _DownloadAshChromeFromCipd(path, version): |
| '''Download the ash chrome with the requested version. |
| |
| Args: |
| path: string for the downloaded ash chrome folder. |
| version: string for the ash chrome version. |
| |
| Returns: |
| A string representing the path for the downloaded ash chrome. |
| ''' |
| with tempfile.TemporaryDirectory() as temp_dir: |
| ensure_file_path = os.path.join(temp_dir, 'ensure_file.txt') |
| f = open(ensure_file_path, 'w+') |
| f.write(_ASH_CIPD_PATH + ' version:' + version) |
| f.close() |
| subprocess.run( |
| ['cipd', 'ensure', '-ensure-file', ensure_file_path, '-root', path]) |
| |
| |
| def _DoubleCheckDownloadedAshChrome(path, version): |
| '''Check the downloaded ash is the expected version. |
| |
| Double check by running the chrome binary with --version. |
| |
| Args: |
| path: string for the downloaded ash chrome folder. |
| version: string for the expected ash chrome version. |
| |
| Raises: |
| RuntimeError if no test_ash_chrome binary can be found. |
| ''' |
| test_ash_chrome = os.path.join(path, 'test_ash_chrome') |
| if not os.path.exists(test_ash_chrome): |
| raise RuntimeError('Can not find test_ash_chrome binary under %s' % path) |
| cp = subprocess.run([test_ash_chrome, '--version'], capture_output=True) |
| assert (cp.returncode == 0) |
| if str(cp.stdout).find(version) == -1: |
| logging.warning( |
| 'The downloaded ash chrome version is %s, but the ' |
| 'expected ash chrome is %s. There is a version mismatch. Please ' |
| 'file a bug to OS>Lacros so someone can take a look.' % |
| (cp.stdout, version)) |
| |
| |
| 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) |
| _DownloadAshChromeFromCipd(ash_chrome_dir, version) |
| _DoubleCheckDownloadedAshChrome(ash_chrome_dir, version) |
| _remove_unused_ash_chrome_versions(version) |
| |
| |
| def _WaitForAshChromeToStart(tmp_xdg_dir, lacros_mojo_socket_file, |
| enable_mojo_crosapi, ash_ready_file): |
| """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. |
| ash_ready_file (str): Path to a non-existing file. After ash is ready for |
| testing, the file will be created. |
| |
| Returns: |
| A boolean indicating whether Ash-chrome is up and running. |
| """ |
| |
| def IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file, |
| enable_mojo_crosapi, ash_ready_file): |
| # There should be 2 wayland files. |
| if len(os.listdir(tmp_xdg_dir)) < 2: |
| return False |
| if enable_mojo_crosapi and not os.path.exists(lacros_mojo_socket_file): |
| return False |
| return os.path.exists(ash_ready_file) |
| |
| time_counter = 0 |
| while not IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file, |
| enable_mojo_crosapi, ash_ready_file): |
| 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, ash_ready_file) |
| |
| |
| 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 _IsRunningOnBots(forward_args): |
| """Detects if the script is running on bots or not. |
| |
| Args: |
| forward_args (list): Args to be forwarded to the test command. |
| |
| Returns: |
| True if the script is running on bots. Otherwise returns False. |
| """ |
| return '--test-launcher-bot-mode' in forward_args |
| |
| |
| def _KillNicely(proc, timeout_secs=2, first_wait_secs=0): |
| """Kills a subprocess nicely. |
| |
| Args: |
| proc: The subprocess to kill. |
| timeout_secs: The timeout to wait in seconds. |
| first_wait_secs: The grace period before sending first SIGTERM in seconds. |
| """ |
| if not proc: |
| return |
| |
| if first_wait_secs: |
| try: |
| proc.wait(first_wait_secs) |
| return |
| except subprocess.TimeoutExpired: |
| pass |
| |
| if proc.poll() is None: |
| proc.terminate() |
| try: |
| proc.wait(timeout_secs) |
| except subprocess.TimeoutExpired: |
| proc.kill() |
| proc.wait() |
| |
| |
| def _ClearDir(dirpath): |
| """Deletes everything within the directory. |
| |
| Args: |
| dirpath: The path of the directory. |
| """ |
| for e in os.scandir(dirpath): |
| if e.is_dir(): |
| shutil.rmtree(e.path) |
| elif e.is_file(): |
| os.remove(e.path) |
| |
| |
| def _LaunchDebugger(args, forward_args, test_env): |
| """Launches the requested debugger. |
| |
| This is used to wrap the test invocation in a debugger. It returns the |
| created Popen class of the debugger process. |
| |
| Args: |
| args (dict): Args for this script. |
| forward_args (list): Args to be forwarded to the test command. |
| test_env (dict): Computed environment variables for the test. |
| """ |
| logging.info('Starting debugger.') |
| |
| # Force the tests into single-process-test mode for debugging unless manually |
| # specified. Otherwise the tests will run in a child process that the debugger |
| # won't be attached to and the debugger won't do anything. |
| if not ("--single-process" in forward_args |
| or "--single-process-tests" in forward_args): |
| forward_args += ["--single-process-tests"] |
| |
| # Adding --single-process-tests can cause some tests to fail when they're |
| # run in the same process. Forcing the user to specify a filter will prevent |
| # a later error. |
| if not [i for i in forward_args if i.startswith("--gtest_filter")]: |
| logging.error("""Interactive debugging requested without --gtest_filter |
| |
| This script adds --single-process-tests to support interactive debugging but |
| some tests will fail in this mode unless run independently. To debug a test |
| specify a --gtest_filter=Foo.Bar to name the test you want to debug. |
| """) |
| sys.exit(1) |
| |
| # This code attempts to source the debugger configuration file. Some |
| # users will have this in their init but sourcing it more than once is |
| # harmless and helps people that haven't configured it. |
| if args.gdb: |
| gdbinit_file = os.path.normpath( |
| os.path.join(os.path.realpath(__file__), "../../../tools/gdb/gdbinit")) |
| debugger_command = [ |
| 'gdb', '--init-eval-command', 'source ' + gdbinit_file, '--args' |
| ] |
| else: |
| lldbinit_dir = os.path.normpath( |
| os.path.join(os.path.realpath(__file__), "../../../tools/lldb")) |
| debugger_command = [ |
| 'lldb', '-O', |
| "script sys.path[:0] = ['%s']" % lldbinit_dir, '-O', |
| 'script import lldbinit', '--' |
| ] |
| debugger_command += [args.command] + forward_args |
| return subprocess.Popen(debugger_command, env=test_env) |
| |
| |
| 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) |
| ash_ready_file = '%s/ash_ready.txt' % tmp_ash_data_dir_name |
| enable_mojo_crosapi = any(t == os.path.basename(args.command) |
| for t in _TARGETS_REQUIRE_MOJO_CROSAPI) |
| ash_wayland_socket_name = 'wayland-exo' |
| |
| 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', |
| '--disable-input-event-activation-protection', |
| '--disable-lacros-keep-alive', |
| '--disable-login-lacros-opening', |
| '--enable-field-trial-config', |
| '--enable-features=LacrosSupport,LacrosPrimary,LacrosOnly', |
| '--ash-ready-file-path=%s' % ash_ready_file, |
| '--wayland-server-socket=%s' % ash_wayland_socket_name, |
| ] |
| if '--enable-pixel-output-in-tests' not in forward_args: |
| ash_cmd.append('--disable-gl-drawing-for-tests') |
| |
| 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 |
| ash_start_time = None |
| |
| # Create a log file if the user wanted to have one. |
| ash_log = None |
| ash_log_path = None |
| |
| run_tests_in_debugger = args.gdb or args.lldb |
| |
| if args.ash_logging_path: |
| ash_log_path = args.ash_logging_path |
| # Put ash logs in a separate file on bots. |
| # For asan builds, the ash log is not symbolized. In order to |
| # read the stack strace, we don't redirect logs to another file. |
| elif _IsRunningOnBots(forward_args) and not args.combine_ash_logs_on_bots: |
| summary_file = _ParseSummaryOutput(forward_args) |
| if summary_file: |
| ash_log_path = os.path.join(os.path.dirname(summary_file), |
| 'ash_chrome.log') |
| elif run_tests_in_debugger: |
| # The debugger is unusable when all Ash logs are getting dumped to the |
| # same terminal. Redirect to a log file if there isn't one specified. |
| logging.info("Running in the debugger and --ash-logging-path is not " + |
| "specified, defaulting to the current directory.") |
| ash_log_path = 'ash_chrome.log' |
| |
| if ash_log_path: |
| ash_log = open(ash_log_path, 'a') |
| logging.info('Writing ash-chrome logs to: %s', ash_log_path) |
| |
| ash_stdout = ash_log or None |
| test_stdout = None |
| |
| # Setup asan symbolizer. |
| ash_symbolize_process = None |
| test_symbolize_process = None |
| should_symbolize = False |
| if args.asan_symbolize_output and os.path.exists(_ASAN_SYMBOLIZER_PATH): |
| should_symbolize = True |
| ash_symbolize_stdout = ash_stdout |
| ash_stdout = subprocess.PIPE |
| test_stdout = subprocess.PIPE |
| |
| while not ash_process_has_started and num_tries < total_tries: |
| num_tries += 1 |
| ash_start_time = time.monotonic() |
| logging.info('Starting ash-chrome.') |
| ash_process = subprocess.Popen(ash_cmd, |
| env=ash_env, |
| stdout=ash_stdout, |
| stderr=subprocess.STDOUT) |
| |
| if should_symbolize: |
| logging.info('Symbolizing ash logs with asan symbolizer.') |
| ash_symbolize_process = subprocess.Popen([_ASAN_SYMBOLIZER_PATH], |
| stdin=ash_process.stdout, |
| stdout=ash_symbolize_stdout, |
| stderr=subprocess.STDOUT) |
| # Allow ash_process to receive a SIGPIPE if symbolize process exits. |
| ash_process.stdout.close() |
| |
| ash_process_has_started = _WaitForAshChromeToStart( |
| tmp_xdg_dir_name, lacros_mojo_socket_file, enable_mojo_crosapi, |
| ash_ready_file) |
| if ash_process_has_started: |
| break |
| |
| logging.warning('Starting ash-chrome timed out after %ds', |
| ASH_CHROME_TIMEOUT_SECONDS) |
| logging.warning('Are you using test_ash_chrome?') |
| logging.warning('Printing the output of "ps aux" for debugging:') |
| subprocess.call(['ps', 'aux']) |
| _KillNicely(ash_process) |
| _KillNicely(ash_symbolize_process, first_wait_secs=1) |
| |
| # Clean up for retry. |
| _ClearDir(tmp_xdg_dir_name) |
| _ClearDir(tmp_ash_data_dir_name) |
| |
| if not ash_process_has_started: |
| raise RuntimeError('Timed out waiting for ash-chrome to start') |
| |
| ash_elapsed_time = time.monotonic() - ash_start_time |
| logging.info('Started ash-chrome in %.3fs on try %d.', ash_elapsed_time, |
| num_tries) |
| |
| # Starts tests. |
| if enable_mojo_crosapi: |
| forward_args.append(lacros_mojo_socket_arg) |
| |
| forward_args.append('--ash-chrome-path=' + ash_chrome_file) |
| test_env = os.environ.copy() |
| test_env['WAYLAND_DISPLAY'] = ash_wayland_socket_name |
| test_env['EGL_PLATFORM'] = 'surfaceless' |
| test_env['XDG_RUNTIME_DIR'] = tmp_xdg_dir_name |
| |
| if run_tests_in_debugger: |
| test_process = _LaunchDebugger(args, forward_args, test_env) |
| else: |
| logging.info('Starting test process.') |
| test_process = subprocess.Popen([args.command] + forward_args, |
| env=test_env, |
| stdout=test_stdout, |
| stderr=subprocess.STDOUT) |
| if should_symbolize: |
| logging.info('Symbolizing test logs with asan symbolizer.') |
| test_symbolize_process = subprocess.Popen([_ASAN_SYMBOLIZER_PATH], |
| stdin=test_process.stdout) |
| # Allow test_process to receive a SIGPIPE if symbolize process exits. |
| test_process.stdout.close() |
| return test_process.wait() |
| |
| finally: |
| _KillNicely(ash_process) |
| # Give symbolizer processes time to finish writing with first_wait_secs. |
| _KillNicely(ash_symbolize_process, first_wait_secs=1) |
| _KillNicely(test_symbolize_process, first_wait_secs=1) |
| |
| 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: |
| _KillNicely(p) |
| |
| |
| 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 _ExpandFilterFileIfNeeded(test_target, forward_args): |
| if (test_target in _DEFAULT_FILTER_FILES_MAPPING.keys() and not any( |
| [arg.startswith('--test-launcher-filter-file') for arg in forward_args])): |
| file_path = os.path.abspath( |
| os.path.join(os.path.dirname(__file__), '..', '..', 'testing', |
| 'buildbot', 'filters', |
| _DEFAULT_FILTER_FILES_MAPPING[test_target])) |
| forward_args.append(f'--test-launcher-filter-file={file_path}') |
| |
| |
| 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) |
| |
| test_target = os.path.basename(args.command) |
| _ExpandFilterFileIfNeeded(test_target, forward_args) |
| |
| # |_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, test_target) 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.') |
| |
| debugger_group = test_parser.add_mutually_exclusive_group() |
| debugger_group.add_argument('--gdb', |
| action='store_true', |
| help='Run the test in GDB.') |
| debugger_group.add_argument('--lldb', |
| action='store_true', |
| help='Run the test in LLDB.') |
| |
| # 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.') |
| test_parser.add_argument( |
| '--ash-logging-path', |
| type=str, |
| help='File & path to ash-chrome logging output while running Lacros ' |
| 'browser tests. If not provided, no output will be generated.') |
| test_parser.add_argument('--combine-ash-logs-on-bots', |
| action='store_true', |
| help='Whether to combine ash logs on bots.') |
| test_parser.add_argument( |
| '--asan-symbolize-output', |
| action='store_true', |
| help='Whether to run subprocess log outputs through the asan symbolizer.') |
| |
| args = arg_parser.parse_known_args() |
| if not hasattr(args[0], "func"): |
| # No command specified. |
| print(__doc__) |
| sys.exit(1) |
| |
| return args[0].func(args[0], args[1]) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(Main()) |