""" Logger tests ============ """ import logging import os import pathlib import sys import time import pytest # Used to determine which tests must be skipped LOG_MODE = os.environ.get("KIVY_LOG_MODE", "KIVY") @pytest.fixture def file_handler(): # restores handler to original state from kivy.config import Config log_dir = Config.get("kivy", "log_dir") log_maxfiles = Config.get("kivy", "log_maxfiles") try: yield None finally: Config.set("kivy", "log_dir", log_dir) Config.set("kivy", "log_maxfiles", log_maxfiles) @pytest.mark.parametrize("n", [0, 1, 5]) def test_purge_logs(tmp_path, file_handler, n): from kivy.config import Config from kivy.logger import FileHandler Config.set("kivy", "log_dir", str(tmp_path)) Config.set("kivy", "log_maxfiles", n) # create the default file first so it gets deleted so names match handler = FileHandler() handler._configure() open_file = pathlib.Path(handler.filename).name # wait a little so the timestamps are different for different files time.sleep(0.05) names = [f"log_{i}.txt" for i in range(n + 2)] for name in names: p = tmp_path / name p.write_text("some data") time.sleep(0.05) handler.purge_logs() # files that should have remained after purge expected_names = list(reversed(names))[:n] files = {f.name for f in tmp_path.iterdir()} if open_file in files: # one of the remaining files is the current open log, remove it files.remove(open_file) if len(expected_names) == len(files) + 1: # the open log may or may not have been counted in the remaining # files, remove one from expected to match removed open file expected_names = expected_names[:-1] assert set(expected_names) == files @pytest.mark.xfail # Issue #7986 not yet fixed. def test_logger_history_size(): from kivy.logger import Logger, LoggerHistory for x in range(200): Logger.info(x) assert len(LoggerHistory.history) == 100, "Wrong size: %s" % len( LoggerHistory.history ) @pytest.mark.skipif( LOG_MODE != "KIVY", reason="Requires KIVY_LOG_MODE==KIVY to run.", ) def test_trace_level(): """Kivy logger defines a custom level of Trace.""" from kivy.logger import Logger, LOG_LEVELS, LoggerHistory import logging Logger.setLevel(9) # Try different ways to trigger a trace: Logger.trace("test: This is trace message 1") logging.log(logging.TRACE, "test: This is trace message 2") Logger.log(LOG_LEVELS["trace"], "test: This is trace message 3") # Not supported: # logging.trace('test: This is trace message 4') last_log_records = LoggerHistory.history[:3] assert all(log_record.levelno == 9 for log_record in last_log_records), [ log_record.levelno for log_record in last_log_records ] @pytest.mark.skipif( LOG_MODE == "PYTHON", reason="Requires KIVY_LOG_MODE!=PYTHON to run.", ) def test_trace_level_has_level_name(): from kivy.logger import Logger, LoggerHistory Logger.setLevel(9) Logger.trace("test: This is trace message 1") assert LoggerHistory.history[0].levelname == "TRACE" def test_logging_does_not_deep_copy(): # If the Logger does a deep copy of an uncopyable # data structure, it will fail. See issues #7585 and #7528. import threading from kivy.logger import Logger class UncopyableDatastructure: def __init__(self, name): self._lock = threading.Lock() self._name = name def __str__(self): return "UncopyableDatastructure(name=%r)" % self._name s = UncopyableDatastructure("Uncopyable") Logger.error("The value of s is %s", s) def configured_string_logging(unique_code, formatter=None): """ Helper function provides logger configured to write to log_output. """ from io import StringIO log_output = StringIO() handler = logging.StreamHandler(stream=log_output) if formatter: handler.setFormatter(formatter) logger = logging.getLogger("tests.%s" % unique_code) # Do not escalate to root/Kivy loggers. logger.setLevel(9) # Catch everything logger.propagate = False assert not logger.hasHandlers(), "Must use unique code between tests." logger.addHandler(handler) return logger, log_output def test_colonsplittinglogrecord_with_colon(): from kivy.logger import ColonSplittingLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg="Part1: Part2: Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) # Just making sure we know what it looks like before. assert ( str(originallogrecord) == '' ) shimmedlogrecord = ColonSplittingLogRecord(originallogrecord) assert ( str(shimmedlogrecord) == "' ) def test_colonsplittinglogrecord_without_colon(): from kivy.logger import ColonSplittingLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg="Part1 Part2 Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = ColonSplittingLogRecord(originallogrecord) # No colons means no change. assert str(originallogrecord) == str(shimmedlogrecord) # Try a non-string (Issue #7984) originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg=1, args=None, exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = ColonSplittingLogRecord(originallogrecord) # No colons means no change. assert str(originallogrecord) == str(shimmedlogrecord) def test_uncoloredlogrecord_without_markup(): from kivy.logger import UncoloredLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg="Part1: Part2 Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = UncoloredLogRecord(originallogrecord) # No markup means no change. assert str(originallogrecord) == str(shimmedlogrecord) # Try a non-string (Issue #7984) originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg=1, args=None, exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = UncoloredLogRecord(originallogrecord) assert str(originallogrecord) == str(shimmedlogrecord) def test_uncoloredlogrecord_with_markup(): from kivy.logger import UncoloredLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg="Part1: $BOLDPart2$RESET Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = UncoloredLogRecord(originallogrecord) # No markup means no change. assert ( str(shimmedlogrecord) == '' ) def test_coloredlogrecord_without_markup(): from kivy.logger import ColoredLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg="Part1: Part2 Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = ColoredLogRecord(originallogrecord) # The str() looks the same, because it doesn't include levelname. assert str(originallogrecord) == str(shimmedlogrecord) # But there is a change in the levelname assert originallogrecord.levelname != shimmedlogrecord.levelname assert shimmedlogrecord.levelname == "\x1b[1;36mDEBUG\x1b[0m" # Try a non-string (Issue #7984) originallogrecord = logging.LogRecord( name="kivy.test", level=logging.DEBUG, pathname="test.py", lineno=1, msg=1, args=None, exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = ColoredLogRecord(originallogrecord) assert str(originallogrecord) == str(shimmedlogrecord) def test_coloredlogrecord_with_markup(): from kivy.logger import ColoredLogRecord originallogrecord = logging.LogRecord( name="kivy.test", level=logging.INFO, pathname="test.py", lineno=1, msg="Part1: $BOLDPart2$RESET Part 3", args=("args",), exc_info=None, func="test_colon_splitting", sinfo=None, ) shimmedlogrecord = ColoredLogRecord(originallogrecord) # Bolding has been added to message. assert ( str(shimmedlogrecord) == "' ) # And there is a change in the levelname assert originallogrecord.levelname != shimmedlogrecord.levelname assert shimmedlogrecord.levelname == "\x1b[1;32mINFO\x1b[0m" def test_kivyformatter_colon_no_color(): from kivy.logger import KivyFormatter formatter = KivyFormatter("[%(levelname)-7s] %(message)s", use_color=False) logger, log_output = configured_string_logging("1", formatter) logger.info("Fancy: $BOLDmess$RESETage") assert log_output.getvalue() == "[INFO ] [Fancy ] message\n" def test_kivyformatter_colon_color(): from kivy.logger import KivyFormatter formatter = KivyFormatter("[%(levelname)-18s] %(message)s", use_color=True) logger, log_output = configured_string_logging("2", formatter) logger.info("Fancy: $BOLDmess$RESETage") assert ( log_output.getvalue() == "[\x1b[1;32mINFO\x1b[0m ] [Fancy ] \x1b[1mmess\x1b[0mage\n" ) @pytest.mark.logmodepython @pytest.mark.logmodemixed @pytest.mark.skipif( LOG_MODE == "KIVY", reason="Requires KIVY_LOG_MODE!=KIVY to run.", ) def test_kivy_log_mode_marker_on(): """ This is a test of the pytest markers. This should only be invoked if the environment variable is appropriately set (before pytest is run). Also, tests that kivy.logger paid attention to the environment variable """ assert logging.root.level != 0, "Root logger was modified" @pytest.mark.skipif( LOG_MODE != "KIVY", reason="Requires KIVY_LOG_MODE==KIVY to run.", ) def test_kivy_log_mode_marker_off(): """ This is a test of the pytest markers. This should only be invoked if the environment variable is properly set (before pytest is run). Also, tests that kivy.logger paid attention to the environment variable """ assert logging.root.level == 0, "Root logger was not modified" # These tests are an overly-dramatic simulation of a third-party library's # logging infrastructure. # # A handler is attached to the root-logger, which takes action when a # critical message is logged. class NuclearReactorMonitoringHandler(logging.Handler): NUCLEAR_REACTOR_STATUS = "Nominal" def __init__(self): super().__init__(level=logging.CRITICAL) def emit(self, log_record): # The fact that this was called means a critical log record # was created. sys.stderr.write("Please proceed immediately to the nearest exit.\n") sys.stderr.flush() self.NUCLEAR_REACTOR_STATUS = "Evacuated" def simulate_evacuation(): logging.getLogger().addHandler(logging.StreamHandler()) handler = NuclearReactorMonitoringHandler() logging.getLogger().addHandler(handler) nuclear_core_logger = logging.getLogger("powerstation.core") nuclear_core_logger.info("Core temperature nominal") assert handler.NUCLEAR_REACTOR_STATUS == "Nominal" nuclear_core_logger.critical("Radioactive gas leak") assert handler.NUCLEAR_REACTOR_STATUS == "Evacuated" @pytest.mark.logmodepython @pytest.mark.logmodemixed def test_third_party_handlers_work(): # This should work under any mode. simulate_evacuation() def are_regular_logs_handled(): from kivy.logger import LoggerHistory LoggerHistory.clear_history() logging.getLogger("test").info(1) return bool(LoggerHistory.history) def are_kivy_logger_logs_handled(): from kivy.logger import LoggerHistory, Logger LoggerHistory.clear_history() Logger.info(1) return bool(LoggerHistory.history) def is_stderr_output_handled(): from kivy.logger import LoggerHistory LoggerHistory.clear_history() sys.stderr.write("Test output to stderr\n") sys.stderr.flush() return bool(LoggerHistory.history) @pytest.mark.skipif( LOG_MODE != "KIVY", reason="Requires KIVY_LOG_MODE==KIVY to run.", ) def test_kivy_mode_handlers(): assert are_regular_logs_handled() assert are_kivy_logger_logs_handled() # This line doesn't work because of pytest's handling of stderr. # Runs fine as stand-alone code. # assert is_stderr_output_handled() @pytest.mark.logmodepython @pytest.mark.skipif( LOG_MODE != "PYTHON", reason="Requires KIVY_LOG_MODE==PYTHON to run.", ) def test_python_mode_handlers(): assert not are_regular_logs_handled() assert not are_kivy_logger_logs_handled() assert not is_stderr_output_handled() @pytest.mark.logmodemixed @pytest.mark.skipif( LOG_MODE != "MIXED", reason="Requires KIVY_LOG_MODE==MIXED to run.", ) def test_mixed_mode_handlers(): assert not are_regular_logs_handled() assert are_kivy_logger_logs_handled() assert not is_stderr_output_handled() @pytest.mark.logmodepython @pytest.mark.skipif( LOG_MODE != "PYTHON", reason="Requires KIVY_LOG_MODE==PYTHON to run.", ) def test_logger_fix_8345(): """ The test checks that the ConsoleHandler is not in the Logger handlers list if stderr is None. Test sets stderr to None, if the Console handler is found, the test fails. Pythonw and Pyinstaller 5.7+ (with console set to false) set stderr to None. """ from kivy.logger import Logger, add_kivy_handlers, ConsoleHandler original_sys_stderr = sys.stderr sys.stderr = None add_kivy_handlers(Logger) sys.stderr = original_sys_stderr # restore sys.stderr console_handler_found = any( isinstance(handler, ConsoleHandler) for handler in Logger.handlers ) assert ( not console_handler_found ), "Console handler added, despite sys.stderr being None"