WebKitTools/Scripts/webkitpy/common/system/logtesting.py
changeset 0 4f2f89ce4247
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebKitTools/Scripts/webkitpy/common/system/logtesting.py	Fri Sep 17 09:02:29 2010 +0300
@@ -0,0 +1,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)