package proto_test import ( "bytes" "encoding/binary" "math" "testing" "time" "github.com/uopi/goca/proto" ) // -------------------------------------------------------------------------- // // Header round-trip // // -------------------------------------------------------------------------- // func TestHeaderRoundTrip(t *testing.T) { h := proto.Header{ Command: proto.CmdCreateChan, PayloadSize: 16, DataType: proto.DBFDouble, DataCount: 1, Parameter1: 0xDEAD, Parameter2: 0xBEEF, } wire := h.Bytes() if len(wire) != proto.HeaderSize { t.Fatalf("Bytes() len = %d, want %d", len(wire), proto.HeaderSize) } got, n, err := proto.DecodeHeader(bytes.NewReader(wire)) if err != nil { t.Fatalf("DecodeHeader: %v", err) } if n != proto.HeaderSize { t.Errorf("bytes consumed = %d, want %d", n, proto.HeaderSize) } if got.Command != h.Command { t.Errorf("Command = %d, want %d", got.Command, h.Command) } if got.DataType != h.DataType { t.Errorf("DataType = %d, want %d", got.DataType, h.DataType) } if got.Parameter1 != h.Parameter1 { t.Errorf("Parameter1 = %d, want %d", got.Parameter1, h.Parameter1) } if got.Parameter2 != h.Parameter2 { t.Errorf("Parameter2 = %d, want %d", got.Parameter2, h.Parameter2) } } func TestBuildMessage(t *testing.T) { payload := []byte("hello\x00\x00\x00") // 8 bytes (padded) h := proto.Header{Command: proto.CmdHostName, PayloadSize: 0} msg := proto.BuildMessage(h, payload) if len(msg) != proto.HeaderSize+len(payload) { t.Fatalf("len(msg) = %d, want %d", len(msg), proto.HeaderSize+len(payload)) } // PayloadSize in wire should equal len(payload). wireSize := binary.BigEndian.Uint16(msg[2:4]) if int(wireSize) != len(payload) { t.Errorf("wire PayloadSize = %d, want %d", wireSize, len(payload)) } } func TestPadTo8(t *testing.T) { cases := [][2]int{{0, 0}, {1, 8}, {7, 8}, {8, 8}, {9, 16}, {16, 16}, {17, 24}} for _, c := range cases { if got := proto.PadTo8(c[0]); got != c[1] { t.Errorf("PadTo8(%d) = %d, want %d", c[0], got, c[1]) } } } func TestBuildStringPayload(t *testing.T) { p := proto.BuildStringPayload("TEST") if len(p)%8 != 0 { t.Errorf("payload length %d not padded to 8", len(p)) } if p[4] != 0 { t.Errorf("expected null terminator at index 4, got %d", p[4]) } } // -------------------------------------------------------------------------- // // DBR_TIME_* decoding // // -------------------------------------------------------------------------- // // buildTimeHeader builds the 12-byte common DBR_TIME header. func buildTimeHeader(status, severity int16, sec, nsec uint32) []byte { b := make([]byte, 12) binary.BigEndian.PutUint16(b[0:], uint16(status)) binary.BigEndian.PutUint16(b[2:], uint16(severity)) binary.BigEndian.PutUint32(b[4:], sec) binary.BigEndian.PutUint32(b[8:], nsec) return b } func TestDecodeTimeDouble(t *testing.T) { const sec = 1_000_000 const nsec = 500_000_000 const want = 3.14159 hdr := buildTimeHeader(0, 0, sec, nsec) pad := make([]byte, 4) // RISC pad val := make([]byte, 8) binary.BigEndian.PutUint64(val, math.Float64bits(want)) payload := append(append(hdr, pad...), val...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } if math.Abs(tv.Double-want) > 1e-10 { t.Errorf("Double = %g, want %g", tv.Double, want) } // EPICS epoch offset: 1990-01-01 00:00:00 UTC = Unix 631152000. wantUnix := int64(sec) + 631152000 if tv.Timestamp.Unix() != wantUnix { t.Errorf("Timestamp.Unix() = %d, want %d", tv.Timestamp.Unix(), wantUnix) } if tv.Timestamp.Nanosecond() != int(nsec) { t.Errorf("Timestamp.Nanosecond() = %d, want %d", tv.Timestamp.Nanosecond(), nsec) } } func TestDecodeTimeLong(t *testing.T) { hdr := buildTimeHeader(0, 1, 0, 0) val := make([]byte, 4) binary.BigEndian.PutUint32(val, 0xFFFFFFD6) // -42 as two's complement payload := append(hdr, val...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeLong, 1, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } if tv.Long != -42 { t.Errorf("Long = %d, want -42", tv.Long) } if tv.Severity != proto.SeverityMinor { t.Errorf("Severity = %v, want Minor", tv.Severity) } } func TestDecodeTimeString(t *testing.T) { hdr := buildTimeHeader(0, 0, 0, 0) str := make([]byte, 40) copy(str, "hello") payload := append(hdr, str...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeString, 1, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } if tv.Str != "hello" { t.Errorf("Str = %q, want %q", tv.Str, "hello") } } func TestDecodeTimeEnum(t *testing.T) { hdr := buildTimeHeader(0, 0, 0, 0) val := []byte{0x00, 0x03, 0x00, 0x00} // enum=3 + 2-byte pad payload := append(hdr, val...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeEnum, 1, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } if tv.Enum != 3 { t.Errorf("Enum = %d, want 3", tv.Enum) } } func TestDecodeTimeWaveform(t *testing.T) { hdr := buildTimeHeader(0, 0, 0, 0) pad := make([]byte, 4) vals := []float64{1.0, 2.5, -0.5} valbytes := make([]byte, len(vals)*8) for i, v := range vals { binary.BigEndian.PutUint64(valbytes[i*8:], math.Float64bits(v)) } payload := append(append(hdr, pad...), valbytes...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 3, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } if len(tv.Doubles) != 3 { t.Fatalf("len(Doubles) = %d, want 3", len(tv.Doubles)) } for i, want := range vals { if math.Abs(tv.Doubles[i]-want) > 1e-12 { t.Errorf("Doubles[%d] = %g, want %g", i, tv.Doubles[i], want) } } } func TestDecodeTimeTooShort(t *testing.T) { _, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, []byte{0, 1, 2}) if ok { t.Error("expected false for short payload") } } // -------------------------------------------------------------------------- // // DBR_CTRL_DOUBLE decoding // // -------------------------------------------------------------------------- // func TestDecodeCtrlDouble(t *testing.T) { p := make([]byte, 88) // status=0, severity=0, precision=3, pad=0, units="mA\0..." binary.BigEndian.PutUint16(p[4:], 3) // precision copy(p[8:], "mA") f64 := func(off int, v float64) { binary.BigEndian.PutUint64(p[off:], math.Float64bits(v)) } f64(16, 100.0) // upper_disp f64(24, 0.0) // lower_disp f64(32, 90.0) // upper_alarm f64(40, 80.0) // upper_warn f64(48, 20.0) // lower_warn f64(56, 10.0) // lower_alarm f64(64, 95.0) // upper_ctrl f64(72, 5.0) // lower_ctrl f64(80, 55.5) // value cd, ok := proto.DecodeCtrlDouble(p) if !ok { t.Fatal("DecodeCtrlDouble returned false") } if cd.Units != "mA" { t.Errorf("Units = %q, want %q", cd.Units, "mA") } if cd.Precision != 3 { t.Errorf("Precision = %d, want 3", cd.Precision) } if math.Abs(cd.Value-55.5) > 1e-10 { t.Errorf("Value = %g, want 55.5", cd.Value) } if math.Abs(cd.UpperDispLimit-100.0) > 1e-10 { t.Errorf("UpperDispLimit = %g, want 100.0", cd.UpperDispLimit) } } func TestDecodeCtrlEnum(t *testing.T) { p := make([]byte, 424) binary.BigEndian.PutUint16(p[4:], 3) // no_str = 3 strs := []string{"OFF", "ON", "FAULT"} for i, s := range strs { copy(p[6+i*26:], s) } binary.BigEndian.PutUint16(p[422:], 1) // value = 1 ce, ok := proto.DecodeCtrlEnum(p) if !ok { t.Fatal("DecodeCtrlEnum returned false") } if len(ce.Strings) != 3 { t.Fatalf("len(Strings) = %d, want 3", len(ce.Strings)) } if ce.Strings[2] != "FAULT" { t.Errorf("Strings[2] = %q, want FAULT", ce.Strings[2]) } if ce.Value != 1 { t.Errorf("Value = %d, want 1", ce.Value) } } // -------------------------------------------------------------------------- // // Put payload encoders // // -------------------------------------------------------------------------- // func TestEncodeDouble(t *testing.T) { b := proto.EncodeDouble(3.14) if len(b) != 8 { t.Fatalf("len = %d, want 8", len(b)) } got := math.Float64frombits(binary.BigEndian.Uint64(b)) if math.Abs(got-3.14) > 1e-15 { t.Errorf("decoded = %g, want 3.14", got) } } func TestEncodeLong(t *testing.T) { b := proto.EncodeLong(-1) if len(b) != 8 { t.Fatalf("len = %d, want 8", len(b)) } got := int32(binary.BigEndian.Uint32(b)) if got != -1 { t.Errorf("decoded = %d, want -1", got) } } func TestEncodeString(t *testing.T) { b := proto.EncodeString("hello") if len(b)%8 != 0 { t.Errorf("len %d not padded to 8", len(b)) } if string(b[:5]) != "hello" { t.Errorf("content = %q, want %q", b[:5], "hello") } } func TestEncodeEventMask(t *testing.T) { b := proto.EncodeEventMask(proto.DBEDefault) if len(b) != 16 { t.Fatalf("len = %d, want 16", len(b)) } // m_mask is at offset 14 (after m_lval[0:4], p_delta[4:8], p_final[8:12], p_count[12:14]). mask := binary.BigEndian.Uint16(b[14:]) if mask != proto.DBEDefault { t.Errorf("mask = 0x%02X, want 0x%02X", mask, proto.DBEDefault) } // p_count at [12:14] must be zero. if pc := binary.BigEndian.Uint16(b[12:]); pc != 0 { t.Errorf("p_count = %d, want 0", pc) } } // -------------------------------------------------------------------------- // // NativeTimeType / NativeCtrlType // // -------------------------------------------------------------------------- // func TestNativeTimeType(t *testing.T) { cases := []struct { dbf int n int want uint16 }{ {proto.DBFDouble, 1, proto.DBRTimeDouble}, {proto.DBFFloat, 1, proto.DBRTimeFloat}, {proto.DBFLong, 1, proto.DBRTimeLong}, {proto.DBFShort, 1, proto.DBRTimeShort}, {proto.DBFEnum, 1, proto.DBRTimeEnum}, {proto.DBFString, 1, proto.DBRTimeString}, {proto.DBFChar, 1, proto.DBRTimeChar}, {proto.DBFChar, 10, proto.DBRTimeString}, // char waveform → string } for _, c := range cases { got := proto.NativeTimeType(c.dbf, c.n) if got != c.want { t.Errorf("NativeTimeType(%d,%d) = %d, want %d", c.dbf, c.n, got, c.want) } } } // -------------------------------------------------------------------------- // // AlarmSeverity.String // // -------------------------------------------------------------------------- // func TestAlarmSeverityString(t *testing.T) { cases := []struct { s proto.AlarmSeverity want string }{ {proto.SeverityNone, "NO_ALARM"}, {proto.SeverityMinor, "MINOR"}, {proto.SeverityMajor, "MAJOR"}, {proto.SeverityInvalid, "INVALID"}, } for _, c := range cases { if got := c.s.String(); got != c.want { t.Errorf("Severity(%d).String() = %q, want %q", c.s, got, c.want) } } } // -------------------------------------------------------------------------- // // Timestamp sanity check // // -------------------------------------------------------------------------- // func TestEPICSEpoch(t *testing.T) { // secPastEpoch=0 should decode to 1990-01-01 00:00:00 UTC. hdr := buildTimeHeader(0, 0, 0, 0) val := make([]byte, 8) payload := append(append(hdr, make([]byte, 4)...), val...) tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload) if !ok { t.Fatal("DecodeTimeValue returned false") } want := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) if !tv.Timestamp.Equal(want) { t.Errorf("timestamp = %v, want %v", tv.Timestamp, want) } }