Home | History | Annotate | Download | only in test
      1 # Copyright (C) 2012 Google, Inc.
      2 # Copyright (C) 2010 Chris Jerdonek (cjerdonek (at] 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 
     24 import logging
     25 import StringIO
     26 
     27 from webkitpy.common.system.systemhost import SystemHost
     28 from webkitpy.layout_tests.views.metered_stream import MeteredStream
     29 
     30 _log = logging.getLogger(__name__)
     31 
     32 
     33 class 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 
    187 class _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