OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 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 """Run layout tests using the test_shell. | 6 """Run layout tests using the test_shell. |
7 | 7 |
8 This is a port of the existing webkit test script run-webkit-tests. | 8 This is a port of the existing webkit test script run-webkit-tests. |
9 | 9 |
10 The TestRunner class runs a series of tests (TestType interface) against a set | 10 The TestRunner class runs a series of tests (TestType interface) against a set |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
59 LOG_DETAILED_PROGRESS = 'detailed-progress' | 59 LOG_DETAILED_PROGRESS = 'detailed-progress' |
60 | 60 |
61 # Log any unexpected results while running (instead of just at the end). | 61 # Log any unexpected results while running (instead of just at the end). |
62 LOG_UNEXPECTED = 'unexpected' | 62 LOG_UNEXPECTED = 'unexpected' |
63 | 63 |
64 # Builder base URL where we have the archived test results. | 64 # Builder base URL where we have the archived test results. |
65 BUILDER_BASE_URL = "http://build.chromium.org/buildbot/layout_test_results/" | 65 BUILDER_BASE_URL = "http://build.chromium.org/buildbot/layout_test_results/" |
66 | 66 |
67 TestExpectationsFile = test_expectations.TestExpectationsFile | 67 TestExpectationsFile = test_expectations.TestExpectationsFile |
68 | 68 |
69 | |
70 class TestInfo: | 69 class TestInfo: |
71 """Groups information about a test for easy passing of data.""" | 70 """Groups information about a test for easy passing of data.""" |
72 | 71 def __init__(self, filename, timeout): |
73 def __init__(self, filename, timeout): | 72 """Generates the URI and stores the filename and timeout for this test. |
74 """Generates the URI and stores the filename and timeout for this test. | 73 Args: |
75 Args: | 74 filename: Full path to the test. |
76 filename: Full path to the test. | 75 timeout: Timeout for running the test in TestShell. |
77 timeout: Timeout for running the test in TestShell. | 76 """ |
78 """ | 77 self.filename = filename |
79 self.filename = filename | 78 self.uri = path_utils.FilenameToUri(filename) |
80 self.uri = path_utils.FilenameToUri(filename) | 79 self.timeout = timeout |
81 self.timeout = timeout | 80 expected_hash_file = path_utils.ExpectedFilename(filename, '.checksum') |
82 expected_hash_file = path_utils.ExpectedFilename(filename, '.checksum') | 81 try: |
83 try: | 82 self.image_hash = open(expected_hash_file, "r").read() |
84 self.image_hash = open(expected_hash_file, "r").read() | 83 except IOError, e: |
85 except IOError, e: | 84 if errno.ENOENT != e.errno: |
86 if errno.ENOENT != e.errno: | 85 raise |
87 raise | 86 self.image_hash = None |
88 self.image_hash = None | |
89 | 87 |
90 | 88 |
91 class ResultSummary(object): | 89 class ResultSummary(object): |
92 """A class for partitioning the test results we get into buckets. | 90 """A class for partitioning the test results we get into buckets. |
93 | 91 |
94 This class is basically a glorified struct and it's private to this file | 92 This class is basically a glorified struct and it's private to this file |
95 so we don't bother with any information hiding.""" | 93 so we don't bother with any information hiding.""" |
96 | 94 def __init__(self, expectations, test_files): |
97 def __init__(self, expectations, test_files): | 95 self.total = len(test_files) |
98 self.total = len(test_files) | 96 self.remaining = self.total |
99 self.remaining = self.total | 97 self.expectations = expectations |
100 self.expectations = expectations | 98 self.expected = 0 |
101 self.expected = 0 | 99 self.unexpected = 0 |
102 self.unexpected = 0 | 100 self.tests_by_expectation = {} |
103 self.tests_by_expectation = {} | 101 self.tests_by_timeline = {} |
104 self.tests_by_timeline = {} | 102 self.results = {} |
105 self.results = {} | 103 self.unexpected_results = {} |
106 self.unexpected_results = {} | 104 self.failures = {} |
107 self.failures = {} | 105 self.tests_by_expectation[test_expectations.SKIP] = set() |
108 self.tests_by_expectation[test_expectations.SKIP] = set() | 106 for expectation in TestExpectationsFile.EXPECTATIONS.values(): |
109 for expectation in TestExpectationsFile.EXPECTATIONS.values(): | 107 self.tests_by_expectation[expectation] = set() |
110 self.tests_by_expectation[expectation] = set() | 108 for timeline in TestExpectationsFile.TIMELINES.values(): |
111 for timeline in TestExpectationsFile.TIMELINES.values(): | 109 self.tests_by_timeline[timeline] = expectations.GetTestsWithTimeline( |
112 self.tests_by_timeline[timeline] = ( | 110 timeline) |
113 expectations.GetTestsWithTimeline(timeline)) | 111 |
114 | 112 def Add(self, test, failures, result, expected): |
115 def Add(self, test, failures, result, expected): | 113 """Add a result into the appropriate bin. |
116 """Add a result into the appropriate bin. | 114 |
117 | 115 Args: |
118 Args: | 116 test: test file name |
119 test: test file name | 117 failures: list of failure objects from test execution |
120 failures: list of failure objects from test execution | 118 result: result of test (PASS, IMAGE, etc.). |
121 result: result of test (PASS, IMAGE, etc.). | 119 expected: whether the result was what we expected it to be. |
122 expected: whether the result was what we expected it to be. | 120 """ |
123 """ | 121 |
124 | 122 self.tests_by_expectation[result].add(test) |
125 self.tests_by_expectation[result].add(test) | 123 self.results[test] = result |
126 self.results[test] = result | 124 self.remaining -= 1 |
127 self.remaining -= 1 | 125 if len(failures): |
128 if len(failures): | 126 self.failures[test] = failures |
129 self.failures[test] = failures | 127 if expected: |
130 if expected: | 128 self.expected += 1 |
131 self.expected += 1 | 129 else: |
| 130 self.unexpected_results[test] = result |
| 131 self.unexpected += 1 |
| 132 |
| 133 |
| 134 class TestRunner: |
| 135 """A class for managing running a series of tests on a series of layout test |
| 136 files.""" |
| 137 |
| 138 HTTP_SUBDIR = os.sep.join(['', 'http', '']) |
| 139 WEBSOCKET_SUBDIR = os.sep.join(['', 'websocket', '']) |
| 140 |
| 141 # The per-test timeout in milliseconds, if no --time-out-ms option was given |
| 142 # to run_webkit_tests. This should correspond to the default timeout in |
| 143 # test_shell.exe. |
| 144 DEFAULT_TEST_TIMEOUT_MS = 6 * 1000 |
| 145 |
| 146 NUM_RETRY_ON_UNEXPECTED_FAILURE = 1 |
| 147 |
| 148 def __init__(self, options, meter): |
| 149 """Initialize test runner data structures. |
| 150 |
| 151 Args: |
| 152 options: a dictionary of command line options |
| 153 meter: a MeteredStream object to record updates to. |
| 154 """ |
| 155 self._options = options |
| 156 self._meter = meter |
| 157 |
| 158 if options.use_apache: |
| 159 self._http_server = apache_http_server.LayoutTestApacheHttpd( |
| 160 options.results_directory) |
| 161 else: |
| 162 self._http_server = http_server.Lighttpd(options.results_directory) |
| 163 |
| 164 self._websocket_server = websocket_server.PyWebSocket( |
| 165 options.results_directory) |
| 166 # disable wss server. need to install pyOpenSSL on buildbots. |
| 167 # self._websocket_secure_server = websocket_server.PyWebSocket( |
| 168 # options.results_directory, use_tls=True, port=9323) |
| 169 |
| 170 # a list of TestType objects |
| 171 self._test_types = [] |
| 172 |
| 173 # a set of test files, and the same tests as a list |
| 174 self._test_files = set() |
| 175 self._test_files_list = None |
| 176 self._file_dir = path_utils.GetAbsolutePath( |
| 177 os.path.dirname(os.path.dirname(sys.argv[0]))) |
| 178 self._result_queue = Queue.Queue() |
| 179 |
| 180 # These are used for --log detailed-progress to track status by directory. |
| 181 self._current_dir = None |
| 182 self._current_progress_str = "" |
| 183 self._current_test_number = 0 |
| 184 |
| 185 def __del__(self): |
| 186 logging.debug("flushing stdout") |
| 187 sys.stdout.flush() |
| 188 logging.debug("flushing stderr") |
| 189 sys.stderr.flush() |
| 190 logging.debug("stopping http server") |
| 191 # Stop the http server. |
| 192 self._http_server.Stop() |
| 193 # Stop the Web Socket / Web Socket Secure servers. |
| 194 self._websocket_server.Stop() |
| 195 # self._websocket_secure_server.Stop() |
| 196 |
| 197 def GatherFilePaths(self, paths): |
| 198 """Find all the files to test. |
| 199 |
| 200 Args: |
| 201 paths: a list of globs to use instead of the defaults.""" |
| 202 self._test_files = test_files.GatherTestFiles(paths) |
| 203 |
| 204 def ParseExpectations(self, platform, is_debug_mode): |
| 205 """Parse the expectations from the test_list files and return a data |
| 206 structure holding them. Throws an error if the test_list files have invalid |
| 207 syntax. |
| 208 """ |
| 209 if self._options.lint_test_files: |
| 210 test_files = None |
| 211 else: |
| 212 test_files = self._test_files |
| 213 |
| 214 try: |
| 215 self._expectations = test_expectations.TestExpectations(test_files, |
| 216 self._file_dir, platform, is_debug_mode, |
| 217 self._options.lint_test_files) |
| 218 return self._expectations |
| 219 except Exception, err: |
| 220 if self._options.lint_test_files: |
| 221 print str(err) |
| 222 else: |
| 223 raise err |
| 224 |
| 225 def PrepareListsAndPrintOutput(self, write): |
| 226 """Create appropriate subsets of test lists and returns a ResultSummary |
| 227 object. Also prints expected test counts. |
| 228 |
| 229 Args: |
| 230 write: A callback to write info to (e.g., a LoggingWriter) or |
| 231 sys.stdout.write. |
| 232 """ |
| 233 |
| 234 # Remove skipped - both fixable and ignored - files from the |
| 235 # top-level list of files to test. |
| 236 num_all_test_files = len(self._test_files) |
| 237 write("Found: %d tests" % (len(self._test_files))) |
| 238 skipped = set() |
| 239 if num_all_test_files > 1 and not self._options.force: |
| 240 skipped = self._expectations.GetTestsWithResultType( |
| 241 test_expectations.SKIP) |
| 242 self._test_files -= skipped |
| 243 |
| 244 # Create a sorted list of test files so the subset chunk, if used, contains |
| 245 # alphabetically consecutive tests. |
| 246 self._test_files_list = list(self._test_files) |
| 247 if self._options.randomize_order: |
| 248 random.shuffle(self._test_files_list) |
| 249 else: |
| 250 self._test_files_list.sort() |
| 251 |
| 252 # If the user specifies they just want to run a subset of the tests, |
| 253 # just grab a subset of the non-skipped tests. |
| 254 if self._options.run_chunk or self._options.run_part: |
| 255 chunk_value = self._options.run_chunk or self._options.run_part |
| 256 test_files = self._test_files_list |
| 257 try: |
| 258 (chunk_num, chunk_len) = chunk_value.split(":") |
| 259 chunk_num = int(chunk_num) |
| 260 assert(chunk_num >= 0) |
| 261 test_size = int(chunk_len) |
| 262 assert(test_size > 0) |
| 263 except: |
| 264 logging.critical("invalid chunk '%s'" % chunk_value) |
| 265 sys.exit(1) |
| 266 |
| 267 # Get the number of tests |
| 268 num_tests = len(test_files) |
| 269 |
| 270 # Get the start offset of the slice. |
| 271 if self._options.run_chunk: |
| 272 chunk_len = test_size |
| 273 # In this case chunk_num can be really large. We need to make the |
| 274 # slave fit in the current number of tests. |
| 275 slice_start = (chunk_num * chunk_len) % num_tests |
| 276 else: |
| 277 # Validate the data. |
| 278 assert(test_size <= num_tests) |
| 279 assert(chunk_num <= test_size) |
| 280 |
| 281 # To count the chunk_len, and make sure we don't skip some tests, we |
| 282 # round to the next value that fits exacly all the parts. |
| 283 rounded_tests = num_tests |
| 284 if rounded_tests % test_size != 0: |
| 285 rounded_tests = num_tests + test_size - (num_tests % test_size) |
| 286 |
| 287 chunk_len = rounded_tests / test_size |
| 288 slice_start = chunk_len * (chunk_num - 1) |
| 289 # It does not mind if we go over test_size. |
| 290 |
| 291 # Get the end offset of the slice. |
| 292 slice_end = min(num_tests, slice_start + chunk_len) |
| 293 |
| 294 files = test_files[slice_start:slice_end] |
| 295 |
| 296 tests_run_msg = 'Running: %d tests (chunk slice [%d:%d] of %d)' % ( |
| 297 (slice_end - slice_start), slice_start, slice_end, num_tests) |
| 298 write(tests_run_msg) |
| 299 |
| 300 # If we reached the end and we don't have enough tests, we run some |
| 301 # from the beginning. |
| 302 if self._options.run_chunk and (slice_end - slice_start < chunk_len): |
| 303 extra = 1 + chunk_len - (slice_end - slice_start) |
| 304 extra_msg = ' last chunk is partial, appending [0:%d]' % extra |
| 305 write(extra_msg) |
| 306 tests_run_msg += "\n" + extra_msg |
| 307 files.extend(test_files[0:extra]) |
| 308 tests_run_filename = os.path.join(self._options.results_directory, |
| 309 "tests_run.txt") |
| 310 tests_run_file = open(tests_run_filename, "w") |
| 311 tests_run_file.write(tests_run_msg + "\n") |
| 312 tests_run_file.close() |
| 313 |
| 314 len_skip_chunk = int(len(files) * len(skipped) / |
| 315 float(len(self._test_files))) |
| 316 skip_chunk_list = list(skipped)[0:len_skip_chunk] |
| 317 skip_chunk = set(skip_chunk_list) |
| 318 |
| 319 # Update expectations so that the stats are calculated correctly. |
| 320 # We need to pass a list that includes the right # of skipped files |
| 321 # to ParseExpectations so that ResultSummary() will get the correct |
| 322 # stats. So, we add in the subset of skipped files, and then subtract |
| 323 # them back out. |
| 324 self._test_files_list = files + skip_chunk_list |
| 325 self._test_files = set(self._test_files_list) |
| 326 |
| 327 self._expectations = self.ParseExpectations( |
| 328 path_utils.PlatformName(), options.target == 'Debug') |
| 329 |
| 330 self._test_files = set(files) |
| 331 self._test_files_list = files |
| 332 else: |
| 333 skip_chunk = skipped |
| 334 |
| 335 result_summary = ResultSummary(self._expectations, |
| 336 self._test_files | skip_chunk) |
| 337 self._PrintExpectedResultsOfType(write, result_summary, |
| 338 test_expectations.PASS, "passes") |
| 339 self._PrintExpectedResultsOfType(write, result_summary, |
| 340 test_expectations.FAIL, "failures") |
| 341 self._PrintExpectedResultsOfType(write, result_summary, |
| 342 test_expectations.FLAKY, "flaky") |
| 343 self._PrintExpectedResultsOfType(write, result_summary, |
| 344 test_expectations.SKIP, "skipped") |
| 345 |
| 346 |
| 347 if self._options.force: |
| 348 write('Running all tests, including skips (--force)') |
| 349 else: |
| 350 # Note that we don't actually run the skipped tests (they were |
| 351 # subtracted out of self._test_files, above), but we stub out the |
| 352 # results here so the statistics can remain accurate. |
| 353 for test in skip_chunk: |
| 354 result_summary.Add(test, [], test_expectations.SKIP, expected=True) |
| 355 write("") |
| 356 |
| 357 return result_summary |
| 358 |
| 359 def AddTestType(self, test_type): |
| 360 """Add a TestType to the TestRunner.""" |
| 361 self._test_types.append(test_type) |
| 362 |
| 363 def _GetDirForTestFile(self, test_file): |
| 364 """Returns the highest-level directory by which to shard the given test |
| 365 file.""" |
| 366 index = test_file.rfind(os.sep + 'LayoutTests' + os.sep) |
| 367 |
| 368 test_file = test_file[index + len('LayoutTests/'):] |
| 369 test_file_parts = test_file.split(os.sep, 1) |
| 370 directory = test_file_parts[0] |
| 371 test_file = test_file_parts[1] |
| 372 |
| 373 # The http tests are very stable on mac/linux. |
| 374 # TODO(ojan): Make the http server on Windows be apache so we can turn |
| 375 # shard the http tests there as well. Switching to apache is what made them |
| 376 # stable on linux/mac. |
| 377 return_value = directory |
| 378 while ((directory != 'http' or sys.platform in ('darwin', 'linux2')) and |
| 379 test_file.find(os.sep) >= 0): |
| 380 test_file_parts = test_file.split(os.sep, 1) |
| 381 directory = test_file_parts[0] |
| 382 return_value = os.path.join(return_value, directory) |
| 383 test_file = test_file_parts[1] |
| 384 |
| 385 return return_value |
| 386 |
| 387 def _GetTestInfoForFile(self, test_file): |
| 388 """Returns the appropriate TestInfo object for the file. Mostly this is used |
| 389 for looking up the timeout value (in ms) to use for the given test.""" |
| 390 if self._expectations.HasModifier(test_file, test_expectations.SLOW): |
| 391 return TestInfo(test_file, self._options.slow_time_out_ms) |
| 392 return TestInfo(test_file, self._options.time_out_ms) |
| 393 |
| 394 def _GetTestFileQueue(self, test_files): |
| 395 """Create the thread safe queue of lists of (test filenames, test URIs) |
| 396 tuples. Each TestShellThread pulls a list from this queue and runs those |
| 397 tests in order before grabbing the next available list. |
| 398 |
| 399 Shard the lists by directory. This helps ensure that tests that depend |
| 400 on each other (aka bad tests!) continue to run together as most |
| 401 cross-tests dependencies tend to occur within the same directory. |
| 402 |
| 403 Return: |
| 404 The Queue of lists of TestInfo objects. |
| 405 """ |
| 406 |
| 407 if (self._options.experimental_fully_parallel or |
| 408 self._IsSingleThreaded()): |
| 409 filename_queue = Queue.Queue() |
| 410 for test_file in test_files: |
| 411 filename_queue.put(('.', [self._GetTestInfoForFile(test_file)])) |
| 412 return filename_queue |
| 413 |
| 414 tests_by_dir = {} |
| 415 for test_file in test_files: |
| 416 directory = self._GetDirForTestFile(test_file) |
| 417 tests_by_dir.setdefault(directory, []) |
| 418 tests_by_dir[directory].append(self._GetTestInfoForFile(test_file)) |
| 419 |
| 420 # Sort by the number of tests in the dir so that the ones with the most |
| 421 # tests get run first in order to maximize parallelization. Number of tests |
| 422 # is a good enough, but not perfect, approximation of how long that set of |
| 423 # tests will take to run. We can't just use a PriorityQueue until we move |
| 424 # to Python 2.6. |
| 425 test_lists = [] |
| 426 http_tests = None |
| 427 for directory in tests_by_dir: |
| 428 test_list = tests_by_dir[directory] |
| 429 # Keep the tests in alphabetical order. |
| 430 # TODO: Remove once tests are fixed so they can be run in any order. |
| 431 test_list.reverse() |
| 432 test_list_tuple = (directory, test_list) |
| 433 if directory == 'LayoutTests' + os.sep + 'http': |
| 434 http_tests = test_list_tuple |
| 435 else: |
| 436 test_lists.append(test_list_tuple) |
| 437 test_lists.sort(lambda a, b: cmp(len(b[1]), len(a[1]))) |
| 438 |
| 439 # Put the http tests first. There are only a couple hundred of them, but |
| 440 # each http test takes a very long time to run, so sorting by the number |
| 441 # of tests doesn't accurately capture how long they take to run. |
| 442 if http_tests: |
| 443 test_lists.insert(0, http_tests) |
| 444 |
| 445 filename_queue = Queue.Queue() |
| 446 for item in test_lists: |
| 447 filename_queue.put(item) |
| 448 return filename_queue |
| 449 |
| 450 def _GetTestShellArgs(self, index): |
| 451 """Returns the tuple of arguments for tests and for test_shell.""" |
| 452 shell_args = [] |
| 453 test_args = test_type_base.TestArguments() |
| 454 if not self._options.no_pixel_tests: |
| 455 png_path = os.path.join(self._options.results_directory, |
| 456 "png_result%s.png" % index) |
| 457 shell_args.append("--pixel-tests=" + png_path) |
| 458 test_args.png_path = png_path |
| 459 |
| 460 test_args.new_baseline = self._options.new_baseline |
| 461 |
| 462 test_args.show_sources = self._options.sources |
| 463 |
| 464 if self._options.startup_dialog: |
| 465 shell_args.append('--testshell-startup-dialog') |
| 466 |
| 467 if self._options.gp_fault_error_box: |
| 468 shell_args.append('--gp-fault-error-box') |
| 469 |
| 470 return (test_args, shell_args) |
| 471 |
| 472 def _ContainsTests(self, subdir): |
| 473 for test_file in self._test_files_list: |
| 474 if test_file.find(subdir) >= 0: |
| 475 return True |
| 476 return False |
| 477 |
| 478 def _InstantiateTestShellThreads(self, test_shell_binary, test_files, |
| 479 result_summary): |
| 480 """Instantitates and starts the TestShellThread(s). |
| 481 |
| 482 Return: |
| 483 The list of threads. |
| 484 """ |
| 485 test_shell_command = [test_shell_binary] |
| 486 |
| 487 if self._options.wrapper: |
| 488 # This split() isn't really what we want -- it incorrectly will |
| 489 # split quoted strings within the wrapper argument -- but in |
| 490 # practice it shouldn't come up and the --help output warns |
| 491 # about it anyway. |
| 492 test_shell_command = self._options.wrapper.split() + test_shell_command |
| 493 |
| 494 filename_queue = self._GetTestFileQueue(test_files) |
| 495 |
| 496 # Instantiate TestShellThreads and start them. |
| 497 threads = [] |
| 498 for i in xrange(int(self._options.num_test_shells)): |
| 499 # Create separate TestTypes instances for each thread. |
| 500 test_types = [] |
| 501 for t in self._test_types: |
| 502 test_types.append(t(self._options.platform, |
| 503 self._options.results_directory)) |
| 504 |
| 505 test_args, shell_args = self._GetTestShellArgs(i) |
| 506 thread = test_shell_thread.TestShellThread(filename_queue, |
| 507 self._result_queue, |
| 508 test_shell_command, |
| 509 test_types, |
| 510 test_args, |
| 511 shell_args, |
| 512 self._options) |
| 513 if self._IsSingleThreaded(): |
| 514 thread.RunInMainThread(self, result_summary) |
| 515 else: |
| 516 thread.start() |
| 517 threads.append(thread) |
| 518 |
| 519 return threads |
| 520 |
| 521 def _StopLayoutTestHelper(self, proc): |
| 522 """Stop the layout test helper and closes it down.""" |
| 523 if proc: |
| 524 logging.debug("Stopping layout test helper") |
| 525 proc.stdin.write("x\n") |
| 526 proc.stdin.close() |
| 527 proc.wait() |
| 528 |
| 529 def _IsSingleThreaded(self): |
| 530 """Returns whether we should run all the tests in the main thread.""" |
| 531 return int(self._options.num_test_shells) == 1 |
| 532 |
| 533 def _RunTests(self, test_shell_binary, file_list, result_summary): |
| 534 """Runs the tests in the file_list. |
| 535 |
| 536 Return: A tuple (failures, thread_timings, test_timings, |
| 537 individual_test_timings) |
| 538 failures is a map from test to list of failure types |
| 539 thread_timings is a list of dicts with the total runtime of each thread |
| 540 with 'name', 'num_tests', 'total_time' properties |
| 541 test_timings is a list of timings for each sharded subdirectory of the |
| 542 form [time, directory_name, num_tests] |
| 543 individual_test_timings is a list of run times for each test in the form |
| 544 {filename:filename, test_run_time:test_run_time} |
| 545 result_summary: summary object to populate with the results |
| 546 """ |
| 547 threads = self._InstantiateTestShellThreads(test_shell_binary, file_list, |
| 548 result_summary) |
| 549 |
| 550 # Wait for the threads to finish and collect test failures. |
| 551 failures = {} |
| 552 test_timings = {} |
| 553 individual_test_timings = [] |
| 554 thread_timings = [] |
| 555 try: |
| 556 for thread in threads: |
| 557 while thread.isAlive(): |
| 558 # Let it timeout occasionally so it can notice a KeyboardInterrupt |
| 559 # Actually, the timeout doesn't really matter: apparently it |
| 560 # suffices to not use an indefinite blocking join for it to |
| 561 # be interruptible by KeyboardInterrupt. |
| 562 thread.join(0.1) |
| 563 self.UpdateSummary(result_summary) |
| 564 thread_timings.append({ 'name': thread.getName(), |
| 565 'num_tests': thread.GetNumTests(), |
| 566 'total_time': thread.GetTotalTime()}); |
| 567 test_timings.update(thread.GetDirectoryTimingStats()) |
| 568 individual_test_timings.extend(thread.GetIndividualTestStats()) |
| 569 except KeyboardInterrupt: |
| 570 for thread in threads: |
| 571 thread.Cancel() |
| 572 self._StopLayoutTestHelper(layout_test_helper_proc) |
| 573 raise |
| 574 for thread in threads: |
| 575 # Check whether a TestShellThread died before normal completion. |
| 576 exception_info = thread.GetExceptionInfo() |
| 577 if exception_info is not None: |
| 578 # Re-raise the thread's exception here to make it clear that |
| 579 # testing was aborted. Otherwise, the tests that did not run |
| 580 # would be assumed to have passed. |
| 581 raise exception_info[0], exception_info[1], exception_info[2] |
| 582 |
| 583 # Make sure we pick up any remaining tests. |
| 584 self.UpdateSummary(result_summary) |
| 585 return (thread_timings, test_timings, individual_test_timings) |
| 586 |
| 587 def Run(self, result_summary): |
| 588 """Run all our tests on all our test files. |
| 589 |
| 590 For each test file, we run each test type. If there are any failures, we |
| 591 collect them for reporting. |
| 592 |
| 593 Args: |
| 594 result_summary: a summary object tracking the test results. |
| 595 |
| 596 Return: |
| 597 We return nonzero if there are regressions compared to the last run. |
| 598 """ |
| 599 if not self._test_files: |
| 600 return 0 |
| 601 start_time = time.time() |
| 602 test_shell_binary = path_utils.TestShellPath(self._options.target) |
| 603 |
| 604 # Start up any helper needed |
| 605 layout_test_helper_proc = None |
| 606 if not options.no_pixel_tests: |
| 607 helper_path = path_utils.LayoutTestHelperPath(self._options.target) |
| 608 if len(helper_path): |
| 609 logging.debug("Starting layout helper %s" % helper_path) |
| 610 layout_test_helper_proc = subprocess.Popen([helper_path], |
| 611 stdin=subprocess.PIPE, |
| 612 stdout=subprocess.PIPE, |
| 613 stderr=None) |
| 614 is_ready = layout_test_helper_proc.stdout.readline() |
| 615 if not is_ready.startswith('ready'): |
| 616 logging.error("layout_test_helper failed to be ready") |
| 617 |
| 618 # Check that the system dependencies (themes, fonts, ...) are correct. |
| 619 if not self._options.nocheck_sys_deps: |
| 620 proc = subprocess.Popen([test_shell_binary, |
| 621 "--check-layout-test-sys-deps"]) |
| 622 if proc.wait() != 0: |
| 623 logging.info("Aborting because system dependencies check failed.\n" |
| 624 "To override, invoke with --nocheck-sys-deps") |
| 625 sys.exit(1) |
| 626 |
| 627 if self._ContainsTests(self.HTTP_SUBDIR): |
| 628 self._http_server.Start() |
| 629 |
| 630 if self._ContainsTests(self.WEBSOCKET_SUBDIR): |
| 631 self._websocket_server.Start() |
| 632 # self._websocket_secure_server.Start() |
| 633 |
| 634 thread_timings, test_timings, individual_test_timings = ( |
| 635 self._RunTests(test_shell_binary, self._test_files_list, |
| 636 result_summary)) |
| 637 |
| 638 # We exclude the crashes from the list of results to retry, because |
| 639 # we want to treat even a potentially flaky crash as an error. |
| 640 failures = self._GetFailures(result_summary, include_crashes=False) |
| 641 retries = 0 |
| 642 retry_summary = result_summary |
| 643 while (retries < self.NUM_RETRY_ON_UNEXPECTED_FAILURE and len(failures)): |
| 644 logging.debug("Retrying %d unexpected failure(s)" % len(failures)) |
| 645 retries += 1 |
| 646 retry_summary = ResultSummary(self._expectations, failures.keys()) |
| 647 self._RunTests(test_shell_binary, failures.keys(), retry_summary) |
| 648 failures = self._GetFailures(retry_summary, include_crashes=True) |
| 649 |
| 650 self._StopLayoutTestHelper(layout_test_helper_proc) |
| 651 end_time = time.time() |
| 652 |
| 653 write = CreateLoggingWriter(self._options, 'timing') |
| 654 self._PrintTimingStatistics(write, end_time - start_time, |
| 655 thread_timings, test_timings, |
| 656 individual_test_timings, |
| 657 result_summary) |
| 658 |
| 659 self._meter.update("") |
| 660 |
| 661 if self._options.verbose: |
| 662 # We write this block to stdout for compatibility with the buildbot |
| 663 # log parser, which only looks at stdout, not stderr :( |
| 664 write = lambda s: sys.stdout.write("%s\n" % s) |
| 665 else: |
| 666 write = CreateLoggingWriter(self._options, 'actual') |
| 667 |
| 668 self._PrintResultSummary(write, result_summary) |
| 669 |
| 670 sys.stdout.flush() |
| 671 sys.stderr.flush() |
| 672 |
| 673 if (LOG_DETAILED_PROGRESS in self._options.log or |
| 674 (LOG_UNEXPECTED in self._options.log and |
| 675 result_summary.total != result_summary.expected)): |
| 676 print |
| 677 |
| 678 # This summary data gets written to stdout regardless of log level |
| 679 self._PrintOneLineSummary(result_summary.total, result_summary.expected) |
| 680 |
| 681 unexpected_results = self._SummarizeUnexpectedResults(result_summary, |
| 682 retry_summary) |
| 683 self._PrintUnexpectedResults(unexpected_results) |
| 684 |
| 685 # Write the same data to log files. |
| 686 self._WriteJSONFiles(unexpected_results, result_summary, |
| 687 individual_test_timings) |
| 688 |
| 689 # Write the summary to disk (results.html) and maybe open the test_shell |
| 690 # to this file. |
| 691 wrote_results = self._WriteResultsHtmlFile(result_summary) |
| 692 if not self._options.noshow_results and wrote_results: |
| 693 self._ShowResultsHtmlFile() |
| 694 |
| 695 # Ignore flaky failures and unexpected passes so we don't turn the |
| 696 # bot red for those. |
| 697 return unexpected_results['num_regressions'] |
| 698 |
| 699 def UpdateSummary(self, result_summary): |
| 700 """Update the summary while running tests.""" |
| 701 while True: |
| 702 try: |
| 703 (test, fail_list) = self._result_queue.get_nowait() |
| 704 result = test_failures.DetermineResultType(fail_list) |
| 705 expected = self._expectations.MatchesAnExpectedResult(test, result) |
| 706 result_summary.Add(test, fail_list, result, expected) |
| 707 if (LOG_DETAILED_PROGRESS in self._options.log and |
| 708 (self._options.experimental_fully_parallel or |
| 709 self._IsSingleThreaded())): |
| 710 self._DisplayDetailedProgress(result_summary) |
132 else: | 711 else: |
133 self.unexpected_results[test] = result | 712 if not expected and LOG_UNEXPECTED in self._options.log: |
134 self.unexpected += 1 | 713 self._PrintUnexpectedTestResult(test, result) |
135 | 714 self._DisplayOneLineProgress(result_summary) |
136 | 715 except Queue.Empty: |
137 class TestRunner: | 716 return |
138 """A class for managing running a series of tests on a series of layout | 717 |
139 test files.""" | 718 def _DisplayOneLineProgress(self, result_summary): |
140 | 719 """Displays the progress through the test run.""" |
141 HTTP_SUBDIR = os.sep.join(['', 'http', '']) | 720 self._meter.update("Testing: %d ran as expected, %d didn't, %d left" % |
142 WEBSOCKET_SUBDIR = os.sep.join(['', 'websocket', '']) | 721 (result_summary.expected, result_summary.unexpected, |
143 | 722 result_summary.remaining)) |
144 # The per-test timeout in milliseconds, if no --time-out-ms option was | 723 |
145 # given to run_webkit_tests. This should correspond to the default timeout | 724 def _DisplayDetailedProgress(self, result_summary): |
146 # in test_shell.exe. | 725 """Display detailed progress output where we print the directory name |
147 DEFAULT_TEST_TIMEOUT_MS = 6 * 1000 | 726 and one dot for each completed test. This is triggered by |
148 | 727 "--log detailed-progress".""" |
149 NUM_RETRY_ON_UNEXPECTED_FAILURE = 1 | 728 if self._current_test_number == len(self._test_files_list): |
150 | 729 return |
151 def __init__(self, options, meter): | 730 |
152 """Initialize test runner data structures. | 731 next_test = self._test_files_list[self._current_test_number] |
153 | 732 next_dir = os.path.dirname(path_utils.RelativeTestFilename(next_test)) |
154 Args: | 733 if self._current_progress_str == "": |
155 options: a dictionary of command line options | 734 self._current_progress_str = "%s: " % (next_dir) |
156 meter: a MeteredStream object to record updates to. | 735 self._current_dir = next_dir |
157 """ | 736 |
158 self._options = options | 737 while next_test in result_summary.results: |
159 self._meter = meter | 738 if next_dir != self._current_dir: |
160 | 739 self._meter.write("%s\n" % (self._current_progress_str)) |
161 if options.use_apache: | 740 self._current_progress_str = "%s: ." % (next_dir) |
162 self._http_server = apache_http_server.LayoutTestApacheHttpd( | 741 self._current_dir = next_dir |
163 options.results_directory) | 742 else: |
| 743 self._current_progress_str += "." |
| 744 |
| 745 if (next_test in result_summary.unexpected_results and |
| 746 LOG_UNEXPECTED in self._options.log): |
| 747 result = result_summary.unexpected_results[next_test] |
| 748 self._meter.write("%s\n" % self._current_progress_str) |
| 749 self._PrintUnexpectedTestResult(next_test, result) |
| 750 self._current_progress_str = "%s: " % self._current_dir |
| 751 |
| 752 self._current_test_number += 1 |
| 753 if self._current_test_number == len(self._test_files_list): |
| 754 break |
| 755 |
| 756 next_test = self._test_files_list[self._current_test_number] |
| 757 next_dir = os.path.dirname(path_utils.RelativeTestFilename(next_test)) |
| 758 |
| 759 if result_summary.remaining: |
| 760 remain_str = " (%d)" % (result_summary.remaining) |
| 761 self._meter.update("%s%s" % (self._current_progress_str, remain_str)) |
| 762 else: |
| 763 self._meter.write("%s\n" % (self._current_progress_str)) |
| 764 |
| 765 def _GetFailures(self, result_summary, include_crashes): |
| 766 """Filters a dict of results and returns only the failures. |
| 767 |
| 768 Args: |
| 769 result_summary: the results of the test run |
| 770 include_crashes: whether crashes are included in the output. |
| 771 We use False when finding the list of failures to retry |
| 772 to see if the results were flaky. Although the crashes may also be |
| 773 flaky, we treat them as if they aren't so that they're not ignored. |
| 774 Returns: |
| 775 a dict of files -> results |
| 776 """ |
| 777 failed_results = {} |
| 778 for test, result in result_summary.unexpected_results.iteritems(): |
| 779 if (result == test_expectations.PASS or |
| 780 result == test_expectations.CRASH and not include_crashes): |
| 781 continue |
| 782 failed_results[test] = result |
| 783 |
| 784 return failed_results |
| 785 |
| 786 def _SummarizeUnexpectedResults(self, result_summary, retry_summary): |
| 787 """Summarize any unexpected results as a dict. |
| 788 |
| 789 TODO(dpranke): split this data structure into a separate class? |
| 790 |
| 791 Args: |
| 792 result_summary: summary object from initial test runs |
| 793 retry_summary: summary object from final test run of retried tests |
| 794 Returns: |
| 795 A dictionary containing a summary of the unexpected results from the |
| 796 run, with the following fields: |
| 797 'version': a version indicator (1 in this version) |
| 798 'fixable': # of fixable tests (NOW - PASS) |
| 799 'skipped': # of skipped tests (NOW & SKIPPED) |
| 800 'num_regressions': # of non-flaky failures |
| 801 'num_flaky': # of flaky failures |
| 802 'num_passes': # of unexpected passes |
| 803 'tests': a dict of tests -> { 'expected' : '...', 'actual' : '...' } |
| 804 """ |
| 805 results = {} |
| 806 results['version'] = 1 |
| 807 |
| 808 tbe = result_summary.tests_by_expectation |
| 809 tbt = result_summary.tests_by_timeline |
| 810 results['fixable'] = len(tbt[test_expectations.NOW] - |
| 811 tbe[test_expectations.PASS]) |
| 812 results['skipped'] = len(tbt[test_expectations.NOW] & |
| 813 tbe[test_expectations.SKIP]) |
| 814 |
| 815 num_passes = 0 |
| 816 num_flaky = 0 |
| 817 num_regressions = 0 |
| 818 keywords = {} |
| 819 for k, v in TestExpectationsFile.EXPECTATIONS.iteritems(): |
| 820 keywords[v] = k.upper() |
| 821 |
| 822 tests = {} |
| 823 for filename, result in result_summary.unexpected_results.iteritems(): |
| 824 # Note that if a test crashed in the original run, we ignore whether or |
| 825 # not it crashed when we retried it (if we retried it), and always |
| 826 # consider the result not flaky. |
| 827 test = path_utils.RelativeTestFilename(filename) |
| 828 expected = self._expectations.GetExpectationsString(filename) |
| 829 actual = [keywords[result]] |
| 830 |
| 831 if result == test_expectations.PASS: |
| 832 num_passes += 1 |
| 833 elif result == test_expectations.CRASH: |
| 834 num_regressions += 1 |
| 835 else: |
| 836 if filename not in retry_summary.unexpected_results: |
| 837 actual.extend( |
| 838 self._expectations.GetExpectationsString(filename).split(" ")) |
| 839 num_flaky += 1 |
164 else: | 840 else: |
165 self._http_server = http_server.Lighttpd(options.results_directory) | 841 retry_result = retry_summary.unexpected_results[filename] |
166 | 842 if result != retry_result: |
167 self._websocket_server = websocket_server.PyWebSocket( | 843 actual.append(keywords[retry_result]) |
168 options.results_directory) | 844 num_flaky += 1 |
169 # disable wss server. need to install pyOpenSSL on buildbots. | 845 else: |
170 # self._websocket_secure_server = websocket_server.PyWebSocket( | 846 num_regressions += 1 |
171 # options.results_directory, use_tls=True, port=9323) | 847 |
172 | 848 tests[test] = {} |
173 # a list of TestType objects | 849 tests[test]['expected'] = expected |
174 self._test_types = [] | 850 tests[test]['actual'] = " ".join(actual) |
175 | 851 |
176 # a set of test files, and the same tests as a list | 852 results['tests'] = tests |
177 self._test_files = set() | 853 results['num_passes'] = num_passes |
178 self._test_files_list = None | 854 results['num_flaky'] = num_flaky |
179 self._file_dir = path_utils.GetAbsolutePath( | 855 results['num_regressions'] = num_regressions |
180 os.path.dirname(sys.argv[0])) | 856 |
181 self._result_queue = Queue.Queue() | 857 return results |
182 | 858 |
183 # These are used for --log detailed-progress to track status by | 859 def _WriteJSONFiles(self, unexpected_results, result_summary, |
184 # directory. | 860 individual_test_timings): |
185 self._current_dir = None | 861 """Writes the results of the test run as JSON files into the results dir. |
186 self._current_progress_str = "" | 862 |
187 self._current_test_number = 0 | 863 There are three different files written into the results dir: |
188 | 864 unexpected_results.json: A short list of any unexpected results. This |
189 def __del__(self): | 865 is used by the buildbots to display results. |
190 logging.debug("flushing stdout") | 866 expectations.json: This is used by the flakiness dashboard. |
191 sys.stdout.flush() | 867 results.json: A full list of the results - used by the flakiness |
192 logging.debug("flushing stderr") | 868 dashboard and the aggregate results dashboard. |
193 sys.stderr.flush() | 869 |
194 logging.debug("stopping http server") | 870 Args: |
195 # Stop the http server. | 871 unexpected_results: dict of unexpected results |
196 self._http_server.Stop() | 872 result_summary: full summary object |
197 # Stop the Web Socket / Web Socket Secure servers. | 873 individual_test_timings: list of test times (used by the flakiness |
198 self._websocket_server.Stop() | 874 dashboard). |
199 # self._websocket_secure_server.Stop() | 875 """ |
200 | 876 logging.debug("Writing JSON files in %s." % self._options.results_directory) |
201 def GatherFilePaths(self, paths): | 877 unexpected_file = open(os.path.join(self._options.results_directory, |
202 """Find all the files to test. | 878 "unexpected_results.json"), "w") |
203 | 879 unexpected_file.write(simplejson.dumps(unexpected_results, sort_keys=True, |
204 Args: | 880 indent=2)) |
205 paths: a list of globs to use instead of the defaults.""" | 881 unexpected_file.close() |
206 self._test_files = test_files.GatherTestFiles(paths) | 882 |
207 | 883 # Write a json file of the test_expectations.txt file for the layout tests |
208 def ParseExpectations(self, platform, is_debug_mode): | 884 # dashboard. |
209 """Parse the expectations from the test_list files and return a data | 885 expectations_file = open(os.path.join(self._options.results_directory, |
210 structure holding them. Throws an error if the test_list files have | 886 "expectations.json"), "w") |
211 invalid syntax.""" | 887 expectations_json = self._expectations.GetExpectationsJsonForAllPlatforms() |
212 if self._options.lint_test_files: | 888 expectations_file.write(("ADD_EXPECTATIONS(" + expectations_json + ");")) |
213 test_files = None | 889 expectations_file.close() |
| 890 |
| 891 json_layout_results_generator.JSONLayoutResultsGenerator( |
| 892 self._options.builder_name, self._options.build_name, |
| 893 self._options.build_number, self._options.results_directory, |
| 894 BUILDER_BASE_URL, individual_test_timings, |
| 895 self._expectations, result_summary, self._test_files_list) |
| 896 |
| 897 logging.debug("Finished writing JSON files.") |
| 898 |
| 899 def _PrintExpectedResultsOfType(self, write, result_summary, result_type, |
| 900 result_type_str): |
| 901 """Print the number of the tests in a given result class. |
| 902 |
| 903 Args: |
| 904 write: A callback to write info to (e.g., a LoggingWriter) or |
| 905 sys.stdout.write. |
| 906 result_summary - the object containing all the results to report on |
| 907 result_type - the particular result type to report in the summary. |
| 908 result_type_str - a string description of the result_type. |
| 909 """ |
| 910 tests = self._expectations.GetTestsWithResultType(result_type) |
| 911 now = result_summary.tests_by_timeline[test_expectations.NOW] |
| 912 wontfix = result_summary.tests_by_timeline[test_expectations.WONTFIX] |
| 913 defer = result_summary.tests_by_timeline[test_expectations.DEFER] |
| 914 |
| 915 # We use a fancy format string in order to print the data out in a |
| 916 # nicely-aligned table. |
| 917 fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd defer, %%%dd wontfix)" % |
| 918 (self._NumDigits(now), self._NumDigits(defer), |
| 919 self._NumDigits(wontfix))) |
| 920 write(fmtstr % |
| 921 (len(tests), result_type_str, len(tests & now), len(tests & defer), |
| 922 len(tests & wontfix))) |
| 923 |
| 924 def _NumDigits(self, num): |
| 925 """Returns the number of digits needed to represent the length of a |
| 926 sequence.""" |
| 927 ndigits = 1 |
| 928 if len(num): |
| 929 ndigits = int(math.log10(len(num))) + 1 |
| 930 return ndigits |
| 931 |
| 932 def _PrintTimingStatistics(self, write, total_time, thread_timings, |
| 933 directory_test_timings, individual_test_timings, |
| 934 result_summary): |
| 935 """Record timing-specific information for the test run. |
| 936 |
| 937 Args: |
| 938 write: A callback to write info to (e.g., a LoggingWriter) or |
| 939 sys.stdout.write. |
| 940 total_time: total elapsed time (in seconds) for the test run |
| 941 thread_timings: wall clock time each thread ran for |
| 942 directory_test_timings: timing by directory |
| 943 individual_test_timings: timing by file |
| 944 result_summary: summary object for the test run |
| 945 """ |
| 946 write("Test timing:") |
| 947 write(" %6.2f total testing time" % total_time) |
| 948 write("") |
| 949 write("Thread timing:") |
| 950 cuml_time = 0 |
| 951 for t in thread_timings: |
| 952 write(" %10s: %5d tests, %6.2f secs" % |
| 953 (t['name'], t['num_tests'], t['total_time'])) |
| 954 cuml_time += t['total_time'] |
| 955 write(" %6.2f cumulative, %6.2f optimal" % |
| 956 (cuml_time, cuml_time / int(self._options.num_test_shells))) |
| 957 write("") |
| 958 |
| 959 self._PrintAggregateTestStatistics(write, individual_test_timings) |
| 960 self._PrintIndividualTestTimes(write, individual_test_timings, |
| 961 result_summary) |
| 962 self._PrintDirectoryTimings(write, directory_test_timings) |
| 963 |
| 964 def _PrintAggregateTestStatistics(self, write, individual_test_timings): |
| 965 """Prints aggregate statistics (e.g. median, mean, etc.) for all tests. |
| 966 Args: |
| 967 write: A callback to write info to (e.g., a LoggingWriter) or |
| 968 sys.stdout.write. |
| 969 individual_test_timings: List of test_shell_thread.TestStats for all |
| 970 tests. |
| 971 """ |
| 972 test_types = individual_test_timings[0].time_for_diffs.keys() |
| 973 times_for_test_shell = [] |
| 974 times_for_diff_processing = [] |
| 975 times_per_test_type = {} |
| 976 for test_type in test_types: |
| 977 times_per_test_type[test_type] = [] |
| 978 |
| 979 for test_stats in individual_test_timings: |
| 980 times_for_test_shell.append(test_stats.test_run_time) |
| 981 times_for_diff_processing.append(test_stats.total_time_for_all_diffs) |
| 982 time_for_diffs = test_stats.time_for_diffs |
| 983 for test_type in test_types: |
| 984 times_per_test_type[test_type].append(time_for_diffs[test_type]) |
| 985 |
| 986 self._PrintStatisticsForTestTimings(write, |
| 987 "PER TEST TIME IN TESTSHELL (seconds):", times_for_test_shell) |
| 988 self._PrintStatisticsForTestTimings(write, |
| 989 "PER TEST DIFF PROCESSING TIMES (seconds):", times_for_diff_processing) |
| 990 for test_type in test_types: |
| 991 self._PrintStatisticsForTestTimings(write, |
| 992 "PER TEST TIMES BY TEST TYPE: %s" % test_type, |
| 993 times_per_test_type[test_type]) |
| 994 |
| 995 def _PrintIndividualTestTimes(self, write, individual_test_timings, |
| 996 result_summary): |
| 997 """Prints the run times for slow, timeout and crash tests. |
| 998 Args: |
| 999 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1000 sys.stdout.write. |
| 1001 individual_test_timings: List of test_shell_thread.TestStats for all |
| 1002 tests. |
| 1003 result_summary: summary object for test run |
| 1004 """ |
| 1005 # Reverse-sort by the time spent in test_shell. |
| 1006 individual_test_timings.sort(lambda a, b: |
| 1007 cmp(b.test_run_time, a.test_run_time)) |
| 1008 |
| 1009 num_printed = 0 |
| 1010 slow_tests = [] |
| 1011 timeout_or_crash_tests = [] |
| 1012 unexpected_slow_tests = [] |
| 1013 for test_tuple in individual_test_timings: |
| 1014 filename = test_tuple.filename |
| 1015 is_timeout_crash_or_slow = False |
| 1016 if self._expectations.HasModifier(filename, test_expectations.SLOW): |
| 1017 is_timeout_crash_or_slow = True |
| 1018 slow_tests.append(test_tuple) |
| 1019 |
| 1020 if filename in result_summary.failures: |
| 1021 result = result_summary.results[filename] |
| 1022 if (result == test_expectations.TIMEOUT or |
| 1023 result == test_expectations.CRASH): |
| 1024 is_timeout_crash_or_slow = True |
| 1025 timeout_or_crash_tests.append(test_tuple) |
| 1026 |
| 1027 if (not is_timeout_crash_or_slow and |
| 1028 num_printed < self._options.num_slow_tests_to_log): |
| 1029 num_printed = num_printed + 1 |
| 1030 unexpected_slow_tests.append(test_tuple) |
| 1031 |
| 1032 write("") |
| 1033 self._PrintTestListTiming(write, "%s slowest tests that are not marked " |
| 1034 "as SLOW and did not timeout/crash:" % |
| 1035 self._options.num_slow_tests_to_log, unexpected_slow_tests) |
| 1036 write("") |
| 1037 self._PrintTestListTiming(write, "Tests marked as SLOW:", slow_tests) |
| 1038 write("") |
| 1039 self._PrintTestListTiming(write, "Tests that timed out or crashed:", |
| 1040 timeout_or_crash_tests) |
| 1041 write("") |
| 1042 |
| 1043 def _PrintTestListTiming(self, write, title, test_list): |
| 1044 """Print timing info for each test. |
| 1045 |
| 1046 Args: |
| 1047 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1048 sys.stdout.write. |
| 1049 title: section heading |
| 1050 test_list: tests that fall in this section |
| 1051 """ |
| 1052 write(title) |
| 1053 for test_tuple in test_list: |
| 1054 filename = test_tuple.filename[len(path_utils.LayoutTestsDir()) + 1:] |
| 1055 filename = filename.replace('\\', '/') |
| 1056 test_run_time = round(test_tuple.test_run_time, 1) |
| 1057 write(" %s took %s seconds" % (filename, test_run_time)) |
| 1058 |
| 1059 def _PrintDirectoryTimings(self, write, directory_test_timings): |
| 1060 """Print timing info by directory for any directories that take > 10 seconds |
| 1061 to run. |
| 1062 |
| 1063 Args: |
| 1064 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1065 sys.stdout.write. |
| 1066 directory_test_timing: time info for each directory |
| 1067 """ |
| 1068 timings = [] |
| 1069 for directory in directory_test_timings: |
| 1070 num_tests, time_for_directory = directory_test_timings[directory] |
| 1071 timings.append((round(time_for_directory, 1), directory, num_tests)) |
| 1072 timings.sort() |
| 1073 |
| 1074 write("Time to process slowest subdirectories:") |
| 1075 min_seconds_to_print = 10 |
| 1076 for timing in timings: |
| 1077 if timing[0] > min_seconds_to_print: |
| 1078 write(" %s took %s seconds to run %s tests." % (timing[1], timing[0], |
| 1079 timing[2])) |
| 1080 write("") |
| 1081 |
| 1082 def _PrintStatisticsForTestTimings(self, write, title, timings): |
| 1083 """Prints the median, mean and standard deviation of the values in timings. |
| 1084 Args: |
| 1085 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1086 sys.stdout.write. |
| 1087 title: Title for these timings. |
| 1088 timings: A list of floats representing times. |
| 1089 """ |
| 1090 write(title) |
| 1091 timings.sort() |
| 1092 |
| 1093 num_tests = len(timings) |
| 1094 percentile90 = timings[int(.9 * num_tests)] |
| 1095 percentile99 = timings[int(.99 * num_tests)] |
| 1096 |
| 1097 if num_tests % 2 == 1: |
| 1098 median = timings[((num_tests - 1) / 2) - 1] |
| 1099 else: |
| 1100 lower = timings[num_tests / 2 - 1] |
| 1101 upper = timings[num_tests / 2] |
| 1102 median = (float(lower + upper)) / 2 |
| 1103 |
| 1104 mean = sum(timings) / num_tests |
| 1105 |
| 1106 for time in timings: |
| 1107 sum_of_deviations = math.pow(time - mean, 2) |
| 1108 |
| 1109 std_deviation = math.sqrt(sum_of_deviations / num_tests) |
| 1110 write(" Median: %6.3f" % median) |
| 1111 write(" Mean: %6.3f" % mean) |
| 1112 write(" 90th percentile: %6.3f" % percentile90) |
| 1113 write(" 99th percentile: %6.3f" % percentile99) |
| 1114 write(" Standard dev: %6.3f" % std_deviation) |
| 1115 write("") |
| 1116 |
| 1117 def _PrintResultSummary(self, write, result_summary): |
| 1118 """Print a short summary to the output file about how many tests passed. |
| 1119 |
| 1120 Args: |
| 1121 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1122 sys.stdout.write. |
| 1123 result_summary: information to log |
| 1124 """ |
| 1125 failed = len(result_summary.failures) |
| 1126 skipped = len(result_summary.tests_by_expectation[test_expectations.SKIP]) |
| 1127 total = result_summary.total |
| 1128 passed = total - failed - skipped |
| 1129 pct_passed = 0.0 |
| 1130 if total > 0: |
| 1131 pct_passed = float(passed) * 100 / total |
| 1132 |
| 1133 write(""); |
| 1134 write("=> Results: %d/%d tests passed (%.1f%%)" % |
| 1135 (passed, total, pct_passed)) |
| 1136 write(""); |
| 1137 self._PrintResultSummaryEntry(write, result_summary, test_expectations.NOW, |
| 1138 "Tests to be fixed for the current release") |
| 1139 |
| 1140 write(""); |
| 1141 self._PrintResultSummaryEntry(write, result_summary, |
| 1142 test_expectations.DEFER, |
| 1143 "Tests we'll fix in the future if they fail (DEFER)") |
| 1144 |
| 1145 write(""); |
| 1146 self._PrintResultSummaryEntry(write, result_summary, |
| 1147 test_expectations.WONTFIX, |
| 1148 "Tests that will only be fixed if they crash (WONTFIX)") |
| 1149 |
| 1150 def _PrintResultSummaryEntry(self, write, result_summary, timeline, heading): |
| 1151 """Print a summary block of results for a particular timeline of test. |
| 1152 |
| 1153 Args: |
| 1154 write: A callback to write info to (e.g., a LoggingWriter) or |
| 1155 sys.stdout.write. |
| 1156 result_summary: summary to print results for |
| 1157 timeline: the timeline to print results for (NOT, WONTFIX, etc.) |
| 1158 heading: a textual description of the timeline |
| 1159 """ |
| 1160 total = len(result_summary.tests_by_timeline[timeline]) |
| 1161 not_passing = (total - |
| 1162 len(result_summary.tests_by_expectation[test_expectations.PASS] & |
| 1163 result_summary.tests_by_timeline[timeline])) |
| 1164 write("=> %s (%d):" % (heading, not_passing)) |
| 1165 |
| 1166 for result in TestExpectationsFile.EXPECTATION_ORDER: |
| 1167 if result == test_expectations.PASS: |
| 1168 continue |
| 1169 results = (result_summary.tests_by_expectation[result] & |
| 1170 result_summary.tests_by_timeline[timeline]) |
| 1171 desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result] |
| 1172 if not_passing and len(results): |
| 1173 pct = len(results) * 100.0 / not_passing |
| 1174 write(" %5d %-24s (%4.1f%%)" % (len(results), |
| 1175 desc[len(results) != 1], pct)) |
| 1176 |
| 1177 def _PrintOneLineSummary(self, total, expected): |
| 1178 """Print a one-line summary of the test run to stdout. |
| 1179 |
| 1180 Args: |
| 1181 total: total number of tests run |
| 1182 expected: number of expected results |
| 1183 """ |
| 1184 unexpected = total - expected |
| 1185 if unexpected == 0: |
| 1186 print "All %d tests ran as expected." % expected |
| 1187 elif expected == 1: |
| 1188 print "1 test ran as expected, %d didn't:" % unexpected |
| 1189 else: |
| 1190 print "%d tests ran as expected, %d didn't:" % (expected, unexpected) |
| 1191 |
| 1192 def _PrintUnexpectedResults(self, unexpected_results): |
| 1193 """Prints any unexpected results in a human-readable form to stdout.""" |
| 1194 passes = {} |
| 1195 flaky = {} |
| 1196 regressions = {} |
| 1197 |
| 1198 if len(unexpected_results['tests']): |
| 1199 print "" |
| 1200 |
| 1201 for test, results in unexpected_results['tests'].iteritems(): |
| 1202 actual = results['actual'].split(" ") |
| 1203 expected = results['expected'].split(" ") |
| 1204 if actual == ['PASS']: |
| 1205 if 'CRASH' in expected: |
| 1206 _AddToDictOfLists(passes, 'Expected to crash, but passed', test) |
| 1207 elif 'TIMEOUT' in expected: |
| 1208 _AddToDictOfLists(passes, 'Expected to timeout, but passed', test) |
214 else: | 1209 else: |
215 test_files = self._test_files | 1210 _AddToDictOfLists(passes, 'Expected to fail, but passed', test) |
216 | 1211 elif len(actual) > 1: |
217 try: | 1212 # We group flaky tests by the first actual result we got. |
218 self._expectations = test_expectations.TestExpectations(test_files, | 1213 _AddToDictOfLists(flaky, actual[0], test) |
219 self._file_dir, platform, is_debug_mode, | 1214 else: |
220 self._options.lint_test_files) | 1215 _AddToDictOfLists(regressions, results['actual'], test) |
221 return self._expectations | 1216 |
222 except Exception, err: | 1217 if len(passes): |
223 if self._options.lint_test_files: | 1218 for key, tests in passes.iteritems(): |
224 print str(err) | 1219 print "%s: (%d)" % (key, len(tests)) |
225 else: | 1220 tests.sort() |
226 raise err | 1221 for test in tests: |
227 | 1222 print " %s" % test |
228 def PrepareListsAndPrintOutput(self, write): | 1223 print |
229 """Create appropriate subsets of test lists and returns a | 1224 |
230 ResultSummary object. Also prints expected test counts. | 1225 if len(flaky): |
231 | 1226 descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS |
232 Args: | 1227 for key, tests in flaky.iteritems(): |
233 write: A callback to write info to (e.g., a LoggingWriter) or | 1228 result = TestExpectationsFile.EXPECTATIONS[key.lower()] |
234 sys.stdout.write. | 1229 print "Unexpected flakiness: %s (%d)" % ( |
235 """ | 1230 descriptions[result][1], len(tests)) |
236 | 1231 tests.sort() |
237 # Remove skipped - both fixable and ignored - files from the | 1232 |
238 # top-level list of files to test. | 1233 for test in tests: |
239 num_all_test_files = len(self._test_files) | 1234 actual = unexpected_results['tests'][test]['actual'].split(" ") |
240 write("Found: %d tests" % (len(self._test_files))) | 1235 expected = unexpected_results['tests'][test]['expected'].split(" ") |
241 skipped = set() | 1236 result = TestExpectationsFile.EXPECTATIONS[key.lower()] |
242 if num_all_test_files > 1 and not self._options.force: | 1237 new_expectations_list = list(set(actual) | set(expected)) |
243 skipped = self._expectations.GetTestsWithResultType( | 1238 print " %s = %s" % (test, " ".join(new_expectations_list)) |
244 test_expectations.SKIP) | 1239 print |
245 self._test_files -= skipped | 1240 |
246 | 1241 if len(regressions): |
247 # Create a sorted list of test files so the subset chunk, | 1242 descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS |
248 # if used, contains alphabetically consecutive tests. | 1243 for key, tests in regressions.iteritems(): |
249 self._test_files_list = list(self._test_files) | 1244 result = TestExpectationsFile.EXPECTATIONS[key.lower()] |
250 if self._options.randomize_order: | 1245 print "Regressions: Unexpected %s : (%d)" % ( |
251 random.shuffle(self._test_files_list) | 1246 descriptions[result][1], len(tests)) |
252 else: | 1247 tests.sort() |
253 self._test_files_list.sort() | 1248 for test in tests: |
254 | 1249 print " %s = %s" % (test, key) |
255 # If the user specifies they just want to run a subset of the tests, | 1250 print |
256 # just grab a subset of the non-skipped tests. | 1251 |
257 if self._options.run_chunk or self._options.run_part: | 1252 if len(unexpected_results['tests']) and self._options.verbose: |
258 chunk_value = self._options.run_chunk or self._options.run_part | 1253 print "-" * 78 |
259 test_files = self._test_files_list | 1254 |
260 try: | 1255 def _PrintUnexpectedTestResult(self, test, result): |
261 (chunk_num, chunk_len) = chunk_value.split(":") | 1256 """Prints one unexpected test result line.""" |
262 chunk_num = int(chunk_num) | 1257 desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result][0] |
263 assert(chunk_num >= 0) | 1258 self._meter.write(" %s -> unexpected %s\n" % |
264 test_size = int(chunk_len) | 1259 (path_utils.RelativeTestFilename(test), desc)) |
265 assert(test_size > 0) | 1260 |
266 except: | 1261 def _WriteResultsHtmlFile(self, result_summary): |
267 logging.critical("invalid chunk '%s'" % chunk_value) | 1262 """Write results.html which is a summary of tests that failed. |
268 sys.exit(1) | 1263 |
269 | 1264 Args: |
270 # Get the number of tests | 1265 result_summary: a summary of the results :) |
271 num_tests = len(test_files) | 1266 |
272 | 1267 Returns: |
273 # Get the start offset of the slice. | 1268 True if any results were written (since expected failures may be omitted) |
274 if self._options.run_chunk: | 1269 """ |
275 chunk_len = test_size | 1270 # test failures |
276 # In this case chunk_num can be really large. We need | 1271 if self._options.full_results_html: |
277 # to make the slave fit in the current number of tests. | 1272 test_files = result_summary.failures.keys() |
278 slice_start = (chunk_num * chunk_len) % num_tests | 1273 else: |
279 else: | 1274 unexpected_failures = self._GetFailures(result_summary, |
280 # Validate the data. | 1275 include_crashes=True) |
281 assert(test_size <= num_tests) | 1276 test_files = unexpected_failures.keys() |
282 assert(chunk_num <= test_size) | 1277 if not len(test_files): |
283 | 1278 return False |
284 # To count the chunk_len, and make sure we don't skip | 1279 |
285 # some tests, we round to the next value that fits exactly | 1280 out_filename = os.path.join(self._options.results_directory, |
286 # all the parts. | 1281 "results.html") |
287 rounded_tests = num_tests | 1282 out_file = open(out_filename, 'w') |
288 if rounded_tests % test_size != 0: | 1283 # header |
289 rounded_tests = (num_tests + test_size - | 1284 if self._options.full_results_html: |
290 (num_tests % test_size)) | 1285 h2 = "Test Failures" |
291 | 1286 else: |
292 chunk_len = rounded_tests / test_size | 1287 h2 = "Unexpected Test Failures" |
293 slice_start = chunk_len * (chunk_num - 1) | 1288 out_file.write("<html><head><title>Layout Test Results (%(time)s)</title>" |
294 # It does not mind if we go over test_size. | 1289 "</head><body><h2>%(h2)s (%(time)s)</h2>\n" |
295 | 1290 % {'h2': h2, 'time': time.asctime()}) |
296 # Get the end offset of the slice. | 1291 |
297 slice_end = min(num_tests, slice_start + chunk_len) | 1292 test_files.sort() |
298 | 1293 for test_file in test_files: |
299 files = test_files[slice_start:slice_end] | 1294 test_failures = result_summary.failures.get(test_file, []) |
300 | 1295 out_file.write("<p><a href='%s'>%s</a><br />\n" |
301 tests_run_msg = 'Running: %d tests (chunk slice [%d:%d] of %d)' % ( | 1296 % (path_utils.FilenameToUri(test_file), |
302 (slice_end - slice_start), slice_start, slice_end, num_tests) | 1297 path_utils.RelativeTestFilename(test_file))) |
303 write(tests_run_msg) | 1298 for failure in test_failures: |
304 | 1299 out_file.write(" %s<br/>" |
305 # If we reached the end and we don't have enough tests, we run some | 1300 % failure.ResultHtmlOutput( |
306 # from the beginning. | 1301 path_utils.RelativeTestFilename(test_file))) |
307 if (self._options.run_chunk and | 1302 out_file.write("</p>\n") |
308 (slice_end - slice_start < chunk_len)): | 1303 |
309 extra = 1 + chunk_len - (slice_end - slice_start) | 1304 # footer |
310 extra_msg = (' last chunk is partial, appending [0:%d]' % | 1305 out_file.write("</body></html>\n") |
311 extra) | 1306 return True |
312 write(extra_msg) | 1307 |
313 tests_run_msg += "\n" + extra_msg | 1308 def _ShowResultsHtmlFile(self): |
314 files.extend(test_files[0:extra]) | 1309 """Launches the test shell open to the results.html page.""" |
315 tests_run_filename = os.path.join(self._options.results_directory, | 1310 results_filename = os.path.join(self._options.results_directory, |
316 "tests_run.txt") | |
317 tests_run_file = open(tests_run_filename, "w") | |
318 tests_run_file.write(tests_run_msg + "\n") | |
319 tests_run_file.close() | |
320 | |
321 len_skip_chunk = int(len(files) * len(skipped) / | |
322 float(len(self._test_files))) | |
323 skip_chunk_list = list(skipped)[0:len_skip_chunk] | |
324 skip_chunk = set(skip_chunk_list) | |
325 | |
326 # Update expectations so that the stats are calculated correctly. | |
327 # We need to pass a list that includes the right # of skipped files | |
328 # to ParseExpectations so that ResultSummary() will get the correct | |
329 # stats. So, we add in the subset of skipped files, and then | |
330 # subtract them back out. | |
331 self._test_files_list = files + skip_chunk_list | |
332 self._test_files = set(self._test_files_list) | |
333 | |
334 self._expectations = self.ParseExpectations( | |
335 path_utils.PlatformName(), options.target == 'Debug') | |
336 | |
337 self._test_files = set(files) | |
338 self._test_files_list = files | |
339 else: | |
340 skip_chunk = skipped | |
341 | |
342 result_summary = ResultSummary(self._expectations, | |
343 self._test_files | skip_chunk) | |
344 self._PrintExpectedResultsOfType(write, result_summary, | |
345 test_expectations.PASS, "passes") | |
346 self._PrintExpectedResultsOfType(write, result_summary, | |
347 test_expectations.FAIL, "failures") | |
348 self._PrintExpectedResultsOfType(write, result_summary, | |
349 test_expectations.FLAKY, "flaky") | |
350 self._PrintExpectedResultsOfType(write, result_summary, | |
351 test_expectations.SKIP, "skipped") | |
352 | |
353 | |
354 if self._options.force: | |
355 write('Running all tests, including skips (--force)') | |
356 else: | |
357 # Note that we don't actually run the skipped tests (they were | |
358 # subtracted out of self._test_files, above), but we stub out the | |
359 # results here so the statistics can remain accurate. | |
360 for test in skip_chunk: | |
361 result_summary.Add(test, [], test_expectations.SKIP, | |
362 expected=True) | |
363 write("") | |
364 | |
365 return result_summary | |
366 | |
367 def AddTestType(self, test_type): | |
368 """Add a TestType to the TestRunner.""" | |
369 self._test_types.append(test_type) | |
370 | |
371 def _GetDirForTestFile(self, test_file): | |
372 """Returns the highest-level directory by which to shard the given | |
373 test file.""" | |
374 index = test_file.rfind(os.sep + 'LayoutTests' + os.sep) | |
375 | |
376 test_file = test_file[index + len('LayoutTests/'):] | |
377 test_file_parts = test_file.split(os.sep, 1) | |
378 directory = test_file_parts[0] | |
379 test_file = test_file_parts[1] | |
380 | |
381 # The http tests are very stable on mac/linux. | |
382 # TODO(ojan): Make the http server on Windows be apache so we can | |
383 # turn shard the http tests there as well. Switching to apache is | |
384 # what made them stable on linux/mac. | |
385 return_value = directory | |
386 while ((directory != 'http' or sys.platform in ('darwin', 'linux2')) | |
387 and test_file.find(os.sep) >= 0): | |
388 test_file_parts = test_file.split(os.sep, 1) | |
389 directory = test_file_parts[0] | |
390 return_value = os.path.join(return_value, directory) | |
391 test_file = test_file_parts[1] | |
392 | |
393 return return_value | |
394 | |
395 def _GetTestInfoForFile(self, test_file): | |
396 """Returns the appropriate TestInfo object for the file. Mostly this | |
397 is used for looking up the timeout value (in ms) to use for the given | |
398 test.""" | |
399 if self._expectations.HasModifier(test_file, test_expectations.SLOW): | |
400 return TestInfo(test_file, self._options.slow_time_out_ms) | |
401 return TestInfo(test_file, self._options.time_out_ms) | |
402 | |
403 def _GetTestFileQueue(self, test_files): | |
404 """Create the thread safe queue of lists of (test filenames, test URIs) | |
405 tuples. Each TestShellThread pulls a list from this queue and runs | |
406 those tests in order before grabbing the next available list. | |
407 | |
408 Shard the lists by directory. This helps ensure that tests that depend | |
409 on each other (aka bad tests!) continue to run together as most | |
410 cross-tests dependencies tend to occur within the same directory. | |
411 | |
412 Return: | |
413 The Queue of lists of TestInfo objects. | |
414 """ | |
415 | |
416 if (self._options.experimental_fully_parallel or | |
417 self._IsSingleThreaded()): | |
418 filename_queue = Queue.Queue() | |
419 for test_file in test_files: | |
420 filename_queue.put('.', [self._GetTestInfoForFile(test_file)]) | |
421 return filename_queue | |
422 | |
423 tests_by_dir = {} | |
424 for test_file in test_files: | |
425 directory = self._GetDirForTestFile(test_file) | |
426 tests_by_dir.setdefault(directory, []) | |
427 tests_by_dir[directory].append(self._GetTestInfoForFile(test_file)) | |
428 | |
429 # Sort by the number of tests in the dir so that the ones with the | |
430 # most tests get run first in order to maximize parallelization. | |
431 # Number of tests is a good enough, but not perfect, approximation | |
432 # of how long that set of tests will take to run. We can't just use | |
433 # a PriorityQueue until we move # to Python 2.6. | |
434 test_lists = [] | |
435 http_tests = None | |
436 for directory in tests_by_dir: | |
437 test_list = tests_by_dir[directory] | |
438 # Keep the tests in alphabetical order. | |
439 # TODO: Remove once tests are fixed so they can be run in any | |
440 # order. | |
441 test_list.reverse() | |
442 test_list_tuple = (directory, test_list) | |
443 if directory == 'LayoutTests' + os.sep + 'http': | |
444 http_tests = test_list_tuple | |
445 else: | |
446 test_lists.append(test_list_tuple) | |
447 test_lists.sort(lambda a, b: cmp(len(b[1]), len(a[1]))) | |
448 | |
449 # Put the http tests first. There are only a couple hundred of them, | |
450 # but each http test takes a very long time to run, so sorting by the | |
451 # number of tests doesn't accurately capture how long they take to run. | |
452 if http_tests: | |
453 test_lists.insert(0, http_tests) | |
454 | |
455 filename_queue = Queue.Queue() | |
456 for item in test_lists: | |
457 filename_queue.put(item) | |
458 return filename_queue | |
459 | |
460 def _GetTestShellArgs(self, index): | |
461 """Returns the tuple of arguments for tests and for test_shell.""" | |
462 shell_args = [] | |
463 test_args = test_type_base.TestArguments() | |
464 if not self._options.no_pixel_tests: | |
465 png_path = os.path.join(self._options.results_directory, | |
466 "png_result%s.png" % index) | |
467 shell_args.append("--pixel-tests=" + png_path) | |
468 test_args.png_path = png_path | |
469 | |
470 test_args.new_baseline = self._options.new_baseline | |
471 | |
472 test_args.show_sources = self._options.sources | |
473 | |
474 if self._options.startup_dialog: | |
475 shell_args.append('--testshell-startup-dialog') | |
476 | |
477 if self._options.gp_fault_error_box: | |
478 shell_args.append('--gp-fault-error-box') | |
479 | |
480 return (test_args, shell_args) | |
481 | |
482 def _ContainsTests(self, subdir): | |
483 for test_file in self._test_files_list: | |
484 if test_file.find(subdir) >= 0: | |
485 return True | |
486 return False | |
487 | |
488 def _InstantiateTestShellThreads(self, test_shell_binary, test_files, | |
489 result_summary): | |
490 """Instantitates and starts the TestShellThread(s). | |
491 | |
492 Return: | |
493 The list of threads. | |
494 """ | |
495 test_shell_command = [test_shell_binary] | |
496 | |
497 if self._options.wrapper: | |
498 # This split() isn't really what we want -- it incorrectly will | |
499 # split quoted strings within the wrapper argument -- but in | |
500 # practice it shouldn't come up and the --help output warns | |
501 # about it anyway. | |
502 test_shell_command = (self._options.wrapper.split() + | |
503 test_shell_command) | |
504 | |
505 filename_queue = self._GetTestFileQueue(test_files) | |
506 | |
507 # Instantiate TestShellThreads and start them. | |
508 threads = [] | |
509 for i in xrange(int(self._options.num_test_shells)): | |
510 # Create separate TestTypes instances for each thread. | |
511 test_types = [] | |
512 for t in self._test_types: | |
513 test_types.append(t(self._options.platform, | |
514 self._options.results_directory)) | |
515 | |
516 test_args, shell_args = self._GetTestShellArgs(i) | |
517 thread = test_shell_thread.TestShellThread(filename_queue, | |
518 self._result_queue, | |
519 test_shell_command, | |
520 test_types, | |
521 test_args, | |
522 shell_args, | |
523 self._options) | |
524 if self._IsSingleThreaded(): | |
525 thread.RunInMainThread(self, result_summary) | |
526 else: | |
527 thread.start() | |
528 threads.append(thread) | |
529 | |
530 return threads | |
531 | |
532 def _StopLayoutTestHelper(self, proc): | |
533 """Stop the layout test helper and closes it down.""" | |
534 if proc: | |
535 logging.debug("Stopping layout test helper") | |
536 proc.stdin.write("x\n") | |
537 proc.stdin.close() | |
538 proc.wait() | |
539 | |
540 def _IsSingleThreaded(self): | |
541 """Returns whether we should run all the tests in the main thread.""" | |
542 return int(self._options.num_test_shells) == 1 | |
543 | |
544 def _RunTests(self, test_shell_binary, file_list, result_summary): | |
545 """Runs the tests in the file_list. | |
546 | |
547 Return: A tuple (failures, thread_timings, test_timings, | |
548 individual_test_timings) | |
549 failures is a map from test to list of failure types | |
550 thread_timings is a list of dicts with the total runtime | |
551 of each thread with 'name', 'num_tests', 'total_time' properties | |
552 test_timings is a list of timings for each sharded subdirectory | |
553 of the form [time, directory_name, num_tests] | |
554 individual_test_timings is a list of run times for each test | |
555 in the form {filename:filename, test_run_time:test_run_time} | |
556 result_summary: summary object to populate with the results | |
557 """ | |
558 threads = self._InstantiateTestShellThreads(test_shell_binary, | |
559 file_list, | |
560 result_summary) | |
561 | |
562 # Wait for the threads to finish and collect test failures. | |
563 failures = {} | |
564 test_timings = {} | |
565 individual_test_timings = [] | |
566 thread_timings = [] | |
567 try: | |
568 for thread in threads: | |
569 while thread.isAlive(): | |
570 # Let it timeout occasionally so it can notice a | |
571 # KeyboardInterrupt. Actually, the timeout doesn't | |
572 # really matter: apparently it suffices to not use | |
573 # an indefinite blocking join for it to | |
574 # be interruptible by KeyboardInterrupt. | |
575 thread.join(0.1) | |
576 self.UpdateSummary(result_summary) | |
577 thread_timings.append({'name': thread.getName(), | |
578 'num_tests': thread.GetNumTests(), | |
579 'total_time': thread.GetTotalTime()}) | |
580 test_timings.update(thread.GetDirectoryTimingStats()) | |
581 individual_test_timings.extend(thread.GetIndividualTestStats()) | |
582 except KeyboardInterrupt: | |
583 for thread in threads: | |
584 thread.Cancel() | |
585 self._StopLayoutTestHelper(layout_test_helper_proc) | |
586 raise | |
587 for thread in threads: | |
588 # Check whether a TestShellThread died before normal completion. | |
589 exception_info = thread.GetExceptionInfo() | |
590 if exception_info is not None: | |
591 # Re-raise the thread's exception here to make it clear that | |
592 # testing was aborted. Otherwise, the tests that did not run | |
593 # would be assumed to have passed. | |
594 raise exception_info[0], exception_info[1], exception_info[2] | |
595 | |
596 # Make sure we pick up any remaining tests. | |
597 self.UpdateSummary(result_summary) | |
598 return (thread_timings, test_timings, individual_test_timings) | |
599 | |
600 def Run(self, result_summary): | |
601 """Run all our tests on all our test files. | |
602 | |
603 For each test file, we run each test type. If there are any failures, | |
604 we collect them for reporting. | |
605 | |
606 Args: | |
607 result_summary: a summary object tracking the test results. | |
608 | |
609 Return: | |
610 We return nonzero if there are regressions compared to the last run. | |
611 """ | |
612 if not self._test_files: | |
613 return 0 | |
614 start_time = time.time() | |
615 test_shell_binary = path_utils.TestShellPath(self._options.target) | |
616 | |
617 # Start up any helper needed | |
618 layout_test_helper_proc = None | |
619 if not options.no_pixel_tests: | |
620 helper_path = path_utils.LayoutTestHelperPath(self._options.target) | |
621 if len(helper_path): | |
622 logging.debug("Starting layout helper %s" % helper_path) | |
623 layout_test_helper_proc = subprocess.Popen( | |
624 [helper_path], stdin=subprocess.PIPE, | |
625 stdout=subprocess.PIPE, stderr=None) | |
626 is_ready = layout_test_helper_proc.stdout.readline() | |
627 if not is_ready.startswith('ready'): | |
628 logging.error("layout_test_helper failed to be ready") | |
629 | |
630 # Check that the system dependencies (themes, fonts, ...) are correct. | |
631 if not self._options.nocheck_sys_deps: | |
632 proc = subprocess.Popen([test_shell_binary, | |
633 "--check-layout-test-sys-deps"]) | |
634 if proc.wait() != 0: | |
635 logging.info("Aborting because system dependencies check " | |
636 "failed.\n To override, invoke with " | |
637 "--nocheck-sys-deps") | |
638 sys.exit(1) | |
639 | |
640 if self._ContainsTests(self.HTTP_SUBDIR): | |
641 self._http_server.Start() | |
642 | |
643 if self._ContainsTests(self.WEBSOCKET_SUBDIR): | |
644 self._websocket_server.Start() | |
645 # self._websocket_secure_server.Start() | |
646 | |
647 thread_timings, test_timings, individual_test_timings = ( | |
648 self._RunTests(test_shell_binary, self._test_files_list, | |
649 result_summary)) | |
650 | |
651 # We exclude the crashes from the list of results to retry, because | |
652 # we want to treat even a potentially flaky crash as an error. | |
653 failures = self._GetFailures(result_summary, include_crashes=False) | |
654 retries = 0 | |
655 retry_summary = result_summary | |
656 while (retries < self.NUM_RETRY_ON_UNEXPECTED_FAILURE and | |
657 len(failures)): | |
658 logging.debug("Retrying %d unexpected failure(s)" % len(failures)) | |
659 retries += 1 | |
660 retry_summary = ResultSummary(self._expectations, failures.keys()) | |
661 self._RunTests(test_shell_binary, failures.keys(), retry_summary) | |
662 failures = self._GetFailures(retry_summary, include_crashes=True) | |
663 | |
664 self._StopLayoutTestHelper(layout_test_helper_proc) | |
665 end_time = time.time() | |
666 | |
667 write = CreateLoggingWriter(self._options, 'timing') | |
668 self._PrintTimingStatistics(write, end_time - start_time, | |
669 thread_timings, test_timings, | |
670 individual_test_timings, | |
671 result_summary) | |
672 | |
673 self._meter.update("") | |
674 | |
675 if self._options.verbose: | |
676 # We write this block to stdout for compatibility with the | |
677 # buildbot log parser, which only looks at stdout, not stderr :( | |
678 write = lambda s: sys.stdout.write("%s\n" % s) | |
679 else: | |
680 write = CreateLoggingWriter(self._options, 'actual') | |
681 | |
682 self._PrintResultSummary(write, result_summary) | |
683 | |
684 sys.stdout.flush() | |
685 sys.stderr.flush() | |
686 | |
687 if (LOG_DETAILED_PROGRESS in self._options.log or | |
688 (LOG_UNEXPECTED in self._options.log and | |
689 result_summary.total != result_summary.expected)): | |
690 print | |
691 | |
692 # This summary data gets written to stdout regardless of log level | |
693 self._PrintOneLineSummary(result_summary.total, | |
694 result_summary.expected) | |
695 | |
696 unexpected_results = self._SummarizeUnexpectedResults(result_summary, | |
697 retry_summary) | |
698 self._PrintUnexpectedResults(unexpected_results) | |
699 | |
700 # Write the same data to log files. | |
701 self._WriteJSONFiles(unexpected_results, result_summary, | |
702 individual_test_timings) | |
703 | |
704 # Write the summary to disk (results.html) and maybe open the | |
705 # test_shell to this file. | |
706 wrote_results = self._WriteResultsHtmlFile(result_summary) | |
707 if not self._options.noshow_results and wrote_results: | |
708 self._ShowResultsHtmlFile() | |
709 | |
710 # Ignore flaky failures and unexpected passes so we don't turn the | |
711 # bot red for those. | |
712 return unexpected_results['num_regressions'] | |
713 | |
714 def UpdateSummary(self, result_summary): | |
715 """Update the summary while running tests.""" | |
716 while True: | |
717 try: | |
718 (test, fail_list) = self._result_queue.get_nowait() | |
719 result = test_failures.DetermineResultType(fail_list) | |
720 expected = self._expectations.MatchesAnExpectedResult(test, | |
721 result) | |
722 result_summary.Add(test, fail_list, result, expected) | |
723 if (LOG_DETAILED_PROGRESS in self._options.log and | |
724 (self._options.experimental_fully_parallel or | |
725 self._IsSingleThreaded())): | |
726 self._DisplayDetailedProgress(result_summary) | |
727 else: | |
728 if not expected and LOG_UNEXPECTED in self._options.log: | |
729 self._PrintUnexpectedTestResult(test, result) | |
730 self._DisplayOneLineProgress(result_summary) | |
731 except Queue.Empty: | |
732 return | |
733 | |
734 def _DisplayOneLineProgress(self, result_summary): | |
735 """Displays the progress through the test run.""" | |
736 self._meter.update("Testing: %d ran as expected, %d didn't, %d left" % | |
737 (result_summary.expected, result_summary.unexpected, | |
738 result_summary.remaining)) | |
739 | |
740 def _DisplayDetailedProgress(self, result_summary): | |
741 """Display detailed progress output where we print the directory name | |
742 and one dot for each completed test. This is triggered by | |
743 "--log detailed-progress".""" | |
744 if self._current_test_number == len(self._test_files_list): | |
745 return | |
746 | |
747 next_test = self._test_files_list[self._current_test_number] | |
748 next_dir = os.path.dirname(path_utils.RelativeTestFilename(next_test)) | |
749 if self._current_progress_str == "": | |
750 self._current_progress_str = "%s: " % (next_dir) | |
751 self._current_dir = next_dir | |
752 | |
753 while next_test in result_summary.results: | |
754 if next_dir != self._current_dir: | |
755 self._meter.write("%s\n" % (self._current_progress_str)) | |
756 self._current_progress_str = "%s: ." % (next_dir) | |
757 self._current_dir = next_dir | |
758 else: | |
759 self._current_progress_str += "." | |
760 | |
761 if (next_test in result_summary.unexpected_results and | |
762 LOG_UNEXPECTED in self._options.log): | |
763 result = result_summary.unexpected_results[next_test] | |
764 self._meter.write("%s\n" % self._current_progress_str) | |
765 self._PrintUnexpectedTestResult(next_test, result) | |
766 self._current_progress_str = "%s: " % self._current_dir | |
767 | |
768 self._current_test_number += 1 | |
769 if self._current_test_number == len(self._test_files_list): | |
770 break | |
771 | |
772 next_test = self._test_files_list[self._current_test_number] | |
773 next_dir = os.path.dirname( | |
774 path_utils.RelativeTestFilename(next_test)) | |
775 | |
776 if result_summary.remaining: | |
777 remain_str = " (%d)" % (result_summary.remaining) | |
778 self._meter.update("%s%s" % | |
779 (self._current_progress_str, remain_str)) | |
780 else: | |
781 self._meter.write("%s\n" % (self._current_progress_str)) | |
782 | |
783 def _GetFailures(self, result_summary, include_crashes): | |
784 """Filters a dict of results and returns only the failures. | |
785 | |
786 Args: | |
787 result_summary: the results of the test run | |
788 include_crashes: whether crashes are included in the output. | |
789 We use False when finding the list of failures to retry | |
790 to see if the results were flaky. Although the crashes may also be | |
791 flaky, we treat them as if they aren't so that they're not ignored. | |
792 Returns: | |
793 a dict of files -> results | |
794 """ | |
795 failed_results = {} | |
796 for test, result in result_summary.unexpected_results.iteritems(): | |
797 if (result == test_expectations.PASS or | |
798 result == test_expectations.CRASH and not include_crashes): | |
799 continue | |
800 failed_results[test] = result | |
801 | |
802 return failed_results | |
803 | |
804 def _SummarizeUnexpectedResults(self, result_summary, retry_summary): | |
805 """Summarize any unexpected results as a dict. | |
806 | |
807 TODO(dpranke): split this data structure into a separate class? | |
808 | |
809 Args: | |
810 result_summary: summary object from initial test runs | |
811 retry_summary: summary object from final test run of retried tests | |
812 Returns: | |
813 A dictionary containing a summary of the unexpected results from the | |
814 run, with the following fields: | |
815 'version': a version indicator (1 in this version) | |
816 'fixable': # of fixable tests (NOW - PASS) | |
817 'skipped': # of skipped tests (NOW & SKIPPED) | |
818 'num_regressions': # of non-flaky failures | |
819 'num_flaky': # of flaky failures | |
820 'num_passes': # of unexpected passes | |
821 'tests': a dict of tests -> {'expected': '...', 'actual': '...'} | |
822 """ | |
823 results = {} | |
824 results['version'] = 1 | |
825 | |
826 tbe = result_summary.tests_by_expectation | |
827 tbt = result_summary.tests_by_timeline | |
828 results['fixable'] = len(tbt[test_expectations.NOW] - | |
829 tbe[test_expectations.PASS]) | |
830 results['skipped'] = len(tbt[test_expectations.NOW] & | |
831 tbe[test_expectations.SKIP]) | |
832 | |
833 num_passes = 0 | |
834 num_flaky = 0 | |
835 num_regressions = 0 | |
836 keywords = {} | |
837 for k, v in TestExpectationsFile.EXPECTATIONS.iteritems(): | |
838 keywords[v] = k.upper() | |
839 | |
840 tests = {} | |
841 for filename, result in result_summary.unexpected_results.iteritems(): | |
842 # Note that if a test crashed in the original run, we ignore | |
843 # whether or not it crashed when we retried it (if we retried it), | |
844 # and always consider the result not flaky. | |
845 test = path_utils.RelativeTestFilename(filename) | |
846 expected = self._expectations.GetExpectationsString(filename) | |
847 actual = [keywords[result]] | |
848 | |
849 if result == test_expectations.PASS: | |
850 num_passes += 1 | |
851 elif result == test_expectations.CRASH: | |
852 num_regressions += 1 | |
853 else: | |
854 if filename not in retry_summary.unexpected_results: | |
855 actual.extend( | |
856 self._expectations.GetExpectationsString( | |
857 filename).split(" ")) | |
858 num_flaky += 1 | |
859 else: | |
860 retry_result = retry_summary.unexpected_results[filename] | |
861 if result != retry_result: | |
862 actual.append(keywords[retry_result]) | |
863 num_flaky += 1 | |
864 else: | |
865 num_regressions += 1 | |
866 | |
867 tests[test] = {} | |
868 tests[test]['expected'] = expected | |
869 tests[test]['actual'] = " ".join(actual) | |
870 | |
871 results['tests'] = tests | |
872 results['num_passes'] = num_passes | |
873 results['num_flaky'] = num_flaky | |
874 results['num_regressions'] = num_regressions | |
875 | |
876 return results | |
877 | |
878 def _WriteJSONFiles(self, unexpected_results, result_summary, | |
879 individual_test_timings): | |
880 """Writes the results of the test run as JSON files into the results | |
881 dir. | |
882 | |
883 There are three different files written into the results dir: | |
884 unexpected_results.json: A short list of any unexpected results. | |
885 This is used by the buildbots to display results. | |
886 expectations.json: This is used by the flakiness dashboard. | |
887 results.json: A full list of the results - used by the flakiness | |
888 dashboard and the aggregate results dashboard. | |
889 | |
890 Args: | |
891 unexpected_results: dict of unexpected results | |
892 result_summary: full summary object | |
893 individual_test_timings: list of test times (used by the flakiness | |
894 dashboard). | |
895 """ | |
896 logging.debug("Writing JSON files in %s." % | |
897 self._options.results_directory) | |
898 unexpected_file = open(os.path.join(self._options.results_directory, | |
899 "unexpected_results.json"), "w") | |
900 unexpected_file.write(simplejson.dumps(unexpected_results, | |
901 sort_keys=True, indent=2)) | |
902 unexpected_file.close() | |
903 | |
904 # Write a json file of the test_expectations.txt file for the layout | |
905 # tests dashboard. | |
906 expectations_file = open(os.path.join(self._options.results_directory, | |
907 "expectations.json"), "w") | |
908 expectations_json = \ | |
909 self._expectations.GetExpectationsJsonForAllPlatforms() | |
910 expectations_file.write("ADD_EXPECTATIONS(" + expectations_json + ");") | |
911 expectations_file.close() | |
912 | |
913 json_layout_results_generator.JSONLayoutResultsGenerator( | |
914 self._options.builder_name, self._options.build_name, | |
915 self._options.build_number, self._options.results_directory, | |
916 BUILDER_BASE_URL, individual_test_timings, | |
917 self._expectations, result_summary, self._test_files_list) | |
918 | |
919 logging.debug("Finished writing JSON files.") | |
920 | |
921 def _PrintExpectedResultsOfType(self, write, result_summary, result_type, | |
922 result_type_str): | |
923 """Print the number of the tests in a given result class. | |
924 | |
925 Args: | |
926 write: A callback to write info to (e.g., a LoggingWriter) or | |
927 sys.stdout.write. | |
928 result_summary - the object containing all the results to report on | |
929 result_type - the particular result type to report in the summary. | |
930 result_type_str - a string description of the result_type. | |
931 """ | |
932 tests = self._expectations.GetTestsWithResultType(result_type) | |
933 now = result_summary.tests_by_timeline[test_expectations.NOW] | |
934 wontfix = result_summary.tests_by_timeline[test_expectations.WONTFIX] | |
935 defer = result_summary.tests_by_timeline[test_expectations.DEFER] | |
936 | |
937 # We use a fancy format string in order to print the data out in a | |
938 # nicely-aligned table. | |
939 fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd defer, %%%dd wontfix)" | |
940 % (self._NumDigits(now), self._NumDigits(defer), | |
941 self._NumDigits(wontfix))) | |
942 write(fmtstr % (len(tests), result_type_str, len(tests & now), | |
943 len(tests & defer), len(tests & wontfix))) | |
944 | |
945 def _NumDigits(self, num): | |
946 """Returns the number of digits needed to represent the length of a | |
947 sequence.""" | |
948 ndigits = 1 | |
949 if len(num): | |
950 ndigits = int(math.log10(len(num))) + 1 | |
951 return ndigits | |
952 | |
953 def _PrintTimingStatistics(self, write, total_time, thread_timings, | |
954 directory_test_timings, individual_test_timings, | |
955 result_summary): | |
956 """Record timing-specific information for the test run. | |
957 | |
958 Args: | |
959 write: A callback to write info to (e.g., a LoggingWriter) or | |
960 sys.stdout.write. | |
961 total_time: total elapsed time (in seconds) for the test run | |
962 thread_timings: wall clock time each thread ran for | |
963 directory_test_timings: timing by directory | |
964 individual_test_timings: timing by file | |
965 result_summary: summary object for the test run | |
966 """ | |
967 write("Test timing:") | |
968 write(" %6.2f total testing time" % total_time) | |
969 write("") | |
970 write("Thread timing:") | |
971 cuml_time = 0 | |
972 for t in thread_timings: | |
973 write(" %10s: %5d tests, %6.2f secs" % | |
974 (t['name'], t['num_tests'], t['total_time'])) | |
975 cuml_time += t['total_time'] | |
976 write(" %6.2f cumulative, %6.2f optimal" % | |
977 (cuml_time, cuml_time / int(self._options.num_test_shells))) | |
978 write("") | |
979 | |
980 self._PrintAggregateTestStatistics(write, individual_test_timings) | |
981 self._PrintIndividualTestTimes(write, individual_test_timings, | |
982 result_summary) | |
983 self._PrintDirectoryTimings(write, directory_test_timings) | |
984 | |
985 def _PrintAggregateTestStatistics(self, write, individual_test_timings): | |
986 """Prints aggregate statistics (e.g. median, mean, etc.) for all tests. | |
987 Args: | |
988 write: A callback to write info to (e.g., a LoggingWriter) or | |
989 sys.stdout.write. | |
990 individual_test_timings: List of test_shell_thread.TestStats for all | |
991 tests. | |
992 """ | |
993 test_types = individual_test_timings[0].time_for_diffs.keys() | |
994 times_for_test_shell = [] | |
995 times_for_diff_processing = [] | |
996 times_per_test_type = {} | |
997 for test_type in test_types: | |
998 times_per_test_type[test_type] = [] | |
999 | |
1000 for test_stats in individual_test_timings: | |
1001 times_for_test_shell.append(test_stats.test_run_time) | |
1002 times_for_diff_processing.append( | |
1003 test_stats.total_time_for_all_diffs) | |
1004 time_for_diffs = test_stats.time_for_diffs | |
1005 for test_type in test_types: | |
1006 times_per_test_type[test_type].append( | |
1007 time_for_diffs[test_type]) | |
1008 | |
1009 self._PrintStatisticsForTestTimings(write, | |
1010 "PER TEST TIME IN TESTSHELL (seconds):", times_for_test_shell) | |
1011 self._PrintStatisticsForTestTimings(write, | |
1012 "PER TEST DIFF PROCESSING TIMES (seconds):", | |
1013 times_for_diff_processing) | |
1014 for test_type in test_types: | |
1015 self._PrintStatisticsForTestTimings(write, | |
1016 "PER TEST TIMES BY TEST TYPE: %s" % test_type, | |
1017 times_per_test_type[test_type]) | |
1018 | |
1019 def _PrintIndividualTestTimes(self, write, individual_test_timings, | |
1020 result_summary): | |
1021 """Prints the run times for slow, timeout and crash tests. | |
1022 Args: | |
1023 write: A callback to write info to (e.g., a LoggingWriter) or | |
1024 sys.stdout.write. | |
1025 individual_test_timings: List of test_shell_thread.TestStats for all | |
1026 tests. | |
1027 result_summary: summary object for test run | |
1028 """ | |
1029 # Reverse-sort by the time spent in test_shell. | |
1030 individual_test_timings.sort(lambda a, b: | |
1031 cmp(b.test_run_time, a.test_run_time)) | |
1032 | |
1033 num_printed = 0 | |
1034 slow_tests = [] | |
1035 timeout_or_crash_tests = [] | |
1036 unexpected_slow_tests = [] | |
1037 for test_tuple in individual_test_timings: | |
1038 filename = test_tuple.filename | |
1039 is_timeout_crash_or_slow = False | |
1040 if self._expectations.HasModifier(filename, | |
1041 test_expectations.SLOW): | |
1042 is_timeout_crash_or_slow = True | |
1043 slow_tests.append(test_tuple) | |
1044 | |
1045 if filename in result_summary.failures: | |
1046 result = result_summary.results[filename] | |
1047 if (result == test_expectations.TIMEOUT or | |
1048 result == test_expectations.CRASH): | |
1049 is_timeout_crash_or_slow = True | |
1050 timeout_or_crash_tests.append(test_tuple) | |
1051 | |
1052 if (not is_timeout_crash_or_slow and | |
1053 num_printed < self._options.num_slow_tests_to_log): | |
1054 num_printed = num_printed + 1 | |
1055 unexpected_slow_tests.append(test_tuple) | |
1056 | |
1057 write("") | |
1058 self._PrintTestListTiming(write, "%s slowest tests that are not " | |
1059 "marked as SLOW and did not timeout/crash:" % | |
1060 self._options.num_slow_tests_to_log, unexpected_slow_tests) | |
1061 write("") | |
1062 self._PrintTestListTiming(write, "Tests marked as SLOW:", slow_tests) | |
1063 write("") | |
1064 self._PrintTestListTiming(write, "Tests that timed out or crashed:", | |
1065 timeout_or_crash_tests) | |
1066 write("") | |
1067 | |
1068 def _PrintTestListTiming(self, write, title, test_list): | |
1069 """Print timing info for each test. | |
1070 | |
1071 Args: | |
1072 write: A callback to write info to (e.g., a LoggingWriter) or | |
1073 sys.stdout.write. | |
1074 title: section heading | |
1075 test_list: tests that fall in this section | |
1076 """ | |
1077 write(title) | |
1078 for test_tuple in test_list: | |
1079 filename = test_tuple.filename[len( | |
1080 path_utils.LayoutTestsDir()) + 1:] | |
1081 filename = filename.replace('\\', '/') | |
1082 test_run_time = round(test_tuple.test_run_time, 1) | |
1083 write(" %s took %s seconds" % (filename, test_run_time)) | |
1084 | |
1085 def _PrintDirectoryTimings(self, write, directory_test_timings): | |
1086 """Print timing info by directory for any directories that | |
1087 take > 10 seconds to run. | |
1088 | |
1089 Args: | |
1090 write: A callback to write info to (e.g., a LoggingWriter) or | |
1091 sys.stdout.write. | |
1092 directory_test_timing: time info for each directory | |
1093 """ | |
1094 timings = [] | |
1095 for directory in directory_test_timings: | |
1096 num_tests, time_for_directory = directory_test_timings[directory] | |
1097 timings.append((round(time_for_directory, 1), directory, | |
1098 num_tests)) | |
1099 timings.sort() | |
1100 | |
1101 write("Time to process slowest subdirectories:") | |
1102 min_seconds_to_print = 10 | |
1103 for timing in timings: | |
1104 if timing[0] > min_seconds_to_print: | |
1105 write(" %s took %s seconds to run %s tests." % (timing[1], | |
1106 timing[0], timing[2])) | |
1107 write("") | |
1108 | |
1109 def _PrintStatisticsForTestTimings(self, write, title, timings): | |
1110 """Prints the median, mean and standard deviation of the values in | |
1111 timings. | |
1112 | |
1113 Args: | |
1114 write: A callback to write info to (e.g., a LoggingWriter) or | |
1115 sys.stdout.write. | |
1116 title: Title for these timings. | |
1117 timings: A list of floats representing times. | |
1118 """ | |
1119 write(title) | |
1120 timings.sort() | |
1121 | |
1122 num_tests = len(timings) | |
1123 percentile90 = timings[int(.9 * num_tests)] | |
1124 percentile99 = timings[int(.99 * num_tests)] | |
1125 | |
1126 if num_tests % 2 == 1: | |
1127 median = timings[((num_tests - 1) / 2) - 1] | |
1128 else: | |
1129 lower = timings[num_tests / 2 - 1] | |
1130 upper = timings[num_tests / 2] | |
1131 median = (float(lower + upper)) / 2 | |
1132 | |
1133 mean = sum(timings) / num_tests | |
1134 | |
1135 for time in timings: | |
1136 sum_of_deviations = math.pow(time - mean, 2) | |
1137 | |
1138 std_deviation = math.sqrt(sum_of_deviations / num_tests) | |
1139 write(" Median: %6.3f" % median) | |
1140 write(" Mean: %6.3f" % mean) | |
1141 write(" 90th percentile: %6.3f" % percentile90) | |
1142 write(" 99th percentile: %6.3f" % percentile99) | |
1143 write(" Standard dev: %6.3f" % std_deviation) | |
1144 write("") | |
1145 | |
1146 def _PrintResultSummary(self, write, result_summary): | |
1147 """Print a short summary about how many tests passed. | |
1148 | |
1149 Args: | |
1150 write: A callback to write info to (e.g., a LoggingWriter) or | |
1151 sys.stdout.write. | |
1152 result_summary: information to log | |
1153 """ | |
1154 failed = len(result_summary.failures) | |
1155 skipped = len( | |
1156 result_summary.tests_by_expectation[test_expectations.SKIP]) | |
1157 total = result_summary.total | |
1158 passed = total - failed - skipped | |
1159 pct_passed = 0.0 | |
1160 if total > 0: | |
1161 pct_passed = float(passed) * 100 / total | |
1162 | |
1163 write("") | |
1164 write("=> Results: %d/%d tests passed (%.1f%%)" % | |
1165 (passed, total, pct_passed)) | |
1166 write("") | |
1167 self._PrintResultSummaryEntry(write, result_summary, | |
1168 test_expectations.NOW, "Tests to be fixed for the current release") | |
1169 | |
1170 write("") | |
1171 self._PrintResultSummaryEntry(write, result_summary, | |
1172 test_expectations.DEFER, | |
1173 "Tests we'll fix in the future if they fail (DEFER)") | |
1174 | |
1175 write("") | |
1176 self._PrintResultSummaryEntry(write, result_summary, | |
1177 test_expectations.WONTFIX, | |
1178 "Tests that will only be fixed if they crash (WONTFIX)") | |
1179 | |
1180 def _PrintResultSummaryEntry(self, write, result_summary, timeline, | |
1181 heading): | |
1182 """Print a summary block of results for a particular timeline of test. | |
1183 | |
1184 Args: | |
1185 write: A callback to write info to (e.g., a LoggingWriter) or | |
1186 sys.stdout.write. | |
1187 result_summary: summary to print results for | |
1188 timeline: the timeline to print results for (NOT, WONTFIX, etc.) | |
1189 heading: a textual description of the timeline | |
1190 """ | |
1191 total = len(result_summary.tests_by_timeline[timeline]) | |
1192 not_passing = (total - | |
1193 len(result_summary.tests_by_expectation[test_expectations.PASS] & | |
1194 result_summary.tests_by_timeline[timeline])) | |
1195 write("=> %s (%d):" % (heading, not_passing)) | |
1196 | |
1197 for result in TestExpectationsFile.EXPECTATION_ORDER: | |
1198 if result == test_expectations.PASS: | |
1199 continue | |
1200 results = (result_summary.tests_by_expectation[result] & | |
1201 result_summary.tests_by_timeline[timeline]) | |
1202 desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result] | |
1203 if not_passing and len(results): | |
1204 pct = len(results) * 100.0 / not_passing | |
1205 write(" %5d %-24s (%4.1f%%)" % (len(results), | |
1206 desc[len(results) != 1], pct)) | |
1207 | |
1208 def _PrintOneLineSummary(self, total, expected): | |
1209 """Print a one-line summary of the test run to stdout. | |
1210 | |
1211 Args: | |
1212 total: total number of tests run | |
1213 expected: number of expected results | |
1214 """ | |
1215 unexpected = total - expected | |
1216 if unexpected == 0: | |
1217 print "All %d tests ran as expected." % expected | |
1218 elif expected == 1: | |
1219 print "1 test ran as expected, %d didn't:" % unexpected | |
1220 else: | |
1221 print "%d tests ran as expected, %d didn't:" % (expected, | |
1222 unexpected) | |
1223 | |
1224 def _PrintUnexpectedResults(self, unexpected_results): | |
1225 """Prints any unexpected results in a human-readable form to stdout.""" | |
1226 passes = {} | |
1227 flaky = {} | |
1228 regressions = {} | |
1229 | |
1230 if len(unexpected_results['tests']): | |
1231 print "" | |
1232 | |
1233 for test, results in unexpected_results['tests'].iteritems(): | |
1234 actual = results['actual'].split(" ") | |
1235 expected = results['expected'].split(" ") | |
1236 if actual == ['PASS']: | |
1237 if 'CRASH' in expected: | |
1238 _AddToDictOfLists(passes, 'Expected to crash, but passed', | |
1239 test) | |
1240 elif 'TIMEOUT' in expected: | |
1241 _AddToDictOfLists(passes, | |
1242 'Expected to timeout, but passed', test) | |
1243 else: | |
1244 _AddToDictOfLists(passes, 'Expected to fail, but passed', | |
1245 test) | |
1246 elif len(actual) > 1: | |
1247 # We group flaky tests by the first actual result we got. | |
1248 _AddToDictOfLists(flaky, actual[0], test) | |
1249 else: | |
1250 _AddToDictOfLists(regressions, results['actual'], test) | |
1251 | |
1252 if len(passes): | |
1253 for key, tests in passes.iteritems(): | |
1254 print "%s: (%d)" % (key, len(tests)) | |
1255 tests.sort() | |
1256 for test in tests: | |
1257 print " %s" % test | |
1258 print | |
1259 | |
1260 if len(flaky): | |
1261 descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS | |
1262 for key, tests in flaky.iteritems(): | |
1263 result = TestExpectationsFile.EXPECTATIONS[key.lower()] | |
1264 print "Unexpected flakiness: %s (%d)" % ( | |
1265 descriptions[result][1], len(tests)) | |
1266 tests.sort() | |
1267 | |
1268 for test in tests: | |
1269 result = unexpected_results['tests'][test] | |
1270 actual = result['actual'].split(" ") | |
1271 expected = result['expected'].split(" ") | |
1272 result = TestExpectationsFile.EXPECTATIONS[key.lower()] | |
1273 new_expectations_list = list(set(actual) | set(expected)) | |
1274 print " %s = %s" % (test, " ".join(new_expectations_list)) | |
1275 print | |
1276 | |
1277 if len(regressions): | |
1278 descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS | |
1279 for key, tests in regressions.iteritems(): | |
1280 result = TestExpectationsFile.EXPECTATIONS[key.lower()] | |
1281 print "Regressions: Unexpected %s : (%d)" % ( | |
1282 descriptions[result][1], len(tests)) | |
1283 tests.sort() | |
1284 for test in tests: | |
1285 print " %s = %s" % (test, key) | |
1286 print | |
1287 | |
1288 if len(unexpected_results['tests']) and self._options.verbose: | |
1289 print "-" * 78 | |
1290 | |
1291 def _PrintUnexpectedTestResult(self, test, result): | |
1292 """Prints one unexpected test result line.""" | |
1293 desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result][0] | |
1294 self._meter.write(" %s -> unexpected %s\n" % | |
1295 (path_utils.RelativeTestFilename(test), desc)) | |
1296 | |
1297 def _WriteResultsHtmlFile(self, result_summary): | |
1298 """Write results.html which is a summary of tests that failed. | |
1299 | |
1300 Args: | |
1301 result_summary: a summary of the results :) | |
1302 | |
1303 Returns: | |
1304 True if any results were written (since expected failures may be | |
1305 omitted) | |
1306 """ | |
1307 # test failures | |
1308 if self._options.full_results_html: | |
1309 test_files = result_summary.failures.keys() | |
1310 else: | |
1311 unexpected_failures = self._GetFailures(result_summary, | |
1312 include_crashes=True) | |
1313 test_files = unexpected_failures.keys() | |
1314 if not len(test_files): | |
1315 return False | |
1316 | |
1317 out_filename = os.path.join(self._options.results_directory, | |
1318 "results.html") | 1311 "results.html") |
1319 out_file = open(out_filename, 'w') | 1312 subprocess.Popen([path_utils.TestShellPath(self._options.target), |
1320 # header | 1313 path_utils.FilenameToUri(results_filename)]) |
1321 if self._options.full_results_html: | |
1322 h2 = "Test Failures" | |
1323 else: | |
1324 h2 = "Unexpected Test Failures" | |
1325 out_file.write("<html><head><title>Layout Test Results (%(time)s)" | |
1326 "</title></head><body><h2>%(h2)s (%(time)s)</h2>\n" | |
1327 % {'h2': h2, 'time': time.asctime()}) | |
1328 | |
1329 test_files.sort() | |
1330 for test_file in test_files: | |
1331 test_failures = result_summary.failures.get(test_file, []) | |
1332 out_file.write("<p><a href='%s'>%s</a><br />\n" | |
1333 % (path_utils.FilenameToUri(test_file), | |
1334 path_utils.RelativeTestFilename(test_file))) | |
1335 for failure in test_failures: | |
1336 out_file.write(" %s<br/>" | |
1337 % failure.ResultHtmlOutput( | |
1338 path_utils.RelativeTestFilename(test_file))) | |
1339 out_file.write("</p>\n") | |
1340 | |
1341 # footer | |
1342 out_file.write("</body></html>\n") | |
1343 return True | |
1344 | |
1345 def _ShowResultsHtmlFile(self): | |
1346 """Launches the test shell open to the results.html page.""" | |
1347 results_filename = os.path.join(self._options.results_directory, | |
1348 "results.html") | |
1349 subprocess.Popen([path_utils.TestShellPath(self._options.target), | |
1350 path_utils.FilenameToUri(results_filename)]) | |
1351 | 1314 |
1352 | 1315 |
1353 def _AddToDictOfLists(dict, key, value): | 1316 def _AddToDictOfLists(dict, key, value): |
1354 dict.setdefault(key, []).append(value) | 1317 dict.setdefault(key, []).append(value) |
1355 | |
1356 | 1318 |
1357 def ReadTestFiles(files): | 1319 def ReadTestFiles(files): |
1358 tests = [] | 1320 tests = [] |
1359 for file in files: | 1321 for file in files: |
1360 for line in open(file): | 1322 for line in open(file): |
1361 line = test_expectations.StripComments(line) | 1323 line = test_expectations.StripComments(line) |
1362 if line: | 1324 if line: tests.append(line) |
1363 tests.append(line) | 1325 return tests |
1364 return tests | |
1365 | |
1366 | 1326 |
1367 def CreateLoggingWriter(options, log_option): | 1327 def CreateLoggingWriter(options, log_option): |
1368 """Returns a write() function that will write the string to logging.info() | 1328 """Returns a write() function that will write the string to logging.info() |
1369 if comp was specified in --log or if --verbose is true. Otherwise the | 1329 if comp was specified in --log or if --verbose is true. Otherwise the |
1370 message is dropped. | 1330 message is dropped. |
1371 | 1331 |
1372 Args: | 1332 Args: |
1373 options: list of command line options from optparse | 1333 options: list of command line options from optparse |
1374 log_option: option to match in options.log in order for the messages | 1334 log_option: option to match in options.log in order for the messages to be |
1375 to be logged (e.g., 'actual' or 'expected') | 1335 logged (e.g., 'actual' or 'expected') |
1376 """ | 1336 """ |
1377 if options.verbose or log_option in options.log.split(","): | 1337 if options.verbose or log_option in options.log.split(","): |
1378 return logging.info | 1338 return logging.info |
1379 return lambda str: 1 | 1339 return lambda str: 1 |
1380 | |
1381 | 1340 |
1382 def main(options, args): | 1341 def main(options, args): |
1383 """Run the tests. Will call sys.exit when complete. | 1342 """Run the tests. Will call sys.exit when complete. |
1384 | 1343 |
1385 Args: | 1344 Args: |
1386 options: a dictionary of command line options | 1345 options: a dictionary of command line options |
1387 args: a list of sub directories or files to test | 1346 args: a list of sub directories or files to test |
1388 """ | 1347 """ |
1389 | 1348 |
1390 if options.sources: | 1349 if options.sources: |
1391 options.verbose = True | 1350 options.verbose = True |
1392 | 1351 |
1393 # Set up our logging format. | 1352 # Set up our logging format. |
1394 meter = metered_stream.MeteredStream(options.verbose, sys.stderr) | 1353 meter = metered_stream.MeteredStream(options.verbose, sys.stderr) |
1395 log_fmt = '%(message)s' | 1354 log_fmt = '%(message)s' |
1396 log_datefmt = '%y%m%d %H:%M:%S' | 1355 log_datefmt = '%y%m%d %H:%M:%S' |
1397 log_level = logging.INFO | 1356 log_level = logging.INFO |
1398 if options.verbose: | 1357 if options.verbose: |
1399 log_fmt = ('%(asctime)s %(filename)s:%(lineno)-4d %(levelname)s ' | 1358 log_fmt = '%(asctime)s %(filename)s:%(lineno)-4d %(levelname)s %(message)s' |
1400 '%(message)s') | 1359 log_level = logging.DEBUG |
1401 log_level = logging.DEBUG | 1360 logging.basicConfig(level=log_level, format=log_fmt, datefmt=log_datefmt, |
1402 logging.basicConfig(level=log_level, format=log_fmt, datefmt=log_datefmt, | 1361 stream=meter) |
1403 stream=meter) | 1362 |
1404 | 1363 if not options.target: |
1405 if not options.target: | 1364 if options.debug: |
1406 if options.debug: | 1365 options.target = "Debug" |
1407 options.target = "Debug" | |
1408 else: | |
1409 options.target = "Release" | |
1410 | |
1411 if not options.use_apache: | |
1412 options.use_apache = sys.platform in ('darwin', 'linux2') | |
1413 | |
1414 if options.results_directory.startswith("/"): | |
1415 # Assume it's an absolute path and normalize. | |
1416 options.results_directory = path_utils.GetAbsolutePath( | |
1417 options.results_directory) | |
1418 else: | 1366 else: |
1419 # If it's a relative path, make the output directory relative to | 1367 options.target = "Release" |
1420 # Debug or Release. | 1368 |
1421 basedir = path_utils.PathFromBase('webkit') | 1369 if not options.use_apache: |
1422 options.results_directory = path_utils.GetAbsolutePath( | 1370 options.use_apache = sys.platform in ('darwin', 'linux2') |
1423 os.path.join(basedir, options.target, options.results_directory)) | 1371 |
1424 | 1372 if options.results_directory.startswith("/"): |
1425 if options.clobber_old_results: | 1373 # Assume it's an absolute path and normalize. |
1426 # Just clobber the actual test results directories since the other | 1374 options.results_directory = path_utils.GetAbsolutePath( |
1427 # files in the results directory are explicitly used for cross-run | 1375 options.results_directory) |
1428 # tracking. | 1376 else: |
1429 path = os.path.join(options.results_directory, 'LayoutTests') | 1377 # If it's a relative path, make the output directory relative to Debug or |
1430 if os.path.exists(path): | 1378 # Release. |
1431 shutil.rmtree(path) | 1379 basedir = path_utils.PathFromBase('webkit') |
1432 | 1380 options.results_directory = path_utils.GetAbsolutePath( |
1433 # Ensure platform is valid and force it to the form 'chromium-<platform>'. | 1381 os.path.join(basedir, options.target, options.results_directory)) |
1434 options.platform = path_utils.PlatformName(options.platform) | 1382 |
1435 | 1383 if options.clobber_old_results: |
1436 if not options.num_test_shells: | 1384 # Just clobber the actual test results directories since the other files |
1437 # TODO(ojan): Investigate perf/flakiness impact of using numcores + 1. | 1385 # in the results directory are explicitly used for cross-run tracking. |
1438 options.num_test_shells = platform_utils.GetNumCores() | 1386 path = os.path.join(options.results_directory, 'LayoutTests') |
1439 | 1387 if os.path.exists(path): |
1440 write = CreateLoggingWriter(options, 'config') | 1388 shutil.rmtree(path) |
1441 write("Running %s test_shells in parallel" % options.num_test_shells) | 1389 |
1442 | 1390 # Ensure platform is valid and force it to the form 'chromium-<platform>'. |
1443 if not options.time_out_ms: | 1391 options.platform = path_utils.PlatformName(options.platform) |
1444 if options.target == "Debug": | 1392 |
1445 options.time_out_ms = str(2 * TestRunner.DEFAULT_TEST_TIMEOUT_MS) | 1393 if not options.num_test_shells: |
1446 else: | 1394 # TODO(ojan): Investigate perf/flakiness impact of using numcores + 1. |
1447 options.time_out_ms = str(TestRunner.DEFAULT_TEST_TIMEOUT_MS) | 1395 options.num_test_shells = platform_utils.GetNumCores() |
1448 | 1396 |
1449 options.slow_time_out_ms = str(5 * int(options.time_out_ms)) | 1397 write = CreateLoggingWriter(options, 'config') |
1450 write("Regular timeout: %s, slow test timeout: %s" % | 1398 write("Running %s test_shells in parallel" % options.num_test_shells) |
1451 (options.time_out_ms, options.slow_time_out_ms)) | 1399 |
1452 | 1400 if not options.time_out_ms: |
1453 # Include all tests if none are specified. | 1401 if options.target == "Debug": |
1454 new_args = [] | 1402 options.time_out_ms = str(2 * TestRunner.DEFAULT_TEST_TIMEOUT_MS) |
1455 for arg in args: | 1403 else: |
1456 if arg and arg != '': | 1404 options.time_out_ms = str(TestRunner.DEFAULT_TEST_TIMEOUT_MS) |
1457 new_args.append(arg) | 1405 |
1458 | 1406 options.slow_time_out_ms = str(5 * int(options.time_out_ms)) |
1459 paths = new_args | 1407 write("Regular timeout: %s, slow test timeout: %s" % |
1460 if not paths: | 1408 (options.time_out_ms, options.slow_time_out_ms)) |
1461 paths = [] | 1409 |
1462 if options.test_list: | 1410 # Include all tests if none are specified. |
1463 paths += ReadTestFiles(options.test_list) | 1411 new_args = [] |
1464 | 1412 for arg in args: |
1465 # Create the output directory if it doesn't already exist. | 1413 if arg and arg != '': |
1466 path_utils.MaybeMakeDirectory(options.results_directory) | 1414 new_args.append(arg) |
1467 meter.update("Gathering files ...") | 1415 |
1468 | 1416 paths = new_args |
1469 test_runner = TestRunner(options, meter) | 1417 if not paths: |
1470 test_runner.GatherFilePaths(paths) | 1418 paths = [] |
1471 | 1419 if options.test_list: |
1472 if options.lint_test_files: | 1420 paths += ReadTestFiles(options.test_list) |
1473 # Creating the expecations for each platform/target pair does all the | 1421 |
1474 # test list parsing and ensures it's correct syntax (e.g. no dupes). | 1422 # Create the output directory if it doesn't already exist. |
1475 for platform in TestExpectationsFile.PLATFORMS: | 1423 path_utils.MaybeMakeDirectory(options.results_directory) |
1476 test_runner.ParseExpectations(platform, is_debug_mode=True) | 1424 meter.update("Gathering files ...") |
1477 test_runner.ParseExpectations(platform, is_debug_mode=False) | 1425 |
1478 print ("If there are no fail messages, errors or exceptions, then the " | 1426 test_runner = TestRunner(options, meter) |
1479 "lint succeeded.") | 1427 test_runner.GatherFilePaths(paths) |
1480 sys.exit(0) | 1428 |
1481 | 1429 if options.lint_test_files: |
1482 try: | 1430 # Creating the expecations for each platform/target pair does all the |
1483 test_shell_binary_path = path_utils.TestShellPath(options.target) | 1431 # test list parsing and ensures it's correct syntax (e.g. no dupes). |
1484 except path_utils.PathNotFound: | 1432 for platform in TestExpectationsFile.PLATFORMS: |
1485 print "\nERROR: test_shell is not found. Be sure that you have built" | 1433 test_runner.ParseExpectations(platform, is_debug_mode=True) |
1486 print "it and that you are using the correct build. This script" | 1434 test_runner.ParseExpectations(platform, is_debug_mode=False) |
1487 print "will run the Release one by default. Use --debug to use the" | 1435 print ("If there are no fail messages, errors or exceptions, then the " |
1488 print "Debug build.\n" | 1436 "lint succeeded.") |
1489 sys.exit(1) | 1437 sys.exit(0) |
1490 | 1438 |
1491 write = CreateLoggingWriter(options, "config") | 1439 try: |
1492 write("Using platform '%s'" % options.platform) | 1440 test_shell_binary_path = path_utils.TestShellPath(options.target) |
1493 write("Placing test results in %s" % options.results_directory) | 1441 except path_utils.PathNotFound: |
1494 if options.new_baseline: | 1442 print "\nERROR: test_shell is not found. Be sure that you have built it" |
1495 write("Placing new baselines in %s" % | 1443 print "and that you are using the correct build. This script will run the" |
1496 path_utils.ChromiumBaselinePath(options.platform)) | 1444 print "Release one by default. Use --debug to use the Debug build.\n" |
1497 write("Using %s build at %s" % (options.target, test_shell_binary_path)) | 1445 sys.exit(1) |
1498 if options.no_pixel_tests: | 1446 |
1499 write("Not running pixel tests") | 1447 write = CreateLoggingWriter(options, "config") |
1500 write("") | 1448 write("Using platform '%s'" % options.platform) |
1501 | 1449 write("Placing test results in %s" % options.results_directory) |
1502 meter.update("Parsing expectations ...") | 1450 if options.new_baseline: |
1503 test_runner.ParseExpectations(options.platform, options.target == 'Debug') | 1451 write("Placing new baselines in %s" % |
1504 | 1452 path_utils.ChromiumBaselinePath(options.platform)) |
1505 meter.update("Preparing tests ...") | 1453 write("Using %s build at %s" % (options.target, test_shell_binary_path)) |
1506 write = CreateLoggingWriter(options, "expected") | 1454 if options.no_pixel_tests: |
1507 result_summary = test_runner.PrepareListsAndPrintOutput(write) | 1455 write("Not running pixel tests") |
1508 | 1456 write("") |
1509 if 'cygwin' == sys.platform: | 1457 |
1510 logging.warn("#" * 40) | 1458 meter.update("Parsing expectations ...") |
1511 logging.warn("# UNEXPECTED PYTHON VERSION") | 1459 test_runner.ParseExpectations(options.platform, options.target == 'Debug') |
1512 logging.warn("# This script should be run using the version of python") | 1460 |
1513 logging.warn("# in third_party/python_24/") | 1461 meter.update("Preparing tests ...") |
1514 logging.warn("#" * 40) | 1462 write = CreateLoggingWriter(options, "expected") |
1515 sys.exit(1) | 1463 result_summary = test_runner.PrepareListsAndPrintOutput(write) |
1516 | 1464 |
1517 # Delete the disk cache if any to ensure a clean test run. | 1465 if 'cygwin' == sys.platform: |
1518 cachedir = os.path.split(test_shell_binary_path)[0] | 1466 logging.warn("#" * 40) |
1519 cachedir = os.path.join(cachedir, "cache") | 1467 logging.warn("# UNEXPECTED PYTHON VERSION") |
1520 if os.path.exists(cachedir): | 1468 logging.warn("# This script should be run using the version of python") |
1521 shutil.rmtree(cachedir) | 1469 logging.warn("# in third_party/python_24/") |
1522 | 1470 logging.warn("#" * 40) |
1523 test_runner.AddTestType(text_diff.TestTextDiff) | 1471 sys.exit(1) |
1524 if not options.no_pixel_tests: | 1472 |
1525 test_runner.AddTestType(image_diff.ImageDiff) | 1473 # Delete the disk cache if any to ensure a clean test run. |
1526 if options.fuzzy_pixel_tests: | 1474 cachedir = os.path.split(test_shell_binary_path)[0] |
1527 test_runner.AddTestType(fuzzy_image_diff.FuzzyImageDiff) | 1475 cachedir = os.path.join(cachedir, "cache") |
1528 | 1476 if os.path.exists(cachedir): |
1529 meter.update("Starting ...") | 1477 shutil.rmtree(cachedir) |
1530 has_new_failures = test_runner.Run(result_summary) | 1478 |
1531 | 1479 test_runner.AddTestType(text_diff.TestTextDiff) |
1532 logging.debug("Exit status: %d" % has_new_failures) | 1480 if not options.no_pixel_tests: |
1533 sys.exit(has_new_failures) | 1481 test_runner.AddTestType(image_diff.ImageDiff) |
| 1482 if options.fuzzy_pixel_tests: |
| 1483 test_runner.AddTestType(fuzzy_image_diff.FuzzyImageDiff) |
| 1484 |
| 1485 meter.update("Starting ...") |
| 1486 has_new_failures = test_runner.Run(result_summary) |
| 1487 |
| 1488 logging.debug("Exit status: %d" % has_new_failures) |
| 1489 sys.exit(has_new_failures) |
1534 | 1490 |
1535 if '__main__' == __name__: | 1491 if '__main__' == __name__: |
1536 option_parser = optparse.OptionParser() | 1492 option_parser = optparse.OptionParser() |
1537 option_parser.add_option("", "--no-pixel-tests", action="store_true", | 1493 option_parser.add_option("", "--no-pixel-tests", action="store_true", |
1538 default=False, | 1494 default=False, |
1539 help="disable pixel-to-pixel PNG comparisons") | 1495 help="disable pixel-to-pixel PNG comparisons") |
1540 option_parser.add_option("", "--fuzzy-pixel-tests", action="store_true", | 1496 option_parser.add_option("", "--fuzzy-pixel-tests", action="store_true", |
1541 default=False, | 1497 default=False, |
1542 help="Also use fuzzy matching to compare pixel " | 1498 help="Also use fuzzy matching to compare pixel test" |
1543 "test outputs.") | 1499 " outputs.") |
1544 option_parser.add_option("", "--results-directory", | 1500 option_parser.add_option("", "--results-directory", |
1545 default="layout-test-results", | 1501 default="layout-test-results", |
1546 help="Output results directory source dir," | 1502 help="Output results directory source dir," |
1547 " relative to Debug or Release") | 1503 " relative to Debug or Release") |
1548 option_parser.add_option("", "--new-baseline", action="store_true", | 1504 option_parser.add_option("", "--new-baseline", action="store_true", |
1549 default=False, | 1505 default=False, |
1550 help="save all generated results as new baselines" | 1506 help="save all generated results as new baselines " |
1551 " into the platform directory, overwriting " | 1507 "into the platform directory, overwriting " |
1552 "whatever's already there.") | 1508 "whatever's already there.") |
1553 option_parser.add_option("", "--noshow-results", action="store_true", | 1509 option_parser.add_option("", "--noshow-results", action="store_true", |
1554 default=False, help="don't launch the test_shell" | 1510 default=False, help="don't launch the test_shell" |
1555 " with results after the tests are done") | 1511 " with results after the tests are done") |
1556 option_parser.add_option("", "--full-results-html", action="store_true", | 1512 option_parser.add_option("", "--full-results-html", action="store_true", |
1557 default=False, help="show all failures in " | 1513 default=False, help="show all failures in " |
1558 "results.html, rather than only regressions") | 1514 "results.html, rather than only regressions") |
1559 option_parser.add_option("", "--clobber-old-results", action="store_true", | 1515 option_parser.add_option("", "--clobber-old-results", action="store_true", |
1560 default=False, help="Clobbers test results from " | 1516 default=False, help="Clobbers test results from " |
1561 "previous runs.") | 1517 "previous runs.") |
1562 option_parser.add_option("", "--lint-test-files", action="store_true", | 1518 option_parser.add_option("", "--lint-test-files", action="store_true", |
1563 default=False, help="Makes sure the test files " | 1519 default=False, help="Makes sure the test files " |
1564 "parse for all configurations. Does not run any " | 1520 "parse for all configurations. Does not run any " |
1565 "tests.") | 1521 "tests.") |
1566 option_parser.add_option("", "--force", action="store_true", | 1522 option_parser.add_option("", "--force", action="store_true", |
1567 default=False, | 1523 default=False, |
1568 help="Run all tests, even those marked SKIP " | 1524 help="Run all tests, even those marked SKIP in the " |
1569 "in the test list") | 1525 "test list") |
1570 option_parser.add_option("", "--num-test-shells", | 1526 option_parser.add_option("", "--num-test-shells", |
1571 help="Number of testshells to run in parallel.") | 1527 help="Number of testshells to run in parallel.") |
1572 option_parser.add_option("", "--use-apache", action="store_true", | 1528 option_parser.add_option("", "--use-apache", action="store_true", |
1573 default=False, | 1529 default=False, |
1574 help="Whether to use apache instead of lighttpd.") | 1530 help="Whether to use apache instead of lighttpd.") |
1575 option_parser.add_option("", "--time-out-ms", default=None, | 1531 option_parser.add_option("", "--time-out-ms", default=None, |
1576 help="Set the timeout for each test") | 1532 help="Set the timeout for each test") |
1577 option_parser.add_option("", "--run-singly", action="store_true", | 1533 option_parser.add_option("", "--run-singly", action="store_true", |
1578 default=False, | 1534 default=False, |
1579 help="run a separate test_shell for each test") | 1535 help="run a separate test_shell for each test") |
1580 option_parser.add_option("", "--debug", action="store_true", default=False, | 1536 option_parser.add_option("", "--debug", action="store_true", default=False, |
1581 help="use the debug binary instead of the release" | 1537 help="use the debug binary instead of the release " |
1582 " binary") | 1538 "binary") |
1583 option_parser.add_option("", "--num-slow-tests-to-log", default=50, | 1539 option_parser.add_option("", "--num-slow-tests-to-log", default=50, |
1584 help="Number of slow tests whose timings " | 1540 help="Number of slow tests whose timings to print.") |
1585 "to print.") | 1541 option_parser.add_option("", "--platform", |
1586 option_parser.add_option("", "--platform", | 1542 help="Override the platform for expected results") |
1587 help="Override the platform for expected results") | 1543 option_parser.add_option("", "--target", default="", |
1588 option_parser.add_option("", "--target", default="", | 1544 help="Set the build target configuration (overrides" |
1589 help="Set the build target configuration " | 1545 " --debug)") |
1590 "(overrides --debug)") | 1546 option_parser.add_option("", "--log", action="store", |
1591 option_parser.add_option("", "--log", action="store", | 1547 default="detailed-progress,unexpected", |
1592 default="detailed-progress,unexpected", | 1548 help="log various types of data. The param should " |
1593 help="log various types of data. The param should" | 1549 "be a comma-separated list of values from: " |
1594 " be a comma-separated list of values from: " | 1550 "actual,config," + LOG_DETAILED_PROGRESS + |
1595 "actual,config," + LOG_DETAILED_PROGRESS + | 1551 ",expected,timing," + LOG_UNEXPECTED + |
1596 ",expected,timing," + LOG_UNEXPECTED + " " | 1552 " (defaults to --log detailed-progress,unexpected)") |
1597 "(defaults to " + | 1553 option_parser.add_option("-v", "--verbose", action="store_true", |
1598 "--log detailed-progress,unexpected)") | 1554 default=False, help="include debug-level logging") |
1599 option_parser.add_option("-v", "--verbose", action="store_true", | 1555 option_parser.add_option("", "--sources", action="store_true", |
1600 default=False, help="include debug-level logging") | 1556 help="show expected result file path for each test " |
1601 option_parser.add_option("", "--sources", action="store_true", | 1557 "(implies --verbose)") |
1602 help="show expected result file path for each " | 1558 option_parser.add_option("", "--startup-dialog", action="store_true", |
1603 "test (implies --verbose)") | 1559 default=False, |
1604 option_parser.add_option("", "--startup-dialog", action="store_true", | 1560 help="create a dialog on test_shell.exe startup") |
1605 default=False, | 1561 option_parser.add_option("", "--gp-fault-error-box", action="store_true", |
1606 help="create a dialog on test_shell.exe startup") | 1562 default=False, |
1607 option_parser.add_option("", "--gp-fault-error-box", action="store_true", | 1563 help="enable Windows GP fault error box") |
1608 default=False, | 1564 option_parser.add_option("", "--wrapper", |
1609 help="enable Windows GP fault error box") | 1565 help="wrapper command to insert before invocations " |
1610 option_parser.add_option("", "--wrapper", | 1566 "of test_shell; option is split on whitespace " |
1611 help="wrapper command to insert before " | 1567 "before running. (example: " |
1612 "invocations of test_shell; option is split " | 1568 "--wrapper='valgrind --smc-check=all')") |
1613 "on whitespace before running. (Example: " | 1569 option_parser.add_option("", "--test-list", action="append", |
1614 "--wrapper='valgrind --smc-check=all')") | 1570 help="read list of tests to run from file", |
1615 option_parser.add_option("", "--test-list", action="append", | 1571 metavar="FILE") |
1616 help="read list of tests to run from file", | 1572 option_parser.add_option("", "--nocheck-sys-deps", action="store_true", |
1617 metavar="FILE") | 1573 default=False, |
1618 option_parser.add_option("", "--nocheck-sys-deps", action="store_true", | 1574 help="Don't check the system dependencies (themes)") |
1619 default=False, | 1575 option_parser.add_option("", "--randomize-order", action="store_true", |
1620 help="Don't check the system dependencies " | 1576 default=False, |
1621 "(themes)") | 1577 help=("Run tests in random order (useful for " |
1622 option_parser.add_option("", "--randomize-order", action="store_true", | 1578 "tracking down corruption)")) |
1623 default=False, | 1579 option_parser.add_option("", "--run-chunk", |
1624 help=("Run tests in random order (useful for " | 1580 default=None, |
1625 "tracking down corruption)")) | 1581 help=("Run a specified chunk (n:l), the nth of len " |
1626 option_parser.add_option("", "--run-chunk", | 1582 "l, of the layout tests")) |
1627 default=None, | 1583 option_parser.add_option("", "--run-part", |
1628 help=("Run a specified chunk (n:l), the " | 1584 default=None, |
1629 "nth of len l, of the layout tests")) | 1585 help=("Run a specified part (n:m), the nth of m" |
1630 option_parser.add_option("", "--run-part", | 1586 " parts, of the layout tests")) |
1631 default=None, | 1587 option_parser.add_option("", "--batch-size", |
1632 help=("Run a specified part (n:m), the nth of m" | 1588 default=None, |
1633 " parts, of the layout tests")) | 1589 help=("Run a the tests in batches (n), after every " |
1634 option_parser.add_option("", "--batch-size", | 1590 "n tests, the test shell is relaunched.")) |
1635 default=None, | 1591 option_parser.add_option("", "--builder-name", |
1636 help=("Run a the tests in batches (n), after " | 1592 default="DUMMY_BUILDER_NAME", |
1637 "every n tests, the test shell is " | 1593 help=("The name of the builder shown on the " |
1638 "relaunched.")) | 1594 "waterfall running this script e.g. WebKit.")) |
1639 option_parser.add_option("", "--builder-name", | 1595 option_parser.add_option("", "--build-name", |
1640 default="DUMMY_BUILDER_NAME", | 1596 default="DUMMY_BUILD_NAME", |
1641 help=("The name of the builder shown on the " | 1597 help=("The name of the builder used in its path, " |
1642 "waterfall running this script e.g. " | 1598 "e.g. webkit-rel.")) |
1643 "WebKit.")) | 1599 option_parser.add_option("", "--build-number", |
1644 option_parser.add_option("", "--build-name", | 1600 default="DUMMY_BUILD_NUMBER", |
1645 default="DUMMY_BUILD_NAME", | 1601 help=("The build number of the builder running" |
1646 help=("The name of the builder used in its path, " | 1602 "this script.")) |
1647 "e.g. webkit-rel.")) | 1603 option_parser.add_option("", "--experimental-fully-parallel", |
1648 option_parser.add_option("", "--build-number", | 1604 action="store_true", default=False, |
1649 default="DUMMY_BUILD_NUMBER", | 1605 help="run all tests in parallel") |
1650 help=("The build number of the builder running" | 1606 |
1651 "this script.")) | 1607 options, args = option_parser.parse_args() |
1652 option_parser.add_option("", "--experimental-fully-parallel", | 1608 main(options, args) |
1653 action="store_true", default=False, | |
1654 help="run all tests in parallel") | |
1655 | |
1656 options, args = option_parser.parse_args() | |
1657 main(options, args) | |
OLD | NEW |