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
377 lines
14 KiB
Python
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
|