1 # Copyright (C) 2010, 2012 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the name of Google Inc. nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 """Package that handles non-debug, non-file output for run-webkit-tests.""" 30 31 import math 32 import optparse 33 34 from webkitpy.tool import grammar 35 from webkitpy.layout_tests.models import test_expectations 36 from webkitpy.layout_tests.models.test_expectations import TestExpectations, TestExpectationParser 37 from webkitpy.layout_tests.views.metered_stream import MeteredStream 38 39 40 NUM_SLOW_TESTS_TO_LOG = 10 41 42 43 def print_options(): 44 return [ 45 optparse.make_option('--debug-rwt-logging', action='store_true', default=False, 46 help='print timestamps and debug information for run-webkit-tests itself'), 47 optparse.make_option('--details', action='store_true', default=False, 48 help='print detailed results for every test'), 49 optparse.make_option('-q', '--quiet', action='store_true', default=False, 50 help='run quietly (errors, warnings, and progress only)'), 51 optparse.make_option('--timing', action='store_true', default=False, 52 help='display test times (summary plus per-test w/ --verbose)'), 53 optparse.make_option('-v', '--verbose', action='store_true', default=False, 54 help='print a summarized result for every test (one line per test)'), 55 ] 56 57 58 class Printer(object): 59 """Class handling all non-debug-logging printing done by run-webkit-tests.""" 60 61 def __init__(self, port, options, regular_output, logger=None): 62 self.num_completed = 0 63 self.num_tests = 0 64 self._port = port 65 self._options = options 66 self._meter = MeteredStream(regular_output, options.debug_rwt_logging, logger=logger, 67 number_of_columns=self._port.host.platform.terminal_width()) 68 self._running_tests = [] 69 self._completed_tests = [] 70 71 def cleanup(self): 72 self._meter.cleanup() 73 74 def __del__(self): 75 self.cleanup() 76 77 def print_config(self, results_directory): 78 self._print_default("Using port '%s'" % self._port.name()) 79 self._print_default("Test configuration: %s" % self._port.test_configuration()) 80 self._print_default("View the test results at file://%s/results.html" % results_directory) 81 self._print_default("View the archived results dashboard at file://%s/dashboard.html" % results_directory) 82 83 # FIXME: should these options be in printing_options? 84 if self._options.new_baseline: 85 self._print_default("Placing new baselines in %s" % self._port.baseline_path()) 86 87 fs = self._port.host.filesystem 88 fallback_path = [fs.split(x)[1] for x in self._port.baseline_search_path()] 89 self._print_default("Baseline search path: %s -> generic" % " -> ".join(fallback_path)) 90 91 self._print_default("Using %s build" % self._options.configuration) 92 if self._options.pixel_tests: 93 self._print_default("Pixel tests enabled") 94 else: 95 self._print_default("Pixel tests disabled") 96 97 self._print_default("Regular timeout: %s, slow test timeout: %s" % 98 (self._options.time_out_ms, self._options.slow_time_out_ms)) 99 100 self._print_default('Command line: ' + ' '.join(self._port.driver_cmd_line())) 101 self._print_default('') 102 103 def print_found(self, num_all_test_files, num_to_run, repeat_each, iterations): 104 found_str = 'Found %s; running %d' % (grammar.pluralize('test', num_all_test_files), num_to_run) 105 if repeat_each * iterations > 1: 106 found_str += ' (%d times each: --repeat-each=%d --iterations=%d)' % (repeat_each * iterations, repeat_each, iterations) 107 found_str += ', skipping %d' % (num_all_test_files - num_to_run) 108 self._print_default(found_str + '.') 109 110 def print_expected(self, run_results, tests_with_result_type_callback): 111 self._print_expected_results_of_type(run_results, test_expectations.PASS, "passes", tests_with_result_type_callback) 112 self._print_expected_results_of_type(run_results, test_expectations.FAIL, "failures", tests_with_result_type_callback) 113 self._print_expected_results_of_type(run_results, test_expectations.FLAKY, "flaky", tests_with_result_type_callback) 114 self._print_debug('') 115 116 def print_workers_and_shards(self, num_workers, num_shards, num_locked_shards): 117 driver_name = self._port.driver_name() 118 if num_workers == 1: 119 self._print_default("Running 1 %s." % driver_name) 120 self._print_debug("(%s)." % grammar.pluralize('shard', num_shards)) 121 else: 122 self._print_default("Running %d %ss in parallel." % (num_workers, driver_name)) 123 self._print_debug("(%d shards; %d locked)." % (num_shards, num_locked_shards)) 124 self._print_default('') 125 126 def _print_expected_results_of_type(self, run_results, result_type, result_type_str, tests_with_result_type_callback): 127 tests = tests_with_result_type_callback(result_type) 128 now = run_results.tests_by_timeline[test_expectations.NOW] 129 wontfix = run_results.tests_by_timeline[test_expectations.WONTFIX] 130 131 # We use a fancy format string in order to print the data out in a 132 # nicely-aligned table. 133 fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd wontfix)" 134 % (self._num_digits(now), self._num_digits(wontfix))) 135 self._print_debug(fmtstr % (len(tests), result_type_str, len(tests & now), len(tests & wontfix))) 136 137 def _num_digits(self, num): 138 ndigits = 1 139 if len(num): 140 ndigits = int(math.log10(len(num))) + 1 141 return ndigits 142 143 def print_results(self, run_time, run_results, summarized_results): 144 self._print_timing_statistics(run_time, run_results) 145 self._print_one_line_summary(run_time, run_results) 146 147 def _print_timing_statistics(self, total_time, run_results): 148 self._print_debug("Test timing:") 149 self._print_debug(" %6.2f total testing time" % total_time) 150 self._print_debug("") 151 152 self._print_worker_statistics(run_results, int(self._options.child_processes)) 153 self._print_aggregate_test_statistics(run_results) 154 self._print_individual_test_times(run_results) 155 self._print_directory_timings(run_results) 156 157 def _print_worker_statistics(self, run_results, num_workers): 158 self._print_debug("Thread timing:") 159 stats = {} 160 cuml_time = 0 161 for result in run_results.results_by_name.values(): 162 stats.setdefault(result.worker_name, {'num_tests': 0, 'total_time': 0}) 163 stats[result.worker_name]['num_tests'] += 1 164 stats[result.worker_name]['total_time'] += result.total_run_time 165 cuml_time += result.total_run_time 166 167 for worker_name in stats: 168 self._print_debug(" %10s: %5d tests, %6.2f secs" % (worker_name, stats[worker_name]['num_tests'], stats[worker_name]['total_time'])) 169 self._print_debug(" %6.2f cumulative, %6.2f optimal" % (cuml_time, cuml_time / num_workers)) 170 self._print_debug("") 171 172 def _print_aggregate_test_statistics(self, run_results): 173 times_for_dump_render_tree = [result.test_run_time for result in run_results.results_by_name.values()] 174 self._print_statistics_for_test_timings("PER TEST TIME IN TESTSHELL (seconds):", times_for_dump_render_tree) 175 176 def _print_individual_test_times(self, run_results): 177 # Reverse-sort by the time spent in the driver. 178 179 individual_test_timings = sorted(run_results.results_by_name.values(), key=lambda result: result.test_run_time, reverse=True) 180 num_printed = 0 181 slow_tests = [] 182 timeout_or_crash_tests = [] 183 unexpected_slow_tests = [] 184 for test_tuple in individual_test_timings: 185 test_name = test_tuple.test_name 186 is_timeout_crash_or_slow = False 187 if test_name in run_results.slow_tests: 188 is_timeout_crash_or_slow = True 189 slow_tests.append(test_tuple) 190 191 if test_name in run_results.failures_by_name: 192 result = run_results.results_by_name[test_name].type 193 if (result == test_expectations.TIMEOUT or 194 result == test_expectations.CRASH): 195 is_timeout_crash_or_slow = True 196 timeout_or_crash_tests.append(test_tuple) 197 198 if (not is_timeout_crash_or_slow and num_printed < NUM_SLOW_TESTS_TO_LOG): 199 num_printed = num_printed + 1 200 unexpected_slow_tests.append(test_tuple) 201 202 self._print_debug("") 203 if unexpected_slow_tests: 204 self._print_test_list_timing("%s slowest tests that are not marked as SLOW and did not timeout/crash:" % 205 NUM_SLOW_TESTS_TO_LOG, unexpected_slow_tests) 206 self._print_debug("") 207 208 if slow_tests: 209 self._print_test_list_timing("Tests marked as SLOW:", slow_tests) 210 self._print_debug("") 211 212 if timeout_or_crash_tests: 213 self._print_test_list_timing("Tests that timed out or crashed:", timeout_or_crash_tests) 214 self._print_debug("") 215 216 def _print_test_list_timing(self, title, test_list): 217 self._print_debug(title) 218 for test_tuple in test_list: 219 test_run_time = round(test_tuple.test_run_time, 1) 220 self._print_debug(" %s took %s seconds" % (test_tuple.test_name, test_run_time)) 221 222 def _print_directory_timings(self, run_results): 223 stats = {} 224 for result in run_results.results_by_name.values(): 225 stats.setdefault(result.shard_name, {'num_tests': 0, 'total_time': 0}) 226 stats[result.shard_name]['num_tests'] += 1 227 stats[result.shard_name]['total_time'] += result.total_run_time 228 229 min_seconds_to_print = 15 230 231 timings = [] 232 for directory in stats: 233 rounded_time = round(stats[directory]['total_time'], 1) 234 if rounded_time > min_seconds_to_print: 235 timings.append((directory, rounded_time, stats[directory]['num_tests'])) 236 237 if not timings: 238 return 239 240 timings.sort() 241 242 self._print_debug("Time to process slowest subdirectories:") 243 for timing in timings: 244 self._print_debug(" %s took %s seconds to run %s tests." % timing) 245 self._print_debug("") 246 247 def _print_statistics_for_test_timings(self, title, timings): 248 self._print_debug(title) 249 timings.sort() 250 251 num_tests = len(timings) 252 if not num_tests: 253 return 254 percentile90 = timings[int(.9 * num_tests)] 255 percentile99 = timings[int(.99 * num_tests)] 256 257 if num_tests % 2 == 1: 258 median = timings[((num_tests - 1) / 2) - 1] 259 else: 260 lower = timings[num_tests / 2 - 1] 261 upper = timings[num_tests / 2] 262 median = (float(lower + upper)) / 2 263 264 mean = sum(timings) / num_tests 265 266 for timing in timings: 267 sum_of_deviations = math.pow(timing - mean, 2) 268 269 std_deviation = math.sqrt(sum_of_deviations / num_tests) 270 self._print_debug(" Median: %6.3f" % median) 271 self._print_debug(" Mean: %6.3f" % mean) 272 self._print_debug(" 90th percentile: %6.3f" % percentile90) 273 self._print_debug(" 99th percentile: %6.3f" % percentile99) 274 self._print_debug(" Standard dev: %6.3f" % std_deviation) 275 self._print_debug("") 276 277 def _print_one_line_summary(self, total_time, run_results): 278 if self._options.timing: 279 parallel_time = sum(result.total_run_time for result in run_results.results_by_name.values()) 280 281 # There is serial overhead in layout_test_runner.run() that we can't easily account for when 282 # really running in parallel, but taking the min() ensures that in the worst case 283 # (if parallel time is less than run_time) we do account for it. 284 serial_time = total_time - min(run_results.run_time, parallel_time) 285 286 speedup = (parallel_time + serial_time) / total_time 287 timing_summary = ' in %.2fs (%.2fs in rwt, %.2gx)' % (total_time, serial_time, speedup) 288 else: 289 timing_summary = '' 290 291 total = run_results.total - run_results.expected_skips 292 expected = run_results.expected - run_results.expected_skips 293 unexpected = run_results.unexpected 294 incomplete = total - expected - unexpected 295 incomplete_str = '' 296 if incomplete: 297 self._print_default("") 298 incomplete_str = " (%d didn't run)" % incomplete 299 300 if self._options.verbose or self._options.debug_rwt_logging or unexpected: 301 self.writeln("") 302 303 expected_summary_str = '' 304 if run_results.expected_failures > 0: 305 expected_summary_str = " (%d passed, %d didn't)" % (expected - run_results.expected_failures, run_results.expected_failures) 306 307 summary = '' 308 if unexpected == 0: 309 if expected == total: 310 if expected > 1: 311 summary = "All %d tests ran as expected%s%s." % (expected, expected_summary_str, timing_summary) 312 else: 313 summary = "The test ran as expected%s%s." % (expected_summary_str, timing_summary) 314 else: 315 summary = "%s ran as expected%s%s%s." % (grammar.pluralize('test', expected), expected_summary_str, incomplete_str, timing_summary) 316 else: 317 summary = "%s ran as expected%s, %d didn't%s%s:" % (grammar.pluralize('test', expected), expected_summary_str, unexpected, incomplete_str, timing_summary) 318 319 self._print_quiet(summary) 320 self._print_quiet("") 321 322 def _test_status_line(self, test_name, suffix): 323 format_string = '[%d/%d] %s%s' 324 status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix) 325 if len(status_line) > self._meter.number_of_columns(): 326 overflow_columns = len(status_line) - self._meter.number_of_columns() 327 ellipsis = '...' 328 if len(test_name) < overflow_columns + len(ellipsis) + 2: 329 # We don't have enough space even if we elide, just show the test filename. 330 fs = self._port.host.filesystem 331 test_name = fs.split(test_name)[1] 332 else: 333 new_length = len(test_name) - overflow_columns - len(ellipsis) 334 prefix = int(new_length / 2) 335 test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):] 336 return format_string % (self.num_completed, self.num_tests, test_name, suffix) 337 338 def print_started_test(self, test_name): 339 self._running_tests.append(test_name) 340 if len(self._running_tests) > 1: 341 suffix = ' (+%d)' % (len(self._running_tests) - 1) 342 else: 343 suffix = '' 344 if self._options.verbose: 345 write = self._meter.write_update 346 else: 347 write = self._meter.write_throttled_update 348 write(self._test_status_line(test_name, suffix)) 349 350 def print_finished_test(self, result, expected, exp_str, got_str): 351 self.num_completed += 1 352 test_name = result.test_name 353 354 result_message = self._result_message(result.type, result.failures, expected, 355 self._options.timing, result.test_run_time) 356 357 if self._options.details: 358 self._print_test_trace(result, exp_str, got_str) 359 elif self._options.verbose or not expected: 360 self.writeln(self._test_status_line(test_name, result_message)) 361 elif self.num_completed == self.num_tests: 362 self._meter.write_update('') 363 else: 364 if test_name == self._running_tests[0]: 365 self._completed_tests.insert(0, [test_name, result_message]) 366 else: 367 self._completed_tests.append([test_name, result_message]) 368 369 for test_name, result_message in self._completed_tests: 370 self._meter.write_throttled_update(self._test_status_line(test_name, result_message)) 371 self._completed_tests = [] 372 self._running_tests.remove(test_name) 373 374 def _result_message(self, result_type, failures, expected, timing, test_run_time): 375 exp_string = ' unexpectedly' if not expected else '' 376 timing_string = ' %.4fs' % test_run_time if timing else '' 377 if result_type == test_expectations.PASS: 378 return ' passed%s%s' % (exp_string, timing_string) 379 else: 380 return ' failed%s (%s)%s' % (exp_string, ', '.join(failure.message() for failure in failures), timing_string) 381 382 def _print_test_trace(self, result, exp_str, got_str): 383 test_name = result.test_name 384 self._print_default(self._test_status_line(test_name, '')) 385 386 base = self._port.lookup_virtual_test_base(test_name) 387 if base: 388 args = ' '.join(self._port.lookup_virtual_test_args(test_name)) 389 self._print_default(' base: %s' % base) 390 self._print_default(' args: %s' % args) 391 392 references = self._port.reference_files(test_name) 393 if references: 394 for _, filename in references: 395 self._print_default(' ref: %s' % self._port.relative_test_filename(filename)) 396 else: 397 for extension in ('.txt', '.png', '.wav'): 398 self._print_baseline(test_name, extension) 399 400 self._print_default(' exp: %s' % exp_str) 401 self._print_default(' got: %s' % got_str) 402 self._print_default(' took: %-.3f' % result.test_run_time) 403 self._print_default('') 404 405 def _print_baseline(self, test_name, extension): 406 baseline = self._port.expected_filename(test_name, extension) 407 if self._port._filesystem.exists(baseline): 408 relpath = self._port.relative_test_filename(baseline) 409 else: 410 relpath = '<none>' 411 self._print_default(' %s: %s' % (extension[1:], relpath)) 412 413 def _print_quiet(self, msg): 414 self.writeln(msg) 415 416 def _print_default(self, msg): 417 if not self._options.quiet: 418 self.writeln(msg) 419 420 def _print_debug(self, msg): 421 if self._options.debug_rwt_logging: 422 self.writeln(msg) 423 424 def write_throttled_update(self, msg): 425 self._meter.write_throttled_update(msg) 426 427 def write_update(self, msg): 428 self._meter.write_update(msg) 429 430 def writeln(self, msg): 431 self._meter.writeln(msg) 432 433 def flush(self): 434 self._meter.flush() 435