package udpsprotocol // Accumulate mode ships one full snapshot of EVERY signal per RT cycle — // arrays included. See UDPStreamer.cpp pass 5 ("ALL signals (scalars and // arrays alike) are tagged accumulated = true") and SerializeAccumulated, // which writes, for each signal in CONFIG order, numSamples consecutive // snapshots of that signal's full element set. // // The tests below build a payload byte-for-byte the way the C++ producer // does, so a decoding regression shows up here rather than as a mangled // waveform three components downstream. import ( "encoding/binary" "math" "testing" "time" ) // buildAccumulatePayload lays out an Accumulate DATA payload exactly as // UDPStreamer::SerializeAccumulated does: // // [8 HRT][4 numSamples] then, per signal, numSamples × NumElements float64. // // slots[i][k] holds signal i's element set for cycle k. func buildAccumulatePayload(hrt uint64, slots [][][]float64) []byte { numSamples := 0 if len(slots) > 0 { numSamples = len(slots[0]) } out := make([]byte, 12) binary.LittleEndian.PutUint64(out[0:8], hrt) binary.LittleEndian.PutUint32(out[8:12], uint32(numSamples)) for _, sig := range slots { for _, elems := range sig { for _, v := range elems { var b [8]byte binary.LittleEndian.PutUint64(b[:], math.Float64bits(v)) out = append(out, b[:]...) } } } return out } // TestParseDataAccumulateGivesEachSlotItsOwnArray pins the array case: with an // accumulated batch, slot k's array signal must decode to the values the // producer captured on cycle k, not to some other cycle's. Handing every slot // slot 0's array would stamp one cycle's data with every slot's timestamp — // the same samples drawn repeatedly at advancing times, with the cycles they // displaced missing entirely. func TestParseDataAccumulateGivesEachSlotItsOwnArray(t *testing.T) { sigs := []SignalInfo{ {Name: "Time", TypeCode: 9, NumRows: 1, NumCols: 1, QuantType: QuantNone}, {Name: "Wave", TypeCode: 9, NumRows: 4, NumCols: 1, QuantType: QuantNone}, } // Three RT cycles. "Wave" carries a different ramp each cycle so a // mix-up is unambiguous. timeSlots := [][]float64{{10}, {20}, {30}} waveSlots := [][]float64{ {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, } payload := buildAccumulatePayload(777, [][][]float64{timeSlots, waveSlots}) samples, err := ParseData(payload, sigs, PublishModeAccumulate, time.Now()) if err != nil { t.Fatalf("ParseData: %v", err) } if len(samples) != 3 { t.Fatalf("expected 3 slots, got %d", len(samples)) } for k, s := range samples { got := s.Values["Wave"] want := waveSlots[k] if len(got) != len(want) { t.Fatalf("slot %d: Wave has %d elements, want %d", k, len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Fatalf("slot %d: Wave = %v, want %v (slot %d's data has been "+ "served for this slot's timestamp)", k, got, want, indexOfSlot(waveSlots, got)) } } if tv := s.Values["Time"]; len(tv) != 1 || tv[0] != timeSlots[k][0] { t.Fatalf("slot %d: Time = %v, want %v", k, tv, timeSlots[k]) } } } // TestParseDataAccumulateConsumesTheWholeArrayBlock catches the same defect // from the other side: a signal following an array must be read at the right // offset. Under-reading the array block slides every later signal backwards // into the array's tail, which decodes as plausible-looking but wrong values // rather than as an error. func TestParseDataAccumulateConsumesTheWholeArrayBlock(t *testing.T) { sigs := []SignalInfo{ {Name: "Wave", TypeCode: 9, NumRows: 4, NumCols: 1, QuantType: QuantNone}, {Name: "Tail", TypeCode: 9, NumRows: 1, NumCols: 1, QuantType: QuantNone}, } waveSlots := [][]float64{ {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, } tailSlots := [][]float64{{100}, {200}, {300}} payload := buildAccumulatePayload(0, [][][]float64{waveSlots, tailSlots}) samples, err := ParseData(payload, sigs, PublishModeAccumulate, time.Now()) if err != nil { t.Fatalf("ParseData: %v", err) } if len(samples) != 3 { t.Fatalf("expected 3 slots, got %d", len(samples)) } for k, s := range samples { tv := s.Values["Tail"] if len(tv) != 1 || tv[0] != tailSlots[k][0] { t.Fatalf("slot %d: Tail = %v, want %v — the array block before it "+ "was not fully consumed", k, tv, tailSlots[k]) } } } // indexOfSlot reports which slot's data a decoded array actually matches, so a // failure message can name the culprit instead of just showing numbers. func indexOfSlot(slots [][]float64, got []float64) int { for k, want := range slots { if len(want) != len(got) { continue } same := true for i := range want { if want[i] != got[i] { same = false break } } if same { return k } } return -1 }