Files
Archerkattri-ComfyUI-HiCache/tests/test_nodes.py
T
Krishi Attri ebe139e5f5 v0.2.0: GPU-validated inside ComfyUI; copy-on-patch cache safety; single-step reset fix
End-to-end validation on RTX 5090 (ComfyUI 0.24.0, Hunyuan3DWrapper master,
Hunyuan3D-2mini fp16, 30-step image-to-mesh workflow via the ComfyUI API):

- patch engages exactly on schedule (hermite i3: 11 computed + 19 skipped;
  dmd i5: 7 + 23), sampling 16.1 -> 43.1 it/s (2.68x) for hermite i3
- quality A/B same seed vs unaccelerated mesh: hermite i3 F1@0.05 0.825
  (different-seed floor 0.751); dmd i5 0.719, at the floor on mini, so the
  upstream DMD lossless claim is not repeated for this node
- repeat runs in one session reset per-run state correctly

Two bugs found by the validation, both fixed with regression tests:

1. ComfyUI cache aliasing: the node mutated and returned the same pipeline
   object, so a cached node output could silently run a different config
   after an intervening run re-patched the shared pipeline (observed live:
   a cached hermite-i3 output ran dmd-i5). apply/remove now return a
   shallow copy that owns its patch; the input pipeline is never mutated.
2. Run-boundary reset missed back-to-back single-step runs (t=0 then t=0);
   the boundary check is now non-increasing instead of strictly decreasing.

Defaults switch to the best-measured config (hermite, interval 3).
Tests 35 -> 38. README validation section rewritten from measurements;
REGISTRY-SUBMIT.md documents the registry.comfy.org publish flow.
2026-06-11 01:42:19 +00:00

107 lines
3.6 KiB
Python

"""Node-pack registration tests — run standalone, no ComfyUI install.
ComfyUI loads a custom node pack by importing the package and reading
``NODE_CLASS_MAPPINGS`` / ``NODE_DISPLAY_NAME_MAPPINGS``; these tests verify
that contract plus the INPUT_TYPES schema shape ComfyUI expects.
"""
import importlib.util
import pathlib
import sys
import pytest
PACK_DIR = pathlib.Path(__file__).resolve().parents[1]
def _load_pack():
"""Import the node pack the way ComfyUI does (as a package by path)."""
name = "comfyui_hicache_under_test"
if name in sys.modules:
return sys.modules[name]
spec = importlib.util.spec_from_file_location(
name, PACK_DIR / "__init__.py",
submodule_search_locations=[str(PACK_DIR)],
)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
def test_pack_imports_without_comfy():
assert "comfy" not in sys.modules, "test must run without ComfyUI"
pack = _load_pack()
assert hasattr(pack, "NODE_CLASS_MAPPINGS")
assert hasattr(pack, "NODE_DISPLAY_NAME_MAPPINGS")
assert "comfy" not in sys.modules, "node pack must not import comfy internals"
def test_node_mappings_consistent():
pack = _load_pack()
assert "HiCacheAccelerate" in pack.NODE_CLASS_MAPPINGS
for key in pack.NODE_CLASS_MAPPINGS:
assert key in pack.NODE_DISPLAY_NAME_MAPPINGS
def test_input_types_schema():
pack = _load_pack()
cls = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]
schema = cls.INPUT_TYPES()
req = schema["required"]
# model input + the contract from the task spec: method/interval/warmup
assert req["pipeline"][0] == "HY3DMODEL"
assert set(req["method"][0]) == {"hermite", "dmd", "auto"}
assert req["method"][1]["default"] in req["method"][0]
assert req["interval"][0] == "INT"
assert req["interval"][1]["min"] >= 1
assert req["warmup_steps"][0] == "INT"
assert req["warmup_steps"][1]["min"] == 0
assert "enable" in schema["optional"]
# ComfyUI node contract
assert cls.RETURN_TYPES == ("HY3DMODEL",)
assert callable(getattr(cls, cls.FUNCTION))
assert isinstance(cls.CATEGORY, str)
def test_node_function_patches_and_unpatches():
import torch
pack = _load_pack()
node = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]()
class FakePipe:
model = torch.nn.Linear(2, 2)
pipe = FakePipe()
original = pipe.model
(out,) = node.patch(pipe, method="hermite", interval=4, warmup_steps=2)
assert out is not pipe, "node must return a copy, not mutate (ComfyUI caching)"
assert pipe.model is original, "input pipeline must stay unpatched"
assert getattr(out.model, "_hicache_is_patch", False)
assert out.model.inner is original
# re-patch with new params replaces (not nests) the patch
(out2,) = node.patch(out, method="dmd", interval=3, warmup_steps=1)
assert out2.model.inner is original
assert out2.model.method == "dmd" and out2.model.interval == 3
assert out.model.method == "hermite", "earlier output keeps its config"
# enable=False restores the original model
(out3,) = node.patch(out2, enable=False)
assert out3.model is original
# interval=1 means "no caching" -> also unpatched (and an unpatched
# input passes through unchanged)
(out4,) = node.patch(pipe, interval=1)
assert out4 is pipe and out4.model is original
def test_node_rejects_bad_pipeline():
pack = _load_pack()
node = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]()
with pytest.raises(TypeError, match="no `.model` attribute"):
node.patch(object())