OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Shards a given test suite and runs the shards in parallel. | 6 """Shards a given test suite and runs the shards in parallel. |
7 | 7 |
8 ShardingSupervisor is called to process the command line options and creates | 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 | 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 | 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 | 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 | 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. | 13 is started for that shard and the output is identical to gtest's output. |
14 """ | 14 """ |
15 | 15 |
16 | 16 |
17 import cStringIO | 17 import cStringIO |
18 import itertools | 18 import itertools |
19 import optparse | 19 import optparse |
20 import os | 20 import os |
21 import Queue | 21 import Queue |
22 import random | 22 import random |
23 import re | 23 import re |
24 import sys | 24 import sys |
25 import threading | 25 import threading |
26 | 26 |
| 27 from xml.dom import minidom |
| 28 |
27 # Add tools/ to path | 29 # Add tools/ to path |
28 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) | 30 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) |
29 sys.path.append(os.path.join(BASE_PATH, "..")) | 31 sys.path.append(os.path.join(BASE_PATH, "..")) |
30 try: | 32 try: |
31 import find_depot_tools | 33 import find_depot_tools |
32 # Fixes a bug in Windows where some shards die upon starting | 34 # Fixes a bug in Windows where some shards die upon starting |
33 # TODO(charleslee): actually fix this bug | 35 # TODO(charleslee): actually fix this bug |
34 import subprocess2 as subprocess | 36 import subprocess2 as subprocess |
35 except ImportError: | 37 except ImportError: |
36 # Unable to find depot_tools, so just use standard subprocess | 38 # Unable to find depot_tools, so just use standard subprocess |
(...skipping 22 matching lines...) Expand all Loading... |
59 return int(os.sysconf("SC_NPROCESSORS_ONLN")) | 61 return int(os.sysconf("SC_NPROCESSORS_ONLN")) |
60 else: | 62 else: |
61 # OSX | 63 # OSX |
62 return int(os.popen2("sysctl -n hw.ncpu")[1].read()) | 64 return int(os.popen2("sysctl -n hw.ncpu")[1].read()) |
63 # Windows | 65 # Windows |
64 return int(os.environ["NUMBER_OF_PROCESSORS"]) | 66 return int(os.environ["NUMBER_OF_PROCESSORS"]) |
65 except ValueError: | 67 except ValueError: |
66 return SS_DEFAULT_NUM_CORES | 68 return SS_DEFAULT_NUM_CORES |
67 | 69 |
68 | 70 |
| 71 def GetGTestOutput(args): |
| 72 """Extracts gtest_output from the args. Returns none if not present.""" |
| 73 |
| 74 for arg in args: |
| 75 if '--gtest_output=' in arg: |
| 76 return arg.split('=')[1] |
| 77 return None |
| 78 |
| 79 |
| 80 def AppendToGTestOutput(gtest_args, value): |
| 81 args = gtest_args[:] |
| 82 current_value = GetGTestOutput(args) |
| 83 if not current_value: |
| 84 return |
| 85 |
| 86 current_arg = '--gtest_output=' + current_value |
| 87 args.remove(current_arg) |
| 88 args.append('--gtest_output=' + current_value + value) |
| 89 return args |
| 90 |
| 91 |
| 92 def RemoveGTestOutput(gtest_args): |
| 93 args = gtest_args[:] |
| 94 current_value = GetGTestOutput(args) |
| 95 if not current_value: |
| 96 return |
| 97 |
| 98 args.remove('--gtest_output=' + current_value) |
| 99 return args |
| 100 |
| 101 |
| 102 def AppendToXML(final_xml, generic_path, shard): |
| 103 """Combine the shard xml file with the final xml file.""" |
| 104 |
| 105 path = generic_path + str(shard) |
| 106 with open(path) as shard_xml_file: |
| 107 shard_xml = minidom.parse(shard_xml_file) |
| 108 |
| 109 if not final_xml: |
| 110 # Out final xml is empty, let's prepopulate it with the first one we see. |
| 111 return shard_xml |
| 112 |
| 113 shard_node = shard_xml.documentElement |
| 114 final_node = final_xml.documentElement |
| 115 |
| 116 testcases = shard_node.getElementsByTagName('testcase') |
| 117 final_testcases = final_node.getElementsByTagName('testcase') |
| 118 for testcase in testcases: |
| 119 name = testcase.getAttribute('name') |
| 120 classname = testcase.getAttribute('classname') |
| 121 failures = testcase.getElementsByTagName('failure') |
| 122 status = testcase.getAttribute('status') |
| 123 elapsed = testcase.getAttribute('time') |
| 124 |
| 125 # don't bother updating the final xml if there is no data. |
| 126 if status == 'notrun': |
| 127 continue |
| 128 |
| 129 # Look in our final xml to see if it's there. |
| 130 # There has to be a better way... |
| 131 for final_testcase in final_testcases: |
| 132 final_name = final_testcase.getAttribute('name') |
| 133 final_classname = final_testcase.getAttribute('classname') |
| 134 if final_name == name and final_classname == classname: |
| 135 # We got the same entry. |
| 136 final_testcase.setAttribute('status', status) |
| 137 final_testcase.setAttribute('time', elapsed) |
| 138 for failure in failures: |
| 139 final_testcase.appendChild(failure) |
| 140 |
| 141 return final_xml |
| 142 |
| 143 |
69 def RunShard(test, total_shards, index, gtest_args, stdout, stderr): | 144 def RunShard(test, total_shards, index, gtest_args, stdout, stderr): |
70 """Runs a single test shard in a subprocess. | 145 """Runs a single test shard in a subprocess. |
71 | 146 |
72 Returns: | 147 Returns: |
73 The Popen object representing the subprocess handle. | 148 The Popen object representing the subprocess handle. |
74 """ | 149 """ |
75 args = [test] | 150 args = [test] |
76 args.extend(gtest_args) | 151 |
| 152 # If there is a gtest_output |
| 153 test_args = AppendToGTestOutput(gtest_args, str(index)) |
| 154 args.extend(test_args) |
77 env = os.environ.copy() | 155 env = os.environ.copy() |
78 env["GTEST_TOTAL_SHARDS"] = str(total_shards) | 156 env["GTEST_TOTAL_SHARDS"] = str(total_shards) |
79 env["GTEST_SHARD_INDEX"] = str(index) | 157 env["GTEST_SHARD_INDEX"] = str(index) |
80 | 158 |
81 # Use a unique log file for each shard | 159 # Use a unique log file for each shard |
82 # Allows ui_tests to be run in parallel on the same machine | 160 # Allows ui_tests to be run in parallel on the same machine |
83 env["CHROME_LOG_FILE"] = "chrome_log_%d" % index | 161 env["CHROME_LOG_FILE"] = "chrome_log_%d" % index |
84 | 162 |
85 return subprocess.Popen( | 163 return subprocess.Popen( |
86 args, stdout=stdout, | 164 args, stdout=stdout, |
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
275 worker = ShardRunner( | 353 worker = ShardRunner( |
276 self, counter, test_start, test_ok, test_fail) | 354 self, counter, test_start, test_ok, test_fail) |
277 worker.start() | 355 worker.start() |
278 workers.append(worker) | 356 workers.append(worker) |
279 if self.original_order: | 357 if self.original_order: |
280 for worker in workers: | 358 for worker in workers: |
281 worker.join() | 359 worker.join() |
282 else: | 360 else: |
283 self.WaitForShards() | 361 self.WaitForShards() |
284 | 362 |
| 363 # All the shards are done. Merge all the XML files and generate the |
| 364 # main one. |
| 365 output_arg = GetGTestOutput(self.gtest_args) |
| 366 if output_arg: |
| 367 xml, xml_path = output_arg.split(':', 1) |
| 368 assert(xml == 'xml') |
| 369 final_xml = None |
| 370 for i in range(start_point, start_point + self.num_shards_to_run): |
| 371 final_xml = AppendToXML(final_xml, xml_path, i) |
| 372 with open(xml_path, 'w') as final_file: |
| 373 final_xml.writexml(final_file) |
| 374 |
285 num_failed = len(self.failed_shards) | 375 num_failed = len(self.failed_shards) |
286 if num_failed > 0: | 376 if num_failed > 0: |
287 self.failed_shards.sort() | 377 self.failed_shards.sort() |
288 self.WriteText(sys.stdout, | 378 self.WriteText(sys.stdout, |
289 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), | 379 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), |
290 "\x1b[1;5;31m") | 380 "\x1b[1;5;31m") |
291 else: | 381 else: |
292 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") | 382 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") |
293 self.PrintSummary(self.failed_tests) | 383 self.PrintSummary(self.failed_tests) |
294 if self.retry_percent < 0: | 384 if self.retry_percent < 0: |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
384 sys.stdout.write("\nNOT RETRYING FAILED TESTS (too many failed)\n") | 474 sys.stdout.write("\nNOT RETRYING FAILED TESTS (too many failed)\n") |
385 return 1 | 475 return 1 |
386 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") | 476 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") |
387 sharded_description = re.compile(r": (?:\d+>)?(.*)") | 477 sharded_description = re.compile(r": (?:\d+>)?(.*)") |
388 gtest_filters = [sharded_description.search(line).group(1) | 478 gtest_filters = [sharded_description.search(line).group(1) |
389 for line in self.failed_tests] | 479 for line in self.failed_tests] |
390 failed_retries = [] | 480 failed_retries = [] |
391 | 481 |
392 for test_filter in gtest_filters: | 482 for test_filter in gtest_filters: |
393 args = [self.test, "--gtest_filter=" + test_filter] | 483 args = [self.test, "--gtest_filter=" + test_filter] |
394 args.extend(self.gtest_args) | 484 # Don't update the xml output files during retry. |
| 485 stripped_gtests_args = RemoveGTestOutput(self.gtest_args) |
| 486 args.extend(stripped_gtests_args) |
395 rerun = subprocess.Popen(args) | 487 rerun = subprocess.Popen(args) |
396 rerun.wait() | 488 rerun.wait() |
397 if rerun.returncode != 0: | 489 if rerun.returncode != 0: |
398 failed_retries.append(test_filter) | 490 failed_retries.append(test_filter) |
399 | 491 |
400 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") | 492 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") |
401 self.PrintSummary(failed_retries) | 493 self.PrintSummary(failed_retries) |
402 return len(failed_retries) > 0 | 494 return len(failed_retries) > 0 |
403 | 495 |
404 def PrintSummary(self, failed_tests): | 496 def PrintSummary(self, failed_tests): |
(...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
528 # shard and run the whole test | 620 # shard and run the whole test |
529 ss = ShardingSupervisor( | 621 ss = ShardingSupervisor( |
530 args[0], num_shards_to_run, num_runs, options.color, | 622 args[0], num_shards_to_run, num_runs, options.color, |
531 options.original_order, options.prefix, options.retry_percent, | 623 options.original_order, options.prefix, options.retry_percent, |
532 options.timeout, options.total_slaves, options.slave_index, gtest_args) | 624 options.timeout, options.total_slaves, options.slave_index, gtest_args) |
533 return ss.ShardTest() | 625 return ss.ShardTest() |
534 | 626 |
535 | 627 |
536 if __name__ == "__main__": | 628 if __name__ == "__main__": |
537 sys.exit(main()) | 629 sys.exit(main()) |
OLD | NEW |