Files
groundstation/internal/display/osd_test.go
T

197 lines
6.2 KiB
Go

package display
import (
"testing"
"git.piksar.eu/piotr/groundstation/internal/osd"
)
// stubSource serves one fixed snapshot, or none.
type stubSource struct {
snapshot osd.Snapshot
ok bool
}
func (s stubSource) Latest() (osd.Snapshot, bool) { return s.snapshot, s.ok }
// armedPendingSource is the contract state until the adaptive-link
// producers exist: armed with a CH6 ceiling, everything else pending.
func armedPendingSource(ceiling uint8) stubSource {
return stubSource{snapshot: osd.Snapshot{
Armed: true,
OpConfig: osd.OperatorConfig{MaxPower: ceiling},
Policy: osd.GSPolicy{Pending: true},
Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending},
Link: osd.GSObservedLink{Known: false},
}, ok: true}
}
func TestOSDTemplateStaysTransparentWhileVideoAbsent(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
NewOSDTemplate(NewVideoPresence(), armedPendingSource(63)).Draw(canvas)
clears, flushes := canvas.drawCounts()
if clears != 1 || flushes != 1 {
t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes)
}
if lines := canvas.lines(); len(lines) != 0 {
t.Fatalf("OSD lines=%q want none: without video the fallback canvas owns the screen", lines)
}
}
func TestOSDTemplateDrawsArmStateAndPendingContractWhileVideoPresent(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
presence.SetActive(true)
NewOSDTemplate(presence, armedPendingSource(63)).Draw(canvas)
clears, flushes := canvas.drawCounts()
if clears != 1 || flushes != 1 {
t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes)
}
lines := canvas.lines()
want := []string{"ARM ARMED", "VTX tbl:-- ceil:63 req:-- ack:--"}
if len(lines) != len(want) {
t.Fatalf("lines=%q want %q", lines, want)
}
for i := range want {
if lines[i] != want[i] {
t.Fatalf("line %d=%q want %q", i, lines[i], want[i])
}
}
}
func TestOSDTemplateDrawsDisarmedState(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
presence.SetActive(true)
NewOSDTemplate(presence, stubSource{ok: true}).Draw(canvas)
lines := canvas.lines()
if len(lines) != 2 || lines[0] != "ARM OFF" {
t.Fatalf("lines=%q want the disarmed arm state and the pending contract line", lines)
}
}
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,
},
}, ok: true}).Draw(canvas)
lines := canvas.lines()
want := []string{"ARM ARMED", "VTX tbl:3 ceil:127 req:42 ack:40"}
if len(lines) != len(want) {
t.Fatalf("lines=%q want %q", lines, want)
}
for i := range want {
if lines[i] != want[i] {
t.Fatalf("line %d=%q want %q", i, lines[i], want[i])
}
}
}
func TestOSDTemplateRendersAckErrorState(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
presence.SetActive(true)
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
OpConfig: osd.OperatorConfig{MaxPower: 10},
Policy: osd.GSPolicy{Pending: true},
Ack: osd.VTXAck{Lifecycle: osd.VTXLifecycleError},
}, ok: true}).Draw(canvas)
lines := canvas.lines()
if len(lines) != 2 || lines[1] != "VTX tbl:-- ceil:10 req:-- ack:ERR" {
t.Fatalf("lines=%q want the acknowledged error state", lines)
}
}
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()
presence.SetActive(true)
NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{
OpConfig: osd.OperatorConfig{MaxPower: 0},
Policy: osd.GSPolicy{Level: 0},
Ack: osd.VTXAck{Level: 0, Lifecycle: osd.VTXLifecycleApplied},
}, ok: true}).Draw(canvas)
lines := canvas.lines()
want := "VTX tbl:-- ceil:0 req:0 ack:0"
if len(lines) != 2 || lines[1] != want {
t.Fatalf("lines=%q want %q", lines, want)
}
}
func TestOSDTemplateStaysTransparentWithoutASnapshotWhileVideoPresent(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
presence.SetActive(true)
NewOSDTemplate(presence, stubSource{}).Draw(canvas)
if lines := canvas.lines(); len(lines) != 0 {
t.Fatalf("OSD lines=%q want none without a published snapshot", lines)
}
}
func TestOSDTemplateGatesOnObservedVideoPresenceNotSnapshotVideo(t *testing.T) {
// The rendering gate is the video lifecycle the display observes on the
// display server, never the snapshot's Video field: the supervisor's
// field is its launcher knowledge, while the display trusts only the
// presented video output itself.
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
claimingVideo := stubSource{snapshot: osd.Snapshot{Armed: true, Video: true}, ok: true}
NewOSDTemplate(presence, claimingVideo).Draw(canvas)
if lines := canvas.lines(); len(lines) != 0 {
t.Fatalf("lines=%q want none: snapshot presence must never replace the observed lifecycle", lines)
}
denyingVideo := stubSource{snapshot: osd.Snapshot{Armed: true}, ok: true}
presence.SetActive(true)
NewOSDTemplate(presence, denyingVideo).Draw(canvas)
if lines := canvas.lines(); len(lines) == 0 {
t.Fatal("the observed video lifecycle must gate rendering even when the snapshot denies video")
}
}
func TestOSDTemplateNeverDrawsTheFallbackText(t *testing.T) {
canvas := &fakeCanvas{width: 800, height: 600}
presence := NewVideoPresence()
template := NewOSDTemplate(presence, armedPendingSource(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)
}
}
}
}