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 | |
M-A Ruel
2012/04/13 20:07:44
"return None" is not strictly necessary.
| |
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 | |
89 new_value = current_value + value | |
90 new_arg = '--gtest_output=' + new_value | |
91 args.append(new_arg) | |
M-A Ruel
2012/04/13 20:07:44
args.append('--gtest_output=' + current_value + va
nsylvain
2012/04/13 20:30:58
Done.
| |
92 | |
93 return args | |
94 | |
95 | |
96 def RemoveGTestOutput(gtest_args): | |
97 args = gtest_args[:] | |
98 current_value = GetGTestOutput(args) | |
99 if not current_value: | |
100 return | |
101 | |
102 current_arg = '--gtest_output=' + current_value | |
M-A Ruel
2012/04/13 20:07:44
args.remove('--gtest_output=' + current_value)
is
nsylvain
2012/04/13 20:30:58
Done.
| |
103 args.remove(current_arg) | |
104 | |
105 return args | |
106 | |
107 | |
108 def AppendToXML(final_xml, generic_path, shard): | |
M-A Ruel
2012/04/13 20:07:44
Can you add a one-liner docstring?
nsylvain
2012/04/13 20:30:58
Done.
| |
109 path = generic_path + str(shard) | |
110 shard_xml_file = open(path) | |
M-A Ruel
2012/04/13 20:07:44
with open(path) as shard_xml_file:
nsylvain
2012/04/13 20:30:58
Done.
| |
111 shard_xml = minidom.parse(shard_xml_file) | |
112 if not final_xml: | |
113 # Out final xml is empty, let's prepopulate it with the first one we see. | |
114 return shard_xml | |
115 | |
116 shard_node = shard_xml.documentElement | |
117 final_node = final_xml.documentElement | |
118 | |
119 testcases = shard_node.getElementsByTagName('testcase') | |
M-A Ruel
2012/04/13 20:07:44
I still think you should have used ElementTree ins
| |
120 final_testcases = final_node.getElementsByTagName('testcase') | |
121 for testcase in testcases: | |
122 name = testcase.getAttribute('name') | |
123 classname = testcase.getAttribute('classname') | |
124 failures = testcase.getElementsByTagName('failure') | |
125 status = testcase.getAttribute('status') | |
126 elapsed = testcase.getAttribute('time') | |
127 | |
128 # don't bother updating the final xml if there is no data. | |
129 if status == 'notrun': | |
130 continue | |
131 | |
132 # Look in our final xml to see if it's there. | |
133 # There has to be a better way... | |
134 for final_testcase in final_testcases: | |
135 final_name = final_testcase.getAttribute('name') | |
136 final_classname = final_testcase.getAttribute('classname') | |
137 if final_name == name and final_classname == classname: | |
138 # We got the same entry. | |
139 final_testcase.setAttribute('status', status) | |
140 final_testcase.setAttribute('time', elapsed) | |
141 for failure in failures: | |
142 final_testcase.appendChild(failure) | |
143 | |
144 return final_xml | |
145 | |
146 | |
69 def RunShard(test, total_shards, index, gtest_args, stdout, stderr): | 147 def RunShard(test, total_shards, index, gtest_args, stdout, stderr): |
70 """Runs a single test shard in a subprocess. | 148 """Runs a single test shard in a subprocess. |
71 | 149 |
72 Returns: | 150 Returns: |
73 The Popen object representing the subprocess handle. | 151 The Popen object representing the subprocess handle. |
74 """ | 152 """ |
75 args = [test] | 153 args = [test] |
76 args.extend(gtest_args) | 154 |
155 # If there is a gtest_output | |
156 test_args = AppendToGTestOutput(gtest_args, str(index)) | |
157 args.extend(test_args) | |
77 env = os.environ.copy() | 158 env = os.environ.copy() |
78 env["GTEST_TOTAL_SHARDS"] = str(total_shards) | 159 env["GTEST_TOTAL_SHARDS"] = str(total_shards) |
79 env["GTEST_SHARD_INDEX"] = str(index) | 160 env["GTEST_SHARD_INDEX"] = str(index) |
80 | 161 |
81 # Use a unique log file for each shard | 162 # Use a unique log file for each shard |
82 # Allows ui_tests to be run in parallel on the same machine | 163 # Allows ui_tests to be run in parallel on the same machine |
83 env["CHROME_LOG_FILE"] = "chrome_log_%d" % index | 164 env["CHROME_LOG_FILE"] = "chrome_log_%d" % index |
84 | 165 |
85 return subprocess.Popen( | 166 return subprocess.Popen( |
86 args, stdout=stdout, | 167 args, stdout=stdout, |
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
275 worker = ShardRunner( | 356 worker = ShardRunner( |
276 self, counter, test_start, test_ok, test_fail) | 357 self, counter, test_start, test_ok, test_fail) |
277 worker.start() | 358 worker.start() |
278 workers.append(worker) | 359 workers.append(worker) |
279 if self.original_order: | 360 if self.original_order: |
280 for worker in workers: | 361 for worker in workers: |
281 worker.join() | 362 worker.join() |
282 else: | 363 else: |
283 self.WaitForShards() | 364 self.WaitForShards() |
284 | 365 |
366 # All the shards are done. Merge all the XML files and generate the | |
367 # main one. | |
368 output_arg = GetGTestOutput(self.gtest_args) | |
369 if output_arg: | |
370 xml, xml_path = output_arg.split(':') | |
371 assert(xml == 'xml') | |
372 final_xml = None | |
373 for i in range(start_point, start_point + self.num_shards_to_run): | |
374 final_xml = AppendToXML(final_xml, xml_path, i) | |
M-A Ruel
2012/04/13 20:07:44
can you do the str(i) here instead of at line 109?
| |
375 final_file = open(xml_path, 'w') | |
M-A Ruel
2012/04/13 20:07:44
with open(xml_path, 'w') as final_file:
final_xm
nsylvain
2012/04/13 20:30:58
Done.
| |
376 final_xml.writexml(final_file) | |
377 final_file.close() | |
378 | |
285 num_failed = len(self.failed_shards) | 379 num_failed = len(self.failed_shards) |
286 if num_failed > 0: | 380 if num_failed > 0: |
287 self.failed_shards.sort() | 381 self.failed_shards.sort() |
288 self.WriteText(sys.stdout, | 382 self.WriteText(sys.stdout, |
289 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), | 383 "\nFAILED SHARDS: %s\n" % str(self.failed_shards), |
290 "\x1b[1;5;31m") | 384 "\x1b[1;5;31m") |
291 else: | 385 else: |
292 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") | 386 self.WriteText(sys.stdout, "\nALL SHARDS PASSED!\n", "\x1b[1;5;32m") |
293 self.PrintSummary(self.failed_tests) | 387 self.PrintSummary(self.failed_tests) |
294 if self.retry_percent < 0: | 388 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") | 478 sys.stdout.write("\nNOT RETRYING FAILED TESTS (too many failed)\n") |
385 return 1 | 479 return 1 |
386 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") | 480 self.WriteText(sys.stdout, "\nRETRYING FAILED TESTS:\n", "\x1b[1;5;33m") |
387 sharded_description = re.compile(r": (?:\d+>)?(.*)") | 481 sharded_description = re.compile(r": (?:\d+>)?(.*)") |
388 gtest_filters = [sharded_description.search(line).group(1) | 482 gtest_filters = [sharded_description.search(line).group(1) |
389 for line in self.failed_tests] | 483 for line in self.failed_tests] |
390 failed_retries = [] | 484 failed_retries = [] |
391 | 485 |
392 for test_filter in gtest_filters: | 486 for test_filter in gtest_filters: |
393 args = [self.test, "--gtest_filter=" + test_filter] | 487 args = [self.test, "--gtest_filter=" + test_filter] |
394 args.extend(self.gtest_args) | 488 # Don't update the xml output files during retry. |
489 stripped_gtests_args = RemoveGTestOutput(self.gtest_args) | |
490 args.extend(stripped_gtests_args) | |
395 rerun = subprocess.Popen(args) | 491 rerun = subprocess.Popen(args) |
396 rerun.wait() | 492 rerun.wait() |
397 if rerun.returncode != 0: | 493 if rerun.returncode != 0: |
398 failed_retries.append(test_filter) | 494 failed_retries.append(test_filter) |
399 | 495 |
400 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") | 496 self.WriteText(sys.stdout, "RETRY RESULTS:\n", "\x1b[1;5;33m") |
401 self.PrintSummary(failed_retries) | 497 self.PrintSummary(failed_retries) |
402 return len(failed_retries) > 0 | 498 return len(failed_retries) > 0 |
403 | 499 |
404 def PrintSummary(self, failed_tests): | 500 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 | 624 # shard and run the whole test |
529 ss = ShardingSupervisor( | 625 ss = ShardingSupervisor( |
530 args[0], num_shards_to_run, num_runs, options.color, | 626 args[0], num_shards_to_run, num_runs, options.color, |
531 options.original_order, options.prefix, options.retry_percent, | 627 options.original_order, options.prefix, options.retry_percent, |
532 options.timeout, options.total_slaves, options.slave_index, gtest_args) | 628 options.timeout, options.total_slaves, options.slave_index, gtest_args) |
533 return ss.ShardTest() | 629 return ss.ShardTest() |
534 | 630 |
535 | 631 |
536 if __name__ == "__main__": | 632 if __name__ == "__main__": |
537 sys.exit(main()) | 633 sys.exit(main()) |
OLD | NEW |