# Mock bpy BEFORE test collection can import src modules import sys from unittest.mock import Mock, MagicMock # Create comprehensive bpy mock bpy_mock = MagicMock() bpy_mock.types = MagicMock() bpy_mock.props = MagicMock() bpy_mock.utils = MagicMock() bpy_mock.ops = MagicMock() bpy_mock.context = MagicMock() bpy_mock.data = MagicMock() bpy_mock.app = MagicMock() # Set up mock attributes that src code needs bpy_mock.types.Operator = Mock bpy_mock.types.Panel = Mock bpy_mock.types.PropertyGroup = Mock bpy_mock.types.UIList = Mock bpy_mock.types.Scene = Mock() bpy_mock.types.NODE_MT_add = Mock() bpy_mock.types.VIEW3D_MT_add = Mock() bpy_mock.types.VIEW3D_MT_mesh_add = Mock() bpy_mock.types.NODE_MT_node = Mock() bpy_mock.types.IMAGE_MT_image = Mock() bpy_mock.props.StringProperty = Mock bpy_mock.props.IntProperty = Mock bpy_mock.props.FloatVectorProperty = Mock bpy_mock.props.FloatProperty = Mock bpy_mock.props.BoolProperty = Mock bpy_mock.props.EnumProperty = Mock bpy_mock.props.CollectionProperty = Mock bpy_mock.props.PointerProperty = Mock bpy_mock.utils.register_class = Mock() bpy_mock.utils.unregister_class = Mock() bpy_mock.app.handlers = Mock() bpy_mock.app.handlers.load_post = [] bpy_mock.app.handlers.persistent = lambda f: f bpy_mock.context.window_manager = Mock() bpy_mock.context.scene = Mock() # Mock ALL Blender modules at import time sys.modules['bpy'] = bpy_mock sys.modules['bmesh'] = MagicMock() sys.modules['mathutils'] = MagicMock() sys.modules['bpy.types'] = bpy_mock.types sys.modules['bpy.props'] = bpy_mock.props sys.modules['bpy.utils'] = bpy_mock.utils sys.modules['bpy.ops'] = bpy_mock.ops sys.modules['bpy.app'] = bpy_mock.app sys.modules['bpy.context'] = bpy_mock.context """ Pytest configuration and fixtures for Text Texture Generator tests. Provides Blender mocking and common test utilities. """ import pytest from PIL import Image, ImageDraw, ImageFont class MockBlenderData: """Mock for bpy.data""" def __init__(self): self.images = MockImageCollection() class MockImageCollection: """Mock for bpy.data.images collection""" def __init__(self): self._images = {} def new(self, name, width, height, alpha=True): """Create a new mock Blender image""" mock_image = MockBlenderImage(name, width, height, alpha) self._images[name] = mock_image return mock_image def remove(self, image): """Remove image from collection""" if hasattr(image, 'name') and image.name in self._images: del self._images[image.name] def __contains__(self, name): return name in self._images def __getitem__(self, name): return self._images[name] class MockBlenderImage: """Mock for Blender image objects""" def __init__(self, name, width, height, alpha=True): self.name = name self.width = width self.height = height self.alpha = alpha self.pixels = [0.0] * (width * height * 4) # RGBA pixels self.packed_file = None def pack(self): """Mock pack method""" self.packed_file = Mock() # Attach mock data to bpy_mock bpy_mock.data = MockBlenderData() @pytest.fixture(autouse=True) def mock_bpy_fixture(): """Provide bpy mock to tests (already mocked at import time).""" yield bpy_mock @pytest.fixture def sample_font(): """Provide a sample font for testing""" try: # Try to use default font return ImageFont.load_default() except Exception: # Create a minimal mock font if default fails mock_font = Mock() mock_font.getmetrics.return_value = (10, 2) # ascent, descent return mock_font @pytest.fixture def sample_text_short(): """Short sample text for testing""" return "Hello World" @pytest.fixture def sample_text_long(): """Long sample text for testing text wrapping""" return "This is a much longer piece of text that should wrap across multiple lines when constrained by width limits." @pytest.fixture def sample_text_multiline(): """Sample text containing newlines for testing""" return "Line 1\nLine 2\nLine 3\nLine 4" @pytest.fixture def standard_dimensions(): """Standard width/height dimensions for testing""" return {"width": 512, "height": 512} @pytest.fixture def mock_pil_image(): """Mock PIL Image for testing""" from unittest.mock import patch with patch('PIL.Image.new') as mock_new: mock_img = Mock() mock_img.size = (512, 512) mock_img.width = 512 mock_img.height = 512 mock_img.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) mock_img.transpose.return_value = mock_img mock_new.return_value = mock_img yield mock_img @pytest.fixture def mock_image_draw(): """Mock PIL ImageDraw for testing""" from unittest.mock import patch with patch('PIL.ImageDraw.Draw') as mock_draw_class: mock_draw = Mock() mock_draw.textbbox.return_value = (0, 0, 100, 20) # x1, y1, x2, y2 mock_draw.text.return_value = None mock_draw_class.return_value = mock_draw yield mock_draw @pytest.fixture def available_area_small(): """Small available area for testing""" return { 'x': 10, 'y': 10, 'width': 100, 'height': 50, 'area': 5000 } @pytest.fixture def available_area_large(): """Large available area for testing""" return { 'x': 0, 'y': 0, 'width': 500, 'height': 400, 'area': 200000 } @pytest.fixture def text_positions_no_overlap(): """Text positions that don't overlap""" return [ {'x': 0, 'y': 0, 'width': 100, 'height': 50}, {'x': 150, 'y': 0, 'width': 100, 'height': 50}, {'x': 0, 'y': 100, 'width': 100, 'height': 50} ] @pytest.fixture def text_positions_with_overlap(): """Text positions that overlap""" return [ {'x': 0, 'y': 0, 'width': 100, 'height': 50}, {'x': 50, 'y': 25, 'width': 100, 'height': 50}, # Overlaps first {'x': 200, 'y': 0, 'width': 100, 'height': 50} ] @pytest.fixture def overlays_list(): """Sample overlays for area calculation""" return [ {'x': 50, 'y': 50, 'width': 100, 'height': 100}, {'x': 200, 'y': 200, 'width': 80, 'height': 80} ] @pytest.fixture def mock_props(): """Mock properties object for generation engine tests""" props = Mock() props.text = "Test Text" props.prepend_text = "" props.append_text = "" props.background_transparent = False props.background_color = (1.0, 1.0, 1.0, 1.0) # White props.text_color = (0.0, 0.0, 0.0, 1.0) # Black props.font_size = 24 props.font_path = "default" props.use_custom_font = False props.custom_font_path = "" props.enable_text_fitting = False props.min_font_size = 8 props.max_font_size = 200 props.text_fitting_margin = 10.0 props.prepend_append_layout = 'HORIZONTAL' props.prepend_margin = 10 props.append_margin = 10 props.generate_normal_map = False props.normal_map_strength = 1.0 props.normal_map_blur_radius = 0 props.normal_map_invert = False return props