Delete tests-unit
Browse files- tests-unit/README.md +0 -8
- tests-unit/app_test/__init__.py +0 -0
- tests-unit/app_test/custom_node_manager_test.py +0 -147
- tests-unit/app_test/frontend_manager_test.py +0 -174
- tests-unit/app_test/model_manager_test.py +0 -62
- tests-unit/comfy_api_nodes_test/mapper_utils_test.py +0 -297
- tests-unit/comfy_api_test/input_impl_test.py +0 -91
- tests-unit/comfy_api_test/video_types_test.py +0 -239
- tests-unit/comfy_extras_test/__init__.py +0 -0
- tests-unit/comfy_extras_test/image_stitch_test.py +0 -243
- tests-unit/comfy_test/folder_path_test.py +0 -162
- tests-unit/execution_test/validate_node_input_test.py +0 -119
- tests-unit/feature_flags_test.py +0 -98
- tests-unit/folder_paths_test/__init__.py +0 -0
- tests-unit/folder_paths_test/filter_by_content_types_test.py +0 -66
- tests-unit/folder_paths_test/misc_test.py +0 -51
- tests-unit/prompt_server_test/__init__.py +0 -0
- tests-unit/prompt_server_test/user_manager_test.py +0 -289
- tests-unit/requirements.txt +0 -4
- tests-unit/server/utils/file_operations_test.py +0 -42
- tests-unit/utils/extra_config_test.py +0 -303
- tests-unit/utils/json_util_test.py +0 -71
- tests-unit/websocket_feature_flags_test.py +0 -77
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|