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