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.capture_manager.suspend_global_capture()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if not self._first_record_emitted:
 | 
				
			||||||
                self.stream.write('\n')
 | 
					                self.stream.write('\n')
 | 
				
			||||||
            # we might consider adding a header at this point using self.stream.sep('-', 'live log') or something
 | 
					                # we might consider adding a header at this point using self.stream.section('live log', sep='-')
 | 
				
			||||||
            # similar when we improve live logging output
 | 
					                # or something similar when we improve live logging output
 | 
				
			||||||
                self._first_record_emitted = True
 | 
					                self._first_record_emitted = True
 | 
				
			||||||
            logging.StreamHandler.emit(self, record)
 | 
					            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