auto flake and corrections

This commit is contained in:
2025-11-08 23:46:15 +01:00
parent 1ea870eef5
commit 781a9b6c08
81 changed files with 4646 additions and 3718 deletions
+126 -113
View File
@@ -5,14 +5,12 @@ This test focuses on verifying that the document layouter properly
integrates word spacing constraints from the style system.
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from typing import List, Optional
from unittest.mock import Mock, patch
from pyWebLayout.layout.document_layouter import paragraph_layouter, table_layouter, DocumentLayouter
from pyWebLayout.style.abstract_style import AbstractStyle
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
from pyWebLayout.abstract.block import Table, TableRow, TableCell
from pyWebLayout.style.concrete_style import StyleResolver, RenderingContext
from pyWebLayout.abstract.block import Table
from pyWebLayout.concrete.table import TableStyle
@@ -29,21 +27,21 @@ class TestDocumentLayouter:
self.mock_page.draw = Mock()
self.mock_page.can_fit_line = Mock(return_value=True)
self.mock_page.add_child = Mock()
# Create mock page style with all required numeric properties
self.mock_page.style = Mock()
self.mock_page.style.max_font_size = 72 # Reasonable maximum font size
self.mock_page.style.line_spacing_multiplier = 1.2 # Standard line spacing
# Create mock style resolver
self.mock_style_resolver = Mock()
self.mock_page.style_resolver = self.mock_style_resolver
# Create mock paragraph
self.mock_paragraph = Mock()
self.mock_paragraph.line_height = 20
self.mock_paragraph.style = AbstractStyle()
# Create mock words
self.mock_words = []
for i in range(5):
@@ -51,20 +49,22 @@ class TestDocumentLayouter:
word.text = f"word{i}"
self.mock_words.append(word)
self.mock_paragraph.words = self.mock_words
# Create mock concrete style with word spacing constraints
self.mock_concrete_style = Mock()
self.mock_concrete_style.word_spacing_min = 2.0
self.mock_concrete_style.word_spacing_max = 8.0
self.mock_concrete_style.text_align = "left"
# Create mock font that returns proper numeric metrics (not Mock objects)
mock_font = Mock()
# CRITICAL: getmetrics() must return actual numeric values, not Mock objects
# This prevents "TypeError: '>' not supported between instances of 'Mock' and 'Mock'"
mock_font.getmetrics.return_value = (12, 4) # (ascent, descent) as actual integers
# This prevents "TypeError: '>' not supported between instances of 'Mock'
# and 'Mock'"
# (ascent, descent) as actual integers
mock_font.getmetrics.return_value = (12, 4)
mock_font.font = mock_font # For accessing .font property
# Create mock font object that can be used by create_font
mock_font_instance = Mock()
mock_font_instance.font = mock_font
@@ -72,7 +72,7 @@ class TestDocumentLayouter:
mock_font_instance.colour = (0, 0, 0)
mock_font_instance.background = (255, 255, 255, 0)
self.mock_concrete_style.create_font = Mock(return_value=mock_font_instance)
# Update mock words to have proper style with font
for word in self.mock_words:
word.style = Mock()
@@ -84,34 +84,39 @@ class TestDocumentLayouter:
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
@patch('pyWebLayout.layout.document_layouter.Line')
def test_paragraph_layouter_basic_flow(self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
def test_paragraph_layouter_basic_flow(
self,
mock_line_class,
mock_style_registry_class,
mock_style_resolver_class):
"""Test basic paragraph layouter functionality."""
# Setup mocks for StyleResolver and ConcreteStyleRegistry
mock_style_resolver = Mock()
mock_style_resolver_class.return_value = mock_style_resolver
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
mock_line = Mock()
mock_line_class.return_value = mock_line
mock_line.add_word.return_value = (True, None) # All words fit successfully
# Call function
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
# Verify results
success, failed_word_index, remaining_pretext = result
assert success is True
assert failed_word_index is None
assert remaining_pretext is None
# Verify StyleResolver and ConcreteStyleRegistry were created correctly
mock_style_resolver_class.assert_called_once()
mock_style_registry_class.assert_called_once_with(mock_style_resolver)
mock_style_registry.get_concrete_style.assert_called_once_with(self.mock_paragraph.style)
mock_style_registry.get_concrete_style.assert_called_once_with(
self.mock_paragraph.style)
# Verify Line was created with correct spacing constraints
expected_spacing = (2, 8) # From mock_concrete_style
mock_line_class.assert_called_once()
@@ -121,36 +126,37 @@ class TestDocumentLayouter:
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
@patch('pyWebLayout.layout.document_layouter.Line')
def test_paragraph_layouter_word_spacing_constraints_extraction(self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
def test_paragraph_layouter_word_spacing_constraints_extraction(
self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
"""Test that word spacing constraints are correctly extracted from style."""
# Create concrete style with specific constraints
concrete_style = Mock()
concrete_style.word_spacing_min = 5.5
concrete_style.word_spacing_max = 15.2
concrete_style.text_align = "justify"
# Create a mock font that concrete_style.create_font returns
mock_font = Mock()
mock_font.font = Mock()
mock_font.font.getmetrics.return_value = (12, 4)
mock_font.font_size = 16
concrete_style.create_font = Mock(return_value=mock_font)
# Setup StyleResolver and ConcreteStyleRegistry mocks
mock_style_resolver = Mock()
mock_style_resolver_class.return_value = mock_style_resolver
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = concrete_style
mock_line = Mock()
mock_line_class.return_value = mock_line
mock_line.add_word.return_value = (True, None)
# Call function
paragraph_layouter(self.mock_paragraph, self.mock_page)
# Verify spacing constraints were extracted correctly (converted to int)
expected_spacing = (5, 15) # int() conversion of 5.5 and 15.2
call_args = mock_line_class.call_args
@@ -159,30 +165,34 @@ class TestDocumentLayouter:
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
@patch('pyWebLayout.layout.document_layouter.Line')
@patch('pyWebLayout.layout.document_layouter.Text')
def test_paragraph_layouter_line_overflow(self, mock_text_class, mock_line_class, mock_style_registry_class):
def test_paragraph_layouter_line_overflow(
self,
mock_text_class,
mock_line_class,
mock_style_registry_class):
"""Test handling of line overflow when words don't fit."""
# Setup mocks
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
# Create two mock lines with proper size attribute
mock_line1 = Mock()
mock_line1.size = (400, 20) # (width, height)
mock_line2 = Mock()
mock_line2.size = (400, 20) # (width, height)
mock_line_class.side_effect = [mock_line1, mock_line2]
# Mock Text.from_word to return mock text objects with numeric width
mock_text = Mock()
mock_text.width = 50 # Reasonable word width
mock_text_class.from_word.return_value = mock_text
# First line: first 2 words fit, third doesn't
# Second line: remaining words fit
mock_line1.add_word.side_effect = [
(True, None), # word0 fits
(True, None), # word1 fits
(True, None), # word1 fits
(False, None), # word2 doesn't fit
]
mock_line2.add_word.side_effect = [
@@ -190,42 +200,43 @@ class TestDocumentLayouter:
(True, None), # word3 fits
(True, None), # word4 fits
]
# Call function
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
# Verify results
success, failed_word_index, remaining_pretext = result
assert success is True
assert failed_word_index is None
assert remaining_pretext is None
# Verify two lines were created
assert mock_line_class.call_count == 2
assert self.mock_page.add_child.call_count == 2
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
@patch('pyWebLayout.layout.document_layouter.Line')
def test_paragraph_layouter_page_full(self, mock_line_class, mock_style_registry_class):
def test_paragraph_layouter_page_full(
self, mock_line_class, mock_style_registry_class):
"""Test handling when page runs out of space."""
# Setup mocks
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
# Page can fit first line but not second
self.mock_page.can_fit_line.side_effect = [True, False]
mock_line = Mock()
mock_line_class.return_value = mock_line
mock_line.add_word.side_effect = [
(True, None), # word0 fits
(False, None), # word1 doesn't fit, need new line
]
# Call function
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
# Verify results indicate page is full
success, failed_word_index, remaining_pretext = result
assert success is False
@@ -236,9 +247,9 @@ class TestDocumentLayouter:
"""Test handling of empty paragraph."""
empty_paragraph = Mock()
empty_paragraph.words = []
result = paragraph_layouter(empty_paragraph, self.mock_page)
success, failed_word_index, remaining_pretext = result
assert success is True
assert failed_word_index is None
@@ -247,7 +258,7 @@ class TestDocumentLayouter:
def test_paragraph_layouter_invalid_start_word(self):
"""Test handling of invalid start_word index."""
result = paragraph_layouter(self.mock_paragraph, self.mock_page, start_word=10)
success, failed_word_index, remaining_pretext = result
assert success is True
assert failed_word_index is None
@@ -259,10 +270,10 @@ class TestDocumentLayouter:
# Setup mock
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
# Create layouter
layouter = DocumentLayouter(self.mock_page)
# Verify initialization
assert layouter.page == self.mock_page
mock_style_registry_class.assert_called_once_with(self.mock_page.style_resolver)
@@ -271,12 +282,13 @@ class TestDocumentLayouter:
def test_document_layouter_layout_paragraph(self, mock_paragraph_layouter):
"""Test DocumentLayouter.layout_paragraph method."""
mock_paragraph_layouter.return_value = (True, None, None)
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
layouter = DocumentLayouter(self.mock_page)
result = layouter.layout_paragraph(self.mock_paragraph, start_word=2, pretext="test")
result = layouter.layout_paragraph(
self.mock_paragraph, start_word=2, pretext="test")
# Verify the function was called correctly
mock_paragraph_layouter.assert_called_once_with(
self.mock_paragraph, self.mock_page, 2, "test"
@@ -286,46 +298,46 @@ class TestDocumentLayouter:
def test_document_layouter_layout_document_success(self):
"""Test DocumentLayouter.layout_document with successful layout."""
from pyWebLayout.abstract import Paragraph
# Create Mock paragraphs that pass isinstance checks
paragraphs = [
Mock(spec=Paragraph),
Mock(spec=Paragraph),
Mock(spec=Paragraph)
]
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
layouter = DocumentLayouter(self.mock_page)
# Mock the layout_paragraph method to return success
layouter.layout_paragraph = Mock(return_value=(True, None, None))
result = layouter.layout_document(paragraphs)
assert result is True
assert layouter.layout_paragraph.call_count == 3
def test_document_layouter_layout_document_failure(self):
"""Test DocumentLayouter.layout_document with layout failure."""
from pyWebLayout.abstract import Paragraph
# Create Mock paragraphs that pass isinstance checks
paragraphs = [
Mock(spec=Paragraph),
Mock(spec=Paragraph)
]
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
layouter = DocumentLayouter(self.mock_page)
# Mock the layout_paragraph method: first succeeds, second fails
layouter.layout_paragraph = Mock(side_effect=[
(True, None, None), # First paragraph succeeds
(False, 3, None), # Second paragraph fails
])
result = layouter.layout_document(paragraphs)
assert result is False
assert layouter.layout_paragraph.call_count == 2
@@ -334,39 +346,40 @@ class TestDocumentLayouter:
# Create real style objects
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
abstract_style = AbstractStyle(
word_spacing=4.0,
word_spacing_min=2.0,
word_spacing_max=10.0
)
concrete_style = resolver.resolve_style(abstract_style)
# Verify constraints are resolved correctly
assert concrete_style.word_spacing_min == 2.0
assert concrete_style.word_spacing_max == 10.0
# This demonstrates the integration works end-to-end
class TestWordSpacingConstraintsInLayout:
"""Specific tests for word spacing constraints in layout context."""
def test_different_spacing_scenarios(self):
"""Test various word spacing constraint scenarios."""
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
test_cases = [
# (word_spacing, word_spacing_min, word_spacing_max, expected_min, expected_max)
(None, None, None, 2.0, 8.0), # Default case
(5.0, None, None, 5.0, 10.0), # Only base specified
(4.0, 2.0, 8.0, 2.0, 8.0), # All specified
(3.0, 1.0, None, 1.0, 3.0), # Min specified, max = max(word_spacing, min*2) = max(3.0, 2.0) = 3.0
(6.0, None, 12.0, 6.0, 12.0), # Max specified, min from base
# Min specified, max = max(word_spacing, min*2) = max(3.0, 2.0) = 3.0
(3.0, 1.0, None, 1.0, 3.0),
(6.0, None, 12.0, 6.0, 12.0), # Max specified, min from base
]
for word_spacing, min_spacing, max_spacing, expected_min, expected_max in test_cases:
style_kwargs = {}
if word_spacing is not None:
@@ -375,17 +388,17 @@ class TestWordSpacingConstraintsInLayout:
style_kwargs['word_spacing_min'] = min_spacing
if max_spacing is not None:
style_kwargs['word_spacing_max'] = max_spacing
abstract_style = AbstractStyle(**style_kwargs)
concrete_style = resolver.resolve_style(abstract_style)
assert concrete_style.word_spacing_min == expected_min, f"Failed for case: {style_kwargs}"
assert concrete_style.word_spacing_max == expected_max, f"Failed for case: {style_kwargs}"
class TestMultiPageLayout:
"""Test cases for multi-page document layout scenarios."""
def setup_method(self):
"""Set up test fixtures for multi-page tests."""
# Create multiple mock pages
@@ -401,19 +414,19 @@ class TestMultiPageLayout:
page.add_child = Mock()
page.style_resolver = Mock()
self.mock_pages.append(page)
# Create a long paragraph that will span multiple pages
self.long_paragraph = Mock()
self.long_paragraph.line_height = 25
self.long_paragraph.style = AbstractStyle()
# Create many words to ensure page overflow
self.long_paragraph.words = []
for i in range(50): # 50 words should definitely overflow a page
word = Mock()
word.text = f"word_{i:02d}"
self.long_paragraph.words.append(word)
# Create mock concrete style
self.mock_concrete_style = Mock()
self.mock_concrete_style.word_spacing_min = 3.0
@@ -421,7 +434,6 @@ class TestMultiPageLayout:
self.mock_concrete_style.text_align = "justify"
self.mock_concrete_style.create_font = Mock()
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
def test_document_layouter_multi_page_scenario(self, mock_style_registry_class):
"""Test DocumentLayouter handling multiple pages with continuation."""
@@ -429,7 +441,7 @@ class TestMultiPageLayout:
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
# Create a multi-page document layouter
class MultiPageDocumentLayouter(DocumentLayouter):
def __init__(self, pages):
@@ -437,7 +449,7 @@ class TestMultiPageLayout:
self.current_page_index = 0
self.page = pages[0]
self.style_registry = Mock()
def get_next_page(self):
"""Get the next available page."""
if self.current_page_index + 1 < len(self.pages):
@@ -445,47 +457,47 @@ class TestMultiPageLayout:
self.page = self.pages[self.current_page_index]
return self.page
return None
def layout_document_with_pagination(self, paragraphs):
"""Layout document with automatic pagination."""
for paragraph in paragraphs:
start_word = 0
pretext = None
while start_word < len(paragraph.words):
complete, next_word, remaining_pretext = self.layout_paragraph(
paragraph, start_word, pretext
)
if complete:
# Paragraph finished
break
if next_word is None:
# Error condition
return False, f"Failed to layout paragraph at word {start_word}"
# Try to get next page
next_page = self.get_next_page()
if not next_page:
return False, f"Ran out of pages at word {next_word}"
# Continue with remaining words on next page
start_word = next_word
pretext = remaining_pretext
return True, "All paragraphs laid out successfully"
# Create layouter with multiple pages
layouter = MultiPageDocumentLayouter(self.mock_pages)
# Mock the layout_paragraph method to simulate page filling
original_layout_paragraph = layouter.layout_paragraph
layouter.layout_paragraph
call_count = [0]
def mock_layout_paragraph(paragraph, start_word=0, pretext=None):
call_count[0] += 1
# Simulate different scenarios based on call count
if call_count[0] == 1:
# First page: can fit words 0-19, fails at word 20
@@ -498,19 +510,19 @@ class TestMultiPageLayout:
return (True, None, None)
else:
return (False, start_word, None)
layouter.layout_paragraph = mock_layout_paragraph
# Test multi-page layout
success, message = layouter.layout_document_with_pagination([self.long_paragraph])
success, message = layouter.layout_document_with_pagination(
[self.long_paragraph])
# Verify results
assert success is True
assert "successfully" in message
assert call_count[0] == 3 # Should have made 3 layout attempts
assert layouter.current_page_index == 2 # Should end on page 3 (index 2)
def test_realistic_multi_page_scenario(self):
"""Test a realistic scenario with actual content and page constraints."""
# Create realistic paragraph with varied content
@@ -522,7 +534,7 @@ class TestMultiPageLayout:
word_spacing_max=8.0,
text_align="justify"
)
# Create words of varying lengths (realistic text)
words = [
"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog.",
@@ -534,13 +546,13 @@ class TestMultiPageLayout:
"system", "to", "handle", "appropriately", "with", "the", "given",
"constraints", "and", "spacing", "requirements."
]
realistic_paragraph.words = []
for word_text in words:
word = Mock()
word.text = word_text
realistic_paragraph.words.append(word)
# Create page with realistic constraints
realistic_page = Mock()
realistic_page.border_size = 30
@@ -550,33 +562,34 @@ class TestMultiPageLayout:
realistic_page.draw = Mock()
realistic_page.add_child = Mock()
realistic_page.style_resolver = Mock()
# Simulate page that can fit approximately 20 lines
lines_fitted = [0]
max_lines = 20
def realistic_can_fit_line(line_height):
lines_fitted[0] += 1
return lines_fitted[0] <= max_lines
realistic_page.can_fit_line = realistic_can_fit_line
# Test with real style system
context = RenderingContext(base_font_size=14)
resolver = StyleResolver(context)
concrete_style = resolver.resolve_style(realistic_paragraph.style)
# Verify realistic constraints were calculated
assert concrete_style.word_spacing == 4.0
assert concrete_style.word_spacing_min == 2.0
assert concrete_style.word_spacing_max == 8.0
# This test demonstrates the integration without mocking everything
# In a real scenario, this would interface with actual Line and Text objects
print(f"✓ Realistic scenario test completed")
print("✓ Realistic scenario test completed")
print(f" - Words to layout: {len(realistic_paragraph.words)}")
print(f" - Page width: {realistic_page.available_width}px")
print(f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
print(
f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
class TestTableLayouter:
@@ -756,24 +769,24 @@ if __name__ == "__main__":
# Run specific tests for debugging
test = TestDocumentLayouter()
test.setup_method()
# Run a simple test
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry') as mock_registry:
with patch('pyWebLayout.layout.document_layouter.Line') as mock_line:
mock_style_registry = Mock()
mock_registry.return_value = mock_style_registry
mock_style_registry.get_concrete_style.return_value = test.mock_concrete_style
mock_line_instance = Mock()
mock_line.return_value = mock_line_instance
mock_line_instance.add_word.return_value = (True, None)
result = paragraph_layouter(test.mock_paragraph, test.mock_page)
print(f"Test result: {result}")
# Run multi-page tests
multi_test = TestMultiPageLayout()
multi_test.setup_method()
multi_test.test_realistic_multi_page_scenario()
print("Document layouter tests completed!")
@@ -7,20 +7,15 @@ in multi-page layout scenarios.
"""
import pytest
from unittest.mock import Mock, patch
from PIL import Image, ImageDraw
import numpy as np
from typing import List, Optional
import os
import logging
from pyWebLayout.layout.document_layouter import paragraph_layouter, DocumentLayouter
from pyWebLayout.layout.document_layouter import paragraph_layouter
from pyWebLayout.style.abstract_style import AbstractStyle
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
from pyWebLayout.style.fonts import Font
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Line, Text
from pyWebLayout.concrete.text import Line
from pyWebLayout.abstract.inline import Word
# Enable logging to see font loading messages
@@ -34,47 +29,53 @@ def verify_bundled_font_available():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Navigate up to pyWebLayout root, then to assets/fonts
project_root = os.path.dirname(os.path.dirname(current_dir))
bundled_font_path = os.path.join(project_root, 'pyWebLayout', 'assets', 'fonts', 'DejaVuSans.ttf')
bundled_font_path = os.path.join(
project_root,
'pyWebLayout',
'assets',
'fonts',
'DejaVuSans.ttf')
logger.info(f"Integration tests checking for bundled font at: {bundled_font_path}")
if not os.path.exists(bundled_font_path):
pytest.fail(
f"INTEGRATION TEST FAILURE: Bundled font not found at {bundled_font_path}\n"
f"Integration tests require the bundled font to ensure consistent behavior.\n"
f"This likely means the font was not included in the package build."
)
logger.info(f"Bundled font found at: {bundled_font_path}")
return bundled_font_path
class MockWord(Word):
"""A simple mock word that extends the real Word class."""
def __init__(self, text, style=None):
if style is None:
# Integration tests MUST use the bundled font for consistency
style = Font(font_size=16)
# Verify the font loaded properly
if style.font.path is None:
logger.warning("Font loaded without explicit path - may be using PIL default")
logger.warning(
"Font loaded without explicit path - may be using PIL default")
# Initialize the base Word with required parameters
super().__init__(text, style)
self._concrete_texts = []
def add_concete(self, texts):
"""Add concrete text representations."""
if isinstance(texts, list):
self._concrete_texts.extend(texts)
else:
self._concrete_texts.append(texts)
def possible_hyphenation(self):
"""Return possible hyphenation points."""
if len(self.text) <= 6:
return []
# Simple hyphenation: split roughly in the middle
mid = len(self.text) // 2
return [(self.text[:mid] + "-", self.text[mid:])]
@@ -82,7 +83,7 @@ class MockWord(Word):
class MockParagraph:
"""A simple paragraph with words and styling."""
def __init__(self, text_content, word_spacing_style=None):
if word_spacing_style is None:
word_spacing_style = AbstractStyle(
@@ -90,10 +91,10 @@ class MockParagraph:
word_spacing_min=2.0,
word_spacing_max=8.0
)
self.style = word_spacing_style
self.line_height = 25
# Create words from text content
self.words = []
for word_text in text_content.split():
@@ -103,39 +104,40 @@ class MockParagraph:
class TestDocumentLayouterIntegration:
"""Integration tests using real components."""
@classmethod
def setup_class(cls):
"""Verify bundled font is available before running any tests."""
verify_bundled_font_available()
def test_single_page_layout_with_real_components(self):
"""Test layout on a single page using real Line and Text objects."""
# Create a real page that can fit content
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
page = Page(size=(500, 400), style=page_style)
# Create a paragraph with realistic content
paragraph = MockParagraph(
"The quick brown fox jumps over the lazy dog and runs through the forest.",
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=6.0)
)
# Layout the paragraph
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, page)
success, failed_word_index, remaining_pretext = paragraph_layouter(
paragraph, page)
# Verify successful layout
assert success is True
assert failed_word_index is None
assert remaining_pretext is None
# Verify lines were added to page
assert len(page.children) > 0
# Verify actual Line objects were created
for child in page.children:
assert isinstance(child, Line)
print(f"✓ Single page test: {len(page.children)} lines created")
def test_multi_page_scenario_with_page_overflow(self):
@@ -143,47 +145,52 @@ class TestDocumentLayouterIntegration:
# Create a very small real page that will definitely overflow
small_page_style = PageStyle(border_width=5, padding=(5, 5, 5, 5))
small_page = Page(size=(150, 80), style=small_page_style)
# Create a long paragraph that will definitely overflow
long_text = " ".join([f"verylongword{i:02d}" for i in range(20)]) # 20 long words
long_text = " ".join(
[f"verylongword{i:02d}" for i in range(20)]) # 20 long words
paragraph = MockParagraph(
long_text,
AbstractStyle(word_spacing=4.0, word_spacing_min=2.0, word_spacing_max=8.0)
)
# Layout the paragraph - should fail due to page overflow
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, small_page)
success, failed_word_index, remaining_pretext = paragraph_layouter(
paragraph, small_page)
# Either should fail due to overflow OR succeed with limited content
if success:
# If it succeeded, verify it fit some content
assert len(small_page.children) > 0
print(f"✓ Multi-page test: Content fit on small page, {len(small_page.children)} lines created")
print(
f"✓ Multi-page test: Content fit on small page, {len(small_page.children)} lines created")
else:
# If it failed, verify overflow handling
assert failed_word_index is not None # Should indicate where it failed
assert failed_word_index < len(paragraph.words) # Should be within word range
print(f"✓ Multi-page test: Page overflow at word {failed_word_index}, {len(small_page.children)} lines fit")
assert failed_word_index < len(
paragraph.words) # Should be within word range
print(
f"✓ Multi-page test: Page overflow at word {failed_word_index}, {len(small_page.children)} lines fit")
def test_word_spacing_constraints_in_real_lines(self):
"""Test that word spacing constraints are properly used in real Line objects."""
# Create real page
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
page = Page(size=(400, 300), style=page_style)
# Create paragraph with specific spacing constraints
paragraph = MockParagraph(
"Testing word spacing constraints with realistic content.",
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=10.0)
)
# Layout paragraph
success, _, _ = paragraph_layouter(paragraph, page)
assert success is True
# Verify that Line objects were created with correct spacing
assert len(page.children) > 0
for line in page.children:
assert isinstance(line, Line)
# Verify spacing constraints were applied
@@ -191,35 +198,34 @@ class TestDocumentLayouterIntegration:
min_spacing, max_spacing = line._spacing
assert min_spacing == 3 # From our constraint
assert max_spacing == 10 # From our constraint
print(f"✓ Word spacing test: {len(page.children)} lines with constraints (3, 10)")
print(
f"✓ Word spacing test: {len(page.children)} lines with constraints (3, 10)")
def test_different_alignment_strategies_with_constraints(self):
"""Test different text alignment strategies with word spacing constraints."""
alignments_to_test = [
("left", AbstractStyle(text_align="left", word_spacing_min=2.0, word_spacing_max=6.0)),
("justify", AbstractStyle(text_align="justify", word_spacing_min=3.0, word_spacing_max=12.0)),
("center", AbstractStyle(text_align="center", word_spacing_min=1.0, word_spacing_max=5.0))
]
("left", AbstractStyle(
text_align="left", word_spacing_min=2.0, word_spacing_max=6.0)), ("justify", AbstractStyle(
text_align="justify", word_spacing_min=3.0, word_spacing_max=12.0)), ("center", AbstractStyle(
text_align="center", word_spacing_min=1.0, word_spacing_max=5.0))]
for alignment_name, style in alignments_to_test:
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
page = Page(size=(350, 200), style=page_style)
paragraph = MockParagraph(
"This sentence will test different alignment strategies with word spacing.",
style
)
"This sentence will test different alignment strategies with word spacing.", style)
success, _, _ = paragraph_layouter(paragraph, page)
assert success is True
assert len(page.children) > 0
# Verify alignment was applied to lines
for line in page.children:
assert isinstance(line, Line)
# Check that the alignment handler was set correctly
assert line._alignment_handler is not None
print(f"{alignment_name} alignment: {len(page.children)} lines created")
def test_realistic_document_with_multiple_pages(self):
@@ -227,38 +233,42 @@ class TestDocumentLayouterIntegration:
# Create multiple real pages
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
pages = [Page(size=(400, 300), style=page_style) for _ in range(3)]
# Create a document with multiple paragraphs
paragraphs = [
MockParagraph(
"This is the first paragraph of our document. It contains enough text to potentially span multiple lines and test the word spacing constraints properly.",
"This is the first paragraph of our document. It contains enough text to "
"potentially span multiple lines and test the word spacing constraints properly.",
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
),
MockParagraph(
"Here is a second paragraph with different styling. This paragraph uses different word spacing constraints to test the flexibility of the system.",
"Here is a second paragraph with different styling. This paragraph uses "
"different word spacing constraints to test the flexibility of the system.",
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=12.0)
),
MockParagraph(
"The third and final paragraph completes our test document. It should demonstrate that the layouter can handle multiple paragraphs with varying content lengths and styling requirements.",
"The third and final paragraph completes our test document. It should "
"demonstrate that the layouter can handle multiple paragraphs with varying "
"content lengths and styling requirements.",
AbstractStyle(word_spacing=4.0, word_spacing_min=2.5, word_spacing_max=10.0)
)
]
# Layout paragraphs across pages
current_page_index = 0
for para_index, paragraph in enumerate(paragraphs):
start_word = 0
while start_word < len(paragraph.words):
if current_page_index >= len(pages):
break # Out of pages
current_page = pages[current_page_index]
success, failed_word_index, _ = paragraph_layouter(
paragraph, current_page, start_word
)
if success:
# Paragraph completed on this page
break
@@ -267,25 +277,25 @@ class TestDocumentLayouterIntegration:
if failed_word_index is not None:
start_word = failed_word_index
current_page_index += 1
# If we're out of pages, stop
if current_page_index >= len(pages):
break
# Verify pages have content
total_lines = sum(len(page.children) for page in pages)
pages_used = sum(1 for page in pages if len(page.children) > 0)
assert total_lines > 0
assert pages_used > 1 # Should use multiple pages
print(f"✓ Multi-document test: {total_lines} lines across {pages_used} pages")
def test_word_spacing_constraint_resolution_integration(self):
"""Test the complete integration from AbstractStyle to Line spacing."""
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
page = Page(size=(400, 600), style=page_style)
_page = Page(size=(400, 600), style=page_style)
# Test different constraint scenarios
test_cases = [
{
@@ -295,7 +305,7 @@ class TestDocumentLayouterIntegration:
"expected_max": 12
},
{
"name": "default_constraints",
"name": "default_constraints",
"style": AbstractStyle(word_spacing=6.0),
"expected_min": 6, # Should use word_spacing as min
"expected_max": 12 # Should use word_spacing * 2 as max
@@ -307,7 +317,7 @@ class TestDocumentLayouterIntegration:
"expected_max": 8 # Default based on font size (16 * 0.5)
}
]
for case in test_cases:
# Create fresh real page for each test
test_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
@@ -316,18 +326,20 @@ class TestDocumentLayouterIntegration:
"Testing constraint resolution with different scenarios.",
case["style"]
)
success, _, _ = paragraph_layouter(paragraph, test_page)
assert success is True
assert len(test_page.children) > 0
# Verify constraints were resolved correctly
line = test_page.children[0]
min_spacing, max_spacing = line._spacing
assert min_spacing == case["expected_min"], f"Min constraint failed for {case['name']}"
assert max_spacing == case["expected_max"], f"Max constraint failed for {case['name']}"
assert min_spacing == case["expected_min"], f"Min constraint failed for {
case['name']}"
assert max_spacing == case["expected_max"], f"Max constraint failed for {
case['name']}"
print(f"{case['name']}: constraints ({min_spacing}, {max_spacing})")
def test_hyphenation_with_word_spacing_constraints(self):
@@ -335,15 +347,16 @@ class TestDocumentLayouterIntegration:
# Create a narrow real page to force hyphenation
narrow_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
narrow_page = Page(size=(200, 300), style=narrow_page_style)
# Create paragraph with long words that will need hyphenation
paragraph = MockParagraph(
"supercalifragilisticexpialidocious antidisestablishmentarianism",
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
)
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, narrow_page)
success, failed_word_index, remaining_pretext = paragraph_layouter(
paragraph, narrow_page)
# Should succeed with hyphenation or handle overflow gracefully
if success:
assert len(narrow_page.children) > 0
@@ -357,10 +370,10 @@ class TestDocumentLayouterIntegration:
if __name__ == "__main__":
# Run integration tests
test = TestDocumentLayouterIntegration()
print("Running document layouter integration tests...")
print("=" * 50)
test.test_single_page_layout_with_real_components()
test.test_multi_page_scenario_with_page_overflow()
test.test_word_spacing_constraints_in_real_lines()
@@ -368,6 +381,6 @@ if __name__ == "__main__":
test.test_realistic_document_with_multiple_pages()
test.test_word_spacing_constraint_resolution_integration()
test.test_hyphenation_with_word_spacing_constraints()
print("=" * 50)
print("✅ All integration tests completed successfully!")