Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
skill-docs:
name: Validate skill docs generation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Run generate_skill_docs.py
run: |
stderr_file=$(mktemp)
python scripts/generate_skill_docs.py > /dev/null 2>"$stderr_file"
if [ -s "$stderr_file" ]; then
cat "$stderr_file"
echo "::error::generate_skill_docs.py produced warnings (missing or uncategorized nodes)"
exit 1
fi
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,21 @@ comfyci --help
| **Assert Image Match** | Compare image against a perceptual hash with configurable threshold |
| **Assert Contains Color** | Verify image contains pixels of a specific color |
| **Assert Tensor Shape** | Validate tensor dimensions (batch, height, width, channels) |
| **Assert Mask Coverage** | Check that mask coverage (% of non-zero pixels) is within an expected range |
| **Assert Mask Binary** | Check that all mask values are exactly 0 or 1 (hard mask) |
| **Assert Mask Fuzzy** | Check that a soft-edge mask has a controlled amount of grey boundary pixels |
| **Assert String Contains** | Check that a string contains an expected substring |
| **Assert String Not Contains** | Check that a string does NOT contain a forbidden substring |
| **Assert String Length** | Check that a string's length is within min/max bounds |
| **Assert String Match** | Check that a string matches a pattern (exact or regex) |
| **Assert In Range** | Check if all tensor values are within min/max bounds |

### Utilities

| Node | Description |
|------|-------------|
| **Test Image Generator** | Generate test images: solid black, white, noise, or face pattern |
| **Test Image Generator** | Generate test images: solid black, white, noise, face, synthetic benchmark pattern, or video benchmark pattern |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the additions to this node 👍

| **Test Mask Generator** | Generate test masks: solid, circle, gradient, checkerboard, noise, or half masks |

## Usage

Expand Down
76 changes: 51 additions & 25 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,67 @@
from __future__ import annotations

import importlib
import inspect
import pkgutil
import traceback

from comfy_api.latest import ComfyExtension, io
from .nodes import (
TestImageGenerator,
TestDefinition,
AssertExecuted,
AssertEqual,
AssertNotEqual,
AssertImageMatch,
AssertContainsColor,
AssertTensorShape,
AssertInRange,
)

PREFIX = "[ComfyUI Test Framework]"

# Register web directory for frontend extensions
WEB_DIRECTORY = "web"


def collect_nodes(module) -> list[type[io.ComfyNode]]:
"""Find all io.ComfyNode subclasses defined in a module."""
return [
obj for _, obj in inspect.getmembers(module, inspect.isclass)
if issubclass(obj, io.ComfyNode)
and obj is not io.ComfyNode
and obj.__module__ == module.__name__
]


def load_nodes() -> list[type[io.ComfyNode]]:
"""Discover and load all io.ComfyNode subclasses from the nodes/ package."""
node_classes: list[type[io.ComfyNode]] = []
failed: list[tuple[str, Exception]] = []

try:
nodes_pkg = importlib.import_module(".nodes", package=__name__)
except Exception as e:
print(f"{PREFIX} Failed to import nodes package: {e}")
traceback.print_exc()
return node_classes

for _, name, _ in pkgutil.walk_packages(nodes_pkg.__path__, prefix=nodes_pkg.__name__ + "."):
try:
mod = importlib.import_module(name)
node_classes.extend(collect_nodes(mod))
except Exception as e:
failed.append((name, e))
traceback.print_exc()

node_names = ", ".join(cls.__name__ for cls in node_classes)
print(f"{PREFIX} Loaded {len(node_classes)} nodes: {node_names}")

if failed:
for name, err in failed:
print(f"{PREFIX} FAILED to load {name}: {type(err).__name__}: {err}")

return node_classes


_discovered_nodes = load_nodes()


class TestFrameworkExtension(ComfyExtension):
"""Extension for testing framework nodes"""

async def get_node_list(self) -> list[type[io.ComfyNode]]:
"""Return all test framework nodes"""
return [
# Utilities
TestImageGenerator,
TestDefinition,
# Assertions
AssertExecuted,
AssertEqual,
AssertNotEqual,
AssertImageMatch,
AssertContainsColor,
AssertTensorShape,
AssertInRange,
]
"""Return all discovered test framework nodes"""
return _discovered_nodes


async def comfy_entrypoint() -> ComfyExtension:
Expand Down
Loading
Loading