Files

616 lines
18 KiB
Python

import json
import importlib.util
import subprocess
import sys
import textwrap
from types import SimpleNamespace
import pytest
from smart_metadata_reader.models import LoraInfo, MetadataBundle, ParseResult
def test_format_setting_outputs_complete_human_readable_text():
from smart_metadata_reader.settings_formatter import format_setting
result = ParseResult(
filename="001.png",
model_name="model.safetensors",
vae_name="vae.vae.safetensors",
loras=[
LoraInfo(
node_id="10",
class_type="LoraLoader",
lora_name="lora_a.safetensors",
strength_model=0.8,
strength_clip=1.0,
path=["KSampler 9.model", "LoraLoader 10"],
),
LoraInfo(
node_id="11",
class_type="LoraLoader",
lora_name="lora_b.safetensors",
strength_model=0.6,
strength_clip=0.75,
path=["KSampler 9.model", "LoraLoader 11"],
),
],
seed=123456789,
steps=30,
cfg=7.0,
sampler_name="euler",
scheduler="normal",
width=1200,
height=1600,
status_message="OK",
confidence=0.94,
partial_result={"source_format": "ComfyUI prompt/workflow"},
)
setting = format_setting(result)
assert "Filename: 001.png" in setting
assert "Source: ComfyUI prompt/workflow" in setting
assert "Model: model.safetensors" in setting
assert "VAE: vae.vae.safetensors" in setting
assert "LoRA:" in setting
assert "* lora_a.safetensors (model 0.80, clip 1.00)" in setting
assert "* lora_b.safetensors (model 0.60, clip 0.75)" in setting
assert "Seed: 123456789" in setting
assert "Steps: 30" in setting
assert "CFG: 7.0" in setting
assert "Sampler: euler" in setting
assert "Scheduler: normal" in setting
assert "Size: 1200 x 1600" in setting
assert "Status: OK" in setting
assert "Confidence: 0.94" in setting
def test_format_setting_handles_missing_fields_without_error():
from smart_metadata_reader.settings_formatter import format_setting
setting = format_setting(ParseResult())
assert "Filename: unknown" in setting
assert "Source: unknown" in setting
assert "Model: unknown" in setting
assert "VAE: unknown" in setting
assert "LoRA: none" in setting
assert "Seed: -1" in setting
assert "Steps: 0" in setting
assert "CFG: 0.0" in setting
assert "Sampler: unknown" in setting
assert "Scheduler: unknown" in setting
assert "Size: unknown" in setting
def test_format_setting_displays_unresolved_partial_result_entries():
from smart_metadata_reader.settings_formatter import format_setting
result = ParseResult(
filename="partial.png",
status_message="PARTIAL",
confidence=0.52,
partial_result={
"source_format": "ComfyUI prompt/workflow",
"unresolved": [
{
"node_id": "88",
"class_type": "UnknownConditioningRouter",
"field": "positive_conditioning",
"role": "positive",
"reason": "no conditioning adapter matched",
}
],
},
)
setting = format_setting(result)
assert "Status: PARTIAL" in setting
assert "Confidence: 0.52" in setting
assert "Unresolved:" in setting
assert (
"* Node 88 UnknownConditioningRouter input positive_conditioning "
"role=positive reason=no conditioning adapter matched"
) in setting
def test_format_setting_displays_a1111_source_and_status():
from smart_metadata_reader.settings_formatter import format_setting
result = ParseResult(
filename="",
positive="<lora:role:0.8>, nsfw",
negative="low quality",
seed=123,
steps=25,
cfg=7.5,
sampler_name="Euler a",
width=768,
height=1024,
model_name="demo_model",
status_message="A1111 parameters fallback",
confidence=0.75,
partial_result={"source_format": "A1111 parameters"},
)
setting = format_setting(result)
assert "Source: A1111 parameters" in setting
assert "Status: A1111 parameters fallback" in setting
assert "Model: demo_model" in setting
def test_format_setting_is_plain_text_not_json():
from smart_metadata_reader.settings_formatter import format_setting
setting = format_setting(
ParseResult(
filename="plain.png",
partial_result={"source_format": "ComfyUI prompt/workflow"},
)
)
assert not setting.strip().startswith("{")
with pytest.raises(json.JSONDecodeError):
json.loads(setting)
def test_node_mappings_expose_smart_metadata_reader():
import nodes
assert nodes.NODE_CLASS_MAPPINGS["SmartMetadataReader"] is nodes.SmartMetadataReader
assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader"
def test_plugin_root_init_exports_comfyui_mappings():
root_dir = pytest.importorskip("pathlib").Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"comfyui_smart_metadata_reader_plugin",
root_dir / "__init__.py",
submodule_search_locations=[str(root_dir)],
)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
finally:
sys.modules.pop(spec.name, None)
sys.modules.pop(f"{spec.name}.nodes", None)
assert "SmartMetadataReader" in module.NODE_CLASS_MAPPINGS
assert module.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader"
def test_plugin_package_import_works_without_repo_root_on_sys_path(tmp_path):
root_dir = pytest.importorskip("pathlib").Path(__file__).resolve().parents[1]
script = textwrap.dedent(
f"""
import importlib.util
import pathlib
import sys
root = pathlib.Path({str(root_dir)!r})
sys.path = [
path for path in sys.path
if pathlib.Path(path or ".").resolve() != root
]
for name in list(sys.modules):
if name == "smart_metadata_reader" or name.startswith("smart_metadata_reader."):
sys.modules.pop(name, None)
spec = importlib.util.spec_from_file_location(
"comfyui_custom_node_under_test",
root / "__init__.py",
submodule_search_locations=[str(root)],
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
assert "SmartMetadataReader" in module.NODE_CLASS_MAPPINGS
assert module.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader"
print("package import ok")
"""
)
completed = subprocess.run(
[sys.executable, "-c", script],
cwd=tmp_path,
text=True,
capture_output=True,
check=False,
)
assert completed.returncode == 0, completed.stderr
assert "package import ok" in completed.stdout
def test_input_types_ignores_unusable_comfyui_filename_list_and_scans_input_dir(
tmp_path,
monkeypatch,
):
import nodes
(tmp_path / "root.png").write_bytes(b"fake")
subdir = tmp_path / "sub"
subdir.mkdir()
(subdir / "a.jpg").write_bytes(b"fake")
(subdir / "b.webp").write_bytes(b"fake")
(subdir / "note.txt").write_text("ignore me")
def broken_filename_list(category):
raise KeyError(category)
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(
get_filename_list=broken_filename_list,
get_input_directory=lambda: str(tmp_path),
),
)
input_types = nodes.SmartMetadataReader.INPUT_TYPES()
assert input_types["required"]["image"][0] == [
"root.png",
"sub/a.jpg",
"sub/b.webp",
]
def test_smart_metadata_reader_input_types_include_uploadable_image(tmp_path, monkeypatch):
import nodes
(tmp_path / "a.png").write_bytes(b"fake")
(tmp_path / "c.jpg").write_bytes(b"fake")
(tmp_path / "d.jpeg").write_bytes(b"fake")
(tmp_path / "b.webp").write_bytes(b"fake")
(tmp_path / "ignored.bmp").write_bytes(b"fake")
(tmp_path / "note.txt").write_text("ignore me")
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_input_directory=lambda: str(tmp_path)),
)
input_types = nodes.SmartMetadataReader.INPUT_TYPES()
required = input_types["required"]
assert required["image"][0] == ["a.png", "b.webp", "c.jpg", "d.jpeg"]
assert required["image"][1]["image_upload"] is True
assert required["parameter_index"][0] == "INT"
assert required["parameter_index"][1]["default"] == 0
assert required["parameter_index"][1]["min"] == 0
assert required["prefer_cached_text"] == ("BOOLEAN", {"default": True})
assert required["include_raw_json"] == ("BOOLEAN", {"default": True})
assert required["max_depth"][0] == "INT"
assert required["max_depth"][1]["default"] == 40
assert required["max_depth"][1]["min"] == 1
assert required["max_depth"][1]["max"] == 200
def test_smart_metadata_reader_input_types_empty_or_missing_input_dir_is_safe(
tmp_path,
monkeypatch,
):
import nodes
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_input_directory=lambda: str(tmp_path)),
)
assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == []
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_input_directory=lambda: str(tmp_path / "missing")),
)
assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == []
def test_smart_metadata_reader_input_types_handles_input_directory_errors(monkeypatch):
import nodes
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_input_directory=lambda: (_ for _ in ()).throw(KeyError("input"))),
)
assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == []
def test_smart_metadata_reader_return_contract():
import nodes
node_cls = nodes.SmartMetadataReader
assert node_cls.RETURN_TYPES == (
"IMAGE",
"MASK",
"STRING",
"STRING",
"INT",
"INT",
"FLOAT",
"INT",
"INT",
"STRING",
"STRING",
"STRING",
)
assert node_cls.RETURN_NAMES == (
"image",
"mask",
"positive",
"negative",
"seed",
"steps",
"cfg",
"width",
"height",
"model_name",
"filename",
"setting",
)
assert len(node_cls.RETURN_TYPES) == len(node_cls.RETURN_NAMES)
assert hasattr(node_cls, node_cls.FUNCTION)
assert node_cls.CATEGORY
def test_smart_metadata_reader_text_output_indices_match_parser_mapping():
import nodes
node_cls = nodes.SmartMetadataReader
assert node_cls.RETURN_NAMES[0] == "image"
assert node_cls.RETURN_NAMES[1] == "mask"
assert node_cls.RETURN_NAMES[2] == "positive"
assert node_cls.RETURN_NAMES[3] == "negative"
assert node_cls.RETURN_TYPES[2] == "STRING"
assert node_cls.RETURN_TYPES[3] == "STRING"
def test_smart_metadata_reader_method_returns_expected_tuple_order(tmp_path, monkeypatch):
import nodes
image_path = tmp_path / "sample.png"
image_path.write_bytes(b"fake image bytes")
calls = {}
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)),
)
def fake_read_metadata(path):
calls["read_metadata"] = path
return MetadataBundle(
filename="sample.png",
width=1200,
height=1600,
prompt_raw="{}",
workflow_raw=None,
parameters_raw=None,
source_format="ComfyUI prompt/workflow",
)
def fake_parse_metadata_bundle(
bundle,
parameter_index,
prefer_cached_text,
include_raw_json,
max_depth,
):
calls["parse"] = {
"bundle": bundle,
"parameter_index": parameter_index,
"prefer_cached_text": prefer_cached_text,
"include_raw_json": include_raw_json,
"max_depth": max_depth,
}
return ParseResult(
positive="real positive",
negative="real negative",
seed=123,
steps=30,
cfg=7.5,
width=1200,
height=1600,
model_name="model.safetensors",
filename="sample.png",
setting="Filename: sample.png\nStatus: OK",
)
monkeypatch.setattr(nodes, "read_metadata", fake_read_metadata)
monkeypatch.setattr(nodes, "parse_metadata_bundle", fake_parse_metadata_bundle)
monkeypatch.setattr(nodes, "load_image_and_mask", lambda path: ("IMAGE_TENSOR", "MASK_TENSOR", 1200, 1600))
output = nodes.SmartMetadataReader().read_metadata(
"sample.png",
parameter_index=2,
prefer_cached_text=False,
include_raw_json=False,
max_depth=12,
)
assert output == (
"IMAGE_TENSOR",
"MASK_TENSOR",
"real positive",
"real negative",
123,
30,
7.5,
1200,
1600,
"model.safetensors",
"sample.png",
"Filename: sample.png\nStatus: OK",
)
assert calls["read_metadata"] == str(image_path)
assert calls["parse"]["parameter_index"] == 2
assert calls["parse"]["prefer_cached_text"] is False
assert calls["parse"]["include_raw_json"] is False
assert calls["parse"]["max_depth"] == 12
def test_smart_metadata_reader_parse_failure_returns_failed_setting(tmp_path, monkeypatch):
import nodes
image_path = tmp_path / "broken-metadata.png"
image_path.write_bytes(b"fake image bytes")
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)),
)
monkeypatch.setattr(
nodes,
"read_metadata",
lambda path: MetadataBundle(
filename="broken-metadata.png",
width=640,
height=480,
prompt_raw=None,
workflow_raw=None,
parameters_raw=None,
source_format="none",
),
)
monkeypatch.setattr(nodes, "parse_metadata_bundle", lambda *args, **kwargs: (_ for _ in ()).throw(ValueError("boom")))
monkeypatch.setattr(nodes, "load_image_and_mask", lambda path: ("IMAGE_TENSOR", "MASK_TENSOR", 640, 480))
output = nodes.SmartMetadataReader().read_metadata("broken-metadata.png", 0, True, True, 40)
assert output[:2] == ("IMAGE_TENSOR", "MASK_TENSOR")
assert output[2] == ""
assert output[3] == ""
assert output[4] == -1
assert output[5] == 0
assert output[6] == 0.0
assert output[7] == 640
assert output[8] == 480
assert output[9] == ""
assert output[10] == "broken-metadata.png"
assert "Status: FAILED" in output[11]
assert "boom" in output[11]
def test_smart_metadata_reader_image_load_failure_raises_clear_error(monkeypatch):
import nodes
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: "missing.png"),
)
monkeypatch.setattr(
nodes,
"load_image_and_mask",
lambda path: (_ for _ in ()).throw(OSError("cannot identify image file")),
)
with pytest.raises(RuntimeError, match="Failed to load image 'missing.png'"):
nodes.SmartMetadataReader().read_metadata("missing.png", 0, True, True, 40)
def test_smart_metadata_reader_is_changed_tracks_file_content(tmp_path, monkeypatch):
import nodes
image_path = tmp_path / "changed.png"
image_path.write_bytes(b"first")
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)),
)
first = nodes.SmartMetadataReader.IS_CHANGED("changed.png")
image_path.write_bytes(b"second")
second = nodes.SmartMetadataReader.IS_CHANGED("changed.png")
assert first
assert second
assert first != second
def test_smart_metadata_reader_is_changed_handles_missing_or_bad_paths(monkeypatch):
import nodes
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: (_ for _ in ()).throw(KeyError("input"))),
)
value = nodes.SmartMetadataReader.IS_CHANGED("missing.png")
assert isinstance(value, str)
assert value.startswith("missing:")
def test_smart_metadata_reader_validate_inputs_checks_image_exists(tmp_path, monkeypatch):
import nodes
existing = tmp_path / "exists.png"
existing.write_bytes(b"fake")
missing = tmp_path / "missing.png"
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(get_annotated_filepath=lambda image: str(tmp_path / image)),
)
assert nodes.SmartMetadataReader.VALIDATE_INPUTS("exists.png") is True
invalid = nodes.SmartMetadataReader.VALIDATE_INPUTS("missing.png")
assert isinstance(invalid, str)
assert "Invalid image file" in invalid
assert str(missing) in invalid
def test_smart_metadata_reader_validate_inputs_falls_back_when_exists_helper_errors(
tmp_path,
monkeypatch,
):
import nodes
existing = tmp_path / "exists.png"
existing.write_bytes(b"fake")
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(
exists_annotated_filepath=lambda image: (_ for _ in ()).throw(KeyError("input")),
get_input_directory=lambda: str(tmp_path),
),
)
assert nodes.SmartMetadataReader.VALIDATE_INPUTS("exists.png") is True
assert "Invalid image file" in nodes.SmartMetadataReader.VALIDATE_INPUTS("missing.png")
def test_smart_metadata_reader_validate_inputs_uses_comfyui_exists_helper(monkeypatch):
import nodes
monkeypatch.setattr(
nodes,
"folder_paths",
SimpleNamespace(exists_annotated_filepath=lambda image: image == "ok.png"),
)
assert nodes.SmartMetadataReader.VALIDATE_INPUTS("ok.png") is True
assert "Invalid image file" in nodes.SmartMetadataReader.VALIDATE_INPUTS("bad.png")