157 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|