Files
groundstation/cmd/groundstation/osd_test.go
T

186 lines
5.4 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"io"
"log"
"os/exec"
"testing"
"time"
"git.piksar.eu/piotr/groundstation/internal/config"
"git.piksar.eu/piotr/groundstation/internal/osd"
"git.piksar.eu/piotr/groundstation/internal/protocol"
)
func TestNewSnapshotDerivesArmPowerAndVideoPresence(t *testing.T) {
cfg := config.Default()
frame := protocol.RCFrame{Sequence: 3, Timestamp: 42}
frame.Channels[4] = 512 // arm channel above the armed threshold
frame.Channels[5] = -1000 // CH6 knob below the video threshold
snapshot := newSnapshot(frame, cfg, nil)
if snapshot.Sequence != 3 || snapshot.TimestampMS != 42 {
t.Fatalf("snapshot=%+v must carry the frame provenance", snapshot)
}
if snapshot.Channels != frame.Channels {
t.Fatal("the snapshot must carry the final RC state unchanged")
}
if !snapshot.Armed {
t.Fatal("arm channel above the threshold must derive armed")
}
if snapshot.Power {
t.Fatal("CH6 below the video threshold must derive no requested WFB power")
}
if snapshot.Video {
t.Fatal("without a video launcher there is no decoded-video presence")
}
frame.Channels[4] = -256
frame.Channels[5] = 100
snapshot = newSnapshot(frame, cfg, &videoController{cmd: &exec.Cmd{}})
if snapshot.Armed {
t.Fatal("arm channel below the threshold must derive disarmed")
}
if !snapshot.Power {
t.Fatal("CH6 above the video threshold must derive requested WFB power")
}
if !snapshot.Video {
t.Fatal("a running video launcher is the supervisor's decoded-video presence")
}
}
func TestRunningReportsVideoLauncherPresence(t *testing.T) {
video := newVideoController("fpv-video")
if video.Running() {
t.Fatal("a launcher never started must not report running")
}
video.cmd = &exec.Cmd{}
video.done = make(chan error, 1)
if !video.Running() {
t.Fatal("a live launcher must report running")
}
// A launcher that exited on its own stays recorded until the next Set;
// its completed wait must end its decoded-video presence.
video.done <- errors.New("exited")
if video.Running() {
t.Fatal("a launcher with a completed wait must not report running")
}
}
func TestRunningEndsWhenTheLauncherExitsOnItsOwn(t *testing.T) {
video := newVideoController("true")
if err := video.Set(true); err != nil {
t.Fatal(err)
}
// The launcher exits on its own; its pending wait result must end its
// decoded-video presence without anyone consuming the stop path's
// done signal.
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if !video.Running() {
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("a launcher that exited on its own must not report running")
}
func TestRunPublishesSnapshotsForValidFramesOnly(t *testing.T) {
cfg := config.Default()
first := protocol.RCFrame{Sequence: 1}
first.Channels[4] = 512
second := protocol.RCFrame{Sequence: 2}
second.Channels[4] = -256
second.Channels[5] = 100
var input bytes.Buffer
input.Write(protocol.EncodeRC(first))
input.Write([]byte("junk frame bytes that fail the CRC"))
input.Write(protocol.EncodeRC(second))
publisher := osd.NewPublisher()
if err := run(context.Background(), &input, log.New(io.Discard, "", 0), nil, cfg, publisher); !errors.Is(err, io.EOF) {
t.Fatalf("run=%v want clean EOF after the input ends", err)
}
got, ok := publisher.Latest()
if !ok {
t.Fatal("valid frames must publish snapshots")
}
if got.Sequence != 2 {
t.Fatalf("sequence=%d want the latest valid frame 2", got.Sequence)
}
if got.Armed {
t.Fatal("the latest frame's arm state must be disarmed")
}
if !got.Power {
t.Fatal("the latest frame's CH6 must request WFB power")
}
if got.PublishedMS == 0 {
t.Fatal("the publisher must stamp the publication time")
}
}
func TestRunPublishesWithoutAConfiguredVideoLauncher(t *testing.T) {
cfg := config.Default()
frame := protocol.RCFrame{Sequence: 5}
frame.Channels[5] = 0
publisher := osd.NewPublisher()
if err := run(context.Background(),
bytes.NewReader(protocol.EncodeRC(frame)),
log.New(io.Discard, "", 0), nil, cfg, publisher); !errors.Is(err, io.EOF) {
t.Fatalf("run=%v want clean EOF", err)
}
got, ok := publisher.Latest()
if !ok {
t.Fatal("frames must publish even without a video launcher")
}
if !got.Power || got.Video {
t.Fatalf("snapshot=%+v must derive the requested power without video presence", got)
}
}
func TestRunPublishNeverBlocksTheUARTPath(t *testing.T) {
// A burst of frames must be read and published with no waiting on the
// state-file writer: the publisher's Publish is non-blocking by design,
// so the loop over a large input returns promptly.
cfg := config.Default()
publisher := osd.NewFilePublisher("/proc/nonexistent/osd-state.bin", log.New(io.Discard, "", 0))
defer publisher.Close()
frame := protocol.RCFrame{Sequence: 1}
var input bytes.Buffer
for i := 0; i < 500; i++ {
frame.Sequence = uint16(i)
input.Write(protocol.EncodeRC(frame))
}
if err := run(context.Background(), &input, log.New(io.Discard, "", 0), nil, cfg, publisher); !errors.Is(err, io.EOF) {
t.Fatalf("run=%v want clean EOF", err)
}
if got, _ := publisher.Latest(); got.Sequence != 499 {
t.Fatalf("sequence=%d want the last frame", got.Sequence)
}
}
func TestNewSnapshotArmChannelConfigurationIsApplied(t *testing.T) {
cfg := config.Default()
cfg.OSD.ArmIndex = 6
cfg.OSD.ArmedThreshold = 256
frame := protocol.RCFrame{}
frame.Channels[6] = 257
if snapshot := newSnapshot(frame, cfg, nil); !snapshot.Armed {
t.Fatal("the configured arm index and threshold must drive the arm state")
}
}