SuperRealCo commited on
Commit
7945cad
·
verified ·
1 Parent(s): 020f27d

Delete tests

Browse files
tests/README.md DELETED
@@ -1,29 +0,0 @@
1
- # Automated Testing
2
-
3
- ## Running tests locally
4
-
5
- Additional requirements for running tests:
6
- ```
7
- pip install pytest
8
- pip install websocket-client==1.6.1
9
- opencv-python==4.6.0.66
10
- scikit-image==0.21.0
11
- ```
12
- Run inference tests:
13
- ```
14
- pytest tests/inference
15
- ```
16
-
17
- ## Quality regression test
18
- Compares images in 2 directories to ensure they are the same
19
-
20
- 1) Run an inference test to save a directory of "ground truth" images
21
- ```
22
- pytest tests/inference --output_dir tests/inference/baseline
23
- ```
24
- 2) Make code edits
25
-
26
- 3) Run inference and quality comparison tests
27
- ```
28
- pytest
29
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/__init__.py DELETED
File without changes
tests/compare/conftest.py DELETED
@@ -1,41 +0,0 @@
1
- import os
2
- import pytest
3
-
4
- # Command line arguments for pytest
5
- def pytest_addoption(parser):
6
- parser.addoption('--baseline_dir', action="store", default='tests/inference/baseline', help='Directory for ground-truth images')
7
- parser.addoption('--test_dir', action="store", default='tests/inference/samples', help='Directory for images to test')
8
- parser.addoption('--metrics_file', action="store", default='tests/metrics.md', help='Output file for metrics')
9
- parser.addoption('--img_output_dir', action="store", default='tests/compare/samples', help='Output directory for diff metric images')
10
-
11
- # This initializes args at the beginning of the test session
12
- @pytest.fixture(scope="session", autouse=True)
13
- def args_pytest(pytestconfig):
14
- args = {}
15
- args['baseline_dir'] = pytestconfig.getoption('baseline_dir')
16
- args['test_dir'] = pytestconfig.getoption('test_dir')
17
- args['metrics_file'] = pytestconfig.getoption('metrics_file')
18
- args['img_output_dir'] = pytestconfig.getoption('img_output_dir')
19
-
20
- # Initialize metrics file
21
- with open(args['metrics_file'], 'a') as f:
22
- # if file is empty, write header
23
- if os.stat(args['metrics_file']).st_size == 0:
24
- f.write("| date | run | file | status | value | \n")
25
- f.write("| --- | --- | --- | --- | --- | \n")
26
-
27
- return args
28
-
29
-
30
- def gather_file_basenames(directory: str):
31
- files = []
32
- for file in os.listdir(directory):
33
- if file.endswith(".png"):
34
- files.append(file)
35
- return files
36
-
37
- # Creates the list of baseline file names to use as a fixture
38
- def pytest_generate_tests(metafunc):
39
- if "baseline_fname" in metafunc.fixturenames:
40
- baseline_fnames = gather_file_basenames(metafunc.config.getoption("baseline_dir"))
41
- metafunc.parametrize("baseline_fname", baseline_fnames)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/compare/test_quality.py DELETED
@@ -1,195 +0,0 @@
1
- import datetime
2
- import numpy as np
3
- import os
4
- from PIL import Image
5
- import pytest
6
- from pytest import fixture
7
- from typing import Tuple, List
8
-
9
- from cv2 import imread, cvtColor, COLOR_BGR2RGB
10
- from skimage.metrics import structural_similarity as ssim
11
-
12
-
13
- """
14
- This test suite compares images in 2 directories by file name
15
- The directories are specified by the command line arguments --baseline_dir and --test_dir
16
-
17
- """
18
- # ssim: Structural Similarity Index
19
- # Returns a tuple of (ssim, diff_image)
20
- def ssim_score(img0: np.ndarray, img1: np.ndarray) -> Tuple[float, np.ndarray]:
21
- score, diff = ssim(img0, img1, channel_axis=-1, full=True)
22
- # rescale the difference image to 0-255 range
23
- diff = (diff * 255).astype("uint8")
24
- return score, diff
25
-
26
- # Metrics must return a tuple of (score, diff_image)
27
- METRICS = {"ssim": ssim_score}
28
- METRICS_PASS_THRESHOLD = {"ssim": 0.95}
29
-
30
-
31
- class TestCompareImageMetrics:
32
- @fixture(scope="class")
33
- def test_file_names(self, args_pytest):
34
- test_dir = args_pytest['test_dir']
35
- fnames = self.gather_file_basenames(test_dir)
36
- yield fnames
37
- del fnames
38
-
39
- @fixture(scope="class", autouse=True)
40
- def teardown(self, args_pytest):
41
- yield
42
- # Runs after all tests are complete
43
- # Aggregate output files into a grid of images
44
- baseline_dir = args_pytest['baseline_dir']
45
- test_dir = args_pytest['test_dir']
46
- img_output_dir = args_pytest['img_output_dir']
47
- metrics_file = args_pytest['metrics_file']
48
-
49
- grid_dir = os.path.join(img_output_dir, "grid")
50
- os.makedirs(grid_dir, exist_ok=True)
51
-
52
- for metric_dir in METRICS.keys():
53
- metric_path = os.path.join(img_output_dir, metric_dir)
54
- for file in os.listdir(metric_path):
55
- if file.endswith(".png"):
56
- score = self.lookup_score_from_fname(file, metrics_file)
57
- image_file_list = []
58
- image_file_list.append([
59
- os.path.join(baseline_dir, file),
60
- os.path.join(test_dir, file),
61
- os.path.join(metric_path, file)
62
- ])
63
- # Create grid
64
- image_list = [[Image.open(file) for file in files] for files in image_file_list]
65
- grid = self.image_grid(image_list)
66
- grid.save(os.path.join(grid_dir, f"{metric_dir}_{score:.3f}_{file}"))
67
-
68
- # Tests run for each baseline file name
69
- @fixture()
70
- def fname(self, baseline_fname):
71
- yield baseline_fname
72
- del baseline_fname
73
-
74
- def test_directories_not_empty(self, args_pytest):
75
- baseline_dir = args_pytest['baseline_dir']
76
- test_dir = args_pytest['test_dir']
77
- assert len(os.listdir(baseline_dir)) != 0, f"Baseline directory {baseline_dir} is empty"
78
- assert len(os.listdir(test_dir)) != 0, f"Test directory {test_dir} is empty"
79
-
80
- def test_dir_has_all_matching_metadata(self, fname, test_file_names, args_pytest):
81
- # Check that all files in baseline_dir have a file in test_dir with matching metadata
82
- baseline_file_path = os.path.join(args_pytest['baseline_dir'], fname)
83
- file_paths = [os.path.join(args_pytest['test_dir'], f) for f in test_file_names]
84
- file_match = self.find_file_match(baseline_file_path, file_paths)
85
- assert file_match is not None, f"Could not find a file in {args_pytest['test_dir']} with matching metadata to {baseline_file_path}"
86
-
87
- # For a baseline image file, finds the corresponding file name in test_dir and
88
- # compares the images using the metrics in METRICS
89
- @pytest.mark.parametrize("metric", METRICS.keys())
90
- def test_pipeline_compare(
91
- self,
92
- args_pytest,
93
- fname,
94
- test_file_names,
95
- metric,
96
- ):
97
- baseline_dir = args_pytest['baseline_dir']
98
- test_dir = args_pytest['test_dir']
99
- metrics_output_file = args_pytest['metrics_file']
100
- img_output_dir = args_pytest['img_output_dir']
101
-
102
- baseline_file_path = os.path.join(baseline_dir, fname)
103
-
104
- # Find file match
105
- file_paths = [os.path.join(test_dir, f) for f in test_file_names]
106
- test_file = self.find_file_match(baseline_file_path, file_paths)
107
-
108
- # Run metrics
109
- sample_baseline = self.read_img(baseline_file_path)
110
- sample_secondary = self.read_img(test_file)
111
-
112
- score, metric_img = METRICS[metric](sample_baseline, sample_secondary)
113
- metric_status = score > METRICS_PASS_THRESHOLD[metric]
114
-
115
- # Save metric values
116
- with open(metrics_output_file, 'a') as f:
117
- run_info = os.path.splitext(fname)[0]
118
- metric_status_str = "PASS ✅" if metric_status else "FAIL ❌"
119
- date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120
- f.write(f"| {date_str} | {run_info} | {metric} | {metric_status_str} | {score} | \n")
121
-
122
- # Save metric image
123
- metric_img_dir = os.path.join(img_output_dir, metric)
124
- os.makedirs(metric_img_dir, exist_ok=True)
125
- output_filename = f'{fname}'
126
- Image.fromarray(metric_img).save(os.path.join(metric_img_dir, output_filename))
127
-
128
- assert score > METRICS_PASS_THRESHOLD[metric]
129
-
130
- def read_img(self, filename: str) -> np.ndarray:
131
- cvImg = imread(filename)
132
- cvImg = cvtColor(cvImg, COLOR_BGR2RGB)
133
- return cvImg
134
-
135
- def image_grid(self, img_list: list[list[Image.Image]]):
136
- # imgs is a 2D list of images
137
- # Assumes the input images are a rectangular grid of equal sized images
138
- rows = len(img_list)
139
- cols = len(img_list[0])
140
-
141
- w, h = img_list[0][0].size
142
- grid = Image.new('RGB', size=(cols*w, rows*h))
143
-
144
- for i, row in enumerate(img_list):
145
- for j, img in enumerate(row):
146
- grid.paste(img, box=(j*w, i*h))
147
- return grid
148
-
149
- def lookup_score_from_fname(self,
150
- fname: str,
151
- metrics_output_file: str
152
- ) -> float:
153
- fname_basestr = os.path.splitext(fname)[0]
154
- with open(metrics_output_file, 'r') as f:
155
- for line in f:
156
- if fname_basestr in line:
157
- score = float(line.split('|')[5])
158
- return score
159
- raise ValueError(f"Could not find score for {fname} in {metrics_output_file}")
160
-
161
- def gather_file_basenames(self, directory: str):
162
- files = []
163
- for file in os.listdir(directory):
164
- if file.endswith(".png"):
165
- files.append(file)
166
- return files
167
-
168
- def read_file_prompt(self, fname:str) -> str:
169
- # Read prompt from image file metadata
170
- img = Image.open(fname)
171
- img.load()
172
- return img.info['prompt']
173
-
174
- def find_file_match(self, baseline_file: str, file_paths: List[str]):
175
- # Find a file in file_paths with matching metadata to baseline_file
176
- baseline_prompt = self.read_file_prompt(baseline_file)
177
-
178
- # Do not match empty prompts
179
- if baseline_prompt is None or baseline_prompt == "":
180
- return None
181
-
182
- # Find file match
183
- # Reorder test_file_names so that the file with matching name is first
184
- # This is an optimization because matching file names are more likely
185
- # to have matching metadata if they were generated with the same script
186
- basename = os.path.basename(baseline_file)
187
- file_path_basenames = [os.path.basename(f) for f in file_paths]
188
- if basename in file_path_basenames:
189
- match_index = file_path_basenames.index(basename)
190
- file_paths.insert(0, file_paths.pop(match_index))
191
-
192
- for f in file_paths:
193
- test_file_prompt = self.read_file_prompt(f)
194
- if baseline_prompt == test_file_prompt:
195
- return f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/conftest.py DELETED
@@ -1,36 +0,0 @@
1
- import os
2
- import pytest
3
-
4
- # Command line arguments for pytest
5
- def pytest_addoption(parser):
6
- parser.addoption('--output_dir', action="store", default='tests/inference/samples', help='Output directory for generated images')
7
- parser.addoption("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0", help="Specify the IP address to listen on (default: 127.0.0.1). If --listen is provided without an argument, it defaults to 0.0.0.0. (listens on all)")
8
- parser.addoption("--port", type=int, default=8188, help="Set the listen port.")
9
-
10
- # This initializes args at the beginning of the test session
11
- @pytest.fixture(scope="session", autouse=True)
12
- def args_pytest(pytestconfig):
13
- args = {}
14
- args['output_dir'] = pytestconfig.getoption('output_dir')
15
- args['listen'] = pytestconfig.getoption('listen')
16
- args['port'] = pytestconfig.getoption('port')
17
-
18
- os.makedirs(args['output_dir'], exist_ok=True)
19
-
20
- return args
21
-
22
- def pytest_collection_modifyitems(items):
23
- # Modifies items so tests run in the correct order
24
-
25
- LAST_TESTS = ['test_quality']
26
-
27
- # Move the last items to the end
28
- last_items = []
29
- for test_name in LAST_TESTS:
30
- for item in items.copy():
31
- print(item.module.__name__, item) # noqa: T201
32
- if item.module.__name__ == test_name:
33
- last_items.append(item)
34
- items.remove(item)
35
-
36
- items.extend(last_items)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/__init__.py DELETED
File without changes
tests/inference/graphs/default_graph_sdxl1_0.json DELETED
@@ -1,144 +0,0 @@
1
- {
2
- "4": {
3
- "inputs": {
4
- "ckpt_name": "sd_xl_base_1.0.safetensors"
5
- },
6
- "class_type": "CheckpointLoaderSimple"
7
- },
8
- "5": {
9
- "inputs": {
10
- "width": 1024,
11
- "height": 1024,
12
- "batch_size": 1
13
- },
14
- "class_type": "EmptyLatentImage"
15
- },
16
- "6": {
17
- "inputs": {
18
- "text": "a photo of a cat",
19
- "clip": [
20
- "4",
21
- 1
22
- ]
23
- },
24
- "class_type": "CLIPTextEncode"
25
- },
26
- "10": {
27
- "inputs": {
28
- "add_noise": "enable",
29
- "noise_seed": 42,
30
- "steps": 20,
31
- "cfg": 7.5,
32
- "sampler_name": "euler",
33
- "scheduler": "normal",
34
- "start_at_step": 0,
35
- "end_at_step": 32,
36
- "return_with_leftover_noise": "enable",
37
- "model": [
38
- "4",
39
- 0
40
- ],
41
- "positive": [
42
- "6",
43
- 0
44
- ],
45
- "negative": [
46
- "15",
47
- 0
48
- ],
49
- "latent_image": [
50
- "5",
51
- 0
52
- ]
53
- },
54
- "class_type": "KSamplerAdvanced"
55
- },
56
- "12": {
57
- "inputs": {
58
- "samples": [
59
- "14",
60
- 0
61
- ],
62
- "vae": [
63
- "4",
64
- 2
65
- ]
66
- },
67
- "class_type": "VAEDecode"
68
- },
69
- "13": {
70
- "inputs": {
71
- "filename_prefix": "test_inference",
72
- "images": [
73
- "12",
74
- 0
75
- ]
76
- },
77
- "class_type": "SaveImage"
78
- },
79
- "14": {
80
- "inputs": {
81
- "add_noise": "disable",
82
- "noise_seed": 42,
83
- "steps": 20,
84
- "cfg": 7.5,
85
- "sampler_name": "euler",
86
- "scheduler": "normal",
87
- "start_at_step": 32,
88
- "end_at_step": 10000,
89
- "return_with_leftover_noise": "disable",
90
- "model": [
91
- "16",
92
- 0
93
- ],
94
- "positive": [
95
- "17",
96
- 0
97
- ],
98
- "negative": [
99
- "20",
100
- 0
101
- ],
102
- "latent_image": [
103
- "10",
104
- 0
105
- ]
106
- },
107
- "class_type": "KSamplerAdvanced"
108
- },
109
- "15": {
110
- "inputs": {
111
- "conditioning": [
112
- "6",
113
- 0
114
- ]
115
- },
116
- "class_type": "ConditioningZeroOut"
117
- },
118
- "16": {
119
- "inputs": {
120
- "ckpt_name": "sd_xl_refiner_1.0.safetensors"
121
- },
122
- "class_type": "CheckpointLoaderSimple"
123
- },
124
- "17": {
125
- "inputs": {
126
- "text": "a photo of a cat",
127
- "clip": [
128
- "16",
129
- 1
130
- ]
131
- },
132
- "class_type": "CLIPTextEncode"
133
- },
134
- "20": {
135
- "inputs": {
136
- "text": "",
137
- "clip": [
138
- "16",
139
- 1
140
- ]
141
- },
142
- "class_type": "CLIPTextEncode"
143
- }
144
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/test_async_nodes.py DELETED
@@ -1,410 +0,0 @@
1
- import pytest
2
- import time
3
- import torch
4
- import urllib.error
5
- import numpy as np
6
- import subprocess
7
-
8
- from pytest import fixture
9
- from comfy_execution.graph_utils import GraphBuilder
10
- from tests.inference.test_execution import ComfyClient
11
-
12
-
13
- @pytest.mark.execution
14
- class TestAsyncNodes:
15
- @fixture(scope="class", autouse=True, params=[
16
- (False, 0),
17
- (True, 0),
18
- (True, 100),
19
- ])
20
- def _server(self, args_pytest, request):
21
- pargs = [
22
- 'python','main.py',
23
- '--output-directory', args_pytest["output_dir"],
24
- '--listen', args_pytest["listen"],
25
- '--port', str(args_pytest["port"]),
26
- '--extra-model-paths-config', 'tests/inference/extra_model_paths.yaml',
27
- ]
28
- use_lru, lru_size = request.param
29
- if use_lru:
30
- pargs += ['--cache-lru', str(lru_size)]
31
- # Running server with args: pargs
32
- p = subprocess.Popen(pargs)
33
- yield
34
- p.kill()
35
- torch.cuda.empty_cache()
36
-
37
- @fixture(scope="class", autouse=True)
38
- def shared_client(self, args_pytest, _server):
39
- client = ComfyClient()
40
- n_tries = 5
41
- for i in range(n_tries):
42
- time.sleep(4)
43
- try:
44
- client.connect(listen=args_pytest["listen"], port=args_pytest["port"])
45
- except ConnectionRefusedError:
46
- # Retrying...
47
- pass
48
- else:
49
- break
50
- yield client
51
- del client
52
- torch.cuda.empty_cache()
53
-
54
- @fixture
55
- def client(self, shared_client, request):
56
- shared_client.set_test_name(f"async_nodes[{request.node.name}]")
57
- yield shared_client
58
-
59
- @fixture
60
- def builder(self, request):
61
- yield GraphBuilder(prefix=request.node.name)
62
-
63
- # Happy Path Tests
64
-
65
- def test_basic_async_execution(self, client: ComfyClient, builder: GraphBuilder):
66
- """Test that a basic async node executes correctly."""
67
- g = builder
68
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
69
- sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.1)
70
- output = g.node("SaveImage", images=sleep_node.out(0))
71
-
72
- result = client.run(g)
73
-
74
- # Verify execution completed
75
- assert result.did_run(sleep_node), "Async sleep node should have executed"
76
- assert result.did_run(output), "Output node should have executed"
77
-
78
- # Verify the image passed through correctly
79
- result_images = result.get_images(output)
80
- assert len(result_images) == 1, "Should have 1 image"
81
- assert np.array(result_images[0]).min() == 0 and np.array(result_images[0]).max() == 0, "Image should be black"
82
-
83
- def test_multiple_async_parallel_execution(self, client: ComfyClient, builder: GraphBuilder):
84
- """Test that multiple async nodes execute in parallel."""
85
- g = builder
86
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
87
-
88
- # Create multiple async sleep nodes with different durations
89
- sleep1 = g.node("TestSleep", value=image.out(0), seconds=0.3)
90
- sleep2 = g.node("TestSleep", value=image.out(0), seconds=0.4)
91
- sleep3 = g.node("TestSleep", value=image.out(0), seconds=0.5)
92
-
93
- # Add outputs for each
94
- _output1 = g.node("PreviewImage", images=sleep1.out(0))
95
- _output2 = g.node("PreviewImage", images=sleep2.out(0))
96
- _output3 = g.node("PreviewImage", images=sleep3.out(0))
97
-
98
- start_time = time.time()
99
- result = client.run(g)
100
- elapsed_time = time.time() - start_time
101
-
102
- # Should take ~0.5s (max duration) not 1.2s (sum of durations)
103
- assert elapsed_time < 0.8, f"Parallel execution took {elapsed_time}s, expected < 0.8s"
104
-
105
- # Verify all nodes executed
106
- assert result.did_run(sleep1) and result.did_run(sleep2) and result.did_run(sleep3)
107
-
108
- def test_async_with_dependencies(self, client: ComfyClient, builder: GraphBuilder):
109
- """Test async nodes with proper dependency handling."""
110
- g = builder
111
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
112
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
113
-
114
- # Chain of async operations
115
- sleep1 = g.node("TestSleep", value=image1.out(0), seconds=0.2)
116
- sleep2 = g.node("TestSleep", value=image2.out(0), seconds=0.2)
117
-
118
- # Average depends on both async results
119
- average = g.node("TestVariadicAverage", input1=sleep1.out(0), input2=sleep2.out(0))
120
- output = g.node("SaveImage", images=average.out(0))
121
-
122
- result = client.run(g)
123
-
124
- # Verify execution order
125
- assert result.did_run(sleep1) and result.did_run(sleep2)
126
- assert result.did_run(average) and result.did_run(output)
127
-
128
- # Verify averaged result
129
- result_images = result.get_images(output)
130
- avg_value = np.array(result_images[0]).mean()
131
- assert abs(avg_value - 127.5) < 1, f"Average value {avg_value} should be ~127.5"
132
-
133
- def test_async_validate_inputs(self, client: ComfyClient, builder: GraphBuilder):
134
- """Test async VALIDATE_INPUTS function."""
135
- g = builder
136
- # Create a test node with async validation
137
- validation_node = g.node("TestAsyncValidation", value=5.0, threshold=10.0)
138
- g.node("SaveImage", images=validation_node.out(0))
139
-
140
- # Should pass validation
141
- result = client.run(g)
142
- assert result.did_run(validation_node)
143
-
144
- # Test validation failure
145
- validation_node.inputs['threshold'] = 3.0 # Will fail since value > threshold
146
- with pytest.raises(urllib.error.HTTPError):
147
- client.run(g)
148
-
149
- def test_async_lazy_evaluation(self, client: ComfyClient, builder: GraphBuilder):
150
- """Test async nodes with lazy evaluation."""
151
- g = builder
152
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
153
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
154
- mask = g.node("StubMask", value=0.0, height=512, width=512, batch_size=1)
155
-
156
- # Create async nodes that will be evaluated lazily
157
- sleep1 = g.node("TestSleep", value=input1.out(0), seconds=0.3)
158
- sleep2 = g.node("TestSleep", value=input2.out(0), seconds=0.3)
159
-
160
- # Use lazy mix that only needs sleep1 (mask=0.0)
161
- lazy_mix = g.node("TestLazyMixImages", image1=sleep1.out(0), image2=sleep2.out(0), mask=mask.out(0))
162
- g.node("SaveImage", images=lazy_mix.out(0))
163
-
164
- start_time = time.time()
165
- result = client.run(g)
166
- elapsed_time = time.time() - start_time
167
-
168
- # Should only execute sleep1, not sleep2
169
- assert elapsed_time < 0.5, f"Should skip sleep2, took {elapsed_time}s"
170
- assert result.did_run(sleep1), "Sleep1 should have executed"
171
- assert not result.did_run(sleep2), "Sleep2 should have been skipped"
172
-
173
- def test_async_check_lazy_status(self, client: ComfyClient, builder: GraphBuilder):
174
- """Test async check_lazy_status function."""
175
- g = builder
176
- # Create a node with async check_lazy_status
177
- lazy_node = g.node("TestAsyncLazyCheck",
178
- input1="value1",
179
- input2="value2",
180
- condition=True)
181
- g.node("SaveImage", images=lazy_node.out(0))
182
-
183
- result = client.run(g)
184
- assert result.did_run(lazy_node)
185
-
186
- # Error Handling Tests
187
-
188
- def test_async_execution_error(self, client: ComfyClient, builder: GraphBuilder):
189
- """Test that async execution errors are properly handled."""
190
- g = builder
191
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
192
- # Create an async node that will error
193
- error_node = g.node("TestAsyncError", value=image.out(0), error_after=0.1)
194
- g.node("SaveImage", images=error_node.out(0))
195
-
196
- try:
197
- client.run(g)
198
- assert False, "Should have raised an error"
199
- except Exception as e:
200
- assert 'prompt_id' in e.args[0], f"Did not get proper error message: {e}"
201
- assert e.args[0]['node_id'] == error_node.id, "Error should be from async error node"
202
-
203
- def test_async_validation_error(self, client: ComfyClient, builder: GraphBuilder):
204
- """Test async validation error handling."""
205
- g = builder
206
- # Node with async validation that will fail
207
- validation_node = g.node("TestAsyncValidationError", value=15.0, max_value=10.0)
208
- g.node("SaveImage", images=validation_node.out(0))
209
-
210
- with pytest.raises(urllib.error.HTTPError) as exc_info:
211
- client.run(g)
212
- # Verify it's a validation error
213
- assert exc_info.value.code == 400
214
-
215
- def test_async_timeout_handling(self, client: ComfyClient, builder: GraphBuilder):
216
- """Test handling of async operations that timeout."""
217
- g = builder
218
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
219
- # Very long sleep that would timeout
220
- timeout_node = g.node("TestAsyncTimeout", value=image.out(0), timeout=0.5, operation_time=2.0)
221
- g.node("SaveImage", images=timeout_node.out(0))
222
-
223
- try:
224
- client.run(g)
225
- assert False, "Should have raised a timeout error"
226
- except Exception as e:
227
- assert 'timeout' in str(e).lower(), f"Expected timeout error, got: {e}"
228
-
229
- def test_concurrent_async_error_recovery(self, client: ComfyClient, builder: GraphBuilder):
230
- """Test that workflow can recover after async errors."""
231
- g = builder
232
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
233
-
234
- # First run with error
235
- error_node = g.node("TestAsyncError", value=image.out(0), error_after=0.1)
236
- g.node("SaveImage", images=error_node.out(0))
237
-
238
- try:
239
- client.run(g)
240
- except Exception:
241
- pass # Expected
242
-
243
- # Second run should succeed
244
- g2 = GraphBuilder(prefix="recovery_test")
245
- image2 = g2.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
246
- sleep_node = g2.node("TestSleep", value=image2.out(0), seconds=0.1)
247
- g2.node("SaveImage", images=sleep_node.out(0))
248
-
249
- result = client.run(g2)
250
- assert result.did_run(sleep_node), "Should be able to run after error"
251
-
252
- def test_sync_error_during_async_execution(self, client: ComfyClient, builder: GraphBuilder):
253
- """Test handling when sync node errors while async node is executing."""
254
- g = builder
255
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
256
-
257
- # Async node that takes time
258
- sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.5)
259
-
260
- # Sync node that will error immediately
261
- error_node = g.node("TestSyncError", value=image.out(0))
262
-
263
- # Both feed into output
264
- g.node("PreviewImage", images=sleep_node.out(0))
265
- g.node("PreviewImage", images=error_node.out(0))
266
-
267
- try:
268
- client.run(g)
269
- assert False, "Should have raised an error"
270
- except Exception as e:
271
- # Verify the sync error was caught even though async was running
272
- assert 'prompt_id' in e.args[0]
273
-
274
- # Edge Cases
275
-
276
- def test_async_with_execution_blocker(self, client: ComfyClient, builder: GraphBuilder):
277
- """Test async nodes with execution blockers."""
278
- g = builder
279
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
280
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
281
-
282
- # Async sleep nodes
283
- sleep1 = g.node("TestSleep", value=image1.out(0), seconds=0.2)
284
- sleep2 = g.node("TestSleep", value=image2.out(0), seconds=0.2)
285
-
286
- # Create list of images
287
- image_list = g.node("TestMakeListNode", value1=sleep1.out(0), value2=sleep2.out(0))
288
-
289
- # Create list of blocking conditions - [False, True] to block only the second item
290
- int1 = g.node("StubInt", value=1)
291
- int2 = g.node("StubInt", value=2)
292
- block_list = g.node("TestMakeListNode", value1=int1.out(0), value2=int2.out(0))
293
-
294
- # Compare each value against 2, so first is False (1 != 2) and second is True (2 == 2)
295
- compare = g.node("TestIntConditions", a=block_list.out(0), b=2, operation="==")
296
-
297
- # Block based on the comparison results
298
- blocker = g.node("TestExecutionBlocker", input=image_list.out(0), block=compare.out(0), verbose=False)
299
-
300
- output = g.node("PreviewImage", images=blocker.out(0))
301
-
302
- result = client.run(g)
303
- images = result.get_images(output)
304
- assert len(images) == 1, "Should have blocked second image"
305
-
306
- def test_async_caching_behavior(self, client: ComfyClient, builder: GraphBuilder):
307
- """Test that async nodes are properly cached."""
308
- g = builder
309
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
310
- sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.2)
311
- g.node("SaveImage", images=sleep_node.out(0))
312
-
313
- # First run
314
- result1 = client.run(g)
315
- assert result1.did_run(sleep_node), "Should run first time"
316
-
317
- # Second run - should be cached
318
- start_time = time.time()
319
- result2 = client.run(g)
320
- elapsed_time = time.time() - start_time
321
-
322
- assert not result2.did_run(sleep_node), "Should be cached"
323
- assert elapsed_time < 0.1, f"Cached run took {elapsed_time}s, should be instant"
324
-
325
- def test_async_with_dynamic_prompts(self, client: ComfyClient, builder: GraphBuilder):
326
- """Test async nodes within dynamically generated prompts."""
327
- g = builder
328
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
329
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
330
-
331
- # Node that generates async nodes dynamically
332
- dynamic_async = g.node("TestDynamicAsyncGeneration",
333
- image1=image1.out(0),
334
- image2=image2.out(0),
335
- num_async_nodes=3,
336
- sleep_duration=0.2)
337
- g.node("SaveImage", images=dynamic_async.out(0))
338
-
339
- start_time = time.time()
340
- result = client.run(g)
341
- elapsed_time = time.time() - start_time
342
-
343
- # Should execute async nodes in parallel within dynamic prompt
344
- assert elapsed_time < 0.5, f"Dynamic async execution took {elapsed_time}s"
345
- assert result.did_run(dynamic_async)
346
-
347
- def test_async_resource_cleanup(self, client: ComfyClient, builder: GraphBuilder):
348
- """Test that async resources are properly cleaned up."""
349
- g = builder
350
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
351
-
352
- # Create multiple async nodes that use resources
353
- resource_nodes = []
354
- for i in range(5):
355
- node = g.node("TestAsyncResourceUser",
356
- value=image.out(0),
357
- resource_id=f"resource_{i}",
358
- duration=0.1)
359
- resource_nodes.append(node)
360
- g.node("PreviewImage", images=node.out(0))
361
-
362
- result = client.run(g)
363
-
364
- # Verify all nodes executed
365
- for node in resource_nodes:
366
- assert result.did_run(node)
367
-
368
- # Run again to ensure resources were cleaned up
369
- result2 = client.run(g)
370
- # Should be cached but not error due to resource conflicts
371
- for node in resource_nodes:
372
- assert not result2.did_run(node), "Should be cached"
373
-
374
- def test_async_cancellation(self, client: ComfyClient, builder: GraphBuilder):
375
- """Test cancellation of async operations."""
376
- # This would require implementing cancellation in the client
377
- # For now, we'll test that long-running async operations can be interrupted
378
- pass # TODO: Implement when cancellation API is available
379
-
380
- def test_mixed_sync_async_execution(self, client: ComfyClient, builder: GraphBuilder):
381
- """Test workflows with both sync and async nodes."""
382
- g = builder
383
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
384
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
385
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
386
-
387
- # Mix of sync and async operations
388
- # Sync: lazy mix images
389
- sync_op1 = g.node("TestLazyMixImages", image1=image1.out(0), image2=image2.out(0), mask=mask.out(0))
390
- # Async: sleep
391
- async_op1 = g.node("TestSleep", value=sync_op1.out(0), seconds=0.2)
392
- # Sync: custom validation
393
- sync_op2 = g.node("TestCustomValidation1", input1=async_op1.out(0), input2=0.5)
394
- # Async: sleep again
395
- async_op2 = g.node("TestSleep", value=sync_op2.out(0), seconds=0.2)
396
-
397
- output = g.node("SaveImage", images=async_op2.out(0))
398
-
399
- result = client.run(g)
400
-
401
- # Verify all nodes executed in correct order
402
- assert result.did_run(sync_op1)
403
- assert result.did_run(async_op1)
404
- assert result.did_run(sync_op2)
405
- assert result.did_run(async_op2)
406
-
407
- # Image should be a mix of black and white (gray)
408
- result_images = result.get_images(output)
409
- avg_value = np.array(result_images[0]).mean()
410
- assert abs(avg_value - 63.75) < 5, f"Average value {avg_value} should be ~63.75"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/test_execution.py DELETED
@@ -1,587 +0,0 @@
1
- from io import BytesIO
2
- import numpy
3
- from PIL import Image
4
- import pytest
5
- from pytest import fixture
6
- import time
7
- import torch
8
- from typing import Union, Dict
9
- import json
10
- import subprocess
11
- import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
12
- import uuid
13
- import urllib.request
14
- import urllib.parse
15
- import urllib.error
16
- from comfy_execution.graph_utils import GraphBuilder, Node
17
-
18
- class RunResult:
19
- def __init__(self, prompt_id: str):
20
- self.outputs: Dict[str,Dict] = {}
21
- self.runs: Dict[str,bool] = {}
22
- self.prompt_id: str = prompt_id
23
-
24
- def get_output(self, node: Node):
25
- return self.outputs.get(node.id, None)
26
-
27
- def did_run(self, node: Node):
28
- return self.runs.get(node.id, False)
29
-
30
- def get_images(self, node: Node):
31
- output = self.get_output(node)
32
- if output is None:
33
- return []
34
- return output.get('image_objects', [])
35
-
36
- def get_prompt_id(self):
37
- return self.prompt_id
38
-
39
- class ComfyClient:
40
- def __init__(self):
41
- self.test_name = ""
42
-
43
- def connect(self,
44
- listen:str = '127.0.0.1',
45
- port:Union[str,int] = 8188,
46
- client_id: str = str(uuid.uuid4())
47
- ):
48
- self.client_id = client_id
49
- self.server_address = f"{listen}:{port}"
50
- ws = websocket.WebSocket()
51
- ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id))
52
- self.ws = ws
53
-
54
- def queue_prompt(self, prompt):
55
- p = {"prompt": prompt, "client_id": self.client_id}
56
- data = json.dumps(p).encode('utf-8')
57
- req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data)
58
- return json.loads(urllib.request.urlopen(req).read())
59
-
60
- def get_image(self, filename, subfolder, folder_type):
61
- data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
62
- url_values = urllib.parse.urlencode(data)
63
- with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response:
64
- return response.read()
65
-
66
- def get_history(self, prompt_id):
67
- with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
68
- return json.loads(response.read())
69
-
70
- def set_test_name(self, name):
71
- self.test_name = name
72
-
73
- def run(self, graph):
74
- prompt = graph.finalize()
75
- for node in graph.nodes.values():
76
- if node.class_type == 'SaveImage':
77
- node.inputs['filename_prefix'] = self.test_name
78
-
79
- prompt_id = self.queue_prompt(prompt)['prompt_id']
80
- result = RunResult(prompt_id)
81
- while True:
82
- out = self.ws.recv()
83
- if isinstance(out, str):
84
- message = json.loads(out)
85
- if message['type'] == 'executing':
86
- data = message['data']
87
- if data['prompt_id'] != prompt_id:
88
- continue
89
- if data['node'] is None:
90
- break
91
- result.runs[data['node']] = True
92
- elif message['type'] == 'execution_error':
93
- raise Exception(message['data'])
94
- elif message['type'] == 'execution_cached':
95
- pass # Probably want to store this off for testing
96
-
97
- history = self.get_history(prompt_id)[prompt_id]
98
- for node_id in history['outputs']:
99
- node_output = history['outputs'][node_id]
100
- result.outputs[node_id] = node_output
101
- images_output = []
102
- if 'images' in node_output:
103
- for image in node_output['images']:
104
- image_data = self.get_image(image['filename'], image['subfolder'], image['type'])
105
- image_obj = Image.open(BytesIO(image_data))
106
- images_output.append(image_obj)
107
- node_output['image_objects'] = images_output
108
-
109
- return result
110
-
111
- #
112
- # Loop through these variables
113
- #
114
- @pytest.mark.execution
115
- class TestExecution:
116
- #
117
- # Initialize server and client
118
- #
119
- @fixture(scope="class", autouse=True, params=[
120
- # (use_lru, lru_size)
121
- (False, 0),
122
- (True, 0),
123
- (True, 100),
124
- ])
125
- def _server(self, args_pytest, request):
126
- # Start server
127
- pargs = [
128
- 'python','main.py',
129
- '--output-directory', args_pytest["output_dir"],
130
- '--listen', args_pytest["listen"],
131
- '--port', str(args_pytest["port"]),
132
- '--extra-model-paths-config', 'tests/inference/extra_model_paths.yaml',
133
- ]
134
- use_lru, lru_size = request.param
135
- if use_lru:
136
- pargs += ['--cache-lru', str(lru_size)]
137
- print("Running server with args:", pargs) # noqa: T201
138
- p = subprocess.Popen(pargs)
139
- yield
140
- p.kill()
141
- torch.cuda.empty_cache()
142
-
143
- def start_client(self, listen:str, port:int):
144
- # Start client
145
- comfy_client = ComfyClient()
146
- # Connect to server (with retries)
147
- n_tries = 5
148
- for i in range(n_tries):
149
- time.sleep(4)
150
- try:
151
- comfy_client.connect(listen=listen, port=port)
152
- except ConnectionRefusedError as e:
153
- print(e) # noqa: T201
154
- print(f"({i+1}/{n_tries}) Retrying...") # noqa: T201
155
- else:
156
- break
157
- return comfy_client
158
-
159
- @fixture(scope="class", autouse=True)
160
- def shared_client(self, args_pytest, _server):
161
- client = self.start_client(args_pytest["listen"], args_pytest["port"])
162
- yield client
163
- del client
164
- torch.cuda.empty_cache()
165
-
166
- @fixture
167
- def client(self, shared_client, request):
168
- shared_client.set_test_name(f"execution[{request.node.name}]")
169
- yield shared_client
170
-
171
- @fixture
172
- def builder(self, request):
173
- yield GraphBuilder(prefix=request.node.name)
174
-
175
- def test_lazy_input(self, client: ComfyClient, builder: GraphBuilder):
176
- g = builder
177
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
178
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
179
- mask = g.node("StubMask", value=0.0, height=512, width=512, batch_size=1)
180
-
181
- lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
182
- output = g.node("SaveImage", images=lazy_mix.out(0))
183
- result = client.run(g)
184
-
185
- result_image = result.get_images(output)[0]
186
- assert numpy.array(result_image).any() == 0, "Image should be black"
187
- assert result.did_run(input1)
188
- assert not result.did_run(input2)
189
- assert result.did_run(mask)
190
- assert result.did_run(lazy_mix)
191
-
192
- def test_full_cache(self, client: ComfyClient, builder: GraphBuilder):
193
- g = builder
194
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
195
- input2 = g.node("StubImage", content="NOISE", height=512, width=512, batch_size=1)
196
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
197
-
198
- lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
199
- g.node("SaveImage", images=lazy_mix.out(0))
200
-
201
- client.run(g)
202
- result2 = client.run(g)
203
- for node_id, node in g.nodes.items():
204
- assert not result2.did_run(node), f"Node {node_id} ran, but should have been cached"
205
-
206
- def test_partial_cache(self, client: ComfyClient, builder: GraphBuilder):
207
- g = builder
208
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
209
- input2 = g.node("StubImage", content="NOISE", height=512, width=512, batch_size=1)
210
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
211
-
212
- lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
213
- g.node("SaveImage", images=lazy_mix.out(0))
214
-
215
- client.run(g)
216
- mask.inputs['value'] = 0.4
217
- result2 = client.run(g)
218
- assert not result2.did_run(input1), "Input1 should have been cached"
219
- assert not result2.did_run(input2), "Input2 should have been cached"
220
-
221
- def test_error(self, client: ComfyClient, builder: GraphBuilder):
222
- g = builder
223
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
224
- # Different size of the two images
225
- input2 = g.node("StubImage", content="NOISE", height=256, width=256, batch_size=1)
226
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
227
-
228
- lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
229
- g.node("SaveImage", images=lazy_mix.out(0))
230
-
231
- try:
232
- client.run(g)
233
- assert False, "Should have raised an error"
234
- except Exception as e:
235
- assert 'prompt_id' in e.args[0], f"Did not get back a proper error message: {e}"
236
-
237
- @pytest.mark.parametrize("test_value, expect_error", [
238
- (5, True),
239
- ("foo", True),
240
- (5.0, False),
241
- ])
242
- def test_validation_error_literal(self, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
243
- g = builder
244
- validation1 = g.node("TestCustomValidation1", input1=test_value, input2=3.0)
245
- g.node("SaveImage", images=validation1.out(0))
246
-
247
- if expect_error:
248
- with pytest.raises(urllib.error.HTTPError):
249
- client.run(g)
250
- else:
251
- client.run(g)
252
-
253
- @pytest.mark.parametrize("test_type, test_value", [
254
- ("StubInt", 5),
255
- ("StubMask", 5.0)
256
- ])
257
- def test_validation_error_edge1(self, test_type, test_value, client: ComfyClient, builder: GraphBuilder):
258
- g = builder
259
- stub = g.node(test_type, value=test_value)
260
- validation1 = g.node("TestCustomValidation1", input1=stub.out(0), input2=3.0)
261
- g.node("SaveImage", images=validation1.out(0))
262
-
263
- with pytest.raises(urllib.error.HTTPError):
264
- client.run(g)
265
-
266
- @pytest.mark.parametrize("test_type, test_value, expect_error", [
267
- ("StubInt", 5, True),
268
- ("StubFloat", 5.0, False)
269
- ])
270
- def test_validation_error_edge2(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
271
- g = builder
272
- stub = g.node(test_type, value=test_value)
273
- validation2 = g.node("TestCustomValidation2", input1=stub.out(0), input2=3.0)
274
- g.node("SaveImage", images=validation2.out(0))
275
-
276
- if expect_error:
277
- with pytest.raises(urllib.error.HTTPError):
278
- client.run(g)
279
- else:
280
- client.run(g)
281
-
282
- @pytest.mark.parametrize("test_type, test_value, expect_error", [
283
- ("StubInt", 5, True),
284
- ("StubFloat", 5.0, False)
285
- ])
286
- def test_validation_error_edge3(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
287
- g = builder
288
- stub = g.node(test_type, value=test_value)
289
- validation3 = g.node("TestCustomValidation3", input1=stub.out(0), input2=3.0)
290
- g.node("SaveImage", images=validation3.out(0))
291
-
292
- if expect_error:
293
- with pytest.raises(urllib.error.HTTPError):
294
- client.run(g)
295
- else:
296
- client.run(g)
297
-
298
- @pytest.mark.parametrize("test_type, test_value, expect_error", [
299
- ("StubInt", 5, True),
300
- ("StubFloat", 5.0, False)
301
- ])
302
- def test_validation_error_edge4(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
303
- g = builder
304
- stub = g.node(test_type, value=test_value)
305
- validation4 = g.node("TestCustomValidation4", input1=stub.out(0), input2=3.0)
306
- g.node("SaveImage", images=validation4.out(0))
307
-
308
- if expect_error:
309
- with pytest.raises(urllib.error.HTTPError):
310
- client.run(g)
311
- else:
312
- client.run(g)
313
-
314
- @pytest.mark.parametrize("test_value1, test_value2, expect_error", [
315
- (0.0, 0.5, False),
316
- (0.0, 5.0, False),
317
- (0.0, 7.0, True)
318
- ])
319
- def test_validation_error_kwargs(self, test_value1, test_value2, expect_error, client: ComfyClient, builder: GraphBuilder):
320
- g = builder
321
- validation5 = g.node("TestCustomValidation5", input1=test_value1, input2=test_value2)
322
- g.node("SaveImage", images=validation5.out(0))
323
-
324
- if expect_error:
325
- with pytest.raises(urllib.error.HTTPError):
326
- client.run(g)
327
- else:
328
- client.run(g)
329
-
330
- def test_cycle_error(self, client: ComfyClient, builder: GraphBuilder):
331
- g = builder
332
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
333
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
334
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
335
-
336
- lazy_mix1 = g.node("TestLazyMixImages", image1=input1.out(0), mask=mask.out(0))
337
- lazy_mix2 = g.node("TestLazyMixImages", image1=lazy_mix1.out(0), image2=input2.out(0), mask=mask.out(0))
338
- g.node("SaveImage", images=lazy_mix2.out(0))
339
-
340
- # When the cycle exists on initial submission, it should raise a validation error
341
- with pytest.raises(urllib.error.HTTPError):
342
- client.run(g)
343
-
344
- def test_dynamic_cycle_error(self, client: ComfyClient, builder: GraphBuilder):
345
- g = builder
346
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
347
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
348
- generator = g.node("TestDynamicDependencyCycle", input1=input1.out(0), input2=input2.out(0))
349
- g.node("SaveImage", images=generator.out(0))
350
-
351
- # When the cycle is in a graph that is generated dynamically, it should raise a runtime error
352
- try:
353
- client.run(g)
354
- assert False, "Should have raised an error"
355
- except Exception as e:
356
- assert 'prompt_id' in e.args[0], f"Did not get back a proper error message: {e}"
357
- assert e.args[0]['node_id'] == generator.id, "Error should have been on the generator node"
358
-
359
- def test_missing_node_error(self, client: ComfyClient, builder: GraphBuilder):
360
- g = builder
361
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
362
- input2 = g.node("StubImage", id="removeme", content="WHITE", height=512, width=512, batch_size=1)
363
- input3 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
364
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
365
- mix1 = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
366
- mix2 = g.node("TestLazyMixImages", image1=input1.out(0), image2=input3.out(0), mask=mask.out(0))
367
- # We have multiple outputs. The first is invalid, but the second is valid
368
- g.node("SaveImage", images=mix1.out(0))
369
- g.node("SaveImage", images=mix2.out(0))
370
- g.remove_node("removeme")
371
-
372
- client.run(g)
373
-
374
- # Add back in the missing node to make sure the error doesn't break the server
375
- input2 = g.node("StubImage", id="removeme", content="WHITE", height=512, width=512, batch_size=1)
376
- client.run(g)
377
-
378
- def test_custom_is_changed(self, client: ComfyClient, builder: GraphBuilder):
379
- g = builder
380
- # Creating the nodes in this specific order previously caused a bug
381
- save = g.node("SaveImage")
382
- is_changed = g.node("TestCustomIsChanged", should_change=False)
383
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
384
-
385
- save.set_input('images', is_changed.out(0))
386
- is_changed.set_input('image', input1.out(0))
387
-
388
- result1 = client.run(g)
389
- result2 = client.run(g)
390
- is_changed.set_input('should_change', True)
391
- result3 = client.run(g)
392
- result4 = client.run(g)
393
- assert result1.did_run(is_changed), "is_changed should have been run"
394
- assert not result2.did_run(is_changed), "is_changed should have been cached"
395
- assert result3.did_run(is_changed), "is_changed should have been re-run"
396
- assert result4.did_run(is_changed), "is_changed should not have been cached"
397
-
398
- def test_undeclared_inputs(self, client: ComfyClient, builder: GraphBuilder):
399
- g = builder
400
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
401
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
402
- input3 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
403
- input4 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
404
- average = g.node("TestVariadicAverage", input1=input1.out(0), input2=input2.out(0), input3=input3.out(0), input4=input4.out(0))
405
- output = g.node("SaveImage", images=average.out(0))
406
-
407
- result = client.run(g)
408
- result_image = result.get_images(output)[0]
409
- expected = 255 // 4
410
- assert numpy.array(result_image).min() == expected and numpy.array(result_image).max() == expected, "Image should be grey"
411
-
412
- def test_for_loop(self, client: ComfyClient, builder: GraphBuilder):
413
- g = builder
414
- iterations = 4
415
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
416
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
417
- is_changed = g.node("TestCustomIsChanged", should_change=True, image=input2.out(0))
418
- for_open = g.node("TestForLoopOpen", remaining=iterations, initial_value1=is_changed.out(0))
419
- average = g.node("TestVariadicAverage", input1=input1.out(0), input2=for_open.out(2))
420
- for_close = g.node("TestForLoopClose", flow_control=for_open.out(0), initial_value1=average.out(0))
421
- output = g.node("SaveImage", images=for_close.out(0))
422
-
423
- for iterations in range(1, 5):
424
- for_open.set_input('remaining', iterations)
425
- result = client.run(g)
426
- result_image = result.get_images(output)[0]
427
- expected = 255 // (2 ** iterations)
428
- assert numpy.array(result_image).min() == expected and numpy.array(result_image).max() == expected, "Image should be grey"
429
- assert result.did_run(is_changed)
430
-
431
- def test_mixed_expansion_returns(self, client: ComfyClient, builder: GraphBuilder):
432
- g = builder
433
- val_list = g.node("TestMakeListNode", value1=0.1, value2=0.2, value3=0.3)
434
- mixed = g.node("TestMixedExpansionReturns", input1=val_list.out(0))
435
- output_dynamic = g.node("SaveImage", images=mixed.out(0))
436
- output_literal = g.node("SaveImage", images=mixed.out(1))
437
-
438
- result = client.run(g)
439
- images_dynamic = result.get_images(output_dynamic)
440
- assert len(images_dynamic) == 3, "Should have 2 images"
441
- assert numpy.array(images_dynamic[0]).min() == 25 and numpy.array(images_dynamic[0]).max() == 25, "First image should be 0.1"
442
- assert numpy.array(images_dynamic[1]).min() == 51 and numpy.array(images_dynamic[1]).max() == 51, "Second image should be 0.2"
443
- assert numpy.array(images_dynamic[2]).min() == 76 and numpy.array(images_dynamic[2]).max() == 76, "Third image should be 0.3"
444
-
445
- images_literal = result.get_images(output_literal)
446
- assert len(images_literal) == 3, "Should have 2 images"
447
- for i in range(3):
448
- assert numpy.array(images_literal[i]).min() == 255 and numpy.array(images_literal[i]).max() == 255, "All images should be white"
449
-
450
- def test_mixed_lazy_results(self, client: ComfyClient, builder: GraphBuilder):
451
- g = builder
452
- val_list = g.node("TestMakeListNode", value1=0.0, value2=0.5, value3=1.0)
453
- mask = g.node("StubMask", value=val_list.out(0), height=512, width=512, batch_size=1)
454
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
455
- input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
456
- mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
457
- rebatch = g.node("RebatchImages", images=mix.out(0), batch_size=3)
458
- output = g.node("SaveImage", images=rebatch.out(0))
459
-
460
- result = client.run(g)
461
- images = result.get_images(output)
462
- assert len(images) == 3, "Should have 3 image"
463
- assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be 0.0"
464
- assert numpy.array(images[1]).min() == 127 and numpy.array(images[1]).max() == 127, "Second image should be 0.5"
465
- assert numpy.array(images[2]).min() == 255 and numpy.array(images[2]).max() == 255, "Third image should be 1.0"
466
-
467
- def test_output_reuse(self, client: ComfyClient, builder: GraphBuilder):
468
- g = builder
469
- input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
470
-
471
- output1 = g.node("SaveImage", images=input1.out(0))
472
- output2 = g.node("SaveImage", images=input1.out(0))
473
-
474
- result = client.run(g)
475
- images1 = result.get_images(output1)
476
- images2 = result.get_images(output2)
477
- assert len(images1) == 1, "Should have 1 image"
478
- assert len(images2) == 1, "Should have 1 image"
479
-
480
-
481
- # This tests that only constant outputs are used in the call to `IS_CHANGED`
482
- def test_is_changed_with_outputs(self, client: ComfyClient, builder: GraphBuilder):
483
- g = builder
484
- input1 = g.node("StubConstantImage", value=0.5, height=512, width=512, batch_size=1)
485
- test_node = g.node("TestIsChangedWithConstants", image=input1.out(0), value=0.5)
486
-
487
- output = g.node("PreviewImage", images=test_node.out(0))
488
-
489
- result = client.run(g)
490
- images = result.get_images(output)
491
- assert len(images) == 1, "Should have 1 image"
492
- assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25"
493
-
494
- result = client.run(g)
495
- images = result.get_images(output)
496
- assert len(images) == 1, "Should have 1 image"
497
- assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25"
498
- assert not result.did_run(test_node), "The execution should have been cached"
499
-
500
- def test_parallel_sleep_nodes(self, client: ComfyClient, builder: GraphBuilder):
501
- g = builder
502
- image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
503
-
504
- # Create sleep nodes for each duration
505
- sleep_node1 = g.node("TestSleep", value=image.out(0), seconds=2.8)
506
- sleep_node2 = g.node("TestSleep", value=image.out(0), seconds=2.9)
507
- sleep_node3 = g.node("TestSleep", value=image.out(0), seconds=3.0)
508
-
509
- # Add outputs to verify the execution
510
- _output1 = g.node("PreviewImage", images=sleep_node1.out(0))
511
- _output2 = g.node("PreviewImage", images=sleep_node2.out(0))
512
- _output3 = g.node("PreviewImage", images=sleep_node3.out(0))
513
-
514
- start_time = time.time()
515
- result = client.run(g)
516
- elapsed_time = time.time() - start_time
517
-
518
- # The test should take around 0.4 seconds (the longest sleep duration)
519
- # plus some overhead, but definitely less than the sum of all sleeps (0.9s)
520
- # We'll allow for up to 0.8s total to account for overhead
521
- assert elapsed_time < 4.0, f"Parallel execution took {elapsed_time}s, expected less than 0.8s"
522
-
523
- # Verify that all nodes executed
524
- assert result.did_run(sleep_node1), "Sleep node 1 should have run"
525
- assert result.did_run(sleep_node2), "Sleep node 2 should have run"
526
- assert result.did_run(sleep_node3), "Sleep node 3 should have run"
527
-
528
- def test_parallel_sleep_expansion(self, client: ComfyClient, builder: GraphBuilder):
529
- g = builder
530
- # Create input images with different values
531
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
532
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
533
- image3 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
534
-
535
- # Create a TestParallelSleep node that expands into multiple TestSleep nodes
536
- parallel_sleep = g.node("TestParallelSleep",
537
- image1=image1.out(0),
538
- image2=image2.out(0),
539
- image3=image3.out(0),
540
- sleep1=0.4,
541
- sleep2=0.5,
542
- sleep3=0.6)
543
- output = g.node("SaveImage", images=parallel_sleep.out(0))
544
-
545
- start_time = time.time()
546
- result = client.run(g)
547
- elapsed_time = time.time() - start_time
548
-
549
- # Similar to the previous test, expect parallel execution of the sleep nodes
550
- # which should complete in less than the sum of all sleeps
551
- assert elapsed_time < 0.8, f"Expansion execution took {elapsed_time}s, expected less than 0.8s"
552
-
553
- # Verify the parallel sleep node executed
554
- assert result.did_run(parallel_sleep), "ParallelSleep node should have run"
555
-
556
- # Verify we get an image as output (blend of the three input images)
557
- result_images = result.get_images(output)
558
- assert len(result_images) == 1, "Should have 1 image"
559
- # Average pixel value should be around 170 (255 * 2 // 3)
560
- avg_value = numpy.array(result_images[0]).mean()
561
- assert avg_value == 170, f"Image average value {avg_value} should be 170"
562
-
563
- # This tests that nodes with OUTPUT_IS_LIST function correctly when they receive an ExecutionBlocker
564
- # as input. We also test that when that list (containing an ExecutionBlocker) is passed to a node,
565
- # only that one entry in the list is blocked.
566
- def test_execution_block_list_output(self, client: ComfyClient, builder: GraphBuilder):
567
- g = builder
568
- image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
569
- image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
570
- image3 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
571
- image_list = g.node("TestMakeListNode", value1=image1.out(0), value2=image2.out(0), value3=image3.out(0))
572
- int1 = g.node("StubInt", value=1)
573
- int2 = g.node("StubInt", value=2)
574
- int3 = g.node("StubInt", value=3)
575
- int_list = g.node("TestMakeListNode", value1=int1.out(0), value2=int2.out(0), value3=int3.out(0))
576
- compare = g.node("TestIntConditions", a=int_list.out(0), b=2, operation="==")
577
- blocker = g.node("TestExecutionBlocker", input=image_list.out(0), block=compare.out(0), verbose=False)
578
-
579
- list_output = g.node("TestMakeListNode", value1=blocker.out(0))
580
- output = g.node("PreviewImage", images=list_output.out(0))
581
-
582
- result = client.run(g)
583
- assert result.did_run(output), "The execution should have run"
584
- images = result.get_images(output)
585
- assert len(images) == 2, "Should have 2 images"
586
- assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be black"
587
- assert numpy.array(images[1]).min() == 0 and numpy.array(images[1]).max() == 0, "Second image should also be black"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/test_inference.py DELETED
@@ -1,237 +0,0 @@
1
- from copy import deepcopy
2
- from io import BytesIO
3
- import numpy
4
- import os
5
- from PIL import Image
6
- import pytest
7
- from pytest import fixture
8
- import time
9
- import torch
10
- from typing import Union
11
- import json
12
- import subprocess
13
- import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
14
- import uuid
15
- import urllib.request
16
- import urllib.parse
17
-
18
-
19
- from comfy.samplers import KSampler
20
-
21
- """
22
- These tests generate and save images through a range of parameters
23
- """
24
-
25
- class ComfyGraph:
26
- def __init__(self,
27
- graph: dict,
28
- sampler_nodes: list[str],
29
- ):
30
- self.graph = graph
31
- self.sampler_nodes = sampler_nodes
32
-
33
- def set_prompt(self, prompt, negative_prompt=None):
34
- # Sets the prompt for the sampler nodes (eg. base and refiner)
35
- for node in self.sampler_nodes:
36
- prompt_node = self.graph[node]['inputs']['positive'][0]
37
- self.graph[prompt_node]['inputs']['text'] = prompt
38
- if negative_prompt:
39
- negative_prompt_node = self.graph[node]['inputs']['negative'][0]
40
- self.graph[negative_prompt_node]['inputs']['text'] = negative_prompt
41
-
42
- def set_sampler_name(self, sampler_name:str, ):
43
- # sets the sampler name for the sampler nodes (eg. base and refiner)
44
- for node in self.sampler_nodes:
45
- self.graph[node]['inputs']['sampler_name'] = sampler_name
46
-
47
- def set_scheduler(self, scheduler:str):
48
- # sets the sampler name for the sampler nodes (eg. base and refiner)
49
- for node in self.sampler_nodes:
50
- self.graph[node]['inputs']['scheduler'] = scheduler
51
-
52
- def set_filename_prefix(self, prefix:str):
53
- # sets the filename prefix for the save nodes
54
- for node in self.graph:
55
- if self.graph[node]['class_type'] == 'SaveImage':
56
- self.graph[node]['inputs']['filename_prefix'] = prefix
57
-
58
-
59
- class ComfyClient:
60
- # From examples/websockets_api_example.py
61
-
62
- def connect(self,
63
- listen:str = '127.0.0.1',
64
- port:Union[str,int] = 8188,
65
- client_id: str = str(uuid.uuid4())
66
- ):
67
- self.client_id = client_id
68
- self.server_address = f"{listen}:{port}"
69
- ws = websocket.WebSocket()
70
- ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id))
71
- self.ws = ws
72
-
73
- def queue_prompt(self, prompt):
74
- p = {"prompt": prompt, "client_id": self.client_id}
75
- data = json.dumps(p).encode('utf-8')
76
- req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data)
77
- return json.loads(urllib.request.urlopen(req).read())
78
-
79
- def get_image(self, filename, subfolder, folder_type):
80
- data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
81
- url_values = urllib.parse.urlencode(data)
82
- with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response:
83
- return response.read()
84
-
85
- def get_history(self, prompt_id):
86
- with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
87
- return json.loads(response.read())
88
-
89
- def get_images(self, graph, save=True):
90
- prompt = graph
91
- if not save:
92
- # Replace save nodes with preview nodes
93
- prompt_str = json.dumps(prompt)
94
- prompt_str = prompt_str.replace('SaveImage', 'PreviewImage')
95
- prompt = json.loads(prompt_str)
96
-
97
- prompt_id = self.queue_prompt(prompt)['prompt_id']
98
- output_images = {}
99
- while True:
100
- out = self.ws.recv()
101
- if isinstance(out, str):
102
- message = json.loads(out)
103
- if message['type'] == 'executing':
104
- data = message['data']
105
- if data['node'] is None and data['prompt_id'] == prompt_id:
106
- break #Execution is done
107
- else:
108
- continue #previews are binary data
109
-
110
- history = self.get_history(prompt_id)[prompt_id]
111
- for node_id in history['outputs']:
112
- node_output = history['outputs'][node_id]
113
- images_output = []
114
- if 'images' in node_output:
115
- for image in node_output['images']:
116
- image_data = self.get_image(image['filename'], image['subfolder'], image['type'])
117
- images_output.append(image_data)
118
- output_images[node_id] = images_output
119
-
120
- return output_images
121
-
122
- #
123
- # Initialize graphs
124
- #
125
- default_graph_file = 'tests/inference/graphs/default_graph_sdxl1_0.json'
126
- with open(default_graph_file, 'r') as file:
127
- default_graph = json.loads(file.read())
128
- DEFAULT_COMFY_GRAPH = ComfyGraph(graph=default_graph, sampler_nodes=['10','14'])
129
- DEFAULT_COMFY_GRAPH_ID = os.path.splitext(os.path.basename(default_graph_file))[0]
130
-
131
- #
132
- # Loop through these variables
133
- #
134
- comfy_graph_list = [DEFAULT_COMFY_GRAPH]
135
- comfy_graph_ids = [DEFAULT_COMFY_GRAPH_ID]
136
- prompt_list = [
137
- 'a painting of a cat',
138
- ]
139
-
140
- sampler_list = KSampler.SAMPLERS
141
- scheduler_list = KSampler.SCHEDULERS
142
-
143
- @pytest.mark.inference
144
- @pytest.mark.parametrize("sampler", sampler_list)
145
- @pytest.mark.parametrize("scheduler", scheduler_list)
146
- @pytest.mark.parametrize("prompt", prompt_list)
147
- class TestInference:
148
- #
149
- # Initialize server and client
150
- #
151
- @fixture(scope="class", autouse=True)
152
- def _server(self, args_pytest):
153
- # Start server
154
- p = subprocess.Popen([
155
- 'python','main.py',
156
- '--output-directory', args_pytest["output_dir"],
157
- '--listen', args_pytest["listen"],
158
- '--port', str(args_pytest["port"]),
159
- ])
160
- yield
161
- p.kill()
162
- torch.cuda.empty_cache()
163
-
164
- def start_client(self, listen:str, port:int):
165
- # Start client
166
- comfy_client = ComfyClient()
167
- # Connect to server (with retries)
168
- n_tries = 5
169
- for i in range(n_tries):
170
- time.sleep(4)
171
- try:
172
- comfy_client.connect(listen=listen, port=port)
173
- except ConnectionRefusedError as e:
174
- print(e) # noqa: T201
175
- print(f"({i+1}/{n_tries}) Retrying...") # noqa: T201
176
- else:
177
- break
178
- return comfy_client
179
-
180
- #
181
- # Client and graph fixtures with server warmup
182
- #
183
- # Returns a "_client_graph", which is client-graph pair corresponding to an initialized server
184
- # The "graph" is the default graph
185
- @fixture(scope="class", params=comfy_graph_list, ids=comfy_graph_ids, autouse=True)
186
- def _client_graph(self, request, args_pytest, _server) -> (ComfyClient, ComfyGraph):
187
- comfy_graph = request.param
188
-
189
- # Start client
190
- comfy_client = self.start_client(args_pytest["listen"], args_pytest["port"])
191
-
192
- # Warm up pipeline
193
- comfy_client.get_images(graph=comfy_graph.graph, save=False)
194
-
195
- yield comfy_client, comfy_graph
196
- del comfy_client
197
- del comfy_graph
198
- torch.cuda.empty_cache()
199
-
200
- @fixture
201
- def client(self, _client_graph):
202
- client = _client_graph[0]
203
- yield client
204
-
205
- @fixture
206
- def comfy_graph(self, _client_graph):
207
- # avoid mutating the graph
208
- graph = deepcopy(_client_graph[1])
209
- yield graph
210
-
211
- def test_comfy(
212
- self,
213
- client,
214
- comfy_graph,
215
- sampler,
216
- scheduler,
217
- prompt,
218
- request
219
- ):
220
- test_info = request.node.name
221
- comfy_graph.set_filename_prefix(test_info)
222
- # Settings for comfy graph
223
- comfy_graph.set_sampler_name(sampler)
224
- comfy_graph.set_scheduler(scheduler)
225
- comfy_graph.set_prompt(prompt)
226
-
227
- # Generate
228
- images = client.get_images(comfy_graph.graph)
229
-
230
- assert len(images) != 0, "No images generated"
231
- # assert all images are not blank
232
- for images_output in images.values():
233
- for image_data in images_output:
234
- pil_image = Image.open(BytesIO(image_data))
235
- assert numpy.array(pil_image).any() != 0, "Image is blank"
236
-
237
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/__init__.py DELETED
@@ -1,26 +0,0 @@
1
- from .specific_tests import TEST_NODE_CLASS_MAPPINGS, TEST_NODE_DISPLAY_NAME_MAPPINGS
2
- from .flow_control import FLOW_CONTROL_NODE_CLASS_MAPPINGS, FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS
3
- from .util import UTILITY_NODE_CLASS_MAPPINGS, UTILITY_NODE_DISPLAY_NAME_MAPPINGS
4
- from .conditions import CONDITION_NODE_CLASS_MAPPINGS, CONDITION_NODE_DISPLAY_NAME_MAPPINGS
5
- from .stubs import TEST_STUB_NODE_CLASS_MAPPINGS, TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS
6
- from .async_test_nodes import ASYNC_TEST_NODE_CLASS_MAPPINGS, ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS
7
-
8
- # NODE_CLASS_MAPPINGS = GENERAL_NODE_CLASS_MAPPINGS.update(COMPONENT_NODE_CLASS_MAPPINGS)
9
- # NODE_DISPLAY_NAME_MAPPINGS = GENERAL_NODE_DISPLAY_NAME_MAPPINGS.update(COMPONENT_NODE_DISPLAY_NAME_MAPPINGS)
10
-
11
- NODE_CLASS_MAPPINGS = {}
12
- NODE_CLASS_MAPPINGS.update(TEST_NODE_CLASS_MAPPINGS)
13
- NODE_CLASS_MAPPINGS.update(FLOW_CONTROL_NODE_CLASS_MAPPINGS)
14
- NODE_CLASS_MAPPINGS.update(UTILITY_NODE_CLASS_MAPPINGS)
15
- NODE_CLASS_MAPPINGS.update(CONDITION_NODE_CLASS_MAPPINGS)
16
- NODE_CLASS_MAPPINGS.update(TEST_STUB_NODE_CLASS_MAPPINGS)
17
- NODE_CLASS_MAPPINGS.update(ASYNC_TEST_NODE_CLASS_MAPPINGS)
18
-
19
- NODE_DISPLAY_NAME_MAPPINGS = {}
20
- NODE_DISPLAY_NAME_MAPPINGS.update(TEST_NODE_DISPLAY_NAME_MAPPINGS)
21
- NODE_DISPLAY_NAME_MAPPINGS.update(FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS)
22
- NODE_DISPLAY_NAME_MAPPINGS.update(UTILITY_NODE_DISPLAY_NAME_MAPPINGS)
23
- NODE_DISPLAY_NAME_MAPPINGS.update(CONDITION_NODE_DISPLAY_NAME_MAPPINGS)
24
- NODE_DISPLAY_NAME_MAPPINGS.update(TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS)
25
- NODE_DISPLAY_NAME_MAPPINGS.update(ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS)
26
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/async_test_nodes.py DELETED
@@ -1,343 +0,0 @@
1
- import torch
2
- import asyncio
3
- from typing import Dict
4
- from comfy.utils import ProgressBar
5
- from comfy_execution.graph_utils import GraphBuilder
6
- from comfy.comfy_types.node_typing import ComfyNodeABC
7
- from comfy.comfy_types import IO
8
-
9
-
10
- class TestAsyncValidation(ComfyNodeABC):
11
- """Test node with async VALIDATE_INPUTS."""
12
-
13
- @classmethod
14
- def INPUT_TYPES(cls):
15
- return {
16
- "required": {
17
- "value": ("FLOAT", {"default": 5.0}),
18
- "threshold": ("FLOAT", {"default": 10.0}),
19
- },
20
- }
21
-
22
- RETURN_TYPES = ("IMAGE",)
23
- FUNCTION = "process"
24
- CATEGORY = "_for_testing/async"
25
-
26
- @classmethod
27
- async def VALIDATE_INPUTS(cls, value, threshold):
28
- # Simulate async validation (e.g., checking remote service)
29
- await asyncio.sleep(0.05)
30
-
31
- if value > threshold:
32
- return f"Value {value} exceeds threshold {threshold}"
33
- return True
34
-
35
- def process(self, value, threshold):
36
- # Create image based on value
37
- intensity = value / 10.0
38
- image = torch.ones([1, 512, 512, 3]) * intensity
39
- return (image,)
40
-
41
-
42
- class TestAsyncError(ComfyNodeABC):
43
- """Test node that errors during async execution."""
44
-
45
- @classmethod
46
- def INPUT_TYPES(cls):
47
- return {
48
- "required": {
49
- "value": (IO.ANY, {}),
50
- "error_after": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 10.0}),
51
- },
52
- }
53
-
54
- RETURN_TYPES = (IO.ANY,)
55
- FUNCTION = "error_execution"
56
- CATEGORY = "_for_testing/async"
57
-
58
- async def error_execution(self, value, error_after):
59
- await asyncio.sleep(error_after)
60
- raise RuntimeError("Intentional async execution error for testing")
61
-
62
-
63
- class TestAsyncValidationError(ComfyNodeABC):
64
- """Test node with async validation that always fails."""
65
-
66
- @classmethod
67
- def INPUT_TYPES(cls):
68
- return {
69
- "required": {
70
- "value": ("FLOAT", {"default": 5.0}),
71
- "max_value": ("FLOAT", {"default": 10.0}),
72
- },
73
- }
74
-
75
- RETURN_TYPES = ("IMAGE",)
76
- FUNCTION = "process"
77
- CATEGORY = "_for_testing/async"
78
-
79
- @classmethod
80
- async def VALIDATE_INPUTS(cls, value, max_value):
81
- await asyncio.sleep(0.05)
82
- # Always fail validation for values > max_value
83
- if value > max_value:
84
- return f"Async validation failed: {value} > {max_value}"
85
- return True
86
-
87
- def process(self, value, max_value):
88
- # This won't be reached if validation fails
89
- image = torch.ones([1, 512, 512, 3]) * (value / max_value)
90
- return (image,)
91
-
92
-
93
- class TestAsyncTimeout(ComfyNodeABC):
94
- """Test node that simulates timeout scenarios."""
95
-
96
- @classmethod
97
- def INPUT_TYPES(cls):
98
- return {
99
- "required": {
100
- "value": (IO.ANY, {}),
101
- "timeout": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0}),
102
- "operation_time": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 10.0}),
103
- },
104
- }
105
-
106
- RETURN_TYPES = (IO.ANY,)
107
- FUNCTION = "timeout_execution"
108
- CATEGORY = "_for_testing/async"
109
-
110
- async def timeout_execution(self, value, timeout, operation_time):
111
- try:
112
- # This will timeout if operation_time > timeout
113
- await asyncio.wait_for(asyncio.sleep(operation_time), timeout=timeout)
114
- return (value,)
115
- except asyncio.TimeoutError:
116
- raise RuntimeError(f"Operation timed out after {timeout} seconds")
117
-
118
-
119
- class TestSyncError(ComfyNodeABC):
120
- """Test node that errors synchronously (for mixed sync/async testing)."""
121
-
122
- @classmethod
123
- def INPUT_TYPES(cls):
124
- return {
125
- "required": {
126
- "value": (IO.ANY, {}),
127
- },
128
- }
129
-
130
- RETURN_TYPES = (IO.ANY,)
131
- FUNCTION = "sync_error"
132
- CATEGORY = "_for_testing/async"
133
-
134
- def sync_error(self, value):
135
- raise RuntimeError("Intentional sync execution error for testing")
136
-
137
-
138
- class TestAsyncLazyCheck(ComfyNodeABC):
139
- """Test node with async check_lazy_status."""
140
-
141
- @classmethod
142
- def INPUT_TYPES(cls):
143
- return {
144
- "required": {
145
- "input1": (IO.ANY, {"lazy": True}),
146
- "input2": (IO.ANY, {"lazy": True}),
147
- "condition": ("BOOLEAN", {"default": True}),
148
- },
149
- }
150
-
151
- RETURN_TYPES = ("IMAGE",)
152
- FUNCTION = "process"
153
- CATEGORY = "_for_testing/async"
154
-
155
- async def check_lazy_status(self, condition, input1, input2):
156
- # Simulate async checking (e.g., querying remote service)
157
- await asyncio.sleep(0.05)
158
-
159
- needed = []
160
- if condition and input1 is None:
161
- needed.append("input1")
162
- if not condition and input2 is None:
163
- needed.append("input2")
164
- return needed
165
-
166
- def process(self, input1, input2, condition):
167
- # Return a simple image
168
- return (torch.ones([1, 512, 512, 3]),)
169
-
170
-
171
- class TestDynamicAsyncGeneration(ComfyNodeABC):
172
- """Test node that dynamically generates async nodes."""
173
-
174
- @classmethod
175
- def INPUT_TYPES(cls):
176
- return {
177
- "required": {
178
- "image1": ("IMAGE",),
179
- "image2": ("IMAGE",),
180
- "num_async_nodes": ("INT", {"default": 3, "min": 1, "max": 10}),
181
- "sleep_duration": ("FLOAT", {"default": 0.2, "min": 0.1, "max": 1.0}),
182
- },
183
- }
184
-
185
- RETURN_TYPES = ("IMAGE",)
186
- FUNCTION = "generate_async_workflow"
187
- CATEGORY = "_for_testing/async"
188
-
189
- def generate_async_workflow(self, image1, image2, num_async_nodes, sleep_duration):
190
- g = GraphBuilder()
191
-
192
- # Create multiple async sleep nodes
193
- sleep_nodes = []
194
- for i in range(num_async_nodes):
195
- image = image1 if i % 2 == 0 else image2
196
- sleep_node = g.node("TestSleep", value=image, seconds=sleep_duration)
197
- sleep_nodes.append(sleep_node)
198
-
199
- # Average all results
200
- if len(sleep_nodes) == 1:
201
- final_node = sleep_nodes[0]
202
- else:
203
- avg_inputs = {"input1": sleep_nodes[0].out(0)}
204
- for i, node in enumerate(sleep_nodes[1:], 2):
205
- avg_inputs[f"input{i}"] = node.out(0)
206
- final_node = g.node("TestVariadicAverage", **avg_inputs)
207
-
208
- return {
209
- "result": (final_node.out(0),),
210
- "expand": g.finalize(),
211
- }
212
-
213
-
214
- class TestAsyncResourceUser(ComfyNodeABC):
215
- """Test node that uses resources during async execution."""
216
-
217
- # Class-level resource tracking for testing
218
- _active_resources: Dict[str, bool] = {}
219
-
220
- @classmethod
221
- def INPUT_TYPES(cls):
222
- return {
223
- "required": {
224
- "value": (IO.ANY, {}),
225
- "resource_id": ("STRING", {"default": "resource_0"}),
226
- "duration": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0}),
227
- },
228
- }
229
-
230
- RETURN_TYPES = (IO.ANY,)
231
- FUNCTION = "use_resource"
232
- CATEGORY = "_for_testing/async"
233
-
234
- async def use_resource(self, value, resource_id, duration):
235
- # Check if resource is already in use
236
- if self._active_resources.get(resource_id, False):
237
- raise RuntimeError(f"Resource {resource_id} is already in use!")
238
-
239
- # Mark resource as in use
240
- self._active_resources[resource_id] = True
241
-
242
- try:
243
- # Simulate resource usage
244
- await asyncio.sleep(duration)
245
- return (value,)
246
- finally:
247
- # Always clean up resource
248
- self._active_resources[resource_id] = False
249
-
250
-
251
- class TestAsyncBatchProcessing(ComfyNodeABC):
252
- """Test async processing of batched inputs."""
253
-
254
- @classmethod
255
- def INPUT_TYPES(cls):
256
- return {
257
- "required": {
258
- "images": ("IMAGE",),
259
- "process_time_per_item": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0}),
260
- },
261
- "hidden": {
262
- "unique_id": "UNIQUE_ID",
263
- },
264
- }
265
-
266
- RETURN_TYPES = ("IMAGE",)
267
- FUNCTION = "process_batch"
268
- CATEGORY = "_for_testing/async"
269
-
270
- async def process_batch(self, images, process_time_per_item, unique_id):
271
- batch_size = images.shape[0]
272
- pbar = ProgressBar(batch_size, node_id=unique_id)
273
-
274
- # Process each image in the batch
275
- processed = []
276
- for i in range(batch_size):
277
- # Simulate async processing
278
- await asyncio.sleep(process_time_per_item)
279
-
280
- # Simple processing: invert the image
281
- processed_image = 1.0 - images[i:i+1]
282
- processed.append(processed_image)
283
-
284
- pbar.update(1)
285
-
286
- # Stack processed images
287
- result = torch.cat(processed, dim=0)
288
- return (result,)
289
-
290
-
291
- class TestAsyncConcurrentLimit(ComfyNodeABC):
292
- """Test concurrent execution limits for async nodes."""
293
-
294
- _semaphore = asyncio.Semaphore(2) # Only allow 2 concurrent executions
295
-
296
- @classmethod
297
- def INPUT_TYPES(cls):
298
- return {
299
- "required": {
300
- "value": (IO.ANY, {}),
301
- "duration": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0}),
302
- "node_id": ("INT", {"default": 0}),
303
- },
304
- }
305
-
306
- RETURN_TYPES = (IO.ANY,)
307
- FUNCTION = "limited_execution"
308
- CATEGORY = "_for_testing/async"
309
-
310
- async def limited_execution(self, value, duration, node_id):
311
- async with self._semaphore:
312
- # Node {node_id} acquired semaphore
313
- await asyncio.sleep(duration)
314
- # Node {node_id} releasing semaphore
315
- return (value,)
316
-
317
-
318
- # Add node mappings
319
- ASYNC_TEST_NODE_CLASS_MAPPINGS = {
320
- "TestAsyncValidation": TestAsyncValidation,
321
- "TestAsyncError": TestAsyncError,
322
- "TestAsyncValidationError": TestAsyncValidationError,
323
- "TestAsyncTimeout": TestAsyncTimeout,
324
- "TestSyncError": TestSyncError,
325
- "TestAsyncLazyCheck": TestAsyncLazyCheck,
326
- "TestDynamicAsyncGeneration": TestDynamicAsyncGeneration,
327
- "TestAsyncResourceUser": TestAsyncResourceUser,
328
- "TestAsyncBatchProcessing": TestAsyncBatchProcessing,
329
- "TestAsyncConcurrentLimit": TestAsyncConcurrentLimit,
330
- }
331
-
332
- ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS = {
333
- "TestAsyncValidation": "Test Async Validation",
334
- "TestAsyncError": "Test Async Error",
335
- "TestAsyncValidationError": "Test Async Validation Error",
336
- "TestAsyncTimeout": "Test Async Timeout",
337
- "TestSyncError": "Test Sync Error",
338
- "TestAsyncLazyCheck": "Test Async Lazy Check",
339
- "TestDynamicAsyncGeneration": "Test Dynamic Async Generation",
340
- "TestAsyncResourceUser": "Test Async Resource User",
341
- "TestAsyncBatchProcessing": "Test Async Batch Processing",
342
- "TestAsyncConcurrentLimit": "Test Async Concurrent Limit",
343
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/conditions.py DELETED
@@ -1,194 +0,0 @@
1
- import re
2
- import torch
3
-
4
- class TestIntConditions:
5
- def __init__(self):
6
- pass
7
-
8
- @classmethod
9
- def INPUT_TYPES(cls):
10
- return {
11
- "required": {
12
- "a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
13
- "b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
14
- "operation": (["==", "!=", "<", ">", "<=", ">="],),
15
- },
16
- }
17
-
18
- RETURN_TYPES = ("BOOLEAN",)
19
- FUNCTION = "int_condition"
20
-
21
- CATEGORY = "Testing/Logic"
22
-
23
- def int_condition(self, a, b, operation):
24
- if operation == "==":
25
- return (a == b,)
26
- elif operation == "!=":
27
- return (a != b,)
28
- elif operation == "<":
29
- return (a < b,)
30
- elif operation == ">":
31
- return (a > b,)
32
- elif operation == "<=":
33
- return (a <= b,)
34
- elif operation == ">=":
35
- return (a >= b,)
36
-
37
-
38
- class TestFloatConditions:
39
- def __init__(self):
40
- pass
41
-
42
- @classmethod
43
- def INPUT_TYPES(cls):
44
- return {
45
- "required": {
46
- "a": ("FLOAT", {"default": 0, "min": -999999999999.0, "max": 999999999999.0, "step": 1}),
47
- "b": ("FLOAT", {"default": 0, "min": -999999999999.0, "max": 999999999999.0, "step": 1}),
48
- "operation": (["==", "!=", "<", ">", "<=", ">="],),
49
- },
50
- }
51
-
52
- RETURN_TYPES = ("BOOLEAN",)
53
- FUNCTION = "float_condition"
54
-
55
- CATEGORY = "Testing/Logic"
56
-
57
- def float_condition(self, a, b, operation):
58
- if operation == "==":
59
- return (a == b,)
60
- elif operation == "!=":
61
- return (a != b,)
62
- elif operation == "<":
63
- return (a < b,)
64
- elif operation == ">":
65
- return (a > b,)
66
- elif operation == "<=":
67
- return (a <= b,)
68
- elif operation == ">=":
69
- return (a >= b,)
70
-
71
- class TestStringConditions:
72
- def __init__(self):
73
- pass
74
-
75
- @classmethod
76
- def INPUT_TYPES(cls):
77
- return {
78
- "required": {
79
- "a": ("STRING", {"multiline": False}),
80
- "b": ("STRING", {"multiline": False}),
81
- "operation": (["a == b", "a != b", "a IN b", "a MATCH REGEX(b)", "a BEGINSWITH b", "a ENDSWITH b"],),
82
- "case_sensitive": ("BOOLEAN", {"default": True}),
83
- },
84
- }
85
-
86
- RETURN_TYPES = ("BOOLEAN",)
87
- FUNCTION = "string_condition"
88
-
89
- CATEGORY = "Testing/Logic"
90
-
91
- def string_condition(self, a, b, operation, case_sensitive):
92
- if not case_sensitive:
93
- a = a.lower()
94
- b = b.lower()
95
-
96
- if operation == "a == b":
97
- return (a == b,)
98
- elif operation == "a != b":
99
- return (a != b,)
100
- elif operation == "a IN b":
101
- return (a in b,)
102
- elif operation == "a MATCH REGEX(b)":
103
- try:
104
- return (re.match(b, a) is not None,)
105
- except:
106
- return (False,)
107
- elif operation == "a BEGINSWITH b":
108
- return (a.startswith(b),)
109
- elif operation == "a ENDSWITH b":
110
- return (a.endswith(b),)
111
-
112
- class TestToBoolNode:
113
- def __init__(self):
114
- pass
115
-
116
- @classmethod
117
- def INPUT_TYPES(cls):
118
- return {
119
- "required": {
120
- "value": ("*",),
121
- },
122
- "optional": {
123
- "invert": ("BOOLEAN", {"default": False}),
124
- },
125
- }
126
-
127
- RETURN_TYPES = ("BOOLEAN",)
128
- FUNCTION = "to_bool"
129
-
130
- CATEGORY = "Testing/Logic"
131
-
132
- def to_bool(self, value, invert = False):
133
- if isinstance(value, torch.Tensor):
134
- if value.max().item() == 0 and value.min().item() == 0:
135
- result = False
136
- else:
137
- result = True
138
- else:
139
- try:
140
- result = bool(value)
141
- except:
142
- # Can't convert it? Well then it's something or other. I dunno, I'm not a Python programmer.
143
- result = True
144
-
145
- if invert:
146
- result = not result
147
-
148
- return (result,)
149
-
150
- class TestBoolOperationNode:
151
- def __init__(self):
152
- pass
153
-
154
- @classmethod
155
- def INPUT_TYPES(cls):
156
- return {
157
- "required": {
158
- "a": ("BOOLEAN",),
159
- "b": ("BOOLEAN",),
160
- "op": (["a AND b", "a OR b", "a XOR b", "NOT a"],),
161
- },
162
- }
163
-
164
- RETURN_TYPES = ("BOOLEAN",)
165
- FUNCTION = "bool_operation"
166
-
167
- CATEGORY = "Testing/Logic"
168
-
169
- def bool_operation(self, a, b, op):
170
- if op == "a AND b":
171
- return (a and b,)
172
- elif op == "a OR b":
173
- return (a or b,)
174
- elif op == "a XOR b":
175
- return (a ^ b,)
176
- elif op == "NOT a":
177
- return (not a,)
178
-
179
-
180
- CONDITION_NODE_CLASS_MAPPINGS = {
181
- "TestIntConditions": TestIntConditions,
182
- "TestFloatConditions": TestFloatConditions,
183
- "TestStringConditions": TestStringConditions,
184
- "TestToBoolNode": TestToBoolNode,
185
- "TestBoolOperationNode": TestBoolOperationNode,
186
- }
187
-
188
- CONDITION_NODE_DISPLAY_NAME_MAPPINGS = {
189
- "TestIntConditions": "Int Condition",
190
- "TestFloatConditions": "Float Condition",
191
- "TestStringConditions": "String Condition",
192
- "TestToBoolNode": "To Bool",
193
- "TestBoolOperationNode": "Bool Operation",
194
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/flow_control.py DELETED
@@ -1,173 +0,0 @@
1
- from comfy_execution.graph_utils import GraphBuilder, is_link
2
- from comfy_execution.graph import ExecutionBlocker
3
- from .tools import VariantSupport
4
-
5
- NUM_FLOW_SOCKETS = 5
6
- @VariantSupport()
7
- class TestWhileLoopOpen:
8
- def __init__(self):
9
- pass
10
-
11
- @classmethod
12
- def INPUT_TYPES(cls):
13
- inputs = {
14
- "required": {
15
- "condition": ("BOOLEAN", {"default": True}),
16
- },
17
- "optional": {
18
- },
19
- }
20
- for i in range(NUM_FLOW_SOCKETS):
21
- inputs["optional"][f"initial_value{i}"] = ("*",)
22
- return inputs
23
-
24
- RETURN_TYPES = tuple(["FLOW_CONTROL"] + ["*"] * NUM_FLOW_SOCKETS)
25
- RETURN_NAMES = tuple(["FLOW_CONTROL"] + [f"value{i}" for i in range(NUM_FLOW_SOCKETS)])
26
- FUNCTION = "while_loop_open"
27
-
28
- CATEGORY = "Testing/Flow"
29
-
30
- def while_loop_open(self, condition, **kwargs):
31
- values = []
32
- for i in range(NUM_FLOW_SOCKETS):
33
- values.append(kwargs.get(f"initial_value{i}", None))
34
- return tuple(["stub"] + values)
35
-
36
- @VariantSupport()
37
- class TestWhileLoopClose:
38
- def __init__(self):
39
- pass
40
-
41
- @classmethod
42
- def INPUT_TYPES(cls):
43
- inputs = {
44
- "required": {
45
- "flow_control": ("FLOW_CONTROL", {"rawLink": True}),
46
- "condition": ("BOOLEAN", {"forceInput": True}),
47
- },
48
- "optional": {
49
- },
50
- "hidden": {
51
- "dynprompt": "DYNPROMPT",
52
- "unique_id": "UNIQUE_ID",
53
- }
54
- }
55
- for i in range(NUM_FLOW_SOCKETS):
56
- inputs["optional"][f"initial_value{i}"] = ("*",)
57
- return inputs
58
-
59
- RETURN_TYPES = tuple(["*"] * NUM_FLOW_SOCKETS)
60
- RETURN_NAMES = tuple([f"value{i}" for i in range(NUM_FLOW_SOCKETS)])
61
- FUNCTION = "while_loop_close"
62
-
63
- CATEGORY = "Testing/Flow"
64
-
65
- def explore_dependencies(self, node_id, dynprompt, upstream):
66
- node_info = dynprompt.get_node(node_id)
67
- if "inputs" not in node_info:
68
- return
69
- for k, v in node_info["inputs"].items():
70
- if is_link(v):
71
- parent_id = v[0]
72
- if parent_id not in upstream:
73
- upstream[parent_id] = []
74
- self.explore_dependencies(parent_id, dynprompt, upstream)
75
- upstream[parent_id].append(node_id)
76
-
77
- def collect_contained(self, node_id, upstream, contained):
78
- if node_id not in upstream:
79
- return
80
- for child_id in upstream[node_id]:
81
- if child_id not in contained:
82
- contained[child_id] = True
83
- self.collect_contained(child_id, upstream, contained)
84
-
85
-
86
- def while_loop_close(self, flow_control, condition, dynprompt=None, unique_id=None, **kwargs):
87
- assert dynprompt is not None
88
- if not condition:
89
- # We're done with the loop
90
- values = []
91
- for i in range(NUM_FLOW_SOCKETS):
92
- values.append(kwargs.get(f"initial_value{i}", None))
93
- return tuple(values)
94
-
95
- # We want to loop
96
- upstream = {}
97
- # Get the list of all nodes between the open and close nodes
98
- self.explore_dependencies(unique_id, dynprompt, upstream)
99
-
100
- contained = {}
101
- open_node = flow_control[0]
102
- self.collect_contained(open_node, upstream, contained)
103
- contained[unique_id] = True
104
- contained[open_node] = True
105
-
106
- # We'll use the default prefix, but to avoid having node names grow exponentially in size,
107
- # we'll use "Recurse" for the name of the recursively-generated copy of this node.
108
- graph = GraphBuilder()
109
- for node_id in contained:
110
- original_node = dynprompt.get_node(node_id)
111
- node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id)
112
- node.set_override_display_id(node_id)
113
- for node_id in contained:
114
- original_node = dynprompt.get_node(node_id)
115
- node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
116
- assert node is not None
117
- for k, v in original_node["inputs"].items():
118
- if is_link(v) and v[0] in contained:
119
- parent = graph.lookup_node(v[0])
120
- assert parent is not None
121
- node.set_input(k, parent.out(v[1]))
122
- else:
123
- node.set_input(k, v)
124
- new_open = graph.lookup_node(open_node)
125
- assert new_open is not None
126
- for i in range(NUM_FLOW_SOCKETS):
127
- key = f"initial_value{i}"
128
- new_open.set_input(key, kwargs.get(key, None))
129
- my_clone = graph.lookup_node("Recurse")
130
- assert my_clone is not None
131
- result = map(lambda x: my_clone.out(x), range(NUM_FLOW_SOCKETS))
132
- return {
133
- "result": tuple(result),
134
- "expand": graph.finalize(),
135
- }
136
-
137
- @VariantSupport()
138
- class TestExecutionBlockerNode:
139
- def __init__(self):
140
- pass
141
-
142
- @classmethod
143
- def INPUT_TYPES(cls):
144
- inputs = {
145
- "required": {
146
- "input": ("*",),
147
- "block": ("BOOLEAN",),
148
- "verbose": ("BOOLEAN", {"default": False}),
149
- },
150
- }
151
- return inputs
152
-
153
- RETURN_TYPES = ("*",)
154
- RETURN_NAMES = ("output",)
155
- FUNCTION = "execution_blocker"
156
-
157
- CATEGORY = "Testing/Flow"
158
-
159
- def execution_blocker(self, input, block, verbose):
160
- if block:
161
- return (ExecutionBlocker("Blocked Execution" if verbose else None),)
162
- return (input,)
163
-
164
- FLOW_CONTROL_NODE_CLASS_MAPPINGS = {
165
- "TestWhileLoopOpen": TestWhileLoopOpen,
166
- "TestWhileLoopClose": TestWhileLoopClose,
167
- "TestExecutionBlocker": TestExecutionBlockerNode,
168
- }
169
- FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS = {
170
- "TestWhileLoopOpen": "While Loop Open",
171
- "TestWhileLoopClose": "While Loop Close",
172
- "TestExecutionBlocker": "Execution Blocker",
173
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/specific_tests.py DELETED
@@ -1,498 +0,0 @@
1
- import torch
2
- import time
3
- import asyncio
4
- from comfy.utils import ProgressBar
5
- from .tools import VariantSupport
6
- from comfy_execution.graph_utils import GraphBuilder
7
- from comfy.comfy_types.node_typing import ComfyNodeABC
8
- from comfy.comfy_types import IO
9
-
10
- class TestLazyMixImages:
11
- @classmethod
12
- def INPUT_TYPES(cls):
13
- return {
14
- "required": {
15
- "image1": ("IMAGE",{"lazy": True}),
16
- "image2": ("IMAGE",{"lazy": True}),
17
- "mask": ("MASK",),
18
- },
19
- }
20
-
21
- RETURN_TYPES = ("IMAGE",)
22
- FUNCTION = "mix"
23
-
24
- CATEGORY = "Testing/Nodes"
25
-
26
- def check_lazy_status(self, mask, image1, image2):
27
- mask_min = mask.min()
28
- mask_max = mask.max()
29
- needed = []
30
- if image1 is None and (mask_min != 1.0 or mask_max != 1.0):
31
- needed.append("image1")
32
- if image2 is None and (mask_min != 0.0 or mask_max != 0.0):
33
- needed.append("image2")
34
- return needed
35
-
36
- # Not trying to handle different batch sizes here just to keep the demo simple
37
- def mix(self, mask, image1, image2):
38
- mask_min = mask.min()
39
- mask_max = mask.max()
40
- if mask_min == 0.0 and mask_max == 0.0:
41
- return (image1,)
42
- elif mask_min == 1.0 and mask_max == 1.0:
43
- return (image2,)
44
-
45
- if len(mask.shape) == 2:
46
- mask = mask.unsqueeze(0)
47
- if len(mask.shape) == 3:
48
- mask = mask.unsqueeze(3)
49
- if mask.shape[3] < image1.shape[3]:
50
- mask = mask.repeat(1, 1, 1, image1.shape[3])
51
-
52
- result = image1 * (1. - mask) + image2 * mask,
53
- return (result[0],)
54
-
55
- class TestVariadicAverage:
56
- @classmethod
57
- def INPUT_TYPES(cls):
58
- return {
59
- "required": {
60
- "input1": ("IMAGE",),
61
- },
62
- }
63
-
64
- RETURN_TYPES = ("IMAGE",)
65
- FUNCTION = "variadic_average"
66
-
67
- CATEGORY = "Testing/Nodes"
68
-
69
- def variadic_average(self, input1, **kwargs):
70
- inputs = [input1]
71
- while 'input' + str(len(inputs) + 1) in kwargs:
72
- inputs.append(kwargs['input' + str(len(inputs) + 1)])
73
- return (torch.stack(inputs).mean(dim=0),)
74
-
75
-
76
- class TestCustomIsChanged:
77
- @classmethod
78
- def INPUT_TYPES(cls):
79
- return {
80
- "required": {
81
- "image": ("IMAGE",),
82
- },
83
- "optional": {
84
- "should_change": ("BOOL", {"default": False}),
85
- },
86
- }
87
-
88
- RETURN_TYPES = ("IMAGE",)
89
- FUNCTION = "custom_is_changed"
90
-
91
- CATEGORY = "Testing/Nodes"
92
-
93
- def custom_is_changed(self, image, should_change=False):
94
- return (image,)
95
-
96
- @classmethod
97
- def IS_CHANGED(cls, should_change=False, *args, **kwargs):
98
- if should_change:
99
- return float("NaN")
100
- else:
101
- return False
102
-
103
- class TestIsChangedWithConstants:
104
- @classmethod
105
- def INPUT_TYPES(cls):
106
- return {
107
- "required": {
108
- "image": ("IMAGE",),
109
- "value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0}),
110
- },
111
- }
112
-
113
- RETURN_TYPES = ("IMAGE",)
114
- FUNCTION = "custom_is_changed"
115
-
116
- CATEGORY = "Testing/Nodes"
117
-
118
- def custom_is_changed(self, image, value):
119
- return (image * value,)
120
-
121
- @classmethod
122
- def IS_CHANGED(cls, image, value):
123
- if image is None:
124
- return value
125
- else:
126
- return image.mean().item() * value
127
-
128
- class TestCustomValidation1:
129
- @classmethod
130
- def INPUT_TYPES(cls):
131
- return {
132
- "required": {
133
- "input1": ("IMAGE,FLOAT",),
134
- "input2": ("IMAGE,FLOAT",),
135
- },
136
- }
137
-
138
- RETURN_TYPES = ("IMAGE",)
139
- FUNCTION = "custom_validation1"
140
-
141
- CATEGORY = "Testing/Nodes"
142
-
143
- def custom_validation1(self, input1, input2):
144
- if isinstance(input1, float) and isinstance(input2, float):
145
- result = torch.ones([1, 512, 512, 3]) * input1 * input2
146
- else:
147
- result = input1 * input2
148
- return (result,)
149
-
150
- @classmethod
151
- def VALIDATE_INPUTS(cls, input1=None, input2=None):
152
- if input1 is not None:
153
- if not isinstance(input1, (torch.Tensor, float)):
154
- return f"Invalid type of input1: {type(input1)}"
155
- if input2 is not None:
156
- if not isinstance(input2, (torch.Tensor, float)):
157
- return f"Invalid type of input2: {type(input2)}"
158
-
159
- return True
160
-
161
- class TestCustomValidation2:
162
- @classmethod
163
- def INPUT_TYPES(cls):
164
- return {
165
- "required": {
166
- "input1": ("IMAGE,FLOAT",),
167
- "input2": ("IMAGE,FLOAT",),
168
- },
169
- }
170
-
171
- RETURN_TYPES = ("IMAGE",)
172
- FUNCTION = "custom_validation2"
173
-
174
- CATEGORY = "Testing/Nodes"
175
-
176
- def custom_validation2(self, input1, input2):
177
- if isinstance(input1, float) and isinstance(input2, float):
178
- result = torch.ones([1, 512, 512, 3]) * input1 * input2
179
- else:
180
- result = input1 * input2
181
- return (result,)
182
-
183
- @classmethod
184
- def VALIDATE_INPUTS(cls, input_types, input1=None, input2=None):
185
- if input1 is not None:
186
- if not isinstance(input1, (torch.Tensor, float)):
187
- return f"Invalid type of input1: {type(input1)}"
188
- if input2 is not None:
189
- if not isinstance(input2, (torch.Tensor, float)):
190
- return f"Invalid type of input2: {type(input2)}"
191
-
192
- if 'input1' in input_types:
193
- if input_types['input1'] not in ["IMAGE", "FLOAT"]:
194
- return f"Invalid type of input1: {input_types['input1']}"
195
- if 'input2' in input_types:
196
- if input_types['input2'] not in ["IMAGE", "FLOAT"]:
197
- return f"Invalid type of input2: {input_types['input2']}"
198
-
199
- return True
200
-
201
- @VariantSupport()
202
- class TestCustomValidation3:
203
- @classmethod
204
- def INPUT_TYPES(cls):
205
- return {
206
- "required": {
207
- "input1": ("IMAGE,FLOAT",),
208
- "input2": ("IMAGE,FLOAT",),
209
- },
210
- }
211
-
212
- RETURN_TYPES = ("IMAGE",)
213
- FUNCTION = "custom_validation3"
214
-
215
- CATEGORY = "Testing/Nodes"
216
-
217
- def custom_validation3(self, input1, input2):
218
- if isinstance(input1, float) and isinstance(input2, float):
219
- result = torch.ones([1, 512, 512, 3]) * input1 * input2
220
- else:
221
- result = input1 * input2
222
- return (result,)
223
-
224
- class TestCustomValidation4:
225
- @classmethod
226
- def INPUT_TYPES(cls):
227
- return {
228
- "required": {
229
- "input1": ("FLOAT",),
230
- "input2": ("FLOAT",),
231
- },
232
- }
233
-
234
- RETURN_TYPES = ("IMAGE",)
235
- FUNCTION = "custom_validation4"
236
-
237
- CATEGORY = "Testing/Nodes"
238
-
239
- def custom_validation4(self, input1, input2):
240
- result = torch.ones([1, 512, 512, 3]) * input1 * input2
241
- return (result,)
242
-
243
- @classmethod
244
- def VALIDATE_INPUTS(cls, input1, input2):
245
- if input1 is not None:
246
- if not isinstance(input1, float):
247
- return f"Invalid type of input1: {type(input1)}"
248
- if input2 is not None:
249
- if not isinstance(input2, float):
250
- return f"Invalid type of input2: {type(input2)}"
251
-
252
- return True
253
-
254
- class TestCustomValidation5:
255
- @classmethod
256
- def INPUT_TYPES(cls):
257
- return {
258
- "required": {
259
- "input1": ("FLOAT", {"min": 0.0, "max": 1.0}),
260
- "input2": ("FLOAT", {"min": 0.0, "max": 1.0}),
261
- },
262
- }
263
-
264
- RETURN_TYPES = ("IMAGE",)
265
- FUNCTION = "custom_validation5"
266
-
267
- CATEGORY = "Testing/Nodes"
268
-
269
- def custom_validation5(self, input1, input2):
270
- value = input1 * input2
271
- return (torch.ones([1, 512, 512, 3]) * value,)
272
-
273
- @classmethod
274
- def VALIDATE_INPUTS(cls, **kwargs):
275
- if kwargs['input2'] == 7.0:
276
- return "7s are not allowed. I've never liked 7s."
277
- return True
278
-
279
- class TestDynamicDependencyCycle:
280
- @classmethod
281
- def INPUT_TYPES(cls):
282
- return {
283
- "required": {
284
- "input1": ("IMAGE",),
285
- "input2": ("IMAGE",),
286
- },
287
- }
288
-
289
- RETURN_TYPES = ("IMAGE",)
290
- FUNCTION = "dynamic_dependency_cycle"
291
-
292
- CATEGORY = "Testing/Nodes"
293
-
294
- def dynamic_dependency_cycle(self, input1, input2):
295
- g = GraphBuilder()
296
- mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
297
- mix1 = g.node("TestLazyMixImages", image1=input1, mask=mask.out(0))
298
- mix2 = g.node("TestLazyMixImages", image1=mix1.out(0), image2=input2, mask=mask.out(0))
299
-
300
- # Create the cyle
301
- mix1.set_input("image2", mix2.out(0))
302
-
303
- return {
304
- "result": (mix2.out(0),),
305
- "expand": g.finalize(),
306
- }
307
-
308
- class TestMixedExpansionReturns:
309
- @classmethod
310
- def INPUT_TYPES(cls):
311
- return {
312
- "required": {
313
- "input1": ("FLOAT",),
314
- },
315
- }
316
-
317
- RETURN_TYPES = ("IMAGE","IMAGE")
318
- FUNCTION = "mixed_expansion_returns"
319
-
320
- CATEGORY = "Testing/Nodes"
321
-
322
- def mixed_expansion_returns(self, input1):
323
- white_image = torch.ones([1, 512, 512, 3])
324
- if input1 <= 0.1:
325
- return (torch.ones([1, 512, 512, 3]) * 0.1, white_image)
326
- elif input1 <= 0.2:
327
- return {
328
- "result": (torch.ones([1, 512, 512, 3]) * 0.2, white_image),
329
- }
330
- else:
331
- g = GraphBuilder()
332
- mask = g.node("StubMask", value=0.3, height=512, width=512, batch_size=1)
333
- black = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
334
- white = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
335
- mix = g.node("TestLazyMixImages", image1=black.out(0), image2=white.out(0), mask=mask.out(0))
336
- return {
337
- "result": (mix.out(0), white_image),
338
- "expand": g.finalize(),
339
- }
340
-
341
- class TestSamplingInExpansion:
342
- @classmethod
343
- def INPUT_TYPES(cls):
344
- return {
345
- "required": {
346
- "model": ("MODEL",),
347
- "clip": ("CLIP",),
348
- "vae": ("VAE",),
349
- "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
350
- "steps": ("INT", {"default": 20, "min": 1, "max": 100}),
351
- "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 30.0}),
352
- "prompt": ("STRING", {"multiline": True, "default": "a beautiful landscape with mountains and trees"}),
353
- "negative_prompt": ("STRING", {"multiline": True, "default": "blurry, bad quality, worst quality"}),
354
- },
355
- }
356
-
357
- RETURN_TYPES = ("IMAGE",)
358
- FUNCTION = "sampling_in_expansion"
359
-
360
- CATEGORY = "Testing/Nodes"
361
-
362
- def sampling_in_expansion(self, model, clip, vae, seed, steps, cfg, prompt, negative_prompt):
363
- g = GraphBuilder()
364
-
365
- # Create a basic image generation workflow using the input model, clip and vae
366
- # 1. Setup text prompts using the provided CLIP model
367
- positive_prompt = g.node("CLIPTextEncode",
368
- text=prompt,
369
- clip=clip)
370
- negative_prompt = g.node("CLIPTextEncode",
371
- text=negative_prompt,
372
- clip=clip)
373
-
374
- # 2. Create empty latent with specified size
375
- empty_latent = g.node("EmptyLatentImage", width=512, height=512, batch_size=1)
376
-
377
- # 3. Setup sampler and generate image latent
378
- sampler = g.node("KSampler",
379
- model=model,
380
- positive=positive_prompt.out(0),
381
- negative=negative_prompt.out(0),
382
- latent_image=empty_latent.out(0),
383
- seed=seed,
384
- steps=steps,
385
- cfg=cfg,
386
- sampler_name="euler_ancestral",
387
- scheduler="normal")
388
-
389
- # 4. Decode latent to image using VAE
390
- output = g.node("VAEDecode", samples=sampler.out(0), vae=vae)
391
-
392
- return {
393
- "result": (output.out(0),),
394
- "expand": g.finalize(),
395
- }
396
-
397
- class TestSleep(ComfyNodeABC):
398
- @classmethod
399
- def INPUT_TYPES(cls):
400
- return {
401
- "required": {
402
- "value": (IO.ANY, {}),
403
- "seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 9999.0, "step": 0.01, "tooltip": "The amount of seconds to sleep."}),
404
- },
405
- "hidden": {
406
- "unique_id": "UNIQUE_ID",
407
- },
408
- }
409
- RETURN_TYPES = (IO.ANY,)
410
- FUNCTION = "sleep"
411
-
412
- CATEGORY = "_for_testing"
413
-
414
- async def sleep(self, value, seconds, unique_id):
415
- pbar = ProgressBar(seconds, node_id=unique_id)
416
- start = time.time()
417
- expiration = start + seconds
418
- now = start
419
- while now < expiration:
420
- now = time.time()
421
- pbar.update_absolute(now - start)
422
- await asyncio.sleep(0.01)
423
- return (value,)
424
-
425
- class TestParallelSleep(ComfyNodeABC):
426
- @classmethod
427
- def INPUT_TYPES(cls):
428
- return {
429
- "required": {
430
- "image1": ("IMAGE", ),
431
- "image2": ("IMAGE", ),
432
- "image3": ("IMAGE", ),
433
- "sleep1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
434
- "sleep2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
435
- "sleep3": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
436
- },
437
- "hidden": {
438
- "unique_id": "UNIQUE_ID",
439
- },
440
- }
441
- RETURN_TYPES = ("IMAGE",)
442
- FUNCTION = "parallel_sleep"
443
- CATEGORY = "_for_testing"
444
- OUTPUT_NODE = True
445
-
446
- def parallel_sleep(self, image1, image2, image3, sleep1, sleep2, sleep3, unique_id):
447
- # Create a graph dynamically with three TestSleep nodes
448
- g = GraphBuilder()
449
-
450
- # Create sleep nodes for each duration and image
451
- sleep_node1 = g.node("TestSleep", value=image1, seconds=sleep1)
452
- sleep_node2 = g.node("TestSleep", value=image2, seconds=sleep2)
453
- sleep_node3 = g.node("TestSleep", value=image3, seconds=sleep3)
454
-
455
- # Blend the results using TestVariadicAverage
456
- blend = g.node("TestVariadicAverage",
457
- input1=sleep_node1.out(0),
458
- input2=sleep_node2.out(0),
459
- input3=sleep_node3.out(0))
460
-
461
- return {
462
- "result": (blend.out(0),),
463
- "expand": g.finalize(),
464
- }
465
-
466
- TEST_NODE_CLASS_MAPPINGS = {
467
- "TestLazyMixImages": TestLazyMixImages,
468
- "TestVariadicAverage": TestVariadicAverage,
469
- "TestCustomIsChanged": TestCustomIsChanged,
470
- "TestIsChangedWithConstants": TestIsChangedWithConstants,
471
- "TestCustomValidation1": TestCustomValidation1,
472
- "TestCustomValidation2": TestCustomValidation2,
473
- "TestCustomValidation3": TestCustomValidation3,
474
- "TestCustomValidation4": TestCustomValidation4,
475
- "TestCustomValidation5": TestCustomValidation5,
476
- "TestDynamicDependencyCycle": TestDynamicDependencyCycle,
477
- "TestMixedExpansionReturns": TestMixedExpansionReturns,
478
- "TestSamplingInExpansion": TestSamplingInExpansion,
479
- "TestSleep": TestSleep,
480
- "TestParallelSleep": TestParallelSleep,
481
- }
482
-
483
- TEST_NODE_DISPLAY_NAME_MAPPINGS = {
484
- "TestLazyMixImages": "Lazy Mix Images",
485
- "TestVariadicAverage": "Variadic Average",
486
- "TestCustomIsChanged": "Custom IsChanged",
487
- "TestIsChangedWithConstants": "IsChanged With Constants",
488
- "TestCustomValidation1": "Custom Validation 1",
489
- "TestCustomValidation2": "Custom Validation 2",
490
- "TestCustomValidation3": "Custom Validation 3",
491
- "TestCustomValidation4": "Custom Validation 4",
492
- "TestCustomValidation5": "Custom Validation 5",
493
- "TestDynamicDependencyCycle": "Dynamic Dependency Cycle",
494
- "TestMixedExpansionReturns": "Mixed Expansion Returns",
495
- "TestSamplingInExpansion": "Sampling In Expansion",
496
- "TestSleep": "Test Sleep",
497
- "TestParallelSleep": "Test Parallel Sleep",
498
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/stubs.py DELETED
@@ -1,129 +0,0 @@
1
- import torch
2
-
3
- class StubImage:
4
- def __init__(self):
5
- pass
6
-
7
- @classmethod
8
- def INPUT_TYPES(cls):
9
- return {
10
- "required": {
11
- "content": (['WHITE', 'BLACK', 'NOISE'],),
12
- "height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
13
- "width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
14
- "batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
15
- },
16
- }
17
-
18
- RETURN_TYPES = ("IMAGE",)
19
- FUNCTION = "stub_image"
20
-
21
- CATEGORY = "Testing/Stub Nodes"
22
-
23
- def stub_image(self, content, height, width, batch_size):
24
- if content == "WHITE":
25
- return (torch.ones(batch_size, height, width, 3),)
26
- elif content == "BLACK":
27
- return (torch.zeros(batch_size, height, width, 3),)
28
- elif content == "NOISE":
29
- return (torch.rand(batch_size, height, width, 3),)
30
-
31
- class StubConstantImage:
32
- def __init__(self):
33
- pass
34
- @classmethod
35
- def INPUT_TYPES(cls):
36
- return {
37
- "required": {
38
- "value": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
39
- "height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
40
- "width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
41
- "batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
42
- },
43
- }
44
-
45
- RETURN_TYPES = ("IMAGE",)
46
- FUNCTION = "stub_constant_image"
47
-
48
- CATEGORY = "Testing/Stub Nodes"
49
-
50
- def stub_constant_image(self, value, height, width, batch_size):
51
- return (torch.ones(batch_size, height, width, 3) * value,)
52
-
53
- class StubMask:
54
- def __init__(self):
55
- pass
56
-
57
- @classmethod
58
- def INPUT_TYPES(cls):
59
- return {
60
- "required": {
61
- "value": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
62
- "height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
63
- "width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
64
- "batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
65
- },
66
- }
67
-
68
- RETURN_TYPES = ("MASK",)
69
- FUNCTION = "stub_mask"
70
-
71
- CATEGORY = "Testing/Stub Nodes"
72
-
73
- def stub_mask(self, value, height, width, batch_size):
74
- return (torch.ones(batch_size, height, width) * value,)
75
-
76
- class StubInt:
77
- def __init__(self):
78
- pass
79
-
80
- @classmethod
81
- def INPUT_TYPES(cls):
82
- return {
83
- "required": {
84
- "value": ("INT", {"default": 0, "min": -0xffffffff, "max": 0xffffffff, "step": 1}),
85
- },
86
- }
87
-
88
- RETURN_TYPES = ("INT",)
89
- FUNCTION = "stub_int"
90
-
91
- CATEGORY = "Testing/Stub Nodes"
92
-
93
- def stub_int(self, value):
94
- return (value,)
95
-
96
- class StubFloat:
97
- def __init__(self):
98
- pass
99
-
100
- @classmethod
101
- def INPUT_TYPES(cls):
102
- return {
103
- "required": {
104
- "value": ("FLOAT", {"default": 0.0, "min": -1.0e38, "max": 1.0e38, "step": 0.01}),
105
- },
106
- }
107
-
108
- RETURN_TYPES = ("FLOAT",)
109
- FUNCTION = "stub_float"
110
-
111
- CATEGORY = "Testing/Stub Nodes"
112
-
113
- def stub_float(self, value):
114
- return (value,)
115
-
116
- TEST_STUB_NODE_CLASS_MAPPINGS = {
117
- "StubImage": StubImage,
118
- "StubConstantImage": StubConstantImage,
119
- "StubMask": StubMask,
120
- "StubInt": StubInt,
121
- "StubFloat": StubFloat,
122
- }
123
- TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS = {
124
- "StubImage": "Stub Image",
125
- "StubConstantImage": "Stub Constant Image",
126
- "StubMask": "Stub Mask",
127
- "StubInt": "Stub Int",
128
- "StubFloat": "Stub Float",
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/tools.py DELETED
@@ -1,53 +0,0 @@
1
-
2
- def MakeSmartType(t):
3
- if isinstance(t, str):
4
- return SmartType(t)
5
- return t
6
-
7
- class SmartType(str):
8
- def __ne__(self, other):
9
- if self == "*" or other == "*":
10
- return False
11
- selfset = set(self.split(','))
12
- otherset = set(other.split(','))
13
- return not selfset.issubset(otherset)
14
-
15
- def VariantSupport():
16
- def decorator(cls):
17
- if hasattr(cls, "INPUT_TYPES"):
18
- old_input_types = getattr(cls, "INPUT_TYPES")
19
- def new_input_types(*args, **kwargs):
20
- types = old_input_types(*args, **kwargs)
21
- for category in ["required", "optional"]:
22
- if category not in types:
23
- continue
24
- for key, value in types[category].items():
25
- if isinstance(value, tuple):
26
- types[category][key] = (MakeSmartType(value[0]),) + value[1:]
27
- return types
28
- setattr(cls, "INPUT_TYPES", new_input_types)
29
- if hasattr(cls, "RETURN_TYPES"):
30
- old_return_types = cls.RETURN_TYPES
31
- setattr(cls, "RETURN_TYPES", tuple(MakeSmartType(x) for x in old_return_types))
32
- if hasattr(cls, "VALIDATE_INPUTS"):
33
- # Reflection is used to determine what the function signature is, so we can't just change the function signature
34
- raise NotImplementedError("VariantSupport does not support VALIDATE_INPUTS yet")
35
- else:
36
- def validate_inputs(input_types):
37
- inputs = cls.INPUT_TYPES()
38
- for key, value in input_types.items():
39
- if isinstance(value, SmartType):
40
- continue
41
- if "required" in inputs and key in inputs["required"]:
42
- expected_type = inputs["required"][key][0]
43
- elif "optional" in inputs and key in inputs["optional"]:
44
- expected_type = inputs["optional"][key][0]
45
- else:
46
- expected_type = None
47
- if expected_type is not None and MakeSmartType(value) != expected_type:
48
- return f"Invalid type of {key}: {value} (expected {expected_type})"
49
- return True
50
- setattr(cls, "VALIDATE_INPUTS", validate_inputs)
51
- return cls
52
- return decorator
53
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/inference/testing_nodes/testing-pack/util.py DELETED
@@ -1,364 +0,0 @@
1
- from comfy_execution.graph_utils import GraphBuilder
2
- from .tools import VariantSupport
3
-
4
- @VariantSupport()
5
- class TestAccumulateNode:
6
- def __init__(self):
7
- pass
8
-
9
- @classmethod
10
- def INPUT_TYPES(cls):
11
- return {
12
- "required": {
13
- "to_add": ("*",),
14
- },
15
- "optional": {
16
- "accumulation": ("ACCUMULATION",),
17
- },
18
- }
19
-
20
- RETURN_TYPES = ("ACCUMULATION",)
21
- FUNCTION = "accumulate"
22
-
23
- CATEGORY = "Testing/Lists"
24
-
25
- def accumulate(self, to_add, accumulation = None):
26
- if accumulation is None:
27
- value = [to_add]
28
- else:
29
- value = accumulation["accum"] + [to_add]
30
- return ({"accum": value},)
31
-
32
- @VariantSupport()
33
- class TestAccumulationHeadNode:
34
- def __init__(self):
35
- pass
36
-
37
- @classmethod
38
- def INPUT_TYPES(cls):
39
- return {
40
- "required": {
41
- "accumulation": ("ACCUMULATION",),
42
- },
43
- }
44
-
45
- RETURN_TYPES = ("ACCUMULATION", "*",)
46
- FUNCTION = "accumulation_head"
47
-
48
- CATEGORY = "Testing/Lists"
49
-
50
- def accumulation_head(self, accumulation):
51
- accum = accumulation["accum"]
52
- if len(accum) == 0:
53
- return (accumulation, None)
54
- else:
55
- return ({"accum": accum[1:]}, accum[0])
56
-
57
- class TestAccumulationTailNode:
58
- def __init__(self):
59
- pass
60
-
61
- @classmethod
62
- def INPUT_TYPES(cls):
63
- return {
64
- "required": {
65
- "accumulation": ("ACCUMULATION",),
66
- },
67
- }
68
-
69
- RETURN_TYPES = ("ACCUMULATION", "*",)
70
- FUNCTION = "accumulation_tail"
71
-
72
- CATEGORY = "Testing/Lists"
73
-
74
- def accumulation_tail(self, accumulation):
75
- accum = accumulation["accum"]
76
- if len(accum) == 0:
77
- return (None, accumulation)
78
- else:
79
- return ({"accum": accum[:-1]}, accum[-1])
80
-
81
- @VariantSupport()
82
- class TestAccumulationToListNode:
83
- def __init__(self):
84
- pass
85
-
86
- @classmethod
87
- def INPUT_TYPES(cls):
88
- return {
89
- "required": {
90
- "accumulation": ("ACCUMULATION",),
91
- },
92
- }
93
-
94
- RETURN_TYPES = ("*",)
95
- OUTPUT_IS_LIST = (True,)
96
-
97
- FUNCTION = "accumulation_to_list"
98
-
99
- CATEGORY = "Testing/Lists"
100
-
101
- def accumulation_to_list(self, accumulation):
102
- return (accumulation["accum"],)
103
-
104
- @VariantSupport()
105
- class TestListToAccumulationNode:
106
- def __init__(self):
107
- pass
108
-
109
- @classmethod
110
- def INPUT_TYPES(cls):
111
- return {
112
- "required": {
113
- "list": ("*",),
114
- },
115
- }
116
-
117
- RETURN_TYPES = ("ACCUMULATION",)
118
- INPUT_IS_LIST = (True,)
119
-
120
- FUNCTION = "list_to_accumulation"
121
-
122
- CATEGORY = "Testing/Lists"
123
-
124
- def list_to_accumulation(self, list):
125
- return ({"accum": list},)
126
-
127
- @VariantSupport()
128
- class TestAccumulationGetLengthNode:
129
- def __init__(self):
130
- pass
131
-
132
- @classmethod
133
- def INPUT_TYPES(cls):
134
- return {
135
- "required": {
136
- "accumulation": ("ACCUMULATION",),
137
- },
138
- }
139
-
140
- RETURN_TYPES = ("INT",)
141
-
142
- FUNCTION = "accumlength"
143
-
144
- CATEGORY = "Testing/Lists"
145
-
146
- def accumlength(self, accumulation):
147
- return (len(accumulation['accum']),)
148
-
149
- @VariantSupport()
150
- class TestAccumulationGetItemNode:
151
- def __init__(self):
152
- pass
153
-
154
- @classmethod
155
- def INPUT_TYPES(cls):
156
- return {
157
- "required": {
158
- "accumulation": ("ACCUMULATION",),
159
- "index": ("INT", {"default":0, "step":1})
160
- },
161
- }
162
-
163
- RETURN_TYPES = ("*",)
164
-
165
- FUNCTION = "get_item"
166
-
167
- CATEGORY = "Testing/Lists"
168
-
169
- def get_item(self, accumulation, index):
170
- return (accumulation['accum'][index],)
171
-
172
- @VariantSupport()
173
- class TestAccumulationSetItemNode:
174
- def __init__(self):
175
- pass
176
-
177
- @classmethod
178
- def INPUT_TYPES(cls):
179
- return {
180
- "required": {
181
- "accumulation": ("ACCUMULATION",),
182
- "index": ("INT", {"default":0, "step":1}),
183
- "value": ("*",),
184
- },
185
- }
186
-
187
- RETURN_TYPES = ("ACCUMULATION",)
188
-
189
- FUNCTION = "set_item"
190
-
191
- CATEGORY = "Testing/Lists"
192
-
193
- def set_item(self, accumulation, index, value):
194
- new_accum = accumulation['accum'][:]
195
- new_accum[index] = value
196
- return ({"accum": new_accum},)
197
-
198
- class TestIntMathOperation:
199
- def __init__(self):
200
- pass
201
-
202
- @classmethod
203
- def INPUT_TYPES(cls):
204
- return {
205
- "required": {
206
- "a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
207
- "b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
208
- "operation": (["add", "subtract", "multiply", "divide", "modulo", "power"],),
209
- },
210
- }
211
-
212
- RETURN_TYPES = ("INT",)
213
- FUNCTION = "int_math_operation"
214
-
215
- CATEGORY = "Testing/Logic"
216
-
217
- def int_math_operation(self, a, b, operation):
218
- if operation == "add":
219
- return (a + b,)
220
- elif operation == "subtract":
221
- return (a - b,)
222
- elif operation == "multiply":
223
- return (a * b,)
224
- elif operation == "divide":
225
- return (a // b,)
226
- elif operation == "modulo":
227
- return (a % b,)
228
- elif operation == "power":
229
- return (a ** b,)
230
-
231
-
232
- from .flow_control import NUM_FLOW_SOCKETS
233
- @VariantSupport()
234
- class TestForLoopOpen:
235
- def __init__(self):
236
- pass
237
-
238
- @classmethod
239
- def INPUT_TYPES(cls):
240
- return {
241
- "required": {
242
- "remaining": ("INT", {"default": 1, "min": 0, "max": 100000, "step": 1}),
243
- },
244
- "optional": {
245
- f"initial_value{i}": ("*",) for i in range(1, NUM_FLOW_SOCKETS)
246
- },
247
- "hidden": {
248
- "initial_value0": ("*",)
249
- }
250
- }
251
-
252
- RETURN_TYPES = tuple(["FLOW_CONTROL", "INT",] + ["*"] * (NUM_FLOW_SOCKETS-1))
253
- RETURN_NAMES = tuple(["flow_control", "remaining"] + [f"value{i}" for i in range(1, NUM_FLOW_SOCKETS)])
254
- FUNCTION = "for_loop_open"
255
-
256
- CATEGORY = "Testing/Flow"
257
-
258
- def for_loop_open(self, remaining, **kwargs):
259
- graph = GraphBuilder()
260
- if "initial_value0" in kwargs:
261
- remaining = kwargs["initial_value0"]
262
- graph.node("TestWhileLoopOpen", condition=remaining, initial_value0=remaining, **{(f"initial_value{i}"): kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)})
263
- outputs = [kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)]
264
- return {
265
- "result": tuple(["stub", remaining] + outputs),
266
- "expand": graph.finalize(),
267
- }
268
-
269
- @VariantSupport()
270
- class TestForLoopClose:
271
- def __init__(self):
272
- pass
273
-
274
- @classmethod
275
- def INPUT_TYPES(cls):
276
- return {
277
- "required": {
278
- "flow_control": ("FLOW_CONTROL", {"rawLink": True}),
279
- },
280
- "optional": {
281
- f"initial_value{i}": ("*",{"rawLink": True}) for i in range(1, NUM_FLOW_SOCKETS)
282
- },
283
- }
284
-
285
- RETURN_TYPES = tuple(["*"] * (NUM_FLOW_SOCKETS-1))
286
- RETURN_NAMES = tuple([f"value{i}" for i in range(1, NUM_FLOW_SOCKETS)])
287
- FUNCTION = "for_loop_close"
288
-
289
- CATEGORY = "Testing/Flow"
290
-
291
- def for_loop_close(self, flow_control, **kwargs):
292
- graph = GraphBuilder()
293
- while_open = flow_control[0]
294
- sub = graph.node("TestIntMathOperation", operation="subtract", a=[while_open,1], b=1)
295
- cond = graph.node("TestToBoolNode", value=sub.out(0))
296
- input_values = {f"initial_value{i}": kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)}
297
- while_close = graph.node("TestWhileLoopClose",
298
- flow_control=flow_control,
299
- condition=cond.out(0),
300
- initial_value0=sub.out(0),
301
- **input_values)
302
- return {
303
- "result": tuple([while_close.out(i) for i in range(1, NUM_FLOW_SOCKETS)]),
304
- "expand": graph.finalize(),
305
- }
306
-
307
- NUM_LIST_SOCKETS = 10
308
- @VariantSupport()
309
- class TestMakeListNode:
310
- def __init__(self):
311
- pass
312
-
313
- @classmethod
314
- def INPUT_TYPES(cls):
315
- return {
316
- "required": {
317
- "value1": ("*",),
318
- },
319
- "optional": {
320
- f"value{i}": ("*",) for i in range(1, NUM_LIST_SOCKETS)
321
- },
322
- }
323
-
324
- RETURN_TYPES = ("*",)
325
- FUNCTION = "make_list"
326
- OUTPUT_IS_LIST = (True,)
327
-
328
- CATEGORY = "Testing/Lists"
329
-
330
- def make_list(self, **kwargs):
331
- result = []
332
- for i in range(NUM_LIST_SOCKETS):
333
- if f"value{i}" in kwargs:
334
- result.append(kwargs[f"value{i}"])
335
- return (result,)
336
-
337
- UTILITY_NODE_CLASS_MAPPINGS = {
338
- "TestAccumulateNode": TestAccumulateNode,
339
- "TestAccumulationHeadNode": TestAccumulationHeadNode,
340
- "TestAccumulationTailNode": TestAccumulationTailNode,
341
- "TestAccumulationToListNode": TestAccumulationToListNode,
342
- "TestListToAccumulationNode": TestListToAccumulationNode,
343
- "TestAccumulationGetLengthNode": TestAccumulationGetLengthNode,
344
- "TestAccumulationGetItemNode": TestAccumulationGetItemNode,
345
- "TestAccumulationSetItemNode": TestAccumulationSetItemNode,
346
- "TestForLoopOpen": TestForLoopOpen,
347
- "TestForLoopClose": TestForLoopClose,
348
- "TestIntMathOperation": TestIntMathOperation,
349
- "TestMakeListNode": TestMakeListNode,
350
- }
351
- UTILITY_NODE_DISPLAY_NAME_MAPPINGS = {
352
- "TestAccumulateNode": "Accumulate",
353
- "TestAccumulationHeadNode": "Accumulation Head",
354
- "TestAccumulationTailNode": "Accumulation Tail",
355
- "TestAccumulationToListNode": "Accumulation to List",
356
- "TestListToAccumulationNode": "List to Accumulation",
357
- "TestAccumulationGetLengthNode": "Accumulation Get Length",
358
- "TestAccumulationGetItemNode": "Accumulation Get Item",
359
- "TestAccumulationSetItemNode": "Accumulation Set Item",
360
- "TestForLoopOpen": "For Loop Open",
361
- "TestForLoopClose": "For Loop Close",
362
- "TestIntMathOperation": "Int Math Operation",
363
- "TestMakeListNode": "Make List",
364
- }