200 lines
7.2 KiB
Python
200 lines
7.2 KiB
Python
"""Tests for grid composition (non-ComfyUI parts)."""
|
|
|
|
import pytest
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
import numpy as np
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from grid import _wrap_text, _wrap_single_line, _measure_text_height
|
|
from PIL import ImageFont
|
|
|
|
|
|
class TestWrapText:
|
|
"""Test text wrapping utility."""
|
|
|
|
def test_empty_text(self):
|
|
"""Empty text should return empty list."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_text("", 100, font)
|
|
assert result == []
|
|
|
|
def test_single_line_fits(self):
|
|
"""Single line that fits should be returned."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_text("hello", 1000, font)
|
|
assert "hello" in result
|
|
|
|
def test_max_lines_enforced(self):
|
|
"""Should not exceed max_lines."""
|
|
font = ImageFont.load_default()
|
|
text = "one two three four five six seven eight"
|
|
result = _wrap_text(text, 50, font, max_lines=2)
|
|
assert len(result) <= 2
|
|
|
|
def test_ellipsis_added(self):
|
|
"""Truncated text at max_lines=1 ends with an ellipsis character."""
|
|
font = ImageFont.load_default()
|
|
text = "one two three four five six seven eight"
|
|
result = _wrap_text(text, 1, font, max_lines=1)
|
|
assert len(result) == 1
|
|
assert result[0].endswith("…")
|
|
|
|
def test_newline_creates_separate_lines(self):
|
|
"""Embedded newlines in the input text produce separate output lines."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_text("hello\nworld", 1000, font, max_lines=5)
|
|
assert result == ["hello", "world"]
|
|
|
|
def test_oversized_word_not_dropped(self):
|
|
"""A single word wider than max_width is still returned rather than dropped."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_text("superlongword", 1, font, max_lines=2)
|
|
assert result == ["superlongword"]
|
|
|
|
def test_newline_overflow_adds_ellipsis(self):
|
|
"""\\n-segments that push past max_lines get an ellipsis on the last kept line."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_text("a\nb\nc", 1000, font, max_lines=2)
|
|
assert len(result) == 2
|
|
assert result[-1].endswith("…")
|
|
|
|
|
|
class TestWrapSingleLine:
|
|
"""Tests for the _wrap_single_line helper."""
|
|
|
|
def test_empty_text_returns_empty(self):
|
|
font = ImageFont.load_default()
|
|
assert _wrap_single_line("", 100, font) == []
|
|
|
|
def test_single_word_fits(self):
|
|
font = ImageFont.load_default()
|
|
assert _wrap_single_line("hello", 1000, font) == ["hello"]
|
|
|
|
def test_multiple_words_fit_on_one_line(self):
|
|
font = ImageFont.load_default()
|
|
assert _wrap_single_line("hello world", 1000, font) == ["hello world"]
|
|
|
|
def test_words_wrap_to_separate_lines(self):
|
|
font = ImageFont.load_default()
|
|
result = _wrap_single_line("one two three", 1, font)
|
|
assert result == ["one", "two", "three"]
|
|
|
|
def test_oversized_word_not_dropped(self):
|
|
font = ImageFont.load_default()
|
|
assert _wrap_single_line("superlongword", 1, font) == ["superlongword"]
|
|
|
|
def test_no_newline_handling(self):
|
|
"""_wrap_single_line treats \\n as whitespace (split() behaviour), not a line break."""
|
|
font = ImageFont.load_default()
|
|
result = _wrap_single_line("hello\nworld", 1000, font)
|
|
assert result == ["hello world"]
|
|
|
|
|
|
class TestMeasureTextHeight:
|
|
"""Test text height measurement."""
|
|
|
|
def test_empty_lines(self):
|
|
"""Empty lines should return 0."""
|
|
font = ImageFont.load_default()
|
|
height = _measure_text_height([], font)
|
|
assert height == 0
|
|
|
|
def test_single_line_height(self):
|
|
"""Single line should return positive height."""
|
|
font = ImageFont.load_default()
|
|
height = _measure_text_height(["hello"], font)
|
|
assert height > 0
|
|
|
|
def test_multiple_lines_height(self):
|
|
"""Multiple lines should scale linearly."""
|
|
font = ImageFont.load_default()
|
|
single_height = _measure_text_height(["hello"], font)
|
|
double_height = _measure_text_height(["hello", "world"], font)
|
|
assert double_height == single_height * 2 or double_height > single_height
|
|
|
|
def test_line_spacing_included(self):
|
|
"""Two identical lines include exactly one inter-line spacing gap."""
|
|
from grid import _LINE_SPACING
|
|
font = ImageFont.load_default()
|
|
h1 = _measure_text_height(["hello"], font)
|
|
h2 = _measure_text_height(["hello", "hello"], font)
|
|
assert h2 == h1 * 2 + _LINE_SPACING
|
|
|
|
|
|
class TestGridComposition:
|
|
"""Test grid composition (high-level)."""
|
|
|
|
def test_single_cell_grid(self):
|
|
"""Single cell should compose without error."""
|
|
from grid import compose_grid
|
|
|
|
cell = torch.ones([512, 512, 3], dtype=torch.float32) * 0.5
|
|
result = compose_grid([cell])
|
|
|
|
assert result.shape[2] == 3 # RGB
|
|
assert result.dtype == torch.float32
|
|
assert result.min() >= 0 and result.max() <= 1
|
|
|
|
def test_multiple_cells(self):
|
|
"""Multiple cells should compose into grid."""
|
|
from grid import compose_grid
|
|
|
|
cells = [torch.ones([256, 256, 3], dtype=torch.float32) * (i / 10) for i in range(4)]
|
|
x_labels = ["a", "b"]
|
|
y_labels = ["c", "d"]
|
|
|
|
result = compose_grid(cells, x_labels=x_labels, y_labels=y_labels)
|
|
|
|
assert result.shape[2] == 3
|
|
assert result.dtype == torch.float32
|
|
|
|
def test_cell_mismatch_error(self):
|
|
"""Mismatch between cells and labels should raise error."""
|
|
from grid import compose_grid
|
|
|
|
cells = [torch.ones([256, 256, 3], dtype=torch.float32)]
|
|
x_labels = ["a", "b"]
|
|
y_labels = ["c", "d"]
|
|
|
|
with pytest.raises(ValueError):
|
|
compose_grid(cells, x_labels=x_labels, y_labels=y_labels)
|
|
|
|
def test_x_labels_add_top_strip_to_height(self):
|
|
"""Output height with x-labels is greater than bare cell height."""
|
|
from grid import compose_grid
|
|
|
|
cells = [torch.zeros([256, 256, 3], dtype=torch.float32) for _ in range(2)]
|
|
result = compose_grid(cells, x_labels=["a", "b"], width=256, height=256)
|
|
|
|
assert result.shape[0] > 256 # top strip adds height beyond one cell row
|
|
|
|
def test_y_labels_add_left_strip_to_width(self):
|
|
"""Output width with y-labels is greater than bare cell width."""
|
|
from grid import compose_grid
|
|
|
|
cells = [torch.zeros([256, 256, 3], dtype=torch.float32) for _ in range(2)]
|
|
result = compose_grid(cells, y_labels=["a", "b"], width=256, height=256)
|
|
|
|
assert result.shape[1] > 256 # left strip adds width beyond one cell column
|
|
|
|
def test_max_composite_width_respected(self):
|
|
"""Composite width does not exceed max_composite_width after rescaling."""
|
|
from grid import compose_grid
|
|
|
|
# Two cells, x-labels only (no y-label strip) so rescaling is exact.
|
|
cells = [torch.ones([512, 512, 3], dtype=torch.float32) for _ in range(2)]
|
|
x_labels = ["a", "b"]
|
|
|
|
result = compose_grid(cells, x_labels=x_labels, max_composite_width=512)
|
|
|
|
assert result.shape[1] <= 512
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|