1#!/usr/bin/env python
2# Copyright (C) 2010 Google Inc. All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#     * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#     * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#     * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30"""Package that handles non-debug, non-file output for run-webkit-tests."""
31
32import logging
33import optparse
34
35from webkitpy.layout_tests.layout_package import metered_stream
36from webkitpy.layout_tests.layout_package import test_expectations
37
38_log = logging.getLogger("webkitpy.layout_tests.printer")
39
40TestExpectationsFile = test_expectations.TestExpectationsFile
41
42NUM_SLOW_TESTS_TO_LOG = 10
43
44PRINT_DEFAULT = ("misc,one-line-progress,one-line-summary,unexpected,"
45                 "unexpected-results,updates")
46PRINT_EVERYTHING = ("actual,config,expected,misc,one-line-progress,"
47                    "one-line-summary,slowest,timing,unexpected,"
48                    "unexpected-results,updates")
49
50HELP_PRINTING = """
51Output for run-webkit-tests is controlled by a comma-separated list of
52values passed to --print.  Values either influence the overall output, or
53the output at the beginning of the run, during the run, or at the end:
54
55Overall options:
56    nothing             don't print anything. This overrides every other option
57    default             include the default options. This is useful for logging
58                        the default options plus additional settings.
59    everything          print everything (except the trace-* options and the
60                        detailed-progress option, see below for the full list )
61    misc                print miscellaneous things like blank lines
62
63At the beginning of the run:
64    config              print the test run configuration
65    expected            print a summary of what is expected to happen
66                        (# passes, # failures, etc.)
67
68During the run:
69    detailed-progress   print one dot per test completed
70    one-line-progress   print a one-line progress bar
71    unexpected          print any unexpected results as they occur
72    updates             print updates on which stage is executing
73    trace-everything    print detailed info on every test's results
74                        (baselines, expectation, time it took to run). If
75                        this is specified it will override the '*-progress'
76                        options, the 'trace-unexpected' option, and the
77                        'unexpected' option.
78    trace-unexpected    like 'trace-everything', but only for tests with
79                        unexpected results. If this option is specified,
80                        it will override the 'unexpected' option.
81
82At the end of the run:
83    actual              print a summary of the actual results
84    slowest             print %(slowest)d slowest tests and the time they took
85    timing              print timing statistics
86    unexpected-results  print a list of the tests with unexpected results
87    one-line-summary    print a one-line summary of the run
88
89Notes:
90    - 'detailed-progress' can only be used if running in a single thread
91      (using --child-processes=1) or a single queue of tests (using
92       --experimental-fully-parallel). If these conditions aren't true,
93      'one-line-progress' will be used instead.
94    - If both 'detailed-progress' and 'one-line-progress' are specified (and
95      both are possible), 'detailed-progress' will be used.
96    - If 'nothing' is specified, it overrides all of the other options.
97    - Specifying --verbose is equivalent to --print everything plus it
98      changes the format of the log messages to add timestamps and other
99      information. If you specify --verbose and --print X, then X overrides
100      the --print everything implied by --verbose.
101
102--print 'everything' is equivalent to --print '%(everything)s'.
103
104The default (--print default) is equivalent to --print '%(default)s'.
105""" % {'slowest': NUM_SLOW_TESTS_TO_LOG, 'everything': PRINT_EVERYTHING,
106       'default': PRINT_DEFAULT}
107
108
109def print_options():
110    return [
111        # Note: We use print_options rather than just 'print' because print
112        # is a reserved word.
113        # Note: Also, we don't specify a default value so we can detect when
114        # no flag is specified on the command line and use different defaults
115        # based on whether or not --verbose is specified (since --print
116        # overrides --verbose).
117        optparse.make_option("--print", dest="print_options",
118            help=("controls print output of test run. "
119                  "Use --help-printing for more.")),
120        optparse.make_option("--help-printing", action="store_true",
121            help="show detailed help on controlling print output"),
122        optparse.make_option("-v", "--verbose", action="store_true",
123            default=False, help="include debug-level logging"),
124    ]
125
126
127def parse_print_options(print_options, verbose, child_processes,
128                        is_fully_parallel):
129    """Parse the options provided to --print and dedup and rank them.
130
131    Returns
132        a set() of switches that govern how logging is done
133
134    """
135    if print_options:
136        switches = set(print_options.split(','))
137    elif verbose:
138        switches = set(PRINT_EVERYTHING.split(','))
139    else:
140        switches = set(PRINT_DEFAULT.split(','))
141
142    if 'nothing' in switches:
143        return set()
144
145    if (child_processes != 1 and not is_fully_parallel and
146        'detailed-progress' in switches):
147        _log.warn("Can only print 'detailed-progress' if running "
148                  "with --child-processes=1 or "
149                  "with --experimental-fully-parallel. "
150                  "Using 'one-line-progress' instead.")
151        switches.discard('detailed-progress')
152        switches.add('one-line-progress')
153
154    if 'everything' in switches:
155        switches.discard('everything')
156        switches.update(set(PRINT_EVERYTHING.split(',')))
157
158    if 'default' in switches:
159        switches.discard('default')
160        switches.update(set(PRINT_DEFAULT.split(',')))
161
162    if 'detailed-progress' in switches:
163        switches.discard('one-line-progress')
164
165    if 'trace-everything' in switches:
166        switches.discard('detailed-progress')
167        switches.discard('one-line-progress')
168        switches.discard('trace-unexpected')
169        switches.discard('unexpected')
170
171    if 'trace-unexpected' in switches:
172        switches.discard('unexpected')
173
174    return switches
175
176
177def _configure_logging(stream, verbose):
178    log_fmt = '%(message)s'
179    log_datefmt = '%y%m%d %H:%M:%S'
180    log_level = logging.INFO
181    if verbose:
182        log_fmt = ('%(asctime)s %(process)d %(filename)s:%(lineno)d '
183                   '%(levelname)s %(message)s')
184        log_level = logging.DEBUG
185
186    root = logging.getLogger()
187    handler = logging.StreamHandler(stream)
188    handler.setFormatter(logging.Formatter(log_fmt, None))
189    root.addHandler(handler)
190    root.setLevel(log_level)
191    return handler
192
193
194def _restore_logging(handler_to_remove):
195    root = logging.getLogger()
196    root.handlers.remove(handler_to_remove)
197
198
199class Printer(object):
200    """Class handling all non-debug-logging printing done by run-webkit-tests.
201
202    Printing from run-webkit-tests falls into two buckets: general or
203    regular output that is read only by humans and can be changed at any
204    time, and output that is parsed by buildbots (and humans) and hence
205    must be changed more carefully and in coordination with the buildbot
206    parsing code (in chromium.org's buildbot/master.chromium/scripts/master/
207    log_parser/webkit_test_command.py script).
208
209    By default the buildbot-parsed code gets logged to stdout, and regular
210    output gets logged to stderr."""
211    def __init__(self, port, options, regular_output, buildbot_output,
212                 child_processes, is_fully_parallel):
213        """
214        Args
215          port               interface to port-specific routines
216          options            OptionParser object with command line settings
217          regular_output     stream to which output intended only for humans
218                             should be written
219          buildbot_output    stream to which output intended to be read by
220                             the buildbots (and humans) should be written
221          child_processes    number of parallel threads running (usually
222                             controlled by --child-processes)
223          is_fully_parallel  are the tests running in a single queue, or
224                             in shards (usually controlled by
225                             --experimental-fully-parallel)
226
227        Note that the last two args are separate rather than bundled into
228        the options structure so that this object does not assume any flags
229        set in options that weren't returned from logging_options(), above.
230        The two are used to determine whether or not we can sensibly use
231        the 'detailed-progress' option, or can only use 'one-line-progress'.
232        """
233        self._buildbot_stream = buildbot_output
234        self._options = options
235        self._port = port
236        self._stream = regular_output
237
238        # These are used for --print detailed-progress to track status by
239        # directory.
240        self._current_dir = None
241        self._current_progress_str = ""
242        self._current_test_number = 0
243
244        self._meter = metered_stream.MeteredStream(options.verbose,
245                                                   regular_output)
246        self._logging_handler = _configure_logging(self._meter,
247            options.verbose)
248
249        self.switches = parse_print_options(options.print_options,
250            options.verbose, child_processes, is_fully_parallel)
251
252    def cleanup(self):
253        """Restore logging configuration to its initial settings."""
254        if self._logging_handler:
255            _restore_logging(self._logging_handler)
256            self._logging_handler = None
257
258    def __del__(self):
259        self.cleanup()
260
261    # These two routines just hide the implementation of the switches.
262    def disabled(self, option):
263        return not option in self.switches
264
265    def enabled(self, option):
266        return option in self.switches
267
268    def help_printing(self):
269        self._write(HELP_PRINTING)
270
271    def print_actual(self, msg):
272        if self.disabled('actual'):
273            return
274        self._buildbot_stream.write("%s\n" % msg)
275
276    def print_config(self, msg):
277        self.write(msg, 'config')
278
279    def print_expected(self, msg):
280        self.write(msg, 'expected')
281
282    def print_timing(self, msg):
283        self.write(msg, 'timing')
284
285    def print_one_line_summary(self, total, expected, unexpected):
286        """Print a one-line summary of the test run to stdout.
287
288        Args:
289          total: total number of tests run
290          expected: number of expected results
291          unexpected: number of unexpected results
292        """
293        if self.disabled('one-line-summary'):
294            return
295
296        incomplete = total - expected - unexpected
297        if incomplete:
298            self._write("")
299            incomplete_str = " (%d didn't run)" % incomplete
300            expected_str = str(expected)
301        else:
302            incomplete_str = ""
303            expected_str = "All %d" % expected
304
305        if unexpected == 0:
306            self._write("%s tests ran as expected%s." %
307                        (expected_str, incomplete_str))
308        elif expected == 1:
309            self._write("1 test ran as expected, %d didn't%s:" %
310                        (unexpected, incomplete_str))
311        else:
312            self._write("%d tests ran as expected, %d didn't%s:" %
313                        (expected, unexpected, incomplete_str))
314        self._write("")
315
316    def print_test_result(self, result, expected, exp_str, got_str):
317        """Print the result of the test as determined by --print.
318
319        This routine is used to print the details of each test as it completes.
320
321        Args:
322            result   - The actual TestResult object
323            expected - Whether the result we got was an expected result
324            exp_str  - What we expected to get (used for tracing)
325            got_str  - What we actually got (used for tracing)
326
327        Note that we need all of these arguments even though they seem
328        somewhat redundant, in order to keep this routine from having to
329        known anything about the set of expectations.
330        """
331        if (self.enabled('trace-everything') or
332            self.enabled('trace-unexpected') and not expected):
333            self._print_test_trace(result, exp_str, got_str)
334        elif (not expected and self.enabled('unexpected') and
335              self.disabled('detailed-progress')):
336            # Note: 'detailed-progress' handles unexpected results internally,
337            # so we skip it here.
338            self._print_unexpected_test_result(result)
339
340    def _print_test_trace(self, result, exp_str, got_str):
341        """Print detailed results of a test (triggered by --print trace-*).
342        For each test, print:
343           - location of the expected baselines
344           - expected results
345           - actual result
346           - timing info
347        """
348        filename = result.filename
349        test_name = self._port.relative_test_filename(filename)
350        self._write('trace: %s' % test_name)
351        txt_file = self._port.expected_filename(filename, '.txt')
352        if self._port.path_exists(txt_file):
353            self._write('  txt: %s' %
354                        self._port.relative_test_filename(txt_file))
355        else:
356            self._write('  txt: <none>')
357        checksum_file = self._port.expected_filename(filename, '.checksum')
358        if self._port.path_exists(checksum_file):
359            self._write('  sum: %s' %
360                        self._port.relative_test_filename(checksum_file))
361        else:
362            self._write('  sum: <none>')
363        png_file = self._port.expected_filename(filename, '.png')
364        if self._port.path_exists(png_file):
365            self._write('  png: %s' %
366                        self._port.relative_test_filename(png_file))
367        else:
368            self._write('  png: <none>')
369        self._write('  exp: %s' % exp_str)
370        self._write('  got: %s' % got_str)
371        self._write(' took: %-.3f' % result.test_run_time)
372        self._write('')
373
374    def _print_unexpected_test_result(self, result):
375        """Prints one unexpected test result line."""
376        desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result.type][0]
377        self.write("  %s -> unexpected %s" %
378                   (self._port.relative_test_filename(result.filename),
379                    desc), "unexpected")
380
381    def print_progress(self, result_summary, retrying, test_list):
382        """Print progress through the tests as determined by --print."""
383        if self.enabled('detailed-progress'):
384            self._print_detailed_progress(result_summary, test_list)
385        elif self.enabled('one-line-progress'):
386            self._print_one_line_progress(result_summary, retrying)
387        else:
388            return
389
390        if result_summary.remaining == 0:
391            self._meter.update('')
392
393    def _print_one_line_progress(self, result_summary, retrying):
394        """Displays the progress through the test run."""
395        percent_complete = 100 * (result_summary.expected +
396            result_summary.unexpected) / result_summary.total
397        action = "Testing"
398        if retrying:
399            action = "Retrying"
400        self._meter.progress("%s (%d%%): %d ran as expected, %d didn't,"
401            " %d left" % (action, percent_complete, result_summary.expected,
402             result_summary.unexpected, result_summary.remaining))
403
404    def _print_detailed_progress(self, result_summary, test_list):
405        """Display detailed progress output where we print the directory name
406        and one dot for each completed test. This is triggered by
407        "--log detailed-progress"."""
408        if self._current_test_number == len(test_list):
409            return
410
411        next_test = test_list[self._current_test_number]
412        next_dir = self._port._filesystem.dirname(
413            self._port.relative_test_filename(next_test))
414        if self._current_progress_str == "":
415            self._current_progress_str = "%s: " % (next_dir)
416            self._current_dir = next_dir
417
418        while next_test in result_summary.results:
419            if next_dir != self._current_dir:
420                self._meter.write("%s\n" % (self._current_progress_str))
421                self._current_progress_str = "%s: ." % (next_dir)
422                self._current_dir = next_dir
423            else:
424                self._current_progress_str += "."
425
426            if (next_test in result_summary.unexpected_results and
427                self.enabled('unexpected')):
428                self._meter.write("%s\n" % self._current_progress_str)
429                test_result = result_summary.results[next_test]
430                self._print_unexpected_test_result(test_result)
431                self._current_progress_str = "%s: " % self._current_dir
432
433            self._current_test_number += 1
434            if self._current_test_number == len(test_list):
435                break
436
437            next_test = test_list[self._current_test_number]
438            next_dir = self._port._filesystem.dirname(
439                self._port.relative_test_filename(next_test))
440
441        if result_summary.remaining:
442            remain_str = " (%d)" % (result_summary.remaining)
443            self._meter.progress("%s%s" % (self._current_progress_str,
444                                           remain_str))
445        else:
446            self._meter.progress("%s" % (self._current_progress_str))
447
448    def print_unexpected_results(self, unexpected_results):
449        """Prints a list of the unexpected results to the buildbot stream."""
450        if self.disabled('unexpected-results'):
451            return
452
453        passes = {}
454        flaky = {}
455        regressions = {}
456
457        for test, results in unexpected_results['tests'].iteritems():
458            actual = results['actual'].split(" ")
459            expected = results['expected'].split(" ")
460            if actual == ['PASS']:
461                if 'CRASH' in expected:
462                    _add_to_dict_of_lists(passes,
463                                          'Expected to crash, but passed',
464                                          test)
465                elif 'TIMEOUT' in expected:
466                    _add_to_dict_of_lists(passes,
467                                          'Expected to timeout, but passed',
468                                           test)
469                else:
470                    _add_to_dict_of_lists(passes,
471                                          'Expected to fail, but passed',
472                                          test)
473            elif len(actual) > 1:
474                # We group flaky tests by the first actual result we got.
475                _add_to_dict_of_lists(flaky, actual[0], test)
476            else:
477                _add_to_dict_of_lists(regressions, results['actual'], test)
478
479        if len(passes) or len(flaky) or len(regressions):
480            self._buildbot_stream.write("\n")
481
482        if len(passes):
483            for key, tests in passes.iteritems():
484                self._buildbot_stream.write("%s: (%d)\n" % (key, len(tests)))
485                tests.sort()
486                for test in tests:
487                    self._buildbot_stream.write("  %s\n" % test)
488                self._buildbot_stream.write("\n")
489            self._buildbot_stream.write("\n")
490
491        if len(flaky):
492            descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS
493            for key, tests in flaky.iteritems():
494                result = TestExpectationsFile.EXPECTATIONS[key.lower()]
495                self._buildbot_stream.write("Unexpected flakiness: %s (%d)\n"
496                    % (descriptions[result][1], len(tests)))
497                tests.sort()
498
499                for test in tests:
500                    result = unexpected_results['tests'][test]
501                    actual = result['actual'].split(" ")
502                    expected = result['expected'].split(" ")
503                    result = TestExpectationsFile.EXPECTATIONS[key.lower()]
504                    new_expectations_list = list(set(actual) | set(expected))
505                    self._buildbot_stream.write("  %s = %s\n" %
506                        (test, " ".join(new_expectations_list)))
507                self._buildbot_stream.write("\n")
508            self._buildbot_stream.write("\n")
509
510        if len(regressions):
511            descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS
512            for key, tests in regressions.iteritems():
513                result = TestExpectationsFile.EXPECTATIONS[key.lower()]
514                self._buildbot_stream.write(
515                    "Regressions: Unexpected %s : (%d)\n" % (
516                    descriptions[result][1], len(tests)))
517                tests.sort()
518                for test in tests:
519                    self._buildbot_stream.write("  %s = %s\n" % (test, key))
520                self._buildbot_stream.write("\n")
521            self._buildbot_stream.write("\n")
522
523        if len(unexpected_results['tests']) and self._options.verbose:
524            self._buildbot_stream.write("%s\n" % ("-" * 78))
525
526    def print_update(self, msg):
527        if self.disabled('updates'):
528            return
529        self._meter.update(msg)
530
531    def write(self, msg, option="misc"):
532        if self.disabled(option):
533            return
534        self._write(msg)
535
536    def _write(self, msg):
537        # FIXME: we could probably get away with calling _log.info() all of
538        # the time, but there doesn't seem to be a good way to test the output
539        # from the logger :(.
540        if self._options.verbose:
541            _log.info(msg)
542        else:
543            self._meter.write("%s\n" % msg)
544
545#
546# Utility routines used by the Controller class
547#
548
549
550def _add_to_dict_of_lists(dict, key, value):
551    dict.setdefault(key, []).append(value)
552