pyPhotoAlbum/tests/test_element_maximizer.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

377 lines
14 KiB
Python

"""
Unit tests for ElementMaximizer class.
Tests each atomic method independently for better test coverage and debugging.
"""
import pytest
from unittest.mock import Mock
from pyPhotoAlbum.alignment import ElementMaximizer
from pyPhotoAlbum.models import BaseLayoutElement
class TestElementMaximizer:
"""Test suite for ElementMaximizer class."""
@pytest.fixture
def mock_element(self):
"""Create a mock element for testing."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (10.0, 10.0)
elem.size = (50.0, 50.0)
return elem
@pytest.fixture
def simple_elements(self):
"""Create a simple list of mock elements."""
elem1 = Mock(spec=BaseLayoutElement)
elem1.position = (10.0, 10.0)
elem1.size = (50.0, 50.0)
elem2 = Mock(spec=BaseLayoutElement)
elem2.position = (70.0, 10.0)
elem2.size = (50.0, 50.0)
return [elem1, elem2]
@pytest.fixture
def maximizer(self, simple_elements):
"""Create an ElementMaximizer instance with simple elements."""
page_size = (200.0, 200.0)
min_gap = 5.0
return ElementMaximizer(simple_elements, page_size, min_gap)
def test_init_records_initial_states(self, simple_elements):
"""Test that __init__ records initial states correctly."""
page_size = (200.0, 200.0)
min_gap = 5.0
maximizer = ElementMaximizer(simple_elements, page_size, min_gap)
assert len(maximizer.changes) == 2
assert maximizer.changes[0][0] is simple_elements[0]
assert maximizer.changes[0][1] == (10.0, 10.0) # position
assert maximizer.changes[0][2] == (50.0, 50.0) # size
def test_check_collision_with_left_boundary(self, maximizer):
"""Test collision detection with left page boundary."""
# Position element too close to left edge
maximizer.elements[0].position = (2.0, 10.0)
new_size = (50.0, 50.0)
assert maximizer.check_collision(0, new_size) is True
def test_check_collision_with_top_boundary(self, maximizer):
"""Test collision detection with top page boundary."""
# Position element too close to top edge
maximizer.elements[0].position = (10.0, 2.0)
new_size = (50.0, 50.0)
assert maximizer.check_collision(0, new_size) is True
def test_check_collision_with_right_boundary(self, maximizer):
"""Test collision detection with right page boundary."""
# Element would extend beyond right boundary
maximizer.elements[0].position = (150.0, 10.0)
new_size = (50.0, 50.0) # 150 + 50 = 200, needs min_gap
assert maximizer.check_collision(0, new_size) is True
def test_check_collision_with_bottom_boundary(self, maximizer):
"""Test collision detection with bottom page boundary."""
# Element would extend beyond bottom boundary
maximizer.elements[0].position = (10.0, 150.0)
new_size = (50.0, 50.0) # 150 + 50 = 200, needs min_gap
assert maximizer.check_collision(0, new_size) is True
def test_check_collision_with_other_element(self, maximizer):
"""Test collision detection with other elements."""
# Make elem1 grow into elem2's space
new_size = (65.0, 50.0) # Would overlap with elem2 at x=70
assert maximizer.check_collision(0, new_size) is True
def test_check_collision_no_collision(self, maximizer):
"""Test that valid sizes don't trigger collision."""
# Element has plenty of space
new_size = (55.0, 55.0)
assert maximizer.check_collision(0, new_size) is False
def test_find_max_scale_basic(self, maximizer):
"""Test binary search finds maximum scale factor."""
current_scale = 1.0
max_scale = maximizer.find_max_scale(0, current_scale)
# Should find a scale larger than 1.0 since there's room to grow
assert max_scale > current_scale
def test_find_max_scale_constrained_by_boundary(self):
"""Test scaling is constrained by page boundaries."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (10.0, 10.0)
elem.size = (80.0, 80.0)
maximizer = ElementMaximizer([elem], (100.0, 100.0), 5.0)
current_scale = 1.0
max_scale = maximizer.find_max_scale(0, current_scale)
# Element at (10,10) with size (80,80) reaches (90,90)
# With min_gap=5, max is (95,95), so max_scale should be around 1.0625
assert 1.0 < max_scale < 1.1
def test_find_max_scale_constrained_by_element(self, maximizer):
"""Test scaling is constrained by nearby elements."""
# Elements are close together, limited growth
current_scale = 1.0
max_scale = maximizer.find_max_scale(0, current_scale)
# There's a gap of 10mm between elements (70-60), with min_gap=5
# So limited growth is possible
assert max_scale > 1.0
assert max_scale < 1.2 # Won't grow too much
def test_grow_iteration_with_space(self, maximizer):
"""Test grow_iteration when elements have space to grow."""
scales = [1.0, 1.0]
growth_rate = 0.05
result = maximizer.grow_iteration(scales, growth_rate)
assert result is True # Some growth occurred
assert scales[0] > 1.0
assert scales[1] > 1.0
def test_grow_iteration_no_space(self):
"""Test grow_iteration when elements have no space to grow."""
# Create elements that fill the entire page
elem1 = Mock(spec=BaseLayoutElement)
elem1.position = (5.0, 5.0)
elem1.size = (190.0, 190.0)
maximizer = ElementMaximizer([elem1], (200.0, 200.0), 5.0)
scales = [1.0]
growth_rate = 0.05
result = maximizer.grow_iteration(scales, growth_rate)
# Should return False since no growth is possible
assert result is False
assert scales[0] == 1.0
def test_check_element_collision_with_overlap(self, maximizer):
"""Test element collision detection with overlap."""
elem = maximizer.elements[0]
new_pos = (65.0, 10.0) # Would overlap with elem2 at (70, 10)
assert maximizer.check_element_collision(elem, new_pos) is True
def test_check_element_collision_no_overlap(self, maximizer):
"""Test element collision detection without overlap."""
elem = maximizer.elements[0]
new_pos = (15.0, 15.0) # Safe position
assert maximizer.check_element_collision(elem, new_pos) is False
def test_center_element_horizontally_centering(self):
"""Test horizontal centering when space is available."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (20.0, 50.0) # Off-center
elem.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
maximizer.center_element_horizontally(elem)
# Element should move towards center
# space_left = 20 - 5 = 15
# space_right = (200 - 5) - (20 + 50) = 125
# adjust_x = (125 - 15) / 4 = 27.5
# new_x should be around 47.5
new_x = elem.position[0]
assert new_x > 20.0 # Moved right towards center
def test_center_element_horizontally_already_centered(self):
"""Test horizontal centering when already centered."""
elem = Mock(spec=BaseLayoutElement)
# Centered position: (200 - 50) / 2 = 75
elem.position = (75.0, 50.0)
elem.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
original_x = elem.position[0]
maximizer.center_element_horizontally(elem)
# Should stay approximately the same
assert abs(elem.position[0] - original_x) < 1.0
def test_center_element_vertically_centering(self):
"""Test vertical centering when space is available."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (50.0, 20.0) # Off-center vertically
elem.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
maximizer.center_element_vertically(elem)
# Element should move towards vertical center
new_y = elem.position[1]
assert new_y > 20.0 # Moved down towards center
def test_center_element_vertically_already_centered(self):
"""Test vertical centering when already centered."""
elem = Mock(spec=BaseLayoutElement)
# Centered position: (200 - 50) / 2 = 75
elem.position = (50.0, 75.0)
elem.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
original_y = elem.position[1]
maximizer.center_element_vertically(elem)
# Should stay approximately the same
assert abs(elem.position[1] - original_y) < 1.0
def test_center_elements_calls_both_directions(self, maximizer):
"""Test that center_elements processes both horizontal and vertical."""
initial_positions = [elem.position for elem in maximizer.elements]
maximizer.center_elements()
# At least some elements should potentially move
# (or stay same if already centered)
assert len(maximizer.elements) == 2
def test_maximize_integration(self, maximizer):
"""Test the full maximize method integration."""
initial_sizes = [elem.size for elem in maximizer.elements]
changes = maximizer.maximize(max_iterations=50, growth_rate=0.05)
# Should return changes for undo
assert len(changes) == 2
assert changes[0][1] == (10.0, 10.0) # old position
assert changes[0][2] == (50.0, 50.0) # old size
# Elements should have grown
final_sizes = [elem.size for elem in maximizer.elements]
assert final_sizes[0][0] >= initial_sizes[0][0]
assert final_sizes[0][1] >= initial_sizes[0][1]
def test_maximize_empty_elements(self):
"""Test maximize with empty element list."""
from pyPhotoAlbum.alignment import AlignmentManager
result = AlignmentManager.maximize_pattern([], (200.0, 200.0))
assert result == []
def test_maximize_single_element_grows_to_fill_page(self):
"""Test that a single element grows to fill the available page."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (50.0, 50.0)
elem.size = (10.0, 10.0) # Small initial size
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
maximizer.maximize(max_iterations=100, growth_rate=0.1)
# Element should grow significantly
final_width, final_height = elem.size
assert final_width > 50.0 # Much larger than initial 10.0
assert final_height > 50.0
class TestElementMaximizerEdgeCases:
"""Test edge cases and boundary conditions."""
def test_zero_min_gap(self):
"""Test with zero minimum gap."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (0.0, 0.0)
elem.size = (100.0, 100.0)
maximizer = ElementMaximizer([elem], (100.0, 100.0), 0.0)
# Should not collide with boundaries at exact edges
assert maximizer.check_collision(0, (100.0, 100.0)) is False
def test_very_large_min_gap(self):
"""Test with very large minimum gap."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (50.0, 50.0)
elem.size = (10.0, 10.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 50.0)
# Element at (50,50) with size (10,10) is OK since:
# - left edge at 50 > min_gap (50)
# - top edge at 50 > min_gap (50)
# - right edge at 60 < page_width (200) - min_gap (50) = 150
# Current size should NOT collide
assert maximizer.check_collision(0, (10.0, 10.0)) is False
# But if we try to position too close to an edge, it should collide
elem.position = (40.0, 50.0) # Left edge at 40 < min_gap
assert maximizer.check_collision(0, (10.0, 10.0)) is True
def test_elements_touching_with_exact_min_gap(self):
"""Test elements that are exactly min_gap apart."""
elem1 = Mock(spec=BaseLayoutElement)
elem1.position = (10.0, 10.0)
elem1.size = (50.0, 50.0)
elem2 = Mock(spec=BaseLayoutElement)
elem2.position = (65.0, 10.0) # Exactly 5mm gap (60 + 5 = 65)
elem2.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem1, elem2], (200.0, 200.0), 5.0)
# Should not grow since they're at minimum gap
result = maximizer.check_collision(0, (50.0, 50.0))
assert result is False # Current size is OK
# But slightly larger would collide
result = maximizer.check_collision(0, (51.0, 50.0))
assert result is True
def test_find_max_scale_tolerance(self):
"""Test that binary search respects tolerance parameter."""
elem = Mock(spec=BaseLayoutElement)
elem.position = (10.0, 10.0)
elem.size = (50.0, 50.0)
maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
# Test with different tolerances
scale_loose = maximizer.find_max_scale(0, 1.0, tolerance=0.1)
scale_tight = maximizer.find_max_scale(0, 1.0, tolerance=0.0001)
# Tighter tolerance might find slightly different result
# Both should be greater than 1.0
assert scale_loose > 1.0
assert scale_tight > 1.0
assert abs(scale_loose - scale_tight) < 0.15 # Should be similar
def test_grow_iteration_alternating_growth(self):
"""Test that elements can alternate growth in tight spaces."""
# Create two elements side by side with limited space
elem1 = Mock(spec=BaseLayoutElement)
elem1.position = (10.0, 10.0)
elem1.size = (40.0, 40.0)
elem2 = Mock(spec=BaseLayoutElement)
elem2.position = (60.0, 10.0)
elem2.size = (40.0, 40.0)
maximizer = ElementMaximizer([elem1, elem2], (200.0, 100.0), 5.0)
scales = [1.0, 1.0]
# First iteration should allow growth
result1 = maximizer.grow_iteration(scales, 0.05)
assert result1 is True
# Continue growing until no more growth
for _ in range(50):
if not maximizer.grow_iteration(scales, 0.05):
break
# Both should have grown
assert scales[0] > 1.0
assert scales[1] > 1.0