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

Delete tests-unit

Browse files
tests-unit/README.md DELETED
@@ -1,8 +0,0 @@
1
- # Pytest Unit Tests
2
-
3
- ## Install test dependencies
4
-
5
- `pip install -r tests-unit/requirements.txt`
6
-
7
- ## Run tests
8
- `pytest tests-unit/`
 
 
 
 
 
 
 
 
 
tests-unit/app_test/__init__.py DELETED
File without changes
tests-unit/app_test/custom_node_manager_test.py DELETED
@@ -1,147 +0,0 @@
1
- import pytest
2
- from aiohttp import web
3
- from unittest.mock import patch
4
- from app.custom_node_manager import CustomNodeManager
5
- import json
6
-
7
- pytestmark = (
8
- pytest.mark.asyncio
9
- ) # This applies the asyncio mark to all test functions in the module
10
-
11
-
12
- @pytest.fixture
13
- def custom_node_manager():
14
- return CustomNodeManager()
15
-
16
-
17
- @pytest.fixture
18
- def app(custom_node_manager):
19
- app = web.Application()
20
- routes = web.RouteTableDef()
21
- custom_node_manager.add_routes(
22
- routes, app, [("ComfyUI-TestExtension1", "ComfyUI-TestExtension1")]
23
- )
24
- app.add_routes(routes)
25
- return app
26
-
27
-
28
- async def test_get_workflow_templates(aiohttp_client, app, tmp_path):
29
- client = await aiohttp_client(app)
30
- # Setup temporary custom nodes file structure with 1 workflow file
31
- custom_nodes_dir = tmp_path / "custom_nodes"
32
- example_workflows_dir = (
33
- custom_nodes_dir / "ComfyUI-TestExtension1" / "example_workflows"
34
- )
35
- example_workflows_dir.mkdir(parents=True)
36
- template_file = example_workflows_dir / "workflow1.json"
37
- template_file.write_text("")
38
-
39
- with patch(
40
- "folder_paths.folder_names_and_paths",
41
- {"custom_nodes": ([str(custom_nodes_dir)], None)},
42
- ):
43
- response = await client.get("/workflow_templates")
44
- assert response.status == 200
45
- workflows_dict = await response.json()
46
- assert isinstance(workflows_dict, dict)
47
- assert "ComfyUI-TestExtension1" in workflows_dict
48
- assert isinstance(workflows_dict["ComfyUI-TestExtension1"], list)
49
- assert workflows_dict["ComfyUI-TestExtension1"][0] == "workflow1"
50
-
51
-
52
- async def test_build_translations_empty_when_no_locales(custom_node_manager, tmp_path):
53
- custom_nodes_dir = tmp_path / "custom_nodes"
54
- custom_nodes_dir.mkdir(parents=True)
55
-
56
- with patch("folder_paths.get_folder_paths", return_value=[str(custom_nodes_dir)]):
57
- translations = custom_node_manager.build_translations()
58
- assert translations == {}
59
-
60
-
61
- async def test_build_translations_loads_all_files(custom_node_manager, tmp_path):
62
- # Setup test directory structure
63
- custom_nodes_dir = tmp_path / "custom_nodes" / "test-extension"
64
- locales_dir = custom_nodes_dir / "locales" / "en"
65
- locales_dir.mkdir(parents=True)
66
-
67
- # Create test translation files
68
- main_content = {"title": "Test Extension"}
69
- (locales_dir / "main.json").write_text(json.dumps(main_content))
70
-
71
- node_defs = {"node1": "Node 1"}
72
- (locales_dir / "nodeDefs.json").write_text(json.dumps(node_defs))
73
-
74
- commands = {"cmd1": "Command 1"}
75
- (locales_dir / "commands.json").write_text(json.dumps(commands))
76
-
77
- settings = {"setting1": "Setting 1"}
78
- (locales_dir / "settings.json").write_text(json.dumps(settings))
79
-
80
- with patch(
81
- "folder_paths.get_folder_paths", return_value=[tmp_path / "custom_nodes"]
82
- ):
83
- translations = custom_node_manager.build_translations()
84
-
85
- assert translations == {
86
- "en": {
87
- "title": "Test Extension",
88
- "nodeDefs": {"node1": "Node 1"},
89
- "commands": {"cmd1": "Command 1"},
90
- "settings": {"setting1": "Setting 1"},
91
- }
92
- }
93
-
94
-
95
- async def test_build_translations_handles_invalid_json(custom_node_manager, tmp_path):
96
- # Setup test directory structure
97
- custom_nodes_dir = tmp_path / "custom_nodes" / "test-extension"
98
- locales_dir = custom_nodes_dir / "locales" / "en"
99
- locales_dir.mkdir(parents=True)
100
-
101
- # Create valid main.json
102
- main_content = {"title": "Test Extension"}
103
- (locales_dir / "main.json").write_text(json.dumps(main_content))
104
-
105
- # Create invalid JSON file
106
- (locales_dir / "nodeDefs.json").write_text("invalid json{")
107
-
108
- with patch(
109
- "folder_paths.get_folder_paths", return_value=[tmp_path / "custom_nodes"]
110
- ):
111
- translations = custom_node_manager.build_translations()
112
-
113
- assert translations == {
114
- "en": {
115
- "title": "Test Extension",
116
- }
117
- }
118
-
119
-
120
- async def test_build_translations_merges_multiple_extensions(
121
- custom_node_manager, tmp_path
122
- ):
123
- # Setup test directory structure for two extensions
124
- custom_nodes_dir = tmp_path / "custom_nodes"
125
- ext1_dir = custom_nodes_dir / "extension1" / "locales" / "en"
126
- ext2_dir = custom_nodes_dir / "extension2" / "locales" / "en"
127
- ext1_dir.mkdir(parents=True)
128
- ext2_dir.mkdir(parents=True)
129
-
130
- # Create translation files for extension 1
131
- ext1_main = {"title": "Extension 1", "shared": "Original"}
132
- (ext1_dir / "main.json").write_text(json.dumps(ext1_main))
133
-
134
- # Create translation files for extension 2
135
- ext2_main = {"description": "Extension 2", "shared": "Override"}
136
- (ext2_dir / "main.json").write_text(json.dumps(ext2_main))
137
-
138
- with patch("folder_paths.get_folder_paths", return_value=[str(custom_nodes_dir)]):
139
- translations = custom_node_manager.build_translations()
140
-
141
- assert translations == {
142
- "en": {
143
- "title": "Extension 1",
144
- "description": "Extension 2",
145
- "shared": "Override", # Second extension should override first
146
- }
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/app_test/frontend_manager_test.py DELETED
@@ -1,174 +0,0 @@
1
- import argparse
2
- import pytest
3
- from requests.exceptions import HTTPError
4
- from unittest.mock import patch
5
-
6
- from app.frontend_management import (
7
- FrontendManager,
8
- FrontEndProvider,
9
- Release,
10
- )
11
- from comfy.cli_args import DEFAULT_VERSION_STRING
12
-
13
-
14
- @pytest.fixture
15
- def mock_releases():
16
- return [
17
- Release(
18
- id=1,
19
- tag_name="1.0.0",
20
- name="Release 1.0.0",
21
- prerelease=False,
22
- created_at="2022-01-01T00:00:00Z",
23
- published_at="2022-01-01T00:00:00Z",
24
- body="Release notes for 1.0.0",
25
- assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}],
26
- ),
27
- Release(
28
- id=2,
29
- tag_name="2.0.0",
30
- name="Release 2.0.0",
31
- prerelease=False,
32
- created_at="2022-02-01T00:00:00Z",
33
- published_at="2022-02-01T00:00:00Z",
34
- body="Release notes for 2.0.0",
35
- assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}],
36
- ),
37
- ]
38
-
39
-
40
- @pytest.fixture
41
- def mock_provider(mock_releases):
42
- provider = FrontEndProvider(
43
- owner="test-owner",
44
- repo="test-repo",
45
- )
46
- provider.all_releases = mock_releases
47
- provider.latest_release = mock_releases[1]
48
- FrontendManager.PROVIDERS = [provider]
49
- return provider
50
-
51
-
52
- def test_get_release(mock_provider, mock_releases):
53
- version = "1.0.0"
54
- release = mock_provider.get_release(version)
55
- assert release == mock_releases[0]
56
-
57
-
58
- def test_get_release_latest(mock_provider, mock_releases):
59
- version = "latest"
60
- release = mock_provider.get_release(version)
61
- assert release == mock_releases[1]
62
-
63
-
64
- def test_get_release_invalid_version(mock_provider):
65
- version = "invalid"
66
- with pytest.raises(ValueError):
67
- mock_provider.get_release(version)
68
-
69
-
70
- def test_init_frontend_default():
71
- version_string = DEFAULT_VERSION_STRING
72
- frontend_path = FrontendManager.init_frontend(version_string)
73
- assert frontend_path == FrontendManager.default_frontend_path()
74
-
75
-
76
- def test_init_frontend_invalid_version():
77
- version_string = "test-owner/test-repo@1.100.99"
78
- with pytest.raises(HTTPError):
79
- FrontendManager.init_frontend_unsafe(version_string)
80
-
81
-
82
- def test_init_frontend_invalid_provider():
83
- version_string = "invalid/invalid@latest"
84
- with pytest.raises(HTTPError):
85
- FrontendManager.init_frontend_unsafe(version_string)
86
-
87
-
88
- @pytest.fixture
89
- def mock_os_functions():
90
- with (
91
- patch("app.frontend_management.os.makedirs") as mock_makedirs,
92
- patch("app.frontend_management.os.listdir") as mock_listdir,
93
- patch("app.frontend_management.os.rmdir") as mock_rmdir,
94
- ):
95
- mock_listdir.return_value = [] # Simulate empty directory
96
- yield mock_makedirs, mock_listdir, mock_rmdir
97
-
98
-
99
- @pytest.fixture
100
- def mock_download():
101
- with patch("app.frontend_management.download_release_asset_zip") as mock:
102
- mock.side_effect = Exception("Download failed") # Simulate download failure
103
- yield mock
104
-
105
-
106
- def test_finally_block(mock_os_functions, mock_download, mock_provider):
107
- # Arrange
108
- mock_makedirs, mock_listdir, mock_rmdir = mock_os_functions
109
- version_string = "test-owner/test-repo@1.0.0"
110
-
111
- # Act & Assert
112
- with pytest.raises(Exception):
113
- FrontendManager.init_frontend_unsafe(version_string, mock_provider)
114
-
115
- # Assert
116
- mock_makedirs.assert_called_once()
117
- mock_download.assert_called_once()
118
- mock_listdir.assert_called_once()
119
- mock_rmdir.assert_called_once()
120
-
121
-
122
- def test_parse_version_string():
123
- version_string = "owner/repo@1.0.0"
124
- repo_owner, repo_name, version = FrontendManager.parse_version_string(
125
- version_string
126
- )
127
- assert repo_owner == "owner"
128
- assert repo_name == "repo"
129
- assert version == "1.0.0"
130
-
131
-
132
- def test_parse_version_string_invalid():
133
- version_string = "invalid"
134
- with pytest.raises(argparse.ArgumentTypeError):
135
- FrontendManager.parse_version_string(version_string)
136
-
137
-
138
- def test_init_frontend_default_with_mocks():
139
- # Arrange
140
- version_string = DEFAULT_VERSION_STRING
141
-
142
- # Act
143
- with (
144
- patch("app.frontend_management.check_frontend_version") as mock_check,
145
- patch.object(
146
- FrontendManager, "default_frontend_path", return_value="/mocked/path"
147
- ),
148
- ):
149
- frontend_path = FrontendManager.init_frontend(version_string)
150
-
151
- # Assert
152
- assert frontend_path == "/mocked/path"
153
- mock_check.assert_called_once()
154
-
155
-
156
- def test_init_frontend_fallback_on_error():
157
- # Arrange
158
- version_string = "test-owner/test-repo@1.0.0"
159
-
160
- # Act
161
- with (
162
- patch.object(
163
- FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error")
164
- ),
165
- patch("app.frontend_management.check_frontend_version") as mock_check,
166
- patch.object(
167
- FrontendManager, "default_frontend_path", return_value="/default/path"
168
- ),
169
- ):
170
- frontend_path = FrontendManager.init_frontend(version_string)
171
-
172
- # Assert
173
- assert frontend_path == "/default/path"
174
- mock_check.assert_called_once()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/app_test/model_manager_test.py DELETED
@@ -1,62 +0,0 @@
1
- import pytest
2
- import base64
3
- import json
4
- import struct
5
- from io import BytesIO
6
- from PIL import Image
7
- from aiohttp import web
8
- from unittest.mock import patch
9
- from app.model_manager import ModelFileManager
10
-
11
- pytestmark = (
12
- pytest.mark.asyncio
13
- ) # This applies the asyncio mark to all test functions in the module
14
-
15
- @pytest.fixture
16
- def model_manager():
17
- return ModelFileManager()
18
-
19
- @pytest.fixture
20
- def app(model_manager):
21
- app = web.Application()
22
- routes = web.RouteTableDef()
23
- model_manager.add_routes(routes)
24
- app.add_routes(routes)
25
- return app
26
-
27
- async def test_get_model_preview_safetensors(aiohttp_client, app, tmp_path):
28
- img = Image.new('RGB', (100, 100), 'white')
29
- img_byte_arr = BytesIO()
30
- img.save(img_byte_arr, format='PNG')
31
- img_byte_arr.seek(0)
32
- img_b64 = base64.b64encode(img_byte_arr.getvalue()).decode('utf-8')
33
-
34
- safetensors_file = tmp_path / "test_model.safetensors"
35
- header_bytes = json.dumps({
36
- "__metadata__": {
37
- "ssmd_cover_images": json.dumps([img_b64])
38
- }
39
- }).encode('utf-8')
40
- length_bytes = struct.pack('<Q', len(header_bytes))
41
- with open(safetensors_file, 'wb') as f:
42
- f.write(length_bytes)
43
- f.write(header_bytes)
44
-
45
- with patch('folder_paths.folder_names_and_paths', {
46
- 'test_folder': ([str(tmp_path)], None)
47
- }):
48
- client = await aiohttp_client(app)
49
- response = await client.get('/experiment/models/preview/test_folder/0/test_model.safetensors')
50
-
51
- # Verify response
52
- assert response.status == 200
53
- assert response.content_type == 'image/webp'
54
-
55
- # Verify the response contains valid image data
56
- img_bytes = BytesIO(await response.read())
57
- img = Image.open(img_bytes)
58
- assert img.format
59
- assert img.format.lower() == 'webp'
60
-
61
- # Clean up
62
- img.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/comfy_api_nodes_test/mapper_utils_test.py DELETED
@@ -1,297 +0,0 @@
1
- from typing import Optional
2
- from enum import Enum
3
-
4
- from pydantic import BaseModel, Field
5
-
6
- from comfy.comfy_types.node_typing import IO
7
- from comfy_api_nodes.mapper_utils import model_field_to_node_input
8
-
9
-
10
- def test_model_field_to_float_input():
11
- """Tests mapping a float field with constraints."""
12
-
13
- class ModelWithFloatField(BaseModel):
14
- cfg_scale: Optional[float] = Field(
15
- default=0.5,
16
- description="Flexibility in video generation",
17
- ge=0.0,
18
- le=1.0,
19
- multiple_of=0.001,
20
- )
21
-
22
- expected_output = (
23
- IO.FLOAT,
24
- {
25
- "default": 0.5,
26
- "tooltip": "Flexibility in video generation",
27
- "min": 0.0,
28
- "max": 1.0,
29
- "step": 0.001,
30
- },
31
- )
32
-
33
- actual_output = model_field_to_node_input(
34
- IO.FLOAT, ModelWithFloatField, "cfg_scale"
35
- )
36
-
37
- assert actual_output[0] == expected_output[0]
38
- assert actual_output[1] == expected_output[1]
39
-
40
-
41
- def test_model_field_to_float_input_no_constraints():
42
- """Tests mapping a float field with no constraints."""
43
-
44
- class ModelWithFloatField(BaseModel):
45
- cfg_scale: Optional[float] = Field(default=0.5)
46
-
47
- expected_output = (
48
- IO.FLOAT,
49
- {
50
- "default": 0.5,
51
- },
52
- )
53
-
54
- actual_output = model_field_to_node_input(
55
- IO.FLOAT, ModelWithFloatField, "cfg_scale"
56
- )
57
-
58
- assert actual_output[0] == expected_output[0]
59
- assert actual_output[1] == expected_output[1]
60
-
61
-
62
- def test_model_field_to_int_input():
63
- """Tests mapping an int field with constraints."""
64
-
65
- class ModelWithIntField(BaseModel):
66
- num_frames: Optional[int] = Field(
67
- default=10,
68
- description="Number of frames to generate",
69
- ge=1,
70
- le=100,
71
- multiple_of=1,
72
- )
73
-
74
- expected_output = (
75
- IO.INT,
76
- {
77
- "default": 10,
78
- "tooltip": "Number of frames to generate",
79
- "min": 1,
80
- "max": 100,
81
- "step": 1,
82
- },
83
- )
84
-
85
- actual_output = model_field_to_node_input(IO.INT, ModelWithIntField, "num_frames")
86
-
87
- assert actual_output[0] == expected_output[0]
88
- assert actual_output[1] == expected_output[1]
89
-
90
-
91
- def test_model_field_to_string_input():
92
- """Tests mapping a string field."""
93
-
94
- class ModelWithStringField(BaseModel):
95
- prompt: Optional[str] = Field(
96
- default="A beautiful sunset over a calm ocean",
97
- description="A prompt for the video generation",
98
- )
99
-
100
- expected_output = (
101
- IO.STRING,
102
- {
103
- "default": "A beautiful sunset over a calm ocean",
104
- "tooltip": "A prompt for the video generation",
105
- },
106
- )
107
-
108
- actual_output = model_field_to_node_input(IO.STRING, ModelWithStringField, "prompt")
109
-
110
- assert actual_output[0] == expected_output[0]
111
- assert actual_output[1] == expected_output[1]
112
-
113
-
114
- def test_model_field_to_string_input_multiline():
115
- """Tests mapping a string field."""
116
-
117
- class ModelWithStringField(BaseModel):
118
- prompt: Optional[str] = Field(
119
- default="A beautiful sunset over a calm ocean",
120
- description="A prompt for the video generation",
121
- )
122
-
123
- expected_output = (
124
- IO.STRING,
125
- {
126
- "default": "A beautiful sunset over a calm ocean",
127
- "tooltip": "A prompt for the video generation",
128
- "multiline": True,
129
- },
130
- )
131
-
132
- actual_output = model_field_to_node_input(
133
- IO.STRING, ModelWithStringField, "prompt", multiline=True
134
- )
135
-
136
- assert actual_output[0] == expected_output[0]
137
- assert actual_output[1] == expected_output[1]
138
-
139
-
140
- def test_model_field_to_combo_input():
141
- """Tests mapping a combo field."""
142
-
143
- class MockEnum(str, Enum):
144
- option_1 = "option 1"
145
- option_2 = "option 2"
146
- option_3 = "option 3"
147
-
148
- class ModelWithComboField(BaseModel):
149
- model_name: Optional[MockEnum] = Field("option 1", description="Model Name")
150
-
151
- expected_output = (
152
- IO.COMBO,
153
- {
154
- "options": ["option 1", "option 2", "option 3"],
155
- "default": "option 1",
156
- "tooltip": "Model Name",
157
- },
158
- )
159
-
160
- actual_output = model_field_to_node_input(
161
- IO.COMBO, ModelWithComboField, "model_name", enum_type=MockEnum
162
- )
163
-
164
- assert actual_output[0] == expected_output[0]
165
- assert actual_output[1] == expected_output[1]
166
-
167
-
168
- def test_model_field_to_combo_input_no_options():
169
- """Tests mapping a combo field with no options."""
170
-
171
- class ModelWithComboField(BaseModel):
172
- model_name: Optional[str] = Field(description="Model Name")
173
-
174
- expected_output = (
175
- IO.COMBO,
176
- {
177
- "tooltip": "Model Name",
178
- },
179
- )
180
-
181
- actual_output = model_field_to_node_input(
182
- IO.COMBO, ModelWithComboField, "model_name"
183
- )
184
-
185
- assert actual_output[0] == expected_output[0]
186
- assert actual_output[1] == expected_output[1]
187
-
188
-
189
- def test_model_field_to_image_input():
190
- """Tests mapping an image field."""
191
-
192
- class ModelWithImageField(BaseModel):
193
- image: Optional[str] = Field(
194
- default=None,
195
- description="An image for the video generation",
196
- )
197
-
198
- expected_output = (
199
- IO.IMAGE,
200
- {
201
- "default": None,
202
- "tooltip": "An image for the video generation",
203
- },
204
- )
205
-
206
- actual_output = model_field_to_node_input(IO.IMAGE, ModelWithImageField, "image")
207
-
208
- assert actual_output[0] == expected_output[0]
209
- assert actual_output[1] == expected_output[1]
210
-
211
-
212
- def test_model_field_to_node_input_no_description():
213
- """Tests mapping a field with no description."""
214
-
215
- class ModelWithNoDescriptionField(BaseModel):
216
- field: Optional[str] = Field(default="default value")
217
-
218
- expected_output = (
219
- IO.STRING,
220
- {
221
- "default": "default value",
222
- },
223
- )
224
-
225
- actual_output = model_field_to_node_input(
226
- IO.STRING, ModelWithNoDescriptionField, "field"
227
- )
228
-
229
- assert actual_output[0] == expected_output[0]
230
- assert actual_output[1] == expected_output[1]
231
-
232
-
233
- def test_model_field_to_node_input_no_default():
234
- """Tests mapping a field with no default."""
235
-
236
- class ModelWithNoDefaultField(BaseModel):
237
- field: Optional[str] = Field(description="A field with no default")
238
-
239
- expected_output = (
240
- IO.STRING,
241
- {
242
- "tooltip": "A field with no default",
243
- },
244
- )
245
-
246
- actual_output = model_field_to_node_input(
247
- IO.STRING, ModelWithNoDefaultField, "field"
248
- )
249
-
250
- assert actual_output[0] == expected_output[0]
251
- assert actual_output[1] == expected_output[1]
252
-
253
-
254
- def test_model_field_to_node_input_no_metadata():
255
- """Tests mapping a field with no metadata or properties defined on the schema."""
256
-
257
- class ModelWithNoMetadataField(BaseModel):
258
- field: Optional[str] = Field()
259
-
260
- expected_output = (
261
- IO.STRING,
262
- {},
263
- )
264
-
265
- actual_output = model_field_to_node_input(
266
- IO.STRING, ModelWithNoMetadataField, "field"
267
- )
268
-
269
- assert actual_output[0] == expected_output[0]
270
- assert actual_output[1] == expected_output[1]
271
-
272
-
273
- def test_model_field_to_node_input_default_is_none():
274
- """
275
- Tests mapping a field with a default of `None`.
276
- I.e., the default field should be included as the schema explicitly sets it to `None`.
277
- """
278
-
279
- class ModelWithNoneDefaultField(BaseModel):
280
- field: Optional[str] = Field(
281
- default=None, description="A field with a default of None"
282
- )
283
-
284
- expected_output = (
285
- IO.STRING,
286
- {
287
- "default": None,
288
- "tooltip": "A field with a default of None",
289
- },
290
- )
291
-
292
- actual_output = model_field_to_node_input(
293
- IO.STRING, ModelWithNoneDefaultField, "field"
294
- )
295
-
296
- assert actual_output[0] == expected_output[0]
297
- assert actual_output[1] == expected_output[1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/comfy_api_test/input_impl_test.py DELETED
@@ -1,91 +0,0 @@
1
- import io
2
- from comfy_api.input_impl.video_types import (
3
- container_to_output_format,
4
- get_open_write_kwargs,
5
- )
6
- from comfy_api.util import VideoContainer
7
-
8
-
9
- def test_container_to_output_format_empty_string():
10
- """Test that an empty string input returns None. `None` arg allows default auto-detection."""
11
- assert container_to_output_format("") is None
12
-
13
-
14
- def test_container_to_output_format_none():
15
- """Test that None input returns None."""
16
- assert container_to_output_format(None) is None
17
-
18
-
19
- def test_container_to_output_format_comma_separated():
20
- """Test that a comma-separated list returns a valid singular format from the list."""
21
- comma_separated_format = "mp4,mov,m4a"
22
- output_format = container_to_output_format(comma_separated_format)
23
- assert output_format in comma_separated_format
24
-
25
-
26
- def test_container_to_output_format_single():
27
- """Test that a single format string (not comma-separated list) is returned as is."""
28
- assert container_to_output_format("mp4") == "mp4"
29
-
30
-
31
- def test_get_open_write_kwargs_filepath_no_format():
32
- """Test that 'format' kwarg is NOT set when dest is a file path."""
33
- kwargs_auto = get_open_write_kwargs("output.mp4", "mp4", VideoContainer.AUTO)
34
- assert "format" not in kwargs_auto, "Format should not be set for file paths (AUTO)"
35
-
36
- kwargs_specific = get_open_write_kwargs("output.avi", "mp4", "avi")
37
- fail_msg = "Format should not be set for file paths (Specific)"
38
- assert "format" not in kwargs_specific, fail_msg
39
-
40
-
41
- def test_get_open_write_kwargs_base_options_mode():
42
- """Test basic kwargs for file path: mode and movflags."""
43
- kwargs = get_open_write_kwargs("output.mp4", "mp4", VideoContainer.AUTO)
44
- assert kwargs["mode"] == "w", "mode should be set to write"
45
-
46
- fail_msg = "movflags should be set to preserve custom metadata tags"
47
- assert "movflags" in kwargs["options"], fail_msg
48
- assert kwargs["options"]["movflags"] == "use_metadata_tags", fail_msg
49
-
50
-
51
- def test_get_open_write_kwargs_bytesio_auto_format():
52
- """Test kwargs for BytesIO dest with AUTO format."""
53
- dest = io.BytesIO()
54
- container_fmt = "mov,mp4,m4a"
55
- kwargs = get_open_write_kwargs(dest, container_fmt, VideoContainer.AUTO)
56
-
57
- assert kwargs["mode"] == "w"
58
- assert kwargs["options"]["movflags"] == "use_metadata_tags"
59
-
60
- fail_msg = (
61
- "Format should be a valid format from the container's format list when AUTO"
62
- )
63
- assert kwargs["format"] in container_fmt, fail_msg
64
-
65
-
66
- def test_get_open_write_kwargs_bytesio_specific_format():
67
- """Test kwargs for BytesIO dest with a specific single format."""
68
- dest = io.BytesIO()
69
- container_fmt = "avi"
70
- to_fmt = VideoContainer.MP4
71
- kwargs = get_open_write_kwargs(dest, container_fmt, to_fmt)
72
-
73
- assert kwargs["mode"] == "w"
74
- assert kwargs["options"]["movflags"] == "use_metadata_tags"
75
-
76
- fail_msg = "Format should be the specified format (lowercased) when output format is not AUTO"
77
- assert kwargs["format"] == "mp4", fail_msg
78
-
79
-
80
- def test_get_open_write_kwargs_bytesio_specific_format_list():
81
- """Test kwargs for BytesIO dest with a specific comma-separated format."""
82
- dest = io.BytesIO()
83
- container_fmt = "avi"
84
- to_fmt = "mov,mp4,m4a" # A format string that is a list
85
- kwargs = get_open_write_kwargs(dest, container_fmt, to_fmt)
86
-
87
- assert kwargs["mode"] == "w"
88
- assert kwargs["options"]["movflags"] == "use_metadata_tags"
89
-
90
- fail_msg = "Format should be a valid format from the specified format list when output format is not AUTO"
91
- assert kwargs["format"] in to_fmt, fail_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/comfy_api_test/video_types_test.py DELETED
@@ -1,239 +0,0 @@
1
- import pytest
2
- import torch
3
- import tempfile
4
- import os
5
- import av
6
- import io
7
- from fractions import Fraction
8
- from comfy_api.input_impl.video_types import VideoFromFile, VideoFromComponents
9
- from comfy_api.util.video_types import VideoComponents
10
- from comfy_api.input.basic_types import AudioInput
11
- from av.error import InvalidDataError
12
-
13
- EPSILON = 0.0001
14
-
15
-
16
- @pytest.fixture
17
- def sample_images():
18
- """3-frame 2x2 RGB video tensor"""
19
- return torch.rand(3, 2, 2, 3)
20
-
21
-
22
- @pytest.fixture
23
- def sample_audio():
24
- """Stereo audio with 44.1kHz sample rate"""
25
- return AudioInput(
26
- {
27
- "waveform": torch.rand(1, 2, 1000),
28
- "sample_rate": 44100,
29
- }
30
- )
31
-
32
-
33
- @pytest.fixture
34
- def video_components(sample_images, sample_audio):
35
- """VideoComponents with images, audio, and metadata"""
36
- return VideoComponents(
37
- images=sample_images,
38
- audio=sample_audio,
39
- frame_rate=Fraction(30),
40
- metadata={"test": "metadata"},
41
- )
42
-
43
-
44
- def create_test_video(width=4, height=4, frames=3, fps=30):
45
- """Helper to create a temporary video file"""
46
- tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
47
- with av.open(tmp.name, mode="w") as container:
48
- stream = container.add_stream("h264", rate=fps)
49
- stream.width = width
50
- stream.height = height
51
- stream.pix_fmt = "yuv420p"
52
-
53
- for i in range(frames):
54
- frame = av.VideoFrame.from_ndarray(
55
- torch.ones(height, width, 3, dtype=torch.uint8).numpy() * (i * 85),
56
- format="rgb24",
57
- )
58
- frame = frame.reformat(format="yuv420p")
59
- packet = stream.encode(frame)
60
- container.mux(packet)
61
-
62
- # Flush
63
- packet = stream.encode(None)
64
- container.mux(packet)
65
-
66
- return tmp.name
67
-
68
-
69
- @pytest.fixture
70
- def simple_video_file():
71
- """4x4 video with 3 frames at 30fps"""
72
- file_path = create_test_video()
73
- yield file_path
74
- os.unlink(file_path)
75
-
76
-
77
- def test_video_from_components_get_duration(video_components):
78
- """Duration calculated correctly from frame count and frame rate"""
79
- video = VideoFromComponents(video_components)
80
- duration = video.get_duration()
81
-
82
- expected_duration = 3.0 / 30.0
83
- assert duration == pytest.approx(expected_duration)
84
-
85
-
86
- def test_video_from_components_get_duration_different_frame_rates(sample_images):
87
- """Duration correct for different frame rates including fractional"""
88
- # Test with 60 fps
89
- components_60fps = VideoComponents(images=sample_images, frame_rate=Fraction(60))
90
- video_60fps = VideoFromComponents(components_60fps)
91
- assert video_60fps.get_duration() == pytest.approx(3.0 / 60.0)
92
-
93
- # Test with fractional frame rate (23.976fps)
94
- components_frac = VideoComponents(
95
- images=sample_images, frame_rate=Fraction(24000, 1001)
96
- )
97
- video_frac = VideoFromComponents(components_frac)
98
- expected_frac = 3.0 / (24000.0 / 1001.0)
99
- assert video_frac.get_duration() == pytest.approx(expected_frac)
100
-
101
-
102
- def test_video_from_components_get_duration_empty_video():
103
- """Duration is zero for empty video"""
104
- empty_components = VideoComponents(
105
- images=torch.zeros(0, 2, 2, 3), frame_rate=Fraction(30)
106
- )
107
- video = VideoFromComponents(empty_components)
108
- assert video.get_duration() == 0.0
109
-
110
-
111
- def test_video_from_components_get_dimensions(video_components):
112
- """Dimensions returned correctly from image tensor shape"""
113
- video = VideoFromComponents(video_components)
114
- width, height = video.get_dimensions()
115
- assert width == 2
116
- assert height == 2
117
-
118
-
119
- def test_video_from_file_get_duration(simple_video_file):
120
- """Duration extracted from file metadata"""
121
- video = VideoFromFile(simple_video_file)
122
- duration = video.get_duration()
123
- assert duration == pytest.approx(0.1, abs=0.01)
124
-
125
-
126
- def test_video_from_file_get_dimensions(simple_video_file):
127
- """Dimensions read from stream without decoding frames"""
128
- video = VideoFromFile(simple_video_file)
129
- width, height = video.get_dimensions()
130
- assert width == 4
131
- assert height == 4
132
-
133
-
134
- def test_video_from_file_bytesio_input():
135
- """VideoFromFile works with BytesIO input"""
136
- buffer = io.BytesIO()
137
- with av.open(buffer, mode="w", format="mp4") as container:
138
- stream = container.add_stream("h264", rate=30)
139
- stream.width = 2
140
- stream.height = 2
141
- stream.pix_fmt = "yuv420p"
142
-
143
- frame = av.VideoFrame.from_ndarray(
144
- torch.zeros(2, 2, 3, dtype=torch.uint8).numpy(), format="rgb24"
145
- )
146
- frame = frame.reformat(format="yuv420p")
147
- packet = stream.encode(frame)
148
- container.mux(packet)
149
- packet = stream.encode(None)
150
- container.mux(packet)
151
-
152
- buffer.seek(0)
153
- video = VideoFromFile(buffer)
154
-
155
- assert video.get_dimensions() == (2, 2)
156
- assert video.get_duration() == pytest.approx(1 / 30, abs=0.01)
157
-
158
-
159
- def test_video_from_file_invalid_file_error():
160
- """InvalidDataError raised for non-video files"""
161
- with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
162
- tmp.write(b"not a video file")
163
- tmp.flush()
164
- tmp_name = tmp.name
165
-
166
- try:
167
- with pytest.raises(InvalidDataError):
168
- video = VideoFromFile(tmp_name)
169
- video.get_dimensions()
170
- finally:
171
- os.unlink(tmp_name)
172
-
173
-
174
- def test_video_from_file_audio_only_error():
175
- """ValueError raised for audio-only files"""
176
- with tempfile.NamedTemporaryFile(suffix=".m4a", delete=False) as tmp:
177
- tmp_name = tmp.name
178
-
179
- try:
180
- with av.open(tmp_name, mode="w") as container:
181
- stream = container.add_stream("aac", rate=44100)
182
- stream.sample_rate = 44100
183
- stream.format = "fltp"
184
-
185
- audio_data = torch.zeros(1, 1024).numpy()
186
- audio_frame = av.AudioFrame.from_ndarray(
187
- audio_data, format="fltp", layout="mono"
188
- )
189
- audio_frame.sample_rate = 44100
190
- audio_frame.pts = 0
191
- packet = stream.encode(audio_frame)
192
- container.mux(packet)
193
-
194
- for packet in stream.encode(None):
195
- container.mux(packet)
196
-
197
- with pytest.raises(ValueError, match="No video stream found"):
198
- video = VideoFromFile(tmp_name)
199
- video.get_dimensions()
200
- finally:
201
- os.unlink(tmp_name)
202
-
203
-
204
- def test_single_frame_video():
205
- """Single frame video has correct duration"""
206
- components = VideoComponents(
207
- images=torch.rand(1, 10, 10, 3), frame_rate=Fraction(1)
208
- )
209
- video = VideoFromComponents(components)
210
- assert video.get_duration() == 1.0
211
-
212
-
213
- @pytest.mark.parametrize(
214
- "frame_rate,expected_fps",
215
- [
216
- (Fraction(24000, 1001), 24000 / 1001),
217
- (Fraction(30000, 1001), 30000 / 1001),
218
- (Fraction(25, 1), 25.0),
219
- (Fraction(50, 2), 25.0),
220
- ],
221
- )
222
- def test_fractional_frame_rates(frame_rate, expected_fps):
223
- """Duration calculated correctly for various fractional frame rates"""
224
- components = VideoComponents(images=torch.rand(100, 4, 4, 3), frame_rate=frame_rate)
225
- video = VideoFromComponents(components)
226
- duration = video.get_duration()
227
- expected_duration = 100.0 / expected_fps
228
- assert duration == pytest.approx(expected_duration)
229
-
230
-
231
- def test_duration_consistency(video_components):
232
- """get_duration() consistent with manual calculation from components"""
233
- video = VideoFromComponents(video_components)
234
-
235
- duration = video.get_duration()
236
- components = video.get_components()
237
- manual_duration = float(components.images.shape[0] / components.frame_rate)
238
-
239
- assert duration == pytest.approx(manual_duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/comfy_extras_test/__init__.py DELETED
File without changes
tests-unit/comfy_extras_test/image_stitch_test.py DELETED
@@ -1,243 +0,0 @@
1
- import torch
2
- from unittest.mock import patch, MagicMock
3
-
4
- # Mock nodes module to prevent CUDA initialization during import
5
- mock_nodes = MagicMock()
6
- mock_nodes.MAX_RESOLUTION = 16384
7
-
8
- # Mock server module for PromptServer
9
- mock_server = MagicMock()
10
-
11
- with patch.dict('sys.modules', {'nodes': mock_nodes, 'server': mock_server}):
12
- from comfy_extras.nodes_images import ImageStitch
13
-
14
-
15
- class TestImageStitch:
16
-
17
- def create_test_image(self, batch_size=1, height=64, width=64, channels=3):
18
- """Helper to create test images with specific dimensions"""
19
- return torch.rand(batch_size, height, width, channels)
20
-
21
- def test_no_image2_passthrough(self):
22
- """Test that when image2 is None, image1 is returned unchanged"""
23
- node = ImageStitch()
24
- image1 = self.create_test_image()
25
-
26
- result = node.stitch(image1, "right", True, 0, "white", image2=None)
27
-
28
- assert len(result) == 1
29
- assert torch.equal(result[0], image1)
30
-
31
- def test_basic_horizontal_stitch_right(self):
32
- """Test basic horizontal stitching to the right"""
33
- node = ImageStitch()
34
- image1 = self.create_test_image(height=32, width=32)
35
- image2 = self.create_test_image(height=32, width=24)
36
-
37
- result = node.stitch(image1, "right", False, 0, "white", image2)
38
-
39
- assert result[0].shape == (1, 32, 56, 3) # 32 + 24 width
40
-
41
- def test_basic_horizontal_stitch_left(self):
42
- """Test basic horizontal stitching to the left"""
43
- node = ImageStitch()
44
- image1 = self.create_test_image(height=32, width=32)
45
- image2 = self.create_test_image(height=32, width=24)
46
-
47
- result = node.stitch(image1, "left", False, 0, "white", image2)
48
-
49
- assert result[0].shape == (1, 32, 56, 3) # 24 + 32 width
50
-
51
- def test_basic_vertical_stitch_down(self):
52
- """Test basic vertical stitching downward"""
53
- node = ImageStitch()
54
- image1 = self.create_test_image(height=32, width=32)
55
- image2 = self.create_test_image(height=24, width=32)
56
-
57
- result = node.stitch(image1, "down", False, 0, "white", image2)
58
-
59
- assert result[0].shape == (1, 56, 32, 3) # 32 + 24 height
60
-
61
- def test_basic_vertical_stitch_up(self):
62
- """Test basic vertical stitching upward"""
63
- node = ImageStitch()
64
- image1 = self.create_test_image(height=32, width=32)
65
- image2 = self.create_test_image(height=24, width=32)
66
-
67
- result = node.stitch(image1, "up", False, 0, "white", image2)
68
-
69
- assert result[0].shape == (1, 56, 32, 3) # 24 + 32 height
70
-
71
- def test_size_matching_horizontal(self):
72
- """Test size matching for horizontal concatenation"""
73
- node = ImageStitch()
74
- image1 = self.create_test_image(height=64, width=64)
75
- image2 = self.create_test_image(height=32, width=32) # Different aspect ratio
76
-
77
- result = node.stitch(image1, "right", True, 0, "white", image2)
78
-
79
- # image2 should be resized to match image1's height (64) with preserved aspect ratio
80
- expected_width = 64 + 64 # original + resized (32*64/32 = 64)
81
- assert result[0].shape == (1, 64, expected_width, 3)
82
-
83
- def test_size_matching_vertical(self):
84
- """Test size matching for vertical concatenation"""
85
- node = ImageStitch()
86
- image1 = self.create_test_image(height=64, width=64)
87
- image2 = self.create_test_image(height=32, width=32)
88
-
89
- result = node.stitch(image1, "down", True, 0, "white", image2)
90
-
91
- # image2 should be resized to match image1's width (64) with preserved aspect ratio
92
- expected_height = 64 + 64 # original + resized (32*64/32 = 64)
93
- assert result[0].shape == (1, expected_height, 64, 3)
94
-
95
- def test_padding_for_mismatched_heights_horizontal(self):
96
- """Test padding when heights don't match in horizontal concatenation"""
97
- node = ImageStitch()
98
- image1 = self.create_test_image(height=64, width=32)
99
- image2 = self.create_test_image(height=48, width=24) # Shorter height
100
-
101
- result = node.stitch(image1, "right", False, 0, "white", image2)
102
-
103
- # Both images should be padded to height 64
104
- assert result[0].shape == (1, 64, 56, 3) # 32 + 24 width, max(64,48) height
105
-
106
- def test_padding_for_mismatched_widths_vertical(self):
107
- """Test padding when widths don't match in vertical concatenation"""
108
- node = ImageStitch()
109
- image1 = self.create_test_image(height=32, width=64)
110
- image2 = self.create_test_image(height=24, width=48) # Narrower width
111
-
112
- result = node.stitch(image1, "down", False, 0, "white", image2)
113
-
114
- # Both images should be padded to width 64
115
- assert result[0].shape == (1, 56, 64, 3) # 32 + 24 height, max(64,48) width
116
-
117
- def test_spacing_horizontal(self):
118
- """Test spacing addition in horizontal concatenation"""
119
- node = ImageStitch()
120
- image1 = self.create_test_image(height=32, width=32)
121
- image2 = self.create_test_image(height=32, width=24)
122
- spacing_width = 16
123
-
124
- result = node.stitch(image1, "right", False, spacing_width, "white", image2)
125
-
126
- # Expected width: 32 + 16 (spacing) + 24 = 72
127
- assert result[0].shape == (1, 32, 72, 3)
128
-
129
- def test_spacing_vertical(self):
130
- """Test spacing addition in vertical concatenation"""
131
- node = ImageStitch()
132
- image1 = self.create_test_image(height=32, width=32)
133
- image2 = self.create_test_image(height=24, width=32)
134
- spacing_width = 16
135
-
136
- result = node.stitch(image1, "down", False, spacing_width, "white", image2)
137
-
138
- # Expected height: 32 + 16 (spacing) + 24 = 72
139
- assert result[0].shape == (1, 72, 32, 3)
140
-
141
- def test_spacing_color_values(self):
142
- """Test that spacing colors are applied correctly"""
143
- node = ImageStitch()
144
- image1 = self.create_test_image(height=32, width=32)
145
- image2 = self.create_test_image(height=32, width=32)
146
-
147
- # Test white spacing
148
- result_white = node.stitch(image1, "right", False, 16, "white", image2)
149
- # Check that spacing region contains white values (close to 1.0)
150
- spacing_region = result_white[0][:, :, 32:48, :] # Middle 16 pixels
151
- assert torch.all(spacing_region >= 0.9) # Should be close to white
152
-
153
- # Test black spacing
154
- result_black = node.stitch(image1, "right", False, 16, "black", image2)
155
- spacing_region = result_black[0][:, :, 32:48, :]
156
- assert torch.all(spacing_region <= 0.1) # Should be close to black
157
-
158
- def test_odd_spacing_width_made_even(self):
159
- """Test that odd spacing widths are made even"""
160
- node = ImageStitch()
161
- image1 = self.create_test_image(height=32, width=32)
162
- image2 = self.create_test_image(height=32, width=32)
163
-
164
- # Use odd spacing width
165
- result = node.stitch(image1, "right", False, 15, "white", image2)
166
-
167
- # Should be made even (16), so total width = 32 + 16 + 32 = 80
168
- assert result[0].shape == (1, 32, 80, 3)
169
-
170
- def test_batch_size_matching(self):
171
- """Test that different batch sizes are handled correctly"""
172
- node = ImageStitch()
173
- image1 = self.create_test_image(batch_size=2, height=32, width=32)
174
- image2 = self.create_test_image(batch_size=1, height=32, width=32)
175
-
176
- result = node.stitch(image1, "right", False, 0, "white", image2)
177
-
178
- # Should match larger batch size
179
- assert result[0].shape == (2, 32, 64, 3)
180
-
181
- def test_channel_matching_rgb_to_rgba(self):
182
- """Test that channel differences are handled (RGB + alpha)"""
183
- node = ImageStitch()
184
- image1 = self.create_test_image(channels=3) # RGB
185
- image2 = self.create_test_image(channels=4) # RGBA
186
-
187
- result = node.stitch(image1, "right", False, 0, "white", image2)
188
-
189
- # Should have 4 channels (RGBA)
190
- assert result[0].shape[-1] == 4
191
-
192
- def test_channel_matching_rgba_to_rgb(self):
193
- """Test that channel differences are handled (RGBA + RGB)"""
194
- node = ImageStitch()
195
- image1 = self.create_test_image(channels=4) # RGBA
196
- image2 = self.create_test_image(channels=3) # RGB
197
-
198
- result = node.stitch(image1, "right", False, 0, "white", image2)
199
-
200
- # Should have 4 channels (RGBA)
201
- assert result[0].shape[-1] == 4
202
-
203
- def test_all_color_options(self):
204
- """Test all available color options"""
205
- node = ImageStitch()
206
- image1 = self.create_test_image(height=32, width=32)
207
- image2 = self.create_test_image(height=32, width=32)
208
-
209
- colors = ["white", "black", "red", "green", "blue"]
210
-
211
- for color in colors:
212
- result = node.stitch(image1, "right", False, 16, color, image2)
213
- assert result[0].shape == (1, 32, 80, 3) # Basic shape check
214
-
215
- def test_all_directions(self):
216
- """Test all direction options"""
217
- node = ImageStitch()
218
- image1 = self.create_test_image(height=32, width=32)
219
- image2 = self.create_test_image(height=32, width=32)
220
-
221
- directions = ["right", "left", "up", "down"]
222
-
223
- for direction in directions:
224
- result = node.stitch(image1, direction, False, 0, "white", image2)
225
- assert result[0].shape == (1, 32, 64, 3) if direction in ["right", "left"] else (1, 64, 32, 3)
226
-
227
- def test_batch_size_channel_spacing_integration(self):
228
- """Test integration of batch matching, channel matching, size matching, and spacings"""
229
- node = ImageStitch()
230
- image1 = self.create_test_image(batch_size=2, height=64, width=48, channels=3)
231
- image2 = self.create_test_image(batch_size=1, height=32, width=32, channels=4)
232
-
233
- result = node.stitch(image1, "right", True, 8, "red", image2)
234
-
235
- # Should handle: batch matching, size matching, channel matching, spacing
236
- assert result[0].shape[0] == 2 # Batch size matched
237
- assert result[0].shape[-1] == 4 # Channels matched to max
238
- assert result[0].shape[1] == 64 # Height from image1 (size matching)
239
- # Width should be: 48 + 8 (spacing) + resized_image2_width
240
- expected_image2_width = int(64 * (32/32)) # Resized to height 64
241
- expected_total_width = 48 + 8 + expected_image2_width
242
- assert result[0].shape[2] == expected_total_width
243
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/comfy_test/folder_path_test.py DELETED
@@ -1,162 +0,0 @@
1
- ### 🗻 This file is created through the spirit of Mount Fuji at its peak
2
- # TODO(yoland): clean up this after I get back down
3
- import sys
4
- import pytest
5
- import os
6
- import tempfile
7
- from unittest.mock import patch
8
- from importlib import reload
9
-
10
- import folder_paths
11
- import comfy.cli_args
12
- from comfy.options import enable_args_parsing
13
- enable_args_parsing()
14
-
15
-
16
- @pytest.fixture()
17
- def clear_folder_paths():
18
- # Reload the module after each test to ensure isolation
19
- yield
20
- reload(folder_paths)
21
-
22
- @pytest.fixture
23
- def temp_dir():
24
- with tempfile.TemporaryDirectory() as tmpdirname:
25
- yield tmpdirname
26
-
27
-
28
- @pytest.fixture
29
- def set_base_dir():
30
- def _set_base_dir(base_dir):
31
- # Mock CLI args
32
- with patch.object(sys, 'argv', ["main.py", "--base-directory", base_dir]):
33
- reload(comfy.cli_args)
34
- reload(folder_paths)
35
- yield _set_base_dir
36
- # Reload the modules after each test to ensure isolation
37
- with patch.object(sys, 'argv', ["main.py"]):
38
- reload(comfy.cli_args)
39
- reload(folder_paths)
40
-
41
-
42
- def test_get_directory_by_type(clear_folder_paths):
43
- test_dir = "/test/dir"
44
- folder_paths.set_output_directory(test_dir)
45
- assert folder_paths.get_directory_by_type("output") == test_dir
46
- assert folder_paths.get_directory_by_type("invalid") is None
47
-
48
- def test_annotated_filepath():
49
- assert folder_paths.annotated_filepath("test.txt") == ("test.txt", None)
50
- assert folder_paths.annotated_filepath("test.txt [output]") == ("test.txt", folder_paths.get_output_directory())
51
- assert folder_paths.annotated_filepath("test.txt [input]") == ("test.txt", folder_paths.get_input_directory())
52
- assert folder_paths.annotated_filepath("test.txt [temp]") == ("test.txt", folder_paths.get_temp_directory())
53
-
54
- def test_get_annotated_filepath():
55
- default_dir = "/default/dir"
56
- assert folder_paths.get_annotated_filepath("test.txt", default_dir) == os.path.join(default_dir, "test.txt")
57
- assert folder_paths.get_annotated_filepath("test.txt [output]") == os.path.join(folder_paths.get_output_directory(), "test.txt")
58
-
59
- def test_add_model_folder_path_append(clear_folder_paths):
60
- folder_paths.add_model_folder_path("test_folder", "/default/path", is_default=True)
61
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=False)
62
- assert folder_paths.get_folder_paths("test_folder") == ["/default/path", "/test/path"]
63
-
64
-
65
- def test_add_model_folder_path_insert(clear_folder_paths):
66
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=False)
67
- folder_paths.add_model_folder_path("test_folder", "/default/path", is_default=True)
68
- assert folder_paths.get_folder_paths("test_folder") == ["/default/path", "/test/path"]
69
-
70
-
71
- def test_add_model_folder_path_re_add_existing_default(clear_folder_paths):
72
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=False)
73
- folder_paths.add_model_folder_path("test_folder", "/old_default/path", is_default=True)
74
- assert folder_paths.get_folder_paths("test_folder") == ["/old_default/path", "/test/path"]
75
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=True)
76
- assert folder_paths.get_folder_paths("test_folder") == ["/test/path", "/old_default/path"]
77
-
78
-
79
- def test_add_model_folder_path_re_add_existing_non_default(clear_folder_paths):
80
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=False)
81
- folder_paths.add_model_folder_path("test_folder", "/default/path", is_default=True)
82
- assert folder_paths.get_folder_paths("test_folder") == ["/default/path", "/test/path"]
83
- folder_paths.add_model_folder_path("test_folder", "/test/path", is_default=False)
84
- assert folder_paths.get_folder_paths("test_folder") == ["/default/path", "/test/path"]
85
-
86
-
87
- def test_recursive_search(temp_dir):
88
- os.makedirs(os.path.join(temp_dir, "subdir"))
89
- open(os.path.join(temp_dir, "file1.txt"), "w").close()
90
- open(os.path.join(temp_dir, "subdir", "file2.txt"), "w").close()
91
-
92
- files, dirs = folder_paths.recursive_search(temp_dir)
93
- assert set(files) == {"file1.txt", os.path.join("subdir", "file2.txt")}
94
- assert len(dirs) == 2 # temp_dir and subdir
95
-
96
- def test_filter_files_extensions():
97
- files = ["file1.txt", "file2.jpg", "file3.png", "file4.txt"]
98
- assert folder_paths.filter_files_extensions(files, [".txt"]) == ["file1.txt", "file4.txt"]
99
- assert folder_paths.filter_files_extensions(files, [".jpg", ".png"]) == ["file2.jpg", "file3.png"]
100
- assert folder_paths.filter_files_extensions(files, []) == files
101
-
102
- @patch("folder_paths.recursive_search")
103
- @patch("folder_paths.folder_names_and_paths")
104
- def test_get_filename_list(mock_folder_names_and_paths, mock_recursive_search):
105
- mock_folder_names_and_paths.__getitem__.return_value = (["/test/path"], {".txt"})
106
- mock_recursive_search.return_value = (["file1.txt", "file2.jpg"], {})
107
- assert folder_paths.get_filename_list("test_folder") == ["file1.txt"]
108
-
109
- def test_get_save_image_path(temp_dir):
110
- with patch("folder_paths.output_directory", temp_dir):
111
- full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path("test", temp_dir, 100, 100)
112
- assert os.path.samefile(full_output_folder, temp_dir)
113
- assert filename == "test"
114
- assert counter == 1
115
- assert subfolder == ""
116
- assert filename_prefix == "test"
117
-
118
-
119
- def test_base_path_changes(set_base_dir):
120
- test_dir = os.path.abspath("/test/dir")
121
- set_base_dir(test_dir)
122
-
123
- assert folder_paths.base_path == test_dir
124
- assert folder_paths.models_dir == os.path.join(test_dir, "models")
125
- assert folder_paths.input_directory == os.path.join(test_dir, "input")
126
- assert folder_paths.output_directory == os.path.join(test_dir, "output")
127
- assert folder_paths.temp_directory == os.path.join(test_dir, "temp")
128
- assert folder_paths.user_directory == os.path.join(test_dir, "user")
129
-
130
- assert os.path.join(test_dir, "custom_nodes") in folder_paths.get_folder_paths("custom_nodes")
131
-
132
- for name in ["checkpoints", "loras", "vae", "configs", "embeddings", "controlnet", "classifiers"]:
133
- assert folder_paths.get_folder_paths(name)[0] == os.path.join(test_dir, "models", name)
134
-
135
-
136
- def test_base_path_change_clears_old(set_base_dir):
137
- test_dir = os.path.abspath("/test/dir")
138
- set_base_dir(test_dir)
139
-
140
- assert len(folder_paths.get_folder_paths("custom_nodes")) == 1
141
-
142
- single_model_paths = [
143
- "checkpoints",
144
- "loras",
145
- "vae",
146
- "configs",
147
- "clip_vision",
148
- "style_models",
149
- "diffusers",
150
- "vae_approx",
151
- "gligen",
152
- "upscale_models",
153
- "embeddings",
154
- "hypernetworks",
155
- "photomaker",
156
- "classifiers",
157
- ]
158
- for name in single_model_paths:
159
- assert len(folder_paths.get_folder_paths(name)) == 1
160
-
161
- for name in ["controlnet", "diffusion_models", "text_encoders"]:
162
- assert len(folder_paths.get_folder_paths(name)) == 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/execution_test/validate_node_input_test.py DELETED
@@ -1,119 +0,0 @@
1
- import pytest
2
- from comfy_execution.validation import validate_node_input
3
-
4
-
5
- def test_exact_match():
6
- """Test cases where types match exactly"""
7
- assert validate_node_input("STRING", "STRING")
8
- assert validate_node_input("STRING,INT", "STRING,INT")
9
- assert validate_node_input("INT,STRING", "STRING,INT") # Order shouldn't matter
10
-
11
-
12
- def test_strict_mode():
13
- """Test strict mode validation"""
14
- # Should pass - received type is subset of input type
15
- assert validate_node_input("STRING", "STRING,INT", strict=True)
16
- assert validate_node_input("INT", "STRING,INT", strict=True)
17
- assert validate_node_input("STRING,INT", "STRING,INT,BOOLEAN", strict=True)
18
-
19
- # Should fail - received type is not subset of input type
20
- assert not validate_node_input("STRING,INT", "STRING", strict=True)
21
- assert not validate_node_input("STRING,BOOLEAN", "STRING", strict=True)
22
- assert not validate_node_input("INT,BOOLEAN", "STRING,INT", strict=True)
23
-
24
-
25
- def test_non_strict_mode():
26
- """Test non-strict mode validation (default behavior)"""
27
- # Should pass - types have overlap
28
- assert validate_node_input("STRING,BOOLEAN", "STRING,INT")
29
- assert validate_node_input("STRING,INT", "INT,BOOLEAN")
30
- assert validate_node_input("STRING", "STRING,INT")
31
-
32
- # Should fail - no overlap in types
33
- assert not validate_node_input("BOOLEAN", "STRING,INT")
34
- assert not validate_node_input("FLOAT", "STRING,INT")
35
- assert not validate_node_input("FLOAT,BOOLEAN", "STRING,INT")
36
-
37
-
38
- def test_whitespace_handling():
39
- """Test that whitespace is handled correctly"""
40
- assert validate_node_input("STRING, INT", "STRING,INT")
41
- assert validate_node_input("STRING,INT", "STRING, INT")
42
- assert validate_node_input(" STRING , INT ", "STRING,INT")
43
- assert validate_node_input("STRING,INT", " STRING , INT ")
44
-
45
-
46
- def test_empty_strings():
47
- """Test behavior with empty strings"""
48
- assert validate_node_input("", "")
49
- assert not validate_node_input("STRING", "")
50
- assert not validate_node_input("", "STRING")
51
-
52
-
53
- def test_single_vs_multiple():
54
- """Test single type against multiple types"""
55
- assert validate_node_input("STRING", "STRING,INT,BOOLEAN")
56
- assert validate_node_input("STRING,INT,BOOLEAN", "STRING", strict=False)
57
- assert not validate_node_input("STRING,INT,BOOLEAN", "STRING", strict=True)
58
-
59
-
60
- def test_non_string():
61
- """Test non-string types"""
62
- obj1 = object()
63
- obj2 = object()
64
- assert validate_node_input(obj1, obj1)
65
- assert not validate_node_input(obj1, obj2)
66
-
67
-
68
- class NotEqualsOverrideTest(str):
69
- """Test class for ``__ne__`` override."""
70
-
71
- def __ne__(self, value: object) -> bool:
72
- if self == "*" or value == "*":
73
- return False
74
- if self == "LONGER_THAN_2":
75
- return not len(value) > 2
76
- raise TypeError("This is a class for unit tests only.")
77
-
78
-
79
- def test_ne_override():
80
- """Test ``__ne__`` any override"""
81
- any = NotEqualsOverrideTest("*")
82
- invalid_type = "INVALID_TYPE"
83
- obj = object()
84
- assert validate_node_input(any, any)
85
- assert validate_node_input(any, invalid_type)
86
- assert validate_node_input(any, obj)
87
- assert validate_node_input(any, {})
88
- assert validate_node_input(any, [])
89
- assert validate_node_input(any, [1, 2, 3])
90
-
91
-
92
- def test_ne_custom_override():
93
- """Test ``__ne__`` custom override"""
94
- special = NotEqualsOverrideTest("LONGER_THAN_2")
95
-
96
- assert validate_node_input(special, special)
97
- assert validate_node_input(special, "*")
98
- assert validate_node_input(special, "INVALID_TYPE")
99
- assert validate_node_input(special, [1, 2, 3])
100
-
101
- # Should fail
102
- assert not validate_node_input(special, [1, 2])
103
- assert not validate_node_input(special, "TY")
104
-
105
-
106
- @pytest.mark.parametrize(
107
- "received,input_type,strict,expected",
108
- [
109
- ("STRING", "STRING", False, True),
110
- ("STRING,INT", "STRING,INT", False, True),
111
- ("STRING", "STRING,INT", True, True),
112
- ("STRING,INT", "STRING", True, False),
113
- ("BOOLEAN", "STRING,INT", False, False),
114
- ("STRING,BOOLEAN", "STRING,INT", False, True),
115
- ],
116
- )
117
- def test_parametrized_cases(received, input_type, strict, expected):
118
- """Parametrized test cases for various scenarios"""
119
- assert validate_node_input(received, input_type, strict) == expected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/feature_flags_test.py DELETED
@@ -1,98 +0,0 @@
1
- """Tests for feature flags functionality."""
2
-
3
- from comfy_api.feature_flags import (
4
- get_connection_feature,
5
- supports_feature,
6
- get_server_features,
7
- SERVER_FEATURE_FLAGS,
8
- )
9
-
10
-
11
- class TestFeatureFlags:
12
- """Test suite for feature flags functions."""
13
-
14
- def test_get_server_features_returns_copy(self):
15
- """Test that get_server_features returns a copy of the server flags."""
16
- features = get_server_features()
17
- # Verify it's a copy by modifying it
18
- features["test_flag"] = True
19
- # Original should be unchanged
20
- assert "test_flag" not in SERVER_FEATURE_FLAGS
21
-
22
- def test_get_server_features_contains_expected_flags(self):
23
- """Test that server features contain expected flags."""
24
- features = get_server_features()
25
- assert "supports_preview_metadata" in features
26
- assert features["supports_preview_metadata"] is True
27
- assert "max_upload_size" in features
28
- assert isinstance(features["max_upload_size"], (int, float))
29
-
30
- def test_get_connection_feature_with_missing_sid(self):
31
- """Test getting feature for non-existent session ID."""
32
- sockets_metadata = {}
33
- result = get_connection_feature(sockets_metadata, "missing_sid", "some_feature")
34
- assert result is False # Default value
35
-
36
- def test_get_connection_feature_with_custom_default(self):
37
- """Test getting feature with custom default value."""
38
- sockets_metadata = {}
39
- result = get_connection_feature(
40
- sockets_metadata, "missing_sid", "some_feature", default="custom_default"
41
- )
42
- assert result == "custom_default"
43
-
44
- def test_get_connection_feature_with_feature_flags(self):
45
- """Test getting feature from connection with feature flags."""
46
- sockets_metadata = {
47
- "sid1": {
48
- "feature_flags": {
49
- "supports_preview_metadata": True,
50
- "custom_feature": "value",
51
- },
52
- }
53
- }
54
- result = get_connection_feature(sockets_metadata, "sid1", "supports_preview_metadata")
55
- assert result is True
56
-
57
- result = get_connection_feature(sockets_metadata, "sid1", "custom_feature")
58
- assert result == "value"
59
-
60
- def test_get_connection_feature_missing_feature(self):
61
- """Test getting non-existent feature from connection."""
62
- sockets_metadata = {
63
- "sid1": {"feature_flags": {"existing_feature": True}}
64
- }
65
- result = get_connection_feature(sockets_metadata, "sid1", "missing_feature")
66
- assert result is False
67
-
68
- def test_supports_feature_returns_boolean(self):
69
- """Test that supports_feature always returns boolean."""
70
- sockets_metadata = {
71
- "sid1": {
72
- "feature_flags": {
73
- "bool_feature": True,
74
- "string_feature": "value",
75
- "none_feature": None,
76
- },
77
- }
78
- }
79
-
80
- # True boolean feature
81
- assert supports_feature(sockets_metadata, "sid1", "bool_feature") is True
82
-
83
- # Non-boolean values should return False
84
- assert supports_feature(sockets_metadata, "sid1", "string_feature") is False
85
- assert supports_feature(sockets_metadata, "sid1", "none_feature") is False
86
- assert supports_feature(sockets_metadata, "sid1", "missing_feature") is False
87
-
88
- def test_supports_feature_with_missing_connection(self):
89
- """Test supports_feature with missing connection."""
90
- sockets_metadata = {}
91
- assert supports_feature(sockets_metadata, "missing_sid", "any_feature") is False
92
-
93
- def test_empty_feature_flags_dict(self):
94
- """Test connection with empty feature flags dictionary."""
95
- sockets_metadata = {"sid1": {"feature_flags": {}}}
96
- result = get_connection_feature(sockets_metadata, "sid1", "any_feature")
97
- assert result is False
98
- assert supports_feature(sockets_metadata, "sid1", "any_feature") is False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/folder_paths_test/__init__.py DELETED
File without changes
tests-unit/folder_paths_test/filter_by_content_types_test.py DELETED
@@ -1,66 +0,0 @@
1
- import pytest
2
- import os
3
- import tempfile
4
- from folder_paths import filter_files_content_types, extension_mimetypes_cache
5
- from unittest.mock import patch
6
-
7
-
8
- @pytest.fixture(scope="module")
9
- def file_extensions():
10
- return {
11
- 'image': ['gif', 'heif', 'ico', 'jpeg', 'jpg', 'png', 'pnm', 'ppm', 'svg', 'tiff', 'webp', 'xbm', 'xpm'],
12
- 'audio': ['aif', 'aifc', 'aiff', 'au', 'flac', 'm4a', 'mp2', 'mp3', 'ogg', 'snd', 'wav'],
13
- 'video': ['avi', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'ogv', 'qt', 'webm', 'wmv'],
14
- 'model': ['gltf', 'glb', 'obj', 'fbx', 'stl']
15
- }
16
-
17
-
18
- @pytest.fixture(scope="module")
19
- def mock_dir(file_extensions):
20
- with tempfile.TemporaryDirectory() as directory:
21
- for content_type, extensions in file_extensions.items():
22
- for extension in extensions:
23
- with open(f"{directory}/sample_{content_type}.{extension}", "w") as f:
24
- f.write(f"Sample {content_type} file in {extension} format")
25
- yield directory
26
-
27
-
28
- @pytest.fixture
29
- def patched_mimetype_cache(file_extensions):
30
- # Mock model file extensions since they may not be in the test-runner system's mimetype cache
31
- new_cache = extension_mimetypes_cache.copy()
32
- for extension in file_extensions["model"]:
33
- new_cache[extension] = "model"
34
-
35
- with patch("folder_paths.extension_mimetypes_cache", new_cache):
36
- yield
37
-
38
-
39
- def test_categorizes_all_correctly(mock_dir, file_extensions, patched_mimetype_cache):
40
- files = os.listdir(mock_dir)
41
- for content_type, extensions in file_extensions.items():
42
- filtered_files = filter_files_content_types(files, [content_type])
43
- for extension in extensions:
44
- assert f"sample_{content_type}.{extension}" in filtered_files
45
-
46
-
47
- def test_categorizes_all_uniquely(mock_dir, file_extensions, patched_mimetype_cache):
48
- files = os.listdir(mock_dir)
49
- for content_type, extensions in file_extensions.items():
50
- filtered_files = filter_files_content_types(files, [content_type])
51
- assert len(filtered_files) == len(extensions)
52
-
53
-
54
- def test_handles_bad_extensions():
55
- files = ["file1.txt", "file2.py", "file3.example", "file4.pdf", "file5.ini", "file6.doc", "file7.md"]
56
- assert filter_files_content_types(files, ["image", "audio", "video"]) == []
57
-
58
-
59
- def test_handles_no_extension():
60
- files = ["file1", "file2", "file3", "file4", "file5", "file6", "file7"]
61
- assert filter_files_content_types(files, ["image", "audio", "video"]) == []
62
-
63
-
64
- def test_handles_no_files():
65
- files = []
66
- assert filter_files_content_types(files, ["image", "audio", "video"]) == []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/folder_paths_test/misc_test.py DELETED
@@ -1,51 +0,0 @@
1
- import pytest
2
- import os
3
- import tempfile
4
- from folder_paths import get_input_subfolders, set_input_directory
5
-
6
- @pytest.fixture(scope="module")
7
- def mock_folder_structure():
8
- with tempfile.TemporaryDirectory() as temp_dir:
9
- # Create a nested folder structure
10
- folders = [
11
- "folder1",
12
- "folder1/subfolder1",
13
- "folder1/subfolder2",
14
- "folder2",
15
- "folder2/deep",
16
- "folder2/deep/nested",
17
- "empty_folder"
18
- ]
19
-
20
- # Create the folders
21
- for folder in folders:
22
- os.makedirs(os.path.join(temp_dir, folder))
23
-
24
- # Add some files to test they're not included
25
- with open(os.path.join(temp_dir, "root_file.txt"), "w") as f:
26
- f.write("test")
27
- with open(os.path.join(temp_dir, "folder1", "test.txt"), "w") as f:
28
- f.write("test")
29
-
30
- set_input_directory(temp_dir)
31
- yield temp_dir
32
-
33
-
34
- def test_gets_all_folders(mock_folder_structure):
35
- folders = get_input_subfolders()
36
- expected = ["folder1", "folder1/subfolder1", "folder1/subfolder2",
37
- "folder2", "folder2/deep", "folder2/deep/nested", "empty_folder"]
38
- assert sorted(folders) == sorted(expected)
39
-
40
-
41
- def test_handles_nonexistent_input_directory():
42
- with tempfile.TemporaryDirectory() as temp_dir:
43
- nonexistent = os.path.join(temp_dir, "nonexistent")
44
- set_input_directory(nonexistent)
45
- assert get_input_subfolders() == []
46
-
47
-
48
- def test_empty_input_directory():
49
- with tempfile.TemporaryDirectory() as temp_dir:
50
- set_input_directory(temp_dir)
51
- assert get_input_subfolders() == [] # Empty since we don't include root
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/prompt_server_test/__init__.py DELETED
File without changes
tests-unit/prompt_server_test/user_manager_test.py DELETED
@@ -1,289 +0,0 @@
1
- import pytest
2
- import os
3
- from aiohttp import web
4
- from app.user_manager import UserManager
5
- from unittest.mock import patch
6
-
7
- pytestmark = (
8
- pytest.mark.asyncio
9
- ) # This applies the asyncio mark to all test functions in the module
10
-
11
-
12
- @pytest.fixture
13
- def user_manager(tmp_path):
14
- um = UserManager()
15
- um.get_request_user_filepath = lambda req, file, **kwargs: os.path.join(
16
- tmp_path, file
17
- ) if file else tmp_path
18
- return um
19
-
20
-
21
- @pytest.fixture
22
- def app(user_manager):
23
- app = web.Application()
24
- routes = web.RouteTableDef()
25
- user_manager.add_routes(routes)
26
- app.add_routes(routes)
27
- return app
28
-
29
-
30
- async def test_listuserdata_empty_directory(aiohttp_client, app, tmp_path):
31
- client = await aiohttp_client(app)
32
- resp = await client.get("/userdata?dir=test_dir")
33
- assert resp.status == 404
34
-
35
-
36
- async def test_listuserdata_with_files(aiohttp_client, app, tmp_path):
37
- os.makedirs(tmp_path / "test_dir")
38
- with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
39
- f.write("test content")
40
-
41
- client = await aiohttp_client(app)
42
- resp = await client.get("/userdata?dir=test_dir")
43
- assert resp.status == 200
44
- assert await resp.json() == ["file1.txt"]
45
-
46
-
47
- async def test_listuserdata_recursive(aiohttp_client, app, tmp_path):
48
- os.makedirs(tmp_path / "test_dir" / "subdir")
49
- with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
50
- f.write("test content")
51
- with open(tmp_path / "test_dir" / "subdir" / "file2.txt", "w") as f:
52
- f.write("test content")
53
-
54
- client = await aiohttp_client(app)
55
- resp = await client.get("/userdata?dir=test_dir&recurse=true")
56
- assert resp.status == 200
57
- assert set(await resp.json()) == {"file1.txt", "subdir/file2.txt"}
58
-
59
-
60
- async def test_listuserdata_full_info(aiohttp_client, app, tmp_path):
61
- os.makedirs(tmp_path / "test_dir")
62
- with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
63
- f.write("test content")
64
-
65
- client = await aiohttp_client(app)
66
- resp = await client.get("/userdata?dir=test_dir&full_info=true")
67
- assert resp.status == 200
68
- result = await resp.json()
69
- assert len(result) == 1
70
- assert result[0]["path"] == "file1.txt"
71
- assert "size" in result[0]
72
- assert "modified" in result[0]
73
-
74
-
75
- async def test_listuserdata_split_path(aiohttp_client, app, tmp_path):
76
- os.makedirs(tmp_path / "test_dir" / "subdir")
77
- with open(tmp_path / "test_dir" / "subdir" / "file1.txt", "w") as f:
78
- f.write("test content")
79
-
80
- client = await aiohttp_client(app)
81
- resp = await client.get("/userdata?dir=test_dir&recurse=true&split=true")
82
- assert resp.status == 200
83
- assert await resp.json() == [["subdir/file1.txt", "subdir", "file1.txt"]]
84
-
85
-
86
- async def test_listuserdata_invalid_directory(aiohttp_client, app):
87
- client = await aiohttp_client(app)
88
- resp = await client.get("/userdata?dir=")
89
- assert resp.status == 400
90
-
91
-
92
- async def test_listuserdata_normalized_separator(aiohttp_client, app, tmp_path):
93
- os_sep = "\\"
94
- with patch("os.sep", os_sep):
95
- with patch("os.path.sep", os_sep):
96
- os.makedirs(tmp_path / "test_dir" / "subdir")
97
- with open(tmp_path / "test_dir" / "subdir" / "file1.txt", "w") as f:
98
- f.write("test content")
99
-
100
- client = await aiohttp_client(app)
101
- resp = await client.get("/userdata?dir=test_dir&recurse=true")
102
- assert resp.status == 200
103
- result = await resp.json()
104
- assert len(result) == 1
105
- assert "/" in result[0] # Ensure forward slash is used
106
- assert "\\" not in result[0] # Ensure backslash is not present
107
- assert result[0] == "subdir/file1.txt"
108
-
109
- # Test with full_info
110
- resp = await client.get(
111
- "/userdata?dir=test_dir&recurse=true&full_info=true"
112
- )
113
- assert resp.status == 200
114
- result = await resp.json()
115
- assert len(result) == 1
116
- assert "/" in result[0]["path"] # Ensure forward slash is used
117
- assert "\\" not in result[0]["path"] # Ensure backslash is not present
118
- assert result[0]["path"] == "subdir/file1.txt"
119
-
120
-
121
- async def test_post_userdata_new_file(aiohttp_client, app, tmp_path):
122
- client = await aiohttp_client(app)
123
- content = b"test content"
124
- resp = await client.post("/userdata/test.txt", data=content)
125
-
126
- assert resp.status == 200
127
- assert await resp.text() == '"test.txt"'
128
-
129
- # Verify file was created with correct content
130
- with open(tmp_path / "test.txt", "rb") as f:
131
- assert f.read() == content
132
-
133
-
134
- async def test_post_userdata_overwrite_existing(aiohttp_client, app, tmp_path):
135
- # Create initial file
136
- with open(tmp_path / "test.txt", "w") as f:
137
- f.write("initial content")
138
-
139
- client = await aiohttp_client(app)
140
- new_content = b"updated content"
141
- resp = await client.post("/userdata/test.txt", data=new_content)
142
-
143
- assert resp.status == 200
144
- assert await resp.text() == '"test.txt"'
145
-
146
- # Verify file was overwritten
147
- with open(tmp_path / "test.txt", "rb") as f:
148
- assert f.read() == new_content
149
-
150
-
151
- async def test_post_userdata_no_overwrite(aiohttp_client, app, tmp_path):
152
- # Create initial file
153
- with open(tmp_path / "test.txt", "w") as f:
154
- f.write("initial content")
155
-
156
- client = await aiohttp_client(app)
157
- resp = await client.post("/userdata/test.txt?overwrite=false", data=b"new content")
158
-
159
- assert resp.status == 409
160
-
161
- # Verify original content unchanged
162
- with open(tmp_path / "test.txt", "r") as f:
163
- assert f.read() == "initial content"
164
-
165
-
166
- async def test_post_userdata_full_info(aiohttp_client, app, tmp_path):
167
- client = await aiohttp_client(app)
168
- content = b"test content"
169
- resp = await client.post("/userdata/test.txt?full_info=true", data=content)
170
-
171
- assert resp.status == 200
172
- result = await resp.json()
173
- assert result["path"] == "test.txt"
174
- assert result["size"] == len(content)
175
- assert "modified" in result
176
-
177
-
178
- async def test_move_userdata(aiohttp_client, app, tmp_path):
179
- # Create initial file
180
- with open(tmp_path / "source.txt", "w") as f:
181
- f.write("test content")
182
-
183
- client = await aiohttp_client(app)
184
- resp = await client.post("/userdata/source.txt/move/dest.txt")
185
-
186
- assert resp.status == 200
187
- assert await resp.text() == '"dest.txt"'
188
-
189
- # Verify file was moved
190
- assert not os.path.exists(tmp_path / "source.txt")
191
- with open(tmp_path / "dest.txt", "r") as f:
192
- assert f.read() == "test content"
193
-
194
-
195
- async def test_move_userdata_no_overwrite(aiohttp_client, app, tmp_path):
196
- # Create source and destination files
197
- with open(tmp_path / "source.txt", "w") as f:
198
- f.write("source content")
199
- with open(tmp_path / "dest.txt", "w") as f:
200
- f.write("destination content")
201
-
202
- client = await aiohttp_client(app)
203
- resp = await client.post("/userdata/source.txt/move/dest.txt?overwrite=false")
204
-
205
- assert resp.status == 409
206
-
207
- # Verify files remain unchanged
208
- with open(tmp_path / "source.txt", "r") as f:
209
- assert f.read() == "source content"
210
- with open(tmp_path / "dest.txt", "r") as f:
211
- assert f.read() == "destination content"
212
-
213
-
214
- async def test_move_userdata_full_info(aiohttp_client, app, tmp_path):
215
- # Create initial file
216
- with open(tmp_path / "source.txt", "w") as f:
217
- f.write("test content")
218
-
219
- client = await aiohttp_client(app)
220
- resp = await client.post("/userdata/source.txt/move/dest.txt?full_info=true")
221
-
222
- assert resp.status == 200
223
- result = await resp.json()
224
- assert result["path"] == "dest.txt"
225
- assert result["size"] == len("test content")
226
- assert "modified" in result
227
-
228
- # Verify file was moved
229
- assert not os.path.exists(tmp_path / "source.txt")
230
- with open(tmp_path / "dest.txt", "r") as f:
231
- assert f.read() == "test content"
232
-
233
-
234
- async def test_listuserdata_v2_empty_root(aiohttp_client, app):
235
- client = await aiohttp_client(app)
236
- resp = await client.get("/v2/userdata")
237
- assert resp.status == 200
238
- assert await resp.json() == []
239
-
240
-
241
- async def test_listuserdata_v2_nonexistent_subdirectory(aiohttp_client, app):
242
- client = await aiohttp_client(app)
243
- resp = await client.get("/v2/userdata?path=does_not_exist")
244
- assert resp.status == 404
245
-
246
-
247
- async def test_listuserdata_v2_default(aiohttp_client, app, tmp_path):
248
- os.makedirs(tmp_path / "test_dir" / "subdir")
249
- (tmp_path / "test_dir" / "file1.txt").write_text("content")
250
- (tmp_path / "test_dir" / "subdir" / "file2.txt").write_text("content")
251
-
252
- client = await aiohttp_client(app)
253
- resp = await client.get("/v2/userdata?path=test_dir")
254
- assert resp.status == 200
255
- data = await resp.json()
256
- file_paths = {item["path"] for item in data if item["type"] == "file"}
257
- assert file_paths == {"test_dir/file1.txt", "test_dir/subdir/file2.txt"}
258
-
259
-
260
- async def test_listuserdata_v2_normalized_separators(aiohttp_client, app, tmp_path, monkeypatch):
261
- # Force backslash as os separator
262
- monkeypatch.setattr(os, 'sep', '\\')
263
- monkeypatch.setattr(os.path, 'sep', '\\')
264
- os.makedirs(tmp_path / "test_dir" / "subdir")
265
- (tmp_path / "test_dir" / "subdir" / "file1.txt").write_text("x")
266
-
267
- client = await aiohttp_client(app)
268
- resp = await client.get("/v2/userdata?path=test_dir")
269
- assert resp.status == 200
270
- data = await resp.json()
271
- for item in data:
272
- assert "/" in item["path"]
273
- assert "\\" not in item["path"]\
274
-
275
- async def test_listuserdata_v2_url_encoded_path(aiohttp_client, app, tmp_path):
276
- # Create a directory with a space in its name and a file inside
277
- os.makedirs(tmp_path / "my dir")
278
- (tmp_path / "my dir" / "file.txt").write_text("content")
279
-
280
- client = await aiohttp_client(app)
281
- # Use URL-encoded space in path parameter
282
- resp = await client.get("/v2/userdata?path=my%20dir&recurse=false")
283
- assert resp.status == 200
284
- data = await resp.json()
285
- assert len(data) == 1
286
- entry = data[0]
287
- assert entry["name"] == "file.txt"
288
- # Ensure the path is correctly decoded and uses forward slash
289
- assert entry["path"] == "my dir/file.txt"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/requirements.txt DELETED
@@ -1,4 +0,0 @@
1
- pytest>=7.8.0
2
- pytest-aiohttp
3
- pytest-asyncio
4
- websocket-client
 
 
 
 
 
tests-unit/server/utils/file_operations_test.py DELETED
@@ -1,42 +0,0 @@
1
- import pytest
2
- from typing import List
3
- from api_server.utils.file_operations import FileSystemOperations, FileSystemItem, is_file_info
4
-
5
- @pytest.fixture
6
- def temp_directory(tmp_path):
7
- # Create a temporary directory structure
8
- dir1 = tmp_path / "dir1"
9
- dir2 = tmp_path / "dir2"
10
- dir1.mkdir()
11
- dir2.mkdir()
12
- (dir1 / "file1.txt").write_text("content1")
13
- (dir2 / "file2.txt").write_text("content2")
14
- (tmp_path / "file3.txt").write_text("content3")
15
- return tmp_path
16
-
17
- def test_walk_directory(temp_directory):
18
- result: List[FileSystemItem] = FileSystemOperations.walk_directory(str(temp_directory))
19
-
20
- assert len(result) == 5 # 2 directories and 3 files
21
-
22
- files = [item for item in result if item['type'] == 'file']
23
- dirs = [item for item in result if item['type'] == 'directory']
24
-
25
- assert len(files) == 3
26
- assert len(dirs) == 2
27
-
28
- file_names = {file['name'] for file in files}
29
- assert file_names == {'file1.txt', 'file2.txt', 'file3.txt'}
30
-
31
- dir_names = {dir['name'] for dir in dirs}
32
- assert dir_names == {'dir1', 'dir2'}
33
-
34
- def test_walk_directory_empty(tmp_path):
35
- result = FileSystemOperations.walk_directory(str(tmp_path))
36
- assert len(result) == 0
37
-
38
- def test_walk_directory_file_size(temp_directory):
39
- result: List[FileSystemItem] = FileSystemOperations.walk_directory(str(temp_directory))
40
- files = [item for item in result if is_file_info(item)]
41
- for file in files:
42
- assert file['size'] > 0 # Assuming all files have some content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/utils/extra_config_test.py DELETED
@@ -1,303 +0,0 @@
1
- import pytest
2
- import yaml
3
- import os
4
- import sys
5
- from unittest.mock import Mock, patch, mock_open
6
-
7
- from utils.extra_config import load_extra_path_config
8
- import folder_paths
9
-
10
-
11
- @pytest.fixture()
12
- def clear_folder_paths():
13
- # Clear the global dictionary before each test to ensure isolation
14
- original = folder_paths.folder_names_and_paths.copy()
15
- folder_paths.folder_names_and_paths.clear()
16
- yield
17
- folder_paths.folder_names_and_paths = original
18
-
19
-
20
- @pytest.fixture
21
- def mock_yaml_content():
22
- return {
23
- 'test_config': {
24
- 'base_path': '~/App/',
25
- 'checkpoints': 'subfolder1',
26
- }
27
- }
28
-
29
-
30
- @pytest.fixture
31
- def mock_expanded_home():
32
- return '/home/user'
33
-
34
-
35
- @pytest.fixture
36
- def yaml_config_with_appdata():
37
- return """
38
- test_config:
39
- base_path: '%APPDATA%/ComfyUI'
40
- checkpoints: 'models/checkpoints'
41
- """
42
-
43
-
44
- @pytest.fixture
45
- def mock_yaml_content_appdata(yaml_config_with_appdata):
46
- return yaml.safe_load(yaml_config_with_appdata)
47
-
48
-
49
- @pytest.fixture
50
- def mock_expandvars_appdata():
51
- mock = Mock()
52
-
53
- def expandvars(path):
54
- if '%APPDATA%' in path:
55
- if sys.platform == 'win32':
56
- return path.replace('%APPDATA%', 'C:/Users/TestUser/AppData/Roaming')
57
- else:
58
- return path.replace('%APPDATA%', '/Users/TestUser/AppData/Roaming')
59
- return path
60
-
61
- mock.side_effect = expandvars
62
- return mock
63
-
64
-
65
- @pytest.fixture
66
- def mock_add_model_folder_path():
67
- return Mock()
68
-
69
-
70
- @pytest.fixture
71
- def mock_expanduser(mock_expanded_home):
72
- def _expanduser(path):
73
- if path.startswith('~/'):
74
- return os.path.join(mock_expanded_home, path[2:])
75
- return path
76
- return _expanduser
77
-
78
-
79
- @pytest.fixture
80
- def mock_yaml_safe_load(mock_yaml_content):
81
- return Mock(return_value=mock_yaml_content)
82
-
83
-
84
- @patch('builtins.open', new_callable=mock_open, read_data="dummy file content")
85
- def test_load_extra_model_paths_expands_userpath(
86
- mock_file,
87
- monkeypatch,
88
- mock_add_model_folder_path,
89
- mock_expanduser,
90
- mock_yaml_safe_load,
91
- mock_expanded_home
92
- ):
93
- # Attach mocks used by load_extra_path_config
94
- monkeypatch.setattr(folder_paths, 'add_model_folder_path', mock_add_model_folder_path)
95
- monkeypatch.setattr(os.path, 'expanduser', mock_expanduser)
96
- monkeypatch.setattr(yaml, 'safe_load', mock_yaml_safe_load)
97
-
98
- dummy_yaml_file_name = 'dummy_path.yaml'
99
- load_extra_path_config(dummy_yaml_file_name)
100
-
101
- expected_calls = [
102
- ('checkpoints', os.path.join(mock_expanded_home, 'App', 'subfolder1'), False),
103
- ]
104
-
105
- assert mock_add_model_folder_path.call_count == len(expected_calls)
106
-
107
- # Check if add_model_folder_path was called with the correct arguments
108
- for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
109
- assert actual_call.args[0] == expected_call[0]
110
- assert os.path.normpath(actual_call.args[1]) == os.path.normpath(expected_call[1]) # Normalize and check the path to check on multiple OS.
111
- assert actual_call.args[2] == expected_call[2]
112
-
113
- # Check if yaml.safe_load was called
114
- mock_yaml_safe_load.assert_called_once()
115
-
116
- # Check if open was called with the correct file path
117
- mock_file.assert_called_once_with(dummy_yaml_file_name, 'r', encoding='utf-8')
118
-
119
-
120
- @patch('builtins.open', new_callable=mock_open)
121
- def test_load_extra_model_paths_expands_appdata(
122
- mock_file,
123
- monkeypatch,
124
- mock_add_model_folder_path,
125
- mock_expandvars_appdata,
126
- yaml_config_with_appdata,
127
- mock_yaml_content_appdata
128
- ):
129
- # Set the mock_file to return yaml with appdata as a variable
130
- mock_file.return_value.read.return_value = yaml_config_with_appdata
131
-
132
- # Attach mocks
133
- monkeypatch.setattr(folder_paths, 'add_model_folder_path', mock_add_model_folder_path)
134
- monkeypatch.setattr(os.path, 'expandvars', mock_expandvars_appdata)
135
- monkeypatch.setattr(yaml, 'safe_load', Mock(return_value=mock_yaml_content_appdata))
136
-
137
- # Mock expanduser to do nothing (since we're not testing it here)
138
- monkeypatch.setattr(os.path, 'expanduser', lambda x: x)
139
-
140
- dummy_yaml_file_name = 'dummy_path.yaml'
141
- load_extra_path_config(dummy_yaml_file_name)
142
-
143
- if sys.platform == "win32":
144
- expected_base_path = 'C:/Users/TestUser/AppData/Roaming/ComfyUI'
145
- else:
146
- expected_base_path = '/Users/TestUser/AppData/Roaming/ComfyUI'
147
- expected_calls = [
148
- ('checkpoints', os.path.normpath(os.path.join(expected_base_path, 'models/checkpoints')), False),
149
- ]
150
-
151
- assert mock_add_model_folder_path.call_count == len(expected_calls)
152
-
153
- # Check the base path variable was expanded
154
- for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
155
- assert actual_call.args == expected_call
156
-
157
- # Verify that expandvars was called
158
- assert mock_expandvars_appdata.called
159
-
160
-
161
- @patch("builtins.open", new_callable=mock_open, read_data="dummy yaml content")
162
- @patch("yaml.safe_load")
163
- def test_load_extra_path_config_relative_base_path(
164
- mock_yaml_load, _mock_file, clear_folder_paths, monkeypatch, tmp_path
165
- ):
166
- """
167
- Test that when 'base_path' is a relative path in the YAML, it is joined to the YAML file directory, and then
168
- the items in the config are correctly converted to absolute paths.
169
- """
170
- sub_folder = "./my_rel_base"
171
- config_data = {
172
- "some_model_folder": {
173
- "base_path": sub_folder,
174
- "is_default": True,
175
- "checkpoints": "checkpoints",
176
- "some_key": "some_value"
177
- }
178
- }
179
- mock_yaml_load.return_value = config_data
180
-
181
- dummy_yaml_name = "dummy_file.yaml"
182
-
183
- def fake_abspath(path):
184
- if path == dummy_yaml_name:
185
- # If it's the YAML path, treat it like it lives in tmp_path
186
- return os.path.join(str(tmp_path), dummy_yaml_name)
187
- return os.path.join(str(tmp_path), path) # Otherwise, do a normal join relative to tmp_path
188
-
189
- def fake_dirname(path):
190
- # We expect path to be the result of fake_abspath(dummy_yaml_name)
191
- if path.endswith(dummy_yaml_name):
192
- return str(tmp_path)
193
- return os.path.dirname(path)
194
-
195
- monkeypatch.setattr(os.path, "abspath", fake_abspath)
196
- monkeypatch.setattr(os.path, "dirname", fake_dirname)
197
-
198
- load_extra_path_config(dummy_yaml_name)
199
-
200
- expected_checkpoints = os.path.abspath(os.path.join(str(tmp_path), "my_rel_base", "checkpoints"))
201
- expected_some_value = os.path.abspath(os.path.join(str(tmp_path), "my_rel_base", "some_value"))
202
-
203
- actual_paths = folder_paths.folder_names_and_paths["checkpoints"][0]
204
- assert len(actual_paths) == 1, "Should have one path added for 'checkpoints'."
205
- assert actual_paths[0] == expected_checkpoints
206
-
207
- actual_paths = folder_paths.folder_names_and_paths["some_key"][0]
208
- assert len(actual_paths) == 1, "Should have one path added for 'some_key'."
209
- assert actual_paths[0] == expected_some_value
210
-
211
-
212
- @patch("builtins.open", new_callable=mock_open, read_data="dummy yaml content")
213
- @patch("yaml.safe_load")
214
- def test_load_extra_path_config_absolute_base_path(
215
- mock_yaml_load, _mock_file, clear_folder_paths, monkeypatch, tmp_path
216
- ):
217
- """
218
- Test that when 'base_path' is an absolute path, each subdirectory is joined with that absolute path,
219
- rather than being relative to the YAML's directory.
220
- """
221
- abs_base = os.path.join(str(tmp_path), "abs_base")
222
- config_data = {
223
- "some_absolute_folder": {
224
- "base_path": abs_base, # <-- absolute
225
- "is_default": True,
226
- "loras": "loras_folder",
227
- "embeddings": "embeddings_folder"
228
- }
229
- }
230
- mock_yaml_load.return_value = config_data
231
-
232
- dummy_yaml_name = "dummy_abs.yaml"
233
-
234
- def fake_abspath(path):
235
- if path == dummy_yaml_name:
236
- # If it's the YAML path, treat it like it is in tmp_path
237
- return os.path.join(str(tmp_path), dummy_yaml_name)
238
- return path # For absolute base, we just return path directly
239
-
240
- def fake_dirname(path):
241
- return str(tmp_path) if path.endswith(dummy_yaml_name) else os.path.dirname(path)
242
-
243
- monkeypatch.setattr(os.path, "abspath", fake_abspath)
244
- monkeypatch.setattr(os.path, "dirname", fake_dirname)
245
-
246
- load_extra_path_config(dummy_yaml_name)
247
-
248
- # Expect the final paths to be <abs_base>/loras_folder and <abs_base>/embeddings_folder
249
- expected_loras = os.path.join(abs_base, "loras_folder")
250
- expected_embeddings = os.path.join(abs_base, "embeddings_folder")
251
-
252
- actual_loras = folder_paths.folder_names_and_paths["loras"][0]
253
- assert len(actual_loras) == 1, "Should have one path for 'loras'."
254
- assert actual_loras[0] == os.path.abspath(expected_loras)
255
-
256
- actual_embeddings = folder_paths.folder_names_and_paths["embeddings"][0]
257
- assert len(actual_embeddings) == 1, "Should have one path for 'embeddings'."
258
- assert actual_embeddings[0] == os.path.abspath(expected_embeddings)
259
-
260
-
261
- @patch("builtins.open", new_callable=mock_open, read_data="dummy yaml content")
262
- @patch("yaml.safe_load")
263
- def test_load_extra_path_config_no_base_path(
264
- mock_yaml_load, _mock_file, clear_folder_paths, monkeypatch, tmp_path
265
- ):
266
- """
267
- Test that if 'base_path' is not present, each path is joined
268
- with the directory of the YAML file (unless it's already absolute).
269
- """
270
- config_data = {
271
- "some_folder_without_base": {
272
- "is_default": True,
273
- "text_encoders": "clip",
274
- "diffusion_models": "unet"
275
- }
276
- }
277
- mock_yaml_load.return_value = config_data
278
-
279
- dummy_yaml_name = "dummy_no_base.yaml"
280
-
281
- def fake_abspath(path):
282
- if path == dummy_yaml_name:
283
- return os.path.join(str(tmp_path), dummy_yaml_name)
284
- return os.path.join(str(tmp_path), path)
285
-
286
- def fake_dirname(path):
287
- return str(tmp_path) if path.endswith(dummy_yaml_name) else os.path.dirname(path)
288
-
289
- monkeypatch.setattr(os.path, "abspath", fake_abspath)
290
- monkeypatch.setattr(os.path, "dirname", fake_dirname)
291
-
292
- load_extra_path_config(dummy_yaml_name)
293
-
294
- expected_clip = os.path.join(str(tmp_path), "clip")
295
- expected_unet = os.path.join(str(tmp_path), "unet")
296
-
297
- actual_text_encoders = folder_paths.folder_names_and_paths["text_encoders"][0]
298
- assert len(actual_text_encoders) == 1, "Should have one path for 'text_encoders'."
299
- assert actual_text_encoders[0] == os.path.abspath(expected_clip)
300
-
301
- actual_diffusion = folder_paths.folder_names_and_paths["diffusion_models"][0]
302
- assert len(actual_diffusion) == 1, "Should have one path for 'diffusion_models'."
303
- assert actual_diffusion[0] == os.path.abspath(expected_unet)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/utils/json_util_test.py DELETED
@@ -1,71 +0,0 @@
1
- from utils.json_util import merge_json_recursive
2
-
3
-
4
- def test_merge_simple_dicts():
5
- base = {"a": 1, "b": 2}
6
- update = {"b": 3, "c": 4}
7
- expected = {"a": 1, "b": 3, "c": 4}
8
- assert merge_json_recursive(base, update) == expected
9
-
10
-
11
- def test_merge_nested_dicts():
12
- base = {"a": {"x": 1, "y": 2}, "b": 3}
13
- update = {"a": {"y": 4, "z": 5}}
14
- expected = {"a": {"x": 1, "y": 4, "z": 5}, "b": 3}
15
- assert merge_json_recursive(base, update) == expected
16
-
17
-
18
- def test_merge_lists():
19
- base = {"a": [1, 2], "b": 3}
20
- update = {"a": [3, 4]}
21
- expected = {"a": [1, 2, 3, 4], "b": 3}
22
- assert merge_json_recursive(base, update) == expected
23
-
24
-
25
- def test_merge_nested_lists():
26
- base = {"a": {"x": [1, 2]}}
27
- update = {"a": {"x": [3, 4]}}
28
- expected = {"a": {"x": [1, 2, 3, 4]}}
29
- assert merge_json_recursive(base, update) == expected
30
-
31
-
32
- def test_merge_mixed_types():
33
- base = {"a": [1, 2], "b": {"x": 1}}
34
- update = {"a": [3], "b": {"y": 2}}
35
- expected = {"a": [1, 2, 3], "b": {"x": 1, "y": 2}}
36
- assert merge_json_recursive(base, update) == expected
37
-
38
-
39
- def test_merge_overwrite_non_dict():
40
- base = {"a": 1}
41
- update = {"a": {"x": 2}}
42
- expected = {"a": {"x": 2}}
43
- assert merge_json_recursive(base, update) == expected
44
-
45
-
46
- def test_merge_empty_dicts():
47
- base = {}
48
- update = {"a": 1}
49
- expected = {"a": 1}
50
- assert merge_json_recursive(base, update) == expected
51
-
52
-
53
- def test_merge_none_values():
54
- base = {"a": None}
55
- update = {"a": {"x": 1}}
56
- expected = {"a": {"x": 1}}
57
- assert merge_json_recursive(base, update) == expected
58
-
59
-
60
- def test_merge_different_types():
61
- base = {"a": [1, 2]}
62
- update = {"a": "string"}
63
- expected = {"a": "string"}
64
- assert merge_json_recursive(base, update) == expected
65
-
66
-
67
- def test_merge_complex_nested():
68
- base = {"a": [1, 2], "b": {"x": [3, 4], "y": {"p": 1}}}
69
- update = {"a": [5], "b": {"x": [6], "y": {"q": 2}}}
70
- expected = {"a": [1, 2, 5], "b": {"x": [3, 4, 6], "y": {"p": 1, "q": 2}}}
71
- assert merge_json_recursive(base, update) == expected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests-unit/websocket_feature_flags_test.py DELETED
@@ -1,77 +0,0 @@
1
- """Simplified tests for WebSocket feature flags functionality."""
2
- from comfy_api import feature_flags
3
-
4
-
5
- class TestWebSocketFeatureFlags:
6
- """Test suite for WebSocket feature flags integration."""
7
-
8
- def test_server_feature_flags_response(self):
9
- """Test server feature flags are properly formatted."""
10
- features = feature_flags.get_server_features()
11
-
12
- # Check expected server features
13
- assert "supports_preview_metadata" in features
14
- assert features["supports_preview_metadata"] is True
15
- assert "max_upload_size" in features
16
- assert isinstance(features["max_upload_size"], (int, float))
17
-
18
- def test_progress_py_checks_feature_flags(self):
19
- """Test that progress.py checks feature flags before sending metadata."""
20
- # This simulates the check in progress.py
21
- client_id = "test_client"
22
- sockets_metadata = {"test_client": {"feature_flags": {}}}
23
-
24
- # The actual check would be in progress.py
25
- supports_metadata = feature_flags.supports_feature(
26
- sockets_metadata, client_id, "supports_preview_metadata"
27
- )
28
-
29
- assert supports_metadata is False
30
-
31
- def test_multiple_clients_different_features(self):
32
- """Test handling multiple clients with different feature support."""
33
- sockets_metadata = {
34
- "modern_client": {
35
- "feature_flags": {"supports_preview_metadata": True}
36
- },
37
- "legacy_client": {
38
- "feature_flags": {}
39
- }
40
- }
41
-
42
- # Check modern client
43
- assert feature_flags.supports_feature(
44
- sockets_metadata, "modern_client", "supports_preview_metadata"
45
- ) is True
46
-
47
- # Check legacy client
48
- assert feature_flags.supports_feature(
49
- sockets_metadata, "legacy_client", "supports_preview_metadata"
50
- ) is False
51
-
52
- def test_feature_negotiation_message_format(self):
53
- """Test the format of feature negotiation messages."""
54
- # Client message format
55
- client_message = {
56
- "type": "feature_flags",
57
- "data": {
58
- "supports_preview_metadata": True,
59
- "api_version": "1.0.0"
60
- }
61
- }
62
-
63
- # Verify structure
64
- assert client_message["type"] == "feature_flags"
65
- assert "supports_preview_metadata" in client_message["data"]
66
-
67
- # Server response format (what would be sent)
68
- server_features = feature_flags.get_server_features()
69
- server_message = {
70
- "type": "feature_flags",
71
- "data": server_features
72
- }
73
-
74
- # Verify structure
75
- assert server_message["type"] == "feature_flags"
76
- assert "supports_preview_metadata" in server_message["data"]
77
- assert server_message["data"]["supports_preview_metadata"] is True