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