feat(pygame): add glyph caching for performance improvement

- Add _glyph_cache dict to PygameDisplay.__init__
- Cache font.render() results per (char, fg, bg) combination
- Use blits() for batch rendering instead of individual blit calls
- Add TestRenderBorder tests (8 new tests) for border rendering
- Update NullDisplay.show() to support border=True for consistency
- Add test_show_with_border_uses_render_border for TerminalDisplay

Closes #28
This commit is contained in:
2026-03-18 04:23:58 -07:00
parent 4b26c947e8
commit 60ae4f7dfb
3 changed files with 159 additions and 12 deletions

View File

@@ -3,9 +3,11 @@ Tests for engine.display module.
"""
import sys
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from engine.display import DisplayRegistry, NullDisplay, TerminalDisplay
import pytest
from engine.display import DisplayRegistry, NullDisplay, TerminalDisplay, render_border
from engine.display.backends.multi import MultiDisplay
@@ -133,7 +135,6 @@ class TestTerminalDisplay:
The fix adds \\033[H\\033[J (cursor home + erase down) before each frame.
"""
from io import BytesIO
from unittest.mock import patch
display = TerminalDisplay()
display.init(80, 24)
@@ -157,7 +158,6 @@ class TestTerminalDisplay:
Regression test: Ensures each show() call includes the clear sequence.
"""
from io import BytesIO
from unittest.mock import patch
# Use target_fps=0 to disable frame skipping in test
display = TerminalDisplay(target_fps=0)
@@ -193,6 +193,40 @@ class TestTerminalDisplay:
# returns different values each call
assert len(set(dims)) == 1, f"Dimensions should be stable, got: {set(dims)}"
def test_show_with_border_uses_render_border(self):
"""show with border=True calls render_border with FPS."""
from unittest.mock import MagicMock
display = TerminalDisplay()
display.init(80, 24)
buffer = ["line1", "line2"]
# Mock get_monitor to provide FPS
mock_monitor = MagicMock()
mock_monitor.get_stats.return_value = {
"pipeline": {"avg_ms": 16.5},
"frame_count": 100,
}
# Mock render_border to verify it's called
with (
patch("engine.display.get_monitor", return_value=mock_monitor),
patch("engine.display.render_border", wraps=render_border) as mock_render,
):
display.show(buffer, border=True)
# Verify render_border was called with correct arguments
assert mock_render.called
args, kwargs = mock_render.call_args
# Arguments: buffer, width, height, fps, frame_time (positional)
assert args[0] == buffer
assert args[1] == 80
assert args[2] == 24
assert args[3] == pytest.approx(60.6, rel=0.1) # fps = 1000/16.5
assert args[4] == pytest.approx(16.5, rel=0.1)
assert kwargs == {} # no keyword arguments
class TestNullDisplay:
"""Tests for NullDisplay class."""
@@ -241,6 +275,92 @@ class TestNullDisplay:
assert display._last_buffer == ["second"]
class TestRenderBorder:
"""Tests for render_border function."""
def test_render_border_adds_corners(self):
"""render_border adds corner characters."""
from engine.display import render_border
buffer = ["hello", "world"]
result = render_border(buffer, width=10, height=5)
assert result[0][0] in "┌┎┍" # top-left
assert result[0][-1] in "┐┒┓" # top-right
assert result[-1][0] in "└┚┖" # bottom-left
assert result[-1][-1] in "┘┛┙" # bottom-right
def test_render_border_dimensions(self):
"""render_border output matches requested dimensions."""
from engine.display import render_border
buffer = ["line1", "line2", "line3"]
result = render_border(buffer, width=20, height=10)
# Output should be exactly height lines
assert len(result) == 10
# Each line should be exactly width characters
for line in result:
assert len(line) == 20
def test_render_border_with_fps(self):
"""render_border includes FPS in top border when provided."""
from engine.display import render_border
buffer = ["test"]
result = render_border(buffer, width=20, height=5, fps=60.0)
top_line = result[0]
assert "FPS:60" in top_line or "FPS: 60" in top_line
def test_render_border_with_frame_time(self):
"""render_border includes frame time in bottom border when provided."""
from engine.display import render_border
buffer = ["test"]
result = render_border(buffer, width=20, height=5, frame_time=16.5)
bottom_line = result[-1]
assert "16.5ms" in bottom_line
def test_render_border_crops_content_to_fit(self):
"""render_border crops content to fit within borders."""
from engine.display import render_border
# Buffer larger than viewport
buffer = ["x" * 100] * 50
result = render_border(buffer, width=20, height=10)
# Result shrinks to fit viewport
assert len(result) == 10
for line in result[1:-1]: # Skip border lines
assert len(line) == 20
def test_render_border_preserves_content(self):
"""render_border preserves content within borders."""
from engine.display import render_border
buffer = ["hello world", "test line"]
result = render_border(buffer, width=20, height=5)
# Content should appear in the middle rows
content_lines = result[1:-1]
assert any("hello world" in line for line in content_lines)
def test_render_border_with_small_buffer(self):
"""render_border handles buffers smaller than viewport."""
from engine.display import render_border
buffer = ["hi"]
result = render_border(buffer, width=20, height=10)
# Should still produce full viewport with padding
assert len(result) == 10
# All lines should be full width
for line in result:
assert len(line) == 20
class TestMultiDisplay:
"""Tests for MultiDisplay class."""