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