From aca1b06747a432e4a7bc3b8b3f98a2a59192c785 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 16 Jan 2018 20:32:59 -0200 Subject: [PATCH] Undo log level set by caplog.set_level at the end of the test Otherwise this leaks the log level information to other tests Ref: #3013 --- _pytest/logging.py | 50 ++++++++++++++++++++++----------- testing/logging/test_fixture.py | 28 ++++++++++++++++++ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 88a50e22c..1568d3f08 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -129,6 +129,17 @@ class LogCaptureFixture(object): def __init__(self, item): """Creates a new funcarg.""" self._item = item + self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level + + def _finalize(self): + """Finalizes the fixture. + + This restores the log levels changed by :meth:`set_level`. + """ + # restore log levels + for logger_name, level in self._initial_log_levels.items(): + logger = logging.getLogger(logger_name) + logger.setLevel(level) @property def handler(self): @@ -167,27 +178,30 @@ class LogCaptureFixture(object): self.handler.records = [] def set_level(self, level, logger=None): - """Sets the level for capturing of logs. + """Sets the level for capturing of logs. The level will be restored to its previous value at the end of + the test. + + :param int level: the logger to level. + :param str logger: the logger to update the level. If not given, the root logger level is updated. + + .. versionchanged:: 3.4 + The levels of the loggers changed by this function will be restored to their initial values at the + end of the test. + """ + logger_name = logger + logger = logging.getLogger(logger_name) + self._initial_log_levels.setdefault(logger_name, logger.level) + logger.setLevel(level) + + @contextmanager + def at_level(self, level, logger=None): + """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the + level is restored to its original value. :param int level: the logger to level. :param str logger: the logger to update the level. If not given, the root logger level is updated. """ logger = logging.getLogger(logger) - logger.setLevel(level) - - @contextmanager - def at_level(self, level, logger=None): - """Context manager that sets the level for capturing of logs. - - By default, the level is set on the handler used to capture - logs. Specify a logger name to instead set the level of any - logger. - """ - if logger is None: - logger = self.handler - else: - logger = logging.getLogger(logger) - orig_level = logger.level logger.setLevel(level) try: @@ -206,7 +220,9 @@ def caplog(request): * caplog.records() -> list of logging.LogRecord instances * caplog.record_tuples() -> list of (logger_name, level, message) tuples """ - return LogCaptureFixture(request.node) + result = LogCaptureFixture(request.node) + yield result + result._finalize() def get_actual_log_level(config, *setting_names): diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 1357dcf36..fcd231867 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -27,6 +27,30 @@ def test_change_level(caplog): assert 'CRITICAL' in caplog.text +def test_change_level_undo(testdir): + """Ensure that 'set_level' is undone after the end of the test""" + testdir.makepyfile(''' + import logging + + def test1(caplog): + caplog.set_level(logging.INFO) + # using + operator here so fnmatch_lines doesn't match the code in the traceback + logging.info('log from ' + 'test1') + assert 0 + + def test2(caplog): + # using + operator here so fnmatch_lines doesn't match the code in the traceback + logging.info('log from ' + 'test2') + assert 0 + ''') + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines([ + '*log from test1*', + '*2 failed in *', + ]) + assert 'log from test2' not in result.stdout.str() + + def test_with_statement(caplog): with caplog.at_level(logging.INFO): logger.debug('handler DEBUG level') @@ -43,6 +67,7 @@ def test_with_statement(caplog): def test_log_access(caplog): + caplog.set_level(logging.INFO) logger.info('boo %s', 'arg') assert caplog.records[0].levelname == 'INFO' assert caplog.records[0].msg == 'boo %s' @@ -50,6 +75,7 @@ def test_log_access(caplog): def test_record_tuples(caplog): + caplog.set_level(logging.INFO) logger.info('boo %s', 'arg') assert caplog.record_tuples == [ @@ -58,6 +84,7 @@ def test_record_tuples(caplog): def test_unicode(caplog): + caplog.set_level(logging.INFO) logger.info(u'bū') assert caplog.records[0].levelname == 'INFO' assert caplog.records[0].msg == u'bū' @@ -65,6 +92,7 @@ def test_unicode(caplog): def test_clear(caplog): + caplog.set_level(logging.INFO) logger.info(u'bū') assert len(caplog.records) caplog.clear()