Files
DumasNodes/tests/test_dumas_image_nodes.py
T

483 lines
16 KiB
Python

import importlib
import json
import os
import sys
import tempfile
import time
import types
import unittest
from unittest import mock
class FakeImageArray:
def __init__(self, width=8, height=6):
self.shape = (height, width, 3)
def cpu(self):
return self
def numpy(self):
return self
def astype(self, _dtype):
return self
def __rmul__(self, _value):
return self
class FakeTensorBatch:
def __init__(self, width=8, height=6, count=1):
self.image = FakeImageArray(width=width, height=height)
self.shape = (count, height, width, 3)
self.count = count
def __getitem__(self, index):
if isinstance(index, slice):
return FakeTensorBatch(
width=self.shape[2],
height=self.shape[1],
count=len(range(*index.indices(self.count))),
)
if index != 0:
raise IndexError(index)
return self.image
class FakePILImage:
saved_paths = []
def save(self, path, *args, **kwargs):
self.saved_paths.append((path, args, kwargs))
def convert(self, _mode):
return self
class DumasImageNodeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp_dir = tempfile.mkdtemp(prefix="dumas-image-node-")
fake_numpy = types.SimpleNamespace(
clip=lambda array, _low, _high: array,
uint8="uint8",
)
fake_pil_image_module = types.SimpleNamespace(fromarray=lambda _array: FakePILImage())
fake_pil_module = types.SimpleNamespace(Image=fake_pil_image_module)
fake_folder_paths = types.SimpleNamespace(
get_temp_directory=lambda: cls.temp_dir,
get_output_directory=lambda: cls.temp_dir,
get_save_image_path=lambda prefix, _out, _width, _height: (
cls.temp_dir,
prefix,
1,
"",
prefix,
),
)
cls._saved_modules = {
name: sys.modules.get(name)
for name in ("numpy", "PIL", "PIL.Image", "folder_paths")
}
sys.modules["numpy"] = fake_numpy
sys.modules["PIL"] = fake_pil_module
sys.modules["PIL.Image"] = fake_pil_image_module
sys.modules["folder_paths"] = fake_folder_paths
cls.image_nodes = importlib.import_module("dumas_image_nodes")
@classmethod
def tearDownClass(cls):
for name, module in cls._saved_modules.items():
if module is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = module
def setUp(self):
FakePILImage.saved_paths = []
def test_compare_images_returns_second_input_as_new_image(self):
node = self.image_nodes.DumasImageCompareNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
result = node.compare_images(image1=image1, image2=image2)
self.assertIs(result["result"][0], image2)
self.assertEqual([item["slot"] for item in result["ui"]["images"]], [1, 2])
self.assertEqual(len(FakePILImage.saved_paths), 2)
def test_compare_images_falls_back_to_first_image(self):
node = self.image_nodes.DumasImageCompareNode()
image1 = FakeTensorBatch()
result = node.compare_images(image1=image1)
self.assertIs(result["result"][0], image1)
self.assertEqual([item["slot"] for item in result["ui"]["images"]], [1])
def test_compare_images_handles_missing_inputs(self):
node = self.image_nodes.DumasImageCompareNode()
result = node.compare_images()
self.assertIsNone(result["result"][0])
self.assertEqual(result["ui"]["images"], [])
def test_saved_filenames_use_dumas_prefix(self):
node = self.image_nodes.DumasImageCompareNode()
image1 = FakeTensorBatch()
result = node.compare_images(image1=image1)
self.assertTrue(result["ui"]["images"][0]["filename"].startswith("dumas_compare"))
self.assertTrue(os.path.basename(FakePILImage.saved_paths[0][0]).startswith("dumas_compare"))
def test_save_image_uses_second_input_token(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch(width=10, height=12)
node.save_images(
images=image,
folder=self.temp_dir,
pattern="shot_%input%_%input2%_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
name="alpha.png",
name_2="beta/final",
)
saved_name = os.path.basename(FakePILImage.saved_paths[0][0])
self.assertEqual(saved_name, "shot_alpha_beta_final_001.png")
def test_save_image_resolves_date_size_and_batch_tokens(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch(width=10, height=12, count=2)
fake_now = time.struct_time((2026, 8, 5, 13, 7, 9, 2, 217, -1))
with mock.patch.object(self.image_nodes.time, "localtime", return_value=fake_now):
node.save_images(
images=image,
folder=self.temp_dir,
pattern="asset_%date:yyyy-MM-dd%_%date:hh-mm-ss%_%width%x%height%_%batch_num%_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
saved_names = [os.path.basename(path) for path, _args, _kwargs in FakePILImage.saved_paths]
self.assertEqual(
saved_names,
[
"asset_2026-08-05_13-07-09_10x12_0_001.png",
"asset_2026-08-05_13-07-09_10x12_1_001.png",
],
)
def test_save_image_counter_increments_for_existing_files(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
node.save_images(
images=image,
folder=self.temp_dir,
pattern="counter_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
open(FakePILImage.saved_paths[0][0], "a", encoding="utf-8").close()
node.save_images(
images=image,
folder=self.temp_dir,
pattern="counter_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
saved_names = [os.path.basename(path) for path, _args, _kwargs in FakePILImage.saved_paths]
self.assertEqual(saved_names, ["counter_001.png", "counter_002.png"])
def test_save_image_uses_same_counter_for_folder_and_filename(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
node.save_images(
images=image,
folder=self.temp_dir,
pattern="Char%counter%/Char%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
saved_path = FakePILImage.saved_paths[0][0].replace("\\", "/")
self.assertTrue(saved_path.endswith("/Char001/Char001.png"))
def test_save_image_creates_nested_directories_before_saving(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
node.save_images(
images=image,
folder=self.temp_dir,
pattern="Char_%counter%/Base",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
saved_path = FakePILImage.saved_paths[0][0]
self.assertTrue(os.path.isdir(os.path.dirname(saved_path)))
def test_character_helper_passes_through_images_and_formats_text(self):
node = self.image_nodes.DumasCharacterHelperNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
result = node.build_character_text(
image1=image1,
image2=image2,
image1_picture_id="2",
image2_picture_id="3",
character_id="char_dave",
name="Dave",
alias="The Locksmith",
gender="male",
age="41",
nationality="English",
occupation="a detective",
height_feet="6",
height_inches="2",
accent="English",
general="wears a long grey coat",
)
self.assertIs(result[0], image1)
self.assertIs(result[1], image2)
self.assertEqual(
result[2],
(
"<Picture 2> and <Picture 3> reference the same character who is called Dave.\n"
"<Picture 2> is the primary full-body reference for Dave.\n"
"<Picture 3> is a frontal facial reference for Dave.\n"
"Dave is also known as The Locksmith, is male, is 41 years old, is English, works as a detective, is 6 feet 2 inches tall, speaks in an English accent.\n"
"wears a long grey coat."
),
)
def test_character_helper_handles_missing_optional_fields(self):
node = self.image_nodes.DumasCharacterHelperNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
result = node.build_character_text(
image1=image1,
image2=image2,
image1_picture_id="4",
image2_picture_id="6",
character_id="",
name="",
alias="",
gender="",
age="unknown",
nationality="",
occupation="",
height_feet="",
height_inches="",
accent="",
general="",
)
self.assertEqual(
result[2],
(
"<Picture 4> and <Picture 6> reference the same character.\n"
"<Picture 4> is the primary full-body reference for the character.\n"
"<Picture 6> is a frontal facial reference for the character."
),
)
def test_save_image_returns_ui_entries_for_output_folder(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
result = node.save_images(
images=image,
folder="",
pattern="result_%counter%",
format="jpg",
quality=90,
embed_workflow=False,
save_on_run=True,
)
self.assertEqual(result["ui"]["images"][0]["type"], "output")
self.assertTrue(result["ui"]["images"][0]["filename"].endswith(".jpg"))
def test_save_image_returns_ui_entries_for_external_folder(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
external_dir = tempfile.mkdtemp(prefix="dumas-image-node-external-")
result = node.save_images(
images=image,
folder=external_dir,
pattern="external_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
self.assertEqual(result["ui"]["images"][0]["type"], "external")
self.assertEqual(result["ui"]["images"][0]["subfolder"], external_dir.replace("\\", "/"))
self.assertTrue(result["ui"]["images"][0]["token"])
def test_save_image_handles_windows_different_drive_paths(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
with mock.patch.object(
self.image_nodes.folder_paths,
"get_output_directory",
return_value="C:\\ComfyUI\\output",
), mock.patch.object(
self.image_nodes.os.path,
"abspath",
return_value="D:\\renders",
):
result = node.save_images(
images=image,
folder="D:\\renders",
pattern="drive_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=True,
)
self.assertEqual(result["ui"]["images"][0]["type"], "external")
self.assertEqual(result["ui"]["images"][0]["subfolder"], "D:/renders")
self.assertTrue(result["ui"]["images"][0]["token"])
def test_save_image_skips_when_save_is_disabled(self):
node = self.image_nodes.DumasSaveImageNode()
image = FakeTensorBatch()
result = node.save_images(
images=image,
folder=self.temp_dir,
pattern="ignored_%counter%",
format="png",
quality=100,
embed_workflow=False,
save_on_run=False,
)
self.assertEqual(result["ui"]["images"], [])
self.assertEqual(FakePILImage.saved_paths, [])
def test_h3_plan_scene_images_attach_and_extract(self):
attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode()
extract_node = self.image_nodes.DumasH3PlanExtractSceneImagesNode()
plan = {"shots": [{"id": "intro"}, {"id": "middle"}]}
image_a = FakeTensorBatch()
image_b = FakeTensorBatch(width=10, height=10)
attached_plan, connected = attach_node.attach(
plan=plan,
scene_index=2,
image1=image_a,
image3=image_b,
)
extracted = extract_node.extract(attached_plan, 2)
self.assertEqual(connected, 2)
self.assertEqual(attached_plan["_dumas_scene_image_bindings"]["scene_counts"], {"2": 2})
self.assertIs(extracted[1], image_a)
self.assertIsNone(extracted[2])
self.assertIs(extracted[3], image_b)
self.assertEqual(extracted[-1], 2)
self.assertNotIn("_dumas_scene_image_bindings", plan)
def test_h3_plan_scene_images_support_multiple_scenes(self):
attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode()
extract_node = self.image_nodes.DumasH3PlanExtractSceneImagesNode()
plan = {"shots": [{"id": "one"}, {"id": "two"}]}
image_1 = FakeTensorBatch()
image_2 = FakeTensorBatch(width=12, height=9)
image_3 = FakeTensorBatch(width=8, height=8)
plan_after_first, _connected = attach_node.attach(plan=plan, scene_index=1, image2=image_1)
plan_after_second, _connected = attach_node.attach(
plan=plan_after_first,
scene_index=2,
image6=image_2,
image7=image_3,
)
scene1 = extract_node.extract(plan_after_second, 1)
scene2 = extract_node.extract(plan_after_second, 2)
self.assertIs(scene1[2], image_1)
self.assertEqual(scene1[-1], 1)
self.assertIs(scene2[6], image_2)
self.assertIs(scene2[7], image_3)
self.assertEqual(scene2[-1], 2)
self.assertEqual(
plan_after_second["_dumas_scene_image_bindings"]["scene_counts"],
{"1": 1, "2": 2},
)
def test_h3_plan_scene_images_metadata_is_json_serializable(self):
attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode()
plan = {"shots": [{"id": "one"}]}
attached_plan, connected = attach_node.attach(
plan=plan,
scene_index=1,
image4=FakeTensorBatch(),
)
self.assertEqual(connected, 1)
json.dumps(attached_plan)
def test_h3_plan_scene_images_can_clear_a_scene_binding(self):
attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode()
extract_node = self.image_nodes.DumasH3PlanExtractSceneImagesNode()
plan = {"shots": [{"id": "one"}]}
attached_plan, connected = attach_node.attach(
plan=plan,
scene_index=1,
image1=FakeTensorBatch(),
)
cleared_plan, cleared = attach_node.attach(plan=attached_plan, scene_index=1)
extracted = extract_node.extract(cleared_plan, 1)
self.assertEqual(connected, 1)
self.assertEqual(cleared, 0)
self.assertNotIn("_dumas_scene_image_bindings", cleared_plan)
self.assertEqual(extracted[-1], 0)
def test_h3_plan_scene_images_reject_invalid_scene_index(self):
attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode()
plan = {"shots": [{"id": "one"}]}
with self.assertRaises(ValueError):
attach_node.attach(plan=plan, scene_index=2, image1=FakeTensorBatch())
if __name__ == "__main__":
unittest.main()