172 lines
6.3 KiB
Python
172 lines
6.3 KiB
Python
"""
|
|
Regression tests for backward page navigation (spec S16).
|
|
|
|
The previous page of P is the position q for which laying out forward from q ends
|
|
exactly at P. The old implementation searched for q by guessing a block index and
|
|
bisecting, with word_index pinned to 0 - so a page starting mid-paragraph was not
|
|
in the search space at all. It exhausted its ten iterations and fell back to a
|
|
position that was not the previous page, typically the start of the document.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from pyWebLayout.abstract.block import Paragraph
|
|
from pyWebLayout.abstract.inline import Word
|
|
from pyWebLayout.layout.ereader_layout import BidirectionalLayouter, RenderingPosition
|
|
from pyWebLayout.style import Font
|
|
from pyWebLayout.style.page_style import PageStyle
|
|
|
|
|
|
PAGE_SIZE = (800, 600)
|
|
|
|
|
|
@pytest.fixture
|
|
def font():
|
|
return Font(font_size=16)
|
|
|
|
|
|
def paragraph(font, count, tag):
|
|
block = Paragraph(font)
|
|
for i in range(count):
|
|
block.add_word(Word(f"{tag}{i}", font))
|
|
return block
|
|
|
|
|
|
@pytest.fixture
|
|
def long_document(font):
|
|
"""Short paragraphs around one that spans several pages."""
|
|
return [
|
|
paragraph(font, 60, "a"),
|
|
paragraph(font, 80, "b"),
|
|
paragraph(font, 1200, "long"),
|
|
paragraph(font, 70, "c"),
|
|
paragraph(font, 90, "d"),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def block_document(font):
|
|
"""Many small blocks, so every page starts on a block boundary."""
|
|
return [paragraph(font, 40, f"p{i}") for i in range(40)]
|
|
|
|
|
|
def forward_chain(layouter, limit=30):
|
|
"""The page start positions a reader would visit going forward."""
|
|
starts = []
|
|
pos = RenderingPosition()
|
|
for _ in range(limit):
|
|
starts.append(pos)
|
|
_, nxt = layouter.render_page_forward(pos, 1.0)
|
|
if nxt.block_index >= len(layouter.blocks):
|
|
break
|
|
if (nxt.block_index, nxt.word_index) == (pos.block_index, pos.word_index):
|
|
pytest.fail("forward pagination made no progress")
|
|
pos = nxt
|
|
return starts
|
|
|
|
|
|
def key(position):
|
|
return (position.chapter_index, position.block_index, position.word_index)
|
|
|
|
|
|
class TestBackwardMatchesForward:
|
|
"""The defining invariant: forward from the answer lands exactly on P."""
|
|
|
|
@pytest.mark.parametrize("document", ["long_document", "block_document"])
|
|
def test_previous_page_is_the_forward_predecessor(self, document, request):
|
|
blocks = request.getfixturevalue(document)
|
|
layouter = BidirectionalLayouter(blocks, PageStyle(), PAGE_SIZE)
|
|
starts = forward_chain(layouter)
|
|
assert len(starts) > 2, "need a few pages to test against"
|
|
|
|
for i in range(1, len(starts)):
|
|
_, got = layouter.render_page_backward(starts[i], 1.0)
|
|
assert key(got) == key(starts[i - 1]), (
|
|
f"page {i}: expected to land on page {i - 1} "
|
|
f"{key(starts[i - 1])}, got {key(got)}")
|
|
|
|
def test_result_lays_out_to_the_target(self, long_document):
|
|
"""Independent of the recorded chain: replaying the answer must reach P."""
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
starts = forward_chain(layouter)
|
|
|
|
for target in starts[1:]:
|
|
_, start = layouter.render_page_backward(target, 1.0)
|
|
_, end = layouter.render_page_forward(start, 1.0)
|
|
assert key(end) == key(target), (
|
|
f"a page starting at {key(start)} ends at {key(end)}, "
|
|
f"not at the requested {key(target)}")
|
|
|
|
def test_mid_paragraph_targets_are_reachable(self, long_document):
|
|
"""The specific regression: starts inside a block, not on its boundary."""
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
starts = forward_chain(layouter)
|
|
mid = [s for s in starts if s.word_index > 0]
|
|
assert mid, "this document should paginate mid-paragraph"
|
|
|
|
for target in mid:
|
|
_, got = layouter.render_page_backward(target, 1.0)
|
|
assert key(got) != (0, 0, 0) or key(target) == key(starts[1]), \
|
|
"backward navigation fell back to the document start"
|
|
|
|
|
|
class TestRoundTrip:
|
|
|
|
def test_forward_then_back_returns_to_the_same_place(self, long_document):
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
pos = RenderingPosition()
|
|
|
|
for _ in range(4):
|
|
_, nxt = layouter.render_page_forward(pos, 1.0)
|
|
_, back = layouter.render_page_backward(nxt, 1.0)
|
|
assert key(back) == key(pos), \
|
|
f"round trip drifted: {key(pos)} -> {key(nxt)} -> {key(back)}"
|
|
pos = nxt
|
|
|
|
|
|
class TestEdges:
|
|
|
|
def test_at_document_start_stays_there(self, long_document):
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
_, got = layouter.render_page_backward(RenderingPosition(), 1.0)
|
|
assert key(got) == (0, 0, 0)
|
|
|
|
def test_second_page_goes_back_to_the_first(self, long_document):
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
_, second = layouter.render_page_forward(RenderingPosition(), 1.0)
|
|
_, got = layouter.render_page_backward(second, 1.0)
|
|
assert key(got) == (0, 0, 0)
|
|
|
|
def test_empty_document_is_safe(self):
|
|
layouter = BidirectionalLayouter([], PageStyle(), PAGE_SIZE)
|
|
page, got = layouter.render_page_backward(RenderingPosition(), 1.0)
|
|
assert page is not None
|
|
assert key(got) == (0, 0, 0)
|
|
|
|
|
|
class TestCost:
|
|
|
|
def test_backward_is_not_wildly_more_expensive_than_forward(self, long_document):
|
|
"""The old path burned ten full layouts per call and still got it wrong."""
|
|
layouter = BidirectionalLayouter(long_document, PageStyle(), PAGE_SIZE)
|
|
starts = forward_chain(layouter)
|
|
|
|
calls = {"n": 0}
|
|
original = BidirectionalLayouter.render_page_forward
|
|
|
|
def counting(self, position, font_scale=1.0):
|
|
calls["n"] += 1
|
|
return original(self, position, font_scale)
|
|
|
|
BidirectionalLayouter.render_page_forward = counting
|
|
try:
|
|
worst = 0
|
|
for target in starts[1:]:
|
|
calls["n"] = 0
|
|
layouter.render_page_backward(target, 1.0)
|
|
worst = max(worst, calls["n"])
|
|
finally:
|
|
BidirectionalLayouter.render_page_forward = original
|
|
|
|
assert worst <= 10, f"backward navigation cost {worst} forward layouts"
|