Files
1hew-ComfyUI-1hewNodes/tests/test_multi_switch_select.py
1hew 161b87e870 feat(release): 发布 v4.1.0,新增 Any to Any 与计数节点,增强 IO 与动态端口
- feat(conversion): 新增 Any to Any 通配透传节点
- feat(int): 新增 Int Mask/Audio/Text Count,并统一计数共享层
- feat(logic): Multi Switch Select 支持 a/all 全选
- feat(io): Load PS 预览默认开启并抑制 psd_tools INFO 噪音;Save Audio 默认 mp3,播放器兼容 Nodes 2.0
- fix(multi): Multi String Join 的 string_1 改为纯插槽,修复动态端口错位
- refactor(shared): 复用 parse_pad_color / pad_to_rgb
- docs: 同步中英文节点文档、README 列表与 v4.1.0 日志
- test: 新增/更新相关回归测试
2026-10-23 22:21:26 +08:00

294 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Regression tests for Multi Switch Select and the shared Image Batch Index grammar."""
import asyncio
import importlib
import sys
import types
from pathlib import Path
def _install_comfy_stub():
class _PortType:
def __init__(self, name):
self.name = name
def Input(self, id, *args, **kwargs):
return {"kind": "input", "type": self.name, "id": id, **kwargs}
def Output(self, *args, **kwargs):
return {"kind": "output", "type": self.name, **kwargs}
port_types = {
"Image": "IMAGE",
"Mask": "MASK",
"Int": "INT",
"Float": "FLOAT",
"Boolean": "BOOLEAN",
"String": "STRING",
"Combo": "COMBO",
}
io = types.SimpleNamespace(
ComfyNode=object,
Schema=lambda **kwargs: kwargs,
NodeOutput=lambda *values, **kwargs: values,
)
for name, io_type in port_types.items():
setattr(io, name, _PortType(io_type))
io.Custom = lambda name: _PortType(name)
latest = types.ModuleType("comfy_api.latest")
latest.io = io
latest.ui = types.SimpleNamespace()
latest.ComfyExtension = object
package = types.ModuleType("comfy_api")
package.latest = latest
sys.modules["comfy_api"] = package
sys.modules["comfy_api.latest"] = latest
def _load_nodes():
_install_comfy_stub()
root = Path(__file__).parents[1]
package_name = "range_nodes_under_test"
top = types.ModuleType(package_name)
top.__path__ = [str(root)]
nodes = types.ModuleType(package_name + ".nodes")
nodes.__path__ = [str(root / "nodes")]
batch = types.ModuleType(package_name + ".nodes.batch")
batch.__path__ = [str(root / "nodes" / "batch")]
logic = types.ModuleType(package_name + ".nodes.logic")
logic.__path__ = [str(root / "nodes" / "logic")]
utils = types.ModuleType(package_name + ".utils")
utils.__path__ = [str(root / "utils")]
sys.modules.update(
{
package_name: top,
package_name + ".nodes": nodes,
package_name + ".nodes.batch": batch,
package_name + ".nodes.logic": logic,
package_name + ".utils": utils,
}
)
multi = importlib.import_module(
package_name + ".nodes.logic.multi_switch_select"
).MultiSwitchSelect
batch_index = importlib.import_module(
package_name + ".nodes.batch.image_batch_index"
).ImageBatchIndex
return multi, batch_index
MULTI, IBI = _load_nodes()
def test_schema_exposes_string_select_and_lazy_inputs():
schema = MULTI.define_schema()
select = schema["inputs"][0]
assert select["id"] == "select"
assert select["type"] == "STRING"
assert select["default"] == "1"
dynamic = [port for port in schema["inputs"] if port["id"].startswith("input_")]
assert len(dynamic) == 10
assert all(port.get("lazy") is True for port in dynamic)
def test_parses_lists_and_loose_separators():
assert MULTI._selected_indices("1,2,4") == [1, 2, 4]
assert MULTI._selected_indices(" 4 , 1 , 2 ") == [1, 2, 4]
assert MULTI._selected_indices("4,2;1 3、") == [1, 2, 3, 4]
assert MULTI._selected_indices("1.3") == [1, 3]
assert MULTI._selected_indices("1。3") == [1, 3]
assert MULTI._selected_indices("1.3.5") == [1, 3, 5]
def test_parses_dash_ranges_like_image_batch_index():
assert MULTI._selected_indices("1-3") == [1, 2, 3]
assert MULTI._selected_indices("1 - 3") == [1, 2, 3]
assert MULTI._selected_indices("1-5:2") == [1, 3, 5]
assert MULTI._selected_indices("5-1:2") == [1, 3, 5]
# Only "-" is a range separator; "~" is skipped like any unknown char.
assert MULTI._selected_indices("5~7") == [5, 7]
assert MULTI._selected_indices("1-3") == [1, 3]
def test_parses_bracket_ranges_and_steps():
assert MULTI._selected_indices("[1,3]") == [1, 2, 3]
assert MULTI._selected_indices("[1,3)") == [1, 2]
assert MULTI._selected_indices("(1,3]") == [2, 3]
assert MULTI._selected_indices("(1,3)") == [2]
assert MULTI._selected_indices("[1,5]:2") == [1, 3, 5]
assert MULTI._selected_indices("【1,3】") == [1, 2, 3]
assert MULTI._selected_indices("(1,3)") == [2]
assert MULTI._selected_indices("【3,1】") == [1, 2, 3]
def test_colon_inside_brackets_matches_image_batch_index():
# ":" is the step syntax, not an endpoint separator, so [1:3] is 1 and 3.
assert IBI._parse_indices("[1:3]", 10) == [1, 3]
assert MULTI._selected_indices("[1:3]", 10) == [1, 3]
assert MULTI._selected_indices("[1:3)", 10) == [1, 3]
assert MULTI._selected_indices("(1:3]", 10) == [1, 3]
assert MULTI._selected_indices("[1:5]:2", 10) == [1, 2, 5]
def test_negative_indices_use_connected_slot_base():
assert MULTI._selected_indices("-1", 4) == [4]
assert MULTI._selected_indices("-4", 4) == [1]
assert MULTI._selected_indices("-5", 4) == []
assert MULTI._selected_indices("[-3,-1]", 4) == [2, 3, 4]
assert MULTI._selected_indices("-1", 0) == []
def test_out_of_range_and_empty_select_nothing():
assert MULTI._selected_indices("0,15", 10) == []
assert MULTI._selected_indices("5", 4) == []
assert MULTI._selected_indices("", 10) == []
assert MULTI._selected_indices("abc", 10) == []
assert MULTI._selected_indices(None, 10) == []
assert MULTI._selected_indices([], 10) == []
def test_select_all_keyword_covers_every_connected_slot():
assert MULTI._selected_indices("all", 4) == [1, 2, 3, 4]
assert MULTI._selected_indices("a", 4) == [1, 2, 3, 4]
assert MULTI._selected_indices("ALL", 10) == list(range(1, 11))
assert MULTI._selected_indices(" a ", 4) == [1, 2, 3, 4]
assert MULTI._selected_indices(["all"], 4) == [1, 2, 3, 4]
# With no connected input there is nothing to select.
assert MULTI._selected_indices("all", 0) == []
def test_select_all_keyword_wins_over_numbers_and_is_whole_token_only():
assert MULTI._selected_indices(" a , 3 ", 4) == [1, 2, 3, 4]
assert MULTI._selected_indices("all,2", 4) == [1, 2, 3, 4]
# Only a standalone token is a shortcut; "a1"/"all1" stay numeric.
assert MULTI._selected_indices("a1", 4) == [1]
assert MULTI._selected_indices("all1", 4) == [1]
assert MULTI._selected_indices("abc", 4) == []
def test_bracket_ranges_drop_slots_outside_valid_range():
assert MULTI._selected_indices("[0,2]", 10) == [1, 2]
assert MULTI._selected_indices("[8,12]", 10) == [8, 9, 10]
def test_legacy_numeric_select():
assert MULTI._selected_indices(3, 10) == [3]
assert MULTI._selected_indices(15, 10) == []
assert MULTI._selected_indices(-1, 4) == [4]
def test_image_batch_index_uses_the_same_grammar():
assert IBI._parse_indices("1-3", 10) == [1, 2, 3]
assert IBI._parse_indices("[1,3]", 10) == [1, 2, 3]
assert IBI._parse_indices("[1,3)", 10) == [1, 2]
assert IBI._parse_indices("1.3", 10) == [1, 3]
assert IBI._parse_indices("1~3", 10) == [1, 3]
assert IBI._parse_indices("[1:3]", 10) == [1, 3]
assert IBI._parse_indices("-1", 10) == [9]
assert MULTI._selected_indices("-1", 10) == [10]
# "a"/"all" is a Multi Switch Select shortcut, not part of the shared grammar.
assert IBI._parse_indices("all", 10) == []
assert MULTI._selected_indices("all", 10) == list(range(1, 11))
def test_lazy_status_requests_all_selected_connected_inputs():
status = MULTI.check_lazy_status(
select="1,2,4", input_1=None, input_2=None, input_4=None
)
assert status == ["input_1", "input_2", "input_4"]
def test_lazy_status_skips_resolved_inputs():
status = MULTI.check_lazy_status(
select="1,2,4", input_1=object(), input_2=None, input_4=object()
)
assert status == ["input_2"]
def test_lazy_status_uses_connected_size_for_negative_index():
status = MULTI.check_lazy_status(select="-1", input_1=object(), input_4=None)
assert status == ["input_4"]
def test_lazy_status_requests_every_connected_input_for_select_all():
status = MULTI.check_lazy_status(select="all", input_1=None, input_3=None)
# input_2 is not connected, so it is never requested; input_3 is.
assert status == ["input_1", "input_3"]
def test_lazy_status_skips_unconnected_port():
# input_3 is selected but absent from kwargs because it is not connected.
assert MULTI.check_lazy_status(select="1,3", input_1=object()) == []
def test_lazy_status_requests_colon_form_pair():
status = MULTI.check_lazy_status(select="[1:3]", input_1=None, input_3=None)
assert status == ["input_1", "input_3"]
def test_execute_routes_only_selected_inputs():
out = asyncio.run(
MULTI.execute(
select="1,2,4",
input_1="a",
input_2="b",
input_3="c",
input_4="d",
)
)
assert out == ("a", "b", None, "d", None, None, None, None, None, None)
def test_execute_colon_form_selects_one_and_three():
out = asyncio.run(
MULTI.execute(select="[1:3]", input_1="a", input_2="b", input_3="c")
)
assert out[0] == "a"
assert out[1] is None
assert out[2] == "c"
def test_execute_range_selects_contiguous():
out = asyncio.run(
MULTI.execute(select="1-3", input_1="a", input_2="b", input_3="c")
)
assert out[:3] == ("a", "b", "c")
def test_execute_resolves_negative_against_connected_size():
out = asyncio.run(MULTI.execute(select="-1", input_1="a", input_4="d"))
assert out[0] is None
assert out[3] == "d"
def test_execute_select_all_routes_every_connected_input():
out = asyncio.run(
MULTI.execute(select="all", input_1="a", input_2="b", input_3="c")
)
assert out[:3] == ("a", "b", "c")
assert out[3:] == (None,) * 7
def test_execute_unmatched_select_outputs_all_none():
out = asyncio.run(MULTI.execute(select="0", input_1="a"))
assert all(value is None for value in out)
def test_execute_selected_but_unconnected_input_is_none():
out = asyncio.run(MULTI.execute(select="1,3", input_1="a", input_2="b"))
assert out[0] == "a"
assert out[2] is None
def test_validate_inputs_accepts_input_types_compat_argument():
assert MULTI.validate_inputs(input_types={"select": "INT"}, select=2) is True
def test_execute_accepts_legacy_numeric_select():
out = asyncio.run(MULTI.execute(select=2, input_2="b"))
assert out[1] == "b"
assert sum(value is not None for value in out) == 1