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