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(':') | |
M-A Ruel
2012/04/13 20:40:45
xml, xml_path = output_arg.split(':', 1)
nsylvain
2012/04/13 22:04:58
Done.
| |
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 final_file = open(xml_path, 'w') | |
M-A Ruel
2012/04/13 20:40:45
with open(xml_path, 'w') as final_file:
nsylvain
2012/04/13 22:04:58
Done.
| |
373 with open(xml_path, 'w') as final_file: | |
374 final_xml.writexml(final_file) | |
375 | |
285 num_failed = len(self.failed_shards) | 376 num_failed = len(self.failed_shards) |
286 if num_failed > 0: | 377 if num_failed > 0: |
287 self.failed_shards.sort() | 378 self.failed_shards.sort() |
288 self.WriteText(sys.stdout, | 379 self.WriteText(sys.stdout, |
289 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), | 380 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), |
290 "\x1b[1;5;31m") | 381 "\x1b[1;5;31m") |
291 else: | 382 else: |
292 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") | 383 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") |
293 self.PrintSummary(self.failed_tests) | 384 self.PrintSummary(self.failed_tests) |
294 if self.retry_percent < 0: | 385 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") | 475 sys.stdout.write("\nNOT RETRYING FAILED TESTS (too many failed)\n") |
385 return 1 | 476 return 1 |
386 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") | 477 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") |
387 sharded_description = re.compile(r": (?:\d+>)?(.*)") | 478 sharded_description = re.compile(r": (?:\d+>)?(.*)") |
388 gtest_filters = [sharded_description.search(line).group(1) | 479 gtest_filters = [sharded_description.search(line).group(1) |
389 for line in self.failed_tests] | 480 for line in self.failed_tests] |
390 failed_retries = [] | 481 failed_retries = [] |
391 | 482 |
392 for test_filter in gtest_filters: | 483 for test_filter in gtest_filters: |
393 args = [self.test, "--gtest_filter=" + test_filter] | 484 args = [self.test, "--gtest_filter=" + test_filter] |
394 args.extend(self.gtest_args) | 485 # Don't update the xml output files during retry. |
486 stripped_gtests_args = RemoveGTestOutput(self.gtest_args) | |
487 args.extend(stripped_gtests_args) | |
395 rerun = subprocess.Popen(args) | 488 rerun = subprocess.Popen(args) |
396 rerun.wait() | 489 rerun.wait() |
397 if rerun.returncode != 0: | 490 if rerun.returncode != 0: |
398 failed_retries.append(test_filter) | 491 failed_retries.append(test_filter) |
399 | 492 |
400 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") | 493 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") |
401 self.PrintSummary(failed_retries) | 494 self.PrintSummary(failed_retries) |
402 return len(failed_retries) > 0 | 495 return len(failed_retries) > 0 |
403 | 496 |
404 def PrintSummary(self, failed_tests): | 497 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 | 621 # shard and run the whole test |
529 ss = ShardingSupervisor( | 622 ss = ShardingSupervisor( |
530 args[0], num_shards_to_run, num_runs, options.color, | 623 args[0], num_shards_to_run, num_runs, options.color, |
531 options.original_order, options.prefix, options.retry_percent, | 624 options.original_order, options.prefix, options.retry_percent, |
532 options.timeout, options.total_slaves, options.slave_index, gtest_args) | 625 options.timeout, options.total_slaves, options.slave_index, gtest_args) |
533 return ss.ShardTest() | 626 return ss.ShardTest() |
534 | 627 |
535 | 628 |
536 if __name__ == "__main__": | 629 if __name__ == "__main__": |
537 sys.exit(main()) | 630 sys.exit(main()) |
OLD | NEW |