summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/lacros/test_runner.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/build/lacros/test_runner.py')
-rwxr-xr-xthird_party/libwebrtc/build/lacros/test_runner.py581
1 files changed, 581 insertions, 0 deletions
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())