tests
This commit is contained in:
3
tests/unit/__init__.py
Normal file
3
tests/unit/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for Text Texture Generator.
|
||||
"""
|
||||
BIN
tests/unit/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
tests/unit/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
461
tests/unit/test_text_fitting.py
Normal file
461
tests/unit/test_text_fitting.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Unit tests for text_fitting.py functions.
|
||||
Tests the core text fitting algorithms in isolation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to path for importing modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from fixtures.sample_data import *
|
||||
from src.core.text_fitting import (
|
||||
calculate_available_text_area,
|
||||
largest_rectangle_in_histogram,
|
||||
calculate_text_position,
|
||||
find_optimal_font_size,
|
||||
calculate_text_scaling,
|
||||
check_text_overlap,
|
||||
resolve_text_conflicts
|
||||
)
|
||||
|
||||
|
||||
class TestLargestRectangleInHistogram:
|
||||
"""Test the largest_rectangle_in_histogram function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_simple_histogram(self):
|
||||
"""Test with simple histogram data"""
|
||||
heights = [3, 1, 3, 2, 2]
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
assert len(result) == 4 # x, y, width, height
|
||||
assert result[2] > 0 # width should be positive
|
||||
assert result[3] > 0 # height should be positive
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_increasing_histogram(self):
|
||||
"""Test with increasing height values"""
|
||||
heights = [1, 2, 3, 4, 5]
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
x, y, width, height = result
|
||||
assert width > 0 and height > 0
|
||||
# The area should be reasonable
|
||||
area = width * height
|
||||
assert area > 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_histogram(self):
|
||||
"""Test with empty histogram"""
|
||||
heights = []
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
x, y, width, height = result
|
||||
assert width == 0 and height == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_single_bar_histogram(self):
|
||||
"""Test with single bar"""
|
||||
heights = [5]
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
x, y, width, height = result
|
||||
assert width == 1 and height == 5
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_zero_height_bars(self):
|
||||
"""Test histogram with zero height bars"""
|
||||
heights = [0, 0, 3, 3, 0, 0]
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
x, y, width, height = result
|
||||
# Should find the rectangle in the middle
|
||||
assert width == 2 and height == 3
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_pyramid_histogram(self):
|
||||
"""Test with pyramid-shaped histogram"""
|
||||
heights = [1, 2, 3, 4, 3, 2, 1]
|
||||
result = largest_rectangle_in_histogram(heights)
|
||||
|
||||
x, y, width, height = result
|
||||
assert width > 0 and height > 0
|
||||
# Should find a reasonable rectangle
|
||||
|
||||
|
||||
class TestCalculateAvailableTextArea:
|
||||
"""Test the calculate_available_text_area function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_overlays(self):
|
||||
"""Test area calculation with no overlays"""
|
||||
width, height = 500, 400
|
||||
overlays = []
|
||||
|
||||
result = calculate_available_text_area(width, height, overlays)
|
||||
|
||||
assert result['width'] == width
|
||||
assert result['height'] == height
|
||||
assert result['x'] == 0
|
||||
assert result['y'] == 0
|
||||
assert result['area'] == width * height
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_single_overlay(self):
|
||||
"""Test area calculation with single overlay"""
|
||||
width, height = 500, 400
|
||||
overlays = [{'x': 100, 'y': 100, 'width': 50, 'height': 50}]
|
||||
|
||||
result = calculate_available_text_area(width, height, overlays)
|
||||
|
||||
assert result['width'] > 0
|
||||
assert result['height'] > 0
|
||||
assert result['x'] >= 0
|
||||
assert result['y'] >= 0
|
||||
assert result['area'] > 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_multiple_overlays(self):
|
||||
"""Test area calculation with multiple overlays"""
|
||||
width, height = 500, 400
|
||||
overlays = [
|
||||
{'x': 50, 'y': 50, 'width': 100, 'height': 100},
|
||||
{'x': 300, 'y': 200, 'width': 80, 'height': 80}
|
||||
]
|
||||
|
||||
result = calculate_available_text_area(width, height, overlays)
|
||||
|
||||
assert result['width'] >= 0
|
||||
assert result['height'] >= 0
|
||||
assert result['area'] >= 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_overlays_covering_entire_area(self):
|
||||
"""Test when overlays cover most of the area"""
|
||||
width, height = 100, 100
|
||||
overlays = [{'x': 0, 'y': 0, 'width': 100, 'height': 100}]
|
||||
|
||||
result = calculate_available_text_area(width, height, overlays)
|
||||
|
||||
# Should still return something, even if very small
|
||||
assert result['width'] >= 0
|
||||
assert result['height'] >= 0
|
||||
|
||||
|
||||
class TestCalculateTextPosition:
|
||||
"""Test the calculate_text_position function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_center_alignment(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test center alignment positioning"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20) # 100x20 text
|
||||
|
||||
available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
|
||||
|
||||
result = calculate_text_position("Test Text", sample_font, available_area, 'center')
|
||||
|
||||
assert result['x'] == 100 # (300 - 100) / 2
|
||||
assert result['y'] == 40 # (100 - 20) / 2
|
||||
assert result['text_width'] == 100
|
||||
assert result['text_height'] == 20
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_top_left_alignment(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test top-left alignment positioning"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
available_area = {'x': 10, 'y': 10, 'width': 200, 'height': 100}
|
||||
|
||||
result = calculate_text_position("Test Text", sample_font, available_area, 'top-left')
|
||||
|
||||
assert result['x'] == 10 # area x
|
||||
assert result['y'] == 10 # area y
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_bottom_right_alignment(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test bottom-right alignment positioning"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
|
||||
|
||||
result = calculate_text_position("Test Text", sample_font, available_area, 'bottom-right')
|
||||
|
||||
assert result['x'] == 200 # 300 - 100
|
||||
assert result['y'] == 80 # 100 - 20
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_invalid_alignment_defaults_to_center(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test invalid alignment defaults to center"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
|
||||
|
||||
result = calculate_text_position("Test Text", sample_font, available_area, 'invalid_alignment')
|
||||
|
||||
assert result['x'] == 100 # Center alignment
|
||||
assert result['y'] == 40
|
||||
|
||||
|
||||
class TestFindOptimalFontSize:
|
||||
"""Test the find_optimal_font_size function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.ImageFont.truetype')
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_find_optimal_size_basic(self, mock_draw_class, mock_image_new, mock_truetype):
|
||||
"""Test basic optimal font size finding"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
# Mock font that gets larger as size increases
|
||||
def textbbox_side_effect(pos, text, font):
|
||||
size = font.size if hasattr(font, 'size') else 24
|
||||
return (0, 0, size * 4, size) # width = size * 4, height = size
|
||||
|
||||
mock_draw.textbbox.side_effect = textbbox_side_effect
|
||||
|
||||
# Mock fonts with size attribute
|
||||
def mock_font_factory(path, size):
|
||||
font = Mock()
|
||||
font.size = size
|
||||
return font
|
||||
|
||||
mock_truetype.side_effect = mock_font_factory
|
||||
|
||||
result = find_optimal_font_size("Test Text", "/fake/font.ttf", 200, 50, 8, 100)
|
||||
|
||||
assert result >= 8
|
||||
assert result <= 100
|
||||
assert result > 0
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.ImageFont.truetype')
|
||||
def test_find_optimal_size_font_load_error(self, mock_truetype):
|
||||
"""Test handling of font loading errors"""
|
||||
mock_truetype.side_effect = OSError("Font not found")
|
||||
|
||||
result = find_optimal_font_size("Test Text", "/fake/font.ttf", 200, 50, 8, 100)
|
||||
|
||||
assert result == 8 # Should return min_size on error
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.ImageFont.truetype')
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_find_optimal_size_tight_constraints(self, mock_draw_class, mock_image_new, mock_truetype):
|
||||
"""Test with very tight size constraints"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 300, 100) # Very large text
|
||||
|
||||
mock_font = Mock()
|
||||
mock_truetype.return_value = mock_font
|
||||
|
||||
result = find_optimal_font_size("Test Text", "/fake/font.ttf", 50, 20, 8, 100)
|
||||
|
||||
# Should return minimum size when text is too large
|
||||
assert result == 8
|
||||
|
||||
|
||||
class TestCalculateTextScaling:
|
||||
"""Test the calculate_text_scaling function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_calculate_scaling_basic(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test basic scaling calculation"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 50) # Current size 100x50
|
||||
|
||||
width_scale, height_scale, uniform_scale = calculate_text_scaling(
|
||||
"Test Text", sample_font, 200, 100 # Target size 200x100
|
||||
)
|
||||
|
||||
assert width_scale == 2.0 # 200 / 100
|
||||
assert height_scale == 2.0 # 100 / 50
|
||||
assert uniform_scale == 2.0 # min(2.0, 2.0)
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_calculate_scaling_different_ratios(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test scaling with different width/height ratios"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 50) # Current size 100x50
|
||||
|
||||
width_scale, height_scale, uniform_scale = calculate_text_scaling(
|
||||
"Test Text", sample_font, 150, 200 # Target size 150x200
|
||||
)
|
||||
|
||||
assert width_scale == 1.5 # 150 / 100
|
||||
assert height_scale == 4.0 # 200 / 50
|
||||
assert uniform_scale == 1.5 # min(1.5, 4.0) - use smaller to ensure fit
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_calculate_scaling_zero_dimensions(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test scaling with zero current dimensions"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 0, 0) # Zero size
|
||||
|
||||
width_scale, height_scale, uniform_scale = calculate_text_scaling(
|
||||
"Test Text", sample_font, 100, 50
|
||||
)
|
||||
|
||||
# Should return 1.0 for all when current dimensions are zero
|
||||
assert width_scale == 1.0
|
||||
assert height_scale == 1.0
|
||||
assert uniform_scale == 1.0
|
||||
|
||||
|
||||
class TestCheckTextOverlap:
|
||||
"""Test the check_text_overlap function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_overlaps(self):
|
||||
"""Test positions with no overlaps"""
|
||||
positions = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50},
|
||||
{'x': 150, 'y': 0, 'width': 100, 'height': 50},
|
||||
{'x': 0, 'y': 100, 'width': 100, 'height': 50}
|
||||
]
|
||||
|
||||
overlaps = check_text_overlap(positions)
|
||||
|
||||
assert len(overlaps) == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_with_overlaps(self):
|
||||
"""Test positions with overlaps"""
|
||||
positions = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50},
|
||||
{'x': 50, 'y': 25, 'width': 100, 'height': 50}, # Overlaps first
|
||||
{'x': 200, 'y': 0, 'width': 100, 'height': 50}
|
||||
]
|
||||
|
||||
overlaps = check_text_overlap(positions)
|
||||
|
||||
assert len(overlaps) == 1
|
||||
assert (0, 1) in overlaps
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_multiple_overlaps(self):
|
||||
"""Test positions with multiple overlaps"""
|
||||
positions = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 100},
|
||||
{'x': 50, 'y': 50, 'width': 100, 'height': 100}, # Overlaps 0
|
||||
{'x': 25, 'y': 25, 'width': 50, 'height': 50} # Overlaps 0 and 1
|
||||
]
|
||||
|
||||
overlaps = check_text_overlap(positions)
|
||||
|
||||
assert len(overlaps) >= 2 # Should detect multiple overlaps
|
||||
overlap_pairs = set(overlaps)
|
||||
assert (0, 1) in overlap_pairs or (1, 0) in overlap_pairs
|
||||
assert (0, 2) in overlap_pairs or (2, 0) in overlap_pairs
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_positions_list(self):
|
||||
"""Test with empty positions list"""
|
||||
overlaps = check_text_overlap([])
|
||||
assert len(overlaps) == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_single_position(self):
|
||||
"""Test with single position"""
|
||||
positions = [{'x': 0, 'y': 0, 'width': 100, 'height': 50}]
|
||||
overlaps = check_text_overlap(positions)
|
||||
assert len(overlaps) == 0
|
||||
|
||||
|
||||
class TestResolveTextConflicts:
|
||||
"""Test the resolve_text_conflicts function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_fitting.check_text_overlap')
|
||||
def test_resolve_no_conflicts(self, mock_check_overlap):
|
||||
"""Test conflict resolution when no conflicts exist"""
|
||||
mock_check_overlap.return_value = [] # No overlaps
|
||||
|
||||
positions = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50},
|
||||
{'x': 150, 'y': 0, 'width': 100, 'height': 50}
|
||||
]
|
||||
|
||||
result = resolve_text_conflicts(positions, 500, 400)
|
||||
|
||||
# Positions should remain unchanged
|
||||
assert len(result) == 2
|
||||
assert result[0] == positions[0]
|
||||
assert result[1] == positions[1]
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_fitting.check_text_overlap')
|
||||
def test_resolve_vertical_move(self, mock_check_overlap):
|
||||
"""Test conflict resolution by moving vertically"""
|
||||
mock_check_overlap.return_value = [(0, 1)] # First two positions overlap
|
||||
|
||||
positions = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50},
|
||||
{'x': 50, 'y': 25, 'width': 100, 'height': 50} # Overlaps first
|
||||
]
|
||||
|
||||
result = resolve_text_conflicts(positions, 500, 400)
|
||||
|
||||
assert len(result) == 2
|
||||
# First position should remain unchanged
|
||||
assert result[0] == positions[0]
|
||||
# Second position should be moved down
|
||||
assert result[1]['y'] == 60 # 0 + 50 + 10 (position + height + margin)
|
||||
assert result[1]['x'] == 50 # x should remain the same
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_fitting.check_text_overlap')
|
||||
def test_resolve_horizontal_move(self, mock_check_overlap):
|
||||
"""Test conflict resolution by moving horizontally when vertical doesn't fit"""
|
||||
mock_check_overlap.return_value = [(0, 1)]
|
||||
|
||||
positions = [
|
||||
{'x': 0, 'y': 300, 'width': 100, 'height': 50}, # Near bottom
|
||||
{'x': 50, 'y': 325, 'width': 100, 'height': 50} # Overlaps, can't move down
|
||||
]
|
||||
|
||||
result = resolve_text_conflicts(positions, 500, 400)
|
||||
|
||||
assert len(result) == 2
|
||||
# First position unchanged
|
||||
assert result[0] == positions[0]
|
||||
# Second position should be moved right since vertical move won't fit
|
||||
assert result[1]['x'] == 110 # 0 + 100 + 10 (position + width + margin)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_empty_list(self):
|
||||
"""Test conflict resolution with empty positions list"""
|
||||
result = resolve_text_conflicts([], 500, 400)
|
||||
assert result == []
|
||||
419
tests/unit/test_text_processor.py
Normal file
419
tests/unit/test_text_processor.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Unit tests for text_processor.py functions.
|
||||
Tests the core text processing logic in isolation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to path for importing modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from fixtures.sample_data import *
|
||||
from src.core.text_processor import (
|
||||
process_multiline_text,
|
||||
wrap_text_to_width,
|
||||
render_text_with_stroke,
|
||||
calculate_text_metrics,
|
||||
apply_text_effects,
|
||||
optimize_text_layout,
|
||||
rectangles_overlap,
|
||||
find_non_overlapping_position,
|
||||
validate_text_rendering
|
||||
)
|
||||
|
||||
|
||||
class TestWrapTextToWidth:
|
||||
"""Test the wrap_text_to_width function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wrap_text_short_text_fits(self, sample_font, mock_image_draw):
|
||||
"""Test that short text that fits is returned as-is"""
|
||||
# Mock textbbox to return small dimensions
|
||||
mock_image_draw.textbbox.return_value = (0, 0, 50, 20)
|
||||
|
||||
result = wrap_text_to_width(SHORT_TEXT, sample_font, 100)
|
||||
|
||||
assert result == [SHORT_TEXT]
|
||||
assert len(result) == 1
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wrap_text_empty_string(self, sample_font):
|
||||
"""Test empty string input"""
|
||||
result = wrap_text_to_width("", sample_font, 100)
|
||||
assert result == [""]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wrap_text_whitespace_only(self, sample_font):
|
||||
"""Test whitespace-only string"""
|
||||
result = wrap_text_to_width(" ", sample_font, 100)
|
||||
assert result == [""]
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('PIL.Image.new')
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_wrap_text_needs_wrapping(self, mock_draw_class, mock_image_new, sample_font):
|
||||
"""Test text that needs to be wrapped"""
|
||||
# Setup mocks
|
||||
mock_img = Mock()
|
||||
mock_draw = Mock()
|
||||
mock_image_new.return_value = mock_img
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
# Mock textbbox to simulate text getting too wide
|
||||
def textbbox_side_effect(pos, text, font):
|
||||
if text == LONG_TEXT:
|
||||
return (0, 0, 300, 20) # Too wide for max_width=100
|
||||
elif "This is a much longer" in text:
|
||||
return (0, 0, 120, 20) # Still too wide
|
||||
elif "This is a much" in text:
|
||||
return (0, 0, 80, 20) # Fits
|
||||
elif "longer piece of text" in text:
|
||||
return (0, 0, 90, 20) # Fits
|
||||
else:
|
||||
return (0, 0, 50, 20) # Default small size
|
||||
|
||||
mock_draw.textbbox.side_effect = textbbox_side_effect
|
||||
|
||||
result = wrap_text_to_width(LONG_TEXT, sample_font, 100)
|
||||
|
||||
assert len(result) > 1 # Should be split into multiple lines
|
||||
assert all(isinstance(line, str) for line in result)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wrap_text_single_word_too_long(self, sample_font):
|
||||
"""Test handling of single word that's too long"""
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 200, 20) # Too wide
|
||||
|
||||
result = wrap_text_to_width("SuperLongWordThatCannotFit", sample_font, 50)
|
||||
|
||||
# Should still return the word, even if it doesn't fit
|
||||
assert len(result) >= 1
|
||||
assert "SuperLongWordThatCannotFit" in result
|
||||
|
||||
|
||||
class TestProcessMultilineText:
|
||||
"""Test the process_multiline_text function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_single_line_text(self, mock_wrap, sample_font):
|
||||
"""Test processing single line text"""
|
||||
mock_wrap.return_value = [SHORT_TEXT]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
result = process_multiline_text(SHORT_TEXT, sample_font, 200, 100)
|
||||
|
||||
assert result['line_count'] == 1
|
||||
assert result['lines'] == [SHORT_TEXT]
|
||||
assert result['total_height'] == 20
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_multiline_text_basic(self, mock_wrap, sample_font):
|
||||
"""Test processing actual multiline text"""
|
||||
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 80, 20)
|
||||
|
||||
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 200)
|
||||
|
||||
assert result['line_count'] == 4
|
||||
assert len(result['lines']) == 4
|
||||
assert result['total_height'] == 80 # 4 lines * 20 height
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_process_empty_text(self, sample_font):
|
||||
"""Test processing empty text"""
|
||||
result = process_multiline_text("", sample_font, 200, 100)
|
||||
|
||||
assert result['line_count'] == 1
|
||||
assert result['lines'] == [""]
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_height_limit_exceeded(self, mock_wrap, sample_font):
|
||||
"""Test text processing when height limit is exceeded"""
|
||||
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 80, 30) # Each line is 30px high
|
||||
|
||||
# With max_height=50, only 1 line should fit
|
||||
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 50)
|
||||
|
||||
assert result['line_count'] <= 2 # Should stop when height limit reached
|
||||
assert result['total_height'] <= 60 # Should not exceed much
|
||||
|
||||
|
||||
class TestCalculateTextMetrics:
|
||||
"""Test the calculate_text_metrics function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_calculate_basic_metrics(self):
|
||||
"""Test basic text metrics calculation"""
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (2, 1, 102, 21) # x1, y1, x2, y2
|
||||
|
||||
# Create a mock font with getmetrics method
|
||||
mock_font = Mock()
|
||||
mock_font.getmetrics.return_value = (18, 4) # ascent, descent
|
||||
|
||||
result = calculate_text_metrics(SHORT_TEXT, mock_font)
|
||||
|
||||
assert result['width'] == 100 # 102 - 2
|
||||
assert result['height'] == 20 # 21 - 1
|
||||
assert result['left_bearing'] == 2
|
||||
assert result['top_bearing'] == 1
|
||||
assert result['ascent'] == 18
|
||||
assert result['descent'] == 4
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_calculate_metrics_font_error(self):
|
||||
"""Test metrics calculation when font.getmetrics fails"""
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
# Create a mock font with getmetrics that raises exception
|
||||
mock_font = Mock()
|
||||
mock_font.getmetrics.side_effect = Exception("Font error")
|
||||
|
||||
result = calculate_text_metrics(SHORT_TEXT, mock_font)
|
||||
|
||||
assert result['width'] == 100
|
||||
assert result['height'] == 20
|
||||
assert result['ascent'] == 20 # Should default to height
|
||||
assert result['descent'] == 0
|
||||
|
||||
|
||||
class TestRectanglesOverlap:
|
||||
"""Test the rectangles_overlap function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_overlap_separate_rects(self):
|
||||
"""Test rectangles that don't overlap"""
|
||||
rect1 = (0, 0, 50, 50) # Top-left
|
||||
rect2 = (100, 100, 50, 50) # Bottom-right, separate
|
||||
|
||||
assert not rectangles_overlap(rect1, rect2)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_clear_overlap(self):
|
||||
"""Test rectangles that clearly overlap"""
|
||||
rect1 = (0, 0, 100, 100)
|
||||
rect2 = (50, 50, 100, 100) # Overlaps in center
|
||||
|
||||
assert rectangles_overlap(rect1, rect2)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_touching_edges_no_overlap(self):
|
||||
"""Test rectangles that touch but don't overlap"""
|
||||
rect1 = (0, 0, 50, 50)
|
||||
rect2 = (50, 0, 50, 50) # Touching right edge
|
||||
|
||||
assert not rectangles_overlap(rect1, rect2)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_identical_rectangles(self):
|
||||
"""Test identical rectangles (complete overlap)"""
|
||||
rect1 = (10, 10, 50, 50)
|
||||
rect2 = (10, 10, 50, 50)
|
||||
|
||||
assert rectangles_overlap(rect1, rect2)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_one_inside_other(self):
|
||||
"""Test one rectangle completely inside another"""
|
||||
rect1 = (0, 0, 100, 100) # Outer
|
||||
rect2 = (25, 25, 50, 50) # Inner
|
||||
|
||||
assert rectangles_overlap(rect1, rect2)
|
||||
|
||||
|
||||
class TestFindNonOverlappingPosition:
|
||||
"""Test the find_non_overlapping_position function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_find_position_no_existing_blocks(self):
|
||||
"""Test finding position when no existing blocks"""
|
||||
x, y = find_non_overlapping_position(100, 50, [], 500, 400)
|
||||
|
||||
assert x == 0
|
||||
assert y == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_find_position_with_existing_blocks(self):
|
||||
"""Test finding position avoiding existing blocks"""
|
||||
existing_blocks = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50}
|
||||
]
|
||||
|
||||
x, y = find_non_overlapping_position(100, 50, existing_blocks, 500, 400)
|
||||
|
||||
# Should find a position that doesn't overlap
|
||||
assert x >= 0 and y >= 0
|
||||
# Verify it doesn't overlap with existing block
|
||||
new_rect = (x, y, 100, 50)
|
||||
existing_rect = (0, 0, 100, 50)
|
||||
assert not rectangles_overlap(new_rect, existing_rect)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_find_position_no_space_available(self):
|
||||
"""Test when no space is available (fallback to 0,0)"""
|
||||
# Fill entire canvas with blocks
|
||||
existing_blocks = []
|
||||
for y in range(0, 400, 20):
|
||||
for x in range(0, 500, 20):
|
||||
existing_blocks.append({'x': x, 'y': y, 'width': 20, 'height': 20})
|
||||
|
||||
x, y = find_non_overlapping_position(100, 50, existing_blocks, 500, 400)
|
||||
|
||||
# Should fall back to 0, 0
|
||||
assert x == 0 and y == 0
|
||||
|
||||
|
||||
class TestValidateTextRendering:
|
||||
"""Test the validate_text_rendering function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_valid_positions(self):
|
||||
"""Test validation with valid text positions"""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 500
|
||||
mock_img.height = 400
|
||||
|
||||
positions = [
|
||||
{'x': 10, 'y': 10, 'width': 100, 'height': 50},
|
||||
{'x': 200, 'y': 200, 'width': 80, 'height': 40}
|
||||
]
|
||||
|
||||
results = validate_text_rendering(mock_img, positions)
|
||||
|
||||
assert len(results) == 2
|
||||
assert all(r['valid'] for r in results)
|
||||
assert all(len(r['issues']) == 0 for r in results)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_negative_positions(self):
|
||||
"""Test validation with negative positions"""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 500
|
||||
mock_img.height = 400
|
||||
|
||||
positions = [
|
||||
{'x': -10, 'y': 10, 'width': 100, 'height': 50}
|
||||
]
|
||||
|
||||
results = validate_text_rendering(mock_img, positions)
|
||||
|
||||
assert len(results) == 1
|
||||
assert not results[0]['valid']
|
||||
assert 'Negative position' in results[0]['issues']
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_exceeds_bounds(self):
|
||||
"""Test validation with positions exceeding image bounds"""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 500
|
||||
mock_img.height = 400
|
||||
|
||||
positions = [
|
||||
{'x': 450, 'y': 10, 'width': 100, 'height': 50}, # Exceeds width
|
||||
{'x': 10, 'y': 350, 'width': 100, 'height': 100} # Exceeds height
|
||||
]
|
||||
|
||||
results = validate_text_rendering(mock_img, positions)
|
||||
|
||||
assert len(results) == 2
|
||||
assert not results[0]['valid']
|
||||
assert not results[1]['valid']
|
||||
assert 'Exceeds image width' in results[0]['issues']
|
||||
assert 'Exceeds image height' in results[1]['issues']
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_zero_dimensions(self):
|
||||
"""Test validation with zero dimensions"""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 500
|
||||
mock_img.height = 400
|
||||
|
||||
positions = [
|
||||
{'x': 10, 'y': 10, 'width': 0, 'height': 50},
|
||||
{'x': 10, 'y': 100, 'width': 100, 'height': 0}
|
||||
]
|
||||
|
||||
results = validate_text_rendering(mock_img, positions)
|
||||
|
||||
assert len(results) == 2
|
||||
assert not results[0]['valid']
|
||||
assert not results[1]['valid']
|
||||
assert 'Zero dimensions' in results[0]['issues']
|
||||
assert 'Zero dimensions' in results[1]['issues']
|
||||
|
||||
|
||||
class TestOptimizeTextLayout:
|
||||
"""Test the optimize_text_layout function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.find_non_overlapping_position')
|
||||
def test_optimize_no_overlaps(self, mock_find_pos):
|
||||
"""Test optimization when no overlaps exist"""
|
||||
text_blocks = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 50, 'text': 'Block 1'},
|
||||
{'x': 200, 'y': 200, 'width': 80, 'height': 40, 'text': 'Block 2'}
|
||||
]
|
||||
|
||||
result = optimize_text_layout(text_blocks, 500, 400)
|
||||
|
||||
assert len(result) == 2
|
||||
# Positions should remain unchanged since no overlaps
|
||||
assert result[0]['x'] == 0 and result[0]['y'] == 0
|
||||
assert result[1]['x'] == 200 and result[1]['y'] == 200
|
||||
# find_non_overlapping_position should not be called
|
||||
mock_find_pos.assert_not_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.find_non_overlapping_position')
|
||||
def test_optimize_with_overlaps(self, mock_find_pos):
|
||||
"""Test optimization when overlaps exist"""
|
||||
mock_find_pos.return_value = (150, 100) # New position
|
||||
|
||||
text_blocks = [
|
||||
{'x': 0, 'y': 0, 'width': 100, 'height': 100, 'text': 'Block 1'},
|
||||
{'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Block 2'} # Overlaps
|
||||
]
|
||||
|
||||
result = optimize_text_layout(text_blocks, 500, 400)
|
||||
|
||||
assert len(result) == 2
|
||||
# First block should remain unchanged
|
||||
assert result[0]['x'] == 0 and result[0]['y'] == 0
|
||||
# Second block should be moved to avoid overlap
|
||||
assert result[1]['x'] == 150 and result[1]['y'] == 100
|
||||
# find_non_overlapping_position should be called
|
||||
mock_find_pos.assert_called_once()
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_optimize_empty_list(self):
|
||||
"""Test optimization with empty text blocks list"""
|
||||
result = optimize_text_layout([], 500, 400)
|
||||
|
||||
assert result == []
|
||||
Reference in New Issue
Block a user