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 get_escalate_sanitizer_warnings_command(json_path): 175 """Construct the command to invoke sanitizer warnings script.""" 176 script_path = os.path.join( 177 ROOT_DIR, 'tools', 'memory', 'sanitizer', 178 'escalate_sanitizer_warnings.py') 179 cmd = [sys.executable, script_path] 180 cmd.append('--test-summary-json-file=%s' % json_path) 181 return cmd 182 183 184def escalate_sanitizer_warnings_in_json(cmd, env): 185 """Escalate sanitizer warnings inside the JSON test summary.""" 186 json_path = get_json_path(cmd) 187 if json_path is None: 188 print("Warning: Cannot escalate sanitizer warnings without a json summary " 189 "file:\n", file=sys.stderr) 190 return 0 191 192 try: 193 escalate_command = get_escalate_sanitizer_warnings_command(json_path) 194 p = subprocess.Popen(escalate_command, stderr=subprocess.PIPE, env=env) 195 (_, stderr) = p.communicate() 196 except OSError as e: 197 print('Exception while escalating sanitizer warnings: %s' % e, 198 file=sys.stderr) 199 raise 200 201 if p.returncode != 0: 202 print("Error: failed to escalate sanitizer warnings status in JSON:\n", 203 file=sys.stderr) 204 print(stderr, file=sys.stderr) 205 return p.returncode 206 207 208 209def run_command_with_output(argv, stdoutfile, env=None, cwd=None): 210 """Run command and stream its stdout/stderr to the console & |stdoutfile|. 211 212 Also forward_signals to obey 213 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 214 215 Returns: 216 integer returncode of the subprocess. 217 """ 218 print('Running %r in %r (env: %r)' % (argv, cwd, env), file=sys.stderr) 219 assert stdoutfile 220 with io.open(stdoutfile, 'wb') as writer, \ 221 io.open(stdoutfile, 'rb', 1) as reader: 222 process = _popen(argv, env=env, cwd=cwd, stdout=writer, 223 stderr=subprocess.STDOUT) 224 forward_signals([process]) 225 while process.poll() is None: 226 sys.stdout.write(reader.read().decode('utf-8')) 227 # This sleep is needed for signal propagation. See the 228 # wait_with_signals() docstring. 229 time.sleep(0.1) 230 # Read the remaining. 231 sys.stdout.write(reader.read().decode('utf-8')) 232 print('Command %r returned exit code %d' % (argv, process.returncode), 233 file=sys.stderr) 234 return process.returncode 235 236 237def run_command(argv, env=None, cwd=None, log=True): 238 """Run command and stream its stdout/stderr both to stdout. 239 240 Also forward_signals to obey 241 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 242 243 Returns: 244 integer returncode of the subprocess. 245 """ 246 if log: 247 print('Running %r in %r (env: %r)' % (argv, cwd, env), file=sys.stderr) 248 process = _popen(argv, env=env, cwd=cwd, stderr=subprocess.STDOUT) 249 forward_signals([process]) 250 exit_code = wait_with_signals(process) 251 if log: 252 print('Command returned exit code %d' % exit_code, file=sys.stderr) 253 return exit_code 254 255 256def run_command_output_to_handle(argv, file_handle, env=None, cwd=None): 257 """Run command and stream its stdout/stderr both to |file_handle|. 258 259 Also forward_signals to obey 260 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 261 262 Returns: 263 integer returncode of the subprocess. 264 """ 265 print('Running %r in %r (env: %r)' % (argv, cwd, env)) 266 process = _popen( 267 argv, env=env, cwd=cwd, stderr=file_handle, stdout=file_handle) 268 forward_signals([process]) 269 exit_code = wait_with_signals(process) 270 print('Command returned exit code %d' % exit_code) 271 return exit_code 272 273 274def wait_with_signals(process): 275 """A version of process.wait() that works cross-platform. 276 277 This version properly surfaces the SIGBREAK signal. 278 279 From reading the subprocess.py source code, it seems we need to explicitly 280 call time.sleep(). The reason is that subprocess.Popen.wait() on Windows 281 directly calls WaitForSingleObject(), but only time.sleep() properly surface 282 the SIGBREAK signal. 283 284 Refs: 285 https://github.com/python/cpython/blob/v2.7.15/Lib/subprocess.py#L692 286 https://github.com/python/cpython/blob/v2.7.15/Modules/timemodule.c#L1084 287 288 Returns: 289 returncode of the process. 290 """ 291 while process.poll() is None: 292 time.sleep(0.1) 293 return process.returncode 294 295 296def forward_signals(procs): 297 """Forwards unix's SIGTERM or win's CTRL_BREAK_EVENT to the given processes. 298 299 This plays nicely with swarming's timeout handling. See also 300 https://chromium.googlesource.com/infra/luci/luci-py/+/main/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance 301 302 Args: 303 procs: A list of subprocess.Popen objects representing child processes. 304 """ 305 assert all(isinstance(p, subprocess.Popen) for p in procs) 306 def _sig_handler(sig, _): 307 for p in procs: 308 if p.poll() is not None: 309 continue 310 # SIGBREAK is defined only for win32. 311 # pylint: disable=no-member 312 if sys.platform == 'win32' and sig == signal.SIGBREAK: 313 p.send_signal(signal.CTRL_BREAK_EVENT) 314 else: 315 print("Forwarding signal(%d) to process %d" % (sig, p.pid)) 316 p.send_signal(sig) 317 # pylint: enable=no-member 318 if sys.platform == 'win32': 319 signal.signal(signal.SIGBREAK, _sig_handler) # pylint: disable=no-member 320 else: 321 signal.signal(signal.SIGTERM, _sig_handler) 322 signal.signal(signal.SIGINT, _sig_handler) 323 324 325def run_executable(cmd, env, stdoutfile=None, cwd=None): 326 """Runs an executable with: 327 - CHROME_HEADLESS set to indicate that the test is running on a 328 bot and shouldn't do anything interactive like show modal dialogs. 329 - environment variable CR_SOURCE_ROOT set to the root directory. 330 - environment variable LANGUAGE to en_US.UTF-8. 331 - environment variable CHROME_DEVEL_SANDBOX set 332 - Reuses sys.executable automatically. 333 """ 334 extra_env = { 335 # Set to indicate that the executable is running non-interactively on 336 # a bot. 337 'CHROME_HEADLESS': '1', 338 339 # Many tests assume a English interface... 340 'LANG': 'en_US.UTF-8', 341 } 342 343 # Used by base/base_paths_linux.cc as an override. Just make sure the default 344 # logic is used. 345 env.pop('CR_SOURCE_ROOT', None) 346 347 # Copy logic from tools/build/scripts/slave/runtest.py. 348 asan = '--asan=1' in cmd 349 lsan = '--lsan=1' in cmd 350 msan = '--msan=1' in cmd 351 tsan = '--tsan=1' in cmd 352 cfi_diag = '--cfi-diag=1' in cmd 353 # Treat sanitizer warnings as test case failures. 354 use_sanitizer_warnings_script = '--fail-san=1' in cmd 355 if stdoutfile or sys.platform in ['win32', 'cygwin']: 356 # Symbolization works in-process on Windows even when sandboxed. 357 use_symbolization_script = False 358 else: 359 # If any sanitizer is enabled, we print unsymbolized stack trace 360 # that is required to run through symbolization script. 361 use_symbolization_script = (asan or msan or cfi_diag or lsan or tsan) 362 363 if asan or lsan or msan or tsan or cfi_diag: 364 extra_env.update(get_sanitizer_env(asan, lsan, msan, tsan, cfi_diag)) 365 366 if lsan or tsan: 367 # LSan and TSan are not sandbox-friendly. 368 cmd.append('--no-sandbox') 369 370 # Enable clang code coverage continuous mode. 371 if '--coverage-continuous-mode=1' in cmd: 372 extra_env.update(get_coverage_continuous_mode_env(env)) 373 374 # pylint: disable=import-outside-toplevel 375 if '--skip-set-lpac-acls=1' not in cmd and sys.platform == 'win32': 376 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 377 'scripts')) 378 from scripts import common 379 common.set_lpac_acls(ROOT_DIR, is_test_script=True) 380 # pylint: enable=import-outside-toplevel 381 382 cmd = trim_cmd(cmd) 383 384 # Ensure paths are correctly separated on windows. 385 cmd[0] = cmd[0].replace('/', os.path.sep) 386 cmd = fix_python_path(cmd) 387 388 # We also want to print the GTEST env vars that were set by the caller, 389 # because you need them to reproduce the task properly. 390 env_to_print = extra_env.copy() 391 for env_var_name in ('GTEST_SHARD_INDEX', 'GTEST_TOTAL_SHARDS'): 392 if env_var_name in env: 393 env_to_print[env_var_name] = env[env_var_name] 394 395 print('Additional test environment:\n%s\n' 396 'Command: %s\n' % ( 397 '\n'.join(' %s=%s' % (k, v) 398 for k, v in sorted(env_to_print.items())), 399 ' '.join(cmd))) 400 sys.stdout.flush() 401 env.update(extra_env or {}) 402 try: 403 if stdoutfile: 404 # Write to stdoutfile and poll to produce terminal output. 405 return run_command_with_output(cmd, 406 env=env, 407 stdoutfile=stdoutfile, 408 cwd=cwd) 409 if use_symbolization_script: 410 # See above comment regarding offline symbolization. 411 # Need to pipe to the symbolizer script. 412 p1 = _popen(cmd, env=env, stdout=subprocess.PIPE, 413 cwd=cwd, stderr=sys.stdout) 414 p2 = _popen( 415 get_sanitizer_symbolize_command(executable_path=cmd[0]), 416 env=env, stdin=p1.stdout) 417 p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits. 418 forward_signals([p1, p2]) 419 wait_with_signals(p1) 420 wait_with_signals(p2) 421 # Also feed the out-of-band JSON output to the symbolizer script. 422 symbolize_snippets_in_json(cmd, env) 423 returncode = p1.returncode 424 else: 425 returncode = run_command(cmd, env=env, cwd=cwd, log=False) 426 # Check if we should post-process sanitizer warnings. 427 if use_sanitizer_warnings_script: 428 escalate_returncode = escalate_sanitizer_warnings_in_json(cmd, env) 429 if not returncode and escalate_returncode: 430 print('Tests with sanitizer warnings led to task failure.') 431 returncode = escalate_returncode 432 return returncode 433 except OSError: 434 print('Failed to start %s' % cmd, file=sys.stderr) 435 raise 436 437 438def _popen(*args, **kwargs): 439 assert 'creationflags' not in kwargs 440 if sys.platform == 'win32': 441 # Necessary for signal handling. See crbug.com/733612#c6. 442 kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP 443 return subprocess.Popen(*args, **kwargs) 444 445 446def main(): 447 return run_executable(sys.argv[1:], os.environ.copy()) 448 449 450if __name__ == '__main__': 451 sys.exit(main()) 452