291 lines
9.3 KiB
Python
291 lines
9.3 KiB
Python
"""
|
|
Acceptance tests for HUD visibility and positioning.
|
|
|
|
These tests verify that HUD appears in the final output frame.
|
|
Frames are captured and saved as HTML reports for visual verification.
|
|
"""
|
|
|
|
import queue
|
|
|
|
from engine.data_sources.sources import ListDataSource, SourceItem
|
|
from engine.effects.plugins.hud import HudEffect
|
|
from engine.pipeline import Pipeline, PipelineConfig
|
|
from engine.pipeline.adapters import (
|
|
DataSourceStage,
|
|
DisplayStage,
|
|
EffectPluginStage,
|
|
SourceItemsToBufferStage,
|
|
)
|
|
from engine.pipeline.core import PipelineContext
|
|
from engine.pipeline.params import PipelineParams
|
|
from tests.acceptance_report import save_report
|
|
|
|
|
|
class FrameCaptureDisplay:
|
|
"""Display that captures frames for HTML report generation."""
|
|
|
|
def __init__(self):
|
|
self.frames: queue.Queue[list[str]] = queue.Queue()
|
|
self.width = 80
|
|
self.height = 24
|
|
self._recorded_frames: list[list[str]] = []
|
|
|
|
def init(self, width: int, height: int, reuse: bool = False) -> None:
|
|
self.width = width
|
|
self.height = height
|
|
|
|
def show(self, buffer: list[str], border: bool = False) -> None:
|
|
self._recorded_frames.append(list(buffer))
|
|
self.frames.put(list(buffer))
|
|
|
|
def clear(self) -> None:
|
|
pass
|
|
|
|
def cleanup(self) -> None:
|
|
pass
|
|
|
|
def get_dimensions(self) -> tuple[int, int]:
|
|
return (self.width, self.height)
|
|
|
|
def get_recorded_frames(self) -> list[list[str]]:
|
|
return self._recorded_frames
|
|
|
|
|
|
def _build_pipeline_with_hud(
|
|
items: list[SourceItem],
|
|
) -> tuple[Pipeline, FrameCaptureDisplay, PipelineContext]:
|
|
"""Build a pipeline with HUD effect."""
|
|
display = FrameCaptureDisplay()
|
|
|
|
ctx = PipelineContext()
|
|
params = PipelineParams()
|
|
params.viewport_width = display.width
|
|
params.viewport_height = display.height
|
|
params.frame_number = 0
|
|
params.effect_order = ["noise", "hud"]
|
|
params.effect_enabled = {"noise": False}
|
|
ctx.params = params
|
|
|
|
pipeline = Pipeline(
|
|
config=PipelineConfig(
|
|
source="list",
|
|
display="terminal",
|
|
effects=["hud"],
|
|
enable_metrics=True,
|
|
),
|
|
context=ctx,
|
|
)
|
|
|
|
source = ListDataSource(items, name="test-source")
|
|
pipeline.add_stage("source", DataSourceStage(source, name="test-source"))
|
|
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
|
|
|
hud_effect = HudEffect()
|
|
pipeline.add_stage("hud", EffectPluginStage(hud_effect, name="hud"))
|
|
|
|
pipeline.add_stage("display", DisplayStage(display, name="terminal"))
|
|
|
|
pipeline.build()
|
|
pipeline.initialize()
|
|
|
|
return pipeline, display, ctx
|
|
|
|
|
|
class TestHUDAcceptance:
|
|
"""Acceptance tests for HUD visibility."""
|
|
|
|
def test_hud_appears_in_final_output(self):
|
|
"""Test that HUD appears in the final display output.
|
|
|
|
This is the key regression test for Issue #47 - HUD was running
|
|
AFTER the display stage, making it invisible. Now it should appear
|
|
in the frame captured by the display.
|
|
"""
|
|
items = [SourceItem(content="Test content line", source="test", timestamp="0")]
|
|
pipeline, display, ctx = _build_pipeline_with_hud(items)
|
|
|
|
result = pipeline.execute(items)
|
|
assert result.success, f"Pipeline execution failed: {result.error}"
|
|
|
|
frame = display.frames.get(timeout=1)
|
|
frame_text = "\n".join(frame)
|
|
|
|
assert "MAINLINE" in frame_text, "HUD header not found in final output"
|
|
assert "EFFECT:" in frame_text, "EFFECT line not found in final output"
|
|
assert "PIPELINE:" in frame_text, "PIPELINE line not found in final output"
|
|
|
|
save_report(
|
|
test_name="test_hud_appears_in_final_output",
|
|
frames=display.get_recorded_frames(),
|
|
status="PASS",
|
|
metadata={
|
|
"description": "Verifies HUD appears in final display output (Issue #47 fix)",
|
|
"frame_lines": len(frame),
|
|
"has_mainline": "MAINLINE" in frame_text,
|
|
"has_effect": "EFFECT:" in frame_text,
|
|
"has_pipeline": "PIPELINE:" in frame_text,
|
|
},
|
|
)
|
|
|
|
def test_hud_cursor_positioning(self):
|
|
"""Test that HUD uses correct cursor positioning."""
|
|
items = [SourceItem(content="Sample content", source="test", timestamp="0")]
|
|
pipeline, display, ctx = _build_pipeline_with_hud(items)
|
|
|
|
result = pipeline.execute(items)
|
|
assert result.success
|
|
|
|
frame = display.frames.get(timeout=1)
|
|
has_cursor_pos = any("\x1b[" in line and "H" in line for line in frame)
|
|
|
|
save_report(
|
|
test_name="test_hud_cursor_positioning",
|
|
frames=display.get_recorded_frames(),
|
|
status="PASS",
|
|
metadata={
|
|
"description": "Verifies HUD uses cursor positioning",
|
|
"has_cursor_positioning": has_cursor_pos,
|
|
},
|
|
)
|
|
|
|
|
|
class TestCameraSpeedAcceptance:
|
|
"""Acceptance tests for camera speed modulation."""
|
|
|
|
def test_camera_speed_modulation(self):
|
|
"""Test that camera speed can be modulated at runtime.
|
|
|
|
This verifies the camera speed modulation feature added in Phase 1.
|
|
"""
|
|
from engine.camera import Camera
|
|
from engine.pipeline.adapters import CameraClockStage, CameraStage
|
|
|
|
display = FrameCaptureDisplay()
|
|
items = [
|
|
SourceItem(content=f"Line {i}", source="test", timestamp=str(i))
|
|
for i in range(50)
|
|
]
|
|
|
|
ctx = PipelineContext()
|
|
params = PipelineParams()
|
|
params.viewport_width = display.width
|
|
params.viewport_height = display.height
|
|
params.frame_number = 0
|
|
params.camera_speed = 1.0
|
|
ctx.params = params
|
|
|
|
pipeline = Pipeline(
|
|
config=PipelineConfig(
|
|
source="list",
|
|
display="terminal",
|
|
camera="scroll",
|
|
enable_metrics=False,
|
|
),
|
|
context=ctx,
|
|
)
|
|
|
|
source = ListDataSource(items, name="test")
|
|
pipeline.add_stage("source", DataSourceStage(source, name="test"))
|
|
pipeline.add_stage("render", SourceItemsToBufferStage(name="render"))
|
|
|
|
camera = Camera.scroll(speed=0.5)
|
|
pipeline.add_stage(
|
|
"camera_update", CameraClockStage(camera, name="camera-clock")
|
|
)
|
|
pipeline.add_stage("camera", CameraStage(camera, name="camera"))
|
|
pipeline.add_stage("display", DisplayStage(display, name="terminal"))
|
|
|
|
pipeline.build()
|
|
pipeline.initialize()
|
|
|
|
initial_camera_speed = camera.speed
|
|
|
|
for _ in range(3):
|
|
pipeline.execute(items)
|
|
|
|
speed_after_first_run = camera.speed
|
|
|
|
params.camera_speed = 5.0
|
|
ctx.params = params
|
|
|
|
for _ in range(3):
|
|
pipeline.execute(items)
|
|
|
|
speed_after_increase = camera.speed
|
|
|
|
assert speed_after_increase == 5.0, (
|
|
f"Camera speed should be modulated to 5.0, got {speed_after_increase}"
|
|
)
|
|
|
|
params.camera_speed = 0.0
|
|
ctx.params = params
|
|
|
|
for _ in range(3):
|
|
pipeline.execute(items)
|
|
|
|
speed_after_stop = camera.speed
|
|
assert speed_after_stop == 0.0, (
|
|
f"Camera speed should be 0.0, got {speed_after_stop}"
|
|
)
|
|
|
|
save_report(
|
|
test_name="test_camera_speed_modulation",
|
|
frames=display.get_recorded_frames()[:5],
|
|
status="PASS",
|
|
metadata={
|
|
"description": "Verifies camera speed can be modulated at runtime",
|
|
"initial_camera_speed": initial_camera_speed,
|
|
"speed_after_first_run": speed_after_first_run,
|
|
"speed_after_increase": speed_after_increase,
|
|
"speed_after_stop": speed_after_stop,
|
|
},
|
|
)
|
|
|
|
|
|
class TestEmptyLinesAcceptance:
|
|
"""Acceptance tests for empty line handling."""
|
|
|
|
def test_empty_lines_remain_empty(self):
|
|
"""Test that empty lines remain empty in output (regression for padding bug)."""
|
|
items = [
|
|
SourceItem(content="Line1\n\nLine3\n\nLine5", source="test", timestamp="0")
|
|
]
|
|
|
|
display = FrameCaptureDisplay()
|
|
ctx = PipelineContext()
|
|
params = PipelineParams()
|
|
params.viewport_width = display.width
|
|
params.viewport_height = display.height
|
|
ctx.params = params
|
|
|
|
pipeline = Pipeline(
|
|
config=PipelineConfig(enable_metrics=False),
|
|
context=ctx,
|
|
)
|
|
|
|
source = ListDataSource(items, name="test")
|
|
pipeline.add_stage("source", DataSourceStage(source, name="test"))
|
|
pipeline.add_stage("render", SourceItemsToBufferStage(name="render"))
|
|
pipeline.add_stage("display", DisplayStage(display, name="terminal"))
|
|
|
|
pipeline.build()
|
|
pipeline.initialize()
|
|
|
|
result = pipeline.execute(items)
|
|
assert result.success
|
|
|
|
frame = display.frames.get(timeout=1)
|
|
has_truly_empty = any(not line for line in frame)
|
|
|
|
save_report(
|
|
test_name="test_empty_lines_remain_empty",
|
|
frames=display.get_recorded_frames(),
|
|
status="PASS",
|
|
metadata={
|
|
"description": "Verifies empty lines remain empty (not padded)",
|
|
"has_truly_empty_lines": has_truly_empty,
|
|
},
|
|
)
|
|
|
|
assert has_truly_empty, f"Expected at least one empty line, got: {frame[1]!r}"
|