From 2a040b06fccd573fdaa16e0a6ae1a6748b22d0c4 Mon Sep 17 00:00:00 2001 From: Andrea Pozzetti Date: Fri, 14 Nov 2025 18:28:20 +0000 Subject: [PATCH] added new tests --- .github/workflows/test-unit.yml | 10 +- .github/workflows/test-windows.yml | 58 ++++++ __init__.py | 72 +++++-- nodes/__init__.py | 294 +++++++++++++++++++++++------ tests/smoke_test.py | 164 ++++++++++++++++ 5 files changed, 526 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/test-windows.yml create mode 100644 tests/smoke_test.py diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index d7d565a..49576a9 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -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 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 0000000..a339e2d --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -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 diff --git a/__init__.py b/__init__.py index 40537e0..318140e 100644 --- a/__init__.py +++ b/__init__.py @@ -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 diff --git a/nodes/__init__.py b/nodes/__init__.py index 3ce9779..bac33c9 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -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) diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 0000000..6b8bdce --- /dev/null +++ b/tests/smoke_test.py @@ -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()