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 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())
@@ -0,0 +1,11 @@
1
+ """ComfyUI server interaction utilities."""
2
+
3
+ from .api import ComfyUIAPI
4
+ from .server import ComfyUIServer
5
+ from .workflow import WorkflowRunner
6
+
7
+ __all__ = [
8
+ "ComfyUIAPI",
9
+ "ComfyUIServer",
10
+ "WorkflowRunner",
11
+ ]