Fix for Geolocation webTCT failures
[platform/framework/web/chromium-efl.git] / build / lacros / test_runner.py
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
12 Example 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
48 Interactively 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
56 import argparse
57 import json
58 import os
59 import logging
60 import re
61 import shutil
62 import signal
63 import subprocess
64 import sys
65 import tempfile
66 import time
67 import zipfile
68
69 _SRC_ROOT = os.path.abspath(
70     os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
71 sys.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.
87 ASH_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
136 def _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
141 def _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
176 def _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
198 def _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
217 def _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
242 def _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
272 def _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
315 def _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
336 def _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
351 def _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
372 def _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
388 def _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
400 def _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
427 def _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
440 def _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
473 This script adds --single-process-tests to support interactive debugging but
474 some tests will fail in this mode unless run independently. To debug a test
475 specify 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
500 def _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 \
513 support 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 \
527 the ash from CIPD? If you don't plan to build your own ash, you need \
528 to download first. Example commandlines:
529  $ cipd auth-login
530  $ echo "chromium/testing/linux-ash-chromium/x86_64/ash.zip \
531 version: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=\
535 lacros_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
732 def _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
746 def _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
762 def _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
772 def _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
802 def 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
875 if __name__ == '__main__':
876   sys.exit(Main())