1# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org) 2# 3# Redistribution and use in source and binary forms, with or without 4# modification, are permitted provided that the following conditions 5# are met: 6# 1. Redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer. 8# 2. Redistributions in binary form must reproduce the above copyright 9# notice, this list of conditions and the following disclaimer in the 10# documentation and/or other materials provided with the distribution. 11# 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 23"""Supports the unit-testing of logging code. 24 25Provides support for unit-testing messages logged using the built-in 26logging module. 27 28Inherit from the LoggingTestCase class for basic testing needs. For 29more advanced needs (e.g. unit-testing methods that configure logging), 30see the TestLogStream class, and perhaps also the LogTesting class. 31 32""" 33 34import logging 35import unittest 36 37 38class TestLogStream(object): 39 40 """Represents a file-like object for unit-testing logging. 41 42 This is meant for passing to the logging.StreamHandler constructor. 43 Log messages captured by instances of this object can be tested 44 using self.assertMessages() below. 45 46 """ 47 48 def __init__(self, test_case): 49 """Create an instance. 50 51 Args: 52 test_case: A unittest.TestCase instance. 53 54 """ 55 self._test_case = test_case 56 self.messages = [] 57 """A list of log messages written to the stream.""" 58 59 # Python documentation says that any object passed to the StreamHandler 60 # constructor should support write() and flush(): 61 # 62 # http://docs.python.org/library/logging.html#module-logging.handlers 63 def write(self, message): 64 self.messages.append(message) 65 66 def flush(self): 67 pass 68 69 def assertMessages(self, messages): 70 """Assert that the given messages match the logged messages. 71 72 messages: A list of log message strings. 73 74 """ 75 self._test_case.assertEquals(messages, self.messages) 76 77 78class LogTesting(object): 79 80 """Supports end-to-end unit-testing of log messages. 81 82 Sample usage: 83 84 class SampleTest(unittest.TestCase): 85 86 def setUp(self): 87 self._log = LogTesting.setUp(self) # Turn logging on. 88 89 def tearDown(self): 90 self._log.tearDown() # Turn off and reset logging. 91 92 def test_logging_in_some_method(self): 93 call_some_method() # Contains calls to _log.info(), etc. 94 95 # Check the resulting log messages. 96 self._log.assertMessages(["INFO: expected message #1", 97 "WARNING: expected message #2"]) 98 99 """ 100 101 def __init__(self, test_stream, handler): 102 """Create an instance. 103 104 This method should never be called directly. Instances should 105 instead be created using the static setUp() method. 106 107 Args: 108 test_stream: A TestLogStream instance. 109 handler: The handler added to the logger. 110 111 """ 112 self._test_stream = test_stream 113 self._handler = handler 114 115 @staticmethod 116 def _getLogger(): 117 """Return the logger being tested.""" 118 # It is possible we might want to return something other than 119 # the root logger in some special situation. For now, the 120 # root logger seems to suffice. 121 return logging.getLogger() 122 123 @staticmethod 124 def setUp(test_case, logging_level=logging.INFO): 125 """Configure logging for unit testing. 126 127 Configures the root logger to log to a testing log stream. 128 Only messages logged at or above the given level are logged 129 to the stream. Messages logged to the stream are formatted 130 in the following way, for example-- 131 132 "INFO: This is a test log message." 133 134 This method should normally be called in the setUp() method 135 of a unittest.TestCase. See the docstring of this class 136 for more details. 137 138 Returns: 139 A LogTesting instance. 140 141 Args: 142 test_case: A unittest.TestCase instance. 143 logging_level: An integer logging level that is the minimum level 144 of log messages you would like to test. 145 146 """ 147 stream = TestLogStream(test_case) 148 handler = logging.StreamHandler(stream) 149 handler.setLevel(logging_level) 150 formatter = logging.Formatter("%(levelname)s: %(message)s") 151 handler.setFormatter(formatter) 152 153 # Notice that we only change the root logger by adding a handler 154 # to it. In particular, we do not reset its level using 155 # logger.setLevel(). This ensures that we have not interfered 156 # with how the code being tested may have configured the root 157 # logger. 158 logger = LogTesting._getLogger() 159 logger.addHandler(handler) 160 161 return LogTesting(stream, handler) 162 163 def tearDown(self): 164 """Assert there are no remaining log messages, and reset logging. 165 166 This method asserts that there are no more messages in the array of 167 log messages, and then restores logging to its original state. 168 This method should normally be called in the tearDown() method of a 169 unittest.TestCase. See the docstring of this class for more details. 170 171 """ 172 self.assertMessages([]) 173 logger = LogTesting._getLogger() 174 logger.removeHandler(self._handler) 175 176 def messages(self): 177 """Return the current list of log messages.""" 178 return self._test_stream.messages 179 180 # FIXME: Add a clearMessages() method for cases where the caller 181 # deliberately doesn't want to assert every message. 182 183 # We clear the log messages after asserting since they are no longer 184 # needed after asserting. This serves two purposes: (1) it simplifies 185 # the calling code when we want to check multiple logging calls in a 186 # single test method, and (2) it lets us check in the tearDown() method 187 # that there are no remaining log messages to be asserted. 188 # 189 # The latter ensures that no extra log messages are getting logged that 190 # the caller might not be aware of or may have forgotten to check for. 191 # This gets us a bit more mileage out of our tests without writing any 192 # additional code. 193 def assertMessages(self, messages): 194 """Assert the current array of log messages, and clear its contents. 195 196 Args: 197 messages: A list of log message strings. 198 199 """ 200 try: 201 self._test_stream.assertMessages(messages) 202 finally: 203 # We want to clear the array of messages even in the case of 204 # an Exception (e.g. an AssertionError). Otherwise, another 205 # AssertionError can occur in the tearDown() because the 206 # array might not have gotten emptied. 207 self._test_stream.messages = [] 208 209 210# This class needs to inherit from unittest.TestCase. Otherwise, the 211# setUp() and tearDown() methods will not get fired for test case classes 212# that inherit from this class -- even if the class inherits from *both* 213# unittest.TestCase and LoggingTestCase. 214# 215# FIXME: Rename this class to LoggingTestCaseBase to be sure that 216# the unittest module does not interpret this class as a unittest 217# test case itself. 218class LoggingTestCase(unittest.TestCase): 219 220 """Supports end-to-end unit-testing of log messages. 221 222 Sample usage: 223 224 class SampleTest(LoggingTestCase): 225 226 def test_logging_in_some_method(self): 227 call_some_method() # Contains calls to _log.info(), etc. 228 229 # Check the resulting log messages. 230 self.assertLog(["INFO: expected message #1", 231 "WARNING: expected message #2"]) 232 233 """ 234 235 def setUp(self): 236 self._log = LogTesting.setUp(self) 237 238 def tearDown(self): 239 self._log.tearDown() 240 241 def logMessages(self): 242 """Return the current list of log messages.""" 243 return self._log.messages() 244 245 # FIXME: Add a clearMessages() method for cases where the caller 246 # deliberately doesn't want to assert every message. 247 248 # See the code comments preceding LogTesting.assertMessages() for 249 # an explanation of why we clear the array of messages after 250 # asserting its contents. 251 def assertLog(self, messages): 252 """Assert the current array of log messages, and clear its contents. 253 254 Args: 255 messages: A list of log message strings. 256 257 """ 258 self._log.assertMessages(messages) 259