Merge pull request #7231 from bluetech/logging-error
logging: propagate errors during log message emits
This commit is contained in:
		
						commit
						919ac2239d
					
				| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					If an error is encountered while formatting the message in a logging call, for
 | 
				
			||||||
 | 
					example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is
 | 
				
			||||||
 | 
					missing), pytest now propagates the error, likely causing the test to fail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Previously, such a mistake would cause an error to be printed to stderr, which
 | 
				
			||||||
 | 
					is not displayed by default for passing tests. This change makes the mistake
 | 
				
			||||||
 | 
					visible during testing.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may supress this behavior temporarily or permanently by setting
 | 
				
			||||||
 | 
					``logging.raiseExceptions = False``.
 | 
				
			||||||
| 
						 | 
					@ -312,6 +312,14 @@ class LogCaptureHandler(logging.StreamHandler):
 | 
				
			||||||
        self.records = []
 | 
					        self.records = []
 | 
				
			||||||
        self.stream = StringIO()
 | 
					        self.stream = StringIO()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handleError(self, record: logging.LogRecord) -> None:
 | 
				
			||||||
 | 
					        if logging.raiseExceptions:
 | 
				
			||||||
 | 
					            # Fail the test if the log message is bad (emit failed).
 | 
				
			||||||
 | 
					            # The default behavior of logging is to print "Logging error"
 | 
				
			||||||
 | 
					            # to stderr with the call stack and some extra details.
 | 
				
			||||||
 | 
					            # pytest wants to make such mistakes visible during testing.
 | 
				
			||||||
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LogCaptureFixture:
 | 
					class LogCaptureFixture:
 | 
				
			||||||
    """Provides access and control of log capturing."""
 | 
					    """Provides access and control of log capturing."""
 | 
				
			||||||
| 
						 | 
					@ -499,9 +507,7 @@ class LoggingPlugin:
 | 
				
			||||||
        # File logging.
 | 
					        # File logging.
 | 
				
			||||||
        self.log_file_level = get_log_level_for_setting(config, "log_file_level")
 | 
					        self.log_file_level = get_log_level_for_setting(config, "log_file_level")
 | 
				
			||||||
        log_file = get_option_ini(config, "log_file") or os.devnull
 | 
					        log_file = get_option_ini(config, "log_file") or os.devnull
 | 
				
			||||||
        self.log_file_handler = logging.FileHandler(
 | 
					        self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
 | 
				
			||||||
            log_file, mode="w", encoding="UTF-8"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        log_file_format = get_option_ini(config, "log_file_format", "log_format")
 | 
					        log_file_format = get_option_ini(config, "log_file_format", "log_format")
 | 
				
			||||||
        log_file_date_format = get_option_ini(
 | 
					        log_file_date_format = get_option_ini(
 | 
				
			||||||
            config, "log_file_date_format", "log_date_format"
 | 
					            config, "log_file_date_format", "log_date_format"
 | 
				
			||||||
| 
						 | 
					@ -687,6 +693,16 @@ class LoggingPlugin:
 | 
				
			||||||
        self.log_file_handler.close()
 | 
					        self.log_file_handler.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _FileHandler(logging.FileHandler):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Custom FileHandler with pytest tweaks.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handleError(self, record: logging.LogRecord) -> None:
 | 
				
			||||||
 | 
					        # Handled by LogCaptureHandler.
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
 | 
					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
 | 
				
			||||||
| 
						 | 
					@ -737,6 +753,10 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
 | 
				
			||||||
                self._section_name_shown = True
 | 
					                self._section_name_shown = True
 | 
				
			||||||
            super().emit(record)
 | 
					            super().emit(record)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handleError(self, record: logging.LogRecord) -> None:
 | 
				
			||||||
 | 
					        # Handled by LogCaptureHandler.
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _LiveLoggingNullHandler(logging.NullHandler):
 | 
					class _LiveLoggingNullHandler(logging.NullHandler):
 | 
				
			||||||
    """A handler used when live logging is disabled."""
 | 
					    """A handler used when live logging is disabled."""
 | 
				
			||||||
| 
						 | 
					@ -746,3 +766,7 @@ class _LiveLoggingNullHandler(logging.NullHandler):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_when(self, when):
 | 
					    def set_when(self, when):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handleError(self, record: logging.LogRecord) -> None:
 | 
				
			||||||
 | 
					        # Handled by LogCaptureHandler.
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					from _pytest.pytester import Testdir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_nothing_logged(testdir):
 | 
					def test_nothing_logged(testdir):
 | 
				
			||||||
| 
						 | 
					@ -1101,3 +1102,48 @@ def test_colored_ansi_esc_caplogtext(testdir):
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    result = testdir.runpytest("--log-level=INFO", "--color=yes")
 | 
					    result = testdir.runpytest("--log-level=INFO", "--color=yes")
 | 
				
			||||||
    assert result.ret == 0
 | 
					    assert result.ret == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_logging_emit_error(testdir: Testdir) -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    An exception raised during emit() should fail the test.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The default behavior of logging is to print "Logging error"
 | 
				
			||||||
 | 
					    to stderr with the call stack and some extra details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pytest overrides this behavior to propagate the exception.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    testdir.makepyfile(
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def test_bad_log():
 | 
				
			||||||
 | 
					            logging.warning('oops', 'first', 2)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    result = testdir.runpytest()
 | 
				
			||||||
 | 
					    result.assert_outcomes(failed=1)
 | 
				
			||||||
 | 
					    result.stdout.fnmatch_lines(
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "====* FAILURES *====",
 | 
				
			||||||
 | 
					            "*not all arguments converted during string formatting*",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_logging_emit_error_supressed(testdir: Testdir) -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    If logging is configured to silently ignore errors, pytest
 | 
				
			||||||
 | 
					    doesn't propagate errors either.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    testdir.makepyfile(
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def test_bad_log(monkeypatch):
 | 
				
			||||||
 | 
					            monkeypatch.setattr(logging, 'raiseExceptions', False)
 | 
				
			||||||
 | 
					            logging.warning('oops', 'first', 2)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    result = testdir.runpytest()
 | 
				
			||||||
 | 
					    result.assert_outcomes(passed=1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue