Skip to content

Commit 3e93eac

Browse files
committed
Suspend stdout/stderr capturing when emitting live logging messages
1 parent cee35bb commit 3e93eac

File tree

2 files changed

+81
-7
lines changed

2 files changed

+81
-7
lines changed

_pytest/logging.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ def _setup_cli_logging(self):
340340
"""
341341
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
342342
if self._config.getini('log_cli') and terminal_reporter is not None:
343-
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter._tw)
343+
capture_manager = self._config.pluginmanager.get_plugin('capturemanager')
344+
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
344345
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
345346
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
346347
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
@@ -356,12 +357,30 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
356357
"""
357358
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
358359
in each test.
360+
361+
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
362+
and won't appear in the terminal.
359363
"""
360364

365+
def __init__(self, terminal_reporter, capture_manager):
366+
"""
367+
:param _pytest.terminal.TerminalReporter terminal_reporter:
368+
:param _pytest.capture.CaptureManager capture_manager:
369+
"""
370+
logging.StreamHandler.__init__(self, stream=terminal_reporter)
371+
self.capture_manager = capture_manager
372+
self._first_record_emitted = False
373+
361374
def emit(self, record):
362-
if not getattr(self, '_first_record_emitted', False):
363-
self.stream.write('\n')
364-
# we might consider adding a header at this point using self.stream.sep('-', 'live log') or something
365-
# similar when we improve live logging output
366-
self._first_record_emitted = True
367-
logging.StreamHandler.emit(self, record)
375+
if self.capture_manager is not None:
376+
self.capture_manager.suspend_global_capture()
377+
try:
378+
if not self._first_record_emitted:
379+
self.stream.write('\n')
380+
# we might consider adding a header at this point using self.stream.section('live log', sep='-')
381+
# or something similar when we improve live logging output
382+
self._first_record_emitted = True
383+
logging.StreamHandler.emit(self, record)
384+
finally:
385+
if self.capture_manager is not None:
386+
self.capture_manager.resume_global_capture()

testing/logging/test_reporting.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# -*- coding: utf-8 -*-
22
import os
3+
4+
import six
5+
36
import pytest
47

58

@@ -410,3 +413,55 @@ def test_log_file(request):
410413
contents = rfh.read()
411414
assert "This log message will be shown" in contents
412415
assert "This log message won't be shown" not in contents
416+
417+
418+
@pytest.mark.parametrize('has_capture_manager', [True, False])
419+
def test_live_logging_suspends_capture(has_capture_manager, request):
420+
"""Test that capture manager is suspended when we emitting messages for live logging.
421+
422+
This tests the implementation calls instead of behavior because it is difficult/impossible to do it using
423+
``testdir`` facilities because they do their own capturing.
424+
425+
We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin
426+
is installed.
427+
"""
428+
import logging
429+
from functools import partial
430+
from _pytest.capture import CaptureManager
431+
from _pytest.logging import _LiveLoggingStreamHandler
432+
433+
if six.PY2:
434+
# need to use the 'generic' StringIO instead of io.StringIO because we might receive both bytes
435+
# and unicode objects; io.StringIO only accepts unicode
436+
from StringIO import StringIO
437+
else:
438+
from io import StringIO
439+
440+
class MockCaptureManager:
441+
calls = []
442+
443+
def suspend_global_capture(self):
444+
self.calls.append('suspend_global_capture')
445+
446+
def resume_global_capture(self):
447+
self.calls.append('resume_global_capture')
448+
449+
# sanity check
450+
assert CaptureManager.suspend_capture_item
451+
assert CaptureManager.resume_global_capture
452+
453+
capture_manager = MockCaptureManager() if has_capture_manager else None
454+
out_file = StringIO()
455+
456+
handler = _LiveLoggingStreamHandler(out_file, capture_manager)
457+
458+
logger = logging.getLogger(__file__ + '.test_live_logging_suspends_capture')
459+
logger.addHandler(handler)
460+
request.addfinalizer(partial(logger.removeHandler, handler))
461+
462+
logger.critical('some message')
463+
if has_capture_manager:
464+
assert MockCaptureManager.calls == ['suspend_global_capture', 'resume_global_capture']
465+
else:
466+
assert MockCaptureManager.calls == []
467+
assert out_file.getvalue() == '\nsome message\n'

0 commit comments

Comments
 (0)