forked from genewildish/Mainline
feat(tests): Add acceptance tests and HTML report generator
This commit is contained in:
290
tests/test_acceptance.py
Normal file
290
tests/test_acceptance.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
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}"
|
||||
Reference in New Issue
Block a user