Files
ComfyAssets-ComfyUI_PromptM…/tests/test_utils_logging_config.py
T
Vito Sansevero 0393d01746 fix: page image listings in SQL and drop three unused helpers
get_recent_images and search_images_by_prompt take limit/offset so the recent
and search image routes no longer over-fetch a 5000-row window and slice it.
generate_content_hash, PromptModel.migrate_database and find_duplicates had no
callers outside their own tests. The isolated logger tests re-apply their
thread filter after update_config rebuilds the handlers. Long lines in the
touched database and image modules are wrapped so the full flake8 hook passes.
2026-10-08 18:01:34 -07:00

268 lines
11 KiB
Python

"""PromptManagerLogger: memory buffer, file rotation, level changes, truncation."""
import logging
import os
import pathlib
import sys
import tempfile
import threading
import unittest
import unittest.mock as mock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import logging_config
from utils.logging_config import (
MemoryBufferHandler,
PromptManagerLogger,
get_logger,
get_logger_manager,
)
class IsolatedLoggerTestCase(unittest.TestCase):
"""A logger manager whose log directory is a temporary folder."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
package = os.path.join(self.tmp.name, "pkg")
os.makedirs(os.path.join(package, "utils"))
self.log_dir = os.path.join(package, "logs")
fake_file = os.path.join(package, "utils", "logging_config.py")
with mock.patch.object(logging_config, "__file__", fake_file):
self.manager = object.__new__(PromptManagerLogger) # bypass singleton
self.manager.__init__()
self.manager.update_config({"console_logging": False})
# Drop the "Updated logging configuration" entry.
self.manager._log_buffer.clear()
# Other tests leave daemon threads (image monitor, watchdog) logging
# under "prompt_manager.*"; only records from this test's thread may
# reach the buffer or the rotating file (a stray line after a truncate
# would roll the file over and resurrect the backups). update_config
# rebuilds the handlers, so the filter is re-applied after every call.
self._filter_to_this_thread()
original_update = self.manager.update_config
def update_and_refilter(new_config):
original_update(new_config)
self._filter_to_this_thread()
self.manager.update_config = update_and_refilter
self.logger = logging.getLogger("prompt_manager.coverage_test")
self.addCleanup(self._restore_global_logging)
def _filter_to_this_thread(self):
this_thread = threading.get_ident()
for handler in self.manager.logger.handlers:
handler.addFilter(lambda record: record.thread == this_thread)
def _restore_global_logging(self):
for handler in logging.getLogger("prompt_manager").handlers[:]:
handler.close()
get_logger_manager()._setup_loggers()
self.tmp.cleanup()
def _rotate(self, max_file_size=400, backup_count=2, messages=30):
self.manager.update_config(
{"max_file_size": max_file_size, "backup_count": backup_count}
)
for i in range(messages):
self.logger.info("message %03d %s", i, "x" * 80)
class TestSingleton(unittest.TestCase):
def test_manager_and_loggers_are_shared(self):
self.assertIs(PromptManagerLogger(), PromptManagerLogger())
self.assertIs(get_logger_manager(), get_logger_manager())
self.assertIs(
get_logger("prompt_manager.x"), logging.getLogger("prompt_manager.x")
)
self.assertEqual(get_logger().name, "prompt_manager")
def test_init_is_a_noop_on_the_existing_instance(self):
manager = get_logger_manager()
before = manager.config
manager.__init__()
self.assertIs(manager.config, before)
class TestMemoryBuffer(IsolatedLoggerTestCase):
def test_records_are_buffered_most_recent_first(self):
self.logger.info("first")
self.logger.warning("second")
self.logger.error("third")
recent = self.manager.get_recent_logs(limit=2)
self.assertEqual([r["message"] for r in recent], ["third", "second"])
entry = recent[0]
self.assertEqual(entry["level"], "ERROR")
self.assertEqual(entry["logger"], self.logger.name)
self.assertIn("third", entry["formatted"])
for key in ("timestamp", "module", "filename", "lineno", "thread", "process"):
self.assertIn(key, entry)
def test_level_filter(self):
self.logger.info("info")
self.logger.warning("warn")
self.logger.error("err")
self.assertEqual(
[r["message"] for r in self.manager.get_recent_logs(level="warning")],
["err", "warn"],
)
self.assertEqual(len(self.manager.get_recent_logs(level="NOPE")), 3)
def test_buffer_is_capped_at_the_configured_size(self):
for i in range(1005):
self.logger.info("entry %d", i)
stats = self.manager.get_log_stats()
self.assertEqual(stats["buffer_count"], 1000)
self.assertEqual(stats["level_counts"], {"INFO": 1000})
self.assertEqual(stats["current_level"], "INFO")
self.assertEqual(
os.path.normcase(os.path.realpath(stats["log_directory"])),
os.path.normcase(os.path.realpath(self.log_dir)),
)
self.assertEqual(
self.manager.get_recent_logs(limit=1)[0]["message"], "entry 1004"
)
def test_handler_swallows_buffer_errors(self):
manager = mock.Mock()
manager.add_to_buffer.side_effect = RuntimeError("boom")
handler = MemoryBufferHandler(manager)
record = logging.LogRecord("x", logging.INFO, __file__, 1, "msg", (), None)
handler.emit(record) # must not raise
manager.add_to_buffer.assert_called_once()
class TestFilesAndRotation(IsolatedLoggerTestCase):
def test_rotation_creates_backups_and_lists_them_newest_first(self):
self._rotate()
files = self.manager.get_log_files()
names = sorted(f["filename"] for f in files)
self.assertEqual(
names,
["prompt_manager.log", "prompt_manager.log.1", "prompt_manager.log.2"],
)
main = [f for f in files if f["is_main"]]
self.assertEqual(len(main), 1)
self.assertTrue(all(f["size"] > 0 for f in files if not f["is_main"]))
modified = [f["modified"] for f in files]
self.assertEqual(modified, sorted(modified, reverse=True))
self.assertEqual(self.manager.get_log_stats()["log_files_count"], 3)
def test_read_log_file_tail_and_whole(self):
self._rotate()
tail = self.manager.read_log_file("prompt_manager.log.1", lines=1)
self.assertEqual(len(tail), 1)
whole = self.manager.read_log_file("prompt_manager.log.1", lines=0)
self.assertGreater(len(whole), 1)
self.assertTrue(whole[-1].endswith(tail[0]))
def test_read_log_file_rejects_missing_and_outside_paths(self):
with self.assertRaises(FileNotFoundError):
self.manager.read_log_file("missing.log")
outside = os.path.join(self.tmp.name, "pkg", "outside.log")
with open(outside, "w", encoding="utf-8") as f:
f.write("secret\n")
with self.assertRaises(ValueError):
self.manager.read_log_file(os.path.join("..", "outside.log"))
def test_read_log_file_reraises_read_errors(self):
self.logger.info("something")
with mock.patch("builtins.open", side_effect=OSError("disk")):
with self.assertRaises(OSError):
self.manager.read_log_file("prompt_manager.log")
def test_unreadable_files_are_skipped_in_the_listing(self):
self.logger.info("something")
original_stat = pathlib.Path.stat
def stat_failing_for_logs(path, *args, **kwargs):
if path.suffix == ".log":
raise OSError("denied")
return original_stat(path, *args, **kwargs)
with mock.patch.object(pathlib.Path, "stat", autospec=True) as stat:
stat.side_effect = stat_failing_for_logs
self.assertEqual(self.manager.get_log_files(), [])
def test_truncate_does_not_roll_the_log_over_while_deleting_backups(self):
# With a file limit smaller than one record, any log line written
# while the backups are being deleted rolls the main log over and
# resurrects prompt_manager.log.1. The handler's own stream must also
# be reset, or its stale offset reports the old size, rolls over on the
# next record anyway and pads the file with zeros on POSIX.
self._rotate(max_file_size=400, backup_count=2, messages=10)
result = self.manager.truncate_logs()
self.assertEqual(result["errors"], [])
self.logger.info("after truncate")
leftovers = sorted(p.name for p in pathlib.Path(self.log_dir).iterdir())
contents = {
name: pathlib.Path(self.log_dir, name).read_bytes() for name in leftovers
}
self.assertEqual(leftovers, ["prompt_manager.log"], contents)
main_log = pathlib.Path(self.log_dir, "prompt_manager.log").read_bytes()
self.assertNotIn(b"\x00", main_log)
self.assertIn(b"after truncate", main_log)
def test_truncate_clears_main_log_backups_and_buffer(self):
self._rotate()
result = self.manager.truncate_logs()
self.assertEqual(result["errors"], [])
self.assertEqual(
sorted(result["truncated"]),
["prompt_manager.log", "prompt_manager.log.1", "prompt_manager.log.2"],
)
self.assertFalse(
os.path.exists(os.path.join(self.log_dir, "prompt_manager.log.1"))
)
# Only the "Truncated log file" lines written after clearing remain
self.assertEqual(
[
r["message"]
for r in self.manager.get_recent_logs()
if "message 0" in r["message"]
],
[],
)
def test_truncate_reports_files_it_cannot_remove(self):
self._rotate()
with mock.patch.object(pathlib.Path, "unlink", side_effect=OSError("busy")):
result = self.manager.truncate_logs()
self.assertEqual(len(result["errors"]), 2)
self.assertEqual(result["truncated"], ["prompt_manager.log"])
class TestConfiguration(IsolatedLoggerTestCase):
def test_level_change_applies_to_every_prompt_manager_logger(self):
self.manager.update_config({"level": "DEBUG"})
self.assertEqual(logging.getLogger("prompt_manager").level, logging.DEBUG)
self.assertEqual(
logging.getLogger("prompt_manager.database").level, logging.DEBUG
)
self.assertEqual(self.manager.get_config()["level"], "DEBUG")
self.logger.debug("now visible")
self.assertEqual(self.manager.get_recent_logs(limit=1)[0]["level"], "DEBUG")
self.manager.update_config({"level": "bogus"}) # falls back to INFO
self.assertEqual(logging.getLogger("prompt_manager").level, logging.INFO)
def test_get_config_returns_a_copy(self):
config = self.manager.get_config()
config["level"] = "CRITICAL"
self.assertEqual(self.manager.get_config()["level"], "INFO")
def test_handler_flags_rebuild_the_handlers(self):
root = logging.getLogger("prompt_manager")
self.assertFalse(any(type(h) is logging.StreamHandler for h in root.handlers))
self.assertTrue(any(isinstance(h, logging.FileHandler) for h in root.handlers))
self.manager.update_config({"console_logging": True, "file_logging": False})
self.assertTrue(any(type(h) is logging.StreamHandler for h in root.handlers))
self.assertFalse(any(isinstance(h, logging.FileHandler) for h in root.handlers))
self.assertTrue(any(isinstance(h, MemoryBufferHandler) for h in root.handlers))
self.manager.update_config({"console_logging": False})
if __name__ == "__main__":
unittest.main()