188 lines
6.1 KiB
Go
188 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.piksar.eu/piotr/groundstation/internal/display"
|
|
"git.piksar.eu/piotr/groundstation/internal/osd"
|
|
)
|
|
|
|
type recordingCanvas struct {
|
|
width, height int
|
|
lines []string
|
|
clears, flushes int
|
|
}
|
|
|
|
func (c *recordingCanvas) Size() (int, int) { return c.width, c.height }
|
|
func (c *recordingCanvas) Clear() { c.clears++ }
|
|
func (c *recordingCanvas) Text(_, _ int, message string) {
|
|
c.lines = append(c.lines, message)
|
|
}
|
|
func (c *recordingCanvas) Flush() { c.flushes++ }
|
|
|
|
func TestBuildTargetBuildsIndependentVitureLayers(t *testing.T) {
|
|
layers, err := buildTarget("x11", filepath.Join(t.TempDir(), "osd-state.bin"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(layers) != 2 {
|
|
t.Fatalf("layers=%d want the fallback canvas and the OSD overlay", len(layers))
|
|
}
|
|
canvas, osdLayer := layers[0], layers[1]
|
|
if canvas.Name != "fallback-canvas" || osdLayer.Name != "osd-overlay" {
|
|
t.Fatalf("layer names %q/%q want fallback-canvas/osd-overlay", canvas.Name, osdLayer.Name)
|
|
}
|
|
|
|
canvasOpener, ok := canvas.Opener.(display.X11Opener)
|
|
if !ok {
|
|
t.Fatalf("canvas opener %T must be the X11 target", canvas.Opener)
|
|
}
|
|
if canvasOpener.Role != display.RoleBelow {
|
|
t.Fatalf("canvas layer role %v must stay below video", canvasOpener.Role)
|
|
}
|
|
osdOpener, ok := osdLayer.Opener.(display.X11Opener)
|
|
if !ok {
|
|
t.Fatalf("OSD opener %T must be the X11 target", osdLayer.Opener)
|
|
}
|
|
if osdOpener.Role != display.RoleAbove {
|
|
t.Fatalf("OSD layer role %v must stay above video", osdOpener.Role)
|
|
}
|
|
// The OSD refreshes on its own slower cadence; the canvas stays purely
|
|
// event-driven.
|
|
if osdOpener.RefreshInterval <= 0 {
|
|
t.Fatal("the OSD overlay must redraw on its own refresh interval")
|
|
}
|
|
if canvasOpener.RefreshInterval != 0 {
|
|
t.Fatal("the fallback canvas must keep its event-driven redraws")
|
|
}
|
|
|
|
// Both layers must share one presence signal so the video lifecycle
|
|
// observed by either session drives both templates.
|
|
if canvasOpener.Video == nil || osdOpener.Video == nil || canvasOpener.Video != osdOpener.Video {
|
|
t.Fatal("layers must share one video presence signal")
|
|
}
|
|
|
|
// The canvas layer keeps the target-specific fallback template with the
|
|
// exact fallback text; the OSD layer shows explicit state without a
|
|
// snapshot. Both layers are fullscreen at startup with no video — the
|
|
// canvas below (RoleBelow) and the OSD above (RoleAbove), per the
|
|
// display package's fullscreen-args contract.
|
|
canvasSurface := &recordingCanvas{width: 800, height: 600}
|
|
canvas.Template.Draw(canvasSurface)
|
|
if len(canvasSurface.lines) != 1 || canvasSurface.lines[0] != "Brak połączenia WFB" {
|
|
t.Fatalf("canvas lines=%q want the exact fallback text", canvasSurface.lines)
|
|
}
|
|
|
|
osdSurface := &recordingCanvas{width: 800, height: 600}
|
|
osdLayer.Template.Draw(osdSurface)
|
|
wantStartup := []string{"NO DATA", "VTX tbl:-- ceil:-- req:-- ack:--", "VIDEO OFF"}
|
|
if len(osdSurface.lines) != len(wantStartup) {
|
|
t.Fatalf("OSD lines=%q want the explicit startup state %q", osdSurface.lines, wantStartup)
|
|
}
|
|
for i := range wantStartup {
|
|
if osdSurface.lines[i] != wantStartup[i] {
|
|
t.Fatalf("line %d=%q want %q", i, osdSurface.lines[i], wantStartup[i])
|
|
}
|
|
}
|
|
|
|
// Video presence hides the fallback on the canvas layer only.
|
|
canvasOpener.Video.SetActive(true)
|
|
canvasSurface.lines = nil
|
|
canvas.Template.Draw(canvasSurface)
|
|
if len(canvasSurface.lines) != 0 {
|
|
t.Fatalf("canvas lines=%q must disappear while video is present", canvasSurface.lines)
|
|
}
|
|
canvasOpener.Video.SetActive(false)
|
|
}
|
|
|
|
func TestBuildTargetOSDLayerRendersPublishedSnapshots(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "run", "osd-state.bin")
|
|
publisher := osd.NewFilePublisher(state, nil)
|
|
defer publisher.Close()
|
|
|
|
snapshot := osd.Snapshot{
|
|
Sequence: 11,
|
|
Armed: true,
|
|
OpConfig: osd.OperatorConfig{MaxPower: 95},
|
|
Policy: osd.GSPolicy{Pending: true},
|
|
Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending},
|
|
Link: osd.GSObservedLink{Known: false},
|
|
}
|
|
publisher.Publish(snapshot)
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := osd.DecodeFile(state); err == nil {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
layers, err := buildTarget("x11", state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
osdLayer := layers[1]
|
|
osdOpener := osdLayer.Opener.(display.X11Opener)
|
|
osdOpener.Video.SetActive(true)
|
|
|
|
surface := &recordingCanvas{width: 800, height: 600}
|
|
osdLayer.Template.Draw(surface)
|
|
|
|
want := []string{"ARM ARMED", "VTX tbl:-- ceil:95 req:-- ack:--", "VIDEO ON"}
|
|
if len(surface.lines) != len(want) {
|
|
t.Fatalf("OSD lines=%q want %q from the supervisor-published snapshot", surface.lines, want)
|
|
}
|
|
for i := range want {
|
|
if surface.lines[i] != want[i] {
|
|
t.Fatalf("line %d=%q want %q", i, surface.lines[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildTargetRejectsUnknownTarget(t *testing.T) {
|
|
if _, err := buildTarget("hdmi-mirror", ""); err == nil {
|
|
t.Fatal("unknown target must fail")
|
|
}
|
|
}
|
|
|
|
func TestFormatDiagnosticRendersStructuredFields(t *testing.T) {
|
|
got := formatDiagnostic(display.Diagnostic{
|
|
Target: "x11",
|
|
Layer: "fallback-canvas",
|
|
Event: display.EventOpenFailed,
|
|
Err: errors.New("connect X11: refused"),
|
|
RetryDelay: 500 * time.Millisecond,
|
|
})
|
|
want := `target=x11 layer=fallback-canvas event=open_failed error="connect X11: refused" retry_delay=500ms`
|
|
if got != want {
|
|
t.Fatalf("formatDiagnostic=%q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestWireDiagnosticsTagsEveryLayerWithItsName(t *testing.T) {
|
|
layers, err := buildTarget("x11", filepath.Join(t.TempDir(), "osd-state.bin"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wireDiagnostics("x11", layers)
|
|
|
|
for _, layer := range layers {
|
|
if layer.Options.Target != "x11" {
|
|
t.Fatalf("layer %q target=%q want x11", layer.Name, layer.Options.Target)
|
|
}
|
|
if layer.Options.Layer != layer.Name {
|
|
t.Fatalf("layer %q diagnostic layer=%q", layer.Name, layer.Options.Layer)
|
|
}
|
|
if layer.Options.Diagnostic == nil {
|
|
t.Fatalf("layer %q must carry the reconnect diagnostic hook", layer.Name)
|
|
}
|
|
}
|
|
|
|
// The shared hook must be safe to call from any layer goroutine.
|
|
layers[0].Options.Diagnostic(display.Diagnostic{Target: "x11", Layer: layers[0].Name})
|
|
}
|