Files
wildminder-ComfyUI-KEEP/tests/test_dependency_decisions.py
T

190 lines
6.9 KiB
Python

"""P0.4 - dependency decision tests.
Two guards, both named by the plan:
* a **grep guard**: the package must contain exactly one diffusers import, and
it must import exactly ``FeedForward`` and ``AdaLayerNorm`` (the two symbols
that P2.2 vendors from diffusers 0.11.x);
* an **xfail guard** "flownet path is wired": the architecture config that is
handed to the KEEP arch must resolve a non-``None`` ``flownet_path``.
It is marked ``xfail(strict=False)`` because it is expected to fail until
**P5.5** wires the GMFlow resolution (the flip owner recorded in the
reason string and in the audit document).
"""
from __future__ import annotations
import ast
import logging
import sys
import types
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
PACKAGE_DIR = REPO_ROOT / "modules"
UTILS_FILE = PACKAGE_DIR / "utils.py"
AUDIT_DOC = REPO_ROOT / "docs" / "audit" / "2026-10-22-dependency-decisions.md"
EXPECTED_DIFFUSERS_SYMBOLS = {"FeedForward", "AdaLayerNorm"}
def _diffusers_imports() -> list[tuple[str, int, set[str]]]:
"""Every diffusers import in the package: (file, line, imported names)."""
found = []
for path in sorted(PACKAGE_DIR.rglob("*.py")):
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
except (SyntaxError, UnicodeDecodeError):
continue
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.module and node.module.split(".")[0] == "diffusers":
names = {alias.name for alias in node.names}
found.append((path.relative_to(REPO_ROOT).as_posix(), node.lineno, names))
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.split(".")[0] == "diffusers":
found.append(
(path.relative_to(REPO_ROOT).as_posix(), node.lineno, {alias.name})
)
return found
@pytest.fixture(scope="module")
def utils_module():
"""Execute ``modules/utils.py`` the way the package does.
The module needs two ComfyUI symbols (``folder_paths`` and
``comfy.utils.ProgressBar``) and the parent package's ``logger``. Only
those *ComfyUI* seams are stubbed - the vendored dependencies are **not**,
so the real ``from re import T`` failure still happens and the real
fallback behaviour is observed.
"""
saved_path = list(sys.path)
saved_modules = dict(sys.modules)
folder_paths = types.ModuleType("folder_paths")
folder_paths.models_dir = str(REPO_ROOT / "models")
comfy = types.ModuleType("comfy")
comfy_utils = types.ModuleType("comfy.utils")
class ProgressBar: # minimal stand-in for comfy.utils.ProgressBar
def __init__(self, total=None):
self.total = total
def update(self, value=1):
return None
comfy_utils.ProgressBar = ProgressBar
comfy.utils = comfy_utils
parent_name = "keep_p0_probe"
parent = types.ModuleType(parent_name)
parent.__path__ = [str(REPO_ROOT)] # type: ignore[attr-defined]
parent.logger = logging.getLogger(parent_name)
package_name = f"{parent_name}.modules"
package = types.ModuleType(package_name)
package.__path__ = [str(PACKAGE_DIR)] # type: ignore[attr-defined]
child_name = f"{package_name}.utils"
child = types.ModuleType(child_name)
child.__file__ = str(UTILS_FILE)
child.__package__ = package_name
child.__path__ = [str(PACKAGE_DIR)] # type: ignore[attr-defined]
sys.modules["folder_paths"] = folder_paths
sys.modules["comfy"] = comfy
sys.modules["comfy.utils"] = comfy_utils
sys.modules[parent_name] = parent
sys.modules[package_name] = package
sys.modules[child_name] = child
try:
source = UTILS_FILE.read_text(encoding="utf-8")
exec(compile(source, str(UTILS_FILE), "exec"), child.__dict__)
yield child
finally:
sys.path[:] = saved_path
sys.modules.clear()
sys.modules.update(saved_modules)
@pytest.mark.unit
def test_no_diffusers_import_remains_in_the_vendored_tree():
"""P2.2 flipped this guard: **no** diffusers import may remain.
The two symbols are vendored byte-for-byte in
``modules/deps/basicsr/archs/_diffusers_compat.py``; the vendored tree
must not depend on diffusers any more.
"""
imports = _diffusers_imports()
assert imports == [], f"no diffusers import may remain, got {imports}"
@pytest.mark.unit
def test_the_vendored_compat_module_provides_the_expected_symbols():
"""The vendored module must define exactly the two planned symbols."""
path = (
REPO_ROOT
/ "modules"
/ "deps"
/ "basicsr"
/ "archs"
/ "_diffusers_compat.py"
)
assert path.is_file(), f"the vendored compat module is missing: {path}"
source = path.read_text(encoding="utf-8")
for symbol in sorted(EXPECTED_DIFFUSERS_SYMBOLS):
assert f"class {symbol}(nn.Module):" in source, symbol
# the attribution header must name the upstream project and version
assert "diffusers 0.11.0" in source, (
"the attribution header must name diffusers 0.11.0"
)
assert "Apache License" in source, (
"the attribution header must name the Apache-2.0 license"
)
# and the architecture must use the vendored copy
keep_arch = (
REPO_ROOT / "modules" / "deps" / "basicsr" / "archs" / "keep_arch.py"
).read_text(encoding="utf-8")
assert "from ._diffusers_compat import FeedForward, AdaLayerNorm" in keep_arch, (
"keep_arch.py must import the vendored classes"
)
@pytest.mark.unit
def test_audit_doc_records_the_diffusers_decision():
text = AUDIT_DOC.read_text(encoding="utf-8")
assert "FeedForward" in text and "AdaLayerNorm" in text
assert "0.11" in text, "the diffusers version must be recorded"
@pytest.mark.xfail(
strict=False,
reason=(
"flownet_path is not wired yet (modules/utils.py sets it to None) - "
"flip owner: P5.5, which resolves the GMFlow manifest entry and sets "
"architecture['flownet_path'] before the arch is built"
),
)
@pytest.mark.unit
def test_flownet_path_is_wired(utils_module):
"""The arch config must resolve a real flownet path (flip owner: P5.5)."""
architecture = utils_module.KEEP_MODEL_CONFIGS["KEEP"]["architecture"]
assert architecture["flownet_path"] is not None, (
"flownet_path is None - the GMFlow net would keep its random weights"
)
@pytest.mark.unit
def test_flownet_is_built_by_the_arch(utils_module):
"""The vendored arch builds the flownet from the config (code read check)."""
arch = REPO_ROOT / "modules" / "deps" / "basicsr" / "archs" / "keep_arch.py"
source = arch.read_text(encoding="utf-8")
assert "self.flownet = FlowGenerator(path=flownet_path)" in source
assert "flows = self.get_flow(x)" in source