Files
wildminder-ComfyUI-Chatterbox/tests/test_init.py
T
WildAi 896c6c34df feat(ui): toast node errors in the web ui
Core app logged execution_error without a toast; startup note dropped.
2026-10-14 13:26:27 +03:00

373 lines
16 KiB
Python

"""S6.5 -- the repository-root entry point.
A V3 custom-node package has exactly one public name: ``comfy_entrypoint``.
Everything else in ``__init__.py`` is bootstrap -- register the folders, scan the
models, wire the routes -- and has to survive being imported in a process that
has no ComfyUI server, because that is what a headless run and this test suite
look like.
The pytest guard is the other half. pytest imports every ``__init__.py`` it
meets during collection, and imports it *parentless*: the relative imports
below would fail with "attempted relative import with no known parent package"
before a single test ran. Under pytest the module therefore exports nothing and
returns. That is why the functional half of this file re-executes the source
with pytest temporarily out of ``sys.modules``, against fake ``folder_paths`` and
``server`` modules, rather than trusting the guarded import.
"""
from __future__ import annotations
import ast
import importlib.util
import logging
import os
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
import conftest
from conftest import PKG_ALIAS, ROOT_DIR
INIT_PATH = os.path.join(ROOT_DIR, "__init__.py")
def read_init_source() -> str:
with open(INIT_PATH, "r", encoding="utf-8") as handle:
return handle.read()
@pytest.fixture(scope="module")
def source() -> str:
return read_init_source()
@pytest.fixture(scope="module")
def tree(source: str) -> ast.Module:
return ast.parse(source, filename=INIT_PATH)
def _assigned_names(tree: ast.Module) -> dict[str, ast.AST]:
"""Every ``NAME = ...`` assignment anywhere in the module, by name."""
assigned: dict[str, ast.AST] = {}
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
assigned[target.id] = node.value
return assigned
def _referenced_names(tree: ast.Module) -> set[str]:
"""Every identifier the module binds or reads, as code.
Deliberately an AST walk rather than a substring search: this file's own
docstrings *name* the artifacts it refuses to declare, so a text scan
reports the documentation as the defect.
"""
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Name):
names.add(node.id)
elif isinstance(node, ast.Attribute):
names.add(node.attr)
return names
def _first_statement(tree: ast.Module) -> ast.stmt:
"""The first *executable* statement, skipping the docstring and imports.
Importing ``sys`` before the guard is fine -- it is the guard's own
subject. What must not precede it is any bootstrap side effect.
"""
for node in tree.body:
if isinstance(node, (ast.Import, ast.ImportFrom)):
continue
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
continue # the module docstring
return node
raise AssertionError("the entry point has no executable statements")
class TestExports:
def test_all_is_exactly_the_entrypoint(self, tree: ast.Module) -> None:
"""One export. A V1 package exported two mapping dicts instead."""
value = _assigned_names(tree)["__all__"]
exported = [ast.literal_eval(element) for element in value.elts]
assert exported == ["comfy_entrypoint"]
def test_the_v1_mappings_are_gone(self, tree: ast.Module) -> None:
referenced = _referenced_names(tree)
assert "NODE_CLASS_MAPPINGS" not in referenced
assert "NODE_DISPLAY_NAME_MAPPINGS" not in referenced
def test_web_directory_is_declared_and_shipped(self, tree: ast.Module) -> None:
"""The web directory the host is told to serve is there and holds the JS.
The guard this replaces was its mirror image: the package deliberately
declared no ``WEB_DIRECTORY``, because pointing the host at a directory
that does not exist warns on every boot and makes "we ship a frontend"
a claim nothing backs up. Both halves still have to hold -- declared
**and** present -- so the check stays, inverted. Resolving the way the
host does, against this module's own directory rather than the process
working directory, is what makes the ``isdir`` half a real assertion
instead of a guess about where the server was started.
The attribute belongs to the module, not to the extension object;
``tests/test_extension.py`` is what keeps the object clean of it.
"""
assigned = _assigned_names(tree)
assert "WEB_DIRECTORY" in assigned, (
"the entry point declares no web directory, so js/extension.js is "
"never served and no node error is ever toasted"
)
web_dir = ast.literal_eval(assigned["WEB_DIRECTORY"])
assert web_dir == "./js"
resolved = os.path.abspath(os.path.join(os.path.dirname(INIT_PATH), web_dir))
assert os.path.isdir(resolved), f"{web_dir} does not resolve to a directory: {resolved}"
assert os.path.isfile(os.path.join(resolved, "extension.js")), (
"the served directory has no extension.js, so the browser imports "
"nothing and the toast listener never registers"
)
class TestPytestGuard:
def test_under_pytest_the_module_exports_nothing(self) -> None:
module = sys.modules[PKG_ALIAS]
assert module.__all__ == []
assert not hasattr(module, "comfy_entrypoint"), (
"the entry point ran its bootstrap during collection; under pytest "
"it must return before touching folder_paths or the server"
)
def test_the_guard_is_the_first_thing_the_module_does(self, tree: ast.Module) -> None:
first = _first_statement(tree)
assert isinstance(first, ast.If)
rendered = ast.unparse(first.test)
assert "pytest" in rendered and "sys.modules" in rendered
def test_the_guard_has_an_empty_exports_branch(self, tree: ast.Module) -> None:
first = _first_statement(tree)
assert isinstance(first, ast.If)
guarded = first.body
assert len(guarded) == 1
statement = guarded[0]
assert isinstance(statement, (ast.Assign, ast.AnnAssign))
assert ast.literal_eval(statement.value) == []
class TestBootstrapRunsForReal:
"""Execute the module the way ComfyUI does, minus the server.
pytest is removed from ``sys.modules`` for the duration so the guard opens,
and ``folder_paths`` / ``server`` are replaced by stand-ins so nothing in
this process's real ComfyUI registry is mutated. ``sys.path`` is snapshotted
and restored because the vendor loader appends to it by design.
"""
@pytest.fixture
def loaded(self, monkeypatch):
fake_folder_paths = SimpleNamespace(
models_dir=os.path.join(ROOT_DIR, ".test-models"),
folder_names_and_paths={},
supported_pt_extensions={".safetensors"},
get_folder_paths=lambda name: [],
add_model_folder_path=lambda *a, **k: None,
get_user_directory=lambda: os.path.join(ROOT_DIR, ".test-user"),
)
fake_server = SimpleNamespace(
PromptServer=SimpleNamespace(instance=SimpleNamespace(routes=MagicMock()))
)
monkeypatch.setitem(sys.modules, "folder_paths", fake_folder_paths)
monkeypatch.setitem(sys.modules, "server", fake_server)
# The guard keys off pytest being importable; hide it for this exec only.
monkeypatch.delitem(sys.modules, "pytest")
spec = importlib.util.spec_from_file_location(
f"{PKG_ALIAS}._entrypoint_probe",
INIT_PATH,
submodule_search_locations=[ROOT_DIR],
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, module)
path_before = list(sys.path)
try:
spec.loader.exec_module(module)
yield module, fake_folder_paths, fake_server
finally:
for entry in list(sys.path):
if entry not in path_before:
sys.path.remove(entry)
def test_the_entrypoint_is_exported(self, loaded) -> None:
module, _folder_paths, _server = loaded
assert module.__all__ == ["comfy_entrypoint"]
assert callable(module.comfy_entrypoint)
def test_folders_are_registered(self, loaded) -> None:
_module, folder_paths_module, _server = loaded
registered = folder_paths_module.folder_names_and_paths
assert "tts" in registered
assert "chatterbox" in registered
def test_the_chatterbox_root_is_under_tts(self, loaded) -> None:
_module, folder_paths_module, _server = loaded
registered = folder_paths_module.folder_names_and_paths
assert registered["tts"][0][-1].endswith(os.path.join("tts"))
def test_routes_are_registered_when_a_server_exists(self, loaded) -> None:
_module, _folder_paths, server = loaded
assert server.PromptServer.instance.routes is not None
def test_module_source_does_not_mutate_sys_modules(self, source: str) -> None:
assert "sys.modules[" not in source
assert "sys.path.insert" not in source
class TestStartupIsQuiet:
"""The bootstrap says nothing at INFO or above.
The pack used to log ``[Chatterbox TTS] Registering the Chatterbox custom
node.`` on every boot. Other extensions chat just as loudly, so the host's
own startup output was buried under a wall of "look at me" -- and the note
told the operator nothing: a silent import succeeded exactly as well as a
chatty one. What survives is one ``debug`` pack-count line, invisible at the
default level.
The re-exec is the same one :class:`TestBootstrapRunsForReal` performs:
pytest out of ``sys.modules`` so the guard opens, fake ``folder_paths`` /
``server`` so nothing real is registered. ``caplog`` is attached to the root
logger first, so the records this fixture collects are exactly the ones the
bootstrap emitted while being imported -- and they are captured explicitly,
because ``caplog`` resets its handler when the *call* phase begins, after
this fixture has already run.
"""
@pytest.fixture
def loaded(self, monkeypatch, caplog):
fake_folder_paths = SimpleNamespace(
models_dir=os.path.join(ROOT_DIR, ".test-models"),
folder_names_and_paths={},
supported_pt_extensions={".safetensors"},
get_folder_paths=lambda name: [],
add_model_folder_path=lambda *a, **k: None,
get_user_directory=lambda: os.path.join(ROOT_DIR, ".test-user"),
)
fake_server = SimpleNamespace(
PromptServer=SimpleNamespace(instance=SimpleNamespace(routes=MagicMock()))
)
monkeypatch.setitem(sys.modules, "folder_paths", fake_folder_paths)
monkeypatch.setitem(sys.modules, "server", fake_server)
# The guard keys off pytest being importable; hide it for this exec only.
monkeypatch.delitem(sys.modules, "pytest")
# Before the exec, so the caller can recount what the bootstrap emitted.
caplog.set_level(logging.INFO)
spec = importlib.util.spec_from_file_location(
f"{PKG_ALIAS}._entrypoint_probe_quiet",
INIT_PATH,
submodule_search_locations=[ROOT_DIR],
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, module)
path_before = list(sys.path)
try:
spec.loader.exec_module(module)
captured = list(caplog.get_records("setup"))
yield module, fake_folder_paths, fake_server, captured
finally:
for entry in list(sys.path):
if entry not in path_before:
sys.path.remove(entry)
def test_importing_the_entry_point_emits_no_info_or_above(self, loaded) -> None:
"""A startup note is a regression; this is where it comes back to fail.
The silence has to be pinned by an assertion that can fail. The removed
``logging.info`` call was one line in this module, and nothing else in
the suite looks at what the bootstrap says on the console.
"""
_module, _folder_paths, _server, captured = loaded
offenders = [
f"{record.name}:{record.levelname}: {record.getMessage()}"
for record in captured
if record.levelno >= logging.INFO
]
assert offenders == [], (
"the bootstrap still speaks at INFO or above; an install-time note "
"is exactly what the quiet start removed:\n " + "\n ".join(offenders)
)
def test_the_bootstrap_still_does_its_job(self, loaded) -> None:
"""Silence is not the goal; a silent, working import is.
The quiet fixture replays the real bootstrap, so the registration it
performs has to be as real as in :class:`TestBootstrapRunsForReal` --
otherwise the silence above could be the sound of a bootstrap that broke
early and logged nothing because it did nothing.
"""
_module, folder_paths_module, _server, _captured = loaded
registered = folder_paths_module.folder_names_and_paths
assert "tts" in registered
assert "chatterbox" in registered
class TestAbsentServerDegrades:
"""A process with no ``PromptServer`` still has to import cleanly."""
@pytest.fixture
def loaded_without_server(self, monkeypatch):
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(
models_dir=os.path.join(ROOT_DIR, ".test-models"),
folder_names_and_paths={},
supported_pt_extensions={".safetensors"},
get_folder_paths=lambda name: [],
add_model_folder_path=lambda *a, **k: None,
get_user_directory=lambda: os.path.join(ROOT_DIR, ".test-user"),
))
monkeypatch.setitem(sys.modules, "server", None)
monkeypatch.delitem(sys.modules, "pytest")
spec = importlib.util.spec_from_file_location(
f"{PKG_ALIAS}._entrypoint_probe_noserver",
INIT_PATH,
submodule_search_locations=[ROOT_DIR],
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, module)
path_before = list(sys.path)
try:
spec.loader.exec_module(module)
yield module
finally:
for entry in list(sys.path):
if entry not in path_before:
sys.path.remove(entry)
def test_the_entrypoint_is_still_exported(self, loaded_without_server) -> None:
module = loaded_without_server
assert module.__all__ == ["comfy_entrypoint"]
assert callable(module.comfy_entrypoint)
class TestSourceHygiene:
def test_the_module_owns_no_logging_handler(self, source: str) -> None:
"""ComfyUI installs its own ``ColoredFormatter`` on the root logger."""
for forbidden in (
"addHandler",
"setLevel",
"logging.Formatter",
"logging.StreamHandler",
"logging.Logger(",
"LoggerAdapter",
):
assert forbidden not in source, (
f"{forbidden} is a custom-logging artifact; the package owns "
f"only its message prefix"
)
def test_the_module_is_reachable_as_the_alias(self) -> None:
assert conftest.PKG_ALIAS in sys.modules
assert sys.modules[PKG_ALIAS].__file__ in (INIT_PATH, os.path.abspath(INIT_PATH))