Files
groundstation/internal/osd/snapshot_test.go
T

157 lines
4.8 KiB
Go

package osd
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
)
func testSnapshot() Snapshot {
var snapshot Snapshot
snapshot.Sequence = 0xbEEF
snapshot.TimestampMS = 123456
for i := range snapshot.Channels {
snapshot.Channels[i] = int16(-1024 + i*137)
}
snapshot.Armed = true
snapshot.Video = true
snapshot.OpConfig = OperatorConfig{Table: Table{Gen: 1}, MaxPower: 95}
snapshot.Policy = GSPolicy{Pending: true}
snapshot.Ack = VTXAck{Pending: true}
snapshot.Link = GSObservedLink{Known: false}
snapshot.PublishedMS = time.Now().UnixMilli()
return snapshot
}
func TestSnapshotSizeMatchesLayout(t *testing.T) {
// Header (magic, version, sequence, timestamp) + all 16 channels +
// flags + publish time + the four contract values + CRC must stay a
// small fixed record.
if Size != 50+1+12+17+14+8+2 {
t.Fatalf("Size=%d must match the documented layout", Size)
}
if encoded := Encode(testSnapshot()); len(encoded) != Size {
t.Fatalf("encoded=%d bytes want %d", len(encoded), Size)
}
}
func TestEncodeDecodeSnapshotRoundtrip(t *testing.T) {
want := testSnapshot()
got, err := Decode(Encode(want))
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("decoded=%+v want %+v", got, want)
}
}
func TestDecodeRejectsDamagedSnapshots(t *testing.T) {
encoded := Encode(testSnapshot())
if _, err := Decode(encoded[:len(encoded)-1]); !errors.Is(err, ErrLength) {
t.Fatalf("truncated decode=%v want ErrLength", err)
}
magic := append([]byte(nil), encoded...)
magic[1] = 'X'
if _, err := Decode(magic); !errors.Is(err, ErrMagic) {
t.Fatalf("magic decode=%v want ErrMagic", err)
}
version := append([]byte(nil), encoded...)
version[2] = Version + 1
if _, err := Decode(version); !errors.Is(err, ErrVersion) {
t.Fatalf("version decode=%v want ErrVersion", err)
}
corrupt := append([]byte(nil), encoded...)
corrupt[10] ^= 0xff
if _, err := Decode(corrupt); !errors.Is(err, ErrCRC) {
t.Fatalf("corrupt payload decode=%v want ErrCRC", err)
}
corruptCRC := append([]byte(nil), encoded...)
corruptCRC[len(corruptCRC)-1] ^= 0xff
if _, err := Decode(corruptCRC); !errors.Is(err, ErrCRC) {
t.Fatalf("corrupt crc decode=%v want ErrCRC", err)
}
}
func TestFileSourceAgeIsNotResetByRewritingIdenticalBytes(t *testing.T) {
// Publisher heartbeats rewrite the last snapshot's identical bytes; the
// display's staleness must key on the snapshot's own publish time, so a
// rewrite can never present stale data as current.
path := filepath.Join(t.TempDir(), "osd-state.bin")
now := time.UnixMilli(10_000_000)
source := NewFileSource(path)
source.now = func() time.Time { return now }
want := testSnapshot()
want.PublishedMS = now.UnixMilli()
if err := os.WriteFile(path, Encode(want), 0644); err != nil {
t.Fatal(err)
}
if _, state := source.Latest(); state != SnapshotFresh {
t.Fatalf("state=%v want fresh at publish time", state)
}
// Heartbeat rewrite of identical bytes at a later wall-clock time.
if err := os.WriteFile(path, Encode(want), 0644); err != nil {
t.Fatal(err)
}
source.now = func() time.Time { return now.Add(2 * Freshness) }
got, state := source.Latest()
if state != SnapshotStale {
t.Fatalf("state=%v want stale despite the heartbeat rewrite", state)
}
if got != want {
t.Fatalf("last-known snapshot=%+v want %+v", got, want)
}
}
func TestFileSourceReportsFreshStaleAndMissingStates(t *testing.T) {
path := filepath.Join(t.TempDir(), "osd-state.bin")
source := NewFileSource(path)
fixed := time.UnixMilli(10_000_000)
source.now = func() time.Time { return fixed }
if _, state := source.Latest(); state != SnapshotNone {
t.Fatalf("a missing state file must report state %v, got %v", SnapshotNone, state)
}
if err := os.WriteFile(path, []byte("garbage that is not a snapshot"), 0644); err != nil {
t.Fatal(err)
}
if _, state := source.Latest(); state != SnapshotNone {
t.Fatalf("a corrupt state file must report state %v, got %v", SnapshotNone, state)
}
want := testSnapshot()
want.PublishedMS = fixed.UnixMilli()
if err := os.WriteFile(path, Encode(want), 0644); err != nil {
t.Fatal(err)
}
got, state := source.Latest()
if state != SnapshotFresh {
t.Fatalf("a fresh snapshot must report state %v, got %v", SnapshotFresh, state)
}
if got != want {
t.Fatalf("got=%+v want %+v", got, want)
}
// The last successfully decoded snapshot must survive past the
// freshness window, marked stale instead of being hidden: the display
// shows last-known values as explicitly stale.
stale := want
stale.PublishedMS = fixed.Add(-Freshness - time.Second).UnixMilli()
if err := os.WriteFile(path, Encode(stale), 0644); err != nil {
t.Fatal(err)
}
got, state = source.Latest()
if state != SnapshotStale {
t.Fatalf("a snapshot beyond the freshness window must report state %v, got %v", SnapshotStale, state)
}
if got != stale {
t.Fatalf("last-known snapshot=%+v want %+v", got, stale)
}
}