summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/common/system/logtesting.py
blob: e361cb593b1a1ce18a41d0741a6eaa1130480e23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Supports the unit-testing of logging code.

Provides support for unit-testing messages logged using the built-in
logging module.

Inherit from the LoggingTestCase class for basic testing needs.  For
more advanced needs (e.g. unit-testing methods that configure logging),
see the TestLogStream class, and perhaps also the LogTesting class.

"""

import logging
import unittest


class TestLogStream(object):

    """Represents a file-like object for unit-testing logging.

    This is meant for passing to the logging.StreamHandler constructor.
    Log messages captured by instances of this object can be tested
    using self.assertMessages() below.

    """

    def __init__(self, test_case):
        """Create an instance.

        Args:
          test_case: A unittest.TestCase instance.

        """
        self._test_case = test_case
        self.messages = []
        """A list of log messages written to the stream."""

    # Python documentation says that any object passed to the StreamHandler
    # constructor should support write() and flush():
    #
    # http://docs.python.org/library/logging.html#module-logging.handlers
    def write(self, message):
        self.messages.append(message)

    def flush(self):
        pass

    def assertMessages(self, messages):
        """Assert that the given messages match the logged messages.

        messages: A list of log message strings.

        """
        self._test_case.assertEquals(messages, self.messages)


class LogTesting(object):

    """Supports end-to-end unit-testing of log messages.

        Sample usage:

          class SampleTest(unittest.TestCase):

              def setUp(self):
                  self._log = LogTesting.setUp(self)  # Turn logging on.

              def tearDown(self):
                  self._log.tearDown()  # Turn off and reset logging.

              def test_logging_in_some_method(self):
                  call_some_method()  # Contains calls to _log.info(), etc.

                  # Check the resulting log messages.
                  self._log.assertMessages(["INFO: expected message #1",
                                          "WARNING: expected message #2"])

    """

    def __init__(self, test_stream, handler):
        """Create an instance.

        This method should never be called directly.  Instances should
        instead be created using the static setUp() method.

        Args:
          test_stream: A TestLogStream instance.
          handler: The handler added to the logger.

        """
        self._test_stream = test_stream
        self._handler = handler

    @staticmethod
    def _getLogger():
        """Return the logger being tested."""
        # It is possible we might want to return something other than
        # the root logger in some special situation.  For now, the
        # root logger seems to suffice.
        return logging.getLogger()

    @staticmethod
    def setUp(test_case, logging_level=logging.INFO):
        """Configure logging for unit testing.

        Configures the root logger to log to a testing log stream.
        Only messages logged at or above the given level are logged
        to the stream.  Messages logged to the stream are formatted
        in the following way, for example--

        "INFO: This is a test log message."

        This method should normally be called in the setUp() method
        of a unittest.TestCase.  See the docstring of this class
        for more details.

        Returns:
          A LogTesting instance.

        Args:
          test_case: A unittest.TestCase instance.
          logging_level: An integer logging level that is the minimum level
                         of log messages you would like to test.

        """
        stream = TestLogStream(test_case)
        handler = logging.StreamHandler(stream)
        handler.setLevel(logging_level)
        formatter = logging.Formatter("%(levelname)s: %(message)s")
        handler.setFormatter(formatter)

        # Notice that we only change the root logger by adding a handler
        # to it.  In particular, we do not reset its level using
        # logger.setLevel().  This ensures that we have not interfered
        # with how the code being tested may have configured the root
        # logger.
        logger = LogTesting._getLogger()
        logger.addHandler(handler)

        return LogTesting(stream, handler)

    def tearDown(self):
        """Assert there are no remaining log messages, and reset logging.

        This method asserts that there are no more messages in the array of
        log messages, and then restores logging to its original state.
        This method should normally be called in the tearDown() method of a
        unittest.TestCase.  See the docstring of this class for more details.

        """
        self.assertMessages([])
        logger = LogTesting._getLogger()
        logger.removeHandler(self._handler)

    def messages(self):
        """Return the current list of log messages."""
        return self._test_stream.messages

    # FIXME: Add a clearMessages() method for cases where the caller
    #        deliberately doesn't want to assert every message.

    # We clear the log messages after asserting since they are no longer
    # needed after asserting.  This serves two purposes: (1) it simplifies
    # the calling code when we want to check multiple logging calls in a
    # single test method, and (2) it lets us check in the tearDown() method
    # that there are no remaining log messages to be asserted.
    #
    # The latter ensures that no extra log messages are getting logged that
    # the caller might not be aware of or may have forgotten to check for.
    # This gets us a bit more mileage out of our tests without writing any
    # additional code.
    def assertMessages(self, messages):
        """Assert the current array of log messages, and clear its contents.

        Args:
          messages: A list of log message strings.

        """
        try:
            self._test_stream.assertMessages(messages)
        finally:
            # We want to clear the array of messages even in the case of
            # an Exception (e.g. an AssertionError).  Otherwise, another
            # AssertionError can occur in the tearDown() because the
            # array might not have gotten emptied.
            self._test_stream.messages = []


# This class needs to inherit from unittest.TestCase.  Otherwise, the
# setUp() and tearDown() methods will not get fired for test case classes
# that inherit from this class -- even if the class inherits from *both*
# unittest.TestCase and LoggingTestCase.
#
# FIXME: Rename this class to LoggingTestCaseBase to be sure that
#        the unittest module does not interpret this class as a unittest
#        test case itself.
class LoggingTestCase(unittest.TestCase):

    """Supports end-to-end unit-testing of log messages.

        Sample usage:

          class SampleTest(LoggingTestCase):

              def test_logging_in_some_method(self):
                  call_some_method()  # Contains calls to _log.info(), etc.

                  # Check the resulting log messages.
                  self.assertLog(["INFO: expected message #1",
                                  "WARNING: expected message #2"])

    """

    def setUp(self):
        self._log = LogTesting.setUp(self)

    def tearDown(self):
        self._log.tearDown()

    def logMessages(self):
        """Return the current list of log messages."""
        return self._log.messages()

    # FIXME: Add a clearMessages() method for cases where the caller
    #        deliberately doesn't want to assert every message.

    # See the code comments preceding LogTesting.assertMessages() for
    # an explanation of why we clear the array of messages after
    # asserting its contents.
    def assertLog(self, messages):
        """Assert the current array of log messages, and clear its contents.

        Args:
          messages: A list of log message strings.

        """
        self._log.assertMessages(messages)