• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
31import math
32import optparse
33
34from webkitpy.tool import grammar
35from webkitpy.layout_tests.models import test_expectations
36from webkitpy.layout_tests.models.test_expectations import TestExpectations, TestExpectationParser
37from webkitpy.layout_tests.views.metered_stream import MeteredStream
38
39
40NUM_SLOW_TESTS_TO_LOG = 10
41
42
43def print_options():
44    return [
45        optparse.make_option('-q', '--quiet', action='store_true', default=False,
46                             help='run quietly (errors, warnings, and progress only)'),
47        optparse.make_option('--timing', action='store_true', default=False,
48                             help='display test times (summary plus per-test w/ --verbose)'),
49        optparse.make_option('-v', '--verbose', action='store_true', default=False,
50                             help='print a summarized result for every test (one line per test)'),
51        optparse.make_option('--details', action='store_true', default=False,
52                             help='print detailed results for every test'),
53        optparse.make_option('--debug-rwt-logging', action='store_true', default=False,
54                             help='print timestamps and debug information for run-webkit-tests itself'),
55    ]
56
57
58class 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
82        # FIXME: should these options be in printing_options?
83        if self._options.new_baseline:
84            self._print_default("Placing new baselines in %s" % self._port.baseline_path())
85
86        fs = self._port.host.filesystem
87        fallback_path = [fs.split(x)[1] for x in self._port.baseline_search_path()]
88        self._print_default("Baseline search path: %s -> generic" % " -> ".join(fallback_path))
89
90        self._print_default("Using %s build" % self._options.configuration)
91        if self._options.pixel_tests:
92            self._print_default("Pixel tests enabled")
93        else:
94            self._print_default("Pixel tests disabled")
95
96        self._print_default("Regular timeout: %s, slow test timeout: %s" %
97                  (self._options.time_out_ms, self._options.slow_time_out_ms))
98
99        self._print_default('Command line: ' + ' '.join(self._port.driver_cmd_line()))
100        self._print_default('')
101
102    def print_found(self, num_all_test_files, num_to_run, repeat_each, iterations):
103        found_str = 'Found %s; running %d' % (grammar.pluralize('test', num_all_test_files), num_to_run)
104        if repeat_each * iterations > 1:
105            found_str += ' (%d times each: --repeat-each=%d --iterations=%d)' % (repeat_each * iterations, repeat_each, iterations)
106        found_str += ', skipping %d' % (num_all_test_files - num_to_run)
107        self._print_default(found_str + '.')
108
109    def print_expected(self, run_results, tests_with_result_type_callback):
110        self._print_expected_results_of_type(run_results, test_expectations.PASS, "passes", tests_with_result_type_callback)
111        self._print_expected_results_of_type(run_results, test_expectations.FAIL, "failures", tests_with_result_type_callback)
112        self._print_expected_results_of_type(run_results, test_expectations.FLAKY, "flaky", tests_with_result_type_callback)
113        self._print_debug('')
114
115    def print_workers_and_shards(self, num_workers, num_shards, num_locked_shards):
116        driver_name = self._port.driver_name()
117        if num_workers == 1:
118            self._print_default("Running 1 %s." % driver_name)
119            self._print_debug("(%s)." % grammar.pluralize('shard', num_shards))
120        else:
121            self._print_default("Running %d %ss in parallel." % (num_workers, driver_name))
122            self._print_debug("(%d shards; %d locked)." % (num_shards, num_locked_shards))
123        self._print_default('')
124
125    def _print_expected_results_of_type(self, run_results, result_type, result_type_str, tests_with_result_type_callback):
126        tests = tests_with_result_type_callback(result_type)
127        now = run_results.tests_by_timeline[test_expectations.NOW]
128        wontfix = run_results.tests_by_timeline[test_expectations.WONTFIX]
129
130        # We use a fancy format string in order to print the data out in a
131        # nicely-aligned table.
132        fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd wontfix)"
133                  % (self._num_digits(now), self._num_digits(wontfix)))
134        self._print_debug(fmtstr % (len(tests), result_type_str, len(tests & now), len(tests & wontfix)))
135
136    def _num_digits(self, num):
137        ndigits = 1
138        if len(num):
139            ndigits = int(math.log10(len(num))) + 1
140        return ndigits
141
142    def print_results(self, run_time, run_results, summarized_results):
143        self._print_timing_statistics(run_time, run_results)
144        self._print_one_line_summary(run_time, run_results)
145
146    def _print_timing_statistics(self, total_time, run_results):
147        self._print_debug("Test timing:")
148        self._print_debug("  %6.2f total testing time" % total_time)
149        self._print_debug("")
150
151        self._print_worker_statistics(run_results, int(self._options.child_processes))
152        self._print_aggregate_test_statistics(run_results)
153        self._print_individual_test_times(run_results)
154        self._print_directory_timings(run_results)
155
156    def _print_worker_statistics(self, run_results, num_workers):
157        self._print_debug("Thread timing:")
158        stats = {}
159        cuml_time = 0
160        for result in run_results.results_by_name.values():
161            stats.setdefault(result.worker_name, {'num_tests': 0, 'total_time': 0})
162            stats[result.worker_name]['num_tests'] += 1
163            stats[result.worker_name]['total_time'] += result.total_run_time
164            cuml_time += result.total_run_time
165
166        for worker_name in stats:
167            self._print_debug("    %10s: %5d tests, %6.2f secs" % (worker_name, stats[worker_name]['num_tests'], stats[worker_name]['total_time']))
168        self._print_debug("   %6.2f cumulative, %6.2f optimal" % (cuml_time, cuml_time / num_workers))
169        self._print_debug("")
170
171    def _print_aggregate_test_statistics(self, run_results):
172        times_for_dump_render_tree = [result.test_run_time for result in run_results.results_by_name.values()]
173        self._print_statistics_for_test_timings("PER TEST TIME IN TESTSHELL (seconds):", times_for_dump_render_tree)
174
175    def _print_individual_test_times(self, run_results):
176        # Reverse-sort by the time spent in the driver.
177
178        individual_test_timings = sorted(run_results.results_by_name.values(), key=lambda result: result.test_run_time, reverse=True)
179        num_printed = 0
180        slow_tests = []
181        timeout_or_crash_tests = []
182        unexpected_slow_tests = []
183        for test_tuple in individual_test_timings:
184            test_name = test_tuple.test_name
185            is_timeout_crash_or_slow = False
186            if test_name in run_results.slow_tests:
187                is_timeout_crash_or_slow = True
188                slow_tests.append(test_tuple)
189
190            if test_name in run_results.failures_by_name:
191                result = run_results.results_by_name[test_name].type
192                if (result == test_expectations.TIMEOUT or
193                    result == test_expectations.CRASH):
194                    is_timeout_crash_or_slow = True
195                    timeout_or_crash_tests.append(test_tuple)
196
197            if (not is_timeout_crash_or_slow and num_printed < NUM_SLOW_TESTS_TO_LOG):
198                num_printed = num_printed + 1
199                unexpected_slow_tests.append(test_tuple)
200
201        self._print_debug("")
202        if unexpected_slow_tests:
203            self._print_test_list_timing("%s slowest tests that are not marked as SLOW and did not timeout/crash:" %
204                NUM_SLOW_TESTS_TO_LOG, unexpected_slow_tests)
205            self._print_debug("")
206
207        if slow_tests:
208            self._print_test_list_timing("Tests marked as SLOW:", slow_tests)
209            self._print_debug("")
210
211        if timeout_or_crash_tests:
212            self._print_test_list_timing("Tests that timed out or crashed:", timeout_or_crash_tests)
213            self._print_debug("")
214
215    def _print_test_list_timing(self, title, test_list):
216        self._print_debug(title)
217        for test_tuple in test_list:
218            test_run_time = round(test_tuple.test_run_time, 1)
219            self._print_debug("  %s took %s seconds" % (test_tuple.test_name, test_run_time))
220
221    def _print_directory_timings(self, run_results):
222        stats = {}
223        for result in run_results.results_by_name.values():
224            stats.setdefault(result.shard_name, {'num_tests': 0, 'total_time': 0})
225            stats[result.shard_name]['num_tests'] += 1
226            stats[result.shard_name]['total_time'] += result.total_run_time
227
228        min_seconds_to_print = 15
229
230        timings = []
231        for directory in stats:
232            rounded_time = round(stats[directory]['total_time'], 1)
233            if rounded_time > min_seconds_to_print:
234                timings.append((directory, rounded_time, stats[directory]['num_tests']))
235
236        if not timings:
237            return
238
239        timings.sort()
240
241        self._print_debug("Time to process slowest subdirectories:")
242        for timing in timings:
243            self._print_debug("  %s took %s seconds to run %s tests." % timing)
244        self._print_debug("")
245
246    def _print_statistics_for_test_timings(self, title, timings):
247        self._print_debug(title)
248        timings.sort()
249
250        num_tests = len(timings)
251        if not num_tests:
252            return
253        percentile90 = timings[int(.9 * num_tests)]
254        percentile99 = timings[int(.99 * num_tests)]
255
256        if num_tests % 2 == 1:
257            median = timings[((num_tests - 1) / 2) - 1]
258        else:
259            lower = timings[num_tests / 2 - 1]
260            upper = timings[num_tests / 2]
261            median = (float(lower + upper)) / 2
262
263        mean = sum(timings) / num_tests
264
265        for timing in timings:
266            sum_of_deviations = math.pow(timing - mean, 2)
267
268        std_deviation = math.sqrt(sum_of_deviations / num_tests)
269        self._print_debug("  Median:          %6.3f" % median)
270        self._print_debug("  Mean:            %6.3f" % mean)
271        self._print_debug("  90th percentile: %6.3f" % percentile90)
272        self._print_debug("  99th percentile: %6.3f" % percentile99)
273        self._print_debug("  Standard dev:    %6.3f" % std_deviation)
274        self._print_debug("")
275
276    def _print_one_line_summary(self, total_time, run_results):
277        if self._options.timing:
278            parallel_time = sum(result.total_run_time for result in run_results.results_by_name.values())
279
280            # There is serial overhead in layout_test_runner.run() that we can't easily account for when
281            # really running in parallel, but taking the min() ensures that in the worst case
282            # (if parallel time is less than run_time) we do account for it.
283            serial_time = total_time - min(run_results.run_time, parallel_time)
284
285            speedup = (parallel_time + serial_time) / total_time
286            timing_summary = ' in %.2fs (%.2fs in rwt, %.2gx)' % (total_time, serial_time, speedup)
287        else:
288            timing_summary = ''
289
290        total = run_results.total - run_results.expected_skips
291        expected = run_results.expected - run_results.expected_skips
292        unexpected = run_results.unexpected
293        incomplete = total - expected - unexpected
294        incomplete_str = ''
295        if incomplete:
296            self._print_default("")
297            incomplete_str = " (%d didn't run)" % incomplete
298
299        if self._options.verbose or self._options.debug_rwt_logging or unexpected:
300            self.writeln("")
301
302        expected_summary_str = ''
303        if run_results.expected_failures > 0:
304            expected_summary_str = " (%d passed, %d didn't)" % (expected - run_results.expected_failures, run_results.expected_failures)
305
306        summary = ''
307        if unexpected == 0:
308            if expected == total:
309                if expected > 1:
310                    summary = "All %d tests ran as expected%s%s." % (expected, expected_summary_str, timing_summary)
311                else:
312                    summary = "The test ran as expected%s%s." % (expected_summary_str, timing_summary)
313            else:
314                summary = "%s ran as expected%s%s%s." % (grammar.pluralize('test', expected), expected_summary_str, incomplete_str, timing_summary)
315        else:
316            summary = "%s ran as expected%s, %d didn't%s%s:" % (grammar.pluralize('test', expected), expected_summary_str, unexpected, incomplete_str, timing_summary)
317
318        self._print_quiet(summary)
319        self._print_quiet("")
320
321    def _test_status_line(self, test_name, suffix):
322        format_string = '[%d/%d] %s%s'
323        status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix)
324        if len(status_line) > self._meter.number_of_columns():
325            overflow_columns = len(status_line) - self._meter.number_of_columns()
326            ellipsis = '...'
327            if len(test_name) < overflow_columns + len(ellipsis) + 2:
328                # We don't have enough space even if we elide, just show the test filename.
329                fs = self._port.host.filesystem
330                test_name = fs.split(test_name)[1]
331            else:
332                new_length = len(test_name) - overflow_columns - len(ellipsis)
333                prefix = int(new_length / 2)
334                test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):]
335        return format_string % (self.num_completed, self.num_tests, test_name, suffix)
336
337    def print_started_test(self, test_name):
338        self._running_tests.append(test_name)
339        if len(self._running_tests) > 1:
340            suffix = ' (+%d)' % (len(self._running_tests) - 1)
341        else:
342            suffix = ''
343        if self._options.verbose:
344            write = self._meter.write_update
345        else:
346            write = self._meter.write_throttled_update
347        write(self._test_status_line(test_name, suffix))
348
349    def print_finished_test(self, result, expected, exp_str, got_str):
350        self.num_completed += 1
351        test_name = result.test_name
352
353        result_message = self._result_message(result.type, result.failures, expected,
354                                              self._options.timing, result.test_run_time)
355
356        if self._options.details:
357            self._print_test_trace(result, exp_str, got_str)
358        elif self._options.verbose or not expected:
359            self.writeln(self._test_status_line(test_name, result_message))
360        elif self.num_completed == self.num_tests:
361            self._meter.write_update('')
362        else:
363            if test_name == self._running_tests[0]:
364                self._completed_tests.insert(0, [test_name, result_message])
365            else:
366                self._completed_tests.append([test_name, result_message])
367
368            for test_name, result_message in self._completed_tests:
369                self._meter.write_throttled_update(self._test_status_line(test_name, result_message))
370            self._completed_tests = []
371        self._running_tests.remove(test_name)
372
373    def _result_message(self, result_type, failures, expected, timing, test_run_time):
374        exp_string = ' unexpectedly' if not expected else ''
375        timing_string = ' %.4fs' % test_run_time if timing else ''
376        if result_type == test_expectations.PASS:
377            return ' passed%s%s' % (exp_string, timing_string)
378        else:
379            return ' failed%s (%s)%s' % (exp_string, ', '.join(failure.message() for failure in failures), timing_string)
380
381    def _print_test_trace(self, result, exp_str, got_str):
382        test_name = result.test_name
383        self._print_default(self._test_status_line(test_name, ''))
384
385        base = self._port.lookup_virtual_test_base(test_name)
386        if base:
387            args = ' '.join(self._port.lookup_virtual_test_args(test_name))
388            self._print_default(' base: %s' % base)
389            self._print_default(' args: %s' % args)
390
391        references = self._port.reference_files(test_name)
392        if references:
393            for _, filename in references:
394                self._print_default('  ref: %s' % self._port.relative_test_filename(filename))
395        else:
396            for extension in ('.txt', '.png', '.wav'):
397                    self._print_baseline(test_name, extension)
398
399        self._print_default('  exp: %s' % exp_str)
400        self._print_default('  got: %s' % got_str)
401        self._print_default(' took: %-.3f' % result.test_run_time)
402        self._print_default('')
403
404    def _print_baseline(self, test_name, extension):
405        baseline = self._port.expected_filename(test_name, extension)
406        if self._port._filesystem.exists(baseline):
407            relpath = self._port.relative_test_filename(baseline)
408        else:
409            relpath = '<none>'
410        self._print_default('  %s: %s' % (extension[1:], relpath))
411
412    def _print_quiet(self, msg):
413        self.writeln(msg)
414
415    def _print_default(self, msg):
416        if not self._options.quiet:
417            self.writeln(msg)
418
419    def _print_debug(self, msg):
420        if self._options.debug_rwt_logging:
421            self.writeln(msg)
422
423    def write_throttled_update(self, msg):
424        self._meter.write_throttled_update(msg)
425
426    def write_update(self, msg):
427        self._meter.write_update(msg)
428
429    def writeln(self, msg):
430        self._meter.writeln(msg)
431
432    def flush(self):
433        self._meter.flush()
434