forked from genewildish/Mainline
- 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.
286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""
|
|
Unit tests for engine.canvas.Canvas.
|
|
|
|
Tests the core 2D rendering surface without any display dependencies.
|
|
"""
|
|
|
|
from engine.canvas import Canvas, CanvasRegion
|
|
|
|
|
|
class TestCanvasRegion:
|
|
"""Tests for CanvasRegion dataclass."""
|
|
|
|
def test_is_valid_positive_dimensions(self):
|
|
"""Positive width and height returns True."""
|
|
region = CanvasRegion(0, 0, 10, 5)
|
|
assert region.is_valid() is True
|
|
|
|
def test_is_valid_zero_width(self):
|
|
"""Zero width returns False."""
|
|
region = CanvasRegion(0, 0, 0, 5)
|
|
assert region.is_valid() is False
|
|
|
|
def test_is_valid_zero_height(self):
|
|
"""Zero height returns False."""
|
|
region = CanvasRegion(0, 0, 10, 0)
|
|
assert region.is_valid() is False
|
|
|
|
def test_is_valid_negative_dimensions(self):
|
|
"""Negative dimensions return False."""
|
|
region = CanvasRegion(0, 0, -1, 5)
|
|
assert region.is_valid() is False
|
|
|
|
def test_rows_computes_correct_set(self):
|
|
"""rows() returns set of row indices in region."""
|
|
region = CanvasRegion(2, 3, 4, 2)
|
|
assert region.rows() == {3, 4}
|
|
|
|
|
|
class TestCanvas:
|
|
"""Tests for Canvas class."""
|
|
|
|
def test_init_default_dimensions(self):
|
|
"""Default width=80, height=24."""
|
|
canvas = Canvas()
|
|
assert canvas.width == 80
|
|
assert canvas.height == 24
|
|
assert len(canvas._grid) == 24
|
|
assert len(canvas._grid[0]) == 80
|
|
|
|
def test_init_custom_dimensions(self):
|
|
"""Custom dimensions are set correctly."""
|
|
canvas = Canvas(100, 50)
|
|
assert canvas.width == 100
|
|
assert canvas.height == 50
|
|
|
|
def test_clear_empties_grid(self):
|
|
"""clear() resets all cells to spaces."""
|
|
canvas = Canvas(5, 3)
|
|
canvas.put_text(0, 0, "Hello")
|
|
canvas.clear()
|
|
region = canvas.get_region(0, 0, 5, 3)
|
|
assert all(all(cell == " " for cell in row) for row in region)
|
|
|
|
def test_clear_marks_entire_canvas_dirty(self):
|
|
"""clear() marks entire canvas as dirty."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.clear()
|
|
dirty = canvas.get_dirty_regions()
|
|
assert len(dirty) == 1
|
|
assert dirty[0].x == 0 and dirty[0].y == 0
|
|
assert dirty[0].width == 10 and dirty[0].height == 5
|
|
|
|
def test_put_text_single_char(self):
|
|
"""put_text writes a single character at position."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(3, 2, "X")
|
|
assert canvas._grid[2][3] == "X"
|
|
|
|
def test_put_text_multiple_chars(self):
|
|
"""put_text writes multiple characters in a row."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(2, 1, "ABC")
|
|
assert canvas._grid[1][2] == "A"
|
|
assert canvas._grid[1][3] == "B"
|
|
assert canvas._grid[1][4] == "C"
|
|
|
|
def test_put_text_ignores_overflow_right(self):
|
|
"""Characters beyond width are ignored."""
|
|
canvas = Canvas(5, 5)
|
|
canvas.put_text(3, 0, "XYZ")
|
|
assert canvas._grid[0][3] == "X"
|
|
assert canvas._grid[0][4] == "Y"
|
|
# Z would be at index 5, which is out of bounds
|
|
|
|
def test_put_text_ignores_overflow_bottom(self):
|
|
"""Rows beyond height are ignored."""
|
|
canvas = Canvas(5, 3)
|
|
canvas.put_text(0, 5, "test")
|
|
# Row 5 doesn't exist, nothing should be written
|
|
assert all(cell == " " for row in canvas._grid for cell in row)
|
|
|
|
def test_put_text_marks_dirty_region(self):
|
|
"""put_text marks the written area as dirty."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(2, 1, "Hello")
|
|
dirty = canvas.get_dirty_regions()
|
|
assert len(dirty) == 1
|
|
assert dirty[0].x == 2 and dirty[0].y == 1
|
|
assert dirty[0].width == 5 and dirty[0].height == 1
|
|
|
|
def test_put_text_empty_string_no_dirty(self):
|
|
"""Empty string does not create dirty region."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(0, 0, "")
|
|
assert not canvas.is_dirty()
|
|
|
|
def test_put_region_single_cell(self):
|
|
"""put_region writes a single cell correctly."""
|
|
canvas = Canvas(5, 5)
|
|
content = [["X"]]
|
|
canvas.put_region(2, 2, content)
|
|
assert canvas._grid[2][2] == "X"
|
|
|
|
def test_put_region_multiple_rows(self):
|
|
"""put_region writes multiple rows correctly."""
|
|
canvas = Canvas(10, 10)
|
|
content = [["A", "B"], ["C", "D"]]
|
|
canvas.put_region(1, 1, content)
|
|
assert canvas._grid[1][1] == "A"
|
|
assert canvas._grid[1][2] == "B"
|
|
assert canvas._grid[2][1] == "C"
|
|
assert canvas._grid[2][2] == "D"
|
|
|
|
def test_put_region_partial_out_of_bounds(self):
|
|
"""put_region clips content that extends beyond canvas bounds."""
|
|
canvas = Canvas(5, 5)
|
|
content = [["A", "B", "C"], ["D", "E", "F"]]
|
|
canvas.put_region(4, 4, content)
|
|
# Only cell (4,4) should be within bounds
|
|
assert canvas._grid[4][4] == "A"
|
|
# Others are out of bounds
|
|
assert canvas._grid[4][5] == " " if 5 < 5 else True # index 5 doesn't exist
|
|
assert canvas._grid[5][4] == " " if 5 < 5 else True # row 5 doesn't exist
|
|
|
|
def test_put_region_marks_dirty(self):
|
|
"""put_region marks dirty region covering written area (clipped)."""
|
|
canvas = Canvas(10, 10)
|
|
content = [["A", "B", "C"], ["D", "E", "F"]]
|
|
canvas.put_region(2, 2, content)
|
|
dirty = canvas.get_dirty_regions()
|
|
assert len(dirty) == 1
|
|
assert dirty[0].x == 2 and dirty[0].y == 2
|
|
assert dirty[0].width == 3 and dirty[0].height == 2
|
|
|
|
def test_fill_rectangle(self):
|
|
"""fill() fills a rectangular region with character."""
|
|
canvas = Canvas(10, 10)
|
|
canvas.fill(2, 2, 3, 2, "*")
|
|
for y in range(2, 4):
|
|
for x in range(2, 5):
|
|
assert canvas._grid[y][x] == "*"
|
|
|
|
def test_fill_entire_canvas(self):
|
|
"""fill() can fill entire canvas."""
|
|
canvas = Canvas(5, 3)
|
|
canvas.fill(0, 0, 5, 3, "#")
|
|
for row in canvas._grid:
|
|
assert all(cell == "#" for cell in row)
|
|
|
|
def test_fill_empty_region_no_dirty(self):
|
|
"""fill with zero dimensions does not mark dirty."""
|
|
canvas = Canvas(10, 10)
|
|
canvas.fill(0, 0, 0, 5, "X")
|
|
assert not canvas.is_dirty()
|
|
|
|
def test_fill_clips_to_bounds(self):
|
|
"""fill clips to canvas boundaries."""
|
|
canvas = Canvas(5, 5)
|
|
canvas.fill(3, 3, 5, 5, "X")
|
|
# Should only fill within bounds: (3,3) to (4,4)
|
|
assert canvas._grid[3][3] == "X"
|
|
assert canvas._grid[3][4] == "X"
|
|
assert canvas._grid[4][3] == "X"
|
|
assert canvas._grid[4][4] == "X"
|
|
# Out of bounds should remain spaces
|
|
assert canvas._grid[5] if 5 < 5 else True # row 5 doesn't exist
|
|
|
|
def test_get_region_extracts_subgrid(self):
|
|
"""get_region returns correct rectangular subgrid."""
|
|
canvas = Canvas(10, 10)
|
|
for y in range(10):
|
|
for x in range(10):
|
|
canvas._grid[y][x] = chr(ord("A") + (x % 26))
|
|
region = canvas.get_region(2, 3, 4, 2)
|
|
assert len(region) == 2
|
|
assert len(region[0]) == 4
|
|
assert region[0][0] == "C" # (2,3) = 'C'
|
|
assert region[1][2] == "E" # (4,4) = 'E'
|
|
|
|
def test_get_region_out_of_bounds_returns_spaces(self):
|
|
"""get_region pads out-of-bounds areas with spaces."""
|
|
canvas = Canvas(5, 5)
|
|
canvas.put_text(0, 0, "HELLO")
|
|
# Region overlapping right edge: cols 3-4 inside, col5+ outside
|
|
region = canvas.get_region(3, 0, 5, 2)
|
|
assert region[0][0] == "L"
|
|
assert region[0][1] == "O"
|
|
assert region[0][2] == " " # col5 out of bounds
|
|
assert all(cell == " " for cell in region[1])
|
|
|
|
def test_get_region_flat_returns_lines(self):
|
|
"""get_region_flat returns list of joined strings."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(0, 0, "FIRST")
|
|
canvas.put_text(0, 1, "SECOND")
|
|
flat = canvas.get_region_flat(0, 0, 6, 2)
|
|
assert flat == ["FIRST ", "SECOND"]
|
|
|
|
def test_mark_dirty_manual(self):
|
|
"""mark_dirty() can be called manually to mark arbitrary region."""
|
|
canvas = Canvas(10, 10)
|
|
canvas.mark_dirty(5, 5, 3, 2)
|
|
dirty = canvas.get_dirty_regions()
|
|
assert len(dirty) == 1
|
|
assert dirty[0] == CanvasRegion(5, 5, 3, 2)
|
|
|
|
def test_get_dirty_rows_union(self):
|
|
"""get_dirty_rows() returns union of all dirty row indices."""
|
|
canvas = Canvas(10, 10)
|
|
canvas.put_text(0, 0, "A") # row 0
|
|
canvas.put_text(0, 2, "B") # row 2
|
|
canvas.mark_dirty(0, 1, 1, 1) # row 1
|
|
rows = canvas.get_dirty_rows()
|
|
assert rows == {0, 1, 2}
|
|
|
|
def test_is_dirty_after_operations(self):
|
|
"""is_dirty() returns True after any modifying operation."""
|
|
canvas = Canvas(10, 10)
|
|
assert not canvas.is_dirty()
|
|
canvas.put_text(0, 0, "X")
|
|
assert canvas.is_dirty()
|
|
_ = canvas.get_dirty_regions() # resets
|
|
assert not canvas.is_dirty()
|
|
|
|
def test_resize_same_size_no_change(self):
|
|
"""resize with same dimensions does nothing."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(0, 0, "TEST")
|
|
canvas.resize(10, 5)
|
|
assert canvas._grid[0][0] == "T"
|
|
|
|
def test_resize_larger_preserves_content(self):
|
|
"""resize to larger canvas preserves existing content."""
|
|
canvas = Canvas(5, 3)
|
|
canvas.put_text(1, 1, "AB")
|
|
canvas.resize(10, 6)
|
|
assert canvas.width == 10
|
|
assert canvas.height == 6
|
|
assert canvas._grid[1][1] == "A"
|
|
assert canvas._grid[1][2] == "B"
|
|
# New area should be spaces
|
|
assert canvas._grid[0][0] == " "
|
|
|
|
def test_resize_smaller_truncates(self):
|
|
"""resize to smaller canvas drops content outside new bounds."""
|
|
canvas = Canvas(10, 5)
|
|
canvas.put_text(8, 4, "XYZ")
|
|
canvas.resize(5, 3)
|
|
assert canvas.width == 5
|
|
assert canvas.height == 3
|
|
# Content at (8,4) should be lost
|
|
# But content within new bounds should remain
|
|
canvas2 = Canvas(10, 5)
|
|
canvas2.put_text(2, 2, "HI")
|
|
canvas2.resize(5, 3)
|
|
assert canvas2._grid[2][2] == "H"
|
|
|
|
def test_resize_does_not_auto_mark_dirty(self):
|
|
"""resize() does not automatically mark dirty (caller responsibility)."""
|
|
canvas = Canvas(10, 10)
|
|
canvas.put_text(0, 0, "A")
|
|
_ = canvas.get_dirty_regions() # reset
|
|
canvas.resize(5, 5)
|
|
# Resize doesn't mark dirty - this is current implementation
|
|
assert not canvas.is_dirty()
|