• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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