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) } }