This commit is contained in:
2025-10-12 16:00:22 +02:00
parent 9917d724a3
commit 97950f44c1
7 changed files with 1227 additions and 146 deletions

View File

@@ -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