""" Pytest configuration and fixtures for Text Texture Generator tests. Provides Blender mocking and common test utilities. """ import pytest import sys from unittest.mock import Mock, MagicMock, patch 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() class MockBpy: """Mock for the bpy module""" def __init__(self): self.data = MockBlenderData() @pytest.fixture(scope="session", autouse=True) def mock_bpy(): """Mock the bpy module for all tests""" # Mock bpy and related Blender modules BEFORE any imports mock_bpy_module = MockBpy() mock_bmesh = Mock() mock_bpy_types = Mock() mock_bpy_props = Mock() mock_bpy_utils = Mock() mock_bpy_app = Mock() mock_bpy_context = Mock() # Set up mock attributes mock_bpy_types.Operator = Mock mock_bpy_types.Panel = Mock mock_bpy_types.PropertyGroup = Mock mock_bpy_types.UIList = Mock mock_bpy_types.Scene = Mock() mock_bpy_props.StringProperty = Mock mock_bpy_props.IntProperty = Mock mock_bpy_props.FloatVectorProperty = Mock mock_bpy_props.FloatProperty = Mock mock_bpy_props.BoolProperty = Mock mock_bpy_props.EnumProperty = Mock mock_bpy_props.CollectionProperty = Mock mock_bpy_props.PointerProperty = Mock mock_bpy_utils.register_class = Mock() mock_bpy_utils.unregister_class = Mock() mock_bpy_app.handlers = Mock() mock_bpy_app.handlers.load_post = [] mock_bpy_app.handlers.persistent = lambda f: f mock_bpy_context.window_manager = Mock() mock_bpy_context.scene = Mock() mock_bpy_module.types = mock_bpy_types mock_bpy_module.props = mock_bpy_props mock_bpy_module.utils = mock_bpy_utils mock_bpy_module.app = mock_bpy_app mock_bpy_module.context = mock_bpy_context mocked_modules = { 'bpy': mock_bpy_module, 'bmesh': mock_bmesh, 'bpy.types': mock_bpy_types, 'bpy.props': mock_bpy_props, 'bpy.utils': mock_bpy_utils, 'bpy.app': mock_bpy_app, 'bpy.context': mock_bpy_context } with patch.dict('sys.modules', mocked_modules): yield mock_bpy_module @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(): """Multiline sample text 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""" 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""" 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