working
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user