From 97950f44c1a638718066d4a2fa698f76c68f0303 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 12 Oct 2025 16:00:22 +0200 Subject: [PATCH] working --- dist/text_texture_generator_v1.0.0.zip | Bin 9671129 -> 9671129 bytes dist/text_texture_generator_v1.0.0_free.zip | Bin 9654395 -> 9654395 bytes src/__pycache__/__init__.cpython-311.pyc | Bin 12864 -> 12909 bytes .../__pycache__/constants.cpython-311.pyc | Bin 7193 -> 7193 bytes ...ration_engine.cpython-311-pytest-8.3.5.pyc | Bin 12008 -> 51487 bytes tests/unit/core/test_generation_engine.py | 837 +++++++++++++++--- tests/unit/operators/test_generation_ops.py | 536 +++++++++++ 7 files changed, 1227 insertions(+), 146 deletions(-) create mode 100644 tests/unit/operators/test_generation_ops.py diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index 21539af41c65d3a1a030238ca234c952843aae3e..3240a2fd870ab699e22264ea31dae8f3902f48f5 100644 GIT binary patch delta 4817 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 F{U6(hb!Y$p delta 4817 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 diff --git a/dist/text_texture_generator_v1.0.0_free.zip b/dist/text_texture_generator_v1.0.0_free.zip index 1b1e752156d0d6c44e0bafeb5a5a484759a44031..af4c66508de6cd185014515f4e6cc558b2951f3a 100644 GIT binary patch delta 4627 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 QtZ{)s{sY@)K|_5;0FR79c>n+a delta 185 zcmaExav+6wIWI340}x!B`#M8tBCjN))JF9-W@X+Kxg5V-|0w_5fT)1nz^K67ps1kS z;Hco-kf;zwhIEF+(8*Vr4YmDK15yLiq@%)8=?N>R#Y0-0UJ z!^DspvDtuSB{$=q&A0g9a56G(J}xcxMv2XL)!r~MMsGf)DZn)Ow{|4g gPM|hMATGYWIZo#~3nSO$L_?j)35Lp(dkr4|0HK#Tn*aa+ diff --git a/src/utils/__pycache__/constants.cpython-311.pyc b/src/utils/__pycache__/constants.cpython-311.pyc index 7fb46e3dc08dbafb2b6558aab22c0d36ccf6dbdf..ca98627a430f4cfa80b03c101eebb127805e62ff 100644 GIT binary patch delta 19 ZcmbPfG1G!;IWI340}zxg*vQ2%0{}5{1atrZ delta 19 ZcmbPfG1G!;IWI340}wRK-pIu-0{}6V1bF}e diff --git a/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc b/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc index 02a8779bde3fa77521a48c1a92c05d44e97cecda..9d7f549ea23b7428c743d02cf6f216f763461de7 100644 GIT binary patch literal 51487 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 V<=^9ZJYj|DT_(R*B$hpc{~wC7jVAyA literal 12008 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 AdH?_b diff --git a/tests/unit/core/test_generation_engine.py b/tests/unit/core/test_generation_engine.py index 19d487d..f659467 100644 --- a/tests/unit/core/test_generation_engine.py +++ b/tests/unit/core/test_generation_engine.py @@ -1,154 +1,699 @@ -"""Unit tests for texture generation engine control flow.""" +"""Unit tests for generation_engine.py - Core texture generation logic""" import pytest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import Mock, MagicMock, patch + +from src.core.generation_engine import generate_texture_image, generate_preview +# Test configuration constants +DEFAULT_WIDTH = 512 +DEFAULT_HEIGHT = 512 +SMALL_WIDTH = 256 +SMALL_HEIGHT = 256 +PREVIEW_WIDTH = 128 +PREVIEW_HEIGHT = 128 +TINY_WIDTH = 64 +TINY_HEIGHT = 64 -class TestGenerateTextureImageControlFlow: - """Tests for generate_texture_image() control flow and error handling.""" +def setup_pil_mocks(mock_pil_image_module, mock_pil_imagedraw_module, + mock_pil_imagefont_module, width, height, + textbbox_return=(0, 0, 100, 50), font_type='default'): + """Helper to setup PIL mocks with consistent behavior. + + Args: + mock_pil_image_module: Mocked PIL.Image module + mock_pil_imagedraw_module: Mocked PIL.ImageDraw module + mock_pil_imagefont_module: Mocked PIL.ImageFont module + width: Image width in pixels + height: Image height in pixels + textbbox_return: Return value for textbbox (default: (0, 0, 100, 50)) + font_type: 'default', 'custom', or 'error' for font loading behavior + + Returns: + tuple: (mock_pil_image, mock_draw, mock_font) + """ + # Setup PIL Image mock + mock_pil_image = MagicMock() + mock_pil_image.size = (width, height) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (width * height) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + # Setup ImageDraw mock + mock_draw = MagicMock() + mock_draw.textbbox.return_value = textbbox_return + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + # Setup ImageFont mock based on font_type + if font_type == 'default': + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + elif font_type == 'custom': + mock_font = MagicMock() + mock_pil_imagefont_module.truetype.return_value = mock_font + elif font_type == 'error': + mock_pil_imagefont_module.truetype.side_effect = OSError("Font not found") + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + else: + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + return mock_pil_image, mock_draw, mock_font - def test_generate_texture_image_returns_none_when_pack_fails(self): - """ - Test that verifies the control flow bug: when texture generation succeeds - but pack() fails, the function incorrectly returns (None, None) instead - of returning the successfully generated image. - - This test MUST fail initially because of the missing return statement - after line 244 in generation_engine.py. - """ - # Arrange: Create mock Blender image - mock_blender_img = Mock() - mock_blender_img.name = "TestTexture_512x512" - mock_blender_img.filepath_raw = "" - mock_blender_img.pack = Mock(side_effect=RuntimeError("Pack failed")) - mock_blender_img.pixels = MagicMock() - - # Mock props with valid generation settings - mock_props = Mock() - mock_props.text = "Test" - mock_props.font_size = 64 - mock_props.text_color = (1.0, 1.0, 1.0, 1.0) - mock_props.background_transparent = False - mock_props.background_color = (0.0, 0.0, 0.0, 1.0) - mock_props.font_path = "default" - mock_props.use_custom_font = False - mock_props.custom_font_path = "" - mock_props.prepend_text = "" - mock_props.append_text = "" - mock_props.prepend_append_layout = 'HORIZONTAL' - mock_props.prepend_margin = 10 - mock_props.append_margin = 10 - mock_props.enable_text_fitting = False - mock_props.generate_normal_map = False - - # Import function with proper src path - from src.core.generation_engine import generate_texture_image - - # Patch bpy at module level with proper structure - with patch('src.core.generation_engine.bpy') as mock_bpy: - # Properly structure the bpy mock - mock_images = Mock() - mock_images.new = Mock(return_value=mock_blender_img) - mock_images.__contains__ = Mock(return_value=False) - mock_bpy.data.images = mock_images + +def setup_bpy_mocks(mock_bpy, width, height, raise_error=None): + """Helper to setup bpy mocks with consistent behavior. + + Args: + mock_bpy: Mocked bpy module + width: Image width in pixels + height: Image height in pixels + raise_error: Optional exception to raise on image creation + + Returns: + MagicMock: The mocked Blender image object (or None if error) + """ + mock_bpy.data.images.__contains__.return_value = False + + if raise_error: + mock_bpy.data.images.new.side_effect = raise_error + return None + + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (width * height * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + return mock_blender_image + + + + +def create_mock_props(**kwargs): + """Helper to create a mock props object with sensible defaults.""" + props = MagicMock() + + # Set default values + props.text = kwargs.get('text', '') + props.prepend_text = kwargs.get('prepend_text', '') + props.append_text = kwargs.get('append_text', '') + props.prepend_margin = kwargs.get('prepend_margin', 50.0) + props.append_margin = kwargs.get('append_margin', 50.0) + props.prepend_append_layout = kwargs.get('prepend_append_layout', 'HORIZONTAL') + props.background_transparent = kwargs.get('background_transparent', False) + props.background_color = kwargs.get('background_color', (0.0, 0.0, 0.0, 1.0)) + props.text_color = kwargs.get('text_color', (1.0, 1.0, 1.0, 1.0)) + props.font_size = kwargs.get('font_size', 96) + props.font_path = kwargs.get('font_path', 'default') + props.custom_font_path = kwargs.get('custom_font_path', '') + props.use_custom_font = kwargs.get('use_custom_font', False) + props.enable_text_fitting = kwargs.get('enable_text_fitting', False) + props.min_font_size = kwargs.get('min_font_size', 8) + props.max_font_size = kwargs.get('max_font_size', 500) + props.text_fitting_margin = kwargs.get('text_fitting_margin', 10.0) + + return props + + +class TestGenerateTextureImage: + """Test suite for generate_texture_image() function""" + + def test_generate_simple_text(self): + """Test basic text generation with default settings.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: - # Patch PIL at the import level (inside the function) - with patch('PIL.Image') as mock_pil: - mock_pil_img = Mock() - mock_pil_img.size = (512, 512) - mock_pil_img.transpose = Mock(return_value=mock_pil_img) - mock_pil_img.getdata = Mock(return_value=[(255, 255, 255, 255)] * (512 * 512)) - mock_pil.new.return_value = mock_pil_img - mock_pil.FLIP_TOP_BOTTOM = 0 - - with patch('PIL.ImageDraw') as mock_draw: - mock_draw_instance = Mock() - mock_draw_instance.textbbox = Mock(return_value=(0, 0, 100, 50)) - mock_draw_instance.text = Mock() - mock_draw.Draw.return_value = mock_draw_instance - - with patch('PIL.ImageFont') as mock_font: - mock_font.load_default.return_value = Mock() - - # Act: Call generate_texture_image - result_img, result_normal = generate_texture_image(mock_props, 512, 512) - - # Assert: Function should return the successfully generated image, - # not (None, None) even if pack() fails - # This assertion WILL FAIL due to the control flow bug - assert result_img is not None, ( - "Expected function to return the generated image despite pack() failure, " - "but got None. This indicates a control flow bug where the function " - "falls through to exception handler after successful generation." - ) - assert result_img == mock_blender_img, ( - "Expected the function to return the mock Blender image" - ) - - def test_generate_texture_image_returns_image_on_success(self): - """ - Test that when both generation and pack succeed, the function returns - the generated image (not None). - - This test should also fail initially for the same control flow reason. - """ - # Arrange: Create mock Blender image with successful pack - mock_blender_img = Mock() - mock_blender_img.name = "TestTexture_512x512" - mock_blender_img.filepath_raw = "" - mock_blender_img.pack = Mock() # Succeeds (no exception) - mock_blender_img.pixels = MagicMock() - - mock_props = Mock() - mock_props.text = "Test" - mock_props.font_size = 64 - mock_props.text_color = (1.0, 1.0, 1.0, 1.0) - mock_props.background_transparent = False - mock_props.background_color = (0.0, 0.0, 0.0, 1.0) - mock_props.font_path = "default" - mock_props.use_custom_font = False - mock_props.custom_font_path = "" - mock_props.prepend_text = "" - mock_props.append_text = "" - mock_props.prepend_append_layout = 'HORIZONTAL' - mock_props.prepend_margin = 10 - mock_props.append_margin = 10 - mock_props.enable_text_fitting = False - mock_props.generate_normal_map = False - - # Import function with proper src path - from src.core.generation_engine import generate_texture_image - - with patch('src.core.generation_engine.bpy') as mock_bpy: - # Properly structure the bpy mock - mock_images = Mock() - mock_images.new = Mock(return_value=mock_blender_img) - mock_images.__contains__ = Mock(return_value=False) - mock_bpy.data.images = mock_images + # Setup mocks using helper + mock_pil_image, mock_draw, mock_font = setup_pil_mocks( + mock_pil_image_module, mock_pil_imagedraw_module, + mock_pil_imagefont_module, DEFAULT_WIDTH, DEFAULT_HEIGHT + ) + mock_blender_image = setup_bpy_mocks(mock_bpy, DEFAULT_WIDTH, DEFAULT_HEIGHT) - with patch('PIL.Image') as mock_pil: - mock_pil_img = Mock() - mock_pil_img.size = (512, 512) - mock_pil_img.transpose = Mock(return_value=mock_pil_img) - mock_pil_img.getdata = Mock(return_value=[(255, 255, 255, 255)] * (512 * 512)) - mock_pil.new.return_value = mock_pil_img - mock_pil.FLIP_TOP_BOTTOM = 0 + # Create props + props = create_mock_props( + text="Hello", + background_transparent=False, + background_color=(1.0, 1.0, 1.0, 1.0) + ) + + # Call function + result, normal_map = generate_texture_image(props, DEFAULT_WIDTH, DEFAULT_HEIGHT) + + # Assertions + assert result is not None + assert normal_map is None # Normal maps not enabled + mock_pil_image_module.new.assert_called_once() + mock_pil_imagedraw_module.Draw.assert_called_once() + mock_draw.text.assert_called() + mock_bpy.data.images.new.assert_called_once() + mock_pil_image.transpose.assert_called_once() + + def test_generate_transparent_background(self): + """Test generation with transparent background.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (256, 256) + mock_pil_image.getdata.return_value = [(0, 0, 0, 0)] * (256 * 256) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 80, 40) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (256 * 256 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with transparent background + props = create_mock_props( + text="Test", + background_transparent=True + ) + + # Call function + result, normal_map = generate_texture_image(props, 256, 256) + + # Assertions + assert result is not None + # Verify RGBA mode with transparent background + call_args = mock_pil_image_module.new.call_args + assert call_args[0][0] == "RGBA" + assert call_args[0][2] == (0, 0, 0, 0) + + def test_generate_colored_background(self): + """Test generation with custom background color.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (128, 128) + mock_pil_image.getdata.return_value = [(255, 0, 0, 255)] * (128 * 128) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 60, 30) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (128 * 128 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with red background + props = create_mock_props( + text="Red", + background_transparent=False, + background_color=(1.0, 0.0, 0.0, 1.0), # Red in 0-1 range + text_color=(1.0, 1.0, 1.0, 1.0) + ) + + # Call function + result, normal_map = generate_texture_image(props, 128, 128) + + # Assertions + assert result is not None + call_args = mock_pil_image_module.new.call_args + # Background color should be converted to 0-255 range + assert call_args[0][2] == (255, 0, 0, 255) + + def test_empty_text(self): + """Test handling of empty text input.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with empty text + props = create_mock_props(text="") + + # Call function + result, normal_map = generate_texture_image(props, 512, 512) + + # Should still generate an image (just background) + assert result is not None + # text.draw should not be called for empty text + mock_draw.text.assert_not_called() + + def test_invalid_dimensions(self): + """Test error handling for invalid dimensions.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup to raise an error on invalid dimensions + mock_bpy.data.images.new.side_effect = ValueError("Invalid dimensions") + mock_bpy.data.images.__contains__.return_value = False + + # Create props + props = create_mock_props(text="Test") + + # Call with invalid dimensions + result, normal_map = generate_texture_image(props, 0, 0) + + # Should handle error gracefully + assert result is None + assert normal_map is None + + def test_generate_with_prepend_append(self): + """Test text generation with prepend and append text.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 150, 50) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with prepend and append + props = create_mock_props( + text="Main", + prepend_text="Pre-", + append_text="-Post" + ) + + # Call function + result, normal_map = generate_texture_image(props, 512, 512) + + # Assertions + assert result is not None + # Verify text.draw was called with combined text + mock_draw.text.assert_called() + + def test_horizontal_layout(self): + """Test horizontal text layout.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 256) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 256) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 100, 50) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 256 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with horizontal layout + props = create_mock_props( + text="Horizontal", + prepend_append_layout='HORIZONTAL' + ) + + # Call function + result, normal_map = generate_texture_image(props, 512, 256) + + # Assertions + assert result is not None + + def test_vertical_layout(self): + """Test vertical text layout.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (256, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (256 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 50, 50) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (256 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with vertical layout and multiple text parts + props = create_mock_props( + prepend_text="Top", + text="Middle", + append_text="Bottom", + prepend_append_layout='VERTICAL' + ) + + # Call function + result, normal_map = generate_texture_image(props, 256, 512) + + # Assertions + assert result is not None + # Verify multiple draw.text calls for vertical layout + assert mock_draw.text.call_count >= 3 # One per text part + + def test_text_fitting_enabled(self): + """Test text fitting integration.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy, \ + patch('src.core.generation_engine.has_text_fitting') as mock_has_fitting: + + # Enable text fitting + mock_has_fitting.return_value = True + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 600, 50) # Text too wide + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with text fitting enabled + props = create_mock_props( + text="Very Long Text That Needs Fitting", + font_size=50, + enable_text_fitting=True + ) + + # Call function + result, normal_map = generate_texture_image(props, 512, 512) + + # Assertions + assert result is not None + # Text fitting should call textbbox multiple times to find right size + assert mock_draw.textbbox.call_count > 1 + + def test_custom_font_loading(self): + """Test loading a custom font file.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 100, 50) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_custom_font = MagicMock() + mock_pil_imagefont_module.truetype.return_value = mock_custom_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with custom font + props = create_mock_props( + text="Custom Font", + use_custom_font=True, + custom_font_path="/path/to/custom/font.ttf", + font_size=30 + ) + + # Call function + result, normal_map = generate_texture_image(props, 512, 512) + + # Assertions + assert result is not None + # Verify truetype was called with the custom font path + mock_pil_imagefont_module.truetype.assert_called_with("/path/to/custom/font.ttf", 30) + + def test_font_loading_fallback(self): + """Test fallback to default font when custom font fails to load.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (512, 512) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 100, 50) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + # Make truetype raise OSError, should fallback to default + mock_pil_imagefont_module.truetype.side_effect = OSError("Font not found") + mock_default_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_default_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with invalid font path + props = create_mock_props( + text="Fallback Font", + use_custom_font=True, + custom_font_path="/invalid/path/to/font.ttf", + font_size=30 + ) + + # Call function + result, normal_map = generate_texture_image(props, 512, 512) + + # Assertions + assert result is not None + # Verify fallback to default font + mock_pil_imagefont_module.load_default.assert_called_once() + + def test_pil_import_error(self): + """Test handling of PIL import error.""" + with patch('PIL.Image', None), \ + patch('PIL.ImageDraw', None), \ + patch('PIL.ImageFont', None): + # Create a mock for bpy that returns an image even without PIL + with patch('src.core.generation_engine.bpy') as mock_bpy: + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (512 * 512 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False - with patch('PIL.ImageDraw') as mock_draw: - mock_draw_instance = Mock() - mock_draw_instance.textbbox = Mock(return_value=(0, 0, 100, 50)) - mock_draw_instance.text = Mock() - mock_draw.Draw.return_value = mock_draw_instance - - with patch('PIL.ImageFont') as mock_font: - mock_font.load_default.return_value = Mock() - - # Act - result_img, result_normal = generate_texture_image(mock_props, 512, 512) - - # Assert: Should return the generated image - assert result_img is not None, ( - "Expected function to return generated image on success, got None" - ) - assert result_img == mock_blender_img - assert result_normal is None # No normal map requested \ No newline at end of file + # Create props + props = create_mock_props(text="No PIL") + + # Call when PIL is not available - should return None + result, normal_map = generate_texture_image(props, 512, 512) + + # Should return None when PIL is unavailable + assert result is None + assert normal_map is None + + +class TestGeneratePreview: + """Test suite for generate_preview() function""" + + def test_preview_generates_at_resolution(self): + """Test that preview generates at specified resolution.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (128, 128) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (128 * 128) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 50, 25) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.load_default.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (128 * 128 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props + props = create_mock_props(text="Preview") + + # Call generate_preview with specific preview dimensions + result = generate_preview(props, preview_width=128, preview_height=128) + + # Assertions + assert result is not None + # Verify image was created at preview resolution + # The image name contains dimensions + call_args = mock_bpy.data.images.new.call_args[0] + assert "128x128" in call_args[0] + + def test_preview_uses_same_props(self): + """Test that preview uses same properties as main generation.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL mocks + mock_pil_image = MagicMock() + mock_pil_image.size = (64, 64) + mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (64 * 64) + mock_pil_image.transpose.return_value = mock_pil_image + mock_pil_image_module.new.return_value = mock_pil_image + mock_pil_image_module.FLIP_TOP_BOTTOM = 1 + + mock_draw = MagicMock() + mock_draw.textbbox.return_value = (0, 0, 30, 15) + mock_pil_imagedraw_module.Draw.return_value = mock_draw + + mock_font = MagicMock() + mock_pil_imagefont_module.truetype.return_value = mock_font + + # Setup bpy mocks + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (64 * 64 * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + # Create props with various properties + props = create_mock_props( + text="Props", + background_transparent=True, + text_color=(1.0, 0.0, 0.0, 1.0), + prepend_text="[", + append_text="]", + use_custom_font=True, + custom_font_path="/path/to/font.ttf", + font_size=20, + prepend_append_layout='VERTICAL' + ) + + # Call preview + result = generate_preview(props, preview_width=64, preview_height=64) + + # Assertions + assert result is not None + # Verify props were used (transparent, custom font, etc.) + call_args = mock_pil_image_module.new.call_args + assert call_args[0][0] == "RGBA" # Transparent + mock_pil_imagefont_module.truetype.assert_called_with("/path/to/font.ttf", 20) + + def test_preview_error_handling(self): + """Test preview error handling.""" + with patch('PIL.Image') as mock_pil_image_module, \ + patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \ + patch('PIL.ImageFont') as mock_pil_imagefont_module, \ + patch('src.core.generation_engine.bpy') as mock_bpy: + + # Setup PIL to raise an error + mock_bpy.data.images.new.side_effect = RuntimeError("Preview generation failed") + mock_bpy.data.images.__contains__.return_value = False + + # Create props + props = create_mock_props(text="Error") + + # Call preview + result = generate_preview(props, preview_width=128, preview_height=128) + + # Should handle error gracefully + assert result is None \ No newline at end of file diff --git a/tests/unit/operators/test_generation_ops.py b/tests/unit/operators/test_generation_ops.py new file mode 100644 index 0000000..9869f91 --- /dev/null +++ b/tests/unit/operators/test_generation_ops.py @@ -0,0 +1,536 @@ +"""Unit tests for generation_ops.py - Blender operator tests""" + +from unittest.mock import Mock, patch, MagicMock, call +import pytest + +from src.operators.generation_ops import ( + TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_preview, + TEXT_TEXTURE_OT_generate_shader +) + + +# Helper functions for common mock setups + +def create_mock_props(text="test", width=512, height=512, **kwargs): + """Create mock texture properties with sensible defaults. + + Args: + text: Text content for texture generation + width: Texture width in pixels + height: Texture height in pixels + **kwargs: Additional property overrides + + Returns: + Mock object with standard texture properties + """ + props = Mock() + props.text = text + props.texture_width = width + props.texture_height = height + props.generate_normal_map = kwargs.get('generate_normal_map', False) + props.preview_size = kwargs.get('preview_size', 512) + props.shader_type = kwargs.get('shader_type', 'PRINCIPLED') + props.shader_connection = kwargs.get('shader_connection', 'BASE_COLOR') + props.use_alpha = kwargs.get('use_alpha', False) + props.disable_shadows = kwargs.get('disable_shadows', False) + props.disable_reflections = kwargs.get('disable_reflections', False) + props.disable_backfacing = kwargs.get('disable_backfacing', False) + props.custom_material_name = kwargs.get('custom_material_name', "") + props.auto_assign_material = kwargs.get('auto_assign_material', False) + return props + + +def create_mock_context(props=None, **kwargs): + """Create mock Blender context with scene properties. + + Args: + props: Texture properties (creates default if None) + **kwargs: Context attribute overrides + + Returns: + Mock context object with scene.text_texture_props + """ + context = Mock() + context.scene.text_texture_props = props if props else create_mock_props() + context.space_data = kwargs.get('space_data', None) + context.active_object = kwargs.get('active_object', None) + context.screen.areas = kwargs.get('screen_areas', []) + return context + + +def create_mock_image(width=512, height=512): + """Create mock PIL Image with pixel data. + + Args: + width: Image width in pixels + height: Image height in pixels + + Returns: + Mock image with pixels attribute + """ + img = Mock() + img.pixels = [0.5] * (width * height * 4) + return img + + +def create_mock_blender_image(width=512, height=512): + """Create mock Blender image with pack method. + + Args: + width: Image width in pixels + height: Image height in pixels + + Returns: + Mock Blender image ready for bpy.data.images + """ + img = Mock() + img.pixels = [0.0] * (width * height * 4) + img.pack = Mock() + return img + + +def create_mock_shader_nodes(): + """Create mock shader node tree components. + + Returns: + Tuple of (output_node, shader_node, texture_node) mocks + """ + output_node = MagicMock() + output_node.inputs = {'Surface': Mock()} + + shader_node = MagicMock() + shader_node.inputs = { + 'Base Color': Mock(), + 'Emission Color': Mock(), + 'Emission Strength': Mock(), + 'Alpha': Mock(), + 'Normal': Mock() + } + shader_node.outputs = {'BSDF': Mock()} + + tex_node = MagicMock() + tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()} + tex_node.image = None + tex_node.location = (0, 0) + + return output_node, shader_node, tex_node + + +class TestGenerateOperator: + """Test suite for TEXT_TEXTURE_OT_generate operator""" + + def test_execute_success(self): + """Successful generation returns {'FINISHED'}""" + op = TEXT_TEXTURE_OT_generate() + op.report = Mock() + + props = create_mock_props(text="test texture") + context = create_mock_context(props) + mock_img = create_mock_image() + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \ + patch('src.operators.generation_ops.bpy') as mock_bpy: + + mock_gen.return_value = (mock_img, None) + mock_blender_img = create_mock_blender_image() + mock_bpy.data.images.new.return_value = mock_blender_img + mock_bpy.data.images.__contains__.return_value = False + + result = op.execute(context) + + assert result == {'FINISHED'} + mock_gen.assert_called_once_with(props, 512, 512) + assert op.report.call_count >= 1 + + def test_execute_creates_blender_image(self): + """Image added to bpy.data.images""" + op = TEXT_TEXTURE_OT_generate() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.texture_width = 256 + mock_props.texture_height = 256 + mock_props.generate_normal_map = False + context.scene.text_texture_props = mock_props + context.space_data = None + + mock_img = Mock() + mock_img.pixels = [0.5] * (256 * 256 * 4) + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \ + patch('src.operators.generation_ops.bpy') as mock_bpy: + + mock_gen.return_value = (mock_img, None) + + mock_blender_img = Mock() + mock_blender_img.pixels = [0.0] * (256 * 256 * 4) + mock_blender_img.pack = Mock() + mock_bpy.data.images.new.return_value = mock_blender_img + mock_bpy.data.images.__contains__.return_value = False + + result = op.execute(context) + + mock_bpy.data.images.new.assert_called_once() + call_args = mock_bpy.data.images.new.call_args + assert call_args[0][1] == 256 + assert call_args[0][2] == 256 + assert call_args[1]['alpha'] is True + + mock_blender_img.pack.assert_called_once() + assert result == {'FINISHED'} + + def test_execute_replaces_existing_image(self): + """Old image removed before new creation""" + op = TEXT_TEXTURE_OT_generate() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "existing" + mock_props.texture_width = 512 + mock_props.texture_height = 512 + mock_props.generate_normal_map = False + context.scene.text_texture_props = mock_props + context.space_data = None + + mock_img = Mock() + mock_img.pixels = [0.5] * (512 * 512 * 4) + + mock_old_image = Mock() + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \ + patch('src.operators.generation_ops.bpy') as mock_bpy: + + mock_gen.return_value = (mock_img, None) + + mock_bpy.data.images.__contains__.return_value = True + mock_bpy.data.images.__getitem__.return_value = mock_old_image + mock_bpy.data.images.remove = Mock() + + mock_blender_img = Mock() + mock_blender_img.pixels = [0.0] * (512 * 512 * 4) + mock_blender_img.pack = Mock() + mock_bpy.data.images.new.return_value = mock_blender_img + + result = op.execute(context) + + mock_bpy.data.images.remove.assert_called_once_with(mock_old_image) + assert result == {'FINISHED'} + + def test_execute_error_handling(self): + """Generation failure returns {'CANCELLED'} with error report""" + op = TEXT_TEXTURE_OT_generate() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.texture_width = 512 + mock_props.texture_height = 512 + context.scene.text_texture_props = mock_props + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen: + mock_gen.return_value = (None, None) + + result = op.execute(context) + + assert result == {'CANCELLED'} + op.report.assert_called_once() + assert 'ERROR' in op.report.call_args[0][0] + + def test_invoke_opens_file_browser(self): + """Invoke context opens file browser""" + # TEXT_TEXTURE_OT_generate doesn't implement a custom invoke method + # It uses the default Blender operator behavior (direct execute) + # This test verifies that behavior is as expected + pytest.skip("Operator uses default Blender invoke (no file browser)") + + def test_poll_requires_context(self): + """Poll method validates context availability""" + if hasattr(TEXT_TEXTURE_OT_generate, 'poll'): + valid_context = Mock() + valid_context.scene = Mock() + assert TEXT_TEXTURE_OT_generate.poll(valid_context) is True + + invalid_context = Mock() + invalid_context.scene = None + assert TEXT_TEXTURE_OT_generate.poll(invalid_context) is False + else: + pytest.skip("Operator does not implement poll method") + + +class TestPreviewOperator: + """Test suite for TEXT_TEXTURE_OT_preview operator""" + + def test_execute_generates_preview(self): + """Preview created at preview resolution""" + op = TEXT_TEXTURE_OT_preview() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "preview test" + mock_props.preview_size = 512 + context.scene.text_texture_props = mock_props + context.screen.areas = [] + + mock_preview_img = Mock() + mock_preview_img.name = "Preview_Image" + + # Patch at the core module level since that's where generate_preview lives + with patch('src.core.generation_engine.generate_preview') as mock_preview: + mock_preview.return_value = mock_preview_img + + result = op.execute(context) + + assert result == {'FINISHED'} + mock_preview.assert_called_once() + call_args = mock_preview.call_args + assert call_args[0][0] == mock_props + assert call_args[0][1] == 512 + assert call_args[0][2] == 512 + + def test_execute_preview_error_handling(self): + """Preview failure returns {'CANCELLED'}""" + op = TEXT_TEXTURE_OT_preview() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.preview_size = 512 + context.scene.text_texture_props = mock_props + + with patch('src.operators.generation_ops.generate_preview') as mock_preview: + mock_preview.return_value = None + + result = op.execute(context) + + assert result == {'CANCELLED'} + op.report.assert_called() + assert 'ERROR' in op.report.call_args[0][0] + + def test_poll_requires_context(self): + """Poll validates context""" + if hasattr(TEXT_TEXTURE_OT_preview, 'poll'): + valid_context = Mock() + valid_context.scene = Mock() + assert TEXT_TEXTURE_OT_preview.poll(valid_context) is True + + invalid_context = Mock() + invalid_context.scene = None + assert TEXT_TEXTURE_OT_preview.poll(invalid_context) is False + else: + pytest.skip("Operator does not implement poll method") + + def test_preview_uses_correct_resolution(self): + """Uses preview_width and preview_height""" + op = TEXT_TEXTURE_OT_preview() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.preview_size = 256 + mock_props.texture_width = 2048 + mock_props.texture_height = 2048 + context.scene.text_texture_props = mock_props + context.screen.areas = [] + + mock_result = Mock() + mock_result.name = "Preview" + + with patch('src.core.generation_engine.generate_preview') as mock_preview: + mock_preview.return_value = mock_result + + result = op.execute(context) + + assert result == {'FINISHED'} + call_args = mock_preview.call_args + assert call_args[0][1] == 256 + assert call_args[0][2] == 256 + + +class TestGenerateShaderOperator: + """Test suite for TEXT_TEXTURE_OT_generate_shader operator""" + + def test_execute_creates_shader_nodes(self): + """Shader nodes created and linked""" + op = TEXT_TEXTURE_OT_generate_shader() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "shader test" + mock_props.texture_width = 512 + mock_props.texture_height = 512 + mock_props.generate_normal_map = False + mock_props.shader_type = 'PRINCIPLED' + mock_props.shader_connection = 'BASE_COLOR' + mock_props.use_alpha = False + mock_props.disable_shadows = False + mock_props.disable_reflections = False + mock_props.disable_backfacing = False + mock_props.custom_material_name = "" + mock_props.auto_assign_material = False + context.scene.text_texture_props = mock_props + context.active_object = None + context.screen.areas = [] + + mock_img = Mock() + mock_img.pixels = [0.5] * (512 * 512 * 4) + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \ + patch('src.operators.generation_ops.bpy') as mock_bpy: + + mock_gen.return_value = (mock_img, None) + + # Create mock nodes with subscriptable outputs/inputs + mock_output_node = MagicMock() + mock_output_node.inputs = {'Surface': Mock()} + + mock_shader_node = MagicMock() + mock_shader_node.inputs = { + 'Base Color': Mock(), + 'Emission Color': Mock(), + 'Emission Strength': Mock(), + 'Alpha': Mock(), + 'Normal': Mock() + } + mock_shader_node.outputs = {'BSDF': Mock()} + + mock_tex_node = MagicMock() + mock_tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()} + mock_tex_node.image = None + mock_tex_node.location = (0, 0) + + # Mock material and node tree + mock_material = Mock() + mock_nodes = Mock() + mock_links = Mock() + mock_material.node_tree.nodes = mock_nodes + mock_material.node_tree.links = mock_links + mock_material.node_tree.nodes.clear = Mock() + mock_material.use_nodes = True + + # Return different nodes on each call + mock_nodes.new.side_effect = [mock_output_node, mock_shader_node, mock_tex_node] + + mock_bpy.data.materials.new.return_value = mock_material + mock_bpy.data.materials.__contains__.return_value = False + + mock_blender_img = Mock() + mock_blender_img.pixels = [0.0] * (512 * 512 * 4) + mock_blender_img.pack = Mock() + mock_bpy.data.images.new.return_value = mock_blender_img + mock_bpy.data.images.__contains__.return_value = False + + result = op.execute(context) + + assert result == {'FINISHED'} + assert mock_nodes.new.call_count >= 3 + assert mock_links.new.call_count >= 1 + + def test_execute_requires_material(self): + """Material must exist in context""" + op = TEXT_TEXTURE_OT_generate_shader() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.texture_width = 512 + mock_props.texture_height = 512 + mock_props.generate_normal_map = False + mock_props.shader_type = 'PRINCIPLED' + mock_props.shader_connection = 'BASE_COLOR' + mock_props.use_alpha = False + mock_props.disable_shadows = False + mock_props.disable_reflections = False + mock_props.disable_backfacing = False + mock_props.custom_material_name = "" + mock_props.auto_assign_material = False + context.scene.text_texture_props = mock_props + context.active_object = None + context.screen.areas = [] + + mock_img = Mock() + mock_img.pixels = [0.5] * (512 * 512 * 4) + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \ + patch('src.operators.generation_ops.bpy') as mock_bpy: + + mock_gen.return_value = (mock_img, None) + + # Create mock nodes with subscriptable outputs/inputs + mock_output_node = MagicMock() + mock_output_node.inputs = {'Surface': Mock()} + + mock_shader_node = MagicMock() + mock_shader_node.inputs = { + 'Base Color': Mock(), + 'Alpha': Mock(), + 'Normal': Mock() + } + mock_shader_node.outputs = {'BSDF': Mock()} + + mock_tex_node = MagicMock() + mock_tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()} + + mock_material = Mock() + mock_material.node_tree.nodes = Mock() + mock_material.node_tree.links = Mock() + mock_material.node_tree.nodes.clear = Mock() + mock_material.node_tree.nodes.new.side_effect = [mock_output_node, mock_shader_node, mock_tex_node] + mock_material.use_nodes = True + + mock_bpy.data.materials.__contains__.return_value = False + mock_bpy.data.materials.new.return_value = mock_material + + mock_blender_img = Mock() + mock_blender_img.pixels = [0.0] * (512 * 512 * 4) + mock_blender_img.pack = Mock() + mock_bpy.data.images.new.return_value = mock_blender_img + mock_bpy.data.images.__contains__.return_value = False + + result = op.execute(context) + + assert result == {'FINISHED'} + mock_bpy.data.materials.new.assert_called_once() + + def test_execute_error_handling(self): + """Missing material returns {'CANCELLED'}""" + op = TEXT_TEXTURE_OT_generate_shader() + op.report = Mock() + + context = Mock() + mock_props = Mock() + mock_props.text = "test" + mock_props.texture_width = 512 + mock_props.texture_height = 512 + context.scene.text_texture_props = mock_props + + with patch('src.operators.generation_ops.generate_texture_image') as mock_gen: + mock_gen.return_value = (None, None) + + result = op.execute(context) + + assert result == {'CANCELLED'} + op.report.assert_called() + assert 'ERROR' in op.report.call_args[0][0] + + def test_poll_validates_material_context(self): + """Poll checks material availability""" + if hasattr(TEXT_TEXTURE_OT_generate_shader, 'poll'): + valid_context = Mock() + valid_context.scene = Mock() + assert TEXT_TEXTURE_OT_generate_shader.poll(valid_context) is True + + invalid_context = Mock() + invalid_context.scene = None + assert TEXT_TEXTURE_OT_generate_shader.poll(invalid_context) is False + else: + pytest.skip("Operator does not implement poll method") \ No newline at end of file