| 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 |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 74 | 74 |
| 75 | 75 |
| 76 class ShardRunner(threading.Thread): | 76 class ShardRunner(threading.Thread): |
| 77 """Worker thread that manages a single shard at a time. | 77 """Worker thread that manages a single shard at a time. |
| 78 | 78 |
| 79 Attributes: | 79 Attributes: |
| 80 supervisor: The ShardingSupervisor that this worker reports to. | 80 supervisor: The ShardingSupervisor that this worker reports to. |
| 81 counter: Called to get the next shard index to run. | 81 counter: Called to get the next shard index to run. |
| 82 """ | 82 """ |
| 83 | 83 |
| 84 def __init__(self, supervisor, counter, test_fail, test_timeout): | 84 def __init__(self, supervisor, counter, test_start, test_ok, test_fail): |
| 85 """Inits ShardRunner with a supervisor and counter.""" | 85 """Inits ShardRunner with a supervisor and counter.""" |
| 86 threading.Thread.__init__(self) | 86 threading.Thread.__init__(self) |
| 87 self.supervisor = supervisor | 87 self.supervisor = supervisor |
| 88 self.counter = counter | 88 self.counter = counter |
| 89 self.test_start = test_start |
| 90 self.test_ok = test_ok |
| 89 self.test_fail = test_fail | 91 self.test_fail = test_fail |
| 90 self.test_timeout = test_timeout | 92 self.current_test = "" |
| 91 | 93 |
| 92 def SearchForFailure(self, regex, prefix, line, description): | 94 def ReportFailure(self, description, prefix, test_name): |
| 93 results = regex.search(line) | 95 log_line = "%s: %s%s\n" % (description, prefix, test_name) |
| 96 self.supervisor.LogLineFailure(log_line) |
| 97 |
| 98 def ProcessLine(self, prefix, line): |
| 99 results = self.test_start.search(line) |
| 94 if results: | 100 if results: |
| 95 log_line = "%s: %s%s\n" % (description, prefix, results.group(1)) | 101 if self.current_test: |
| 96 self.supervisor.LogLineFailure(log_line) | 102 self.ReportFailure("INCOMPLETE", prefix, self.current_test) |
| 97 return True | 103 self.current_test = results.group(1) |
| 98 return False | 104 return |
| 105 |
| 106 results = self.test_ok.search(line) |
| 107 if results: |
| 108 self.current_test = "" |
| 109 return |
| 110 |
| 111 results = self.test_fail.search(line) |
| 112 if results: |
| 113 self.ReportFailure("FAILED", prefix, results.group(1)) |
| 114 self.current_test = "" |
| 99 | 115 |
| 100 def run(self): | 116 def run(self): |
| 101 """Runs shards and outputs the results. | 117 """Runs shards and outputs the results. |
| 102 | 118 |
| 103 Gets the next shard index from the supervisor, runs it in a subprocess, | 119 Gets the next shard index from the supervisor, runs it in a subprocess, |
| 104 and collects the output. Each line is prefixed with 'N>', where N is the | 120 and collects the output. Each line is prefixed with 'N>', where N is the |
| 105 current shard index. | 121 current shard index. |
| 106 """ | 122 """ |
| 107 while True: | 123 while True: |
| 108 try: | 124 try: |
| 109 index = self.counter.get_nowait() | 125 index = self.counter.get_nowait() |
| 110 except Queue.Empty: | 126 except Queue.Empty: |
| 111 break | 127 break |
| 112 prefix = "%i>" % index | 128 prefix = "%i>" % index |
| 113 chars = StringIO() | 129 chars = StringIO() |
| 114 shard_running = True | 130 shard_running = True |
| 115 shard = RunShard( | 131 shard = RunShard( |
| 116 self.supervisor.test, self.supervisor.num_shards, index, | 132 self.supervisor.test, self.supervisor.num_shards, index, |
| 117 self.supervisor.gtest_args, subprocess.PIPE, subprocess.STDOUT) | 133 self.supervisor.gtest_args, subprocess.PIPE, subprocess.STDOUT) |
| 118 while shard_running: | 134 while shard_running: |
| 119 char = shard.stdout.read(1) | 135 char = shard.stdout.read(1) |
| 120 if not char and shard.poll() is not None: | 136 if not char and shard.poll() is not None: |
| 121 shard_running = False | 137 shard_running = False |
| 122 chars.write(char) | 138 chars.write(char) |
| 123 if char == "\n" or not shard_running: | 139 if char == "\n" or not shard_running: |
| 124 line = chars.getvalue() | 140 line = chars.getvalue() |
| 125 if not line and not shard_running: | 141 if not line and not shard_running: |
| 126 break | 142 break |
| 127 if not self.SearchForFailure( | 143 self.ProcessLine(prefix, line) |
| 128 self.test_fail, prefix, line, "FAILED"): | |
| 129 self.SearchForFailure(self.test_timeout, prefix, line, "TIMEOUT") | |
| 130 line = prefix + line | 144 line = prefix + line |
| 131 self.supervisor.LogOutputLine(index, line) | 145 self.supervisor.LogOutputLine(index, line) |
| 132 chars.close() | 146 chars.close() |
| 133 chars = StringIO() | 147 chars = StringIO() |
| 134 self.supervisor.ShardIndexCompleted(index) | 148 self.supervisor.ShardIndexCompleted(index) |
| 135 if shard.returncode != 0: | 149 if shard.returncode != 0: |
| 136 self.supervisor.LogShardFailure(index) | 150 self.supervisor.LogShardFailure(index) |
| 137 | 151 |
| 138 | 152 |
| 139 class ShardingSupervisor(object): | 153 class ShardingSupervisor(object): |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 178 """ | 192 """ |
| 179 | 193 |
| 180 # Regular expressions for parsing GTest logs. Test names look like | 194 # Regular expressions for parsing GTest logs. Test names look like |
| 181 # SomeTestCase.SomeTest | 195 # SomeTestCase.SomeTest |
| 182 # SomeName/SomeTestCase.SomeTest/1 | 196 # SomeName/SomeTestCase.SomeTest/1 |
| 183 # This regex also matches SomeName.SomeTest/1 and | 197 # This regex also matches SomeName.SomeTest/1 and |
| 184 # SomeName/SomeTestCase.SomeTest, which should be harmless. | 198 # SomeName/SomeTestCase.SomeTest, which should be harmless. |
| 185 test_name_regex = r"((\w+/)?\w+\.\w+(/\d+)?)" | 199 test_name_regex = r"((\w+/)?\w+\.\w+(/\d+)?)" |
| 186 | 200 |
| 187 # Regex for filtering out ANSI escape codes when using color. | 201 # Regex for filtering out ANSI escape codes when using color. |
| 188 ansi_code_regex = r"(?:\x1b\[.*?[a-zA-Z])?" | 202 ansi_regex = r"(?:\x1b\[.*?[a-zA-Z])?" |
| 189 | 203 |
| 204 test_start = re.compile( |
| 205 ansi_regex + r"\[\s+RUN\s+\] " + ansi_regex + test_name_regex) |
| 206 test_ok = re.compile( |
| 207 ansi_regex + r"\[\s+OK\s+\] " + ansi_regex + test_name_regex) |
| 190 test_fail = re.compile( | 208 test_fail = re.compile( |
| 191 ansi_code_regex + "\[\s+FAILED\s+\] " + ansi_code_regex + | 209 ansi_regex + r"\[\s+FAILED\s+\] " + ansi_regex + test_name_regex) |
| 192 test_name_regex) | |
| 193 test_timeout = re.compile( | |
| 194 "Test timeout \([0-9]+ ms\) exceeded for " + test_name_regex) | |
| 195 | 210 |
| 196 workers = [] | 211 workers = [] |
| 197 counter = Queue.Queue() | 212 counter = Queue.Queue() |
| 198 for i in range(self.num_shards): | 213 for i in range(self.num_shards): |
| 199 counter.put(i) | 214 counter.put(i) |
| 200 | 215 |
| 201 # Disable stdout buffering to read shard output one character at a time. | |
| 202 # This allows for shard crashes that do not end with a "\n". | |
| 203 sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 0) | |
| 204 | |
| 205 for i in range(self.num_runs): | 216 for i in range(self.num_runs): |
| 206 worker = ShardRunner(self, counter, test_fail, test_timeout) | 217 worker = ShardRunner( |
| 218 self, counter, test_start, test_ok, test_fail) |
| 207 worker.start() | 219 worker.start() |
| 208 workers.append(worker) | 220 workers.append(worker) |
| 209 if self.reorder: | 221 if self.reorder: |
| 210 self.WaitForShards() | 222 self.WaitForShards() |
| 211 else: | 223 else: |
| 212 for worker in workers: | 224 for worker in workers: |
| 213 worker.join() | 225 worker.join() |
| 214 | 226 |
| 215 # Re-enable stdout buffering. | 227 return self.PrintSummary(self.failure_log) |
| 216 sys.stdout = sys.__stdout__ | |
| 217 | |
| 218 return self.PrintSummary() | |
| 219 | 228 |
| 220 def LogLineFailure(self, line): | 229 def LogLineFailure(self, line): |
| 221 """Saves a line in the failure log to be printed at the end. | 230 """Saves a line in the failure log to be printed at the end. |
| 222 | 231 |
| 223 Args: | 232 Args: |
| 224 line: The line to save in the failure_log. | 233 line: The line to save in the failure_log. |
| 225 """ | 234 """ |
| 226 if line not in self.failure_log: | 235 if line not in self.failure_log: |
| 227 self.failure_log.append(line) | 236 self.failure_log.append(line) |
| 228 | 237 |
| (...skipping 15 matching lines...) Expand all Loading... |
| 244 | 253 |
| 245 def LogOutputLine(self, index, line): | 254 def LogOutputLine(self, index, line): |
| 246 if self.reorder: | 255 if self.reorder: |
| 247 self.shard_output[index].put(line) | 256 self.shard_output[index].put(line) |
| 248 else: | 257 else: |
| 249 sys.stdout.write(line) | 258 sys.stdout.write(line) |
| 250 | 259 |
| 251 def ShardIndexCompleted(self, index): | 260 def ShardIndexCompleted(self, index): |
| 252 self.shard_output[index].put(self.SHARD_COMPLETED) | 261 self.shard_output[index].put(self.SHARD_COMPLETED) |
| 253 | 262 |
| 254 def PrintSummary(self): | 263 def PrintSummary(self, failed_tests): |
| 255 """Prints a summary of the test results. | 264 """Prints a summary of the test results. |
| 256 | 265 |
| 257 If any shards had failing tests, the list is sorted and printed. Then all | 266 If any shards had failing tests, the list is sorted and printed. Then all |
| 258 the lines that indicate a test failure are reproduced. | 267 the lines that indicate a test failure are reproduced. |
| 259 | 268 |
| 260 Returns: | 269 Returns: |
| 261 The number of shards that had failing tests. | 270 The number of shards that had failing tests. |
| 262 """ | 271 """ |
| 263 sys.stderr.write("\n") | 272 sys.stderr.write("\n") |
| 264 num_failed = len(self.failed_shards) | 273 num_failed = len(self.failed_shards) |
| 265 if num_failed > 0: | 274 if num_failed > 0: |
| 266 self.failed_shards.sort() | 275 self.failed_shards.sort() |
| 267 if self.color: | 276 self.WriteText(sys.stderr, |
| 268 sys.stderr.write("\x1b[1;5;31m") | 277 "FAILED SHARDS: %s\n" % str(self.failed_shards), |
| 269 sys.stderr.write("SHARDS THAT FAILED: %s\n" % str(self.failed_shards)) | 278 "\x1b[1;5;31m") |
| 270 else: | 279 else: |
| 271 if self.color: | 280 self.WriteText(sys.stderr, "ALL SHARDS PASSED!\n", "\x1b[1;5;32m") |
| 272 sys.stderr.write("\x1b[1;5;32m") | 281 if failed_tests: |
| 273 sys.stderr.write("ALL SHARDS PASSED!\n") | 282 self.WriteText(sys.stderr, "FAILED TESTS:\n", "\x1b[1;5;31m") |
| 274 if self.failure_log: | 283 for line in failed_tests: |
| 275 if self.color: | |
| 276 sys.stderr.write("\x1b[1;5;31m") | |
| 277 sys.stderr.write("TESTS THAT DID NOT PASS:\n") | |
| 278 if self.color: | |
| 279 sys.stderr.write("\x1b[m") | |
| 280 for line in self.failure_log: | |
| 281 sys.stderr.write(line) | 284 sys.stderr.write(line) |
| 282 if self.color: | 285 if self.color: |
| 283 sys.stderr.write("\x1b[m") | 286 sys.stderr.write("\x1b[m") |
| 284 return num_failed | 287 return num_failed |
| 285 | 288 |
| 289 def WriteText(self, pipe, text, ansi): |
| 290 if self.color: |
| 291 pipe.write(ansi) |
| 292 pipe.write(text) |
| 293 if self.color: |
| 294 pipe.write("\x1b[m") |
| 295 |
| 286 | 296 |
| 287 def main(): | 297 def main(): |
| 288 parser = optparse.OptionParser(usage=SS_USAGE) | 298 parser = optparse.OptionParser(usage=SS_USAGE) |
| 289 parser.add_option( | 299 parser.add_option( |
| 290 "-n", "--shards_per_core", type="int", default=SS_DEFAULT_SHARDS_PER_CORE, | 300 "-n", "--shards_per_core", type="int", default=SS_DEFAULT_SHARDS_PER_CORE, |
| 291 help="number of shards to generate per CPU") | 301 help="number of shards to generate per CPU") |
| 292 parser.add_option( | 302 parser.add_option( |
| 293 "-r", "--runs_per_core", type="int", default=SS_DEFAULT_RUNS_PER_CORE, | 303 "-r", "--runs_per_core", type="int", default=SS_DEFAULT_RUNS_PER_CORE, |
| 294 help="number of shards to run in parallel per CPU") | 304 help="number of shards to run in parallel per CPU") |
| 295 parser.add_option( | 305 parser.add_option( |
| 296 "-c", "--color", action="store_true", default=sys.stdout.isatty(), | 306 "-c", "--color", action="store_true", |
| 307 default=sys.platform != "win32" and sys.stdout.isatty(), |
| 297 help="force color output, also used by gtest if --gtest_color is not" | 308 help="force color output, also used by gtest if --gtest_color is not" |
| 298 " specified") | 309 " specified") |
| 299 parser.add_option( | 310 parser.add_option( |
| 300 "--no-color", action="store_false", dest="color", | 311 "--no-color", action="store_false", dest="color", |
| 301 help="disable color output") | 312 help="disable color output") |
| 302 parser.add_option( | 313 parser.add_option( |
| 303 "-s", "--runshard", type="int", help="single shard index to run") | 314 "-s", "--runshard", type="int", help="single shard index to run") |
| 304 parser.add_option( | 315 parser.add_option( |
| 305 "--reorder", action="store_true", | 316 "--reorder", action="store_true", |
| 306 help="ensure that all output from an earlier shard is printed before" | 317 help="ensure that all output from an earlier shard is printed before" |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 343 | 354 |
| 344 # shard and run the whole test | 355 # shard and run the whole test |
| 345 ss = ShardingSupervisor(args[0], num_shards, num_runs, options.color, | 356 ss = ShardingSupervisor(args[0], num_shards, num_runs, options.color, |
| 346 options.reorder, gtest_args) | 357 options.reorder, gtest_args) |
| 347 return ss.ShardTest() | 358 return ss.ShardTest() |
| 348 | 359 |
| 349 | 360 |
| 350 if __name__ == "__main__": | 361 if __name__ == "__main__": |
| 351 sys.exit(main()) | 362 sys.exit(main()) |
| 352 | 363 |
| OLD | NEW |