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