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 StringIO
25import logging
26
27from webkitpy.common.system import outputcapture
28from webkitpy.common.system.systemhost import SystemHost
29from webkitpy.layout_tests.views.metered_stream import MeteredStream
30
31_log = logging.getLogger(__name__)
32
33
34class Printer(object):
35    def __init__(self, stream, options=None):
36        self.stream = stream
37        self.meter = None
38        self.options = options
39        self.num_tests = 0
40        self.num_completed = 0
41        self.num_errors = 0
42        self.num_failures = 0
43        self.running_tests = []
44        self.completed_tests = []
45        if options:
46            self.configure(options)
47
48    def configure(self, options):
49        self.options = options
50
51        if options.timing:
52            # --timing implies --verbose
53            options.verbose = max(options.verbose, 1)
54
55        log_level = logging.INFO
56        if options.quiet:
57            log_level = logging.WARNING
58        elif options.verbose >= 2:
59            log_level = logging.DEBUG
60
61        self.meter = MeteredStream(self.stream, (options.verbose >= 2),
62            number_of_columns=SystemHost().platform.terminal_width())
63
64        handler = logging.StreamHandler(self.stream)
65        # We constrain the level on the handler rather than on the root
66        # logger itself.  This is probably better because the handler is
67        # configured and known only to this module, whereas the root logger
68        # is an object shared (and potentially modified) by many modules.
69        # Modifying the handler, then, is less intrusive and less likely to
70        # interfere with modifications made by other modules (e.g. in unit
71        # tests).
72        handler.name = __name__
73        handler.setLevel(log_level)
74        formatter = logging.Formatter("%(message)s")
75        handler.setFormatter(formatter)
76
77        logger = logging.getLogger()
78        logger.addHandler(handler)
79        logger.setLevel(logging.NOTSET)
80
81        # Filter out most webkitpy messages.
82        #
83        # Messages can be selectively re-enabled for this script by updating
84        # this method accordingly.
85        def filter_records(record):
86            """Filter out non-third-party webkitpy messages."""
87            # FIXME: Figure out a way not to use strings here, for example by
88            #        using syntax like webkitpy.test.__name__.  We want to be
89            #        sure not to import any non-Python 2.4 code, though, until
90            #        after the version-checking code has executed.
91            if (record.name.startswith("webkitpy.test")):
92                return True
93            if record.name.startswith("webkitpy"):
94                return False
95            return True
96
97        testing_filter = logging.Filter()
98        testing_filter.filter = filter_records
99
100        # Display a message so developers are not mystified as to why
101        # logging does not work in the unit tests.
102        _log.info("Suppressing most webkitpy logging while running unit tests.")
103        handler.addFilter(testing_filter)
104
105        if self.options.pass_through:
106            outputcapture.OutputCapture.stream_wrapper = _CaptureAndPassThroughStream
107
108    def write_update(self, msg):
109        self.meter.write_update(msg)
110
111    def print_started_test(self, source, test_name):
112        self.running_tests.append(test_name)
113        if len(self.running_tests) > 1:
114            suffix = ' (+%d)' % (len(self.running_tests) - 1)
115        else:
116            suffix = ''
117
118        if self.options.verbose:
119            write = self.meter.write_update
120        else:
121            write = self.meter.write_throttled_update
122
123        write(self._test_line(self.running_tests[0], suffix))
124
125    def print_finished_test(self, source, test_name, test_time, failures, errors):
126        write = self.meter.writeln
127        if failures:
128            lines = failures[0].splitlines() + ['']
129            suffix = ' failed:'
130            self.num_failures += 1
131        elif errors:
132            lines = errors[0].splitlines() + ['']
133            suffix = ' erred:'
134            self.num_errors += 1
135        else:
136            suffix = ' passed'
137            lines = []
138            if self.options.verbose:
139                write = self.meter.writeln
140            else:
141                write = self.meter.write_throttled_update
142        if self.options.timing:
143            suffix += ' %.4fs' % test_time
144
145        self.num_completed += 1
146
147        if test_name == self.running_tests[0]:
148            self.completed_tests.insert(0, [test_name, suffix, lines])
149        else:
150            self.completed_tests.append([test_name, suffix, lines])
151        self.running_tests.remove(test_name)
152
153        for test_name, msg, lines in self.completed_tests:
154            if lines:
155                self.meter.writeln(self._test_line(test_name, msg))
156                for line in lines:
157                    self.meter.writeln('  ' + line)
158            else:
159                write(self._test_line(test_name, msg))
160        self.completed_tests = []
161
162    def _test_line(self, test_name, suffix):
163        format_string = '[%d/%d] %s%s'
164        status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix)
165        if len(status_line) > self.meter.number_of_columns():
166            overflow_columns = len(status_line) - self.meter.number_of_columns()
167            ellipsis = '...'
168            if len(test_name) < overflow_columns + len(ellipsis) + 3:
169                # We don't have enough space even if we elide, just show the test method name.
170                test_name = test_name.split('.')[-1]
171            else:
172                new_length = len(test_name) - overflow_columns - len(ellipsis)
173                prefix = int(new_length / 2)
174                test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):]
175        return format_string % (self.num_completed, self.num_tests, test_name, suffix)
176
177    def print_result(self, run_time):
178        write = self.meter.writeln
179        write('Ran %d test%s in %.3fs' % (self.num_completed, self.num_completed != 1 and "s" or "", run_time))
180        if self.num_failures or self.num_errors:
181            write('FAILED (failures=%d, errors=%d)\n' % (self.num_failures, self.num_errors))
182        else:
183            write('\nOK\n')
184
185
186class _CaptureAndPassThroughStream(object):
187    def __init__(self, stream):
188        self._buffer = StringIO.StringIO()
189        self._stream = stream
190
191    def write(self, msg):
192        self._stream.write(msg)
193
194        # Note that we don't want to capture any output generated by the debugger
195        # because that could cause the results of capture_output() to be invalid.
196        if not self._message_is_from_pdb():
197            self._buffer.write(msg)
198
199    def _message_is_from_pdb(self):
200        # We will assume that if the pdb module is in the stack then the output
201        # is being generated by the python debugger (or the user calling something
202        # from inside the debugger).
203        import inspect
204        import pdb
205        stack = inspect.stack()
206        return any(frame[1] == pdb.__file__.replace('.pyc', '.py') for frame in stack)
207
208    def flush(self):
209        self._stream.flush()
210
211    def getvalue(self):
212        return self._buffer.getvalue()
213