OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Shards a given test suite and runs the shards in parallel. | |
7 | |
8 ShardingSupervisor is called to process the command line options and creates | |
9 the specified number of worker threads. These threads then run each shard of | |
10 the test in a separate process and report on the results. When all the shards | |
11 have been completed, the supervisor reprints any lines indicating a test | |
12 failure for convenience. If only one shard is to be run, a single subprocess | |
13 is started for that shard and the output is identical to gtest's output. | |
14 """ | |
15 | |
16 import itertools | |
17 import optparse | |
18 import os | |
19 import Queue | |
20 import random | |
21 import re | |
22 import sys | |
23 import threading | |
24 | |
25 from stdio_buffer import StdioBuffer | |
26 from xml.dom import minidom | |
27 | |
28 # Add tools/ to path | |
29 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) | |
30 sys.path.append(os.path.join(BASE_PATH, "..")) | |
31 try: | |
32 import find_depot_tools # pylint: disable=F0401,W0611 | |
33 # Fixes a bug in Windows where some shards die upon starting | |
34 # TODO(charleslee): actually fix this bug | |
35 import subprocess2 as subprocess | |
36 except ImportError: | |
37 # Unable to find depot_tools, so just use standard subprocess | |
38 import subprocess | |
39 | |
40 SS_USAGE = "python %prog [options] path/to/test [gtest_args]" | |
41 SS_DEFAULT_NUM_CORES = 4 | |
42 SS_DEFAULT_SHARDS_PER_CORE = 5 # num_shards = cores * SHARDS_PER_CORE | |
43 SS_DEFAULT_RUNS_PER_CORE = 1 # num_workers = cores * RUNS_PER_CORE | |
44 SS_DEFAULT_RETRY_PERCENT = 5 # --retry-failed ignored if more than 5% fail | |
45 SS_DEFAULT_TIMEOUT = 530 # Slightly less than buildbot's default 600 seconds | |
46 | |
47 | |
48 def DetectNumCores(): | |
49 """Detects the number of cores on the machine. | |
50 | |
51 Returns: | |
52 The number of cores on the machine or DEFAULT_NUM_CORES if it could not | |
53 be found. | |
54 """ | |
55 try: | |
56 # Override on some Chromium Valgrind bots. | |
57 if "CHROME_VALGRIND_NUMCPUS" in os.environ: | |
58 return int(os.environ["CHROME_VALGRIND_NUMCPUS"]) | |
59 # Linux, Unix, MacOS | |
60 if hasattr(os, "sysconf"): | |
61 if "SC_NPROCESSORS_ONLN" in os.sysconf_names: | |
62 # Linux, Unix | |
63 return int(os.sysconf("SC_NPROCESSORS_ONLN")) | |
64 else: | |
65 # OSX | |
66 return int(os.popen2("sysctl -n hw.ncpu")[1].read()) | |
67 # Windows | |
68 return int(os.environ["NUMBER_OF_PROCESSORS"]) | |
69 except ValueError: | |
70 return SS_DEFAULT_NUM_CORES | |
71 | |
72 | |
73 def GetGTestOutput(args): | |
74 """Extracts gtest_output from the args. Returns none if not present.""" | |
75 | |
76 for arg in args: | |
77 if '--gtest_output=' in arg: | |
78 return arg.split('=')[1] | |
79 return None | |
80 | |
81 | |
82 def AppendToGTestOutput(gtest_args, value): | |
83 args = gtest_args[:] | |
84 current_value = GetGTestOutput(args) | |
85 if not current_value: | |
86 return gtest_args | |
87 | |
88 current_arg = '--gtest_output=' + current_value | |
89 args.remove(current_arg) | |
90 args.append('--gtest_output=' + current_value + value) | |
91 return args | |
92 | |
93 | |
94 def RemoveGTestOutput(gtest_args): | |
95 args = gtest_args[:] | |
96 current_value = GetGTestOutput(args) | |
97 if not current_value: | |
98 return gtest_args | |
99 | |
100 args.remove('--gtest_output=' + current_value) | |
101 return args | |
102 | |
103 | |
104 def AppendToXML(final_xml, generic_path, shard): | |
105 """Combine the shard xml file with the final xml file.""" | |
106 | |
107 path = generic_path + str(shard) | |
108 | |
109 try: | |
110 with open(path) as shard_xml_file: | |
111 shard_xml = minidom.parse(shard_xml_file) | |
112 except IOError: | |
113 # If the shard crashed, gtest will not have generated an xml file. | |
114 return final_xml | |
115 | |
116 if not final_xml: | |
117 # Out final xml is empty, let's prepopulate it with the first one we see. | |
118 return shard_xml | |
119 | |
120 shard_node = shard_xml.documentElement | |
121 final_node = final_xml.documentElement | |
122 | |
123 testcases = shard_node.getElementsByTagName('testcase') | |
124 final_testcases = final_node.getElementsByTagName('testcase') | |
125 | |
126 final_testsuites = final_node.getElementsByTagName('testsuite') | |
127 final_testsuites_by_name = dict( | |
128 (suite.getAttribute('name'), suite) for suite in final_testsuites) | |
129 | |
130 for testcase in testcases: | |
131 name = testcase.getAttribute('name') | |
132 classname = testcase.getAttribute('classname') | |
133 failures = testcase.getElementsByTagName('failure') | |
134 status = testcase.getAttribute('status') | |
135 elapsed = testcase.getAttribute('time') | |
136 | |
137 # don't bother updating the final xml if there is no data. | |
138 if status == 'notrun': | |
139 continue | |
140 | |
141 # Look in our final xml to see if it's there. | |
142 # There has to be a better way... | |
143 merged_into_final_testcase = False | |
144 for final_testcase in final_testcases: | |
145 final_name = final_testcase.getAttribute('name') | |
146 final_classname = final_testcase.getAttribute('classname') | |
147 if final_name == name and final_classname == classname: | |
148 # We got the same entry. | |
149 final_testcase.setAttribute('status', status) | |
150 final_testcase.setAttribute('time', elapsed) | |
151 for failure in failures: | |
152 final_testcase.appendChild(failure) | |
153 merged_into_final_testcase = True | |
154 | |
155 # We couldn't find an existing testcase to merge the results into, so we | |
156 # copy the node into the existing test suite. | |
157 if not merged_into_final_testcase: | |
158 testsuite = testcase.parentNode | |
159 final_testsuite = final_testsuites_by_name[testsuite.getAttribute('name')] | |
160 final_testsuite.appendChild(testcase) | |
161 | |
162 return final_xml | |
163 | |
164 | |
165 def RunShard(test, total_shards, index, gtest_args, stdout, stderr): | |
166 """Runs a single test shard in a subprocess. | |
167 | |
168 Returns: | |
169 The Popen object representing the subprocess handle. | |
170 """ | |
171 args = [test] | |
172 | |
173 # If there is a gtest_output | |
174 test_args = AppendToGTestOutput(gtest_args, str(index)) | |
175 args.extend(test_args) | |
176 env = os.environ.copy() | |
177 env["GTEST_TOTAL_SHARDS"] = str(total_shards) | |
178 env["GTEST_SHARD_INDEX"] = str(index) | |
179 | |
180 # Use a unique log file for each shard | |
181 # Allows ui_tests to be run in parallel on the same machine | |
182 env["CHROME_LOG_FILE"] = "chrome_log_%d" % index | |
183 | |
184 return subprocess.Popen( | |
185 args, stdout=stdout, | |
186 stderr=stderr, | |
187 env=env, | |
188 bufsize=0, | |
189 universal_newlines=True) | |
190 | |
191 | |
192 class ShardRunner(threading.Thread): | |
193 """Worker thread that manages a single shard at a time. | |
194 | |
195 Attributes: | |
196 supervisor: The ShardingSupervisor that this worker reports to. | |
197 counter: Called to get the next shard index to run. | |
198 test_start: Regex that detects when a test runs. | |
199 test_ok: Regex that detects a passing test. | |
200 test_fail: Regex that detects a failing test. | |
201 current_test: The name of the currently running test. | |
202 """ | |
203 | |
204 def __init__(self, supervisor, counter, test_start, test_ok, test_fail): | |
205 """Inits ShardRunner and sets the current test to nothing.""" | |
206 threading.Thread.__init__(self) | |
207 self.supervisor = supervisor | |
208 self.counter = counter | |
209 self.test_start = test_start | |
210 self.test_ok = test_ok | |
211 self.test_fail = test_fail | |
212 self.current_test = "" | |
213 | |
214 def ReportFailure(self, description, index, test_name): | |
215 """Assembles and reports a failure line to be printed later.""" | |
216 log_line = "%s (%i): %s\n" % (description, index, test_name) | |
217 self.supervisor.LogTestFailure(log_line) | |
218 | |
219 def ProcessLine(self, index, line): | |
220 """Checks a shard output line for test status, and reports a failure or | |
221 incomplete test if needed. | |
222 """ | |
223 results = self.test_start.search(line) | |
224 if results: | |
225 if self.current_test: | |
226 self.ReportFailure("INCOMPLETE", index, self.current_test) | |
227 self.current_test = results.group(1) | |
228 self.supervisor.IncrementTestCount() | |
229 return | |
230 | |
231 results = self.test_ok.search(line) | |
232 if results: | |
233 self.current_test = "" | |
234 return | |
235 | |
236 results = self.test_fail.search(line) | |
237 if results: | |
238 self.ReportFailure("FAILED", index, results.group(1)) | |
239 self.current_test = "" | |
240 | |
241 def run(self): | |
242 """Runs shards and outputs the results. | |
243 | |
244 Gets the next shard index from the supervisor, runs it in a subprocess, | |
245 and collects the output. The output is read character by character in | |
246 case the shard crashes without an ending newline. Each line is processed | |
247 as it is finished. | |
248 """ | |
249 while True: | |
250 try: | |
251 index = self.counter.get_nowait() | |
252 except Queue.Empty: | |
253 break | |
254 shard_running = True | |
255 shard = RunShard( | |
256 self.supervisor.test, self.supervisor.total_shards, index, | |
257 self.supervisor.gtest_args, subprocess.PIPE, subprocess.PIPE) | |
258 buf = StdioBuffer(shard) | |
259 # Spawn two threads to collect stdio output | |
260 stdout_collector_thread = buf.handle_pipe(sys.stdout, shard.stdout) | |
261 stderr_collector_thread = buf.handle_pipe(sys.stderr, shard.stderr) | |
262 while shard_running: | |
263 pipe, line = buf.readline() | |
264 if pipe is None and line is None: | |
265 shard_running = False | |
266 if not line and not shard_running: | |
267 break | |
268 self.ProcessLine(index, line) | |
269 self.supervisor.LogOutputLine(index, line, pipe) | |
270 stdout_collector_thread.join() | |
271 stderr_collector_thread.join() | |
272 if self.current_test: | |
273 self.ReportFailure("INCOMPLETE", index, self.current_test) | |
274 self.supervisor.ShardIndexCompleted(index) | |
275 if shard.returncode != 0: | |
276 self.supervisor.LogShardFailure(index) | |
277 | |
278 | |
279 class ShardingSupervisor(object): | |
280 """Supervisor object that handles the worker threads. | |
281 | |
282 Attributes: | |
283 test: Name of the test to shard. | |
284 num_shards_to_run: Total number of shards to split the test into. | |
285 num_runs: Total number of worker threads to create for running shards. | |
286 color: Indicates which coloring mode to use in the output. | |
287 original_order: True if shard output should be printed as it comes. | |
288 prefix: True if each line should indicate the shard index. | |
289 retry_percent: Integer specifying the max percent of tests to retry. | |
290 gtest_args: The options to pass to gtest. | |
291 failed_tests: List of statements from shard output indicating a failure. | |
292 failed_shards: List of shards that contained failing tests. | |
293 shards_completed: List of flags indicating which shards have finished. | |
294 shard_output: Buffer that stores output from each shard as (stdio, line). | |
295 test_counter: Stores the total number of tests run. | |
296 total_slaves: Total number of slaves running this test. | |
297 slave_index: Current slave to run tests for. | |
298 | |
299 If total_slaves is set, we run only a subset of the tests. This is meant to be | |
300 used when we want to shard across machines as well as across cpus. In that | |
301 case the number of shards to execute will be the same, but they will be | |
302 smaller, as the total number of shards in the test suite will be multiplied | |
303 by 'total_slaves'. | |
304 | |
305 For example, if you are on a quad core machine, the sharding supervisor by | |
306 default will use 20 shards for the whole suite. However, if you set | |
307 total_slaves to 2, it will split the suite in 40 shards and will only run | |
308 shards [0-19] or shards [20-39] depending if you set slave_index to 0 or 1. | |
309 """ | |
310 | |
311 SHARD_COMPLETED = object() | |
312 | |
313 def __init__(self, test, num_shards_to_run, num_runs, color, original_order, | |
314 prefix, retry_percent, timeout, total_slaves, slave_index, | |
315 gtest_args): | |
316 """Inits ShardingSupervisor with given options and gtest arguments.""" | |
317 self.test = test | |
318 # Number of shards to run locally. | |
319 self.num_shards_to_run = num_shards_to_run | |
320 # Total shards in the test suite running across all slaves. | |
321 self.total_shards = num_shards_to_run * total_slaves | |
322 self.slave_index = slave_index | |
323 self.num_runs = num_runs | |
324 self.color = color | |
325 self.original_order = original_order | |
326 self.prefix = prefix | |
327 self.retry_percent = retry_percent | |
328 self.timeout = timeout | |
329 self.gtest_args = gtest_args | |
330 self.failed_tests = [] | |
331 self.failed_shards = [] | |
332 self.shards_completed = [False] * self.num_shards_to_run | |
333 self.shard_output = [Queue.Queue() for _ in range(self.num_shards_to_run)] | |
334 self.test_counter = itertools.count() | |
335 | |
336 def ShardTest(self): | |
337 """Runs the test and manages the worker threads. | |
338 | |
339 Runs the test and outputs a summary at the end. All the tests in the | |
340 suite are run by creating (cores * runs_per_core) threads and | |
341 (cores * shards_per_core) shards. When all the worker threads have | |
342 finished, the lines saved in failed_tests are printed again. If enabled, | |
343 and failed tests that do not have FLAKY or FAILS in their names are run | |
344 again, serially, and the results are printed. | |
345 | |
346 Returns: | |
347 1 if some unexpected (not FLAKY or FAILS) tests failed, 0 otherwise. | |
348 """ | |
349 | |
350 # Regular expressions for parsing GTest logs. Test names look like | |
351 # SomeTestCase.SomeTest | |
352 # SomeName/SomeTestCase.SomeTest/1 | |
353 # This regex also matches SomeName.SomeTest/1 and | |
354 # SomeName/SomeTestCase.SomeTest, which should be harmless. | |
355 test_name_regex = r"((\w+/)?\w+\.\w+(/\d+)?)" | |
356 | |
357 # Regex for filtering out ANSI escape codes when using color. | |
358 ansi_regex = r"(?:\x1b\[.*?[a-zA-Z])?" | |
359 | |
360 test_start = re.compile( | |
361 ansi_regex + r"\[\s+RUN\s+\] " + ansi_regex + test_name_regex) | |
362 test_ok = re.compile( | |
363 ansi_regex + r"\[\s+OK\s+\] " + ansi_regex + test_name_regex) | |
364 test_fail = re.compile( | |
365 ansi_regex + r"\[\s+FAILED\s+\] " + ansi_regex + test_name_regex) | |
366 | |
367 workers = [] | |
368 counter = Queue.Queue() | |
369 start_point = self.num_shards_to_run * self.slave_index | |
370 for i in range(start_point, start_point + self.num_shards_to_run): | |
371 counter.put(i) | |
372 | |
373 for i in range(self.num_runs): | |
374 worker = ShardRunner( | |
375 self, counter, test_start, test_ok, test_fail) | |
376 worker.start() | |
377 workers.append(worker) | |
378 if self.original_order: | |
379 for worker in workers: | |
380 worker.join() | |
381 else: | |
382 self.WaitForShards() | |
383 | |
384 # All the shards are done. Merge all the XML files and generate the | |
385 # main one. | |
386 output_arg = GetGTestOutput(self.gtest_args) | |
387 if output_arg: | |
388 xml, xml_path = output_arg.split(':', 1) | |
389 assert(xml == 'xml') | |
390 final_xml = None | |
391 for i in range(start_point, start_point + self.num_shards_to_run): | |
392 final_xml = AppendToXML(final_xml, xml_path, i) | |
393 | |
394 if final_xml: | |
395 with open(xml_path, 'w') as final_file: | |
396 final_xml.writexml(final_file) | |
397 | |
398 num_failed = len(self.failed_shards) | |
399 if num_failed > 0: | |
400 self.failed_shards.sort() | |
401 self.WriteText(sys.stdout, | |
402 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), | |
403 "\x1b[1;5;31m") | |
404 else: | |
405 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") | |
406 self.PrintSummary(self.failed_tests) | |
407 if self.retry_percent < 0: | |
408 return len(self.failed_shards) > 0 | |
409 | |
410 self.failed_tests = [x for x in self.failed_tests if x.find("FAILS_") < 0] | |
411 self.failed_tests = [x for x in self.failed_tests if x.find("FLAKY_") < 0] | |
412 if not self.failed_tests: | |
413 return 0 | |
414 return self.RetryFailedTests() | |
415 | |
416 def LogTestFailure(self, line): | |
417 """Saves a line in the lsit of failed tests to be printed at the end.""" | |
418 if line not in self.failed_tests: | |
419 self.failed_tests.append(line) | |
420 | |
421 def LogShardFailure(self, index): | |
422 """Records that a test in the given shard has failed.""" | |
423 self.failed_shards.append(index) | |
424 | |
425 def WaitForShards(self): | |
426 """Prints the output from each shard in consecutive order, waiting for | |
427 the current shard to finish before starting on the next shard. | |
428 """ | |
429 try: | |
430 for shard_index in range(self.num_shards_to_run): | |
431 while True: | |
432 try: | |
433 _, line = self.shard_output[shard_index].get(True, self.timeout) | |
434 except Queue.Empty: | |
435 # Shard timed out, notice failure and move on. | |
436 self.LogShardFailure(shard_index) | |
437 # TODO(maruel): Print last test. It'd be simpler to have the | |
438 # processing in the main thread. | |
439 # TODO(maruel): Make sure the worker thread terminates. | |
440 sys.stdout.write('TIMED OUT\n\n') | |
441 self.LogTestFailure( | |
442 'FAILURE: SHARD %d TIMED OUT; %d seconds' % ( | |
443 shard_index, self.timeout)) | |
444 break | |
445 if line is self.SHARD_COMPLETED: | |
446 break | |
447 sys.stdout.write(line) | |
448 except: | |
449 sys.stdout.flush() | |
450 print 'CAUGHT EXCEPTION: dumping remaining data:' | |
451 for shard_index in range(self.num_shards_to_run): | |
452 while True: | |
453 try: | |
454 _, line = self.shard_output[shard_index].get(False) | |
455 except Queue.Empty: | |
456 # Shard timed out, notice failure and move on. | |
457 self.LogShardFailure(shard_index) | |
458 break | |
459 if line is self.SHARD_COMPLETED: | |
460 break | |
461 sys.stdout.write(line) | |
462 raise | |
463 | |
464 def LogOutputLine(self, index, line, pipe=sys.stdout): | |
465 """Either prints the shard output line immediately or saves it in the | |
466 output buffer, depending on the settings. Also optionally adds a prefix. | |
467 Adds a (sys.stdout, line) or (sys.stderr, line) tuple in the output queue. | |
468 """ | |
469 # Fix up the index. | |
470 array_index = index - (self.num_shards_to_run * self.slave_index) | |
471 if self.prefix: | |
472 line = "%i>%s" % (index, line) | |
473 if self.original_order: | |
474 pipe.write(line) | |
475 else: | |
476 self.shard_output[array_index].put((pipe, line)) | |
477 | |
478 def IncrementTestCount(self): | |
479 """Increments the number of tests run. This is relevant to the | |
480 --retry-percent option. | |
481 """ | |
482 self.test_counter.next() | |
483 | |
484 def ShardIndexCompleted(self, index): | |
485 """Records that a shard has finished so the output from the next shard | |
486 can now be printed. | |
487 """ | |
488 # Fix up the index. | |
489 array_index = index - (self.num_shards_to_run * self.slave_index) | |
490 self.shard_output[array_index].put((sys.stdout, self.SHARD_COMPLETED)) | |
491 | |
492 def RetryFailedTests(self): | |
493 """Reruns any failed tests serially and prints another summary of the | |
494 results if no more than retry_percent failed. | |
495 """ | |
496 num_tests_run = self.test_counter.next() | |
497 if len(self.failed_tests) > self.retry_percent * num_tests_run: | |
498 sys.stdout.write("\nNOT RETRYING FAILED TESTS (too many failed)\n") | |
499 return 1 | |
500 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") | |
501 sharded_description = re.compile(r": (?:\d+>)?(.*)") | |
502 gtest_filters = [sharded_description.search(line).group(1) | |
503 for line in self.failed_tests] | |
504 sys.stdout.write("\nRETRY GTEST FILTERS: %r\n" % gtest_filters) | |
505 failed_retries = [] | |
506 | |
507 for test_filter in gtest_filters: | |
508 args = [self.test, "--gtest_filter=" + test_filter] | |
509 # Don't update the xml output files during retry. | |
510 stripped_gtests_args = RemoveGTestOutput(self.gtest_args) | |
511 args.extend(stripped_gtests_args) | |
512 sys.stdout.write("\nRETRY COMMAND: %r\n" % args) | |
513 rerun = subprocess.Popen(args, stdout=sys.stdout, stderr=sys.stderr) | |
514 rerun.wait() | |
515 if rerun.returncode != 0: | |
516 failed_retries.append(test_filter) | |
517 | |
518 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") | |
519 self.PrintSummary(failed_retries) | |
520 return len(failed_retries) > 0 | |
521 | |
522 def PrintSummary(self, failed_tests): | |
523 """Prints a summary of the test results. | |
524 | |
525 If any shards had failing tests, the list is sorted and printed. Then all | |
526 the lines that indicate a test failure are reproduced. | |
527 """ | |
528 if failed_tests: | |
529 self.WriteText(sys.stdout, "FAILED TESTS:\n", "\x1b[1;5;31m") | |
530 for line in failed_tests: | |
531 sys.stdout.write(line) | |
532 else: | |
533 self.WriteText(sys.stdout, "ALL TESTS PASSED!\n", "\x1b[1;5;32m") | |
534 | |
535 def WriteText(self, pipe, text, ansi): | |
536 """Writes the text to the pipe with the ansi escape code, if colored | |
537 output is set, for Unix systems. | |
538 """ | |
539 if self.color: | |
540 pipe.write(ansi) | |
541 pipe.write(text) | |
542 if self.color: | |
543 pipe.write("\x1b[m") | |
544 | |
545 | |
546 def main(): | |
547 parser = optparse.OptionParser(usage=SS_USAGE) | |
548 parser.add_option( | |
549 "-n", "--shards_per_core", type="int", default=SS_DEFAULT_SHARDS_PER_CORE, | |
550 help="number of shards to generate per CPU") | |
551 parser.add_option( | |
552 "-r", "--runs_per_core", type="int", default=SS_DEFAULT_RUNS_PER_CORE, | |
553 help="number of shards to run in parallel per CPU") | |
554 parser.add_option( | |
555 "-c", "--color", action="store_true", | |
556 default=sys.platform != "win32" and sys.stdout.isatty(), | |
557 help="force color output, also used by gtest if --gtest_color is not" | |
558 " specified") | |
559 parser.add_option( | |
560 "--no-color", action="store_false", dest="color", | |
561 help="disable color output") | |
562 parser.add_option( | |
563 "-s", "--runshard", type="int", help="single shard index to run") | |
564 parser.add_option( | |
565 "--reorder", action="store_true", | |
566 help="ensure that all output from an earlier shard is printed before" | |
567 " output from a later shard") | |
568 # TODO(charleslee): for backwards compatibility with master.cfg file | |
569 parser.add_option( | |
570 "--original-order", action="store_true", | |
571 help="print shard output in its orginal jumbled order of execution" | |
572 " (useful for debugging flaky tests)") | |
573 parser.add_option( | |
574 "--prefix", action="store_true", | |
575 help="prefix each line of shard output with 'N>', where N is the shard" | |
576 " index (forced True when --original-order is True)") | |
577 parser.add_option( | |
578 "--random-seed", action="store_true", | |
579 help="shuffle the tests with a random seed value") | |
580 parser.add_option( | |
581 "--retry-failed", action="store_true", | |
582 help="retry tests that did not pass serially") | |
583 parser.add_option( | |
584 "--retry-percent", type="int", | |
585 default=SS_DEFAULT_RETRY_PERCENT, | |
586 help="ignore --retry-failed if more than this percent fail [0, 100]" | |
587 " (default = %i)" % SS_DEFAULT_RETRY_PERCENT) | |
588 parser.add_option( | |
589 "-t", "--timeout", type="int", default=SS_DEFAULT_TIMEOUT, | |
590 help="timeout in seconds to wait for a shard (default=%default s)") | |
591 parser.add_option( | |
592 "--total-slaves", type="int", default=1, | |
593 help="if running a subset, number of slaves sharing the test") | |
594 parser.add_option( | |
595 "--slave-index", type="int", default=0, | |
596 help="if running a subset, index of the slave to run tests for") | |
597 | |
598 parser.disable_interspersed_args() | |
599 (options, args) = parser.parse_args() | |
600 | |
601 if not args: | |
602 parser.error("You must specify a path to test!") | |
603 if not os.path.exists(args[0]): | |
604 parser.error("%s does not exist!" % args[0]) | |
605 | |
606 num_cores = DetectNumCores() | |
607 | |
608 if options.shards_per_core < 1: | |
609 parser.error("You must have at least 1 shard per core!") | |
610 num_shards_to_run = num_cores * options.shards_per_core | |
611 | |
612 if options.runs_per_core < 1: | |
613 parser.error("You must have at least 1 run per core!") | |
614 num_runs = num_cores * options.runs_per_core | |
615 | |
616 test = args[0] | |
617 gtest_args = ["--gtest_color=%s" % { | |
618 True: "yes", False: "no"}[options.color]] + args[1:] | |
619 | |
620 if options.original_order: | |
621 options.prefix = True | |
622 | |
623 # TODO(charleslee): for backwards compatibility with buildbot's log_parser | |
624 if options.reorder: | |
625 options.original_order = False | |
626 options.prefix = True | |
627 | |
628 if options.random_seed: | |
629 seed = random.randint(1, 99999) | |
630 gtest_args.extend(["--gtest_shuffle", "--gtest_random_seed=%i" % seed]) | |
631 | |
632 if options.retry_failed: | |
633 if options.retry_percent < 0 or options.retry_percent > 100: | |
634 parser.error("Retry percent must be an integer [0, 100]!") | |
635 else: | |
636 options.retry_percent = -1 | |
637 | |
638 if options.runshard != None: | |
639 # run a single shard and exit | |
640 if (options.runshard < 0 or options.runshard >= num_shards_to_run): | |
641 parser.error("Invalid shard number given parameters!") | |
642 shard = RunShard( | |
643 test, num_shards_to_run, options.runshard, gtest_args, None, None) | |
644 shard.communicate() | |
645 return shard.poll() | |
646 | |
647 # When running browser_tests, load the test binary into memory before running | |
648 # any tests. This is needed to prevent loading it from disk causing the first | |
649 # run tests to timeout flakily. See: http://crbug.com/124260 | |
650 if "browser_tests" in test: | |
651 args = [test] | |
652 args.extend(gtest_args) | |
653 args.append("--warmup") | |
654 result = subprocess.call(args, | |
655 bufsize=0, | |
656 universal_newlines=True) | |
657 # If the test fails, don't run anything else. | |
658 if result != 0: | |
659 return result | |
660 | |
661 # shard and run the whole test | |
662 ss = ShardingSupervisor( | |
663 test, num_shards_to_run, num_runs, options.color, | |
664 options.original_order, options.prefix, options.retry_percent, | |
665 options.timeout, options.total_slaves, options.slave_index, gtest_args) | |
666 return ss.ShardTest() | |
667 | |
668 | |
669 if __name__ == "__main__": | |
670 sys.exit(main()) | |
OLD | NEW |