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