forked from genewildish/Mainline
test: add comprehensive unit tests for core components
- tests/test_canvas.py: 33 tests for Canvas (2D rendering surface) - tests/test_firehose.py: 5 tests for FirehoseEffect - tests/test_pipeline_order.py: 3 tests for execution order verification - tests/test_renderer.py: 22 tests for ANSI parsing and PIL rendering These tests provide solid coverage for foundational modules.
This commit is contained in:
164
tests/test_renderer.py
Normal file
164
tests/test_renderer.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Unit tests for engine.display.renderer module.
|
||||
|
||||
Tests ANSI parsing and PIL rendering utilities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
|
||||
from engine.display.renderer import ANSI_COLORS, parse_ansi, render_to_pil
|
||||
|
||||
|
||||
class TestParseANSI:
|
||||
"""Tests for parse_ansi function."""
|
||||
|
||||
def test_plain_text(self):
|
||||
"""Plain text without ANSI codes returns single token."""
|
||||
tokens = parse_ansi("Hello World")
|
||||
assert len(tokens) == 1
|
||||
assert tokens[0][0] == "Hello World"
|
||||
# Check default colors
|
||||
assert tokens[0][1] == (204, 204, 204) # fg
|
||||
assert tokens[0][2] == (0, 0, 0) # bg
|
||||
assert tokens[0][3] is False # bold
|
||||
|
||||
def test_empty_string(self):
|
||||
"""Empty string returns single empty token."""
|
||||
tokens = parse_ansi("")
|
||||
assert tokens == [("", (204, 204, 204), (0, 0, 0), False)]
|
||||
|
||||
def test_reset_code(self):
|
||||
"""Reset code (ESC[0m) restores defaults."""
|
||||
tokens = parse_ansi("\x1b[31mRed\x1b[0mNormal")
|
||||
assert len(tokens) == 2
|
||||
assert tokens[0][0] == "Red"
|
||||
# Red fg should be ANSI_COLORS[1]
|
||||
assert tokens[0][1] == ANSI_COLORS[1]
|
||||
assert tokens[1][0] == "Normal"
|
||||
assert tokens[1][1] == (204, 204, 204) # back to default
|
||||
|
||||
def test_bold_code(self):
|
||||
"""Bold code (ESC[1m) sets bold flag."""
|
||||
tokens = parse_ansi("\x1b[1mBold")
|
||||
assert tokens[0][3] is True
|
||||
|
||||
def test_bold_off_code(self):
|
||||
"""Bold off (ESC[22m) clears bold."""
|
||||
tokens = parse_ansi("\x1b[1mBold\x1b[22mNormal")
|
||||
assert tokens[0][3] is True
|
||||
assert tokens[1][3] is False
|
||||
|
||||
def test_4bit_foreground_colors(self):
|
||||
"""4-bit foreground colors (30-37, 90-97) work."""
|
||||
# Test normal red (31)
|
||||
tokens = parse_ansi("\x1b[31mRed")
|
||||
assert tokens[0][1] == ANSI_COLORS[1] # color 1 = red
|
||||
|
||||
# Test bright cyan (96) - maps to index 14 (bright cyan)
|
||||
tokens = parse_ansi("\x1b[96mCyan")
|
||||
assert tokens[0][1] == ANSI_COLORS[14] # bright cyan
|
||||
|
||||
def test_4bit_background_colors(self):
|
||||
"""4-bit background colors (40-47, 100-107) work."""
|
||||
# Green bg = 42
|
||||
tokens = parse_ansi("\x1b[42mText")
|
||||
assert tokens[0][2] == ANSI_COLORS[2] # color 2 = green
|
||||
|
||||
# Bright magenta bg = 105
|
||||
tokens = parse_ansi("\x1b[105mText")
|
||||
assert tokens[0][2] == ANSI_COLORS[13] # bright magenta (13)
|
||||
|
||||
def test_multiple_ansi_codes_in_sequence(self):
|
||||
"""Multiple codes in one escape sequence are parsed."""
|
||||
tokens = parse_ansi("\x1b[1;31;42mBold Red on Green")
|
||||
assert tokens[0][0] == "Bold Red on Green"
|
||||
assert tokens[0][3] is True # bold
|
||||
assert tokens[0][1] == ANSI_COLORS[1] # red fg
|
||||
assert tokens[0][2] == ANSI_COLORS[2] # green bg
|
||||
|
||||
def test_nested_ansi_sequences(self):
|
||||
"""Multiple separate ANSI sequences are tokenized correctly."""
|
||||
text = "\x1b[31mRed\x1b[32mGreen\x1b[0mNormal"
|
||||
tokens = parse_ansi(text)
|
||||
assert len(tokens) == 3
|
||||
assert tokens[0][0] == "Red"
|
||||
assert tokens[1][0] == "Green"
|
||||
assert tokens[2][0] == "Normal"
|
||||
|
||||
def test_interleaved_text_and_ansi(self):
|
||||
"""Text before and after ANSI codes is tokenized."""
|
||||
tokens = parse_ansi("Pre\x1b[31mRedPost")
|
||||
assert len(tokens) == 2
|
||||
assert tokens[0][0] == "Pre"
|
||||
assert tokens[1][0] == "RedPost"
|
||||
assert tokens[1][1] == ANSI_COLORS[1]
|
||||
|
||||
def test_all_standard_4bit_colors(self):
|
||||
"""All 4-bit color indices (0-15) map to valid RGB."""
|
||||
for i in range(16):
|
||||
tokens = parse_ansi(f"\x1b[{i}mX")
|
||||
# Should be a defined color or default fg
|
||||
fg = tokens[0][1]
|
||||
valid = fg in ANSI_COLORS.values() or fg == (204, 204, 204)
|
||||
assert valid, f"Color {i} produced invalid fg {fg}"
|
||||
|
||||
def test_unknown_code_ignored(self):
|
||||
"""Unknown numeric codes are ignored, keep current style."""
|
||||
tokens = parse_ansi("\x1b[99mText")
|
||||
# 99 is not recognized, should keep previous state (defaults)
|
||||
assert tokens[0][1] == (204, 204, 204)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL not available")
|
||||
class TestRenderToPIL:
|
||||
"""Tests for render_to_pil function (requires PIL)."""
|
||||
|
||||
def test_renders_plain_text(self):
|
||||
"""Plain buffer renders as image."""
|
||||
buffer = ["Hello"]
|
||||
img = render_to_pil(buffer, width=10, height=1)
|
||||
assert isinstance(img, Image.Image)
|
||||
assert img.mode == "RGBA"
|
||||
|
||||
def test_renders_with_ansi_colors(self):
|
||||
"""Buffer with ANSI colors renders correctly."""
|
||||
buffer = ["\x1b[31mRed\x1b[0mNormal"]
|
||||
img = render_to_pil(buffer, width=20, height=1)
|
||||
assert isinstance(img, Image.Image)
|
||||
|
||||
def test_multi_line_buffer(self):
|
||||
"""Multiple lines render with correct height."""
|
||||
buffer = ["Line1", "Line2", "Line3"]
|
||||
img = render_to_pil(buffer, width=10, height=3)
|
||||
# Height should be approximately 3 * cell_height (18-2 padding)
|
||||
assert img.height > 0
|
||||
|
||||
def test_clipping_to_height(self):
|
||||
"""Buffer longer than height is clipped."""
|
||||
buffer = ["Line1", "Line2", "Line3", "Line4"]
|
||||
img = render_to_pil(buffer, width=10, height=2)
|
||||
# Should only render 2 lines
|
||||
assert img.height < img.width * 2 # roughly 2 lines tall
|
||||
|
||||
def test_cell_dimensions_respected(self):
|
||||
"""Custom cell_width and cell_height are used."""
|
||||
buffer = ["Test"]
|
||||
img = render_to_pil(buffer, width=5, height=1, cell_width=20, cell_height=25)
|
||||
assert img.width == 5 * 20
|
||||
assert img.height == 25
|
||||
|
||||
def test_font_fallback_on_invalid(self):
|
||||
"""Invalid font path falls back to default font."""
|
||||
buffer = ["Test"]
|
||||
# Should not crash with invalid font path
|
||||
img = render_to_pil(
|
||||
buffer, width=5, height=1, font_path="/nonexistent/font.ttf"
|
||||
)
|
||||
assert isinstance(img, Image.Image)
|
||||
Reference in New Issue
Block a user