diff options
-rw-r--r-- | functional_tests/test_config_files.py | 16 | ||||
-rw-r--r-- | nose/__init__.py | 2 | ||||
-rw-r--r-- | nose/config.py | 2 | ||||
-rw-r--r-- | nose/plugins/__init__.py | 4 | ||||
-rw-r--r-- | nose/plugins/builtin.py | 1 | ||||
-rw-r--r-- | nose/plugins/capture.py | 2 | ||||
-rw-r--r-- | nose/plugins/logcapture.py | 124 | ||||
-rw-r--r-- | unit_tests/test_logcapture_plugin.py | 70 |
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]) + |