1#!/usr/bin/env python3 2# 3# Copyright 2020 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6"""This script facilitates running tests for lacros on Linux. 7 8 In order to run lacros tests on Linux, please first follow bit.ly/3juQVNJ 9 to setup build directory with the lacros-chrome-on-linux build configuration, 10 and corresponding test targets are built successfully. 11 12Example usages 13 14 ./build/lacros/test_runner.py test out/lacros/url_unittests 15 ./build/lacros/test_runner.py test out/lacros/browser_tests 16 17 The commands above run url_unittests and browser_tests respectively, and more 18 specifically, url_unitests is executed directly while browser_tests is 19 executed with the latest version of prebuilt ash-chrome, and the behavior is 20 controlled by |_TARGETS_REQUIRE_ASH_CHROME|, and it's worth noting that the 21 list is maintained manually, so if you see something is wrong, please upload a 22 CL to fix it. 23 24 ./build/lacros/test_runner.py test out/lacros/browser_tests \\ 25 --gtest_filter=BrowserTest.Title 26 27 The above command only runs 'BrowserTest.Title', and any argument accepted by 28 the underlying test binary can be specified in the command. 29 30 ./build/lacros/test_runner.py test out/lacros/browser_tests \\ 31 --ash-chrome-version=793554 32 33 The above command runs tests with a given version of ash-chrome, which is 34 useful to reproduce test failures, the version corresponds to the commit 35 position of commits on the master branch, and a list of prebuilt versions can 36 be found at: gs://ash-chromium-on-linux-prebuilts/x86_64. 37 38 ./testing/xvfb.py ./build/lacros/test_runner.py test out/lacros/browser_tests 39 40 The above command starts ash-chrome with xvfb instead of an X11 window, and 41 it's useful when running tests without a display attached, such as sshing. 42 43 For version skew testing when passing --ash-chrome-path-override, the runner 44 will try to find the ash major version and Lacros major version. If ash is 45 newer(major version larger), the runner will not run any tests and just 46 returns success. 47 48Interactively debugging tests 49 50 Any of the previous examples accept the switches 51 --gdb 52 --lldb 53 to run the tests in the corresponding debugger. 54""" 55 56import argparse 57import json 58import os 59import logging 60import re 61import shutil 62import signal 63import subprocess 64import sys 65import tempfile 66import time 67import zipfile 68 69_SRC_ROOT = os.path.abspath( 70 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir)) 71sys.path.append(os.path.join(_SRC_ROOT, 'third_party', 'depot_tools')) 72 73 74# The cipd path for prebuilt ash chrome. 75_ASH_CIPD_PATH = 'chromium/testing/linux-ash-chromium/x86_64/ash.zip' 76 77 78# Directory to cache downloaded ash-chrome versions to avoid re-downloading. 79_PREBUILT_ASH_CHROME_DIR = os.path.join(os.path.dirname(__file__), 80 'prebuilt_ash_chrome') 81 82# File path to the asan symbolizer executable. 83_ASAN_SYMBOLIZER_PATH = os.path.join(_SRC_ROOT, 'tools', 'valgrind', 'asan', 84 'asan_symbolize.py') 85 86# Number of seconds to wait for ash-chrome to start. 87ASH_CHROME_TIMEOUT_SECONDS = ( 88 300 if os.environ.get('ASH_WRAPPER', None) else 10) 89 90# List of targets that require ash-chrome as a Wayland server in order to run. 91_TARGETS_REQUIRE_ASH_CHROME = [ 92 'app_shell_unittests', 93 'aura_unittests', 94 'browser_tests', 95 'components_unittests', 96 'compositor_unittests', 97 'content_unittests', 98 'dbus_unittests', 99 'extensions_unittests', 100 'media_unittests', 101 'message_center_unittests', 102 'snapshot_unittests', 103 'sync_integration_tests', 104 'unit_tests', 105 'views_unittests', 106 'wm_unittests', 107 108 # regex patterns. 109 '.*_browsertests', 110 '.*interactive_ui_tests' 111] 112 113# List of targets that require ash-chrome to support crosapi mojo APIs. 114_TARGETS_REQUIRE_MOJO_CROSAPI = [ 115 # TODO(jamescook): Add 'browser_tests' after multiple crosapi connections 116 # are allowed. For now we only enable crosapi in targets that run tests 117 # serially. 118 'interactive_ui_tests', 119 'lacros_chrome_browsertests', 120] 121 122# Default test filter file for each target. These filter files will be 123# used by default if no other filter file get specified. 124_DEFAULT_FILTER_FILES_MAPPING = { 125 'browser_tests': 'linux-lacros.browser_tests.filter', 126 'components_unittests': 'linux-lacros.components_unittests.filter', 127 'content_browsertests': 'linux-lacros.content_browsertests.filter', 128 'interactive_ui_tests': 'linux-lacros.interactive_ui_tests.filter', 129 'lacros_chrome_browsertests': 130 'linux-lacros.lacros_chrome_browsertests.filter', 131 'sync_integration_tests': 'linux-lacros.sync_integration_tests.filter', 132 'unit_tests': 'linux-lacros.unit_tests.filter', 133} 134 135 136def _GetAshChromeDirPath(version): 137 """Returns a path to the dir storing the downloaded version of ash-chrome.""" 138 return os.path.join(_PREBUILT_ASH_CHROME_DIR, version) 139 140 141def _remove_unused_ash_chrome_versions(version_to_skip): 142 """Removes unused ash-chrome versions to save disk space. 143 144 Currently, when an ash-chrome zip is downloaded and unpacked, the atime/mtime 145 of the dir and the files are NOW instead of the time when they were built, but 146 there is no garanteen it will always be the behavior in the future, so avoid 147 removing the current version just in case. 148 149 Args: 150 version_to_skip (str): the version to skip removing regardless of its age. 151 """ 152 days = 7 153 expiration_duration = 60 * 60 * 24 * days 154 155 for f in os.listdir(_PREBUILT_ASH_CHROME_DIR): 156 if f == version_to_skip: 157 continue 158 159 p = os.path.join(_PREBUILT_ASH_CHROME_DIR, f) 160 if os.path.isfile(p): 161 # The prebuilt ash-chrome dir is NOT supposed to contain any files, remove 162 # them to keep the directory clean. 163 os.remove(p) 164 continue 165 chrome_path = os.path.join(p, 'test_ash_chrome') 166 if not os.path.exists(chrome_path): 167 chrome_path = p 168 age = time.time() - os.path.getatime(chrome_path) 169 if age > expiration_duration: 170 logging.info( 171 'Removing ash-chrome: "%s" as it hasn\'t been used in the ' 172 'past %d days', p, days) 173 shutil.rmtree(p) 174 175 176def _GetLatestVersionOfAshChrome(): 177 '''Get the latest ash chrome version. 178 179 Get the package version info with canary ref. 180 181 Returns: 182 A string with the chrome version. 183 184 Raises: 185 RuntimeError: if we can not get the version. 186 ''' 187 cp = subprocess.run( 188 ['cipd', 'describe', _ASH_CIPD_PATH, '-version', 'canary'], 189 capture_output=True) 190 assert (cp.returncode == 0) 191 groups = re.search(r'version:(?P<version>[\d\.]+)', str(cp.stdout)) 192 if not groups: 193 raise RuntimeError('Can not find the version. Error message: %s' % 194 cp.stdout) 195 return groups.group('version') 196 197 198def _DownloadAshChromeFromCipd(path, version): 199 '''Download the ash chrome with the requested version. 200 201 Args: 202 path: string for the downloaded ash chrome folder. 203 version: string for the ash chrome version. 204 205 Returns: 206 A string representing the path for the downloaded ash chrome. 207 ''' 208 with tempfile.TemporaryDirectory() as temp_dir: 209 ensure_file_path = os.path.join(temp_dir, 'ensure_file.txt') 210 f = open(ensure_file_path, 'w+') 211 f.write(_ASH_CIPD_PATH + ' version:' + version) 212 f.close() 213 subprocess.run( 214 ['cipd', 'ensure', '-ensure-file', ensure_file_path, '-root', path]) 215 216 217def _DoubleCheckDownloadedAshChrome(path, version): 218 '''Check the downloaded ash is the expected version. 219 220 Double check by running the chrome binary with --version. 221 222 Args: 223 path: string for the downloaded ash chrome folder. 224 version: string for the expected ash chrome version. 225 226 Raises: 227 RuntimeError if no test_ash_chrome binary can be found. 228 ''' 229 test_ash_chrome = os.path.join(path, 'test_ash_chrome') 230 if not os.path.exists(test_ash_chrome): 231 raise RuntimeError('Can not find test_ash_chrome binary under %s' % path) 232 cp = subprocess.run([test_ash_chrome, '--version'], capture_output=True) 233 assert (cp.returncode == 0) 234 if str(cp.stdout).find(version) == -1: 235 logging.warning( 236 'The downloaded ash chrome version is %s, but the ' 237 'expected ash chrome is %s. There is a version mismatch. Please ' 238 'file a bug to OS>Lacros so someone can take a look.' % 239 (cp.stdout, version)) 240 241 242def _DownloadAshChromeIfNecessary(version): 243 """Download a given version of ash-chrome if not already exists. 244 245 Args: 246 version: A string representing the version, such as "793554". 247 248 Raises: 249 RuntimeError: If failed to download the specified version, for example, 250 if the version is not present on gcs. 251 """ 252 253 def IsAshChromeDirValid(ash_chrome_dir): 254 # This function assumes that once 'chrome' is present, other dependencies 255 # will be present as well, it's not always true, for example, if the test 256 # runner process gets killed in the middle of unzipping (~2 seconds), but 257 # it's unlikely for the assumption to break in practice. 258 return os.path.isdir(ash_chrome_dir) and os.path.isfile( 259 os.path.join(ash_chrome_dir, 'test_ash_chrome')) 260 261 ash_chrome_dir = _GetAshChromeDirPath(version) 262 if IsAshChromeDirValid(ash_chrome_dir): 263 return 264 265 shutil.rmtree(ash_chrome_dir, ignore_errors=True) 266 os.makedirs(ash_chrome_dir) 267 _DownloadAshChromeFromCipd(ash_chrome_dir, version) 268 _DoubleCheckDownloadedAshChrome(ash_chrome_dir, version) 269 _remove_unused_ash_chrome_versions(version) 270 271 272def _WaitForAshChromeToStart(tmp_xdg_dir, lacros_mojo_socket_file, 273 enable_mojo_crosapi, ash_ready_file): 274 """Waits for Ash-Chrome to be up and running and returns a boolean indicator. 275 276 Determine whether ash-chrome is up and running by checking whether two files 277 (lock file + socket) have been created in the |XDG_RUNTIME_DIR| and the lacros 278 mojo socket file has been created if enabling the mojo "crosapi" interface. 279 TODO(crbug.com/1107966): Figure out a more reliable hook to determine the 280 status of ash-chrome, likely through mojo connection. 281 282 Args: 283 tmp_xdg_dir (str): Path to the XDG_RUNTIME_DIR. 284 lacros_mojo_socket_file (str): Path to the lacros mojo socket file. 285 enable_mojo_crosapi (bool): Whether to bootstrap the crosapi mojo interface 286 between ash and the lacros test binary. 287 ash_ready_file (str): Path to a non-existing file. After ash is ready for 288 testing, the file will be created. 289 290 Returns: 291 A boolean indicating whether Ash-chrome is up and running. 292 """ 293 294 def IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file, 295 enable_mojo_crosapi, ash_ready_file): 296 # There should be 2 wayland files. 297 if len(os.listdir(tmp_xdg_dir)) < 2: 298 return False 299 if enable_mojo_crosapi and not os.path.exists(lacros_mojo_socket_file): 300 return False 301 return os.path.exists(ash_ready_file) 302 303 time_counter = 0 304 while not IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file, 305 enable_mojo_crosapi, ash_ready_file): 306 time.sleep(0.5) 307 time_counter += 0.5 308 if time_counter > ASH_CHROME_TIMEOUT_SECONDS: 309 break 310 311 return IsAshChromeReady(tmp_xdg_dir, lacros_mojo_socket_file, 312 enable_mojo_crosapi, ash_ready_file) 313 314 315def _ExtractAshMajorVersion(file_path): 316 """Extract major version from file_path. 317 318 File path like this: 319 ../../lacros_version_skew_tests_v94.0.4588.0/test_ash_chrome 320 321 Returns: 322 int representing the major version. Or 0 if it can't extract 323 major version. 324 """ 325 m = re.search( 326 'lacros_version_skew_tests_v(?P<version>[0-9]+).[0-9]+.[0-9]+.[0-9]+/', 327 file_path) 328 if (m and 'version' in m.groupdict().keys()): 329 return int(m.group('version')) 330 logging.warning('Can not find the ash version in %s.' % file_path) 331 # Returns ash major version as 0, so we can still run tests. 332 # This is likely happen because user is running in local environments. 333 return 0 334 335 336def _FindLacrosMajorVersionFromMetadata(): 337 # This handles the logic on bots. When running on bots, 338 # we don't copy source files to test machines. So we build a 339 # metadata.json file which contains version information. 340 if not os.path.exists('metadata.json'): 341 logging.error('Can not determine current version.') 342 # Returns 0 so it can't run any tests. 343 return 0 344 version = '' 345 with open('metadata.json', 'r') as file: 346 content = json.load(file) 347 version = content['content']['version'] 348 return int(version[:version.find('.')]) 349 350 351def _FindLacrosMajorVersion(): 352 """Returns the major version in the current checkout. 353 354 It would try to read src/chrome/VERSION. If it's not available, 355 then try to read metadata.json. 356 357 Returns: 358 int representing the major version. Or 0 if it fails to 359 determine the version. 360 """ 361 version_file = os.path.abspath( 362 os.path.join(os.path.abspath(os.path.dirname(__file__)), 363 '../../chrome/VERSION')) 364 # This is mostly happens for local development where 365 # src/chrome/VERSION exists. 366 if os.path.exists(version_file): 367 lines = open(version_file, 'r').readlines() 368 return int(lines[0][lines[0].find('=') + 1:-1]) 369 return _FindLacrosMajorVersionFromMetadata() 370 371 372def _ParseSummaryOutput(forward_args): 373 """Find the summary output file path. 374 375 Args: 376 forward_args (list): Args to be forwarded to the test command. 377 378 Returns: 379 None if not found, or str representing the output file path. 380 """ 381 logging.warning(forward_args) 382 for arg in forward_args: 383 if arg.startswith('--test-launcher-summary-output='): 384 return arg[len('--test-launcher-summary-output='):] 385 return None 386 387 388def _IsRunningOnBots(forward_args): 389 """Detects if the script is running on bots or not. 390 391 Args: 392 forward_args (list): Args to be forwarded to the test command. 393 394 Returns: 395 True if the script is running on bots. Otherwise returns False. 396 """ 397 return '--test-launcher-bot-mode' in forward_args 398 399 400def _KillNicely(proc, timeout_secs=2, first_wait_secs=0): 401 """Kills a subprocess nicely. 402 403 Args: 404 proc: The subprocess to kill. 405 timeout_secs: The timeout to wait in seconds. 406 first_wait_secs: The grace period before sending first SIGTERM in seconds. 407 """ 408 if not proc: 409 return 410 411 if first_wait_secs: 412 try: 413 proc.wait(first_wait_secs) 414 return 415 except subprocess.TimeoutExpired: 416 pass 417 418 if proc.poll() is None: 419 proc.terminate() 420 try: 421 proc.wait(timeout_secs) 422 except subprocess.TimeoutExpired: 423 proc.kill() 424 proc.wait() 425 426 427def _ClearDir(dirpath): 428 """Deletes everything within the directory. 429 430 Args: 431 dirpath: The path of the directory. 432 """ 433 for e in os.scandir(dirpath): 434 if e.is_dir(): 435 shutil.rmtree(e.path) 436 elif e.is_file(): 437 os.remove(e.path) 438 439 440def _LaunchDebugger(args, forward_args, test_env): 441 """Launches the requested debugger. 442 443 This is used to wrap the test invocation in a debugger. It returns the 444 created Popen class of the debugger process. 445 446 Args: 447 args (dict): Args for this script. 448 forward_args (list): Args to be forwarded to the test command. 449 test_env (dict): Computed environment variables for the test. 450 """ 451 logging.info('Starting debugger.') 452 453 # Redirect fatal signals to "ignore." When running an interactive debugger, 454 # these signals should go only to the debugger so the user can break back out 455 # of the debugged test process into the debugger UI without killing this 456 # parent script. 457 for sig in (signal.SIGTERM, signal.SIGINT): 458 signal.signal(sig, signal.SIG_IGN) 459 460 # Force the tests into single-process-test mode for debugging unless manually 461 # specified. Otherwise the tests will run in a child process that the debugger 462 # won't be attached to and the debugger won't do anything. 463 if not ("--single-process" in forward_args 464 or "--single-process-tests" in forward_args): 465 forward_args += ["--single-process-tests"] 466 467 # Adding --single-process-tests can cause some tests to fail when they're 468 # run in the same process. Forcing the user to specify a filter will prevent 469 # a later error. 470 if not [i for i in forward_args if i.startswith("--gtest_filter")]: 471 logging.error("""Interactive debugging requested without --gtest_filter 472 473This script adds --single-process-tests to support interactive debugging but 474some tests will fail in this mode unless run independently. To debug a test 475specify a --gtest_filter=Foo.Bar to name the test you want to debug. 476""") 477 sys.exit(1) 478 479 # This code attempts to source the debugger configuration file. Some 480 # users will have this in their init but sourcing it more than once is 481 # harmless and helps people that haven't configured it. 482 if args.gdb: 483 gdbinit_file = os.path.normpath( 484 os.path.join(os.path.realpath(__file__), "../../../tools/gdb/gdbinit")) 485 debugger_command = [ 486 'gdb', '--init-eval-command', 'source ' + gdbinit_file, '--args' 487 ] 488 else: 489 lldbinit_dir = os.path.normpath( 490 os.path.join(os.path.realpath(__file__), "../../../tools/lldb")) 491 debugger_command = [ 492 'lldb', '-O', 493 "script sys.path[:0] = ['%s']" % lldbinit_dir, '-O', 494 'script import lldbinit', '--' 495 ] 496 debugger_command += [args.command] + forward_args 497 return subprocess.Popen(debugger_command, env=test_env) 498 499 500def _RunTestWithAshChrome(args, forward_args): 501 """Runs tests with ash-chrome. 502 503 Args: 504 args (dict): Args for this script. 505 forward_args (list): Args to be forwarded to the test command. 506 """ 507 if args.ash_chrome_path_override: 508 ash_chrome_file = args.ash_chrome_path_override 509 ash_major_version = _ExtractAshMajorVersion(ash_chrome_file) 510 lacros_major_version = _FindLacrosMajorVersion() 511 if ash_major_version > lacros_major_version: 512 logging.warning('''Not running any tests, because we do not \ 513support version skew testing for Lacros M%s against ash M%s''' % 514 (lacros_major_version, ash_major_version)) 515 # Create an empty output.json file so result adapter can read 516 # the file. Or else result adapter will report no file found 517 # and result infra failure. 518 output_json = _ParseSummaryOutput(forward_args) 519 if output_json: 520 with open(output_json, 'w') as f: 521 f.write("""{"all_tests":[],"disabled_tests":[],"global_tags":[], 522"per_iteration_data":[],"test_locations":{}}""") 523 # Although we don't run any tests, this is considered as success. 524 return 0 525 if not os.path.exists(ash_chrome_file): 526 logging.error("""Can not find ash chrome at %s. Did you download \ 527the ash from CIPD? If you don't plan to build your own ash, you need \ 528to download first. Example commandlines: 529 $ cipd auth-login 530 $ echo "chromium/testing/linux-ash-chromium/x86_64/ash.zip \ 531version:92.0.4515.130" > /tmp/ensure-file.txt 532 $ cipd ensure -ensure-file /tmp/ensure-file.txt \ 533-root lacros_version_skew_tests_v92.0.4515.130 534 Then you can use --ash-chrome-path-override=\ 535lacros_version_skew_tests_v92.0.4515.130/test_ash_chrome 536""" % ash_chrome_file) 537 return 1 538 elif args.ash_chrome_path: 539 ash_chrome_file = args.ash_chrome_path 540 else: 541 ash_chrome_version = (args.ash_chrome_version 542 or _GetLatestVersionOfAshChrome()) 543 _DownloadAshChromeIfNecessary(ash_chrome_version) 544 logging.info('Ash-chrome version: %s', ash_chrome_version) 545 546 ash_chrome_file = os.path.join(_GetAshChromeDirPath(ash_chrome_version), 547 'test_ash_chrome') 548 try: 549 # Starts Ash-Chrome. 550 tmp_xdg_dir_name = tempfile.mkdtemp() 551 tmp_ash_data_dir_name = tempfile.mkdtemp() 552 tmp_unique_ash_dir_name = tempfile.mkdtemp() 553 554 # Please refer to below file for how mojo connection is set up in testing. 555 # //chrome/browser/ash/crosapi/test_mojo_connection_manager.h 556 lacros_mojo_socket_file = '%s/lacros.sock' % tmp_ash_data_dir_name 557 lacros_mojo_socket_arg = ('--lacros-mojo-socket-for-testing=%s' % 558 lacros_mojo_socket_file) 559 ash_ready_file = '%s/ash_ready.txt' % tmp_ash_data_dir_name 560 enable_mojo_crosapi = any(t == os.path.basename(args.command) 561 for t in _TARGETS_REQUIRE_MOJO_CROSAPI) 562 ash_wayland_socket_name = 'wayland-exo' 563 564 ash_process = None 565 ash_env = os.environ.copy() 566 ash_env['XDG_RUNTIME_DIR'] = tmp_xdg_dir_name 567 ash_cmd = [ 568 ash_chrome_file, 569 '--user-data-dir=%s' % tmp_ash_data_dir_name, 570 '--enable-wayland-server', 571 '--no-startup-window', 572 '--disable-input-event-activation-protection', 573 '--disable-lacros-keep-alive', 574 '--disable-login-lacros-opening', 575 '--enable-field-trial-config', 576 '--enable-logging=stderr', 577 '--enable-features=LacrosSupport,LacrosPrimary,LacrosOnly', 578 '--ash-ready-file-path=%s' % ash_ready_file, 579 '--wayland-server-socket=%s' % ash_wayland_socket_name, 580 ] 581 if '--enable-pixel-output-in-tests' not in forward_args: 582 ash_cmd.append('--disable-gl-drawing-for-tests') 583 584 if enable_mojo_crosapi: 585 ash_cmd.append(lacros_mojo_socket_arg) 586 587 # Users can specify a wrapper for the ash binary to do things like 588 # attaching debuggers. For example, this will open a new terminal window 589 # and run GDB. 590 # $ export ASH_WRAPPER="gnome-terminal -- gdb --ex=r --args" 591 ash_wrapper = os.environ.get('ASH_WRAPPER', None) 592 if ash_wrapper: 593 logging.info('Running ash with "ASH_WRAPPER": %s', ash_wrapper) 594 ash_cmd = list(ash_wrapper.split()) + ash_cmd 595 596 ash_process = None 597 ash_process_has_started = False 598 total_tries = 3 599 num_tries = 0 600 ash_start_time = None 601 602 # Create a log file if the user wanted to have one. 603 ash_log = None 604 ash_log_path = None 605 606 run_tests_in_debugger = args.gdb or args.lldb 607 608 if args.ash_logging_path: 609 ash_log_path = args.ash_logging_path 610 # Put ash logs in a separate file on bots. 611 # For asan builds, the ash log is not symbolized. In order to 612 # read the stack strace, we don't redirect logs to another file. 613 elif _IsRunningOnBots(forward_args) and not args.combine_ash_logs_on_bots: 614 summary_file = _ParseSummaryOutput(forward_args) 615 if summary_file: 616 ash_log_path = os.path.join(os.path.dirname(summary_file), 617 'ash_chrome.log') 618 elif run_tests_in_debugger: 619 # The debugger is unusable when all Ash logs are getting dumped to the 620 # same terminal. Redirect to a log file if there isn't one specified. 621 logging.info("Running in the debugger and --ash-logging-path is not " + 622 "specified, defaulting to the current directory.") 623 ash_log_path = 'ash_chrome.log' 624 625 if ash_log_path: 626 ash_log = open(ash_log_path, 'a') 627 logging.info('Writing ash-chrome logs to: %s', ash_log_path) 628 629 ash_stdout = ash_log or None 630 test_stdout = None 631 632 # Setup asan symbolizer. 633 ash_symbolize_process = None 634 test_symbolize_process = None 635 should_symbolize = False 636 if args.asan_symbolize_output and os.path.exists(_ASAN_SYMBOLIZER_PATH): 637 should_symbolize = True 638 ash_symbolize_stdout = ash_stdout 639 ash_stdout = subprocess.PIPE 640 test_stdout = subprocess.PIPE 641 642 while not ash_process_has_started and num_tries < total_tries: 643 num_tries += 1 644 ash_start_time = time.monotonic() 645 logging.info('Starting ash-chrome.') 646 647 # Using preexec_fn=os.setpgrp here will detach the forked process from 648 # this process group before exec-ing Ash. This prevents interactive 649 # Control-C from being seen by Ash. Otherwise Control-C in a debugger 650 # can kill Ash out from under the debugger. In non-debugger cases, this 651 # script attempts to clean up the spawned processes nicely. 652 ash_process = subprocess.Popen(ash_cmd, 653 env=ash_env, 654 preexec_fn=os.setpgrp, 655 stdout=ash_stdout, 656 stderr=subprocess.STDOUT) 657 658 if should_symbolize: 659 logging.info('Symbolizing ash logs with asan symbolizer.') 660 ash_symbolize_process = subprocess.Popen([_ASAN_SYMBOLIZER_PATH], 661 stdin=ash_process.stdout, 662 preexec_fn=os.setpgrp, 663 stdout=ash_symbolize_stdout, 664 stderr=subprocess.STDOUT) 665 # Allow ash_process to receive a SIGPIPE if symbolize process exits. 666 ash_process.stdout.close() 667 668 ash_process_has_started = _WaitForAshChromeToStart( 669 tmp_xdg_dir_name, lacros_mojo_socket_file, enable_mojo_crosapi, 670 ash_ready_file) 671 if ash_process_has_started: 672 break 673 674 logging.warning('Starting ash-chrome timed out after %ds', 675 ASH_CHROME_TIMEOUT_SECONDS) 676 logging.warning('Are you using test_ash_chrome?') 677 logging.warning('Printing the output of "ps aux" for debugging:') 678 subprocess.call(['ps', 'aux']) 679 _KillNicely(ash_process) 680 _KillNicely(ash_symbolize_process, first_wait_secs=1) 681 682 # Clean up for retry. 683 _ClearDir(tmp_xdg_dir_name) 684 _ClearDir(tmp_ash_data_dir_name) 685 686 if not ash_process_has_started: 687 raise RuntimeError('Timed out waiting for ash-chrome to start') 688 689 ash_elapsed_time = time.monotonic() - ash_start_time 690 logging.info('Started ash-chrome in %.3fs on try %d.', ash_elapsed_time, 691 num_tries) 692 693 # Starts tests. 694 if enable_mojo_crosapi: 695 forward_args.append(lacros_mojo_socket_arg) 696 697 forward_args.append('--ash-chrome-path=' + ash_chrome_file) 698 forward_args.append('--unique-ash-dir=' + tmp_unique_ash_dir_name) 699 700 test_env = os.environ.copy() 701 test_env['WAYLAND_DISPLAY'] = ash_wayland_socket_name 702 test_env['EGL_PLATFORM'] = 'surfaceless' 703 test_env['XDG_RUNTIME_DIR'] = tmp_xdg_dir_name 704 705 if run_tests_in_debugger: 706 test_process = _LaunchDebugger(args, forward_args, test_env) 707 else: 708 logging.info('Starting test process.') 709 test_process = subprocess.Popen([args.command] + forward_args, 710 env=test_env, 711 stdout=test_stdout, 712 stderr=subprocess.STDOUT) 713 if should_symbolize: 714 logging.info('Symbolizing test logs with asan symbolizer.') 715 test_symbolize_process = subprocess.Popen([_ASAN_SYMBOLIZER_PATH], 716 stdin=test_process.stdout) 717 # Allow test_process to receive a SIGPIPE if symbolize process exits. 718 test_process.stdout.close() 719 return test_process.wait() 720 721 finally: 722 _KillNicely(ash_process) 723 # Give symbolizer processes time to finish writing with first_wait_secs. 724 _KillNicely(ash_symbolize_process, first_wait_secs=1) 725 _KillNicely(test_symbolize_process, first_wait_secs=1) 726 727 shutil.rmtree(tmp_xdg_dir_name, ignore_errors=True) 728 shutil.rmtree(tmp_ash_data_dir_name, ignore_errors=True) 729 shutil.rmtree(tmp_unique_ash_dir_name, ignore_errors=True) 730 731 732def _RunTestDirectly(args, forward_args): 733 """Runs tests by invoking the test command directly. 734 735 args (dict): Args for this script. 736 forward_args (list): Args to be forwarded to the test command. 737 """ 738 try: 739 p = None 740 p = subprocess.Popen([args.command] + forward_args) 741 return p.wait() 742 finally: 743 _KillNicely(p) 744 745 746def _HandleSignal(sig, _): 747 """Handles received signals to make sure spawned test process are killed. 748 749 sig (int): An integer representing the received signal, for example SIGTERM. 750 """ 751 logging.warning('Received signal: %d, killing spawned processes', sig) 752 753 # Don't do any cleanup here, instead, leave it to the finally blocks. 754 # Assumption is based on https://docs.python.org/3/library/sys.html#sys.exit: 755 # cleanup actions specified by finally clauses of try statements are honored. 756 757 # https://tldp.org/LDP/abs/html/exitcodes.html: 758 # Exit code 128+n -> Fatal error signal "n". 759 sys.exit(128 + sig) 760 761 762def _ExpandFilterFileIfNeeded(test_target, forward_args): 763 if (test_target in _DEFAULT_FILTER_FILES_MAPPING.keys() and not any( 764 [arg.startswith('--test-launcher-filter-file') for arg in forward_args])): 765 file_path = os.path.abspath( 766 os.path.join(os.path.dirname(__file__), '..', '..', 'testing', 767 'buildbot', 'filters', 768 _DEFAULT_FILTER_FILES_MAPPING[test_target])) 769 forward_args.append(f'--test-launcher-filter-file={file_path}') 770 771 772def _RunTest(args, forward_args): 773 """Runs tests with given args. 774 775 args (dict): Args for this script. 776 forward_args (list): Args to be forwarded to the test command. 777 778 Raises: 779 RuntimeError: If the given test binary doesn't exist or the test runner 780 doesn't know how to run it. 781 """ 782 783 if not os.path.isfile(args.command): 784 raise RuntimeError('Specified test command: "%s" doesn\'t exist' % 785 args.command) 786 787 test_target = os.path.basename(args.command) 788 _ExpandFilterFileIfNeeded(test_target, forward_args) 789 790 # |_TARGETS_REQUIRE_ASH_CHROME| may not always be accurate as it is updated 791 # with a best effort only, therefore, allow the invoker to override the 792 # behavior with a specified ash-chrome version, which makes sure that 793 # automated CI/CQ builders would always work correctly. 794 requires_ash_chrome = any( 795 re.match(t, test_target) for t in _TARGETS_REQUIRE_ASH_CHROME) 796 if not requires_ash_chrome and not args.ash_chrome_version: 797 return _RunTestDirectly(args, forward_args) 798 799 return _RunTestWithAshChrome(args, forward_args) 800 801 802def Main(): 803 for sig in (signal.SIGTERM, signal.SIGINT): 804 signal.signal(sig, _HandleSignal) 805 806 logging.basicConfig(level=logging.INFO) 807 arg_parser = argparse.ArgumentParser() 808 arg_parser.usage = __doc__ 809 810 subparsers = arg_parser.add_subparsers() 811 812 test_parser = subparsers.add_parser('test', help='Run tests') 813 test_parser.set_defaults(func=_RunTest) 814 815 test_parser.add_argument( 816 'command', 817 help='A single command to invoke the tests, for example: ' 818 '"./url_unittests". Any argument unknown to this test runner script will ' 819 'be forwarded to the command, for example: "--gtest_filter=Suite.Test"') 820 821 version_group = test_parser.add_mutually_exclusive_group() 822 version_group.add_argument( 823 '--ash-chrome-version', 824 type=str, 825 help='Version of an prebuilt ash-chrome to use for testing, for example: ' 826 '"793554", and the version corresponds to the commit position of commits ' 827 'on the main branch. If not specified, will use the latest version ' 828 'available') 829 version_group.add_argument( 830 '--ash-chrome-path', 831 type=str, 832 help='Path to an locally built ash-chrome to use for testing. ' 833 'In general you should build //chrome/test:test_ash_chrome.') 834 835 debugger_group = test_parser.add_mutually_exclusive_group() 836 debugger_group.add_argument('--gdb', 837 action='store_true', 838 help='Run the test in GDB.') 839 debugger_group.add_argument('--lldb', 840 action='store_true', 841 help='Run the test in LLDB.') 842 843 # This is for version skew testing. The current CI/CQ builder builds 844 # an ash chrome and pass it using --ash-chrome-path. In order to use the same 845 # builder for version skew testing, we use a new argument to override 846 # the ash chrome. 847 test_parser.add_argument( 848 '--ash-chrome-path-override', 849 type=str, 850 help='The same as --ash-chrome-path. But this will override ' 851 '--ash-chrome-path or --ash-chrome-version if any of these ' 852 'arguments exist.') 853 test_parser.add_argument( 854 '--ash-logging-path', 855 type=str, 856 help='File & path to ash-chrome logging output while running Lacros ' 857 'browser tests. If not provided, no output will be generated.') 858 test_parser.add_argument('--combine-ash-logs-on-bots', 859 action='store_true', 860 help='Whether to combine ash logs on bots.') 861 test_parser.add_argument( 862 '--asan-symbolize-output', 863 action='store_true', 864 help='Whether to run subprocess log outputs through the asan symbolizer.') 865 866 args = arg_parser.parse_known_args() 867 if not hasattr(args[0], "func"): 868 # No command specified. 869 print(__doc__) 870 sys.exit(1) 871 872 return args[0].func(args[0], args[1]) 873 874 875if __name__ == '__main__': 876 sys.exit(Main()) 877