comfy-test 0.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_test/__init__.py +98 -0
- comfy_test/cli.py +544 -0
- comfy_test/comfyui/__init__.py +11 -0
- comfy_test/comfyui/api.py +184 -0
- comfy_test/comfyui/server.py +211 -0
- comfy_test/comfyui/validator.py +616 -0
- comfy_test/comfyui/workflow.py +195 -0
- comfy_test/errors.py +88 -0
- comfy_test/github/__init__.py +1 -0
- comfy_test/runner.py +81 -0
- comfy_test/screenshot.py +294 -0
- comfy_test/screenshot_cache.py +332 -0
- comfy_test/test/__init__.py +13 -0
- comfy_test/test/comfy_env.py +75 -0
- comfy_test/test/config.py +217 -0
- comfy_test/test/config_file.py +301 -0
- comfy_test/test/manager.py +668 -0
- comfy_test/test/node_discovery.py +212 -0
- comfy_test/test/platform/__init__.py +74 -0
- comfy_test/test/platform/base.py +218 -0
- comfy_test/test/platform/linux.py +239 -0
- comfy_test/test/platform/windows.py +236 -0
- comfy_test/test/platform/windows_portable.py +359 -0
- comfy_test-0.0.23.dist-info/METADATA +231 -0
- comfy_test-0.0.23.dist-info/RECORD +28 -0
- comfy_test-0.0.23.dist-info/WHEEL +4 -0
- comfy_test-0.0.23.dist-info/entry_points.txt +2 -0
- comfy_test-0.0.23.dist-info/licenses/LICENSE +21 -0
comfy_test/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
comfy-test: Installation testing infrastructure for ComfyUI custom nodes.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- Multi-platform installation testing (Linux, Windows, Windows Portable)
|
|
6
|
+
- Workflow execution verification
|
|
7
|
+
- GitHub Actions integration
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
from comfy_test import run_tests, verify_nodes
|
|
12
|
+
|
|
13
|
+
# Run all tests from config
|
|
14
|
+
results = run_tests()
|
|
15
|
+
|
|
16
|
+
# Or verify nodes only
|
|
17
|
+
results = verify_nodes()
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
comfy-test run # Run installation tests
|
|
22
|
+
comfy-test verify # Verify node registration
|
|
23
|
+
comfy-test info # Show configuration
|
|
24
|
+
comfy-test init-ci # Generate GitHub Actions workflow
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Create comfy-test.toml in your custom node directory:
|
|
29
|
+
|
|
30
|
+
[test]
|
|
31
|
+
name = "MyNode"
|
|
32
|
+
|
|
33
|
+
[test.workflow]
|
|
34
|
+
file = "tests/workflows/smoke_test.json"
|
|
35
|
+
|
|
36
|
+
Nodes are auto-discovered from NODE_CLASS_MAPPINGS in your __init__.py.
|
|
37
|
+
|
|
38
|
+
## GitHub Actions
|
|
39
|
+
|
|
40
|
+
Add this workflow to your repository:
|
|
41
|
+
|
|
42
|
+
# .github/workflows/test-install.yml
|
|
43
|
+
name: Test Installation
|
|
44
|
+
on: [push, pull_request]
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
test:
|
|
48
|
+
uses: PozzettiAndrea/comfy-test/.github/workflows/test-matrix.yml@main
|
|
49
|
+
with:
|
|
50
|
+
config-file: "comfy-test.toml"
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
__version__ = "0.0.3"
|
|
54
|
+
|
|
55
|
+
from .test.config import TestConfig, WorkflowConfig, PlatformTestConfig
|
|
56
|
+
from .test.config_file import load_config, discover_config, CONFIG_FILE_NAMES
|
|
57
|
+
from .test.manager import TestManager, TestResult
|
|
58
|
+
from .test.node_discovery import discover_nodes
|
|
59
|
+
from .errors import (
|
|
60
|
+
TestError,
|
|
61
|
+
ConfigError,
|
|
62
|
+
SetupError,
|
|
63
|
+
ServerError,
|
|
64
|
+
WorkflowError,
|
|
65
|
+
VerificationError,
|
|
66
|
+
TimeoutError,
|
|
67
|
+
DownloadError,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Convenience functions
|
|
71
|
+
from .runner import run_tests, verify_nodes
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
# Config
|
|
75
|
+
"TestConfig",
|
|
76
|
+
"WorkflowConfig",
|
|
77
|
+
"PlatformTestConfig",
|
|
78
|
+
"load_config",
|
|
79
|
+
"discover_config",
|
|
80
|
+
"CONFIG_FILE_NAMES",
|
|
81
|
+
# Manager
|
|
82
|
+
"TestManager",
|
|
83
|
+
"TestResult",
|
|
84
|
+
# Node discovery
|
|
85
|
+
"discover_nodes",
|
|
86
|
+
# Errors
|
|
87
|
+
"TestError",
|
|
88
|
+
"ConfigError",
|
|
89
|
+
"SetupError",
|
|
90
|
+
"ServerError",
|
|
91
|
+
"WorkflowError",
|
|
92
|
+
"VerificationError",
|
|
93
|
+
"TimeoutError",
|
|
94
|
+
"DownloadError",
|
|
95
|
+
# Convenience
|
|
96
|
+
"run_tests",
|
|
97
|
+
"verify_nodes",
|
|
98
|
+
]
|
comfy_test/cli.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""CLI for comfy-test."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .test.config import TestLevel
|
|
10
|
+
from .test.config_file import discover_config, load_config, CONFIG_FILE_NAMES
|
|
11
|
+
from .test.manager import TestManager
|
|
12
|
+
from .test.node_discovery import discover_nodes
|
|
13
|
+
from .errors import TestError, ConfigError, SetupError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_run(args) -> int:
|
|
17
|
+
"""Run installation tests."""
|
|
18
|
+
try:
|
|
19
|
+
# Load config
|
|
20
|
+
if args.config:
|
|
21
|
+
config = load_config(args.config)
|
|
22
|
+
else:
|
|
23
|
+
config = discover_config()
|
|
24
|
+
|
|
25
|
+
# Parse level if specified
|
|
26
|
+
level = None
|
|
27
|
+
if args.level:
|
|
28
|
+
level = TestLevel(args.level)
|
|
29
|
+
|
|
30
|
+
# Create manager
|
|
31
|
+
manager = TestManager(config)
|
|
32
|
+
|
|
33
|
+
# Run tests
|
|
34
|
+
if args.platform:
|
|
35
|
+
results = [manager.run_platform(args.platform, args.dry_run, level)]
|
|
36
|
+
else:
|
|
37
|
+
results = manager.run_all(args.dry_run, level)
|
|
38
|
+
|
|
39
|
+
# Report results
|
|
40
|
+
print(f"\n{'='*60}")
|
|
41
|
+
print("RESULTS")
|
|
42
|
+
print(f"{'='*60}")
|
|
43
|
+
|
|
44
|
+
all_passed = True
|
|
45
|
+
for result in results:
|
|
46
|
+
status = "PASS" if result.success else "FAIL"
|
|
47
|
+
print(f" {result.platform}: {status}")
|
|
48
|
+
if not result.success:
|
|
49
|
+
all_passed = False
|
|
50
|
+
if result.error:
|
|
51
|
+
print(f" Error: {result.error}")
|
|
52
|
+
|
|
53
|
+
return 0 if all_passed else 1
|
|
54
|
+
|
|
55
|
+
except ConfigError as e:
|
|
56
|
+
print(f"Configuration error: {e.message}", file=sys.stderr)
|
|
57
|
+
if e.details:
|
|
58
|
+
print(f"Details: {e.details}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
except TestError as e:
|
|
61
|
+
print(f"Test error: {e.message}", file=sys.stderr)
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_verify(args) -> int:
|
|
66
|
+
"""Verify node registration only."""
|
|
67
|
+
try:
|
|
68
|
+
if args.config:
|
|
69
|
+
config = load_config(args.config)
|
|
70
|
+
else:
|
|
71
|
+
config = discover_config()
|
|
72
|
+
|
|
73
|
+
manager = TestManager(config)
|
|
74
|
+
results = manager.verify_only(args.platform)
|
|
75
|
+
|
|
76
|
+
all_passed = all(r.success for r in results)
|
|
77
|
+
for result in results:
|
|
78
|
+
status = "PASS" if result.success else "FAIL"
|
|
79
|
+
print(f"{result.platform}: {status}")
|
|
80
|
+
if not result.success and result.error:
|
|
81
|
+
print(f" Error: {result.error}")
|
|
82
|
+
|
|
83
|
+
return 0 if all_passed else 1
|
|
84
|
+
|
|
85
|
+
except (ConfigError, TestError) as e:
|
|
86
|
+
print(f"Error: {e.message}", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_info(args) -> int:
|
|
91
|
+
"""Show configuration and environment info."""
|
|
92
|
+
try:
|
|
93
|
+
if args.config:
|
|
94
|
+
config = load_config(args.config)
|
|
95
|
+
config_path = args.config
|
|
96
|
+
else:
|
|
97
|
+
try:
|
|
98
|
+
config = discover_config()
|
|
99
|
+
config_path = "auto-discovered"
|
|
100
|
+
except ConfigError:
|
|
101
|
+
print("No configuration file found.")
|
|
102
|
+
print(f"Searched for: {', '.join(CONFIG_FILE_NAMES)}")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
print(f"Configuration: {config_path}")
|
|
106
|
+
print(f" Name: {config.name}")
|
|
107
|
+
print(f" ComfyUI Version: {config.comfyui_version}")
|
|
108
|
+
print(f" Python Version: {config.python_version}")
|
|
109
|
+
print(f" CPU Only: {config.cpu_only}")
|
|
110
|
+
print(f" Timeout: {config.timeout}s")
|
|
111
|
+
print(f" Levels: {', '.join(l.value for l in config.levels)}")
|
|
112
|
+
print()
|
|
113
|
+
print("Platforms:")
|
|
114
|
+
print(f" Linux: {'enabled' if config.linux.enabled else 'disabled'}")
|
|
115
|
+
print(f" Windows: {'enabled' if config.windows.enabled else 'disabled'}")
|
|
116
|
+
print(f" Windows Portable: {'enabled' if config.windows_portable.enabled else 'disabled'}")
|
|
117
|
+
print()
|
|
118
|
+
print("Nodes (auto-discovered from NODE_CLASS_MAPPINGS):")
|
|
119
|
+
try:
|
|
120
|
+
node_dir = Path(args.config).parent if args.config else Path.cwd()
|
|
121
|
+
nodes = discover_nodes(node_dir)
|
|
122
|
+
print(f" Found {len(nodes)} node(s):")
|
|
123
|
+
for node in nodes:
|
|
124
|
+
print(f" - {node}")
|
|
125
|
+
except SetupError as e:
|
|
126
|
+
print(f" Error discovering nodes: {e.message}")
|
|
127
|
+
print()
|
|
128
|
+
print("Workflows:")
|
|
129
|
+
print(f" Timeout: {config.workflow.timeout}s")
|
|
130
|
+
if config.workflow.run:
|
|
131
|
+
print(f" Run (execution): {len(config.workflow.run)} workflow(s)")
|
|
132
|
+
for wf in config.workflow.run:
|
|
133
|
+
print(f" - {wf}")
|
|
134
|
+
else:
|
|
135
|
+
print(" Run (execution): none configured")
|
|
136
|
+
if config.workflow.screenshot:
|
|
137
|
+
print(f" Screenshot: {len(config.workflow.screenshot)} workflow(s)")
|
|
138
|
+
for wf in config.workflow.screenshot:
|
|
139
|
+
print(f" - {wf}")
|
|
140
|
+
else:
|
|
141
|
+
print(" Screenshot: none configured")
|
|
142
|
+
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
except ConfigError as e:
|
|
146
|
+
print(f"Error: {e.message}", file=sys.stderr)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_init_ci(args) -> int:
|
|
151
|
+
"""Generate GitHub Actions workflow file."""
|
|
152
|
+
output_path = Path(args.output)
|
|
153
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
|
|
155
|
+
workflow_content = '''name: Test Installation
|
|
156
|
+
on: [push, pull_request]
|
|
157
|
+
|
|
158
|
+
jobs:
|
|
159
|
+
test:
|
|
160
|
+
uses: PozzettiAndrea/comfy-test/.github/workflows/test-matrix.yml@main
|
|
161
|
+
with:
|
|
162
|
+
config-file: "comfy-test.toml"
|
|
163
|
+
'''
|
|
164
|
+
|
|
165
|
+
with open(output_path, "w") as f:
|
|
166
|
+
f.write(workflow_content)
|
|
167
|
+
|
|
168
|
+
print(f"Generated GitHub Actions workflow: {output_path}")
|
|
169
|
+
print()
|
|
170
|
+
print("Make sure to:")
|
|
171
|
+
print(" 1. Create a comfy-test.toml in your repository root")
|
|
172
|
+
print(" 2. Commit both files to your repository")
|
|
173
|
+
print()
|
|
174
|
+
print("Example comfy-test.toml:")
|
|
175
|
+
print('''
|
|
176
|
+
[test]
|
|
177
|
+
name = "MyNode"
|
|
178
|
+
python_version = "3.10"
|
|
179
|
+
|
|
180
|
+
[test.workflows]
|
|
181
|
+
timeout = 120
|
|
182
|
+
run = ["workflows/basic.json"]
|
|
183
|
+
screenshot = ["workflows/basic.json"]
|
|
184
|
+
''')
|
|
185
|
+
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def cmd_download_portable(args) -> int:
|
|
190
|
+
"""Download ComfyUI Portable for testing."""
|
|
191
|
+
from .test.platform.windows_portable import WindowsPortableTestPlatform
|
|
192
|
+
|
|
193
|
+
platform = WindowsPortableTestPlatform()
|
|
194
|
+
|
|
195
|
+
version = args.version
|
|
196
|
+
if version == "latest":
|
|
197
|
+
version = platform._get_latest_release_tag()
|
|
198
|
+
|
|
199
|
+
output_path = Path(args.output)
|
|
200
|
+
archive_path = output_path / f"ComfyUI_portable_{version}.7z"
|
|
201
|
+
|
|
202
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
platform._download_portable(version, archive_path)
|
|
204
|
+
|
|
205
|
+
print(f"Downloaded to: {archive_path}")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_screenshot(args) -> int:
|
|
210
|
+
"""Generate workflow screenshots."""
|
|
211
|
+
try:
|
|
212
|
+
# Import screenshot module (requires optional dependencies)
|
|
213
|
+
try:
|
|
214
|
+
from .screenshot import (
|
|
215
|
+
WorkflowScreenshot,
|
|
216
|
+
check_dependencies,
|
|
217
|
+
ScreenshotError,
|
|
218
|
+
)
|
|
219
|
+
from .screenshot_cache import ScreenshotCache
|
|
220
|
+
check_dependencies()
|
|
221
|
+
except ImportError as e:
|
|
222
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
223
|
+
print("Install with: pip install comfy-test[screenshot]", file=sys.stderr)
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
# Load config to get workflow files
|
|
227
|
+
if args.config:
|
|
228
|
+
config = load_config(args.config)
|
|
229
|
+
node_dir = Path(args.config).parent
|
|
230
|
+
else:
|
|
231
|
+
try:
|
|
232
|
+
config = discover_config()
|
|
233
|
+
node_dir = Path.cwd()
|
|
234
|
+
except ConfigError:
|
|
235
|
+
config = None
|
|
236
|
+
node_dir = Path.cwd()
|
|
237
|
+
|
|
238
|
+
# Determine which workflows to capture
|
|
239
|
+
workflow_files = []
|
|
240
|
+
|
|
241
|
+
if args.workflow:
|
|
242
|
+
# Specific workflow provided
|
|
243
|
+
workflow_path = Path(args.workflow)
|
|
244
|
+
if not workflow_path.is_absolute():
|
|
245
|
+
workflow_path = node_dir / workflow_path
|
|
246
|
+
workflow_files = [workflow_path]
|
|
247
|
+
elif config and config.workflow.screenshot:
|
|
248
|
+
# Use workflows from config's screenshot list
|
|
249
|
+
workflow_files = config.workflow.screenshot
|
|
250
|
+
else:
|
|
251
|
+
# Auto-discover from workflows/ directory
|
|
252
|
+
workflows_dir = node_dir / "workflows"
|
|
253
|
+
if workflows_dir.exists():
|
|
254
|
+
workflow_files = sorted(workflows_dir.glob("*.json"))
|
|
255
|
+
|
|
256
|
+
if not workflow_files:
|
|
257
|
+
print("No workflow files found.", file=sys.stderr)
|
|
258
|
+
print("Specify a workflow file or configure workflows in comfy-test.toml", file=sys.stderr)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
# Determine output directory
|
|
262
|
+
output_dir = Path(args.output) if args.output else None
|
|
263
|
+
|
|
264
|
+
# Initialize cache
|
|
265
|
+
cache = ScreenshotCache(node_dir)
|
|
266
|
+
|
|
267
|
+
# Filter workflows that need updating (unless --force)
|
|
268
|
+
def get_output_path(wf: Path) -> Path:
|
|
269
|
+
if output_dir:
|
|
270
|
+
return output_dir / wf.with_suffix(".png").name
|
|
271
|
+
return wf.with_suffix(".png")
|
|
272
|
+
|
|
273
|
+
if args.force:
|
|
274
|
+
workflows_to_capture = workflow_files
|
|
275
|
+
skipped = []
|
|
276
|
+
else:
|
|
277
|
+
workflows_to_capture = []
|
|
278
|
+
skipped = []
|
|
279
|
+
for wf in workflow_files:
|
|
280
|
+
out_path = get_output_path(wf)
|
|
281
|
+
if cache.needs_update(wf, out_path):
|
|
282
|
+
workflows_to_capture.append(wf)
|
|
283
|
+
else:
|
|
284
|
+
skipped.append(wf)
|
|
285
|
+
|
|
286
|
+
# Determine server URL
|
|
287
|
+
if args.server is True:
|
|
288
|
+
# --server flag without URL, use default
|
|
289
|
+
server_url = "http://localhost:8188"
|
|
290
|
+
use_existing_server = True
|
|
291
|
+
elif args.server:
|
|
292
|
+
# --server with custom URL
|
|
293
|
+
server_url = args.server
|
|
294
|
+
use_existing_server = True
|
|
295
|
+
else:
|
|
296
|
+
# No --server flag, need to start our own server
|
|
297
|
+
server_url = "http://127.0.0.1:8188"
|
|
298
|
+
use_existing_server = False
|
|
299
|
+
|
|
300
|
+
# Dry run mode
|
|
301
|
+
if args.dry_run:
|
|
302
|
+
if skipped:
|
|
303
|
+
print(f"Skipping {len(skipped)} unchanged workflow(s):")
|
|
304
|
+
for wf in skipped:
|
|
305
|
+
print(f" {wf.name} (cached)")
|
|
306
|
+
if workflows_to_capture:
|
|
307
|
+
print(f"Would capture {len(workflows_to_capture)} screenshot(s):")
|
|
308
|
+
for wf in workflows_to_capture:
|
|
309
|
+
out_path = get_output_path(wf)
|
|
310
|
+
print(f" {wf} -> {out_path}")
|
|
311
|
+
else:
|
|
312
|
+
print("All screenshots up to date.")
|
|
313
|
+
if use_existing_server and workflows_to_capture:
|
|
314
|
+
print(f"Using existing server at: {server_url}")
|
|
315
|
+
elif workflows_to_capture:
|
|
316
|
+
print("Would start ComfyUI server for screenshots")
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
# Log function
|
|
320
|
+
def log(msg: str) -> None:
|
|
321
|
+
print(msg)
|
|
322
|
+
|
|
323
|
+
# Report skipped workflows
|
|
324
|
+
if skipped:
|
|
325
|
+
log(f"Skipping {len(skipped)} unchanged workflow(s)")
|
|
326
|
+
|
|
327
|
+
if not workflows_to_capture:
|
|
328
|
+
log("All screenshots up to date.")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
# Capture screenshots
|
|
332
|
+
results = []
|
|
333
|
+
|
|
334
|
+
if use_existing_server:
|
|
335
|
+
# Connect to existing server
|
|
336
|
+
log(f"Connecting to existing server at {server_url}...")
|
|
337
|
+
with WorkflowScreenshot(server_url, log_callback=log) as ws:
|
|
338
|
+
for wf in workflows_to_capture:
|
|
339
|
+
out_path = get_output_path(wf)
|
|
340
|
+
try:
|
|
341
|
+
result = ws.capture(wf, out_path)
|
|
342
|
+
cache.save_fingerprint(wf, out_path)
|
|
343
|
+
results.append(result)
|
|
344
|
+
except ScreenshotError as e:
|
|
345
|
+
log(f" ERROR: {e.message}")
|
|
346
|
+
else:
|
|
347
|
+
# Start our own server (requires full test environment)
|
|
348
|
+
if not config:
|
|
349
|
+
print("Error: No config file found.", file=sys.stderr)
|
|
350
|
+
print("Use --server to connect to an existing ComfyUI server,", file=sys.stderr)
|
|
351
|
+
print("or create a comfy-test.toml config file.", file=sys.stderr)
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
log("Setting up ComfyUI environment for screenshots...")
|
|
355
|
+
from .test.platform import get_platform
|
|
356
|
+
from .test.comfy_env import get_cuda_packages
|
|
357
|
+
from .comfyui.server import ComfyUIServer
|
|
358
|
+
|
|
359
|
+
platform = get_platform(log_callback=log)
|
|
360
|
+
|
|
361
|
+
with tempfile.TemporaryDirectory(prefix="comfy_screenshot_") as work_dir:
|
|
362
|
+
work_path = Path(work_dir)
|
|
363
|
+
|
|
364
|
+
# Setup ComfyUI
|
|
365
|
+
log("Setting up ComfyUI...")
|
|
366
|
+
paths = platform.setup_comfyui(config, work_path)
|
|
367
|
+
|
|
368
|
+
# Install the node
|
|
369
|
+
log("Installing custom node...")
|
|
370
|
+
platform.install_node(paths, node_dir)
|
|
371
|
+
|
|
372
|
+
# Get CUDA packages to mock
|
|
373
|
+
cuda_packages = get_cuda_packages(node_dir)
|
|
374
|
+
|
|
375
|
+
# Start server
|
|
376
|
+
log("Starting ComfyUI server...")
|
|
377
|
+
with ComfyUIServer(
|
|
378
|
+
platform, paths, config,
|
|
379
|
+
cuda_mock_packages=cuda_packages,
|
|
380
|
+
log_callback=log,
|
|
381
|
+
) as server:
|
|
382
|
+
with WorkflowScreenshot(server.base_url, log_callback=log) as ws:
|
|
383
|
+
for wf in workflows_to_capture:
|
|
384
|
+
out_path = get_output_path(wf)
|
|
385
|
+
try:
|
|
386
|
+
result = ws.capture(wf, out_path)
|
|
387
|
+
cache.save_fingerprint(wf, out_path)
|
|
388
|
+
results.append(result)
|
|
389
|
+
except ScreenshotError as e:
|
|
390
|
+
log(f" ERROR: {e.message}")
|
|
391
|
+
|
|
392
|
+
# Report results
|
|
393
|
+
print(f"\nCaptured {len(results)} screenshot(s)")
|
|
394
|
+
for path in results:
|
|
395
|
+
print(f" {path}")
|
|
396
|
+
|
|
397
|
+
return 0
|
|
398
|
+
|
|
399
|
+
except ScreenshotError as e:
|
|
400
|
+
print(f"Screenshot error: {e.message}", file=sys.stderr)
|
|
401
|
+
if e.details:
|
|
402
|
+
print(f"Details: {e.details}", file=sys.stderr)
|
|
403
|
+
return 1
|
|
404
|
+
except (ConfigError, TestError) as e:
|
|
405
|
+
print(f"Error: {e.message}", file=sys.stderr)
|
|
406
|
+
return 1
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def main(args=None) -> int:
|
|
413
|
+
"""Main CLI entry point."""
|
|
414
|
+
parser = argparse.ArgumentParser(
|
|
415
|
+
prog="comfy-test",
|
|
416
|
+
description="Installation testing for ComfyUI custom nodes",
|
|
417
|
+
)
|
|
418
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
419
|
+
|
|
420
|
+
# run command
|
|
421
|
+
run_parser = subparsers.add_parser(
|
|
422
|
+
"run",
|
|
423
|
+
help="Run installation tests",
|
|
424
|
+
)
|
|
425
|
+
run_parser.add_argument(
|
|
426
|
+
"--config", "-c",
|
|
427
|
+
help="Path to config file (default: auto-discover)",
|
|
428
|
+
)
|
|
429
|
+
run_parser.add_argument(
|
|
430
|
+
"--platform", "-p",
|
|
431
|
+
choices=["linux", "windows", "windows-portable"],
|
|
432
|
+
help="Run on specific platform only",
|
|
433
|
+
)
|
|
434
|
+
run_parser.add_argument(
|
|
435
|
+
"--level", "-l",
|
|
436
|
+
choices=["syntax", "install", "registration", "instantiation", "validation", "execution"],
|
|
437
|
+
help="Run only up to this level (overrides config)",
|
|
438
|
+
)
|
|
439
|
+
run_parser.add_argument(
|
|
440
|
+
"--dry-run",
|
|
441
|
+
action="store_true",
|
|
442
|
+
help="Show what would be done without doing it",
|
|
443
|
+
)
|
|
444
|
+
run_parser.set_defaults(func=cmd_run)
|
|
445
|
+
|
|
446
|
+
# verify command
|
|
447
|
+
verify_parser = subparsers.add_parser(
|
|
448
|
+
"verify",
|
|
449
|
+
help="Verify node registration only",
|
|
450
|
+
)
|
|
451
|
+
verify_parser.add_argument(
|
|
452
|
+
"--config", "-c",
|
|
453
|
+
help="Path to config file",
|
|
454
|
+
)
|
|
455
|
+
verify_parser.add_argument(
|
|
456
|
+
"--platform", "-p",
|
|
457
|
+
choices=["linux", "windows", "windows-portable"],
|
|
458
|
+
help="Platform to verify on",
|
|
459
|
+
)
|
|
460
|
+
verify_parser.set_defaults(func=cmd_verify)
|
|
461
|
+
|
|
462
|
+
# info command
|
|
463
|
+
info_parser = subparsers.add_parser(
|
|
464
|
+
"info",
|
|
465
|
+
help="Show configuration info",
|
|
466
|
+
)
|
|
467
|
+
info_parser.add_argument(
|
|
468
|
+
"--config", "-c",
|
|
469
|
+
help="Path to config file",
|
|
470
|
+
)
|
|
471
|
+
info_parser.set_defaults(func=cmd_info)
|
|
472
|
+
|
|
473
|
+
# init-ci command
|
|
474
|
+
init_ci_parser = subparsers.add_parser(
|
|
475
|
+
"init-ci",
|
|
476
|
+
help="Generate GitHub Actions workflow",
|
|
477
|
+
)
|
|
478
|
+
init_ci_parser.add_argument(
|
|
479
|
+
"--output", "-o",
|
|
480
|
+
default=".github/workflows/test-install.yml",
|
|
481
|
+
help="Output file path",
|
|
482
|
+
)
|
|
483
|
+
init_ci_parser.set_defaults(func=cmd_init_ci)
|
|
484
|
+
|
|
485
|
+
# download-portable command
|
|
486
|
+
download_parser = subparsers.add_parser(
|
|
487
|
+
"download-portable",
|
|
488
|
+
help="Download ComfyUI Portable",
|
|
489
|
+
)
|
|
490
|
+
download_parser.add_argument(
|
|
491
|
+
"--version", "-v",
|
|
492
|
+
default="latest",
|
|
493
|
+
help="Version to download (default: latest)",
|
|
494
|
+
)
|
|
495
|
+
download_parser.add_argument(
|
|
496
|
+
"--output", "-o",
|
|
497
|
+
default=".",
|
|
498
|
+
help="Output directory",
|
|
499
|
+
)
|
|
500
|
+
download_parser.set_defaults(func=cmd_download_portable)
|
|
501
|
+
|
|
502
|
+
# screenshot command
|
|
503
|
+
screenshot_parser = subparsers.add_parser(
|
|
504
|
+
"screenshot",
|
|
505
|
+
help="Generate workflow screenshots with embedded metadata",
|
|
506
|
+
)
|
|
507
|
+
screenshot_parser.add_argument(
|
|
508
|
+
"workflow",
|
|
509
|
+
nargs="?",
|
|
510
|
+
help="Specific workflow file to screenshot (default: all from config)",
|
|
511
|
+
)
|
|
512
|
+
screenshot_parser.add_argument(
|
|
513
|
+
"--config", "-c",
|
|
514
|
+
help="Path to config file",
|
|
515
|
+
)
|
|
516
|
+
screenshot_parser.add_argument(
|
|
517
|
+
"--output", "-o",
|
|
518
|
+
help="Output directory for screenshots (default: same as workflow)",
|
|
519
|
+
)
|
|
520
|
+
screenshot_parser.add_argument(
|
|
521
|
+
"--server", "-s",
|
|
522
|
+
nargs="?",
|
|
523
|
+
const=True,
|
|
524
|
+
default=False,
|
|
525
|
+
help="Use existing ComfyUI server (default: localhost:8188, or specify URL)",
|
|
526
|
+
)
|
|
527
|
+
screenshot_parser.add_argument(
|
|
528
|
+
"--dry-run",
|
|
529
|
+
action="store_true",
|
|
530
|
+
help="Show what would be captured without doing it",
|
|
531
|
+
)
|
|
532
|
+
screenshot_parser.add_argument(
|
|
533
|
+
"--force", "-f",
|
|
534
|
+
action="store_true",
|
|
535
|
+
help="Force regeneration, ignoring cache",
|
|
536
|
+
)
|
|
537
|
+
screenshot_parser.set_defaults(func=cmd_screenshot)
|
|
538
|
+
|
|
539
|
+
parsed_args = parser.parse_args(args)
|
|
540
|
+
return parsed_args.func(parsed_args)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
if __name__ == "__main__":
|
|
544
|
+
sys.exit(main())
|