OLD | NEW |
---|---|
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Implements test sharding logic.""" | 5 """Implements test sharding logic.""" |
6 | 6 |
7 import logging | 7 import logging |
8 import sys | |
9 import threading | |
10 | 8 |
11 from pylib import android_commands | 9 from pylib import android_commands |
12 from pylib import forwarder | 10 from pylib import forwarder |
11 from pylib.utils import reraiser_thread | |
13 | 12 |
14 import test_result | 13 import test_result |
15 | 14 |
16 | 15 |
17 class _Worker(threading.Thread): | 16 def _RunTestsFromQueue(runner, test_queue, out_results, out_retry): |
18 """Runs tests from the test_queue using the given runner in a separate thread. | 17 """Runs tests from the test_queue until empty using the given runner. |
19 | 18 |
20 Places results in the out_results. | 19 Adds TestResults objects to the out_results list and may add tests to the |
20 out_retry list. | |
21 | |
22 Args: | |
23 runner: A TestRunner object used to run the tests. | |
24 test_queue: A list from which to get tests to run. | |
25 out_results: A list to add TestResults to. | |
26 out_retry: A list to add tests to retry. | |
21 """ | 27 """ |
22 def __init__(self, runner, test_queue, out_results, out_retry): | 28 try: |
23 """Initializes the worker. | 29 while True: |
30 test = test_queue.pop() | |
31 result, retry = runner.RunTest(test) | |
32 out_results.append(result) | |
33 if retry: | |
34 out_retry.append(retry) | |
35 except IndexError: | |
36 pass | |
24 | 37 |
25 Args: | |
26 runner: A TestRunner object used to run the tests. | |
27 test_queue: A list from which to get tests to run. | |
28 out_results: A list to add TestResults to. | |
29 out_retry: A list to add tests to retry. | |
30 """ | |
31 super(_Worker, self).__init__() | |
32 self.daemon = True | |
33 self._exc_info = None | |
34 self._runner = runner | |
35 self._test_queue = test_queue | |
36 self._out_results = out_results | |
37 self._out_retry = out_retry | |
38 | 38 |
39 #override | 39 def _SetUp(runner_factory, device, out_runners): |
40 def run(self): | 40 """Creates a test runner for each device and calls SetUp() in parallel. |
41 """Run tests from the queue in a seperate thread until it is empty. | |
42 | 41 |
43 Adds TestResults objects to the out_results list and may add tests to the | 42 Note: if a device is unresponsive the corresponding TestRunner will not be |
44 out_retry list. | 43 added to out_runners. |
45 """ | |
46 try: | |
47 while True: | |
48 test = self._test_queue.pop() | |
49 result, retry = self._runner.Run(test) | |
50 self._out_results.append(result) | |
51 if retry: | |
52 self._out_retry.append(retry) | |
53 except IndexError: | |
54 pass | |
55 except: | |
56 self._exc_info = sys.exc_info() | |
57 raise | |
58 | 44 |
59 def ReraiseIfException(self): | 45 Args: |
60 """Reraise exception if an exception was raised in the thread.""" | 46 runner_factory: callable that takes a device and returns a TestRunner. |
61 if self._exc_info: | 47 device: the device serial number to set up. |
62 raise self._exc_info[0], self._exc_info[1], self._exc_info[2] | 48 out_runners: list to add the successfully set up TestRunner object. |
49 """ | |
50 try: | |
51 logging.warning('****Creating shard for %s', device) | |
52 runner = runner_factory(device) | |
53 runner.SetUp() | |
54 out_runners.append(runner) | |
55 except android_commands.errors.DeviceUnresponsiveError as e: | |
56 logging.warning('****Failed to create shard for %s: [%s]', (device, e)) | |
63 | 57 |
64 | 58 |
65 def _RunAllTests(runners, tests): | 59 def _RunAllTests(runners, tests): |
66 """Run all tests using the given TestRunners. | 60 """Run all tests using the given TestRunners. |
67 | 61 |
68 Args: | 62 Args: |
69 runners: a list of TestRunner objects. | 63 runners: a list of TestRunner objects. |
70 tests: a list of Tests to run using the given TestRunners. | 64 tests: a list of Tests to run using the given TestRunners. |
71 | 65 |
72 Returns: | 66 Returns: |
73 Tuple: (list of TestResults, list of tests to retry) | 67 Tuple: (list of TestResults, list of tests to retry) |
74 """ | 68 """ |
75 tests_queue = list(tests) | 69 tests_queue = list(tests) |
76 workers = [] | |
77 results = [] | 70 results = [] |
78 retry = [] | 71 retry = [] |
79 for r in runners: | 72 workers = reraiser_thread.ReraiserThreadGroup([reraiser_thread.ReraiserThread( |
80 worker = _Worker(r, tests_queue, results, retry) | 73 _RunTestsFromQueue, [r, tests_queue, results, retry]) for r in runners]) |
81 worker.start() | 74 workers.StartAll() |
82 workers.append(worker) | 75 workers.JoinAll() |
83 while workers: | |
84 for w in workers[:]: | |
85 # Allow the main thread to periodically check for keyboard interrupts. | |
86 w.join(0.1) | |
87 if not w.isAlive(): | |
88 w.ReraiseIfException() | |
89 workers.remove(w) | |
90 return (results, retry) | 76 return (results, retry) |
91 | 77 |
92 | 78 |
93 def _CreateRunners(runner_factory, devices): | 79 def _CreateRunners(runner_factory, devices): |
94 """Creates a test runner for each device. | 80 """Creates a test runner for each device and calls SetUp() in parallel. |
95 | 81 |
96 Note: if a device is unresponsive the corresponding TestRunner will not be | 82 Note: if a device is unresponsive the corresponding TestRunner will not be |
97 included in the returned list. | 83 included in the returned list. |
98 | 84 |
99 Args: | 85 Args: |
100 runner_factory: callable that takes a device and returns a TestRunner. | 86 runner_factory: callable that takes a device and returns a TestRunner. |
101 devices: list of device serial numbers as strings. | 87 devices: list of device serial numbers as strings. |
102 | 88 |
103 Returns: | 89 Returns: |
104 A list of TestRunner objects. | 90 A list of TestRunner objects. |
105 """ | 91 """ |
106 test_runners = [] | 92 test_runners = [] |
107 for index, device in enumerate(devices): | 93 threads = reraiser_thread.ReraiserThreadGroup( |
108 logging.warning('*' * 80) | 94 [reraiser_thread.ReraiserThread(_SetUp, [runner_factory, d, test_runners]) |
109 logging.warning('Creating shard %d for %s', index, device) | 95 for d in devices]) |
110 logging.warning('*' * 80) | 96 threads.StartAll() |
111 try: | 97 threads.JoinAll() |
frankf
2013/02/22 02:42:55
For all these JoinAll invokations, we need to thin
craigdh
2013/02/22 17:24:20
I thought about it a bit. Here's what's currently
| |
112 test_runners.append(runner_factory(device)) | |
113 except android_commands.errors.DeviceUnresponsiveError as e: | |
114 logging.warning('****Failed to create a shard: [%s]', e) | |
115 return test_runners | 98 return test_runners |
116 | 99 |
117 | 100 |
101 def _TearDownRunners(runners): | |
102 """Calls TearDown() for each test runner in parallel. | |
103 Args: | |
104 runners: a list of TestRunner objects. | |
105 """ | |
106 threads = reraiser_thread.ReraiserThreadGroup( | |
107 [reraiser_thread.ReraiserThread(runner.TearDown) | |
108 for runner in runners]) | |
109 threads.StartAll() | |
110 threads.JoinAll() | |
111 | |
112 | |
118 def ShardAndRunTests(runner_factory, devices, tests, build_type='Debug', | 113 def ShardAndRunTests(runner_factory, devices, tests, build_type='Debug', |
119 tries=3): | 114 tries=3): |
120 """Run all tests on attached devices, retrying tests that don't pass. | 115 """Run all tests on attached devices, retrying tests that don't pass. |
121 | 116 |
122 Args: | 117 Args: |
123 runner_factory: callable that takes a device and returns a TestRunner. | 118 runner_factory: callable that takes a device and returns a TestRunner. |
124 devices: list of attached device serial numbers as strings. | 119 devices: list of attached device serial numbers as strings. |
125 tests: list of tests to run. | 120 tests: list of tests to run. |
126 build_type: either 'Debug' or 'Release'. | 121 build_type: either 'Debug' or 'Release'. |
127 tries: number of tries before accepting failure. | 122 tries: number of tries before accepting failure. |
128 | 123 |
129 Returns: | 124 Returns: |
130 A test_result.TestResults object. | 125 A test_result.TestResults object. |
131 """ | 126 """ |
132 final_results = test_result.TestResults() | 127 final_results = test_result.TestResults() |
133 results = test_result.TestResults() | 128 results = test_result.TestResults() |
134 forwarder.Forwarder.KillHost(build_type) | 129 forwarder.Forwarder.KillHost(build_type) |
135 try_count = 0 | 130 try_count = 0 |
136 while tests: | 131 runners = _CreateRunners(runner_factory, set(devices)) |
nilesh
2013/02/22 02:00:29
I think the numbers of runners we create should al
craigdh
2013/02/22 17:24:20
There's an issue, though, if devices fail. I think
| |
137 devices = set(devices).intersection(android_commands.GetAttachedDevices()) | 132 try: |
138 if not devices: | 133 while tests: |
139 # There are no visible devices attached, this is unrecoverable. | 134 devices = set(devices).intersection(android_commands.GetAttachedDevices()) |
140 msg = 'No devices attached and visible to run tests!' | 135 runners = [r for r in runners if r.device in devices] |
141 logging.critical(msg) | 136 if not devices: |
142 raise Exception(msg) | 137 # There are no visible devices attached, this is unrecoverable. |
143 if try_count >= tries: | 138 msg = 'No devices attached and visible to run tests!' |
144 # We've retried too many times, return the TestResults up to this point. | 139 logging.critical(msg) |
145 results.ok = final_results.ok | 140 raise Exception(msg) |
146 final_results = results | 141 if try_count >= tries: |
147 break | 142 # We've retried too many times, return the TestResults up to this point. |
148 try_count += 1 | 143 results.ok = final_results.ok |
149 runners = _CreateRunners(runner_factory, devices) | 144 final_results = results |
150 try: | 145 break |
151 results_list, tests = _RunAllTests(runners, tests) | 146 try_count += 1 |
152 results = test_result.TestResults.FromTestResults(results_list) | 147 try: |
153 final_results.ok += results.ok | 148 results_list, tests = _RunAllTests(runners, tests) |
154 except android_commands.errors.DeviceUnresponsiveError as e: | 149 results = test_result.TestResults.FromTestResults(results_list) |
155 logging.warning('****Failed to run test: [%s]', e) | 150 final_results.ok += results.ok |
151 except android_commands.errors.DeviceUnresponsiveError as e: | |
152 logging.warning('****Failed to run test: [%s]', e) | |
153 finally: | |
154 _TearDownRunners(runners) | |
156 forwarder.Forwarder.KillHost(build_type) | 155 forwarder.Forwarder.KillHost(build_type) |
157 return final_results | 156 return final_results |
OLD | NEW |