388 lines
11 KiB
Go
388 lines
11 KiB
Go
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, 0x00, 0x00, 0x03} // 2-byte RISC pad + enum=3
|
|
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 12 per caProto.h struct mon_info.
|
|
mask := binary.BigEndian.Uint16(b[12:])
|
|
if mask != proto.DBEDefault {
|
|
t.Errorf("mask = 0x%02X, want 0x%02X", mask, proto.DBEDefault)
|
|
}
|
|
// m_pad at [14:16] must be zero.
|
|
if pad := binary.BigEndian.Uint16(b[14:]); pad != 0 {
|
|
t.Errorf("m_pad = %d, want 0", pad)
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// 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)
|
|
}
|
|
}
|