Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(520)

Side by Side Diff: webkit/tools/layout_tests/webkitpy/run_chromium_webkit_tests.py

Issue 545145: Move the layout test scripts into a 'webkitpy' subdirectory in preparation... (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: try to de-confuse svn and the try bots Created 10 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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("&nbsp;&nbsp;%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("&nbsp;&nbsp;%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)
OLDNEW
« no previous file with comments | « webkit/tools/layout_tests/webkitpy/rebaseline.py ('k') | webkit/tools/layout_tests/webkitpy/test_output_formatter.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698