pyWebLayout/tests/layout/test_table_optimizer.py
Duncan Tourolle 3bcd1bffb5
All checks were successful
Python CI / test (3.10) (push) Successful in 2m22s
Python CI / test (3.12) (push) Successful in 2m13s
Python CI / test (3.13) (push) Successful in 2m9s
Tables now use ""dynamic page" allowing the contents to be anything that can be rendered ion a page.
2025-11-11 18:10:47 +01:00

398 lines
12 KiB
Python

"""
Unit tests for table column width optimization.
"""
import pytest
from pyWebLayout.layout.table_optimizer import (
optimize_table_layout,
sample_table_rows,
extract_html_column_widths,
parse_html_width,
distribute_column_widths,
get_column_count,
calculate_table_overhead
)
from pyWebLayout.abstract.block import Table
from pyWebLayout.concrete.table import TableStyle
class TestParseHtmlWidth:
"""Test HTML width parsing."""
def test_parse_int(self):
assert parse_html_width(100) == 100
def test_parse_px_string(self):
assert parse_html_width("150px") == 150
def test_parse_plain_number_string(self):
assert parse_html_width("200") == 200
def test_parse_percentage_returns_none(self):
assert parse_html_width("50%") is None
def test_parse_invalid_string(self):
assert parse_html_width("invalid") is None
def test_parse_with_whitespace(self):
assert parse_html_width(" 120px ") == 120
class TestDistributeColumnWidths:
"""Test column width distribution."""
def test_distribute_with_no_fixed_columns(self):
min_widths = [50, 60, 70]
pref_widths = [100, 120, 140]
available = 360
fixed = {}
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
# Should use preferred widths (they fit)
assert result == [100, 120, 140]
def test_distribute_when_preferred_fits(self):
min_widths = [50, 50]
pref_widths = [100, 100]
available = 250
fixed = {}
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
# Preferred widths fit, extra 50px distributed proportionally (25px each)
assert result == [125, 125]
def test_distribute_when_must_use_minimum(self):
min_widths = [100, 100]
pref_widths = [200, 200]
available = 150
fixed = {}
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
# Can't even fit minimum, but force it anyway
assert result == [100, 100]
def test_distribute_proportional(self):
min_widths = [50, 50]
pref_widths = [200, 100]
available = 200 # Between min and pref totals
fixed = {}
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
# Should distribute proportionally
assert len(result) == 2
assert result[0] + result[1] == 200
# First column should get more (higher pref)
assert result[0] > result[1]
def test_distribute_with_fixed_columns(self):
min_widths = [50, 50, 50]
pref_widths = [100, 100, 100]
available = 300
fixed = {1: 80} # Second column fixed at 80
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
# Second column should be 80
assert result[1] == 80
# Other columns share remaining space
assert result[0] + result[2] == 220
def test_distribute_all_fixed(self):
min_widths = [50, 50]
pref_widths = [100, 100]
available = 300
fixed = {0: 120, 1: 150}
result = distribute_column_widths(min_widths, pref_widths, available, fixed)
assert result == [120, 150]
def test_distribute_empty(self):
result = distribute_column_widths([], [], 100, {})
assert result == []
class TestGetColumnCount:
"""Test column counting."""
def test_empty_table(self):
table = Table()
assert get_column_count(table) == 0
def test_table_with_header(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph, Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
row = TableRow()
for text in ["A", "B", "C"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="header")
assert get_column_count(table) == 3
def test_table_with_body(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph, Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
row = TableRow()
for text in ["1", "2"]:
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
assert get_column_count(table) == 2
class TestSampleTableRows:
"""Test row sampling."""
def test_sample_small_table(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
for text in ["1", "2"]:
row = TableRow()
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
sampled = sample_table_rows(table, sample_size=5)
# Should get all rows (only 2)
assert len(sampled) == 2
def test_sample_large_table(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
for i in range(20):
row = TableRow()
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(str(i), font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
sampled = sample_table_rows(table, sample_size=5)
# Should get only 5 body rows
assert len(sampled) == 5
def test_sample_with_header_body_footer(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
# 3 header rows
for i in range(3):
row = TableRow()
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(f"H{i}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="header")
# 10 body rows
for i in range(10):
row = TableRow()
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"B{i}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
# 2 footer rows
for i in range(2):
row = TableRow()
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"F{i}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="footer")
sampled = sample_table_rows(table, sample_size=2)
# Should get 2 from each section = 6 total
assert len(sampled) == 6
class TestExtractHtmlColumnWidths:
"""Test HTML width extraction."""
def test_no_widths(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
row = TableRow()
for text in ["A", "B"]:
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
widths = extract_html_column_widths(table)
assert widths == [None, None]
def test_cell_width_attributes(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
row = TableRow()
cell1 = TableCell()
cell1.width = "100px"
para1 = Paragraph(font)
para1.add_word(Word("A", font))
cell1.add_block(para1)
row.add_cell(cell1)
cell2 = TableCell()
cell2.width = "150"
para2 = Paragraph(font)
para2.add_word(Word("B", font))
cell2.add_block(para2)
row.add_cell(cell2)
table.add_row(row, section="body")
widths = extract_html_column_widths(table)
assert widths == [100, 150]
class TestCalculateTableOverhead:
"""Test table overhead calculation."""
def test_basic_overhead(self):
style = TableStyle(border_width=1, cell_spacing=0)
overhead = calculate_table_overhead(3, style)
# 3 columns = 4 borders (n+1)
assert overhead == 4
def test_with_cell_spacing(self):
style = TableStyle(border_width=1, cell_spacing=5)
overhead = calculate_table_overhead(3, style)
# 4 borders + 2 spacings (n-1)
assert overhead == 4 + 10
def test_thicker_borders(self):
style = TableStyle(border_width=3, cell_spacing=0)
overhead = calculate_table_overhead(2, style)
# 2 columns = 3 borders * 3px
assert overhead == 9
class TestOptimizeTableLayout:
"""Test full table optimization."""
def test_optimize_simple_table(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
row = TableRow()
for text in ["Short", "A bit longer text"]:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
style = TableStyle()
widths = optimize_table_layout(table, available_width=400, style=style)
# Should return 2 column widths
assert len(widths) == 2
# Second column should be wider
assert widths[1] > widths[0]
def test_optimize_empty_table(self):
table = Table()
widths = optimize_table_layout(table, available_width=400)
assert widths == []
def test_optimize_respects_sample_size(self):
from pyWebLayout.abstract.block import TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
table = Table()
font = Font(font_size=12)
# Create 20 rows but only first 5 should be sampled
for i in range(20):
row = TableRow()
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"Data {i}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
widths = optimize_table_layout(table, available_width=400, sample_size=5)
# Should return width for 1 column
assert len(widths) == 1
if __name__ == '__main__':
pytest.main([__file__, '-v'])