"""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")