1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import contextlib
7import collections
8import itertools
9import logging
10import math
11import os
12import posixpath
13import subprocess
14import shutil
15import time
16
17from six.moves import range  # pylint: disable=redefined-builtin
18from devil import base_error
19from devil.android import crash_handler
20from devil.android import device_errors
21from devil.android import device_temp_file
22from devil.android import logcat_monitor
23from devil.android import ports
24from devil.android.sdk import version_codes
25from devil.utils import reraiser_thread
26from incremental_install import installer
27from pylib import constants
28from pylib.base import base_test_result
29from pylib.gtest import gtest_test_instance
30from pylib.local import local_test_server_spawner
31from pylib.local.device import local_device_environment
32from pylib.local.device import local_device_test_run
33from pylib.utils import google_storage_helper
34from pylib.utils import logdog_helper
35from py_trace_event import trace_event
36from py_utils import contextlib_ext
37from py_utils import tempfile_ext
38import tombstones
39
40_MAX_INLINE_FLAGS_LENGTH = 50  # Arbitrarily chosen.
41_EXTRA_COMMAND_LINE_FILE = (
42    'org.chromium.native_test.NativeTest.CommandLineFile')
43_EXTRA_COMMAND_LINE_FLAGS = (
44    'org.chromium.native_test.NativeTest.CommandLineFlags')
45_EXTRA_COVERAGE_DEVICE_FILE = (
46    'org.chromium.native_test.NativeTest.CoverageDeviceFile')
47_EXTRA_STDOUT_FILE = (
48    'org.chromium.native_test.NativeTestInstrumentationTestRunner'
49        '.StdoutFile')
50_EXTRA_TEST = (
51    'org.chromium.native_test.NativeTestInstrumentationTestRunner'
52        '.Test')
53_EXTRA_TEST_LIST = (
54    'org.chromium.native_test.NativeTestInstrumentationTestRunner'
55        '.TestList')
56
57_SECONDS_TO_NANOS = int(1e9)
58
59# Tests that use SpawnedTestServer must run the LocalTestServerSpawner on the
60# host machine.
61# TODO(jbudorick): Move this up to the test instance if the net test server is
62# handled outside of the APK for the remote_device environment.
63_SUITE_REQUIRES_TEST_SERVER_SPAWNER = [
64  'components_browsertests', 'content_unittests', 'content_browsertests',
65  'net_unittests', 'services_unittests', 'unit_tests'
66]
67
68# These are use for code coverage.
69_LLVM_PROFDATA_PATH = os.path.join(constants.DIR_SOURCE_ROOT, 'third_party',
70                                   'llvm-build', 'Release+Asserts', 'bin',
71                                   'llvm-profdata')
72# Name of the file extension for profraw data files.
73_PROFRAW_FILE_EXTENSION = 'profraw'
74# Name of the file where profraw data files are merged.
75_MERGE_PROFDATA_FILE_NAME = 'coverage_merged.' + _PROFRAW_FILE_EXTENSION
76
77# No-op context manager. If we used Python 3, we could change this to
78# contextlib.ExitStack()
79class _NullContextManager(object):
80  def __enter__(self):
81    pass
82  def __exit__(self, *args):
83    pass
84
85
86def _GenerateSequentialFileNames(filename):
87  """Infinite generator of names: 'name.ext', 'name_1.ext', 'name_2.ext', ..."""
88  yield filename
89  base, ext = os.path.splitext(filename)
90  for i in itertools.count(1):
91    yield '%s_%d%s' % (base, i, ext)
92
93
94def _ExtractTestsFromFilter(gtest_filter):
95  """Returns the list of tests specified by the given filter.
96
97  Returns:
98    None if the device should be queried for the test list instead.
99  """
100  # Empty means all tests, - means exclude filter.
101  if not gtest_filter or '-' in gtest_filter:
102    return None
103
104  patterns = gtest_filter.split(':')
105  # For a single pattern, allow it even if it has a wildcard so long as the
106  # wildcard comes at the end and there is at least one . to prove the scope is
107  # not too large.
108  # This heuristic is not necessarily faster, but normally is.
109  if len(patterns) == 1 and patterns[0].endswith('*'):
110    no_suffix = patterns[0].rstrip('*')
111    if '*' not in no_suffix and '.' in no_suffix:
112      return patterns
113
114  if '*' in gtest_filter:
115    return None
116  return patterns
117
118
119def _GetDeviceTimeoutMultiplier():
120  # Emulated devices typically run 20-150x slower than real-time.
121  # Give a way to control this through the DEVICE_TIMEOUT_MULTIPLIER
122  # environment variable.
123  multiplier = os.getenv("DEVICE_TIMEOUT_MULTIPLIER")
124  if multiplier:
125    return int(multiplier)
126  return 1
127
128
129def _MergeCoverageFiles(coverage_dir, profdata_dir):
130  """Merge coverage data files.
131
132  Each instrumentation activity generates a separate profraw data file. This
133  merges all profraw files in profdata_dir into a single file in
134  coverage_dir. This happens after each test, rather than waiting until after
135  all tests are ran to reduce the memory footprint used by all the profraw
136  files.
137
138  Args:
139    coverage_dir: The path to the coverage directory.
140    profdata_dir: The directory where the profraw data file(s) are located.
141
142  Return:
143    None
144  """
145  # profdata_dir may not exist if pulling coverage files failed.
146  if not os.path.exists(profdata_dir):
147    logging.debug('Profraw directory does not exist.')
148    return
149
150  merge_file = os.path.join(coverage_dir, _MERGE_PROFDATA_FILE_NAME)
151  profraw_files = [
152      os.path.join(profdata_dir, f) for f in os.listdir(profdata_dir)
153      if f.endswith(_PROFRAW_FILE_EXTENSION)
154  ]
155
156  try:
157    logging.debug('Merging target profraw files into merged profraw file.')
158    subprocess_cmd = [
159        _LLVM_PROFDATA_PATH,
160        'merge',
161        '-o',
162        merge_file,
163        '-sparse=true',
164    ]
165    # Grow the merge file by merging it with itself and the new files.
166    if os.path.exists(merge_file):
167      subprocess_cmd.append(merge_file)
168    subprocess_cmd.extend(profraw_files)
169    output = subprocess.check_output(subprocess_cmd)
170    logging.debug('Merge output: %s', output)
171  except subprocess.CalledProcessError:
172    # Don't raise error as that will kill the test run. When code coverage
173    # generates a report, that will raise the error in the report generation.
174    logging.error(
175        'Failed to merge target profdata files to create merged profraw file.')
176
177  # Free up memory space on bot as all data is in the merge file.
178  for f in profraw_files:
179    os.remove(f)
180
181
182def _PullCoverageFiles(device, device_coverage_dir, output_dir):
183  """Pulls coverage files on device to host directory.
184
185  Args:
186    device: The working device.
187    device_coverage_dir: The directory to store coverage data on device.
188    output_dir: The output directory on host.
189  """
190  try:
191    if not os.path.exists(output_dir):
192      os.makedirs(output_dir)
193    device.PullFile(device_coverage_dir, output_dir)
194    if not os.listdir(os.path.join(output_dir, 'profraw')):
195      logging.warning('No coverage data was generated for this run')
196  except (OSError, base_error.BaseError) as e:
197    logging.warning('Failed to handle coverage data after tests: %s', e)
198  finally:
199    device.RemovePath(device_coverage_dir, force=True, recursive=True)
200
201
202def _GetDeviceCoverageDir(device):
203  """Gets the directory to generate coverage data on device.
204
205  Args:
206    device: The working device.
207
208  Returns:
209    The directory path on the device.
210  """
211  return posixpath.join(device.GetExternalStoragePath(), 'chrome', 'test',
212                        'coverage', 'profraw')
213
214
215def _GetLLVMProfilePath(device_coverage_dir, suite, coverage_index):
216  """Gets 'LLVM_PROFILE_FILE' environment variable path.
217
218  Dumping data to ONLY 1 file may cause warning and data overwrite in
219  browsertests, so that pattern "%2m" is used to expand to 2 raw profiles
220  at runtime.
221
222  Args:
223    device_coverage_dir: The directory to generate data on device.
224    suite: Test suite name.
225    coverage_index: The incremental index for this test suite.
226
227  Returns:
228    The path pattern for environment variable 'LLVM_PROFILE_FILE'.
229  """
230  return posixpath.join(device_coverage_dir,
231                        '_'.join([suite,
232                                  str(coverage_index), '%2m.profraw']))
233
234
235class _ApkDelegate(object):
236  def __init__(self, test_instance, tool):
237    self._activity = test_instance.activity
238    self._apk_helper = test_instance.apk_helper
239    self._test_apk_incremental_install_json = (
240        test_instance.test_apk_incremental_install_json)
241    self._package = test_instance.package
242    self._runner = test_instance.runner
243    self._permissions = test_instance.permissions
244    self._suite = test_instance.suite
245    self._component = '%s/%s' % (self._package, self._runner)
246    self._extras = test_instance.extras
247    self._wait_for_java_debugger = test_instance.wait_for_java_debugger
248    self._tool = tool
249    self._coverage_dir = test_instance.coverage_dir
250    self._coverage_index = 0
251    self._use_existing_test_data = test_instance.use_existing_test_data
252
253  def GetTestDataRoot(self, device):
254    # pylint: disable=no-self-use
255    return posixpath.join(device.GetExternalStoragePath(),
256                          'chromium_tests_root')
257
258  def Install(self, device):
259    if self._use_existing_test_data:
260      return
261    if self._test_apk_incremental_install_json:
262      installer.Install(device, self._test_apk_incremental_install_json,
263                        apk=self._apk_helper, permissions=self._permissions)
264    else:
265      device.Install(
266          self._apk_helper,
267          allow_downgrade=True,
268          reinstall=True,
269          permissions=self._permissions)
270
271  def ResultsDirectory(self, device):
272    return device.GetApplicationDataDirectory(self._package)
273
274  def Run(self, test, device, flags=None, **kwargs):
275    extras = dict(self._extras)
276    device_api = device.build_version_sdk
277
278    if self._coverage_dir and device_api >= version_codes.LOLLIPOP:
279      device_coverage_dir = _GetDeviceCoverageDir(device)
280      extras[_EXTRA_COVERAGE_DEVICE_FILE] = _GetLLVMProfilePath(
281          device_coverage_dir, self._suite, self._coverage_index)
282      self._coverage_index += 1
283
284    if ('timeout' in kwargs
285        and gtest_test_instance.EXTRA_SHARD_NANO_TIMEOUT not in extras):
286      # Make sure the instrumentation doesn't kill the test before the
287      # scripts do. The provided timeout value is in seconds, but the
288      # instrumentation deals with nanoseconds because that's how Android
289      # handles time.
290      extras[gtest_test_instance.EXTRA_SHARD_NANO_TIMEOUT] = int(
291          kwargs['timeout'] * _SECONDS_TO_NANOS)
292
293    # pylint: disable=redefined-variable-type
294    command_line_file = _NullContextManager()
295    if flags:
296      if len(flags) > _MAX_INLINE_FLAGS_LENGTH:
297        command_line_file = device_temp_file.DeviceTempFile(device.adb)
298        device.WriteFile(command_line_file.name, '_ %s' % flags)
299        extras[_EXTRA_COMMAND_LINE_FILE] = command_line_file.name
300      else:
301        extras[_EXTRA_COMMAND_LINE_FLAGS] = flags
302
303    test_list_file = _NullContextManager()
304    if test:
305      if len(test) > 1:
306        test_list_file = device_temp_file.DeviceTempFile(device.adb)
307        device.WriteFile(test_list_file.name, '\n'.join(test))
308        extras[_EXTRA_TEST_LIST] = test_list_file.name
309      else:
310        extras[_EXTRA_TEST] = test[0]
311    # pylint: enable=redefined-variable-type
312
313    # We need to use GetAppWritablePath here instead of GetExternalStoragePath
314    # since we will not have yet applied legacy storage permission workarounds
315    # on R+.
316    stdout_file = device_temp_file.DeviceTempFile(
317        device.adb, dir=device.GetAppWritablePath(), suffix='.gtest_out')
318    extras[_EXTRA_STDOUT_FILE] = stdout_file.name
319
320    if self._wait_for_java_debugger:
321      cmd = ['am', 'set-debug-app', '-w', self._package]
322      device.RunShellCommand(cmd, check_return=True)
323      logging.warning('*' * 80)
324      logging.warning('Waiting for debugger to attach to process: %s',
325                      self._package)
326      logging.warning('*' * 80)
327
328    with command_line_file, test_list_file, stdout_file:
329      try:
330        device.StartInstrumentation(
331            self._component, extras=extras, raw=False, **kwargs)
332      except device_errors.CommandFailedError:
333        logging.exception('gtest shard failed.')
334      except device_errors.CommandTimeoutError:
335        logging.exception('gtest shard timed out.')
336      except device_errors.DeviceUnreachableError:
337        logging.exception('gtest shard device unreachable.')
338      except Exception:
339        device.ForceStop(self._package)
340        raise
341      finally:
342        if self._coverage_dir and device_api >= version_codes.LOLLIPOP:
343          if not os.path.isdir(self._coverage_dir):
344            os.makedirs(self._coverage_dir)
345        # TODO(crbug.com/1179004) Use _MergeCoverageFiles when llvm-profdata
346        # not found is fixed.
347          _PullCoverageFiles(
348              device, device_coverage_dir,
349              os.path.join(self._coverage_dir, str(self._coverage_index)))
350
351      return device.ReadFile(stdout_file.name).splitlines()
352
353  def PullAppFiles(self, device, files, directory):
354    device_dir = device.GetApplicationDataDirectory(self._package)
355    host_dir = os.path.join(directory, str(device))
356    for f in files:
357      device_file = posixpath.join(device_dir, f)
358      host_file = os.path.join(host_dir, *f.split(posixpath.sep))
359      for host_file in _GenerateSequentialFileNames(host_file):
360        if not os.path.exists(host_file):
361          break
362      device.PullFile(device_file, host_file)
363
364  def Clear(self, device):
365    device.ClearApplicationState(self._package, permissions=self._permissions)
366
367
368class _ExeDelegate(object):
369
370  def __init__(self, tr, test_instance, tool):
371    self._host_dist_dir = test_instance.exe_dist_dir
372    self._exe_file_name = os.path.basename(
373        test_instance.exe_dist_dir)[:-len('__dist')]
374    self._device_dist_dir = posixpath.join(
375        constants.TEST_EXECUTABLE_DIR,
376        os.path.basename(test_instance.exe_dist_dir))
377    self._test_run = tr
378    self._tool = tool
379    self._suite = test_instance.suite
380    self._coverage_dir = test_instance.coverage_dir
381    self._coverage_index = 0
382
383  def GetTestDataRoot(self, device):
384    # pylint: disable=no-self-use
385    # pylint: disable=unused-argument
386    return posixpath.join(constants.TEST_EXECUTABLE_DIR, 'chromium_tests_root')
387
388  def Install(self, device):
389    # TODO(jbudorick): Look into merging this with normal data deps pushing if
390    # executables become supported on nonlocal environments.
391    device.PushChangedFiles([(self._host_dist_dir, self._device_dist_dir)],
392                            delete_device_stale=True)
393
394  def ResultsDirectory(self, device):
395    # pylint: disable=no-self-use
396    # pylint: disable=unused-argument
397    return constants.TEST_EXECUTABLE_DIR
398
399  def Run(self, test, device, flags=None, **kwargs):
400    tool = self._test_run.GetTool(device).GetTestWrapper()
401    if tool:
402      cmd = [tool]
403    else:
404      cmd = []
405    cmd.append(posixpath.join(self._device_dist_dir, self._exe_file_name))
406
407    if test:
408      cmd.append('--gtest_filter=%s' % ':'.join(test))
409    if flags:
410      # TODO(agrieve): This won't work if multiple flags are passed.
411      cmd.append(flags)
412    cwd = constants.TEST_EXECUTABLE_DIR
413
414    env = {
415      'LD_LIBRARY_PATH': self._device_dist_dir
416    }
417
418    if self._coverage_dir:
419      device_coverage_dir = _GetDeviceCoverageDir(device)
420      env['LLVM_PROFILE_FILE'] = _GetLLVMProfilePath(
421          device_coverage_dir, self._suite, self._coverage_index)
422      self._coverage_index += 1
423
424    if self._tool != 'asan':
425      env['UBSAN_OPTIONS'] = constants.UBSAN_OPTIONS
426
427    try:
428      gcov_strip_depth = os.environ['NATIVE_COVERAGE_DEPTH_STRIP']
429      external = device.GetExternalStoragePath()
430      env['GCOV_PREFIX'] = '%s/gcov' % external
431      env['GCOV_PREFIX_STRIP'] = gcov_strip_depth
432    except (device_errors.CommandFailedError, KeyError):
433      pass
434
435    # Executable tests return a nonzero exit code on test failure, which is
436    # fine from the test runner's perspective; thus check_return=False.
437    output = device.RunShellCommand(
438        cmd, cwd=cwd, env=env, check_return=False, large_output=True, **kwargs)
439
440    if self._coverage_dir:
441      _PullCoverageFiles(
442          device, device_coverage_dir,
443          os.path.join(self._coverage_dir, str(self._coverage_index)))
444
445    return output
446
447  def PullAppFiles(self, device, files, directory):
448    pass
449
450  def Clear(self, device):
451    device.KillAll(self._exe_file_name,
452                   blocking=True,
453                   timeout=30 * _GetDeviceTimeoutMultiplier(),
454                   quiet=True)
455
456
457class LocalDeviceGtestRun(local_device_test_run.LocalDeviceTestRun):
458
459  def __init__(self, env, test_instance):
460    assert isinstance(env, local_device_environment.LocalDeviceEnvironment)
461    assert isinstance(test_instance, gtest_test_instance.GtestTestInstance)
462    super(LocalDeviceGtestRun, self).__init__(env, test_instance)
463
464    if self._test_instance.apk_helper:
465      self._installed_packages = [
466          self._test_instance.apk_helper.GetPackageName()
467      ]
468
469    # pylint: disable=redefined-variable-type
470    if self._test_instance.apk:
471      self._delegate = _ApkDelegate(self._test_instance, env.tool)
472    elif self._test_instance.exe_dist_dir:
473      self._delegate = _ExeDelegate(self, self._test_instance, self._env.tool)
474    if self._test_instance.isolated_script_test_perf_output:
475      self._test_perf_output_filenames = _GenerateSequentialFileNames(
476          self._test_instance.isolated_script_test_perf_output)
477    else:
478      self._test_perf_output_filenames = itertools.repeat(None)
479    # pylint: enable=redefined-variable-type
480    self._crashes = set()
481    self._servers = collections.defaultdict(list)
482
483  #override
484  def TestPackage(self):
485    return self._test_instance.suite
486
487  #override
488  def SetUp(self):
489    @local_device_environment.handle_shard_failures_with(
490        on_failure=self._env.DenylistDevice)
491    @trace_event.traced
492    def individual_device_set_up(device, host_device_tuples):
493      def install_apk(dev):
494        # Install test APK.
495        self._delegate.Install(dev)
496
497      def push_test_data(dev):
498        if self._test_instance.use_existing_test_data:
499          return
500        # Push data dependencies.
501        device_root = self._delegate.GetTestDataRoot(dev)
502        host_device_tuples_substituted = [
503            (h, local_device_test_run.SubstituteDeviceRoot(d, device_root))
504            for h, d in host_device_tuples]
505        local_device_environment.place_nomedia_on_device(dev, device_root)
506        dev.PushChangedFiles(
507            host_device_tuples_substituted,
508            delete_device_stale=True,
509            # Some gtest suites, e.g. unit_tests, have data dependencies that
510            # can take longer than the default timeout to push. See
511            # crbug.com/791632 for context.
512            timeout=600 * math.ceil(_GetDeviceTimeoutMultiplier() / 10))
513        if not host_device_tuples:
514          dev.RemovePath(device_root, force=True, recursive=True, rename=True)
515          dev.RunShellCommand(['mkdir', '-p', device_root], check_return=True)
516
517      def init_tool_and_start_servers(dev):
518        tool = self.GetTool(dev)
519        tool.CopyFiles(dev)
520        tool.SetupEnvironment()
521
522        try:
523          # See https://crbug.com/1030827.
524          # This is a hack that may break in the future. We're relying on the
525          # fact that adb doesn't use ipv6 for it's server, and so doesn't
526          # listen on ipv6, but ssh remote forwarding does. 5037 is the port
527          # number adb uses for its server.
528          if "[::1]:5037" in subprocess.check_output(
529              "ss -o state listening 'sport = 5037'", shell=True):
530            logging.error(
531                'Test Server cannot be started with a remote-forwarded adb '
532                'server. Continuing anyways, but some tests may fail.')
533            return
534        except subprocess.CalledProcessError:
535          pass
536
537        self._servers[str(dev)] = []
538        if self.TestPackage() in _SUITE_REQUIRES_TEST_SERVER_SPAWNER:
539          self._servers[str(dev)].append(
540              local_test_server_spawner.LocalTestServerSpawner(
541                  ports.AllocateTestServerPort(), dev, tool))
542
543        for s in self._servers[str(dev)]:
544          s.SetUp()
545
546      def bind_crash_handler(step, dev):
547        return lambda: crash_handler.RetryOnSystemCrash(step, dev)
548
549      # Explicitly enable root to ensure that tests run under deterministic
550      # conditions. Without this explicit call, EnableRoot() is called from
551      # push_test_data() when PushChangedFiles() determines that it should use
552      # _PushChangedFilesZipped(), which is only most of the time.
553      # Root is required (amongst maybe other reasons) to pull the results file
554      # from the device, since it lives within the application's data directory
555      # (via GetApplicationDataDirectory()).
556      device.EnableRoot()
557
558      steps = [
559          bind_crash_handler(s, device)
560          for s in (install_apk, push_test_data, init_tool_and_start_servers)]
561      if self._env.concurrent_adb:
562        reraiser_thread.RunAsync(steps)
563      else:
564        for step in steps:
565          step()
566
567    self._env.parallel_devices.pMap(
568        individual_device_set_up,
569        self._test_instance.GetDataDependencies())
570
571  #override
572  def _ShouldShard(self):
573    return True
574
575  #override
576  def _CreateShards(self, tests):
577    # _crashes are tests that might crash and make the tests in the same shard
578    # following the crashed testcase not run.
579    # Thus we need to create separate shards for each crashed testcase,
580    # so that other tests can be run.
581    device_count = len(self._env.devices)
582    shards = []
583
584    # Add shards with only one suspect testcase.
585    shards += [[crash] for crash in self._crashes if crash in tests]
586
587    # Delete suspect testcase from tests.
588    tests = [test for test in tests if not test in self._crashes]
589
590    max_shard_size = self._test_instance.test_launcher_batch_limit
591
592    shards.extend(self._PartitionTests(tests, device_count, max_shard_size))
593    return shards
594
595  #override
596  def _GetTests(self):
597    if self._test_instance.extract_test_list_from_filter:
598      # When the exact list of tests to run is given via command-line (e.g. when
599      # locally iterating on a specific test), skip querying the device (which
600      # takes ~3 seconds).
601      tests = _ExtractTestsFromFilter(self._test_instance.gtest_filter)
602      if tests:
603        return tests
604
605    # Even when there's only one device, it still makes sense to retrieve the
606    # test list so that tests can be split up and run in batches rather than all
607    # at once (since test output is not streamed).
608    @local_device_environment.handle_shard_failures_with(
609        on_failure=self._env.DenylistDevice)
610    def list_tests(dev):
611      timeout = 30 * _GetDeviceTimeoutMultiplier()
612      retries = 1
613      if self._test_instance.wait_for_java_debugger:
614        timeout = None
615
616      flags = [
617          f for f in self._test_instance.flags
618          if f not in ['--wait-for-debugger', '--wait-for-java-debugger']
619      ]
620      flags.append('--gtest_list_tests')
621
622      # TODO(crbug.com/726880): Remove retries when no longer necessary.
623      for i in range(0, retries+1):
624        logging.info('flags:')
625        for f in flags:
626          logging.info('  %s', f)
627
628        with self._ArchiveLogcat(dev, 'list_tests'):
629          raw_test_list = crash_handler.RetryOnSystemCrash(
630              lambda d: self._delegate.Run(
631                  None, d, flags=' '.join(flags), timeout=timeout),
632              device=dev)
633
634        tests = gtest_test_instance.ParseGTestListTests(raw_test_list)
635        if not tests:
636          logging.info('No tests found. Output:')
637          for l in raw_test_list:
638            logging.info('  %s', l)
639          if i < retries:
640            logging.info('Retrying...')
641        else:
642          break
643      return tests
644
645    # Query all devices in case one fails.
646    test_lists = self._env.parallel_devices.pMap(list_tests).pGet(None)
647
648    # If all devices failed to list tests, raise an exception.
649    # Check that tl is not None and is not empty.
650    if all(not tl for tl in test_lists):
651      raise device_errors.CommandFailedError(
652          'Failed to list tests on any device')
653    tests = list(sorted(set().union(*[set(tl) for tl in test_lists if tl])))
654    tests = self._test_instance.FilterTests(tests)
655    tests = self._ApplyExternalSharding(
656        tests, self._test_instance.external_shard_index,
657        self._test_instance.total_external_shards)
658    return tests
659
660  def _UploadTestArtifacts(self, device, test_artifacts_dir):
661    # TODO(jbudorick): Reconcile this with the output manager once
662    # https://codereview.chromium.org/2933993002/ lands.
663    if test_artifacts_dir:
664      with tempfile_ext.NamedTemporaryDirectory() as test_artifacts_host_dir:
665        device.PullFile(test_artifacts_dir.name, test_artifacts_host_dir)
666        with tempfile_ext.NamedTemporaryDirectory() as temp_zip_dir:
667          zip_base_name = os.path.join(temp_zip_dir, 'test_artifacts')
668          test_artifacts_zip = shutil.make_archive(
669              zip_base_name, 'zip', test_artifacts_host_dir)
670          link = google_storage_helper.upload(
671              google_storage_helper.unique_name(
672                  'test_artifacts', device=device),
673              test_artifacts_zip,
674              bucket='%s/test_artifacts' % (
675                  self._test_instance.gs_test_artifacts_bucket))
676          logging.info('Uploading test artifacts to %s.', link)
677          return link
678    return None
679
680  def _PullRenderTestOutput(self, device, render_test_output_device_dir):
681    # We pull the render tests into a temp directory then copy them over
682    # individually. Otherwise we end up with a temporary directory name
683    # in the host output directory.
684    with tempfile_ext.NamedTemporaryDirectory() as tmp_host_dir:
685      try:
686        device.PullFile(render_test_output_device_dir, tmp_host_dir)
687      except device_errors.CommandFailedError:
688        logging.exception('Failed to pull render test output dir %s',
689                          render_test_output_device_dir)
690      temp_host_dir = os.path.join(
691          tmp_host_dir, os.path.basename(render_test_output_device_dir))
692      for output_file in os.listdir(temp_host_dir):
693        src_path = os.path.join(temp_host_dir, output_file)
694        dst_path = os.path.join(self._test_instance.render_test_output_dir,
695                                output_file)
696        shutil.move(src_path, dst_path)
697
698  @contextlib.contextmanager
699  def _ArchiveLogcat(self, device, test):
700    if isinstance(test, str):
701      desc = test
702    else:
703      desc = hash(tuple(test))
704
705    stream_name = 'logcat_%s_shard%s_%s_%s' % (
706        desc, self._test_instance.external_shard_index,
707        time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), device.serial)
708
709    logcat_file = None
710    logmon = None
711    try:
712      with self._env.output_manager.ArchivedTempfile(stream_name,
713                                                     'logcat') as logcat_file:
714        with logcat_monitor.LogcatMonitor(
715            device.adb,
716            filter_specs=local_device_environment.LOGCAT_FILTERS,
717            output_file=logcat_file.name,
718            check_error=False) as logmon:
719          with contextlib_ext.Optional(trace_event.trace(str(test)),
720                                       self._env.trace_output):
721            yield logcat_file
722    finally:
723      if logmon:
724        logmon.Close()
725      if logcat_file and logcat_file.Link():
726        logging.info('Logcat saved to %s', logcat_file.Link())
727
728  #override
729  def _RunTest(self, device, test):
730    # Run the test.
731    timeout = (self._test_instance.shard_timeout *
732               self.GetTool(device).GetTimeoutScale() *
733               _GetDeviceTimeoutMultiplier())
734    if self._test_instance.wait_for_java_debugger:
735      timeout = None
736    if self._test_instance.store_tombstones:
737      tombstones.ClearAllTombstones(device)
738    test_perf_output_filename = next(self._test_perf_output_filenames)
739
740    if self._test_instance.isolated_script_test_output:
741      suffix = '.json'
742    else:
743      suffix = '.xml'
744
745    with device_temp_file.DeviceTempFile(
746        adb=device.adb,
747        dir=self._delegate.ResultsDirectory(device),
748        suffix=suffix) as device_tmp_results_file:
749      with contextlib_ext.Optional(
750          device_temp_file.NamedDeviceTemporaryDirectory(
751              adb=device.adb, dir='/sdcard/'),
752          self._test_instance.gs_test_artifacts_bucket) as test_artifacts_dir:
753        with (contextlib_ext.Optional(
754            device_temp_file.DeviceTempFile(
755                adb=device.adb, dir=self._delegate.ResultsDirectory(device)),
756            test_perf_output_filename)) as isolated_script_test_perf_output:
757          with contextlib_ext.Optional(
758              device_temp_file.NamedDeviceTemporaryDirectory(adb=device.adb,
759                                                             dir='/sdcard/'),
760              self._test_instance.render_test_output_dir
761          ) as render_test_output_dir:
762
763            flags = list(self._test_instance.flags)
764            if self._test_instance.enable_xml_result_parsing:
765              flags.append('--gtest_output=xml:%s' %
766                           device_tmp_results_file.name)
767
768            if self._test_instance.gs_test_artifacts_bucket:
769              flags.append('--test_artifacts_dir=%s' % test_artifacts_dir.name)
770
771            if self._test_instance.isolated_script_test_output:
772              flags.append('--isolated-script-test-output=%s' %
773                           device_tmp_results_file.name)
774
775            if test_perf_output_filename:
776              flags.append('--isolated_script_test_perf_output=%s' %
777                           isolated_script_test_perf_output.name)
778
779            if self._test_instance.render_test_output_dir:
780              flags.append('--render-test-output-dir=%s' %
781                           render_test_output_dir.name)
782
783            logging.info('flags:')
784            for f in flags:
785              logging.info('  %s', f)
786
787            with self._ArchiveLogcat(device, test) as logcat_file:
788              output = self._delegate.Run(test,
789                                          device,
790                                          flags=' '.join(flags),
791                                          timeout=timeout,
792                                          retries=0)
793
794            if self._test_instance.enable_xml_result_parsing:
795              try:
796                gtest_xml = device.ReadFile(device_tmp_results_file.name)
797              except device_errors.CommandFailedError:
798                logging.exception('Failed to pull gtest results XML file %s',
799                                  device_tmp_results_file.name)
800                gtest_xml = None
801
802            if self._test_instance.isolated_script_test_output:
803              try:
804                gtest_json = device.ReadFile(device_tmp_results_file.name)
805              except device_errors.CommandFailedError:
806                logging.exception('Failed to pull gtest results JSON file %s',
807                                  device_tmp_results_file.name)
808                gtest_json = None
809
810            if test_perf_output_filename:
811              try:
812                device.PullFile(isolated_script_test_perf_output.name,
813                                test_perf_output_filename)
814              except device_errors.CommandFailedError:
815                logging.exception('Failed to pull chartjson results %s',
816                                  isolated_script_test_perf_output.name)
817
818            test_artifacts_url = self._UploadTestArtifacts(
819                device, test_artifacts_dir)
820
821            if render_test_output_dir:
822              self._PullRenderTestOutput(device, render_test_output_dir.name)
823
824    for s in self._servers[str(device)]:
825      s.Reset()
826    if self._test_instance.app_files:
827      self._delegate.PullAppFiles(device, self._test_instance.app_files,
828                                  self._test_instance.app_file_dir)
829    if not self._env.skip_clear_data:
830      self._delegate.Clear(device)
831
832    for l in output:
833      logging.info(l)
834
835    # Parse the output.
836    # TODO(jbudorick): Transition test scripts away from parsing stdout.
837    if self._test_instance.enable_xml_result_parsing:
838      results = gtest_test_instance.ParseGTestXML(gtest_xml)
839    elif self._test_instance.isolated_script_test_output:
840      results = gtest_test_instance.ParseGTestJSON(gtest_json)
841    else:
842      results = gtest_test_instance.ParseGTestOutput(
843          output, self._test_instance.symbolizer, device.product_cpu_abi)
844
845    tombstones_url = None
846    for r in results:
847      if logcat_file:
848        r.SetLink('logcat', logcat_file.Link())
849
850      if self._test_instance.gs_test_artifacts_bucket:
851        r.SetLink('test_artifacts', test_artifacts_url)
852
853      if r.GetType() == base_test_result.ResultType.CRASH:
854        self._crashes.add(r.GetName())
855        if self._test_instance.store_tombstones:
856          if not tombstones_url:
857            resolved_tombstones = tombstones.ResolveTombstones(
858                device,
859                resolve_all_tombstones=True,
860                include_stack_symbols=False,
861                wipe_tombstones=True)
862            stream_name = 'tombstones_%s_%s' % (
863                time.strftime('%Y%m%dT%H%M%S', time.localtime()),
864                device.serial)
865            tombstones_url = logdog_helper.text(
866                stream_name, '\n'.join(resolved_tombstones))
867          r.SetLink('tombstones', tombstones_url)
868
869    tests_stripped_disabled_prefix = set()
870    for t in test:
871      tests_stripped_disabled_prefix.add(
872          gtest_test_instance.TestNameWithoutDisabledPrefix(t))
873    not_run_tests = tests_stripped_disabled_prefix.difference(
874        set(r.GetName() for r in results))
875    return results, list(not_run_tests) if results else None
876
877  #override
878  def TearDown(self):
879    # By default, teardown will invoke ADB. When receiving SIGTERM due to a
880    # timeout, there's a high probability that ADB is non-responsive. In these
881    # cases, sending an ADB command will potentially take a long time to time
882    # out. Before this happens, the process will be hard-killed for not
883    # responding to SIGTERM fast enough.
884    if self._received_sigterm:
885      return
886
887    @local_device_environment.handle_shard_failures
888    @trace_event.traced
889    def individual_device_tear_down(dev):
890      for s in self._servers.get(str(dev), []):
891        s.TearDown()
892
893      tool = self.GetTool(dev)
894      tool.CleanUpEnvironment()
895
896    self._env.parallel_devices.pMap(individual_device_tear_down)
897