Files
groundstation/internal/display/osd_test.go
T

228 lines
6.6 KiB
Go

package display
import (
"strings"
"testing"
"git.piksar.eu/piotr/groundstation/internal/osd"
)
// stubSource serves one fixed snapshot state, or none.
type stubSource struct {
snapshot osd.Snapshot
state osd.SnapshotState
}
func (s stubSource) Latest() (osd.Snapshot, osd.SnapshotState) { return s.snapshot, s.state }
// pendingSource is the contract state while the adaptive-link producers do
// not exist: arm state and CH6 ceiling from the snapshot, everything else
// explicitly pending.
func pendingSource(armed bool, ceiling uint8) stubSource {
return stubSource{snapshot: osd.Snapshot{
Armed: armed,
OpConfig: osd.OperatorConfig{MaxPower: ceiling},
Policy: osd.GSPolicy{Pending: true},
Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending},
Link: osd.GSObservedLink{Known: false},
}, state: osd.SnapshotFresh}
}
func assertLines(t *testing.T, canvas *fakeCanvas, want []string) {
t.Helper()
lines := canvas.lines()
if len(lines) < len(want) {
t.Fatalf("lines=%q want at least %q", lines, want)
}
// Compare only the most recent draw pass: the canvas accumulates lines
// across passes.
lines = lines[len(lines)-len(want):]
for i := range want {
if lines[i] != want[i] {
t.Fatalf("line %d=%q want %q", i, lines[i], want[i])
}
}
}
func TestOSDTemplateRendersExplicitNoDataState(t *testing.T) {
// Before any snapshot has ever existed the overlay must show explicit
// unknown state plus the authoritative video state instead of a blank
// overlay — including over the no-video fallback presentation.
canvas := &fakeCanvas{width: 800, height: 600}
NewOSDTemplate(NewVideoPresence(), stubSource{}).Draw(canvas)
clears, flushes := canvas.drawCounts()
if clears != 1 || flushes != 1 {
t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes)
}
assertLines(t, canvas, []string{
"NO DATA",
"VTX tbl:-- ceil:-- req:-- ack:--",
"VIDEO OFF",
})
}
func TestOSDTemplateDrawsFreshSnapshotWithVideoOff(t *testing.T) {
// Fresh snapshot data renders on top of the no-video fallback
// presentation; the observed video lifecycle shows as VIDEO OFF.
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
NewOSDTemplate(presence, pendingSource(true, 63)).Draw(canvas)
assertLines(t, canvas, []string{
"ARM ARMED",
"VTX tbl:-- ceil:63 req:-- ack:--",
"VIDEO OFF",
})
}
func TestOSDTemplateMarksStaleLastKnownData(t *testing.T) {
// A snapshot beyond the freshness window stays visible as last-known
// data, explicitly marked stale — never presented as current.
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
Armed: true,
OpConfig: osd.OperatorConfig{
Table: osd.Table{Gen: 3, Hash: 0xcafe},
MaxPower: 63,
},
Policy: osd.GSPolicy{Level: 42},
Ack: osd.VTXAck{
Table: osd.Table{Gen: 3},
Level: 40,
Lifecycle: osd.VTXLifecycleApplied,
},
}, state: osd.SnapshotStale}).Draw(canvas)
assertLines(t, canvas, []string{
"ARM ARMED",
"VTX tbl:3 ceil:63 req:42 ack:40",
"DATA STALE",
"VIDEO OFF",
})
}
func TestOSDTemplateStaysVisibleAcrossVideoTransition(t *testing.T) {
// Video appearing fullscreen must not hide the OSD: the data lines stay
// and only the authoritative VIDEO line flips.
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
template := NewOSDTemplate(presence, pendingSource(true, 63))
template.Draw(canvas)
assertLines(t, canvas, []string{
"ARM ARMED",
"VTX tbl:-- ceil:63 req:-- ack:--",
"VIDEO OFF",
})
presence.SetActive(true)
template.Draw(canvas)
assertLines(t, canvas, []string{
"ARM ARMED",
"VTX tbl:-- ceil:63 req:-- ack:--",
"VIDEO ON",
})
}
func TestOSDTemplateRendersSelectedTableRequestedAndAppliedStates(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
presence.SetActive(true)
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
Armed: true,
OpConfig: osd.OperatorConfig{
Table: osd.Table{Gen: 3, Hash: 0xcafe},
MaxPower: 127,
},
Policy: osd.GSPolicy{Level: 42},
Ack: osd.VTXAck{
Table: osd.Table{Gen: 3},
Level: 40,
Lifecycle: osd.VTXLifecycleApplied,
},
}, state: osd.SnapshotFresh}).Draw(canvas)
assertLines(t, canvas, []string{
"ARM ARMED",
"VTX tbl:3 ceil:127 req:42 ack:40",
"VIDEO ON",
})
}
func TestOSDTemplateRendersAckErrorState(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
OpConfig: osd.OperatorConfig{MaxPower: 10},
Policy: osd.GSPolicy{Pending: true},
Ack: osd.VTXAck{Lifecycle: osd.VTXLifecycleError},
}, state: osd.SnapshotFresh}).Draw(canvas)
assertLines(t, canvas, []string{
"ARM OFF",
"VTX tbl:-- ceil:10 req:-- ack:ERR",
"VIDEO OFF",
})
}
func TestOSDTemplateRendersLevelZeroAsExplicitOff(t *testing.T) {
// Level 0 in an available policy or acknowledgment is an explicit
// off / no-runtime-command state and must render as 0; only a pending
// value — no producer yet — renders as the pending mark.
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
OpConfig: osd.OperatorConfig{MaxPower: 0},
Policy: osd.GSPolicy{Level: 0},
Ack: osd.VTXAck{Level: 0, Lifecycle: osd.VTXLifecycleApplied},
}, state: osd.SnapshotFresh}).Draw(canvas)
assertLines(t, canvas, []string{
"ARM OFF",
"VTX tbl:-- ceil:0 req:0 ack:0",
"VIDEO OFF",
})
}
func TestOSDTemplateVideoLineUsesOnlyObservedLifecycle(t *testing.T) {
// The VIDEO line is sourced from the video lifecycle the display
// observes on the display server, never from the snapshot's Video
// field (the supervisor's launcher knowledge).
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
Video: true,
}, state: osd.SnapshotFresh}).Draw(canvas)
for _, line := range canvas.lines() {
if strings.HasPrefix(line, "VIDEO") && line != "VIDEO OFF" {
t.Fatalf("line=%q: snapshot presence must never replace the observed lifecycle", line)
}
}
}
func TestOSDTemplateNeverDrawsTheFallbackText(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
template := NewOSDTemplate(presence, pendingSource(true, 63))
for _, active := range []bool{false, true, false} {
presence.SetActive(active)
template.Draw(canvas)
for _, line := range canvas.lines() {
if line == wantFallbackText {
t.Fatalf("the OSD is a telemetry surface, not a fallback: drew %q", line)
}
}
}
}