Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
michael@0 | 1 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
michael@0 | 2 | # Use of this source code is governed by a BSD-style license that can be |
michael@0 | 3 | # found in the LICENSE file. |
michael@0 | 4 | |
michael@0 | 5 | """Takes care of sharding the python-drive tests in multiple devices.""" |
michael@0 | 6 | |
michael@0 | 7 | import copy |
michael@0 | 8 | import logging |
michael@0 | 9 | import multiprocessing |
michael@0 | 10 | |
michael@0 | 11 | from python_test_caller import CallPythonTest |
michael@0 | 12 | from run_java_tests import FatalTestException |
michael@0 | 13 | import sharded_tests_queue |
michael@0 | 14 | from test_result import TestResults |
michael@0 | 15 | |
michael@0 | 16 | |
michael@0 | 17 | def SetTestsContainer(tests_container): |
michael@0 | 18 | """Sets PythonTestSharder as a top-level field. |
michael@0 | 19 | |
michael@0 | 20 | PythonTestSharder uses multiprocessing.Pool, which creates a pool of |
michael@0 | 21 | processes. This is used to initialize each worker in the pool, ensuring that |
michael@0 | 22 | each worker has access to this shared pool of tests. |
michael@0 | 23 | |
michael@0 | 24 | The multiprocessing module requires that this be a top-level method. |
michael@0 | 25 | |
michael@0 | 26 | Args: |
michael@0 | 27 | tests_container: the container for all the tests. |
michael@0 | 28 | """ |
michael@0 | 29 | PythonTestSharder.tests_container = tests_container |
michael@0 | 30 | |
michael@0 | 31 | |
michael@0 | 32 | def _DefaultRunnable(test_runner): |
michael@0 | 33 | """A default runnable for a PythonTestRunner. |
michael@0 | 34 | |
michael@0 | 35 | Args: |
michael@0 | 36 | test_runner: A PythonTestRunner which will run tests. |
michael@0 | 37 | |
michael@0 | 38 | Returns: |
michael@0 | 39 | The test results. |
michael@0 | 40 | """ |
michael@0 | 41 | return test_runner.RunTests() |
michael@0 | 42 | |
michael@0 | 43 | |
michael@0 | 44 | class PythonTestRunner(object): |
michael@0 | 45 | """Thin wrapper around a list of PythonTestBase instances. |
michael@0 | 46 | |
michael@0 | 47 | This is meant to be a long-lived object which can run multiple Python tests |
michael@0 | 48 | within its lifetime. Tests will receive the device_id and shard_index. |
michael@0 | 49 | |
michael@0 | 50 | The shard index affords the ability to create unique port numbers (e.g. |
michael@0 | 51 | DEFAULT_PORT + shard_index) if the test so wishes. |
michael@0 | 52 | """ |
michael@0 | 53 | |
michael@0 | 54 | def __init__(self, options): |
michael@0 | 55 | """Constructor. |
michael@0 | 56 | |
michael@0 | 57 | Args: |
michael@0 | 58 | options: Options to use for setting up tests. |
michael@0 | 59 | """ |
michael@0 | 60 | self.options = options |
michael@0 | 61 | |
michael@0 | 62 | def RunTests(self): |
michael@0 | 63 | """Runs tests from the shared pool of tests, aggregating results. |
michael@0 | 64 | |
michael@0 | 65 | Returns: |
michael@0 | 66 | A list of test results for all of the tests which this runner executed. |
michael@0 | 67 | """ |
michael@0 | 68 | tests = PythonTestSharder.tests_container |
michael@0 | 69 | |
michael@0 | 70 | results = [] |
michael@0 | 71 | for t in tests: |
michael@0 | 72 | res = CallPythonTest(t, self.options) |
michael@0 | 73 | results.append(res) |
michael@0 | 74 | |
michael@0 | 75 | return TestResults.FromTestResults(results) |
michael@0 | 76 | |
michael@0 | 77 | |
michael@0 | 78 | class PythonTestSharder(object): |
michael@0 | 79 | """Runs Python tests in parallel on multiple devices. |
michael@0 | 80 | |
michael@0 | 81 | This is lifted more or less wholesale from BaseTestRunner. |
michael@0 | 82 | |
michael@0 | 83 | Under the covers, it creates a pool of long-lived PythonTestRunners, which |
michael@0 | 84 | execute tests from the pool of tests. |
michael@0 | 85 | |
michael@0 | 86 | Args: |
michael@0 | 87 | attached_devices: a list of device IDs attached to the host. |
michael@0 | 88 | available_tests: a list of tests to run which subclass PythonTestBase. |
michael@0 | 89 | options: Options to use for setting up tests. |
michael@0 | 90 | |
michael@0 | 91 | Returns: |
michael@0 | 92 | An aggregated list of test results. |
michael@0 | 93 | """ |
michael@0 | 94 | tests_container = None |
michael@0 | 95 | |
michael@0 | 96 | def __init__(self, attached_devices, available_tests, options): |
michael@0 | 97 | self.options = options |
michael@0 | 98 | self.attached_devices = attached_devices |
michael@0 | 99 | self.retries = options.shard_retries |
michael@0 | 100 | self.tests = available_tests |
michael@0 | 101 | |
michael@0 | 102 | def _SetupSharding(self, tests): |
michael@0 | 103 | """Creates the shared pool of tests and makes it available to test runners. |
michael@0 | 104 | |
michael@0 | 105 | Args: |
michael@0 | 106 | tests: the list of tests which will be consumed by workers. |
michael@0 | 107 | """ |
michael@0 | 108 | SetTestsContainer(sharded_tests_queue.ShardedTestsQueue( |
michael@0 | 109 | len(self.attached_devices), tests)) |
michael@0 | 110 | |
michael@0 | 111 | def RunShardedTests(self): |
michael@0 | 112 | """Runs tests in parallel using a pool of workers. |
michael@0 | 113 | |
michael@0 | 114 | Returns: |
michael@0 | 115 | A list of test results aggregated from all test runs. |
michael@0 | 116 | """ |
michael@0 | 117 | logging.warning('*' * 80) |
michael@0 | 118 | logging.warning('Sharding in ' + str(len(self.attached_devices)) + |
michael@0 | 119 | ' devices.') |
michael@0 | 120 | logging.warning('Note that the output is not synchronized.') |
michael@0 | 121 | logging.warning('Look for the "Final result" banner in the end.') |
michael@0 | 122 | logging.warning('*' * 80) |
michael@0 | 123 | all_passed = [] |
michael@0 | 124 | test_results = TestResults() |
michael@0 | 125 | tests_to_run = self.tests |
michael@0 | 126 | for retry in xrange(self.retries): |
michael@0 | 127 | logging.warning('Try %d of %d', retry + 1, self.retries) |
michael@0 | 128 | self._SetupSharding(self.tests) |
michael@0 | 129 | test_runners = self._MakeTestRunners(self.attached_devices) |
michael@0 | 130 | logging.warning('Starting...') |
michael@0 | 131 | pool = multiprocessing.Pool(len(self.attached_devices), |
michael@0 | 132 | SetTestsContainer, |
michael@0 | 133 | [PythonTestSharder.tests_container]) |
michael@0 | 134 | |
michael@0 | 135 | # List of TestResults objects from each test execution. |
michael@0 | 136 | try: |
michael@0 | 137 | results_lists = pool.map(_DefaultRunnable, test_runners) |
michael@0 | 138 | except Exception: |
michael@0 | 139 | logging.exception('Unable to run tests. Something with the ' |
michael@0 | 140 | 'PythonTestRunners has gone wrong.') |
michael@0 | 141 | raise FatalTestException('PythonTestRunners were unable to run tests.') |
michael@0 | 142 | |
michael@0 | 143 | test_results = TestResults.FromTestResults(results_lists) |
michael@0 | 144 | # Accumulate passing results. |
michael@0 | 145 | all_passed += test_results.ok |
michael@0 | 146 | # If we have failed tests, map them to tests to retry. |
michael@0 | 147 | failed_tests = test_results.GetAllBroken() |
michael@0 | 148 | tests_to_run = self._GetTestsToRetry(self.tests, |
michael@0 | 149 | failed_tests) |
michael@0 | 150 | |
michael@0 | 151 | # Bail out early if we have no more tests. This can happen if all tests |
michael@0 | 152 | # pass before we're out of retries, for example. |
michael@0 | 153 | if not tests_to_run: |
michael@0 | 154 | break |
michael@0 | 155 | |
michael@0 | 156 | final_results = TestResults() |
michael@0 | 157 | # all_passed has accumulated all passing test results. |
michael@0 | 158 | # test_results will have the results from the most recent run, which could |
michael@0 | 159 | # include a variety of failure modes (unknown, crashed, failed, etc). |
michael@0 | 160 | final_results = test_results |
michael@0 | 161 | final_results.ok = all_passed |
michael@0 | 162 | |
michael@0 | 163 | return final_results |
michael@0 | 164 | |
michael@0 | 165 | def _MakeTestRunners(self, attached_devices): |
michael@0 | 166 | """Initialize and return a list of PythonTestRunners. |
michael@0 | 167 | |
michael@0 | 168 | Args: |
michael@0 | 169 | attached_devices: list of device IDs attached to host. |
michael@0 | 170 | |
michael@0 | 171 | Returns: |
michael@0 | 172 | A list of PythonTestRunners, one for each device. |
michael@0 | 173 | """ |
michael@0 | 174 | test_runners = [] |
michael@0 | 175 | for index, device in enumerate(attached_devices): |
michael@0 | 176 | logging.warning('*' * 80) |
michael@0 | 177 | logging.warning('Creating shard %d for %s', index, device) |
michael@0 | 178 | logging.warning('*' * 80) |
michael@0 | 179 | # Bind the PythonTestRunner to a device & shard index. Give it the |
michael@0 | 180 | # runnable which it will use to actually execute the tests. |
michael@0 | 181 | test_options = copy.deepcopy(self.options) |
michael@0 | 182 | test_options.ensure_value('device_id', device) |
michael@0 | 183 | test_options.ensure_value('shard_index', index) |
michael@0 | 184 | test_runner = PythonTestRunner(test_options) |
michael@0 | 185 | test_runners.append(test_runner) |
michael@0 | 186 | |
michael@0 | 187 | return test_runners |
michael@0 | 188 | |
michael@0 | 189 | def _GetTestsToRetry(self, available_tests, failed_tests): |
michael@0 | 190 | """Infers a list of tests to retry from failed tests and available tests. |
michael@0 | 191 | |
michael@0 | 192 | Args: |
michael@0 | 193 | available_tests: a list of tests which subclass PythonTestBase. |
michael@0 | 194 | failed_tests: a list of SingleTestResults representing failed tests. |
michael@0 | 195 | |
michael@0 | 196 | Returns: |
michael@0 | 197 | A list of test objects which correspond to test names found in |
michael@0 | 198 | failed_tests, or an empty list if there is no correspondence. |
michael@0 | 199 | """ |
michael@0 | 200 | failed_test_names = map(lambda t: t.test_name, failed_tests) |
michael@0 | 201 | tests_to_retry = [t for t in available_tests |
michael@0 | 202 | if t.qualified_name in failed_test_names] |
michael@0 | 203 | return tests_to_retry |