1#!/usr/bin/env python 2# Copyright 2012 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Sets environment variables needed to run a chromium unit test.""" 7 8from __future__ import print_function 9import io 10import os 11import signal 12import subprocess 13import sys 14import time 15 16 17# This is hardcoded to be src/ relative to this script. 18ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 20 21def trim_cmd(cmd): 22 """Removes internal flags from cmd since they're just used to communicate from 23 the host machine to this script running on the swarm slaves.""" 24 sanitizers = ['asan', 'lsan', 'msan', 'tsan', 'coverage-continuous-mode', 25 'skip-set-lpac-acls'] 26 internal_flags = frozenset('--%s=%d' % (name, value) 27 for name in sanitizers 28 for value in [0, 1]) 29 return [i for i in cmd if i not in internal_flags] 30 31 32def fix_python_path(cmd): 33 """Returns the fixed command line to call the right python executable.""" 34 out = cmd[:] 35 if out[0] == 'python': 36 out[0] = sys.executable 37 elif out[0].endswith('.py'): 38 out.insert(0, sys.executable) 39 return out 40 41 42def get_sanitizer_env(asan, lsan, msan, tsan, cfi_diag): 43 """Returns the environment flags needed for sanitizer tools.""" 44 45 extra_env = {} 46 47 # Instruct GTK to use malloc while running sanitizer-instrumented tests. 48 extra_env['G_SLICE'] = 'always-malloc' 49 50 extra_env['NSS_DISABLE_ARENA_FREE_LIST'] = '1' 51 extra_env['NSS_DISABLE_UNLOAD'] = '1' 52 53 # TODO(glider): remove the symbolizer path once 54 # https://code.google.com/p/address-sanitizer/issues/detail?id=134 is fixed. 55 symbolizer_path = os.path.join(ROOT_DIR, 56 'third_party', 'llvm-build', 'Release+Asserts', 'bin', 'llvm-symbolizer') 57 58 if lsan or tsan: 59 # LSan is not sandbox-compatible, so we can use online symbolization. In 60 # fact, it needs symbolization to be able to apply suppressions. 61 symbolization_options = ['symbolize=1', 62 'external_symbolizer_path=%s' % symbolizer_path] 63 elif (asan or msan or cfi_diag) and sys.platform not in ['win32', 'cygwin']: 64 # ASan uses a script for offline symbolization, except on Windows. 65 # Important note: when running ASan with leak detection enabled, we must use 66 # the LSan symbolization options above. 67 symbolization_options = ['symbolize=0'] 68 # Set the path to llvm-symbolizer to be used by asan_symbolize.py 69 extra_env['LLVM_SYMBOLIZER_PATH'] = symbolizer_path 70 else: 71 symbolization_options = [] 72 73 # Leverage sanitizer to print stack trace on abort (e.g. assertion failure). 74 symbolization_options.append('handle_abort=1') 75 76 if asan: 77 asan_options = symbolization_options[:] 78 if lsan: 79 asan_options.append('detect_leaks=1') 80 # LSan appears to have trouble with later versions of glibc. 81 # See https://github.com/google/sanitizers/issues/1322 82 if 'linux' in sys.platform: 83 asan_options.append('intercept_tls_get_addr=0') 84 85 if asan_options: 86 extra_env['ASAN_OPTIONS'] = ' '.join(asan_options) 87 88 if lsan: 89 if asan or msan: 90 lsan_options = [] 91 else: 92 lsan_options = symbolization_options[:] 93 if sys.platform == 'linux2': 94 # Use the debug version of libstdc++ under LSan. If we don't, there will 95 # be a lot of incomplete stack traces in the reports. 96 extra_env['LD_LIBRARY_PATH'] = '/usr/lib/x86_64-linux-gnu/debug:' 97 98 extra_env['LSAN_OPTIONS'] = ' '.join(lsan_options) 99 100 if msan: 101 msan_options = symbolization_options[:] 102 if lsan: 103 msan_options.append('detect_leaks=1') 104 extra_env['MSAN_OPTIONS'] = ' '.join(msan_options) 105 extra_env['VK_ICD_FILENAMES'] = '' 106 extra_env['LIBGL_DRIVERS_PATH'] = '' 107 108 if tsan: 109 tsan_options = symbolization_options[:] 110 extra_env['TSAN_OPTIONS'] = ' '.join(tsan_options) 111 112 # CFI uses the UBSan runtime to provide diagnostics. 113 if cfi_diag: 114 ubsan_options = symbolization_options[:] + ['print_stacktrace=1'] 115 extra_env['UBSAN_OPTIONS'] = ' '.join(ubsan_options) 116 117 return extra_env 118 119def get_coverage_continuous_mode_env(env): 120 """Append %c (clang code coverage continuous mode) flag to LLVM_PROFILE_FILE 121 pattern string.""" 122 llvm_profile_file = env.get('LLVM_PROFILE_FILE') 123 if not llvm_profile_file: 124 return {} 125 126 dirname, basename = os.path.split(llvm_profile_file) 127 root, ext = os.path.splitext(basename) 128 return { 129 'LLVM_PROFILE_FILE': os.path.join(dirname, root + "%c" + ext) 130 } 131 132def get_sanitizer_symbolize_command(json_path=None, executable_path=None): 133 """Construct the command to invoke offline symbolization script.""" 134 script_path = os.path.join( 135 ROOT_DIR, 'tools', 'valgrind', 'asan', 'asan_symbolize.py') 136 cmd = [sys.executable, script_path] 137 if json_path is not None: 138 cmd.append('--test-summary-json-file=%s' % json_path) 139 if executable_path is not None: 140 cmd.append('--executable-path=%s' % executable_path) 141 return cmd 142 143 144def get_json_path(cmd): 145 """Extract the JSON test summary path from a command line.""" 146 json_path_flag = '--test-launcher-summary-output=' 147 for arg in cmd: 148 if arg.startswith(json_path_flag): 149 return arg.split(json_path_flag).pop() 150 return None 151 152 153def symbolize_snippets_in_json(cmd, env): 154 """Symbolize output snippets inside the JSON test summary.""" 155 json_path = get_json_path(cmd) 156 if json_path is None: 157 return 158 159 try: 160 symbolize_command = get_sanitizer_symbolize_command( 161 json_path=json_path, executable_path=cmd[0]) 162 p = subprocess.Popen(symbolize_command, stderr=subprocess.PIPE, env=env) 163 (_, stderr) = p.communicate() 164 except OSError as e: 165 print('Exception while symbolizing snippets: %s' % e, file=sys.stderr) 166 raise 167 168 if p.returncode != 0: 169 print("Error: failed to symbolize snippets in JSON:\n", file=sys.stderr) 170 print(stderr, file=sys.stderr) 171 raise subprocess.CalledProcessError(p.returncode, symbolize_command) 172 173 174def run_command_with_output(argv, stdoutfile, env=None, cwd=None): 175 """Run command and stream its stdout/stderr to the console & |stdoutfile|. 176 177 Also forward_signals to obey 178 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 179 180 Returns: 181 integer returncode of the subprocess. 182 """ 183 print('Running %r in %r (env: %r)' % (argv, cwd, env), file=sys.stderr) 184 assert stdoutfile 185 with io.open(stdoutfile, 'wb') as writer, \ 186 io.open(stdoutfile, 'rb', 1) as reader: 187 process = _popen(argv, env=env, cwd=cwd, stdout=writer, 188 stderr=subprocess.STDOUT) 189 forward_signals([process]) 190 while process.poll() is None: 191 sys.stdout.write(reader.read().decode('utf-8')) 192 # This sleep is needed for signal propagation. See the 193 # wait_with_signals() docstring. 194 time.sleep(0.1) 195 # Read the remaining. 196 sys.stdout.write(reader.read().decode('utf-8')) 197 print('Command %r returned exit code %d' % (argv, process.returncode), 198 file=sys.stderr) 199 return process.returncode 200 201 202def run_command(argv, env=None, cwd=None, log=True): 203 """Run command and stream its stdout/stderr both to stdout. 204 205 Also forward_signals to obey 206 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 207 208 Returns: 209 integer returncode of the subprocess. 210 """ 211 if log: 212 print('Running %r in %r (env: %r)' % (argv, cwd, env), file=sys.stderr) 213 process = _popen(argv, env=env, cwd=cwd, stderr=subprocess.STDOUT) 214 forward_signals([process]) 215 exit_code = wait_with_signals(process) 216 if log: 217 print('Command returned exit code %d' % exit_code, file=sys.stderr) 218 return exit_code 219 220 221def run_command_output_to_handle(argv, file_handle, env=None, cwd=None): 222 """Run command and stream its stdout/stderr both to |file_handle|. 223 224 Also forward_signals to obey 225 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 226 227 Returns: 228 integer returncode of the subprocess. 229 """ 230 print('Running %r in %r (env: %r)' % (argv, cwd, env)) 231 process = _popen( 232 argv, env=env, cwd=cwd, stderr=file_handle, stdout=file_handle) 233 forward_signals([process]) 234 exit_code = wait_with_signals(process) 235 print('Command returned exit code %d' % exit_code) 236 return exit_code 237 238 239def wait_with_signals(process): 240 """A version of process.wait() that works cross-platform. 241 242 This version properly surfaces the SIGBREAK signal. 243 244 From reading the subprocess.py source code, it seems we need to explicitly 245 call time.sleep(). The reason is that subprocess.Popen.wait() on Windows 246 directly calls WaitForSingleObject(), but only time.sleep() properly surface 247 the SIGBREAK signal. 248 249 Refs: 250 https://github.com/python/cpython/blob/v2.7.15/Lib/subprocess.py#L692 251 https://github.com/python/cpython/blob/v2.7.15/Modules/timemodule.c#L1084 252 253 Returns: 254 returncode of the process. 255 """ 256 while process.poll() is None: 257 time.sleep(0.1) 258 return process.returncode 259 260 261def forward_signals(procs): 262 """Forwards unix's SIGTERM or win's CTRL_BREAK_EVENT to the given processes. 263 264 This plays nicely with swarming's timeout handling. See also 265 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 266 267 Args: 268 procs: A list of subprocess.Popen objects representing child processes. 269 """ 270 assert all(isinstance(p, subprocess.Popen) for p in procs) 271 def _sig_handler(sig, _): 272 for p in procs: 273 if p.poll() is not None: 274 continue 275 # SIGBREAK is defined only for win32. 276 # pylint: disable=no-member 277 if sys.platform == 'win32' and sig == signal.SIGBREAK: 278 p.send_signal(signal.CTRL_BREAK_EVENT) 279 else: 280 print("Forwarding signal(%d) to process %d" % (sig, p.pid)) 281 p.send_signal(sig) 282 # pylint: enable=no-member 283 if sys.platform == 'win32': 284 signal.signal(signal.SIGBREAK, _sig_handler) # pylint: disable=no-member 285 else: 286 signal.signal(signal.SIGTERM, _sig_handler) 287 signal.signal(signal.SIGINT, _sig_handler) 288 289 290def run_executable(cmd, env, stdoutfile=None, cwd=None): 291 """Runs an executable with: 292 - CHROME_HEADLESS set to indicate that the test is running on a 293 bot and shouldn't do anything interactive like show modal dialogs. 294 - environment variable CR_SOURCE_ROOT set to the root directory. 295 - environment variable LANGUAGE to en_US.UTF-8. 296 - environment variable CHROME_DEVEL_SANDBOX set 297 - Reuses sys.executable automatically. 298 """ 299 extra_env = { 300 # Set to indicate that the executable is running non-interactively on 301 # a bot. 302 'CHROME_HEADLESS': '1', 303 304 # Many tests assume a English interface... 305 'LANG': 'en_US.UTF-8', 306 } 307 308 # Used by base/base_paths_linux.cc as an override. Just make sure the default 309 # logic is used. 310 env.pop('CR_SOURCE_ROOT', None) 311 312 # Copy logic from tools/build/scripts/slave/runtest.py. 313 asan = '--asan=1' in cmd 314 lsan = '--lsan=1' in cmd 315 msan = '--msan=1' in cmd 316 tsan = '--tsan=1' in cmd 317 cfi_diag = '--cfi-diag=1' in cmd 318 if stdoutfile or sys.platform in ['win32', 'cygwin']: 319 # Symbolization works in-process on Windows even when sandboxed. 320 use_symbolization_script = False 321 else: 322 # If any sanitizer is enabled, we print unsymbolized stack trace 323 # that is required to run through symbolization script. 324 use_symbolization_script = (asan or msan or cfi_diag or lsan or tsan) 325 326 if asan or lsan or msan or tsan or cfi_diag: 327 extra_env.update(get_sanitizer_env(asan, lsan, msan, tsan, cfi_diag)) 328 329 if lsan or tsan: 330 # LSan and TSan are not sandbox-friendly. 331 cmd.append('--no-sandbox') 332 333 # Enable clang code coverage continuous mode. 334 if '--coverage-continuous-mode=1' in cmd: 335 extra_env.update(get_coverage_continuous_mode_env(env)) 336 337 # pylint: disable=import-outside-toplevel 338 if '--skip-set-lpac-acls=1' not in cmd and sys.platform == 'win32': 339 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 340 'scripts')) 341 from scripts import common 342 common.set_lpac_acls(ROOT_DIR, is_test_script=True) 343 # pylint: enable=import-outside-toplevel 344 345 cmd = trim_cmd(cmd) 346 347 # Ensure paths are correctly separated on windows. 348 cmd[0] = cmd[0].replace('/', os.path.sep) 349 cmd = fix_python_path(cmd) 350 351 # We also want to print the GTEST env vars that were set by the caller, 352 # because you need them to reproduce the task properly. 353 env_to_print = extra_env.copy() 354 for env_var_name in ('GTEST_SHARD_INDEX', 'GTEST_TOTAL_SHARDS'): 355 if env_var_name in env: 356 env_to_print[env_var_name] = env[env_var_name] 357 358 print('Additional test environment:\n%s\n' 359 'Command: %s\n' % ( 360 '\n'.join(' %s=%s' % (k, v) 361 for k, v in sorted(env_to_print.items())), 362 ' '.join(cmd))) 363 sys.stdout.flush() 364 env.update(extra_env or {}) 365 try: 366 if stdoutfile: 367 # Write to stdoutfile and poll to produce terminal output. 368 return run_command_with_output(cmd, 369 env=env, 370 stdoutfile=stdoutfile, 371 cwd=cwd) 372 if use_symbolization_script: 373 # See above comment regarding offline symbolization. 374 # Need to pipe to the symbolizer script. 375 p1 = _popen(cmd, env=env, stdout=subprocess.PIPE, 376 cwd=cwd, stderr=sys.stdout) 377 p2 = _popen( 378 get_sanitizer_symbolize_command(executable_path=cmd[0]), 379 env=env, stdin=p1.stdout) 380 p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits. 381 forward_signals([p1, p2]) 382 wait_with_signals(p1) 383 wait_with_signals(p2) 384 # Also feed the out-of-band JSON output to the symbolizer script. 385 symbolize_snippets_in_json(cmd, env) 386 return p1.returncode 387 return run_command(cmd, env=env, cwd=cwd, log=False) 388 except OSError: 389 print('Failed to start %s' % cmd, file=sys.stderr) 390 raise 391 392 393def _popen(*args, **kwargs): 394 assert 'creationflags' not in kwargs 395 if sys.platform == 'win32': 396 # Necessary for signal handling. See crbug.com/733612#c6. 397 kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP 398 return subprocess.Popen(*args, **kwargs) 399 400 401def main(): 402 return run_executable(sys.argv[1:], os.environ.copy()) 403 404 405if __name__ == '__main__': 406 sys.exit(main()) 407