Files
olivv-cs-ComfyUI-FunPack/core/tests/test_log.py
T
digital-garbageandClaude Opus 5 74671329f9 log: a severity, a source, and a message
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
    [Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
    [Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
    [Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start

The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.

Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.

Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.

Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.

Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:09:33 +03:00

87 lines
2.7 KiB
Python

"""The log: a severity, a source, and a message.
The format is the contract -- the app will parse it and a person will scan it --
so it is pinned rather than left to drift line by line.
"""
import pytest
from core import log
@pytest.fixture(autouse=True)
def clean():
log._reset()
yield
log._reset()
def test_the_shape_is_label_severity_source_message():
assert log.line("Warning", "FunPack LoRA Loader", "style.safetensors is empty") == (
"[Log] Warning: FunPack LoRA Loader | style.safetensors is empty")
def test_a_message_with_no_source_does_not_print_an_empty_one():
assert log.line("Info", "", "ready") == "[Log] Info: ready"
@pytest.mark.parametrize("fn,level", [
(log.info, "Info"), (log.alert, "Alert"),
(log.warning, "Warning"), (log.critical, "Critical"),
])
def test_each_level_records_itself(fn, level):
fn("somewhere", "something")
assert log.history()[-1] == {
"at": log.history()[-1]["at"], "level": level,
"source": "somewhere", "message": "something"}
def test_it_prints_as_well_as_records(capsys):
log.warning("loader", "a file went missing")
assert "[Log] Warning: loader | a file went missing" in capsys.readouterr().err
def test_history_can_be_filtered_by_level():
log.info("a", "one")
log.warning("b", "two")
log.info("c", "three")
assert [r["source"] for r in log.history(log.INFO)] == ["a", "c"]
def test_the_history_is_bounded():
"""A long rental session must not turn the log into a memory leak."""
for i in range(log.HISTORY + 50):
log.info("loop", str(i))
assert len(log.history()) == log.HISTORY
assert log.history()[-1]["message"] == str(log.HISTORY + 49)
def test_failed_is_a_warning_because_it_means_a_feature_is_absent():
log.failed("some.module", ValueError("bad"))
entry = log.history()[-1]
assert entry["level"] == log.WARNING
assert "ValueError" in entry["message"] and "bad" in entry["message"]
# --- once, and what "once" means -------------------------------------------
def test_once_says_it_a_single_time():
for _ in range(10):
log.once("k", log.ALERT, "src", "inert")
assert len(log.history()) == 1
def test_once_is_per_run_not_per_process():
"""v4 deduped for the life of the interpreter, so a session reported the
first generation that went inert and stayed quiet for every one after."""
log.once("k", log.ALERT, "src", "inert")
log.new_run()
log.once("k", log.ALERT, "src", "inert")
assert len(log.history()) == 2
def test_different_keys_are_independent():
log.once("a", log.ALERT, "src", "one")
log.once("b", log.ALERT, "src", "two")
assert len(log.history()) == 2