working
This commit is contained in:
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
# 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
|
||||
536
tests/unit/operators/test_generation_ops.py
Normal file
536
tests/unit/operators/test_generation_ops.py
Normal file
@@ -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")
|
||||
Reference in New Issue
Block a user