1# Copyright (C) 2012 Google, Inc.
2# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions
6# are met:
7# 1.  Redistributions of source code must retain the above copyright
8#     notice, this list of conditions and the following disclaimer.
9# 2.  Redistributions in binary form must reproduce the above copyright
10#     notice, this list of conditions and the following disclaimer in the
11#     documentation and/or other materials provided with the distribution.
12#
13# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
14# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
17# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
24import logging
25import StringIO
26
27from webkitpy.common.system.systemhost import SystemHost
28from webkitpy.layout_tests.views.metered_stream import MeteredStream
29
30_log = logging.getLogger(__name__)
31
32
33class Printer(object):
34    def __init__(self, stream, options=None):
35        self.stream = stream
36        self.meter = None
37        self.options = options
38        self.num_tests = 0
39        self.num_completed = 0
40        self.num_errors = 0
41        self.num_failures = 0
42        self.running_tests = []
43        self.completed_tests = []
44        if options:
45            self.configure(options)
46
47    def configure(self, options):
48        self.options = options
49
50        if options.timing:
51            # --timing implies --verbose
52            options.verbose = max(options.verbose, 1)
53
54        log_level = logging.INFO
55        if options.quiet:
56            log_level = logging.WARNING
57        elif options.verbose == 2:
58            log_level = logging.DEBUG
59
60        self.meter = MeteredStream(self.stream, (options.verbose == 2),
61            number_of_columns=SystemHost().platform.terminal_width())
62
63        handler = logging.StreamHandler(self.stream)
64        # We constrain the level on the handler rather than on the root
65        # logger itself.  This is probably better because the handler is
66        # configured and known only to this module, whereas the root logger
67        # is an object shared (and potentially modified) by many modules.
68        # Modifying the handler, then, is less intrusive and less likely to
69        # interfere with modifications made by other modules (e.g. in unit
70        # tests).
71        handler.name = __name__
72        handler.setLevel(log_level)
73        formatter = logging.Formatter("%(message)s")
74        handler.setFormatter(formatter)
75
76        logger = logging.getLogger()
77        logger.addHandler(handler)
78        logger.setLevel(logging.NOTSET)
79
80        # Filter out most webkitpy messages.
81        #
82        # Messages can be selectively re-enabled for this script by updating
83        # this method accordingly.
84        def filter_records(record):
85            """Filter out non-third-party webkitpy messages."""
86            # FIXME: Figure out a way not to use strings here, for example by
87            #        using syntax like webkitpy.test.__name__.  We want to be
88            #        sure not to import any non-Python 2.4 code, though, until
89            #        after the version-checking code has executed.
90            if (record.name.startswith("webkitpy.test")):
91                return True
92            if record.name.startswith("webkitpy"):
93                return False
94            return True
95
96        testing_filter = logging.Filter()
97        testing_filter.filter = filter_records
98
99        # Display a message so developers are not mystified as to why
100        # logging does not work in the unit tests.
101        _log.info("Suppressing most webkitpy logging while running unit tests.")
102        handler.addFilter(testing_filter)
103
104        if self.options.pass_through:
105            # FIXME: Can't import at top of file, as outputcapture needs unittest2
106            from webkitpy.common.system import outputcapture
107            outputcapture.OutputCapture.stream_wrapper = _CaptureAndPassThroughStream
108
109    def write_update(self, msg):
110        self.meter.write_update(msg)
111
112    def print_started_test(self, source, test_name):
113        self.running_tests.append(test_name)
114        if len(self.running_tests) > 1:
115            suffix = ' (+%d)' % (len(self.running_tests) - 1)
116        else:
117            suffix = ''
118
119        if self.options.verbose:
120            write = self.meter.write_update
121        else:
122            write = self.meter.write_throttled_update
123
124        write(self._test_line(self.running_tests[0], suffix))
125
126    def print_finished_test(self, source, test_name, test_time, failures, errors):
127        write = self.meter.writeln
128        if failures:
129            lines = failures[0].splitlines() + ['']
130            suffix = ' failed:'
131            self.num_failures += 1
132        elif errors:
133            lines = errors[0].splitlines() + ['']
134            suffix = ' erred:'
135            self.num_errors += 1
136        else:
137            suffix = ' passed'
138            lines = []
139            if self.options.verbose:
140                write = self.meter.writeln
141            else:
142                write = self.meter.write_throttled_update
143        if self.options.timing:
144            suffix += ' %.4fs' % test_time
145
146        self.num_completed += 1
147
148        if test_name == self.running_tests[0]:
149            self.completed_tests.insert(0, [test_name, suffix, lines])
150        else:
151            self.completed_tests.append([test_name, suffix, lines])
152        self.running_tests.remove(test_name)
153
154        for test_name, msg, lines in self.completed_tests:
155            if lines:
156                self.meter.writeln(self._test_line(test_name, msg))
157                for line in lines:
158                    self.meter.writeln('  ' + line)
159            else:
160                write(self._test_line(test_name, msg))
161        self.completed_tests = []
162
163    def _test_line(self, test_name, suffix):
164        format_string = '[%d/%d] %s%s'
165        status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix)
166        if len(status_line) > self.meter.number_of_columns():
167            overflow_columns = len(status_line) - self.meter.number_of_columns()
168            ellipsis = '...'
169            if len(test_name) < overflow_columns + len(ellipsis) + 3:
170                # We don't have enough space even if we elide, just show the test method name.
171                test_name = test_name.split('.')[-1]
172            else:
173                new_length = len(test_name) - overflow_columns - len(ellipsis)
174                prefix = int(new_length / 2)
175                test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):]
176        return format_string % (self.num_completed, self.num_tests, test_name, suffix)
177
178    def print_result(self, run_time):
179        write = self.meter.writeln
180        write('Ran %d test%s in %.3fs' % (self.num_completed, self.num_completed != 1 and "s" or "", run_time))
181        if self.num_failures or self.num_errors:
182            write('FAILED (failures=%d, errors=%d)\n' % (self.num_failures, self.num_errors))
183        else:
184            write('\nOK\n')
185
186
187class _CaptureAndPassThroughStream(object):
188    def __init__(self, stream):
189        self._buffer = StringIO.StringIO()
190        self._stream = stream
191
192    def write(self, msg):
193        self._stream.write(msg)
194
195        # Note that we don't want to capture any output generated by the debugger
196        # because that could cause the results of capture_output() to be invalid.
197        if not self._message_is_from_pdb():
198            self._buffer.write(msg)
199
200    def _message_is_from_pdb(self):
201        # We will assume that if the pdb module is in the stack then the output
202        # is being generated by the python debugger (or the user calling something
203        # from inside the debugger).
204        import inspect
205        import pdb
206        stack = inspect.stack()
207        return any(frame[1] == pdb.__file__.replace('.pyc', '.py') for frame in stack)
208
209    def flush(self):
210        self._stream.flush()
211
212    def getvalue(self):
213        return self._buffer.getvalue()
214