Testing
This commit is contained in:
+2
-2
@@ -264,8 +264,8 @@ type CtrlInfo struct {
|
||||
Access uint32 // proto.AccessRead | proto.AccessWrite bitmask
|
||||
|
||||
Double *proto.CtrlDouble // non-nil for DBFDouble / DBFFloat
|
||||
Long *proto.CtrlLong // non-nil for DBFLong / DBFShort / DBFChar
|
||||
Enum *proto.CtrlEnum // non-nil for DBFEnum
|
||||
Long *proto.CtrlLong // non-nil for DBFLong / DBFShort / DBFChar
|
||||
Enum *proto.CtrlEnum // non-nil for DBFEnum
|
||||
Str *proto.CtrlString // non-nil for DBFString
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/uopi/goca"
|
||||
"github.com/uopi/goca/testca"
|
||||
"github.com/uopi/goca/proto"
|
||||
"github.com/uopi/goca/testca"
|
||||
)
|
||||
|
||||
// newTestClient creates a Client pointing only at the fake server's addresses.
|
||||
|
||||
+10
-10
@@ -57,15 +57,15 @@ type chanState struct {
|
||||
cid uint32
|
||||
pvName string
|
||||
|
||||
mu sync.RWMutex
|
||||
sid uint32 // server-assigned channel ID (0 until CREATE_CHAN reply)
|
||||
dbfType int // native DBF field type
|
||||
count uint32 // element count
|
||||
access uint32 // AccessRead | AccessWrite bitmask
|
||||
gotChan bool // CREATE_CHAN reply received for current connection
|
||||
gotAccess bool // ACCESS_RIGHTS received for current connection
|
||||
readyC chan struct{} // closed once both CREATE_CHAN and ACCESS_RIGHTS received; replaced on reconnect
|
||||
monitors []*monState // active subscriptions
|
||||
mu sync.RWMutex
|
||||
sid uint32 // server-assigned channel ID (0 until CREATE_CHAN reply)
|
||||
dbfType int // native DBF field type
|
||||
count uint32 // element count
|
||||
access uint32 // AccessRead | AccessWrite bitmask
|
||||
gotChan bool // CREATE_CHAN reply received for current connection
|
||||
gotAccess bool // ACCESS_RIGHTS received for current connection
|
||||
readyC chan struct{} // closed once both CREATE_CHAN and ACCESS_RIGHTS received; replaced on reconnect
|
||||
monitors []*monState // active subscriptions
|
||||
}
|
||||
|
||||
func newChanState(cid uint32, pvName string) *chanState {
|
||||
@@ -151,7 +151,7 @@ type circuit struct {
|
||||
bySubID map[uint32]*monState // subID → monState (fast event dispatch)
|
||||
byIOID map[uint32]chan reply // ioid → GET/PUT callback (circuit-wide)
|
||||
|
||||
writeQ chan []byte // serialised outbound message queue
|
||||
writeQ chan []byte // serialised outbound message queue
|
||||
seq atomic.Uint32 // shared ID sequence (cid, ioid, subID)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package ca
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/uopi/goca/proto"
|
||||
)
|
||||
|
||||
// ---- encodePut: every DBF branch + error paths -----------------------
|
||||
|
||||
func TestEncodePut(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
dbf int
|
||||
v any
|
||||
wantDBR uint16
|
||||
}{
|
||||
{"double", proto.DBFDouble, 1.5, proto.DBRDouble},
|
||||
{"float", proto.DBFFloat, float32(2.0), proto.DBRDouble},
|
||||
{"long", proto.DBFLong, int(3), proto.DBRLong},
|
||||
{"short", proto.DBFShort, int16(4), proto.DBRShort},
|
||||
{"char", proto.DBFChar, int(5), proto.DBRShort},
|
||||
{"enum", proto.DBFEnum, int(1), proto.DBRShort},
|
||||
{"string", proto.DBFString, "hi", proto.DBRString},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
dbr, payload, err := encodePut(c.dbf, c.v)
|
||||
if err != nil {
|
||||
t.Fatalf("encodePut: %v", err)
|
||||
}
|
||||
if dbr != c.wantDBR {
|
||||
t.Errorf("dbr = %d, want %d", dbr, c.wantDBR)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
t.Error("empty payload")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Coercion failures propagate.
|
||||
if _, _, err := encodePut(proto.DBFDouble, "nope"); err == nil {
|
||||
t.Error("double from string: want error")
|
||||
}
|
||||
if _, _, err := encodePut(proto.DBFLong, "nope"); err == nil {
|
||||
t.Error("long from string: want error")
|
||||
}
|
||||
if _, _, err := encodePut(proto.DBFShort, "nope"); err == nil {
|
||||
t.Error("short from string: want error")
|
||||
}
|
||||
// Unsupported field type.
|
||||
if _, _, err := encodePut(9999, 1); err == nil {
|
||||
t.Error("unsupported DBF: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- NewClient: error path + default name/host -----------------------
|
||||
|
||||
func TestNewClientNoAddrs(t *testing.T) {
|
||||
if _, err := NewClient(context.Background(), Config{AutoAddrList: false}); err == nil {
|
||||
t.Error("NewClient with no addresses: want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientDefaultsNameHost(t *testing.T) {
|
||||
// AddrList present but ClientName/HostName empty → default branches run.
|
||||
cli, err := NewClient(context.Background(), Config{
|
||||
AddrList: []string{"127.0.0.1:5064"},
|
||||
AutoAddrList: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
if cli.cfg.ClientName == "" {
|
||||
t.Error("ClientName should be defaulted")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- EncodeSearchReply with an explicit server IP --------------------
|
||||
|
||||
func TestEncodeSearchReplyExplicitIP(t *testing.T) {
|
||||
pkt := EncodeSearchReply(7, net.ParseIP("10.1.2.3"), 5064)
|
||||
h, _, err := proto.DecodeHeader(newBytesReader(pkt))
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeHeader: %v", err)
|
||||
}
|
||||
if h.Command != proto.CmdSearch {
|
||||
t.Errorf("command = %d, want CmdSearch", h.Command)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- parseReply robustness -------------------------------------------
|
||||
|
||||
func TestParseReplyTruncated(t *testing.T) {
|
||||
se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
|
||||
src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
|
||||
// Must not panic on a too-short datagram.
|
||||
se.parseReply([]byte{0x00, 0x01}, src)
|
||||
}
|
||||
|
||||
func TestParseReplyUnknownSearchID(t *testing.T) {
|
||||
se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
|
||||
src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
|
||||
// Valid reply but no waiter is registered for this search ID → dropped.
|
||||
se.parseReply(EncodeSearchReply(12345, nil, 5064), src)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package ca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestGetCtrlNotFound covers the resolve-failure branch of GetCtrl.
|
||||
func TestGetCtrlNotFound(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if _, err := cli.GetCtrl(ctx, "NO:SUCH:PV"); err == nil {
|
||||
t.Fatal("GetCtrl on missing PV: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPutNotFound covers the resolve-failure branch of Put.
|
||||
func TestPutNotFound(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := cli.Put(ctx, "NO:SUCH:PV", 1.0); err == nil {
|
||||
t.Fatal("Put on missing PV: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPutUncoercibleValue covers the encodePut-failure branch of Put: the PV
|
||||
// resolves but the supplied value cannot be coerced to the native type.
|
||||
func TestPutUncoercibleValue(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := cli.Put(ctx, "TEST:DOUBLE", []int{1, 2, 3}); err == nil {
|
||||
t.Fatal("Put with uncoercible value: want error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package ca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/goca/proto"
|
||||
"github.com/uopi/goca/testca"
|
||||
)
|
||||
|
||||
// TestGetDisconnectMidRequest covers the "circuit disconnected during GET" path
|
||||
// in conn.go: the server drops the connection while a READ_NOTIFY is in flight,
|
||||
// so the in-flight reply channel is closed by the reconnect loop and the waiter
|
||||
// unblocks with ok=false.
|
||||
func TestGetDisconnectMidRequest(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First GET establishes the channel/circuit successfully.
|
||||
if _, err := cli.Get(ctx, "TEST:DOUBLE"); err != nil {
|
||||
t.Fatalf("initial Get: %v", err)
|
||||
}
|
||||
|
||||
// Arm a mid-request disconnect for the next READ_NOTIFY.
|
||||
srv.SetGetFault(testca.GetFaultDisconnect)
|
||||
|
||||
if _, err := cli.Get(ctx, "TEST:DOUBLE"); err == nil {
|
||||
t.Fatal("Get with mid-request disconnect: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCorruptReply covers the "failed to decode GET reply" path: the server
|
||||
// answers READ_NOTIFY with a payload too short for DecodeTimeValue.
|
||||
func TestGetCorruptReply(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Establish the channel first so resolution succeeds.
|
||||
if _, err := cli.Get(ctx, "TEST:DOUBLE"); err != nil {
|
||||
t.Fatalf("initial Get: %v", err)
|
||||
}
|
||||
|
||||
srv.SetGetFault(testca.GetFaultCorrupt)
|
||||
|
||||
if _, err := cli.Get(ctx, "TEST:DOUBLE"); err == nil {
|
||||
t.Fatal("Get with corrupt reply: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerDisconnectReconnects covers the CmdServerDisc dispatch branch and the
|
||||
// reconnect loop: after an orderly server disconnect the client transparently
|
||||
// reconnects, re-creates its channel, re-subscribes the monitor, and continues to
|
||||
// receive updates.
|
||||
func TestServerDisconnectReconnects(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ch := make(chan proto.TimeValue, 16)
|
||||
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
|
||||
if err != nil {
|
||||
t.Fatalf("Subscribe: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
// Drain the initial value.
|
||||
select {
|
||||
case <-ch:
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for initial value")
|
||||
}
|
||||
|
||||
// Force an orderly disconnect; the client must reconnect on its own.
|
||||
srv.Disconnect()
|
||||
|
||||
// After the reconnect settles, push a fresh value and expect to see it.
|
||||
// The reconnect back-off is ~1s, so poll generously and re-arm the value
|
||||
// until it is observed (the re-subscribe also delivers an initial value).
|
||||
deadline := time.After(8 * time.Second)
|
||||
tick := time.NewTicker(300 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case tv := <-ch:
|
||||
if math.Abs(tv.Double-88.8) < 1e-6 {
|
||||
return // reconnect succeeded and the update flowed through
|
||||
}
|
||||
case <-tick.C:
|
||||
_ = srv.SetValue("TEST:DOUBLE", 88.8)
|
||||
case <-deadline:
|
||||
t.Fatal("timeout waiting for post-reconnect update")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package ca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestGetCtrl exercises GetCtrl across the native DBF types served by the mock,
|
||||
// covering NativeCtrlType dispatch and the DBR_CTRL_* decoders.
|
||||
func TestGetCtrl(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
cli := newTestClient(t, srv)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
t.Run("double", func(t *testing.T) {
|
||||
ci, err := cli.GetCtrl(ctx, "TEST:DOUBLE")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCtrl: %v", err)
|
||||
}
|
||||
if ci.Double == nil {
|
||||
t.Fatal("expected Double ctrl info")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("long", func(t *testing.T) {
|
||||
ci, err := cli.GetCtrl(ctx, "TEST:LONG")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCtrl: %v", err)
|
||||
}
|
||||
if ci.Long == nil {
|
||||
t.Fatal("expected Long ctrl info")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enum", func(t *testing.T) {
|
||||
ci, err := cli.GetCtrl(ctx, "TEST:ENUM")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCtrl: %v", err)
|
||||
}
|
||||
if ci.Enum == nil {
|
||||
t.Fatal("expected Enum ctrl info")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
ci, err := cli.GetCtrl(ctx, "TEST:STRING")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCtrl: %v", err)
|
||||
}
|
||||
if ci.Str == nil {
|
||||
t.Fatal("expected Str ctrl info")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package ca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/uopi/goca/proto"
|
||||
)
|
||||
|
||||
// ---- value coercion helpers ------------------------------------------
|
||||
|
||||
func TestToFloat64(t *testing.T) {
|
||||
cases := []struct {
|
||||
in any
|
||||
want float64
|
||||
}{
|
||||
{float64(1.5), 1.5},
|
||||
{float32(2.5), 2.5},
|
||||
{int(3), 3},
|
||||
{int64(4), 4},
|
||||
{int32(5), 5},
|
||||
{int16(6), 6},
|
||||
{uint64(7), 7},
|
||||
{uint32(8), 8},
|
||||
{true, 1},
|
||||
{false, 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := toFloat64(c.in)
|
||||
if err != nil || got != c.want {
|
||||
t.Errorf("toFloat64(%v) = %v, %v; want %v", c.in, got, err, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := toFloat64("nope"); err == nil {
|
||||
t.Error("toFloat64(string): want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToInt32(t *testing.T) {
|
||||
cases := []struct {
|
||||
in any
|
||||
want int32
|
||||
}{
|
||||
{int(3), 3},
|
||||
{int64(4), 4},
|
||||
{int32(5), 5},
|
||||
{int16(6), 6},
|
||||
{float64(7.9), 7},
|
||||
{float32(8.9), 8},
|
||||
{true, 1},
|
||||
{false, 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := toInt32(c.in)
|
||||
if err != nil || got != c.want {
|
||||
t.Errorf("toInt32(%v) = %v, %v; want %v", c.in, got, err, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := toInt32("nope"); err == nil {
|
||||
t.Error("toInt32(string): want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
if s, _ := toString("hi"); s != "hi" {
|
||||
t.Errorf("toString(string) = %q", s)
|
||||
}
|
||||
if s, _ := toString([]byte("bytes")); s != "bytes" {
|
||||
t.Errorf("toString([]byte) = %q", s)
|
||||
}
|
||||
if s, _ := toString(42); s != "42" {
|
||||
t.Errorf("toString(int) = %q, want 42", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ConfigFromEnv ---------------------------------------------------
|
||||
|
||||
func TestConfigFromEnv(t *testing.T) {
|
||||
t.Setenv("EPICS_CA_ADDR_LIST", "1.2.3.4 5.6.7.8")
|
||||
t.Setenv("EPICS_CA_AUTO_ADDR_LIST", "no")
|
||||
cfg := ConfigFromEnv()
|
||||
if len(cfg.AddrList) != 2 || cfg.AddrList[0] != "1.2.3.4" {
|
||||
t.Errorf("AddrList = %v", cfg.AddrList)
|
||||
}
|
||||
if cfg.AutoAddrList {
|
||||
t.Error("AutoAddrList should be false when env = no")
|
||||
}
|
||||
if cfg.ClientName == "" {
|
||||
t.Error("ClientName should be populated")
|
||||
}
|
||||
|
||||
t.Setenv("EPICS_CA_ADDR_LIST", "")
|
||||
t.Setenv("EPICS_CA_AUTO_ADDR_LIST", "yes")
|
||||
cfg = ConfigFromEnv()
|
||||
if !cfg.AutoAddrList {
|
||||
t.Error("AutoAddrList should default to true")
|
||||
}
|
||||
if len(cfg.AddrList) != 0 {
|
||||
t.Errorf("AddrList should be empty, got %v", cfg.AddrList)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- address helpers -------------------------------------------------
|
||||
|
||||
func TestResolveAddrs(t *testing.T) {
|
||||
got := resolveAddrs([]string{"host", "host2:9999", ""}, 5064)
|
||||
if len(got) != 2 || got[0] != "host:5064" || got[1] != "host2:9999" {
|
||||
t.Errorf("resolveAddrs = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalBroadcastAddrs(t *testing.T) {
|
||||
// Should not panic; the result is environment-dependent.
|
||||
_ = localBroadcastAddrs(5064)
|
||||
}
|
||||
|
||||
// ---- isAllFF ---------------------------------------------------------
|
||||
|
||||
func TestIsAllFF(t *testing.T) {
|
||||
if !isAllFF([]byte{0xFF, 0xFF, 0xFF}) {
|
||||
t.Error("all-FF should be true")
|
||||
}
|
||||
if isAllFF([]byte{0xFF, 0x00}) {
|
||||
t.Error("mixed should be false")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- EncodedSearchPair -----------------------------------------------
|
||||
|
||||
func TestEncodedSearchPair(t *testing.T) {
|
||||
pkt := EncodedSearchPair("TEST:PV", 7)
|
||||
// First header must be a VERSION message.
|
||||
h, _, err := proto.DecodeHeader(newBytesReader(pkt))
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeHeader: %v", err)
|
||||
}
|
||||
if h.Command != proto.CmdVersion {
|
||||
t.Errorf("first command = %d, want CmdVersion", h.Command)
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -218,19 +218,19 @@ func DecodeTimeValue(dbrType uint16, count uint32, payload []byte) (TimeValue, b
|
||||
|
||||
// CtrlDouble is the decoded form of a DBR_CTRL_DOUBLE payload.
|
||||
type CtrlDouble struct {
|
||||
Status int16
|
||||
Severity AlarmSeverity
|
||||
Precision int16
|
||||
Units string
|
||||
UpperDispLimit float64
|
||||
LowerDispLimit float64
|
||||
UpperAlarmLimit float64
|
||||
UpperWarnLimit float64
|
||||
LowerWarnLimit float64
|
||||
LowerAlarmLimit float64
|
||||
UpperCtrlLimit float64
|
||||
LowerCtrlLimit float64
|
||||
Value float64
|
||||
Status int16
|
||||
Severity AlarmSeverity
|
||||
Precision int16
|
||||
Units string
|
||||
UpperDispLimit float64
|
||||
LowerDispLimit float64
|
||||
UpperAlarmLimit float64
|
||||
UpperWarnLimit float64
|
||||
LowerWarnLimit float64
|
||||
LowerAlarmLimit float64
|
||||
UpperCtrlLimit float64
|
||||
LowerCtrlLimit float64
|
||||
Value float64
|
||||
}
|
||||
|
||||
// DecodeCtrlDouble decodes a DBR_CTRL_DOUBLE payload (minimum 88 bytes).
|
||||
|
||||
@@ -31,8 +31,8 @@ const DefaultPort = 5064
|
||||
|
||||
// Search reply data_type values.
|
||||
const (
|
||||
SearchReply = 10 // DO_REPLY: ask server to respond
|
||||
SearchNoReply = 5 // DONT_REPLY: suppress response (used by repeater)
|
||||
SearchReply = 10 // DO_REPLY: ask server to respond
|
||||
SearchNoReply = 5 // DONT_REPLY: suppress response (used by repeater)
|
||||
)
|
||||
|
||||
// AccessRights bitmask values (parameter2 of CmdAccessRights message).
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package proto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/uopi/goca/proto"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// DBR_TIME_* variants not covered by proto_test.go //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestDecodeTimeFloat(t *testing.T) {
|
||||
hdr := buildTimeHeader(0, 0, 0, 0)
|
||||
val := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(val, math.Float32bits(2.5))
|
||||
tv, ok := proto.DecodeTimeValue(proto.DBRTimeFloat, 1, append(hdr, val...))
|
||||
if !ok || math.Abs(float64(tv.Float)-2.5) > 1e-6 {
|
||||
t.Errorf("float: ok=%v Float=%g", ok, tv.Float)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTimeShort(t *testing.T) {
|
||||
hdr := buildTimeHeader(0, 0, 0, 0)
|
||||
val := []byte{0x00, 0x00, 0xFF, 0xD6} // 2-byte pad + int16(-42)
|
||||
tv, ok := proto.DecodeTimeValue(proto.DBRTimeShort, 1, append(hdr, val...))
|
||||
if !ok || tv.Short != -42 {
|
||||
t.Errorf("short: ok=%v Short=%d", ok, tv.Short)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTimeChar(t *testing.T) {
|
||||
hdr := buildTimeHeader(0, 0, 0, 0)
|
||||
val := []byte{0x00, 0x00, 0x00, 0x41} // pad0 + pad1 + value 'A'
|
||||
tv, ok := proto.DecodeTimeValue(proto.DBRTimeChar, 1, append(hdr, val...))
|
||||
if !ok || tv.Char != 0x41 {
|
||||
t.Errorf("char: ok=%v Char=%d", ok, tv.Char)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTimeUnknownType(t *testing.T) {
|
||||
hdr := buildTimeHeader(0, 0, 0, 0)
|
||||
if _, ok := proto.DecodeTimeValue(9999, 1, append(hdr, make([]byte, 16)...)); ok {
|
||||
t.Error("unknown DBR type should return false")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// DBR_CTRL_LONG / DBR_CTRL_STRING //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestDecodeCtrlLong(t *testing.T) {
|
||||
p := make([]byte, 48)
|
||||
binary.BigEndian.PutUint16(p[2:], 2) // severity = Major
|
||||
copy(p[4:], "counts")
|
||||
i32 := func(off int, v int32) { binary.BigEndian.PutUint32(p[off:], uint32(v)) }
|
||||
i32(12, 1000) // upper_disp
|
||||
i32(16, -10) // lower_disp
|
||||
i32(44, 777) // value
|
||||
|
||||
cl, ok := proto.DecodeCtrlLong(p)
|
||||
if !ok {
|
||||
t.Fatal("DecodeCtrlLong returned false")
|
||||
}
|
||||
if cl.Units != "counts" || cl.Value != 777 || cl.UpperDispLimit != 1000 || cl.LowerDispLimit != -10 {
|
||||
t.Errorf("got %+v", cl)
|
||||
}
|
||||
if cl.Severity != proto.SeverityMajor {
|
||||
t.Errorf("severity = %v, want Major", cl.Severity)
|
||||
}
|
||||
if _, ok := proto.DecodeCtrlLong(make([]byte, 10)); ok {
|
||||
t.Error("short CtrlLong payload should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeCtrlString(t *testing.T) {
|
||||
p := make([]byte, 44)
|
||||
binary.BigEndian.PutUint16(p[2:], 1) // severity = Minor
|
||||
copy(p[4:], "READY")
|
||||
|
||||
cs, ok := proto.DecodeCtrlString(p)
|
||||
if !ok {
|
||||
t.Fatal("DecodeCtrlString returned false")
|
||||
}
|
||||
if cs.Value != "READY" || cs.Severity != proto.SeverityMinor {
|
||||
t.Errorf("got %+v", cs)
|
||||
}
|
||||
if _, ok := proto.DecodeCtrlString(make([]byte, 10)); ok {
|
||||
t.Error("short CtrlString payload should return false")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// EncodeShort //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestEncodeShort(t *testing.T) {
|
||||
b := proto.EncodeShort(-7)
|
||||
if len(b) != 8 {
|
||||
t.Fatalf("len = %d, want 8", len(b))
|
||||
}
|
||||
if got := int16(binary.BigEndian.Uint16(b)); got != -7 {
|
||||
t.Errorf("decoded = %d, want -7", got)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// NativeCtrlType //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestNativeCtrlType(t *testing.T) {
|
||||
cases := []struct {
|
||||
dbf int
|
||||
want uint16
|
||||
}{
|
||||
{proto.DBFDouble, proto.DBRCtrlDouble},
|
||||
{proto.DBFFloat, proto.DBRCtrlDouble},
|
||||
{proto.DBFLong, proto.DBRCtrlLong},
|
||||
{proto.DBFShort, proto.DBRCtrlLong},
|
||||
{proto.DBFChar, proto.DBRCtrlLong},
|
||||
{proto.DBFEnum, proto.DBRCtrlEnum},
|
||||
{proto.DBFString, proto.DBRCtrlString},
|
||||
{9999, proto.DBRCtrlDouble}, // default
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := proto.NativeCtrlType(c.dbf); got != c.want {
|
||||
t.Errorf("NativeCtrlType(%d) = %d, want %d", c.dbf, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// DecodeHeader: extended encoding + error path //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestDecodeHeaderExtended(t *testing.T) {
|
||||
raw := make([]byte, 24)
|
||||
binary.BigEndian.PutUint16(raw[0:], proto.CmdReadNotify)
|
||||
binary.BigEndian.PutUint16(raw[2:], 0xFFFF) // extended marker
|
||||
binary.BigEndian.PutUint16(raw[6:], 0x0000)
|
||||
binary.BigEndian.PutUint32(raw[16:], 70000) // real payload size
|
||||
binary.BigEndian.PutUint32(raw[20:], 5) // real count
|
||||
|
||||
h, n, err := proto.DecodeHeader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 24 {
|
||||
t.Errorf("bytes read = %d, want 24", n)
|
||||
}
|
||||
if h.PayloadSize != 70000 || h.DataCount != 5 {
|
||||
t.Errorf("extended header = %+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeHeaderTruncated(t *testing.T) {
|
||||
if _, _, err := proto.DecodeHeader(bytes.NewReader([]byte{0x00, 0x01})); err == nil {
|
||||
t.Error("truncated header: want error")
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func TestDecodeTimeDouble(t *testing.T) {
|
||||
const want = 3.14159
|
||||
|
||||
hdr := buildTimeHeader(0, 0, sec, nsec)
|
||||
pad := make([]byte, 4) // RISC pad
|
||||
pad := make([]byte, 4) // RISC pad
|
||||
val := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(val, math.Float64bits(want))
|
||||
payload := append(append(hdr, pad...), val...)
|
||||
|
||||
+68
-9
@@ -29,13 +29,21 @@ type Server struct {
|
||||
tcpLn net.Listener
|
||||
udpCn *net.UDPConn
|
||||
|
||||
mu sync.RWMutex
|
||||
pvs map[string]*serverPV
|
||||
subs map[uint32]*serverSub // subID → sub
|
||||
mu sync.RWMutex
|
||||
pvs map[string]*serverPV
|
||||
subs map[uint32]*serverSub // subID → sub
|
||||
getFault int // fault to inject on the next READ_NOTIFY
|
||||
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Fault modes for SetGetFault, used to exercise the client's error paths.
|
||||
const (
|
||||
GetFaultNone = iota // serve replies normally
|
||||
GetFaultDisconnect // drop the connection mid-request
|
||||
GetFaultCorrupt // reply with an undecodable (too-short) payload
|
||||
)
|
||||
|
||||
type serverPV struct {
|
||||
spec PVSpec
|
||||
mu sync.RWMutex
|
||||
@@ -43,11 +51,11 @@ type serverPV struct {
|
||||
}
|
||||
|
||||
type serverSub struct {
|
||||
pvName string
|
||||
subID uint32
|
||||
cid uint32
|
||||
sid uint32
|
||||
conn net.Conn
|
||||
pvName string
|
||||
subID uint32
|
||||
cid uint32
|
||||
sid uint32
|
||||
conn net.Conn
|
||||
dbrType uint16
|
||||
count uint32
|
||||
}
|
||||
@@ -93,6 +101,29 @@ func (s *Server) Close() {
|
||||
s.udpCn.Close()
|
||||
}
|
||||
|
||||
// SetGetFault arms a fault to be injected on the next READ_NOTIFY request.
|
||||
// It is reset to GetFaultNone after a single request is faulted.
|
||||
func (s *Server) SetGetFault(mode int) {
|
||||
s.mu.Lock()
|
||||
s.getFault = mode
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Disconnect pushes a SERVER_DISCONN message to every connected subscriber,
|
||||
// simulating an orderly server shutdown that forces clients to reconnect.
|
||||
func (s *Server) Disconnect() {
|
||||
msg := proto.BuildMessage(proto.Header{Command: proto.CmdServerDisc}, nil)
|
||||
seen := make(map[net.Conn]bool)
|
||||
s.mu.RLock()
|
||||
for _, sub := range s.subs {
|
||||
if sub.conn != nil && !seen[sub.conn] {
|
||||
seen[sub.conn] = true
|
||||
_, _ = sub.conn.Write(msg)
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
// SetValue updates a PV's value and pushes a monitor event to all subscribers.
|
||||
func (s *Server) SetValue(pvName string, val any) error {
|
||||
s.mu.RLock()
|
||||
@@ -350,6 +381,31 @@ func (s *Server) handleConn(conn net.Conn) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fault injection: consume a one-shot armed fault, if any.
|
||||
s.mu.Lock()
|
||||
fault := s.getFault
|
||||
s.getFault = GetFaultNone
|
||||
s.mu.Unlock()
|
||||
switch fault {
|
||||
case GetFaultDisconnect:
|
||||
// Drop the connection mid-request: the client's in-flight GET
|
||||
// unblocks with ok=false ("circuit disconnected during GET").
|
||||
conn.Close()
|
||||
return
|
||||
case GetFaultCorrupt:
|
||||
// Reply with a too-short payload so DecodeTimeValue fails
|
||||
// ("failed to decode GET reply").
|
||||
reply := proto.BuildMessage(proto.Header{
|
||||
Command: proto.CmdReadNotify,
|
||||
DataType: hdr.DataType,
|
||||
DataCount: hdr.DataCount,
|
||||
Parameter1: cid,
|
||||
Parameter2: ioid,
|
||||
}, []byte{0, 0, 0, 0})
|
||||
conn.Write(reply)
|
||||
continue
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
pv := s.pvs[pvName]
|
||||
s.mu.RUnlock()
|
||||
@@ -591,7 +647,10 @@ func nullStr(b []byte) string {
|
||||
// Minimal io.Reader for proto.DecodeHeader //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
type bytesReader struct{ b []byte; i int }
|
||||
type bytesReader struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
func newBytesReader(b []byte) *bytesReader { return &bytesReader{b: b} }
|
||||
|
||||
|
||||
+3
-3
@@ -224,10 +224,10 @@ func (cl *Client) sendSearchRequest(seq uint32, pvName string) {
|
||||
laddr := cl.udp.LocalAddr().(*net.UDPAddr)
|
||||
binary.Write(&buf, binary.LittleEndian, uint16(laddr.Port))
|
||||
|
||||
buf.WriteByte(1) // transportCount = 1
|
||||
buf.WriteByte(0x01) // transport[0]: TCP
|
||||
buf.WriteByte(1) // transportCount = 1
|
||||
buf.WriteByte(0x01) // transport[0]: TCP
|
||||
|
||||
binary.Write(&buf, binary.LittleEndian, uint16(1)) // pvCount = 1
|
||||
binary.Write(&buf, binary.LittleEndian, uint16(1)) // pvCount = 1
|
||||
binary.Write(&buf, binary.LittleEndian, uint32(seq)) // channelID (reuse seq)
|
||||
pvdata.WriteString(&buf, pvName)
|
||||
|
||||
|
||||
+6
-7
@@ -38,7 +38,7 @@ type serverChannel struct {
|
||||
|
||||
type monitorSub struct {
|
||||
ioid uint32
|
||||
sid uint32 // server channel ID — needed for pipeline ACKs and DESTROY
|
||||
sid uint32 // server channel ID — needed for pipeline ACKs and DESTROY
|
||||
cid uint32
|
||||
desc pvdata.FieldDesc // structure descriptor (received on INIT response)
|
||||
updates chan MonitorEvent
|
||||
@@ -56,10 +56,10 @@ type MonitorEvent struct {
|
||||
|
||||
// conn is a single PVA TCP connection to one server.
|
||||
type conn struct {
|
||||
nc net.Conn
|
||||
br *bufio.Reader // shared reader — used by handshake then readLoop
|
||||
bw *bufio.Writer
|
||||
mu sync.Mutex // protects writes + state maps
|
||||
nc net.Conn
|
||||
br *bufio.Reader // shared reader — used by handshake then readLoop
|
||||
bw *bufio.Writer
|
||||
mu sync.Mutex // protects writes + state maps
|
||||
|
||||
// auto-incrementing IDs
|
||||
nextCID atomic.Uint32
|
||||
@@ -68,7 +68,7 @@ type conn struct {
|
||||
// state
|
||||
chans map[uint32]*serverChannel // keyed by client CID
|
||||
byPV map[string]*serverChannel // keyed by PV name
|
||||
monitors map[uint32]*monitorSub // keyed by IOID
|
||||
monitors map[uint32]*monitorSub // keyed by IOID
|
||||
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -573,4 +573,3 @@ func asError(s Status) error {
|
||||
}
|
||||
return fmt.Errorf("%s", s.Error())
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -59,21 +59,21 @@ const (
|
||||
// ---- Control message sub-commands -------------------------------------
|
||||
|
||||
const (
|
||||
CtrlSetMarker byte = 0x00
|
||||
CtrlAckMarker byte = 0x01
|
||||
CtrlSetByteOrder byte = 0x02
|
||||
CtrlEchoRequest byte = 0x03
|
||||
CtrlEchoResponse byte = 0x04
|
||||
CtrlSetMarker byte = 0x00
|
||||
CtrlAckMarker byte = 0x01
|
||||
CtrlSetByteOrder byte = 0x02
|
||||
CtrlEchoRequest byte = 0x03
|
||||
CtrlEchoResponse byte = 0x04
|
||||
)
|
||||
|
||||
// ---- Status codes -----------------------------------------------------
|
||||
|
||||
const (
|
||||
StatusOK byte = 0xFF // special "OK" short encoding
|
||||
StatusOKFull byte = 0x00 // full OK message (type=OK, msg="")
|
||||
StatusWarn byte = 0x01
|
||||
StatusError byte = 0x02
|
||||
StatusFatal byte = 0x03
|
||||
StatusOK byte = 0xFF // special "OK" short encoding
|
||||
StatusOKFull byte = 0x00 // full OK message (type=OK, msg="")
|
||||
StatusWarn byte = 0x01
|
||||
StatusError byte = 0x02
|
||||
StatusFatal byte = 0x03
|
||||
)
|
||||
|
||||
// ---- Request sub-command bits -----------------------------------------
|
||||
@@ -90,11 +90,11 @@ const (
|
||||
|
||||
// PVAHeader is the 8-byte fixed header on every PVA message.
|
||||
//
|
||||
// byte 0 : magic 0xCA
|
||||
// byte 1 : protocol version (0x01)
|
||||
// byte 2 : flags (see flag* constants)
|
||||
// byte 3 : command code
|
||||
// bytes 4–7 : payload size (uint32, matches byte-order flag)
|
||||
// byte 0 : magic 0xCA
|
||||
// byte 1 : protocol version (0x01)
|
||||
// byte 2 : flags (see flag* constants)
|
||||
// byte 3 : command code
|
||||
// bytes 4–7 : payload size (uint32, matches byte-order flag)
|
||||
type PVAHeader struct {
|
||||
Version byte
|
||||
Flags byte
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package pva
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ---- Header codec -----------------------------------------------------
|
||||
|
||||
func TestHeaderRoundTrip(t *testing.T) {
|
||||
h := PVAHeader{Version: 1, Flags: flagApp, Command: CmdGet, Size: 1234}
|
||||
var buf bytes.Buffer
|
||||
if err := WriteHeader(&buf, h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ReadHeader(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Version != h.Version || got.Command != h.Command || got.Size != h.Size {
|
||||
t.Errorf("round-trip header: got %+v want %+v", got, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadHeaderBadMagic(t *testing.T) {
|
||||
raw := []byte{0xAB, 0x01, 0x00, CmdGet, 0, 0, 0, 0}
|
||||
if _, err := ReadHeader(bytes.NewReader(raw)); err == nil {
|
||||
t.Error("bad magic: want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadHeaderTruncated(t *testing.T) {
|
||||
if _, err := ReadHeader(bytes.NewReader([]byte{0xCA, 0x01})); err == nil {
|
||||
t.Error("truncated header: want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderBigEndian(t *testing.T) {
|
||||
// Hand-assemble a big-endian server header with size 0x00010203.
|
||||
raw := []byte{magic, 0x01, flagBigEndian | flagFromServer, CmdMonitor, 0x00, 0x01, 0x02, 0x03}
|
||||
h, err := ReadHeader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.isLittleEndian() {
|
||||
t.Error("expected big-endian header")
|
||||
}
|
||||
if !h.isFromServer() {
|
||||
t.Error("expected from-server bit")
|
||||
}
|
||||
if h.byteOrder() != binary.BigEndian {
|
||||
t.Error("byteOrder should be big-endian")
|
||||
}
|
||||
if h.Size != 0x00010203 {
|
||||
t.Errorf("size = %#x, want 0x00010203", h.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderPredicates(t *testing.T) {
|
||||
le := PVAHeader{Flags: flagApp}
|
||||
if !le.isLittleEndian() || le.isFromServer() || le.isControl() {
|
||||
t.Errorf("app/LE header predicates wrong: %+v", le)
|
||||
}
|
||||
ctrl := PVAHeader{Flags: flagControl}
|
||||
if !ctrl.isControl() {
|
||||
t.Error("control header should report isControl")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- BuildMessage -----------------------------------------------------
|
||||
|
||||
func TestBuildMessage(t *testing.T) {
|
||||
payload := []byte{1, 2, 3, 4, 5}
|
||||
// Pass the server flag too; BuildMessage must clear it (client direction).
|
||||
msg := BuildMessage(CmdCreateChannel, flagFromServer, payload)
|
||||
h, err := ReadHeader(bytes.NewReader(msg))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Command != CmdCreateChannel {
|
||||
t.Errorf("command = %#x, want %#x", h.Command, CmdCreateChannel)
|
||||
}
|
||||
if h.isFromServer() {
|
||||
t.Error("BuildMessage must clear the from-server flag")
|
||||
}
|
||||
if int(h.Size) != len(payload) {
|
||||
t.Errorf("size = %d, want %d", h.Size, len(payload))
|
||||
}
|
||||
if got := msg[headerSize:]; !bytes.Equal(got, payload) {
|
||||
t.Errorf("payload = %v, want %v", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Status decoding --------------------------------------------------
|
||||
|
||||
func TestReadStatusOKShort(t *testing.T) {
|
||||
s, err := ReadStatus(bytes.NewReader([]byte{StatusOK}), binary.LittleEndian)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.OK() || s.Error() != "" {
|
||||
t.Errorf("short OK status: got %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadStatusError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte(StatusError)
|
||||
buf.WriteByte(0x04) // compact size 4
|
||||
buf.WriteString("boom")
|
||||
buf.WriteByte(0x00) // empty stack
|
||||
s, err := ReadStatus(&buf, binary.LittleEndian)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.OK() {
|
||||
t.Error("error status should not be OK")
|
||||
}
|
||||
if s.Message != "boom" {
|
||||
t.Errorf("message = %q, want boom", s.Message)
|
||||
}
|
||||
if s.Error() == "" {
|
||||
t.Error("Error() should be non-empty for an error status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadStatusTruncated(t *testing.T) {
|
||||
if _, err := ReadStatus(bytes.NewReader(nil), binary.LittleEndian); err == nil {
|
||||
t.Error("empty status: want error")
|
||||
}
|
||||
// type byte present but the message string is missing.
|
||||
if _, err := ReadStatus(bytes.NewReader([]byte{StatusError}), binary.LittleEndian); err == nil {
|
||||
t.Error("status missing message: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- readPVAString ----------------------------------------------------
|
||||
|
||||
func TestReadPVAString(t *testing.T) {
|
||||
// Short form.
|
||||
short := append([]byte{0x03}, []byte("abc")...)
|
||||
s, err := readPVAString(bytes.NewReader(short), binary.LittleEndian)
|
||||
if err != nil || s != "abc" {
|
||||
t.Errorf("short string: got %q err %v", s, err)
|
||||
}
|
||||
// Empty.
|
||||
s, err = readPVAString(bytes.NewReader([]byte{0x00}), binary.LittleEndian)
|
||||
if err != nil || s != "" {
|
||||
t.Errorf("empty string: got %q err %v", s, err)
|
||||
}
|
||||
// Extended form: 0xFF + int32 length + bytes.
|
||||
var ext bytes.Buffer
|
||||
ext.WriteByte(0xFF)
|
||||
_ = binary.Write(&ext, binary.LittleEndian, int32(5))
|
||||
ext.WriteString("hello")
|
||||
s, err = readPVAString(&ext, binary.LittleEndian)
|
||||
if err != nil || s != "hello" {
|
||||
t.Errorf("extended string: got %q err %v", s, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- env --------------------------------------------------------------
|
||||
|
||||
func TestAddrsFromEnv(t *testing.T) {
|
||||
t.Setenv("EPICS_PVA_ADDR_LIST", "")
|
||||
if got := addrsFromEnv(); got != nil {
|
||||
t.Errorf("empty env: got %v, want nil", got)
|
||||
}
|
||||
t.Setenv("EPICS_PVA_ADDR_LIST", " 1.2.3.4 5.6.7.8 ")
|
||||
got := addrsFromEnv()
|
||||
if len(got) != 2 || got[0] != "1.2.3.4" || got[1] != "5.6.7.8" {
|
||||
t.Errorf("addr list: got %v", got)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package pvdata
|
||||
import "io"
|
||||
|
||||
// BitSet is a variable-length set of bit indices, encoded on the wire as:
|
||||
// compact-size(n_bytes) + n_bytes of little-endian bits.
|
||||
//
|
||||
// compact-size(n_bytes) + n_bytes of little-endian bits.
|
||||
//
|
||||
// Bit 0 of byte 0 is field index 0, bit 1 of byte 0 is field index 1, etc.
|
||||
// Used in pvData Monitor responses for "changed" and "overrun" masks.
|
||||
type BitSet struct {
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
package pvdata_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/uopi/gopva/pvdata"
|
||||
)
|
||||
|
||||
// ---- Scalar value codec: every type code -----------------------------
|
||||
|
||||
func TestScalarRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
tc byte
|
||||
v pvdata.Value
|
||||
}{
|
||||
{"bool-true", pvdata.TypeCodeBoolean, true},
|
||||
{"bool-false", pvdata.TypeCodeBoolean, false},
|
||||
{"byte", pvdata.TypeCodeByte, int8(-12)},
|
||||
{"short", pvdata.TypeCodeShort, int16(-3000)},
|
||||
{"int", pvdata.TypeCodeInt, int32(-100000)},
|
||||
{"long", pvdata.TypeCodeLong, int64(-1 << 40)},
|
||||
{"ubyte", pvdata.TypeCodeUByte, uint8(200)},
|
||||
{"ushort", pvdata.TypeCodeUShort, uint16(60000)},
|
||||
{"uint", pvdata.TypeCodeUInt, uint32(4000000000)},
|
||||
{"ulong", pvdata.TypeCodeULong, uint64(1 << 50)},
|
||||
{"float", pvdata.TypeCodeFloat, float32(1.5)},
|
||||
{"double", pvdata.TypeCodeDouble, float64(2.5)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
desc := pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: c.tc}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, c.v); err != nil {
|
||||
t.Fatalf("WriteValue: %v", err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadValue: %v", err)
|
||||
}
|
||||
if got != c.v {
|
||||
t.Errorf("got %v (%T) want %v (%T)", got, got, c.v, c.v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scalar array codec: every array code ----------------------------
|
||||
|
||||
func TestScalarArrayRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
tc byte
|
||||
v pvdata.Value
|
||||
}{
|
||||
{"bool", pvdata.TypeCodeBoolArr, []bool{true, false, true}},
|
||||
{"byte", pvdata.TypeCodeByteArr, []int8{-1, 2, -3}},
|
||||
{"short", pvdata.TypeCodeShortArr, []int16{-1, 2, 30000}},
|
||||
{"int", pvdata.TypeCodeIntArr, []int32{-1, 2, 1 << 30}},
|
||||
{"long", pvdata.TypeCodeLongArr, []int64{-1, 1 << 40}},
|
||||
{"ubyte", pvdata.TypeCodeUByteArr, []uint8{1, 2, 255}},
|
||||
{"ushort", pvdata.TypeCodeUShortArr, []uint16{1, 60000}},
|
||||
{"uint", pvdata.TypeCodeUIntArr, []uint32{1, 4000000000}},
|
||||
{"ulong", pvdata.TypeCodeULongArr, []uint64{1, 1 << 50}},
|
||||
{"float", pvdata.TypeCodeFloatArr, []float32{1.5, -2.25}},
|
||||
{"double", pvdata.TypeCodeDoubleArr, []float64{1.5, -2.25}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
desc := pvdata.FieldDesc{Kind: pvdata.KindScalarArr, TypeCode: c.tc}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, c.v); err != nil {
|
||||
t.Fatalf("WriteValue: %v", err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadValue: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, c.v) {
|
||||
t.Errorf("got %v want %v", got, c.v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringArrayRoundTrip(t *testing.T) {
|
||||
desc := pvdata.FieldDesc{Kind: pvdata.KindStringArr, TypeCode: pvdata.TypeCodeStringArr}
|
||||
for _, v := range [][]string{{"a", "", "epics"}, {}} {
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, v); err != nil {
|
||||
t.Fatalf("WriteValue: %v", err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadValue: %v", err)
|
||||
}
|
||||
gs := got.([]string)
|
||||
if len(gs) != len(v) {
|
||||
t.Fatalf("len: got %d want %d", len(gs), len(v))
|
||||
}
|
||||
for i := range v {
|
||||
if gs[i] != v[i] {
|
||||
t.Errorf("[%d]: got %q want %q", i, gs[i], v[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- FieldDesc round-trip for every Kind -----------------------------
|
||||
|
||||
func TestFieldDescVariants(t *testing.T) {
|
||||
descs := []pvdata.FieldDesc{
|
||||
{TypeCode: pvdata.TypeCodeNull},
|
||||
{Kind: pvdata.KindString, TypeCode: pvdata.TypeCodeString},
|
||||
{Kind: pvdata.KindStringArr, TypeCode: pvdata.TypeCodeStringArr},
|
||||
{Kind: pvdata.KindVariant, TypeCode: pvdata.TypeCodeVariant},
|
||||
{Kind: pvdata.KindScalarArr, TypeCode: pvdata.TypeCodeDoubleArr},
|
||||
{Kind: pvdata.KindUnion, TypeCode: pvdata.TypeCodeUnion, TypeID: "u", Fields: []pvdata.Field{
|
||||
{Name: "a", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
|
||||
}},
|
||||
{Kind: pvdata.KindStructArr, TypeCode: pvdata.TypeCodeStructArr, TypeID: "s", Fields: []pvdata.Field{
|
||||
{Name: "x", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}},
|
||||
}},
|
||||
{Kind: pvdata.KindUnionArr, TypeCode: pvdata.TypeCodeUnionArr, TypeID: "ua", Fields: []pvdata.Field{
|
||||
{Name: "y", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
|
||||
}},
|
||||
}
|
||||
for _, d := range descs {
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteFieldDesc(&buf, d); err != nil {
|
||||
t.Fatalf("WriteFieldDesc(%#x): %v", d.TypeCode, err)
|
||||
}
|
||||
got, err := pvdata.ReadFieldDesc(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFieldDesc(%#x): %v", d.TypeCode, err)
|
||||
}
|
||||
if got.TypeCode != d.TypeCode {
|
||||
t.Errorf("TypeCode: got %#x want %#x", got.TypeCode, d.TypeCode)
|
||||
}
|
||||
if got.TypeID != d.TypeID {
|
||||
t.Errorf("TypeID: got %q want %q", got.TypeID, d.TypeID)
|
||||
}
|
||||
if len(got.Fields) != len(d.Fields) {
|
||||
t.Errorf("Fields: got %d want %d", len(got.Fields), len(d.Fields))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Struct array value (readStructArr) ------------------------------
|
||||
|
||||
func TestStructArrRoundTrip(t *testing.T) {
|
||||
fields := []pvdata.Field{
|
||||
{Name: "x", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
|
||||
}
|
||||
elems := []pvdata.StructValue{
|
||||
{TypeID: "p", Fields: []pvdata.FieldValue{{Name: "x", Value: int32(1)}}},
|
||||
{TypeID: "p", Fields: []pvdata.FieldValue{{Name: "x", Value: int32(2)}}},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteSize(&buf, int64(len(elems))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sdesc := pvdata.FieldDesc{Kind: pvdata.KindStruct, TypeCode: pvdata.TypeCodeStruct, TypeID: "p", Fields: fields}
|
||||
for _, sv := range elems {
|
||||
if err := pvdata.WriteValue(&buf, sdesc, sv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
adesc := pvdata.FieldDesc{Kind: pvdata.KindStructArr, TypeCode: pvdata.TypeCodeStructArr, TypeID: "p", Fields: fields}
|
||||
got, err := pvdata.ReadValue(&buf, adesc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
arr := got.([]pvdata.StructValue)
|
||||
if len(arr) != 2 || arr[0].Fields[0].Value.(int32) != 1 || arr[1].Fields[0].Value.(int32) != 2 {
|
||||
t.Errorf("got %+v", arr)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Union value (readUnion / writeUnion) ----------------------------
|
||||
|
||||
func unionDesc() pvdata.FieldDesc {
|
||||
return pvdata.FieldDesc{
|
||||
Kind: pvdata.KindUnion, TypeCode: pvdata.TypeCodeUnion, TypeID: "u",
|
||||
Fields: []pvdata.Field{
|
||||
{Name: "i", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
|
||||
{Name: "s", Desc: pvdata.FieldDesc{Kind: pvdata.KindString, TypeCode: pvdata.TypeCodeString}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnionSelectedValue(t *testing.T) {
|
||||
desc := unionDesc()
|
||||
// Manually encode: select field 1 (string) with value "hi".
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteSize(&buf, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pvdata.WriteValue(&buf, desc.Fields[1].Desc, "hi"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uv := got.(pvdata.UnionValue)
|
||||
if uv.Selected != 1 || uv.Name != "s" || uv.Value.(string) != "hi" {
|
||||
t.Errorf("got %+v", uv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnionNullSelector(t *testing.T) {
|
||||
desc := unionDesc()
|
||||
// writeUnion only writes the selector; an out-of-range selector → null.
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, pvdata.UnionValue{Selected: 7}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uv := got.(pvdata.UnionValue)
|
||||
if uv.Selected != 7 || uv.Value != nil {
|
||||
t.Errorf("got %+v, want null selector", uv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnionArrRoundTrip(t *testing.T) {
|
||||
desc := pvdata.FieldDesc{
|
||||
Kind: pvdata.KindUnionArr, TypeCode: pvdata.TypeCodeUnionArr, TypeID: "u",
|
||||
Fields: []pvdata.Field{
|
||||
{Name: "i", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteSize(&buf, 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, val := range []int32{10, 20} {
|
||||
if err := pvdata.WriteSize(&buf, 0); err != nil { // selector 0
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pvdata.WriteValue(&buf, desc.Fields[0].Desc, val); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
arr := got.([]pvdata.UnionValue)
|
||||
if len(arr) != 2 || arr[0].Value.(int32) != 10 || arr[1].Value.(int32) != 20 {
|
||||
t.Errorf("got %+v", arr)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Variant value (readVariant + WriteValue variant path) -----------
|
||||
|
||||
func TestVariantRoundTrip(t *testing.T) {
|
||||
inner := pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}
|
||||
vv := pvdata.VariantValue{Desc: inner, Value: float64(9.5)}
|
||||
desc := pvdata.FieldDesc{Kind: pvdata.KindVariant, TypeCode: pvdata.TypeCodeVariant}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, vv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := pvdata.ReadValue(&buf, desc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gvv := got.(pvdata.VariantValue)
|
||||
if gvv.Value.(float64) != 9.5 || gvv.Desc.TypeCode != pvdata.TypeCodeDouble {
|
||||
t.Errorf("got %+v", gvv)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Compact size: extended (>MaxInt32) encodings --------------------
|
||||
|
||||
func TestCompactSizeLarge(t *testing.T) {
|
||||
cases := []int64{math.MaxInt32, math.MaxInt32 + 1, 1 << 40}
|
||||
for _, n := range cases {
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteSize(&buf, n); err != nil {
|
||||
t.Fatalf("WriteSize(%d): %v", n, err)
|
||||
}
|
||||
got, err := pvdata.ReadSize(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadSize(%d): %v", n, err)
|
||||
}
|
||||
if got != n {
|
||||
t.Errorf("round-trip size %d → %d", n, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Error / truncation paths ----------------------------------------
|
||||
|
||||
func TestReadErrors(t *testing.T) {
|
||||
if _, err := pvdata.ReadSize(bytes.NewReader(nil)); err == nil {
|
||||
t.Error("ReadSize on empty: want error")
|
||||
}
|
||||
if _, err := pvdata.ReadString(bytes.NewReader([]byte{0x05})); err == nil {
|
||||
t.Error("ReadString truncated: want error")
|
||||
}
|
||||
if _, err := pvdata.ReadFieldDesc(bytes.NewReader(nil)); err == nil {
|
||||
t.Error("ReadFieldDesc on empty: want error")
|
||||
}
|
||||
// Unknown scalar type code.
|
||||
badScalar := pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: 0x99}
|
||||
if _, err := pvdata.ReadValue(bytes.NewReader([]byte{1, 2, 3, 4}), badScalar); err == nil {
|
||||
t.Error("ReadValue unknown scalar code: want error")
|
||||
}
|
||||
// Unknown array type code (after a valid size byte).
|
||||
badArr := pvdata.FieldDesc{Kind: pvdata.KindScalarArr, TypeCode: 0x99}
|
||||
if _, err := pvdata.ReadValue(bytes.NewReader([]byte{0x01}), badArr); err == nil {
|
||||
t.Error("ReadValue unknown array code: want error")
|
||||
}
|
||||
// Unknown kind.
|
||||
if _, err := pvdata.ReadValue(bytes.NewReader(nil), pvdata.FieldDesc{Kind: pvdata.Kind(99)}); err == nil {
|
||||
t.Error("ReadValue unknown kind: want error")
|
||||
}
|
||||
// WriteValue unsupported kind (struct array has no writer).
|
||||
if err := pvdata.WriteValue(&bytes.Buffer{}, pvdata.FieldDesc{Kind: pvdata.KindStructArr}, nil); err == nil {
|
||||
t.Error("WriteValue unsupported kind: want error")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- BitSet edges ----------------------------------------------------
|
||||
|
||||
func TestBitSetEdges(t *testing.T) {
|
||||
bs := pvdata.NewBitSet()
|
||||
if bs.Has(0) {
|
||||
t.Error("fresh NewBitSet should be empty")
|
||||
}
|
||||
bs.Set(0)
|
||||
bs.Set(5)
|
||||
bs.Set(12)
|
||||
if got := bs.FieldIndices(); !reflect.DeepEqual(got, []int{0, 5, 12}) {
|
||||
t.Errorf("FieldIndices: got %v want [0 5 12]", got)
|
||||
}
|
||||
if bs.Has(100) { // far out of range → no panic, false
|
||||
t.Error("Has(100) should be false")
|
||||
}
|
||||
|
||||
// Empty BitSet round-trips as a zero-length payload.
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteBitSet(&buf, pvdata.NewBitSet()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := pvdata.ReadBitSet(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.FieldIndices()) != 0 {
|
||||
t.Errorf("empty BitSet round-trip: got %v", got.FieldIndices())
|
||||
}
|
||||
}
|
||||
+34
-33
@@ -22,30 +22,30 @@ import (
|
||||
// types. Values from the spec §3.1.
|
||||
|
||||
const (
|
||||
TypeCodeBoolean byte = 0x00
|
||||
TypeCodeByte byte = 0x20 // int8
|
||||
TypeCodeShort byte = 0x21 // int16
|
||||
TypeCodeInt byte = 0x22 // int32
|
||||
TypeCodeLong byte = 0x23 // int64
|
||||
TypeCodeUByte byte = 0x24 // uint8
|
||||
TypeCodeUShort byte = 0x25 // uint16
|
||||
TypeCodeUInt byte = 0x26 // uint32
|
||||
TypeCodeULong byte = 0x27 // uint64
|
||||
TypeCodeFloat byte = 0x42 // float32
|
||||
TypeCodeDouble byte = 0x43 // float64
|
||||
TypeCodeString byte = 0x60
|
||||
TypeCodeStruct byte = 0x80
|
||||
TypeCodeUnion byte = 0x81
|
||||
TypeCodeBoolArr byte = 0x08
|
||||
TypeCodeByteArr byte = 0x28
|
||||
TypeCodeShortArr byte = 0x29
|
||||
TypeCodeIntArr byte = 0x2A
|
||||
TypeCodeLongArr byte = 0x2B
|
||||
TypeCodeUByteArr byte = 0x2C
|
||||
TypeCodeBoolean byte = 0x00
|
||||
TypeCodeByte byte = 0x20 // int8
|
||||
TypeCodeShort byte = 0x21 // int16
|
||||
TypeCodeInt byte = 0x22 // int32
|
||||
TypeCodeLong byte = 0x23 // int64
|
||||
TypeCodeUByte byte = 0x24 // uint8
|
||||
TypeCodeUShort byte = 0x25 // uint16
|
||||
TypeCodeUInt byte = 0x26 // uint32
|
||||
TypeCodeULong byte = 0x27 // uint64
|
||||
TypeCodeFloat byte = 0x42 // float32
|
||||
TypeCodeDouble byte = 0x43 // float64
|
||||
TypeCodeString byte = 0x60
|
||||
TypeCodeStruct byte = 0x80
|
||||
TypeCodeUnion byte = 0x81
|
||||
TypeCodeBoolArr byte = 0x08
|
||||
TypeCodeByteArr byte = 0x28
|
||||
TypeCodeShortArr byte = 0x29
|
||||
TypeCodeIntArr byte = 0x2A
|
||||
TypeCodeLongArr byte = 0x2B
|
||||
TypeCodeUByteArr byte = 0x2C
|
||||
TypeCodeUShortArr byte = 0x2D
|
||||
TypeCodeUIntArr byte = 0x2E
|
||||
TypeCodeULongArr byte = 0x2F
|
||||
TypeCodeFloatArr byte = 0x4A
|
||||
TypeCodeUIntArr byte = 0x2E
|
||||
TypeCodeULongArr byte = 0x2F
|
||||
TypeCodeFloatArr byte = 0x4A
|
||||
TypeCodeDoubleArr byte = 0x4B
|
||||
TypeCodeStringArr byte = 0x68
|
||||
TypeCodeStructArr byte = 0x88
|
||||
@@ -142,19 +142,19 @@ const (
|
||||
KindScalarArr // variable-length array of scalars
|
||||
KindString // pvData string (treated specially)
|
||||
KindStringArr
|
||||
KindStruct // structure
|
||||
KindStruct // structure
|
||||
KindStructArr
|
||||
KindUnion // discriminated union
|
||||
KindUnion // discriminated union
|
||||
KindUnionArr
|
||||
KindVariant // any (variant)
|
||||
KindVariant // any (variant)
|
||||
)
|
||||
|
||||
// FieldDesc describes the type of a single field in a pvData structure.
|
||||
type FieldDesc struct {
|
||||
Kind Kind
|
||||
TypeCode byte // scalar/array type code; 0 for struct/union
|
||||
TypeID string // optional structure/union type ID string
|
||||
Fields []Field // child fields for struct/union
|
||||
Kind Kind
|
||||
TypeCode byte // scalar/array type code; 0 for struct/union
|
||||
TypeID string // optional structure/union type ID string
|
||||
Fields []Field // child fields for struct/union
|
||||
}
|
||||
|
||||
// Field is a named FieldDesc (one member of a struct or union).
|
||||
@@ -293,9 +293,10 @@ func WriteFieldDesc(w io.Writer, fd FieldDesc) error {
|
||||
// callers type-assert to concrete types listed below.
|
||||
//
|
||||
// Scalar types:
|
||||
// bool, int8, int16, int32, int64,
|
||||
// uint8, uint16, uint32, uint64,
|
||||
// float32, float64, string
|
||||
//
|
||||
// bool, int8, int16, int32, int64,
|
||||
// uint8, uint16, uint32, uint64,
|
||||
// float32, float64, string
|
||||
//
|
||||
// Array types: []T for each scalar T above; []string.
|
||||
// Struct: StructValue
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestValueDouble(t *testing.T) {
|
||||
|
||||
func TestValueIntArray(t *testing.T) {
|
||||
desc := pvdata.FieldDesc{Kind: pvdata.KindScalarArr, TypeCode: pvdata.TypeCodeIntArr}
|
||||
var v pvdata.Value = []int32{1, 2, 3, -7, 1<<30}
|
||||
var v pvdata.Value = []int32{1, 2, 3, -7, 1 << 30}
|
||||
var buf bytes.Buffer
|
||||
if err := pvdata.WriteValue(&buf, desc, v); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user