• 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
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