Files
Text-Texture-Generator-for-…/tests/unit/operators/test_generation_ops.py
2025-10-12 16:00:22 +02:00

536 lines
19 KiB
Python

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