Suspend stdout/stderr capturing when emitting live logging messages

This commit is contained in:
Bruno Oliveira 2018-01-18 21:10:35 -02:00
parent 4a436572a8
commit 9dbcac9af3
2 changed files with 117 additions and 8 deletions

View File

@ -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()

View File

@ -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'