Suspend stdout/stderr capturing when emitting live logging messages
This commit is contained in:
parent
4a436572a8
commit
9dbcac9af3
|
@ -319,6 +319,8 @@ class LoggingPlugin(object):
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_setup(self, item):
|
def pytest_runtest_setup(self, item):
|
||||||
|
if self.log_cli_handler is not None:
|
||||||
|
self.log_cli_handler.reset()
|
||||||
with self._runtest_for(item, 'setup'):
|
with self._runtest_for(item, 'setup'):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@ -352,12 +354,13 @@ class LoggingPlugin(object):
|
||||||
"""
|
"""
|
||||||
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
|
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
|
||||||
if self._config.getini('log_cli') and terminal_reporter is not None:
|
if self._config.getini('log_cli') and terminal_reporter is not None:
|
||||||
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter._tw)
|
capture_manager = self._config.pluginmanager.get_plugin('capturemanager')
|
||||||
|
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
|
||||||
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
|
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
|
||||||
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
|
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
|
||||||
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
|
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
|
||||||
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
|
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
|
||||||
self.log_cli_handler = log_cli_handler # needed for a single unittest
|
self.log_cli_handler = log_cli_handler
|
||||||
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
|
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
|
||||||
else:
|
else:
|
||||||
self.log_cli_handler = None
|
self.log_cli_handler = None
|
||||||
|
@ -368,12 +371,33 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||||
"""
|
"""
|
||||||
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
||||||
in each test.
|
in each test.
|
||||||
|
|
||||||
|
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
|
||||||
|
and won't appear in the terminal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, terminal_reporter, capture_manager):
|
||||||
|
"""
|
||||||
|
:param _pytest.terminal.TerminalReporter terminal_reporter:
|
||||||
|
:param _pytest.capture.CaptureManager capture_manager:
|
||||||
|
"""
|
||||||
|
logging.StreamHandler.__init__(self, stream=terminal_reporter)
|
||||||
|
self.capture_manager = capture_manager
|
||||||
|
self._first_record_emitted = False
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._first_record_emitted = False
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
if not getattr(self, '_first_record_emitted', False):
|
if self.capture_manager is not None:
|
||||||
self.stream.write('\n')
|
self.capture_manager.suspend_global_capture()
|
||||||
# we might consider adding a header at this point using self.stream.sep('-', 'live log') or something
|
try:
|
||||||
# similar when we improve live logging output
|
if not self._first_record_emitted:
|
||||||
self._first_record_emitted = True
|
self.stream.write('\n')
|
||||||
logging.StreamHandler.emit(self, record)
|
# we might consider adding a header at this point using self.stream.section('live log', sep='-')
|
||||||
|
# or something similar when we improve live logging output
|
||||||
|
self._first_record_emitted = True
|
||||||
|
logging.StreamHandler.emit(self, record)
|
||||||
|
finally:
|
||||||
|
if self.capture_manager is not None:
|
||||||
|
self.capture_manager.resume_global_capture()
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -193,6 +196,36 @@ def test_log_cli_default_level(testdir):
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_cli_default_level_multiple_tests(testdir):
|
||||||
|
"""Ensure we reset the first newline added by the live logger between tests"""
|
||||||
|
# Default log file level
|
||||||
|
testdir.makepyfile('''
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def test_log_1(request):
|
||||||
|
logging.warning("log message from test_log_1")
|
||||||
|
|
||||||
|
def test_log_2(request):
|
||||||
|
logging.warning("log message from test_log_2")
|
||||||
|
''')
|
||||||
|
testdir.makeini('''
|
||||||
|
[pytest]
|
||||||
|
log_cli=true
|
||||||
|
''')
|
||||||
|
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
'test_log_cli_default_level_multiple_tests.py::test_log_1 ',
|
||||||
|
'*WARNING*log message from test_log_1*',
|
||||||
|
'PASSED *50%*',
|
||||||
|
'test_log_cli_default_level_multiple_tests.py::test_log_2 ',
|
||||||
|
'*WARNING*log message from test_log_2*',
|
||||||
|
'PASSED *100%*',
|
||||||
|
'=* 2 passed in *=',
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def test_log_cli_level(testdir):
|
def test_log_cli_level(testdir):
|
||||||
# Default log file level
|
# Default log file level
|
||||||
testdir.makepyfile('''
|
testdir.makepyfile('''
|
||||||
|
@ -410,3 +443,55 @@ def test_log_file_ini_level(testdir):
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "This log message will be shown" in contents
|
assert "This log message will be shown" in contents
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('has_capture_manager', [True, False])
|
||||||
|
def test_live_logging_suspends_capture(has_capture_manager, request):
|
||||||
|
"""Test that capture manager is suspended when we emitting messages for live logging.
|
||||||
|
|
||||||
|
This tests the implementation calls instead of behavior because it is difficult/impossible to do it using
|
||||||
|
``testdir`` facilities because they do their own capturing.
|
||||||
|
|
||||||
|
We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin
|
||||||
|
is installed.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from functools import partial
|
||||||
|
from _pytest.capture import CaptureManager
|
||||||
|
from _pytest.logging import _LiveLoggingStreamHandler
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
# need to use the 'generic' StringIO instead of io.StringIO because we might receive both bytes
|
||||||
|
# and unicode objects; io.StringIO only accepts unicode
|
||||||
|
from StringIO import StringIO
|
||||||
|
else:
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
class MockCaptureManager:
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def suspend_global_capture(self):
|
||||||
|
self.calls.append('suspend_global_capture')
|
||||||
|
|
||||||
|
def resume_global_capture(self):
|
||||||
|
self.calls.append('resume_global_capture')
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
assert CaptureManager.suspend_capture_item
|
||||||
|
assert CaptureManager.resume_global_capture
|
||||||
|
|
||||||
|
capture_manager = MockCaptureManager() if has_capture_manager else None
|
||||||
|
out_file = StringIO()
|
||||||
|
|
||||||
|
handler = _LiveLoggingStreamHandler(out_file, capture_manager)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__file__ + '.test_live_logging_suspends_capture')
|
||||||
|
logger.addHandler(handler)
|
||||||
|
request.addfinalizer(partial(logger.removeHandler, handler))
|
||||||
|
|
||||||
|
logger.critical('some message')
|
||||||
|
if has_capture_manager:
|
||||||
|
assert MockCaptureManager.calls == ['suspend_global_capture', 'resume_global_capture']
|
||||||
|
else:
|
||||||
|
assert MockCaptureManager.calls == []
|
||||||
|
assert out_file.getvalue() == '\nsome message\n'
|
||||||
|
|
Loading…
Reference in New Issue