mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-29 01:16:39 +08:00
Addresses review feedback: the V1 executability check fell back to constructing the node (class_def()) when the FUNCTION method wasn't found on the class. That runs __init__ during validation, so a constructor's side effects or failure could be misreported as invalid_node_definition for an otherwise valid node. Inspect only the class. No core/extra node defines its FUNCTION method on the instance, so this loses no real coverage while removing the side-effect risk. Replace the instance-fallback test with one asserting a node with a raising __init__ but a valid class-level method still passes validation (i.e. it is never instantiated).
138 lines
3.7 KiB
Python
138 lines
3.7 KiB
Python
"""Tests for pre-execution validation that a node is actually executable.
|
|
|
|
validate_prompt rejects a node whose declared entry point does not resolve to a
|
|
real method (a V1 FUNCTION typo, or a V3 node missing its execute override) before
|
|
any node runs, attributing the error to the offending node.
|
|
"""
|
|
import asyncio
|
|
|
|
import nodes
|
|
from comfy_api.latest import io
|
|
from execution import node_not_executable_reason, validate_prompt
|
|
|
|
|
|
class _GoodV1Node:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "run"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "Test"
|
|
|
|
def run(self):
|
|
return (None,)
|
|
|
|
|
|
class _TypoV1Node:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "invert" # method below is misspelled
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "Test"
|
|
|
|
def invvert(self):
|
|
return (None,)
|
|
|
|
|
|
class _SideEffectInitV1Node:
|
|
"""Valid class-level method, but a constructor that must never run in validation."""
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "run"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "Test"
|
|
|
|
def __init__(self):
|
|
raise RuntimeError("__init__ must not run during validation")
|
|
|
|
def run(self):
|
|
return (None,)
|
|
|
|
|
|
def _v3_schema(node_id):
|
|
return io.Schema(
|
|
node_id=node_id,
|
|
display_name=node_id,
|
|
category="Test",
|
|
inputs=[],
|
|
outputs=[io.Image.Output()],
|
|
is_output_node=True,
|
|
)
|
|
|
|
|
|
class _GoodV3Node(io.ComfyNode):
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return _v3_schema("GoodV3Node")
|
|
|
|
@classmethod
|
|
def execute(cls):
|
|
return io.NodeOutput(None)
|
|
|
|
|
|
class _TypoV3Node(io.ComfyNode):
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return _v3_schema("TypoV3Node")
|
|
|
|
@classmethod
|
|
def exicute(cls): # typo: should be "execute"
|
|
return io.NodeOutput(None)
|
|
|
|
|
|
def _register(class_type, class_def):
|
|
nodes.NODE_CLASS_MAPPINGS[class_type] = class_def
|
|
|
|
|
|
def _validate(class_type):
|
|
prompt = {"1": {"class_type": class_type, "inputs": {}}}
|
|
return asyncio.run(validate_prompt("pid", prompt, None))
|
|
|
|
|
|
def test_good_node_passes():
|
|
_register("GoodV1Node", _GoodV1Node)
|
|
assert node_not_executable_reason(_GoodV1Node, "GoodV1Node") is None
|
|
valid, _, _, _ = _validate("GoodV1Node")
|
|
assert valid is True
|
|
|
|
|
|
def test_typo_node_rejected_with_node_error():
|
|
_register("TypoV1Node", _TypoV1Node)
|
|
valid, error, _, node_errors = _validate("TypoV1Node")
|
|
assert valid is False
|
|
assert error["type"] == "invalid_node_definition"
|
|
assert node_errors["1"]["class_type"] == "TypoV1Node"
|
|
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"
|
|
assert "invert" in node_errors["1"]["errors"][0]["details"]
|
|
|
|
|
|
def test_validation_does_not_instantiate_node():
|
|
"""A valid node is not constructed during validation, so __init__ never runs."""
|
|
_register("SideEffectInitV1Node", _SideEffectInitV1Node)
|
|
assert node_not_executable_reason(_SideEffectInitV1Node, "SideEffectInitV1Node") is None
|
|
valid, _, _, _ = _validate("SideEffectInitV1Node")
|
|
assert valid is True
|
|
|
|
|
|
def test_good_v3_node_passes():
|
|
_register("GoodV3Node", _GoodV3Node)
|
|
assert node_not_executable_reason(_GoodV3Node, "GoodV3Node") is None
|
|
valid, _, _, _ = _validate("GoodV3Node")
|
|
assert valid is True
|
|
|
|
|
|
def test_typo_v3_node_rejected_with_node_error():
|
|
_register("TypoV3Node", _TypoV3Node)
|
|
valid, error, _, node_errors = _validate("TypoV3Node")
|
|
assert valid is False
|
|
assert error["type"] == "invalid_node_definition"
|
|
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"
|