comfy-test 0.0.1__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,94 @@
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
+ expected_nodes = ["MyNode1", "MyNode2"]
33
+
34
+ [test.workflow]
35
+ file = "tests/workflows/smoke_test.json"
36
+
37
+ ## GitHub Actions
38
+
39
+ Add this workflow to your repository:
40
+
41
+ # .github/workflows/test-install.yml
42
+ name: Test Installation
43
+ on: [push, pull_request]
44
+
45
+ jobs:
46
+ test:
47
+ uses: PozzettiAndrea/comfy-test/.github/workflows/test-matrix.yml@main
48
+ with:
49
+ config-file: "comfy-test.toml"
50
+ """
51
+
52
+ __version__ = "0.0.1"
53
+
54
+ from .test.config import TestConfig, WorkflowConfig, PlatformTestConfig
55
+ from .test.config_file import load_config, discover_config, CONFIG_FILE_NAMES
56
+ from .test.manager import TestManager, TestResult
57
+ from .errors import (
58
+ TestError,
59
+ ConfigError,
60
+ SetupError,
61
+ ServerError,
62
+ WorkflowError,
63
+ VerificationError,
64
+ TimeoutError,
65
+ DownloadError,
66
+ )
67
+
68
+ # Convenience functions
69
+ from .runner import run_tests, verify_nodes
70
+
71
+ __all__ = [
72
+ # Config
73
+ "TestConfig",
74
+ "WorkflowConfig",
75
+ "PlatformTestConfig",
76
+ "load_config",
77
+ "discover_config",
78
+ "CONFIG_FILE_NAMES",
79
+ # Manager
80
+ "TestManager",
81
+ "TestResult",
82
+ # Errors
83
+ "TestError",
84
+ "ConfigError",
85
+ "SetupError",
86
+ "ServerError",
87
+ "WorkflowError",
88
+ "VerificationError",
89
+ "TimeoutError",
90
+ "DownloadError",
91
+ # Convenience
92
+ "run_tests",
93
+ "verify_nodes",
94
+ ]
comfy_test/cli.py ADDED
@@ -0,0 +1,277 @@
1
+ """CLI for comfy-test."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .test.config_file import discover_config, load_config, CONFIG_FILE_NAMES
8
+ from .test.manager import TestManager
9
+ from .errors import TestError, ConfigError
10
+
11
+
12
+ def cmd_run(args) -> int:
13
+ """Run installation tests."""
14
+ try:
15
+ # Load config
16
+ if args.config:
17
+ config = load_config(args.config)
18
+ else:
19
+ config = discover_config()
20
+
21
+ # Create manager
22
+ manager = TestManager(config)
23
+
24
+ # Run tests
25
+ if args.platform:
26
+ results = [manager.run_platform(args.platform, args.dry_run)]
27
+ else:
28
+ results = manager.run_all(args.dry_run)
29
+
30
+ # Report results
31
+ print(f"\n{'='*60}")
32
+ print("RESULTS")
33
+ print(f"{'='*60}")
34
+
35
+ all_passed = True
36
+ for result in results:
37
+ status = "PASS" if result.success else "FAIL"
38
+ print(f" {result.platform}: {status}")
39
+ if not result.success:
40
+ all_passed = False
41
+ if result.error:
42
+ print(f" Error: {result.error}")
43
+
44
+ return 0 if all_passed else 1
45
+
46
+ except ConfigError as e:
47
+ print(f"Configuration error: {e.message}", file=sys.stderr)
48
+ if e.details:
49
+ print(f"Details: {e.details}", file=sys.stderr)
50
+ return 1
51
+ except TestError as e:
52
+ print(f"Test error: {e.message}", file=sys.stderr)
53
+ return 1
54
+
55
+
56
+ def cmd_verify(args) -> int:
57
+ """Verify node registration only."""
58
+ try:
59
+ if args.config:
60
+ config = load_config(args.config)
61
+ else:
62
+ config = discover_config()
63
+
64
+ manager = TestManager(config)
65
+ results = manager.verify_only(args.platform)
66
+
67
+ all_passed = all(r.success for r in results)
68
+ for result in results:
69
+ status = "PASS" if result.success else "FAIL"
70
+ print(f"{result.platform}: {status}")
71
+ if not result.success and result.error:
72
+ print(f" Error: {result.error}")
73
+
74
+ return 0 if all_passed else 1
75
+
76
+ except (ConfigError, TestError) as e:
77
+ print(f"Error: {e.message}", file=sys.stderr)
78
+ return 1
79
+
80
+
81
+ def cmd_info(args) -> int:
82
+ """Show configuration and environment info."""
83
+ try:
84
+ if args.config:
85
+ config = load_config(args.config)
86
+ config_path = args.config
87
+ else:
88
+ try:
89
+ config = discover_config()
90
+ config_path = "auto-discovered"
91
+ except ConfigError:
92
+ print("No configuration file found.")
93
+ print(f"Searched for: {', '.join(CONFIG_FILE_NAMES)}")
94
+ return 1
95
+
96
+ print(f"Configuration: {config_path}")
97
+ print(f" Name: {config.name}")
98
+ print(f" ComfyUI Version: {config.comfyui_version}")
99
+ print(f" Python Version: {config.python_version}")
100
+ print(f" CPU Only: {config.cpu_only}")
101
+ print(f" Timeout: {config.timeout}s")
102
+ print()
103
+ print("Platforms:")
104
+ print(f" Linux: {'enabled' if config.linux.enabled else 'disabled'}")
105
+ print(f" Windows: {'enabled' if config.windows.enabled else 'disabled'}")
106
+ print(f" Windows Portable: {'enabled' if config.windows_portable.enabled else 'disabled'}")
107
+ print()
108
+ print("Verification:")
109
+ if config.expected_nodes:
110
+ print(f" Expected nodes ({len(config.expected_nodes)}):")
111
+ for node in config.expected_nodes:
112
+ print(f" - {node}")
113
+ else:
114
+ print(" No expected nodes configured")
115
+ print()
116
+ print("Workflow:")
117
+ if config.workflow.file:
118
+ print(f" File: {config.workflow.file}")
119
+ print(f" Timeout: {config.workflow.timeout}s")
120
+ else:
121
+ print(" No workflow configured")
122
+
123
+ return 0
124
+
125
+ except ConfigError as e:
126
+ print(f"Error: {e.message}", file=sys.stderr)
127
+ return 1
128
+
129
+
130
+ def cmd_init_ci(args) -> int:
131
+ """Generate GitHub Actions workflow file."""
132
+ output_path = Path(args.output)
133
+ output_path.parent.mkdir(parents=True, exist_ok=True)
134
+
135
+ workflow_content = '''name: Test Installation
136
+ on: [push, pull_request]
137
+
138
+ jobs:
139
+ test:
140
+ uses: PozzettiAndrea/comfy-test/.github/workflows/test-matrix.yml@main
141
+ with:
142
+ config-file: "comfy-test.toml"
143
+ '''
144
+
145
+ with open(output_path, "w") as f:
146
+ f.write(workflow_content)
147
+
148
+ print(f"Generated GitHub Actions workflow: {output_path}")
149
+ print()
150
+ print("Make sure to:")
151
+ print(" 1. Create a comfy-test.toml in your repository root")
152
+ print(" 2. Commit both files to your repository")
153
+ print()
154
+ print("Example comfy-test.toml:")
155
+ print('''
156
+ [test]
157
+ name = "MyNode"
158
+ python_version = "3.10"
159
+
160
+ [test.verification]
161
+ expected_nodes = ["MyNode1", "MyNode2"]
162
+ ''')
163
+
164
+ return 0
165
+
166
+
167
+ def cmd_download_portable(args) -> int:
168
+ """Download ComfyUI Portable for testing."""
169
+ from .test.platform.windows_portable import WindowsPortableTestPlatform
170
+
171
+ platform = WindowsPortableTestPlatform()
172
+
173
+ version = args.version
174
+ if version == "latest":
175
+ version = platform._get_latest_release_tag()
176
+
177
+ output_path = Path(args.output)
178
+ archive_path = output_path / f"ComfyUI_portable_{version}.7z"
179
+
180
+ output_path.mkdir(parents=True, exist_ok=True)
181
+ platform._download_portable(version, archive_path)
182
+
183
+ print(f"Downloaded to: {archive_path}")
184
+ return 0
185
+
186
+
187
+ def main(args=None) -> int:
188
+ """Main CLI entry point."""
189
+ parser = argparse.ArgumentParser(
190
+ prog="comfy-test",
191
+ description="Installation testing for ComfyUI custom nodes",
192
+ )
193
+ subparsers = parser.add_subparsers(dest="command", required=True)
194
+
195
+ # run command
196
+ run_parser = subparsers.add_parser(
197
+ "run",
198
+ help="Run installation tests",
199
+ )
200
+ run_parser.add_argument(
201
+ "--config", "-c",
202
+ help="Path to config file (default: auto-discover)",
203
+ )
204
+ run_parser.add_argument(
205
+ "--platform", "-p",
206
+ choices=["linux", "windows", "windows-portable"],
207
+ help="Run on specific platform only",
208
+ )
209
+ run_parser.add_argument(
210
+ "--dry-run",
211
+ action="store_true",
212
+ help="Show what would be done without doing it",
213
+ )
214
+ run_parser.set_defaults(func=cmd_run)
215
+
216
+ # verify command
217
+ verify_parser = subparsers.add_parser(
218
+ "verify",
219
+ help="Verify node registration only",
220
+ )
221
+ verify_parser.add_argument(
222
+ "--config", "-c",
223
+ help="Path to config file",
224
+ )
225
+ verify_parser.add_argument(
226
+ "--platform", "-p",
227
+ choices=["linux", "windows", "windows-portable"],
228
+ help="Platform to verify on",
229
+ )
230
+ verify_parser.set_defaults(func=cmd_verify)
231
+
232
+ # info command
233
+ info_parser = subparsers.add_parser(
234
+ "info",
235
+ help="Show configuration info",
236
+ )
237
+ info_parser.add_argument(
238
+ "--config", "-c",
239
+ help="Path to config file",
240
+ )
241
+ info_parser.set_defaults(func=cmd_info)
242
+
243
+ # init-ci command
244
+ init_ci_parser = subparsers.add_parser(
245
+ "init-ci",
246
+ help="Generate GitHub Actions workflow",
247
+ )
248
+ init_ci_parser.add_argument(
249
+ "--output", "-o",
250
+ default=".github/workflows/test-install.yml",
251
+ help="Output file path",
252
+ )
253
+ init_ci_parser.set_defaults(func=cmd_init_ci)
254
+
255
+ # download-portable command
256
+ download_parser = subparsers.add_parser(
257
+ "download-portable",
258
+ help="Download ComfyUI Portable",
259
+ )
260
+ download_parser.add_argument(
261
+ "--version", "-v",
262
+ default="latest",
263
+ help="Version to download (default: latest)",
264
+ )
265
+ download_parser.add_argument(
266
+ "--output", "-o",
267
+ default=".",
268
+ help="Output directory",
269
+ )
270
+ download_parser.set_defaults(func=cmd_download_portable)
271
+
272
+ parsed_args = parser.parse_args(args)
273
+ return parsed_args.func(parsed_args)
274
+
275
+
276
+ if __name__ == "__main__":
277
+ 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
+ ]
@@ -0,0 +1,184 @@
1
+ """ComfyUI REST API client."""
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import requests
7
+
8
+ from ..errors import ServerError, VerificationError
9
+
10
+
11
+ class ComfyUIAPI:
12
+ """Client for ComfyUI REST API.
13
+
14
+ Provides methods to interact with a running ComfyUI server.
15
+
16
+ Args:
17
+ base_url: Base URL of the ComfyUI server (e.g., "http://127.0.0.1:8188")
18
+ timeout: Request timeout in seconds
19
+
20
+ Example:
21
+ >>> api = ComfyUIAPI("http://127.0.0.1:8188")
22
+ >>> nodes = api.get_object_info()
23
+ >>> print(list(nodes.keys())[:5])
24
+ """
25
+
26
+ def __init__(self, base_url: str = "http://127.0.0.1:8188", timeout: int = 30):
27
+ self.base_url = base_url.rstrip("/")
28
+ self.timeout = timeout
29
+ self.session = requests.Session()
30
+
31
+ def health_check(self) -> bool:
32
+ """Check if the server is responsive.
33
+
34
+ Returns:
35
+ True if server responds, False otherwise
36
+ """
37
+ try:
38
+ response = self.session.get(
39
+ f"{self.base_url}/system_stats",
40
+ timeout=self.timeout,
41
+ )
42
+ return response.status_code == 200
43
+ except requests.RequestException:
44
+ return False
45
+
46
+ def get_object_info(self) -> Dict[str, Any]:
47
+ """Get information about all registered nodes.
48
+
49
+ Returns:
50
+ Dictionary mapping node names to their info
51
+
52
+ Raises:
53
+ ServerError: If request fails
54
+ """
55
+ try:
56
+ response = self.session.get(
57
+ f"{self.base_url}/object_info",
58
+ timeout=self.timeout,
59
+ )
60
+ response.raise_for_status()
61
+ return response.json()
62
+ except requests.RequestException as e:
63
+ raise ServerError(
64
+ "Failed to get object_info from ComfyUI",
65
+ str(e)
66
+ )
67
+
68
+ def verify_nodes(self, expected_nodes: List[str]) -> None:
69
+ """Verify that expected nodes are registered.
70
+
71
+ Args:
72
+ expected_nodes: List of node names that must exist
73
+
74
+ Raises:
75
+ VerificationError: If any expected nodes are missing
76
+ """
77
+ nodes = self.get_object_info()
78
+ missing = [name for name in expected_nodes if name not in nodes]
79
+
80
+ if missing:
81
+ raise VerificationError(
82
+ f"Expected nodes not found: {', '.join(missing)}",
83
+ missing_nodes=missing,
84
+ )
85
+
86
+ def queue_prompt(self, workflow: Dict[str, Any]) -> str:
87
+ """Queue a workflow for execution.
88
+
89
+ Args:
90
+ workflow: Workflow definition (the "prompt" part of a workflow JSON)
91
+
92
+ Returns:
93
+ Prompt ID for tracking execution
94
+
95
+ Raises:
96
+ ServerError: If request fails
97
+ """
98
+ try:
99
+ response = self.session.post(
100
+ f"{self.base_url}/prompt",
101
+ json={"prompt": workflow},
102
+ timeout=self.timeout,
103
+ )
104
+ response.raise_for_status()
105
+ data = response.json()
106
+ return data["prompt_id"]
107
+ except requests.RequestException as e:
108
+ raise ServerError(
109
+ "Failed to queue prompt",
110
+ str(e)
111
+ )
112
+ except KeyError:
113
+ raise ServerError(
114
+ "Invalid response from /prompt endpoint",
115
+ "Missing prompt_id in response"
116
+ )
117
+
118
+ def get_history(self, prompt_id: str) -> Optional[Dict[str, Any]]:
119
+ """Get execution history for a prompt.
120
+
121
+ Args:
122
+ prompt_id: ID from queue_prompt
123
+
124
+ Returns:
125
+ History data if available, None if prompt hasn't started
126
+
127
+ Raises:
128
+ ServerError: If request fails
129
+ """
130
+ try:
131
+ response = self.session.get(
132
+ f"{self.base_url}/history/{prompt_id}",
133
+ timeout=self.timeout,
134
+ )
135
+ response.raise_for_status()
136
+ data = response.json()
137
+ return data.get(prompt_id)
138
+ except requests.RequestException as e:
139
+ raise ServerError(
140
+ f"Failed to get history for prompt {prompt_id}",
141
+ str(e)
142
+ )
143
+
144
+ def get_queue(self) -> Dict[str, Any]:
145
+ """Get current queue status.
146
+
147
+ Returns:
148
+ Queue data with 'queue_running' and 'queue_pending'
149
+
150
+ Raises:
151
+ ServerError: If request fails
152
+ """
153
+ try:
154
+ response = self.session.get(
155
+ f"{self.base_url}/queue",
156
+ timeout=self.timeout,
157
+ )
158
+ response.raise_for_status()
159
+ return response.json()
160
+ except requests.RequestException as e:
161
+ raise ServerError(
162
+ "Failed to get queue status",
163
+ str(e)
164
+ )
165
+
166
+ def interrupt(self) -> None:
167
+ """Interrupt currently running workflow."""
168
+ try:
169
+ self.session.post(
170
+ f"{self.base_url}/interrupt",
171
+ timeout=self.timeout,
172
+ )
173
+ except requests.RequestException:
174
+ pass # Best effort
175
+
176
+ def close(self) -> None:
177
+ """Close the session."""
178
+ self.session.close()
179
+
180
+ def __enter__(self) -> "ComfyUIAPI":
181
+ return self
182
+
183
+ def __exit__(self, *args) -> None:
184
+ self.close()