| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import fnmatch |
| import functools |
| import logging |
| |
| from devil.android import device_errors |
| from pylib import valgrind_tools |
| from pylib.base import base_test_result |
| from pylib.base import test_run |
| from pylib.base import test_collection |
| |
| |
| def handle_shard_failures(f): |
| """A decorator that handles device failures for per-device functions. |
| |
| Args: |
| f: the function being decorated. The function must take at least one |
| argument, and that argument must be the device. |
| """ |
| return handle_shard_failures_with(None)(f) |
| |
| |
| def handle_shard_failures_with(on_failure): |
| """A decorator that handles device failures for per-device functions. |
| |
| This calls on_failure in the event of a failure. |
| |
| Args: |
| f: the function being decorated. The function must take at least one |
| argument, and that argument must be the device. |
| on_failure: A binary function to call on failure. |
| """ |
| def decorator(f): |
| @functools.wraps(f) |
| def wrapper(dev, *args, **kwargs): |
| try: |
| return f(dev, *args, **kwargs) |
| except device_errors.CommandFailedError: |
| logging.exception('Shard failed: %s(%s)', f.__name__, str(dev)) |
| except device_errors.CommandTimeoutError: |
| logging.exception('Shard timed out: %s(%s)', f.__name__, str(dev)) |
| except device_errors.DeviceUnreachableError: |
| logging.exception('Shard died: %s(%s)', f.__name__, str(dev)) |
| if on_failure: |
| on_failure(dev, f.__name__) |
| return None |
| |
| return wrapper |
| |
| return decorator |
| |
| |
| class LocalDeviceTestRun(test_run.TestRun): |
| |
| def __init__(self, env, test_instance): |
| super(LocalDeviceTestRun, self).__init__(env, test_instance) |
| self._tools = {} |
| |
| #override |
| def RunTests(self): |
| tests = self._GetTests() |
| |
| @handle_shard_failures |
| def run_tests_on_device(dev, tests, results): |
| for test in tests: |
| result = None |
| try: |
| result = self._RunTest(dev, test) |
| if isinstance(result, base_test_result.BaseTestResult): |
| results.AddResult(result) |
| elif isinstance(result, list): |
| results.AddResults(result) |
| else: |
| raise Exception( |
| 'Unexpected result type: %s' % type(result).__name__) |
| except: |
| if isinstance(tests, test_collection.TestCollection): |
| tests.add(test) |
| raise |
| finally: |
| if isinstance(tests, test_collection.TestCollection): |
| tests.test_completed() |
| |
| |
| logging.info('Finished running tests on this device.') |
| |
| tries = 0 |
| results = base_test_result.TestRunResults() |
| all_fail_results = {} |
| while tries < self._env.max_tries and tests: |
| logging.info('STARTING TRY #%d/%d', tries + 1, self._env.max_tries) |
| logging.info('Will run %d tests on %d devices: %s', |
| len(tests), len(self._env.devices), |
| ', '.join(str(d) for d in self._env.devices)) |
| for t in tests: |
| logging.debug(' %s', t) |
| |
| try_results = base_test_result.TestRunResults() |
| if self._ShouldShard(): |
| tc = test_collection.TestCollection(self._CreateShards(tests)) |
| self._env.parallel_devices.pMap( |
| run_tests_on_device, tc, try_results).pGet(None) |
| else: |
| self._env.parallel_devices.pMap( |
| run_tests_on_device, tests, try_results).pGet(None) |
| |
| for result in try_results.GetAll(): |
| if result.GetType() in (base_test_result.ResultType.PASS, |
| base_test_result.ResultType.SKIP): |
| results.AddResult(result) |
| else: |
| all_fail_results[result.GetName()] = result |
| |
| results_names = set(r.GetName() for r in results.GetAll()) |
| |
| def has_test_result(name): |
| # When specifying a test filter, names can contain trailing wildcards. |
| # See local_device_gtest_run._ExtractTestsFromFilter() |
| if name.endswith('*'): |
| return any(fnmatch.fnmatch(n, name) for n in results_names) |
| return name in results_names |
| |
| tests = [t for t in tests if not has_test_result(self._GetTestName(t))] |
| tries += 1 |
| logging.info('FINISHED TRY #%d/%d', tries, self._env.max_tries) |
| if tests: |
| logging.info('%d failed tests remain.', len(tests)) |
| else: |
| logging.info('All tests completed.') |
| |
| all_unknown_test_names = set(self._GetTestName(t) for t in tests) |
| all_failed_test_names = set(all_fail_results.iterkeys()) |
| |
| unknown_tests = all_unknown_test_names.difference(all_failed_test_names) |
| failed_tests = all_failed_test_names.intersection(all_unknown_test_names) |
| |
| if unknown_tests: |
| results.AddResults( |
| base_test_result.BaseTestResult( |
| u, base_test_result.ResultType.UNKNOWN) |
| for u in unknown_tests) |
| if failed_tests: |
| results.AddResults(all_fail_results[f] for f in failed_tests) |
| |
| return results |
| |
| def GetTool(self, device): |
| if not str(device) in self._tools: |
| self._tools[str(device)] = valgrind_tools.CreateTool( |
| self._env.tool, device) |
| return self._tools[str(device)] |
| |
| def _CreateShards(self, tests): |
| raise NotImplementedError |
| |
| # pylint: disable=no-self-use |
| def _GetTestName(self, test): |
| return test |
| |
| def _GetTests(self): |
| raise NotImplementedError |
| |
| def _RunTest(self, device, test): |
| raise NotImplementedError |
| |
| def _ShouldShard(self): |
| raise NotImplementedError |