added new tests

This commit is contained in:
Andrea Pozzetti
2025-11-14 18:28:20 +00:00
parent 80cca20d4d
commit 2a040b06fc
5 changed files with 526 additions and 72 deletions
+7 -3
View File
@@ -9,15 +9,19 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
@@ -44,5 +48,5 @@ jobs:
- name: Test summary
if: always()
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "## Test Results (Python ${{ matrix.python-version }})" >> $GITHUB_STEP_SUMMARY
echo "✅ Unit tests completed" >> $GITHUB_STEP_SUMMARY
+58
View File
@@ -0,0 +1,58 @@
name: Windows Tests
on:
push:
branches: [main, master, dev]
pull_request:
branches: [main, master, dev]
jobs:
test-windows:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-mock pytest-asyncio
pip install -r .github/requirements-ci.txt
pip install -r requirements-dev.txt
- name: Run smoke test (import check)
run: |
python -c "import sys; sys.path.insert(0, '.'); print('Testing module import...'); exec(open('tests/smoke_test.py').read())"
env:
PYTHONPATH: ${{ github.workspace }}
- name: Run unit tests
run: |
pytest tests/unit/ -v --cov=nodes --cov-report=xml --cov-report=term-missing
env:
PYTHONPATH: ${{ github.workspace }}
- name: Upload coverage reports
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: windows-tests
name: codecov-windows-py${{ matrix.python-version }}
fail_ci_if_error: false
- name: Test summary
if: always()
run: |
echo "## Windows Test Results (Python ${{ matrix.python-version }})" >> $env:GITHUB_STEP_SUMMARY
echo "✅ Windows tests completed" >> $env:GITHUB_STEP_SUMMARY
+59 -13
View File
@@ -7,23 +7,69 @@ Now includes SAM2 segmentation nodes
# Only run initialization and imports when loaded by ComfyUI, not during pytest
# This prevents relative import errors when pytest collects test modules
import sys
import traceback
# Track initialization status
INIT_SUCCESS = False
INIT_ERRORS = []
if 'pytest' not in sys.modules:
from .grounding_init import init
# Initialize web extensions for compatibility with older ComfyUI versions
init()
print("[ComfyUI-Grounding] Initializing custom node...")
# Step 1: Run initialization script
try:
from .grounding_init import init
# Initialize web extensions for compatibility with older ComfyUI versions
init()
print("[ComfyUI-Grounding] ✓ Initialization script completed")
except Exception as e:
error_msg = f"Initialization script failed: {str(e)}"
INIT_ERRORS.append(error_msg)
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
# Step 2: Import node classes
try:
from .nodes import (
GroundingModelLoader,
GroundingDetector,
BboxVisualizer,
DownLoadSAM2Model,
Sam2Segment,
GroundingMaskModelLoader,
GroundingMaskDetector,
BatchCropAndPadFromMask,
)
print("[ComfyUI-Grounding] ✓ Node classes imported successfully")
INIT_SUCCESS = True
except Exception as e:
error_msg = f"Failed to import node classes: {str(e)}"
INIT_ERRORS.append(error_msg)
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
# Set all to None if import failed
GroundingModelLoader = None
GroundingDetector = None
BboxVisualizer = None
DownLoadSAM2Model = None
Sam2Segment = None
GroundingMaskModelLoader = None
GroundingMaskDetector = None
BatchCropAndPadFromMask = None
# Report final status
if INIT_SUCCESS:
print("[ComfyUI-Grounding] ✅ Loaded successfully!")
else:
print(f"[ComfyUI-Grounding] ❌ Failed to load ({len(INIT_ERRORS)} error(s)):")
for error in INIT_ERRORS:
print(f" - {error}")
print("[ComfyUI-Grounding] Please check the errors above and your installation.")
from .nodes import (
GroundingModelLoader,
GroundingDetector,
BboxVisualizer,
DownLoadSAM2Model,
Sam2Segment,
GroundingMaskModelLoader,
GroundingMaskDetector,
BatchCropAndPadFromMask,
)
else:
# During testing, set dummy values
print("[ComfyUI-Grounding] Running in pytest mode - skipping initialization")
GroundingModelLoader = None
GroundingDetector = None
BboxVisualizer = None
+238 -56
View File
@@ -3,23 +3,138 @@ ComfyUI-Grounding: Unified object detection nodes for ComfyUI
Reorganized architecture with thin wrapper nodes delegating to model implementations
"""
import torch
import numpy as np
import random
import os
import sys
import traceback
# Track import failures for diagnostics
IMPORT_ERRORS = []
def log_import_error(module_name, error):
"""Log import errors for diagnostics"""
error_msg = f"Failed to import {module_name}: {str(error)}"
IMPORT_ERRORS.append(error_msg)
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
def log_import_success(module_name):
"""Log successful imports"""
print(f"[ComfyUI-Grounding] ✓ Imported {module_name}")
# Basic Python dependencies
print("[ComfyUI-Grounding] Loading nodes module...")
try:
import torch
import numpy as np
import random
import os
log_import_success("basic dependencies (torch, numpy, etc.)")
except ImportError as e:
log_import_error("basic dependencies", e)
raise
# Import configurations
from .config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
from .utils.cache import MODEL_CACHE
from .utils import draw_boxes, boxes_to_masks
try:
from .config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
log_import_success("config (MODEL_REGISTRY, MASK_MODEL_REGISTRY)")
except Exception as e:
log_import_error("config", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
raise
# Import model loaders and detectors
from . import grounding_dino, owlv2, florence2, yolo_world, florence2_seg, sa2va
from . import sam2, visualizer
# Import utils
try:
from .utils.cache import MODEL_CACHE
log_import_success("utils.cache")
except Exception as e:
log_import_error("utils.cache", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
raise
try:
from .utils import draw_boxes, boxes_to_masks
log_import_success("utils (draw_boxes, boxes_to_masks)")
except Exception as e:
log_import_error("utils", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
raise
# Import model modules - each wrapped separately for better diagnostics
grounding_dino = None
owlv2 = None
florence2 = None
yolo_world = None
florence2_seg = None
sa2va = None
sam2 = None
visualizer = None
try:
from . import grounding_dino
log_import_success("grounding_dino")
except Exception as e:
log_import_error("grounding_dino", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import owlv2
log_import_success("owlv2")
except Exception as e:
log_import_error("owlv2", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import florence2
log_import_success("florence2")
except Exception as e:
log_import_error("florence2", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import yolo_world
log_import_success("yolo_world")
except Exception as e:
log_import_error("yolo_world", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import florence2_seg
log_import_success("florence2_seg")
except Exception as e:
log_import_error("florence2_seg", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import sa2va
log_import_success("sa2va")
except Exception as e:
log_import_error("sa2va", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import sam2
log_import_success("sam2")
except Exception as e:
log_import_error("sam2", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
try:
from . import visualizer
log_import_success("visualizer")
except Exception as e:
log_import_error("visualizer", e)
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
# Get the ComfyUI-Grounding root directory (parent of nodes/)
script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Report import status
if IMPORT_ERRORS:
print(f"[ComfyUI-Grounding] ⚠️ {len(IMPORT_ERRORS)} import error(s) occurred:")
for error in IMPORT_ERRORS:
print(f" - {error}")
print("[ComfyUI-Grounding] Some nodes may not function correctly.")
else:
print("[ComfyUI-Grounding] ✓ All modules imported successfully")
# ============================================================================
# Bbox Detection Nodes
@@ -33,20 +148,22 @@ class GroundingModelLoader:
@classmethod
def INPUT_TYPES(cls):
optional_params = {}
# Only add model-specific params if modules loaded successfully
if grounding_dino is not None:
optional_params.update(grounding_dino.params.LOADER_PARAMS.get("optional", {}))
if florence2 is not None:
optional_params.update(florence2.params.LOADER_PARAMS.get("optional", {}))
if yolo_world is not None:
optional_params.update(yolo_world.params.LOADER_PARAMS.get("optional", {}))
return {
"required": {
"model": (list(MODEL_REGISTRY.keys()), {
"default": "Florence-2: Base (0.23B params)",
}),
},
"optional": {
# GroundingDINO parameters
**grounding_dino.params.LOADER_PARAMS.get("optional", {}),
# Florence-2 parameters
**florence2.params.LOADER_PARAMS.get("optional", {}),
# YOLO-World parameters (none currently in loader)
**yolo_world.params.LOADER_PARAMS.get("optional", {}),
}
"optional": optional_params
}
RETURN_TYPES = ("GROUNDING_MODEL",)
@@ -69,12 +186,20 @@ class GroundingModelLoader:
# Route to appropriate loader
if model_type == "grounding_dino":
if grounding_dino is None:
raise RuntimeError(f"Cannot load {model}: grounding_dino module failed to import. Check console for import errors.")
model_dict = grounding_dino.load_grounding_dino(model, config)
elif model_type == "owlv2":
if owlv2 is None:
raise RuntimeError(f"Cannot load {model}: owlv2 module failed to import. Check console for import errors.")
model_dict = owlv2.load_owlv2(model, config)
elif model_type == "florence2":
if florence2 is None:
raise RuntimeError(f"Cannot load {model}: florence2 module failed to import. Check console for import errors.")
model_dict = florence2.load_florence2(model, config)
elif model_type == "yolo_world":
if yolo_world is None:
raise RuntimeError(f"Cannot load {model}: yolo_world module failed to import. Check console for import errors.")
model_dict = yolo_world.load_yolo_world(model, config)
else:
raise ValueError(f"Unknown model type: {model_type}")
@@ -94,6 +219,35 @@ class GroundingDetector:
@classmethod
def INPUT_TYPES(cls):
optional_params = {
"single_box_mode": ("BOOLEAN", {
"default": False,
"tooltip": "Return only the highest-scoring detection. Use for referring expressions (e.g., 'the red car on the left')"
}),
"single_box_per_prompt_mode": ("BOOLEAN", {
"default": False,
"tooltip": "Return highest-scoring detection for each prompt/label (e.g., 'banana. orange' returns best banana and best orange). Ignored if single_box_mode is True"
}),
"bbox_output_format": (["list_only", "dict_with_data"], {
"default": "list_only",
"tooltip": "list_only: SAM2-compatible | dict_with_data: includes labels/scores"
}),
"seed": ("INT", {
"default": 42,
"min": 0,
"max": 0xffffffff, # 2^32 - 1, max for numpy.random.seed
"tooltip": "Fixed seed for reproducible results (affects mask visualization colors and model randomness)"
}),
}
# Only add model-specific params if modules loaded successfully
if grounding_dino is not None:
optional_params.update(grounding_dino.params.DETECTOR_PARAMS.get("optional", {}))
if florence2 is not None:
optional_params.update(florence2.params.DETECTOR_PARAMS.get("optional", {}))
if yolo_world is not None:
optional_params.update(yolo_world.params.DETECTOR_PARAMS.get("optional", {}))
return {
"required": {
"model": ("GROUNDING_MODEL",),
@@ -111,32 +265,7 @@ class GroundingDetector:
"tooltip": "Confidence threshold for detections. Typical: 0.2-0.35 (permissive), 0.35-0.5 (balanced), 0.5+ (strict)"
}),
},
"optional": {
"single_box_mode": ("BOOLEAN", {
"default": False,
"tooltip": "Return only the highest-scoring detection. Use for referring expressions (e.g., 'the red car on the left')"
}),
"single_box_per_prompt_mode": ("BOOLEAN", {
"default": False,
"tooltip": "Return highest-scoring detection for each prompt/label (e.g., 'banana. orange' returns best banana and best orange). Ignored if single_box_mode is True"
}),
"bbox_output_format": (["list_only", "dict_with_data"], {
"default": "list_only",
"tooltip": "list_only: SAM2-compatible | dict_with_data: includes labels/scores"
}),
# GroundingDINO parameters
**grounding_dino.params.DETECTOR_PARAMS.get("optional", {}),
# Florence-2 parameters
**florence2.params.DETECTOR_PARAMS.get("optional", {}),
# YOLO-World parameters
**yolo_world.params.DETECTOR_PARAMS.get("optional", {}),
"seed": ("INT", {
"default": 42,
"min": 0,
"max": 0xffffffff, # 2^32 - 1, max for numpy.random.seed
"tooltip": "Fixed seed for reproducible results (affects mask visualization colors and model randomness)"
}),
}
"optional": optional_params
}
RETURN_TYPES = ("BBOX", "IMAGE", "STRING", "MASK")
@@ -167,22 +296,30 @@ class GroundingDetector:
# Route to appropriate detection method
if model_type == "grounding_dino":
if grounding_dino is None:
raise RuntimeError(f"Cannot detect with {model_type}: grounding_dino module failed to import. Check console for import errors.")
return grounding_dino.detect_grounding_dino(
model, image, prompt, confidence_threshold, text_threshold,
single_box_mode, single_box_per_prompt_mode, bbox_output_format, output_masks, self._format_output
)
elif model_type == "owlv2":
if owlv2 is None:
raise RuntimeError(f"Cannot detect with {model_type}: owlv2 module failed to import. Check console for import errors.")
return owlv2.detect_owlv2(
model, image, prompt, confidence_threshold,
single_box_mode, single_box_per_prompt_mode, bbox_output_format, output_masks, self._format_output
)
elif model_type == "florence2":
if florence2 is None:
raise RuntimeError(f"Cannot detect with {model_type}: florence2 module failed to import. Check console for import errors.")
return florence2.detect_florence2(
model, image, prompt, confidence_threshold, single_box_mode,
single_box_per_prompt_mode, bbox_output_format, output_masks, florence2_max_tokens,
florence2_num_beams, self._format_output
)
elif model_type == "yolo_world":
if yolo_world is None:
raise RuntimeError(f"Cannot detect with {model_type}: yolo_world module failed to import. Check console for import errors.")
return yolo_world.detect_yolo_world(
model, image, prompt, confidence_threshold, single_box_mode,
single_box_per_prompt_mode, bbox_output_format, output_masks, yolo_iou, yolo_agnostic_nms,
@@ -237,18 +374,20 @@ class GroundingMaskModelLoader:
@classmethod
def INPUT_TYPES(cls):
optional_params = {}
# Only add model-specific params if modules loaded successfully
if florence2_seg is not None:
optional_params.update(florence2_seg.params.LOADER_PARAMS.get("optional", {}))
if sa2va is not None:
optional_params.update(sa2va.params.LOADER_PARAMS.get("optional", {}))
return {
"required": {
"model": (list(MASK_MODEL_REGISTRY.keys()), {
"default": "Florence-2: Base (Segmentation)",
}),
},
"optional": {
# Florence-2 Seg parameters
**florence2_seg.params.LOADER_PARAMS.get("optional", {}),
# SA2VA parameters
**sa2va.params.LOADER_PARAMS.get("optional", {}),
}
"optional": optional_params
}
RETURN_TYPES = ("MASK_MODEL",)
@@ -271,8 +410,12 @@ class GroundingMaskModelLoader:
# Route to appropriate loader
if model_type == "florence2_seg":
if florence2_seg is None:
raise RuntimeError(f"Cannot load {model}: florence2_seg module failed to import. Check console for import errors.")
model_dict = florence2_seg.load_florence2_seg(model, config)
elif model_type == "sa2va":
if sa2va is None:
raise RuntimeError(f"Cannot load {model}: sa2va module failed to import. Check console for import errors.")
model_dict = sa2va.load_sa2va(model, config, sa2va_dtype)
elif model_type == "lisa":
raise NotImplementedError("LISA support coming soon")
@@ -375,12 +518,16 @@ class GroundingMaskDetector:
# Route to appropriate detection method
if model_type == "florence2_seg":
if florence2_seg is None:
raise RuntimeError(f"Cannot detect with {model_type}: florence2_seg module failed to import. Check console for import errors.")
return florence2_seg.detect_florence2_seg(
model, image, prompt, confidence_threshold,
florence2_max_tokens, florence2_num_beams,
self._format_output
)
elif model_type == "sa2va":
if sa2va is None:
raise RuntimeError(f"Cannot detect with {model_type}: sa2va module failed to import. Check console for import errors.")
return sa2va.detect_sa2va(
model, image, prompt, confidence_threshold,
sa2va_max_tokens, sa2va_num_beams, self._format_output
@@ -452,15 +599,27 @@ class GroundingMaskDetector:
class DownLoadSAM2Model:
@classmethod
def INPUT_TYPES(s):
if sam2 is None:
# Return minimal params if sam2 failed to import
return {
"required": {
"model": (["sam2_model"], {}),
"segmentor": (["auto"], {}),
"device": (["auto"], {}),
"precision": (["auto"], {}),
}
}
return sam2.params.LOADER_PARAMS
RETURN_TYPES = sam2.params.LOADER_RETURN_TYPES
RETURN_NAMES = sam2.params.LOADER_RETURN_NAMES
RETURN_TYPES = ("SAM2_MODEL",) if sam2 is None else sam2.params.LOADER_RETURN_TYPES
RETURN_NAMES = ("model",) if sam2 is None else sam2.params.LOADER_RETURN_NAMES
FUNCTION = "loadmodel"
CATEGORY = "SAM2"
def loadmodel(self, model, segmentor, device, precision):
"""Load SAM2 model"""
if sam2 is None:
raise RuntimeError("Cannot load SAM2 model: sam2 module failed to import. Check console for import errors.")
sam2_model = sam2.load_sam2(model, segmentor, device, precision, script_directory)
return (sam2_model,)
@@ -471,10 +630,19 @@ class Sam2Segment:
@classmethod
def INPUT_TYPES(s):
if sam2 is None:
# Return minimal params if sam2 failed to import
return {
"required": {
"image": ("IMAGE",),
"sam2_model": ("SAM2_MODEL",),
"keep_model_loaded": ("BOOLEAN", {"default": True}),
}
}
return sam2.params.SEGMENTATION_PARAMS
RETURN_TYPES = sam2.params.SEGMENTATION_RETURN_TYPES
RETURN_NAMES = sam2.params.SEGMENTATION_RETURN_NAMES
RETURN_TYPES = ("MASK",) if sam2 is None else sam2.params.SEGMENTATION_RETURN_TYPES
RETURN_NAMES = ("masks",) if sam2 is None else sam2.params.SEGMENTATION_RETURN_NAMES
FUNCTION = "segment"
CATEGORY = "SAM2"
@@ -482,6 +650,9 @@ class Sam2Segment:
coordinates_negative=None, individual_objects=False, bboxes=None,
mask=None, mask_threshold=0.0, max_hole_area=0.0, max_sprinkle_area=0.0):
"""Perform SAM2 segmentation"""
if sam2 is None:
raise RuntimeError("Cannot perform SAM2 segmentation: sam2 module failed to import. Check console for import errors.")
# Create a reference dict to hold inference_state
inference_state_ref = {'state': self.inference_state}
@@ -508,15 +679,26 @@ class BboxVisualizer:
@classmethod
def INPUT_TYPES(cls):
if visualizer is None:
# Return minimal params if visualizer failed to import
return {
"required": {
"image": ("IMAGE",),
"bboxes": ("BBOX",),
"line_width": ("INT", {"default": 3, "min": 1, "max": 20}),
}
}
return visualizer.params.VISUALIZER_PARAMS
RETURN_TYPES = visualizer.params.RETURN_TYPES
RETURN_NAMES = visualizer.params.RETURN_NAMES
RETURN_TYPES = ("IMAGE",) if visualizer is None else visualizer.params.RETURN_TYPES
RETURN_NAMES = ("image",) if visualizer is None else visualizer.params.RETURN_NAMES
FUNCTION = "visualize"
CATEGORY = "grounding"
def visualize(self, image, bboxes, line_width=3):
"""Draw bounding boxes on image (supports batches)"""
if visualizer is None:
raise RuntimeError("Cannot visualize bboxes: visualizer module failed to import. Check console for import errors.")
return visualizer.visualize_bboxes(image, bboxes, line_width)
+164
View File
@@ -0,0 +1,164 @@
"""
Smoke test for ComfyUI-Grounding
Tests basic module import functionality without requiring ComfyUI to be installed
"""
import sys
import traceback
def test_basic_imports():
"""Test that basic Python dependencies can be imported"""
print("Testing basic dependencies...")
try:
import torch
print(f" ✓ torch {torch.__version__}")
except ImportError as e:
print(f" ✗ torch: {e}")
return False
try:
import numpy as np
print(f" ✓ numpy {np.__version__}")
except ImportError as e:
print(f" ✗ numpy: {e}")
return False
try:
from PIL import Image
print(f" ✓ PIL")
except ImportError as e:
print(f" ✗ PIL: {e}")
return False
return True
def test_config_imports():
"""Test that config modules can be imported"""
print("\nTesting config imports...")
try:
from nodes.config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
print(f" ✓ Config imported ({len(MODEL_REGISTRY)} bbox models, {len(MASK_MODEL_REGISTRY)} mask models)")
return True
except Exception as e:
print(f" ✗ Config import failed: {e}")
traceback.print_exc()
return False
def test_utils_imports():
"""Test that utility modules can be imported"""
print("\nTesting utils imports...")
try:
from nodes.utils.cache import MODEL_CACHE
print(f" ✓ Cache imported")
except Exception as e:
print(f" ✗ Cache import failed: {e}")
traceback.print_exc()
return False
try:
from nodes.utils import draw_boxes, boxes_to_masks
print(f" ✓ Utils imported")
except Exception as e:
print(f" ✗ Utils import failed: {e}")
traceback.print_exc()
return False
return True
def test_model_module_imports():
"""Test that model modules can be imported"""
print("\nTesting model module imports...")
modules = ['grounding_dino', 'owlv2', 'florence2', 'yolo_world',
'florence2_seg', 'sa2va', 'sam2', 'visualizer']
failed = []
for module_name in modules:
try:
module = __import__(f'nodes.{module_name}', fromlist=[''])
print(f" ✓ {module_name}")
except Exception as e:
print(f" ✗ {module_name}: {e}")
failed.append(module_name)
return len(failed) == 0
def test_node_class_definitions():
"""Test that node classes are defined"""
print("\nTesting node class definitions...")
try:
from nodes import (
GroundingModelLoader,
GroundingDetector,
BboxVisualizer,
DownLoadSAM2Model,
Sam2Segment,
GroundingMaskModelLoader,
GroundingMaskDetector,
BatchCropAndPadFromMask,
)
# Check they're not None
classes = {
'GroundingModelLoader': GroundingModelLoader,
'GroundingDetector': GroundingDetector,
'BboxVisualizer': BboxVisualizer,
'DownLoadSAM2Model': DownLoadSAM2Model,
'Sam2Segment': Sam2Segment,
'GroundingMaskModelLoader': GroundingMaskModelLoader,
'GroundingMaskDetector': GroundingMaskDetector,
'BatchCropAndPadFromMask': BatchCropAndPadFromMask,
}
failed = []
for name, cls in classes.items():
if cls is None:
print(f" ✗ {name} is None!")
failed.append(name)
else:
print(f" ✓ {name}")
return len(failed) == 0
except Exception as e:
print(f" ✗ Node import failed: {e}")
traceback.print_exc()
return False
def main():
"""Run all smoke tests"""
print("="*60)
print("ComfyUI-Grounding Smoke Test")
print("="*60)
results = []
# Run tests
results.append(("Basic imports", test_basic_imports()))
results.append(("Config imports", test_config_imports()))
results.append(("Utils imports", test_utils_imports()))
results.append(("Model modules", test_model_module_imports()))
results.append(("Node classes", test_node_class_definitions()))
# Summary
print("\n" + "="*60)
print("SUMMARY")
print("="*60)
all_passed = True
for name, passed in results:
status = "✓ PASS" if passed else "✗ FAIL"
print(f"{status}: {name}")
if not passed:
all_passed = False
print("="*60)
if all_passed:
print("✓ All smoke tests passed!")
sys.exit(0)
else:
print("✗ Some smoke tests failed!")
sys.exit(1)
if __name__ == "__main__":
main()