193 lines
5.8 KiB
Python
193 lines
5.8 KiB
Python
"""Pytest configuration and fixtures for ComfyUI-GeometryPack tests."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
import numpy as np
|
|
import trimesh
|
|
|
|
# Add parent directory to path so we can import nodes
|
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
# Set up paths
|
|
TEST_DIR = Path(__file__).parent.parent # Go up to tests/ directory
|
|
ASSETS_DIR = TEST_DIR / "assets"
|
|
OUTPUT_DIR = TEST_DIR / "outputs"
|
|
MESHES_DIR = OUTPUT_DIR / "meshes"
|
|
RENDERS_DIR = OUTPUT_DIR / "renders"
|
|
|
|
# Mock ComfyUI modules before importing nodes
|
|
sys.modules["folder_paths"] = MagicMock()
|
|
sys.modules["folder_paths"].get_input_directory = MagicMock(
|
|
return_value=str(ASSETS_DIR)
|
|
)
|
|
sys.modules["folder_paths"].get_output_directory = MagicMock(
|
|
return_value=str(MESHES_DIR)
|
|
)
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Register custom markers."""
|
|
config.addinivalue_line("markers", "unit: Unit tests (fast, no optional dependencies)")
|
|
config.addinivalue_line("markers", "optional: Tests requiring optional dependencies")
|
|
config.addinivalue_line("markers", "blender: Tests requiring Blender")
|
|
config.addinivalue_line("markers", "slow: Slow tests (heavy processing)")
|
|
config.addinivalue_line("markers", "browser: Browser automation tests (require Selenium + browser + ComfyUI running)")
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def setup_output_dirs():
|
|
"""Create output directories for test artifacts."""
|
|
MESHES_DIR.mkdir(parents=True, exist_ok=True)
|
|
RENDERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_assets_dir():
|
|
"""Return path to test assets directory."""
|
|
return ASSETS_DIR
|
|
|
|
|
|
@pytest.fixture
|
|
def test_output_dir(request):
|
|
"""Return path to test-specific output directory."""
|
|
# Create a directory for this specific test
|
|
test_name = request.node.name
|
|
test_dir = OUTPUT_DIR / test_name
|
|
test_dir.mkdir(parents=True, exist_ok=True)
|
|
return test_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def meshes_output_dir(test_output_dir):
|
|
"""Return path to test-specific meshes directory.
|
|
|
|
For I/O tests, this returns the mocked ComfyUI output directory
|
|
where SaveMesh actually saves files.
|
|
"""
|
|
# Return the mocked output directory that SaveMesh uses
|
|
# This is where files are actually saved via folder_paths.get_output_directory()
|
|
return Path(sys.modules["folder_paths"].get_output_directory())
|
|
|
|
|
|
@pytest.fixture
|
|
def renders_output_dir(test_output_dir):
|
|
"""Return path to test-specific renders directory."""
|
|
render_dir = test_output_dir / "renders"
|
|
render_dir.mkdir(exist_ok=True)
|
|
return render_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def bunny_mesh():
|
|
"""Load Stanford Bunny mesh."""
|
|
bunny_path = ASSETS_DIR / "Stanford_Bunny.stl"
|
|
if bunny_path.exists():
|
|
return trimesh.load(str(bunny_path))
|
|
return None
|
|
|
|
|
|
@pytest.fixture
|
|
def cube_mesh():
|
|
"""Create a simple cube mesh."""
|
|
return trimesh.creation.box(extents=[1.0, 1.0, 1.0])
|
|
|
|
|
|
@pytest.fixture
|
|
def sphere_mesh():
|
|
"""Create a simple sphere mesh."""
|
|
return trimesh.creation.icosphere(subdivisions=2, radius=1.0)
|
|
|
|
|
|
@pytest.fixture
|
|
def open_mesh():
|
|
"""Create an open mesh (cylinder without caps) for UV unwrapping tests."""
|
|
# Create a cylinder and remove the caps to make it open
|
|
cylinder = trimesh.creation.cylinder(radius=1.0, height=2.0, sections=16)
|
|
|
|
# Identify and remove cap faces (faces at top/bottom with normals pointing up/down)
|
|
face_normals = cylinder.face_normals
|
|
# Cap faces have normals nearly vertical (pointing up or down)
|
|
is_cap = np.abs(face_normals[:, 2]) > 0.9 # Z component dominates
|
|
|
|
# Keep only side faces
|
|
side_faces = cylinder.faces[~is_cap]
|
|
|
|
# Create open mesh with only side faces and clean it up
|
|
open_cylinder = trimesh.Trimesh(vertices=cylinder.vertices, faces=side_faces, process=True)
|
|
|
|
# Remove unreferenced vertices
|
|
open_cylinder.remove_unreferenced_vertices()
|
|
|
|
return open_cylinder
|
|
|
|
|
|
def render_mesh_to_png(mesh, output_path, resolution=(800, 800)):
|
|
"""Render mesh to PNG file for visual verification."""
|
|
try:
|
|
import matplotlib.pyplot as plt
|
|
from mpl_toolkits.mplot3d import Axes3D
|
|
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
|
|
fig = plt.figure(figsize=(8, 8))
|
|
ax = fig.add_subplot(111, projection='3d')
|
|
|
|
# Create mesh collection
|
|
collection = Poly3DCollection(
|
|
mesh.vertices[mesh.faces],
|
|
alpha=0.7,
|
|
facecolor='cyan',
|
|
edgecolor='black',
|
|
linewidths=0.1
|
|
)
|
|
ax.add_collection3d(collection)
|
|
|
|
# Set axis limits
|
|
scale = mesh.vertices.flatten()
|
|
ax.auto_scale_xyz(scale, scale, scale)
|
|
|
|
# Labels
|
|
ax.set_xlabel('X')
|
|
ax.set_ylabel('Y')
|
|
ax.set_zlabel('Z')
|
|
|
|
plt.savefig(output_path, dpi=100, bbox_inches='tight')
|
|
plt.close()
|
|
return True
|
|
except Exception as e:
|
|
print(f"Warning: Could not render mesh: {e}")
|
|
return False
|
|
|
|
|
|
@pytest.fixture
|
|
def render_helper(renders_output_dir):
|
|
"""Helper fixture for rendering meshes."""
|
|
def _render(mesh, name, **kwargs):
|
|
output_path = renders_output_dir / f"{name}.png"
|
|
render_mesh_to_png(mesh, output_path, **kwargs)
|
|
return output_path
|
|
return _render
|
|
|
|
|
|
@pytest.fixture
|
|
def save_mesh_helper(meshes_output_dir):
|
|
"""Helper fixture for saving meshes."""
|
|
def _save(mesh, name, format="obj"):
|
|
output_path = meshes_output_dir / f"{name}.{format}"
|
|
mesh.export(str(output_path))
|
|
return output_path
|
|
return _save
|
|
|
|
|
|
@pytest.fixture
|
|
def save_and_render(save_mesh_helper, render_helper):
|
|
"""Combined helper to save mesh and render it."""
|
|
def _save_and_render(mesh, name, format="obj"):
|
|
save_mesh_helper(mesh, name, format)
|
|
render_helper(mesh, name)
|
|
return True
|
|
return _save_and_render
|