auto flake and corrections
This commit is contained in:
@@ -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!")
|
||||
|
||||
Reference in New Issue
Block a user