summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--functional_tests/test_config_files.py16
-rw-r--r--nose/__init__.py2
-rw-r--r--nose/config.py2
-rw-r--r--nose/plugins/__init__.py4
-rw-r--r--nose/plugins/builtin.py1
-rw-r--r--nose/plugins/capture.py2
-rw-r--r--nose/plugins/logcapture.py124
-rw-r--r--unit_tests/test_logcapture_plugin.py70
8 files changed, 216 insertions, 5 deletions
diff --git a/functional_tests/test_config_files.py b/functional_tests/test_config_files.py
index 8912458..5f18f3a 100644
--- a/functional_tests/test_config_files.py
+++ b/functional_tests/test_config_files.py
@@ -1,3 +1,4 @@
+import logging
import os
import unittest
from nose.config import Config
@@ -7,7 +8,22 @@ support = os.path.join(os.path.dirname(__file__), 'support')
class TestConfigurationFromFile(unittest.TestCase):
def setUp(self):
self.cfg_file = os.path.join(support, 'test.cfg')
+ # install mock root logger so that these tests don't stomp on
+ # the real logging config of the test runner
+ class MockLogger(logging.Logger):
+ root = logging.RootLogger(logging.WARNING)
+ manager = logging.Manager(root)
+
+ self.real_logger = logging.Logger
+ self.real_root = logging.root
+ logging.Logger = MockLogger
+ logging.root = MockLogger.root
+ def tearDown(self):
+ # reset real root logger
+ logging.Logger = self.real_logger
+ logging.root = self.real_root
+
def test_load_config_file(self):
c = Config(files=self.cfg_file)
c.configure(['test_load_config_file'])
diff --git a/nose/__init__.py b/nose/__init__.py
index 9deb02c..6f1da47 100644
--- a/nose/__init__.py
+++ b/nose/__init__.py
@@ -397,7 +397,7 @@ from nose.exc import SkipTest, DeprecatedTest
from nose.tools import with_setup
__author__ = 'Jason Pellerin'
-__versioninfo__ = (0, 10, 3)
+__versioninfo__ = (0, 11, 0)
__version__ = '.'.join(map(str, __versioninfo__))
__all__ = [
diff --git a/nose/config.py b/nose/config.py
index 738ef48..e4f6712 100644
--- a/nose/config.py
+++ b/nose/config.py
@@ -317,7 +317,7 @@ class Config(object):
# only add our default handler if there isn't already one there
# this avoids annoying duplicate log messages.
- if not logger.handlers:
+ if handler not in logger.handlers:
logger.addHandler(handler)
# default level
diff --git a/nose/plugins/__init__.py b/nose/plugins/__init__.py
index 91ea9a1..8f4329a 100644
--- a/nose/plugins/__init__.py
+++ b/nose/plugins/__init__.py
@@ -7,8 +7,8 @@ reporting. There are two basic rules for plugins:
* Plugin classes should subclass `nose.plugins.Plugin`_.
* Plugins may implement any of the methods described in the class
- `PluginInterface`_ in nose.plugins.base. Please note that this class is for
- documentary purposes only; plugins may not subclass PluginInterface.
+ `IPluginInterface`_ in nose.plugins.base. Please note that this class is for
+ documentary purposes only; plugins may not subclass IPluginInterface.
.. _nose.plugins.Plugin: http://python-nose.googlecode.com/svn/trunk/nose/plugins/base.py
diff --git a/nose/plugins/builtin.py b/nose/plugins/builtin.py
index 3912e5e..b9b9fb8 100644
--- a/nose/plugins/builtin.py
+++ b/nose/plugins/builtin.py
@@ -5,6 +5,7 @@ plugins = []
builtins = (
('nose.plugins.attrib', 'AttributeSelector'),
('nose.plugins.capture', 'Capture'),
+ ('nose.plugins.logcapture', 'LogCapture'),
('nose.plugins.cover', 'Coverage'),
('nose.plugins.debug', 'Pdb'),
('nose.plugins.deprecated', 'Deprecated'),
diff --git a/nose/plugins/capture.py b/nose/plugins/capture.py
index cf2a734..874a68e 100644
--- a/nose/plugins/capture.py
+++ b/nose/plugins/capture.py
@@ -1,7 +1,7 @@
"""
This plugin captures stdout during test execution, appending any
output captured to the error or failure output, should the test fail
-or raise an error. It is enabled by default but may be disable with
+or raise an error. It is enabled by default but may be disabled with
the options -s or --nocapture.
"""
import logging
diff --git a/nose/plugins/logcapture.py b/nose/plugins/logcapture.py
new file mode 100644
index 0000000..a580171
--- /dev/null
+++ b/nose/plugins/logcapture.py
@@ -0,0 +1,124 @@
+"""
+This plugin captures logging statements issued during test
+execution, appending any output captured to the error or failure
+output, should the test fail or raise an error. It is enabled by
+default but may be disabled with the options --nologcapture.
+
+To remove any other installed logging handlers, use the
+--logging-clear-handlers option.
+
+When an error or failure occurs, captures log messages are attached to
+the running test in the test.capturedLogging attribute, and added to
+the error failure output.
+
+Status: http://code.google.com/p/python-nose/issues/detail?id=148
+"""
+
+import os
+import logging
+from logging.handlers import BufferingHandler
+
+from nose.plugins.base import Plugin
+from nose.util import ln
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+log = logging.getLogger(__name__)
+
+
+class MyMemoryHandler(BufferingHandler):
+ def flush(self):
+ pass # do nothing
+ def truncate(self):
+ self.buffer = []
+
+
+class LogCapture(Plugin):
+ """
+ Log capture plugin. Enabled by default. Disable with --nologcapture.
+ This plugin captures logging statement issued during test execution,
+ appending any output captured to the error or failure output,
+ should the test fail or raise an error.
+ """
+ enabled = True
+ env_opt = 'NOSE_NOLOGCAPTURE'
+ name = 'logcapture'
+ score = 500
+ logformat = '%(name)s: %(levelname)s: %(message)s'
+ clear = False
+
+ def options(self, parser, env=os.environ):
+ parser.add_option(
+ "", "--nologcapture", action="store_false",
+ default=not env.get(self.env_opt), dest="logcapture",
+ help="Don't capture logging output [NOSE_NOLOGCAPTURE]")
+ parser.add_option(
+ "", "--logging-format", action="store", dest="logcapture_format",
+ default=env.get('NOSE_LOGFORMAT') or self.logformat,
+ help="logging statements formatting [NOSE_LOGFORMAT]")
+ parser.add_option(
+ "", "--logging-clear-handlers", action="store_true",
+ default=False, dest="logcapture_clear",
+ help="Clear all other logging handlers")
+
+ def configure(self, options, conf):
+ self.conf = conf
+ # Disable if explicitly disabled, or if logging is
+ # configured via logging config file
+ if not options.logcapture or conf.loggingConfig:
+ self.enabled = False
+ self.logformat = options.logcapture_format
+ self.clear = options.logcapture_clear
+
+ def setupLoghandler(self):
+ # setup our handler with root logger
+ root_logger = logging.getLogger()
+ if self.clear:
+ for handler in root_logger.handlers:
+ root_logger.removeHandler(handler)
+ # unless it's already there
+ if self.handler not in root_logger.handlers:
+ root_logger.addHandler(self.handler)
+ # to make sure everything gets captured
+ root_logger.setLevel(logging.NOTSET)
+
+ def begin(self):
+ self.start()
+
+ def start(self):
+ self.handler = MyMemoryHandler(1000)
+ fmt = logging.Formatter(self.logformat)
+ self.handler.setFormatter(fmt)
+ self.setupLoghandler()
+
+ def end(self):
+ pass
+
+ def beforeTest(self, test):
+ self.setupLoghandler()
+
+ def afterTest(self, test):
+ self.handler.truncate()
+
+ def formatFailure(self, test, err):
+ return self.formatError(test, err)
+
+ def formatError(self, test, err):
+ # logic flow copied from Capture.formatError
+ test.capturedLogging = records = self.formatLogRecords()
+ if not records:
+ return err
+ ec, ev, tb = err
+ return (ec, self.addCaptureToErr(ev, records), tb)
+
+ def formatLogRecords(self):
+ format = self.handler.format
+ return [format(r) for r in self.handler.buffer]
+
+ def addCaptureToErr(self, ev, records):
+ return '\n'.join([str(ev), ln('>> begin captured logging <<')] + \
+ records + \
+ [ln('>> end captured logging <<')])
diff --git a/unit_tests/test_logcapture_plugin.py b/unit_tests/test_logcapture_plugin.py
new file mode 100644
index 0000000..397a350
--- /dev/null
+++ b/unit_tests/test_logcapture_plugin.py
@@ -0,0 +1,70 @@
+
+import sys
+from optparse import OptionParser
+from nose.config import Config
+from nose.plugins.logcapture import LogCapture
+from nose.tools import eq_
+import logging
+
+class TestLogCapturePlugin(object):
+
+ def test_enabled_by_default(self):
+ c = LogCapture()
+ assert c.enabled
+
+ def test_default_options(self):
+ c = LogCapture()
+ parser = OptionParser()
+ c.addOptions(parser)
+
+ options, args = parser.parse_args(['default_options'])
+ c.configure(options, Config())
+ assert c.enabled
+ eq_(LogCapture.logformat, c.logformat)
+
+ def test_disable_option(self):
+ parser = OptionParser()
+ c = LogCapture()
+ c.addOptions(parser)
+ options, args = parser.parse_args(['test_can_be_disabled_long',
+ '--nologcapture'])
+ c.configure(options, Config())
+ assert not c.enabled
+
+ env = {'NOSE_NOLOGCAPTURE': 1}
+ c = LogCapture()
+ parser = OptionParser()
+ c.addOptions(parser, env)
+ options, args = parser.parse_args(['test_can_be_disabled'])
+ c.configure(options, Config())
+ assert not c.enabled
+
+ def test_logging_format_option(self):
+ env = {'NOSE_LOGFORMAT': '++%(message)s++'}
+ c = LogCapture()
+ parser = OptionParser()
+ c.addOptions(parser, env)
+ options, args = parser.parse_args(['logging_format'])
+ c.configure(options, Config())
+ eq_('++%(message)s++', c.logformat)
+
+ def test_captures_logging(self):
+ c = LogCapture()
+ c.start()
+ log = logging.getLogger("foobar.something")
+ log.debug("Hello")
+ c.end()
+ eq_(1, len(c.handler.buffer))
+ eq_("Hello", c.handler.buffer[0].msg)
+
+ def test_custom_formatter(self):
+ c = LogCapture()
+ c.logformat = '++%(message)s++'
+ c.start()
+ log = logging.getLogger("foobar.something")
+ log.debug("Hello")
+ c.end()
+ records = c.formatLogRecords()
+ eq_(1, len(records))
+ eq_("++Hello++", records[0])
+