简体   繁体   English

pytest caplog LogCaptureFixture 在使用 logging.config.dictConfig() 时损坏

[英]pytest caplog LogCaptureFixture is broken when using logging.config.dictConfig()

I have been going around in circles on this problem for several days now and I am no closer to a solution than when I started.几天来,我一直在围绕这个问题兜圈子,与开始时相比,我离解决方案还差得很远。

I have reviewed all of the other stackoverflow entries dealing with the pytest caplog fixture and I have narrowed my problem down to the use of logging.config.dictConfig()我已经审查了所有其他处理 pytest caplog fixture 的 stackoverflow 条目,我已经将我的问题缩小到使用logging.config.dictConfig()

I have tried multiple configurations, with and without propagate=True , and they all result in the same problem... logging is not captured when using dictConfig() .我尝试了多种配置,有和没有propagate=True ,它们都会导致同样的问题......使用dictConfig()时不会捕获日志记录。

Pytest logging when used in conjunction with config.dictConfig() is broken.config.dictConfig()结合使用时的 Pytest 日志记录被破坏。


Here's my test code which demonstrates the problem:这是我的测试代码,它演示了这个问题:

# =====================
# File: test_caplog.py
# =====================

class TestCapLog:

    def _test_logger(self, tf_caplog):
        """Display caplog capture text"""
        # display capture log
        print("\nCAPLOG:")
        output = tf_caplog.text.rstrip('\n').split(sep='\n')
        if output == ['']:
            print("Nothing captured")
        else:
            for i in range(len(output)):
                print(f'{i}: {output[i]}')

    def test_caplog0_root(self, caplog):
        """Test caplog 'root' logger w/o dictConfig()"""
        import logging
        # use logging configuration "as-is"
        logger = logging.getLogger()
        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

    def test_caplog1_main1(self, caplog):
        """Test caplog 'main' logger w/ dictConfig(), propagate=False"""
        import logging.config
        import logging
        import log_config
        # configure logging, propagate False
        log_config.LOGGING['loggers']['main']['propagate'] = False
        logging.config.dictConfig(log_config.LOGGING)
        logger = logging.getLogger(name='main')
        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

    def test_caplog1_main2(self, caplog):
        """Test caplog 'main' logger w/ dictConfig(), propagate=True"""
        import logging.config
        import logging
        import log_config
        # configure logging, propagate True
        log_config.LOGGING['loggers']['main']['propagate'] = True
        logging.config.dictConfig(log_config.LOGGING)
        logger = logging.getLogger(name='main')
        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

Here's the logging configuration file这是日志记录配置文件

# =====================
# File: log_config.py
# =====================

"""logging configuration support"""

# System imports
import logging.handlers
import sys


#: logging formatters
_formatters = {
    'msgonly': {
        'format': '%(message)s'
    },
    'minimal': {
        'format': '(%(name)s) %(message)s'
    },
    'normal': {
        'format': '%(asctime)s (%(name)s) %(levelname)s %(message)s'
    },
    'debug': {
        'format': '%(asctime)s (%(name)s) %(levelname)s %(module)s %(funcName)s %(message)s'
    }
}

#: logging stream handler string
LOGGING_STREAM_HANDLER = 'logging.StreamHandler'
#: logging timed file handler string
LOGGING_TIMED_FILE_HANDLER = 'logging.handlers.TimedRotatingFileHandler'

#: logging handlers
_handlers = {
    'debugHandler': {
        'class': LOGGING_STREAM_HANDLER,
        'level': logging.DEBUG,
        'formatter': 'debug',
        'stream': sys.stdout,
    },
    'consoleHandler': {
        'class': LOGGING_STREAM_HANDLER,
        'level': logging.DEBUG,
        'formatter': 'normal',
        'stream': sys.stdout,
    },
    'fileHandler': {
        'class': LOGGING_TIMED_FILE_HANDLER,
        'level': logging.DEBUG,
        'formatter': 'normal',
        'filename': 'logging.log',
        'when': 'D',
        'interval': 1,
        'backupCount': 7,
        'delay': True,
    },
}

#: Loggers
_loggers = {
    '': {
        'level': logging.INFO,
        'handlers': ['consoleHandler', 'fileHandler'],
        'qualname': 'root',
        'propagate': False,
    },
    'root': {
        'level': logging.DEBUG,
        'handlers': ['debugHandler', 'fileHandler'],
        'qualname': 'root',
        'propagate': False,
    },
    '__main__': {
        'level': logging.DEBUG,
        'handlers': ['debugHandler', 'fileHandler'],
        'qualname': '__main__',
        'propagate': False,
    },
    'main': {
        'level': logging.DEBUG,
        'handlers': ['debugHandler', 'fileHandler'],
        'qualname': 'main',
        'propagate': False,
    },
}

#: Configuration dictionary
LOGGING = {
    "version": 1,
    "loggers": _loggers,
    "handlers": _handlers,
    "formatters": _formatters,
}

The 3 tests that I run are:我运行的 3 个测试是:

  1. logging using the root logger with no call to dictConfig()使用root记录器记录而不调用dictConfig()
  2. logging using named logger ( main ) with call to dictConfig() and propagate=False使用命名记录器 ( main ) 调用dictConfig()propagate=False进行记录
  3. logging using named logger ( main ) with call to dictConfig() and propagate=True使用命名记录器 ( main ) 调用dictConfig()propagate=True进行记录

What follows is the output of executing my test code:以下是执行我的测试代码的输出:

/home/mark/PycharmProjects/pytest_caplog/venv/bin/python /home/mark/.local/share/JetBrains/pycharm-2022.2.2/plugins/python/helpers/pycharm/_jb_pytest_runner.py --path /home/mark/PycharmProjects/pytest_caplog/test_caplog.py 
Testing started at 1:09 AM ...
Launching pytest with arguments /home/mark/PycharmProjects/pytest_caplog/test_caplog.py --no-header --no-summary -q in /home/mark/PycharmProjects/pytest_caplog

============================= test session starts ==============================
collecting ... collected 3 items

test_caplog.py::TestCapLog::test_caplog0_root PASSED                     [ 33%]
CAPLOG:
0: ERROR    root:test_caplog.py:23 ERROR: log entry captured
1: WARNING  root:test_caplog.py:24 WARNING: log entry captured

test_caplog.py::TestCapLog::test_caplog1_main1 PASSED                    [ 66%]2022-12-22 01:09:28,810 (main) DEBUG test_caplog test_caplog1_main1 DEBUG: log entry captured
2022-12-22 01:09:28,810 (main) INFO test_caplog test_caplog1_main1 INFO: log entry captured
2022-12-22 01:09:28,810 (main) ERROR test_caplog test_caplog1_main1 ERROR: log entry captured
2022-12-22 01:09:28,811 (main) WARNING test_caplog test_caplog1_main1 WARNING: log entry captured

CAPLOG:
Nothing captured

test_caplog.py::TestCapLog::test_caplog1_main2 PASSED                    [100%]2022-12-22 01:09:28,815 (main) DEBUG test_caplog test_caplog1_main2 DEBUG: log entry captured
2022-12-22 01:09:28,815 (main) DEBUG DEBUG: log entry captured
2022-12-22 01:09:28,815 (main) INFO test_caplog test_caplog1_main2 INFO: log entry captured
2022-12-22 01:09:28,815 (main) INFO INFO: log entry captured
2022-12-22 01:09:28,815 (main) ERROR test_caplog test_caplog1_main2 ERROR: log entry captured
2022-12-22 01:09:28,815 (main) ERROR ERROR: log entry captured
2022-12-22 01:09:28,816 (main) WARNING test_caplog test_caplog1_main2 WARNING: log entry captured
2022-12-22 01:09:28,816 (main) WARNING WARNING: log entry captured

CAPLOG:
Nothing captured


============================== 3 passed in 0.03s ===============================

Process finished with exit code 0

The only way that I have been able to get caplog to behave as I would expect it to behave is to not use dictConfig() and write my own get_logger() function.我能够让 caplog 像我期望的那样运行的唯一方法是不使用dictConfig()并编写我自己的get_logger()函数。

That seems like a waste and would not be necessary if the pytest caplog fixture would respect the dictConfig() settings.这似乎是一种浪费,如果pytest caplog fixture 遵守dictConfig()设置,则没有必要。


I have read through the pytest documentation and none of the caplog examples that I have found address using anything other than the root logger.我已经通读了 pytest 文档,但我发现的caplog示例中没有一个使用root记录器以外的任何地址。

At this point I am reconsidering my decision to switch from the standard Python unittest capability to pytest在这一点上,我正在重新考虑我从标准 Python unittest功能切换到pytest的决定

This is a major blocker for me.这对我来说是一个主要障碍。

Any help that anyone can give me will be greatly appreciated.任何人都可以给我的任何帮助将不胜感激。

I am not sure if this solution acceptable for you but you can have a look.我不确定这个解决方案是否适合您,但您可以看看。 Seems like you need to overwrite logging-plugin caplog_handler property that is used by caplog fixture after dictConfig call.似乎您需要在dictConfig调用后覆盖caplog fixture 使用的logging-plugin caplog_handler属性。

You can write your own fixture that sets config and overwrites caplog_handler property of logging-plugin instance with your LogCaptureHandler that is described in config.您可以编写自己的装置来设置配置并使用配置中描述的LogCaptureHandler覆盖logging-plugin实例的caplog_handler属性。 Also this handler must be specified with loggers that needs it.此外,必须使用需要它的记录器指定此处理程序。

# log_config.py

...
CAPLOG_HANDLER = '_pytest.logging.LogCaptureHandler'

#: logging handlers
_handlers = {
    'logCaptureHandler': {
        'class': CAPLOG_HANDLER,
        'level': logging.DEBUG,
        'formatter': 'debug'
    },
    'debugHandler': {

...

    'main': {
        'level': logging.DEBUG,
        'handlers': ['debugHandler', 'fileHandler', 'logCaptureHandler'],
        'qualname': 'main',
        'propagate': False,
    },
...
# conftest.py

import logging.config
import log_config
import pytest


@pytest.fixture(scope="function")
def logging_config(request):
    logging_plugin = request.config.pluginmanager.get_plugin("logging-plugin")
    config = getattr(request, "param", log_config.LOGGING)
    logging.config.dictConfig(config)
    logging_plugin.caplog_handler = logging._handlers["logCaptureHandler"]

Also keep in mind that your logging config must not be reconfigured during tests with logging.config.dictConfig(log_config.LOGGING) because it will cause recreation of handlers.另请记住,在使用logging.config.dictConfig(log_config.LOGGING)进行测试期间,不得重新配置您的日志记录配置,因为这会导致重新处理程序。

So logging configuration will be done only with your logging_config fixture.因此,日志记录配置将仅通过您的logging_config fixture 完成。

To change config before test you can use indirect parametrization .要在测试前更改配置,您可以使用间接参数化 Example of changing propagate in main logger in 3rd test:在第 3 次测试中更改main记录器中propagate的示例:

import log_config
import logging

import pytest


class TestCapLog:

    def _test_logger(self, tf_caplog):
        """Display caplog capture text"""
        # display capture log
        print("\nCAPLOG:")
        output = tf_caplog.text.rstrip('\n').split(sep='\n')
        if output == ['']:
            print("Nothing captured")
        else:
            for i in range(len(output)):
                print(f'{i}: {output[i]}')

    def test_caplog0_root(self, caplog):
        """Test caplog 'root' logger w/o dictConfig()"""
        import logging
        # use logging configuration "as-is"
        logger = logging.getLogger()
        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

    def test_caplog1_main1(self, logging_config, caplog):
        """Test caplog 'main' logger w/ dictConfig(), propagate=False"""
        import logging
        # configure logging, propagate False
        logger = logging.getLogger(name='main')

        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

    MAIN_PROPAGATE_TRUE = log_config.LOGGING.copy()
    MAIN_PROPAGATE_TRUE['loggers']['main']['propagate'] = True

    @pytest.mark.parametrize("logging_config", [MAIN_PROPAGATE_TRUE], indirect=True)
    def test_caplog1_main2(self, logging_config, caplog):
        """Test caplog 'main' logger w/ dictConfig(), propagate=True"""

        # configure logging, propagate True
        # logging.config.dictConfig(log_config.LOGGING)
        logger = logging.getLogger(name='main')
        # log at all logging levels
        logger.debug('DEBUG: log entry captured')
        logger.info('INFO: log entry captured')
        logger.error('ERROR: log entry captured')
        logger.warning('WARNING: log entry captured')
        self._test_logger(caplog)

Also you can rewrite fixture to use mergedeep to merge your initial config ( LOGGING ) with request.param just to avoid defining and passing whole config before @pytest.mark.parametrize .您还可以重写 fixture 以使用mergedeep将您的初始配置( LOGGING )与request.param合并,以避免在@pytest.mark.parametrize之前定义和传递整个配置。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM