398 lines
9.8 KiB
Go
398 lines
9.8 KiB
Go
package ca_test
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/uopi/goca"
|
|
"github.com/uopi/goca/testca"
|
|
"github.com/uopi/goca/proto"
|
|
)
|
|
|
|
// newTestClient creates a Client pointing only at the fake server's addresses.
|
|
func newTestClient(t *testing.T, srv *testca.Server) *ca.Client {
|
|
t.Helper()
|
|
cfg := ca.Config{
|
|
AddrList: []string{srv.UDPAddr()},
|
|
AutoAddrList: false,
|
|
ClientName: "testclient",
|
|
HostName: "localhost",
|
|
}
|
|
cli, err := ca.NewClient(context.Background(), cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
t.Cleanup(cli.Close)
|
|
return cli
|
|
}
|
|
|
|
// newTestServer creates a fake CA server with standard test PVs.
|
|
func newTestServer(t *testing.T) *testca.Server {
|
|
t.Helper()
|
|
srv, err := testca.New([]testca.PVSpec{
|
|
{Name: "TEST:DOUBLE", DBFType: proto.DBFDouble, Count: 1, Value: 3.14},
|
|
{Name: "TEST:LONG", DBFType: proto.DBFLong, Count: 1, Value: int32(42)},
|
|
{Name: "TEST:STRING", DBFType: proto.DBFString, Count: 1, Value: "hello"},
|
|
{Name: "TEST:ENUM", DBFType: proto.DBFEnum, Count: 1, Value: int16(1)},
|
|
{Name: "TEST:READONLY", DBFType: proto.DBFDouble, Count: 1, Value: 1.0,
|
|
Access: proto.AccessRead},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("testca.New: %v", err)
|
|
}
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Get tests //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func TestGetDouble(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
tv, err := cli.Get(ctx, "TEST:DOUBLE")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if math.Abs(tv.Double-3.14) > 1e-6 {
|
|
t.Errorf("Double = %g, want ~3.14", tv.Double)
|
|
}
|
|
if tv.Timestamp.IsZero() {
|
|
t.Error("Timestamp is zero")
|
|
}
|
|
}
|
|
|
|
func TestGetLong(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
tv, err := cli.Get(ctx, "TEST:LONG")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if tv.Long != 42 {
|
|
t.Errorf("Long = %d, want 42", tv.Long)
|
|
}
|
|
}
|
|
|
|
func TestGetString(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
tv, err := cli.Get(ctx, "TEST:STRING")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if tv.Str != "hello" {
|
|
t.Errorf("Str = %q, want %q", tv.Str, "hello")
|
|
}
|
|
}
|
|
|
|
func TestGetNotFound(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := cli.Get(ctx, "NO:SUCH:PV")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing PV, got nil")
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Subscribe tests //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func TestSubscribeReceivesInitialValue(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
defer unsub()
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
if math.Abs(tv.Double-3.14) > 1e-6 {
|
|
t.Errorf("initial Double = %g, want ~3.14", tv.Double)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for initial monitor value")
|
|
}
|
|
}
|
|
|
|
func TestSubscribeReceivesUpdates(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
defer unsub()
|
|
|
|
// Drain initial value.
|
|
select {
|
|
case <-ch:
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for initial value")
|
|
}
|
|
|
|
// Push a new value from the server side.
|
|
if err := srv.SetValue("TEST:DOUBLE", 99.9); err != nil {
|
|
t.Fatalf("SetValue: %v", err)
|
|
}
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
if math.Abs(tv.Double-99.9) > 1e-6 {
|
|
t.Errorf("updated Double = %g, want ~99.9", tv.Double)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for updated monitor value")
|
|
}
|
|
}
|
|
|
|
func TestUnsubscribeStopsUpdates(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
|
|
// Drain initial value.
|
|
select {
|
|
case <-ch:
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for initial value")
|
|
}
|
|
|
|
unsub() // unsubscribe
|
|
|
|
// Give the EVENT_CANCEL message time to be delivered.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Push a value; channel should NOT receive it.
|
|
srv.SetValue("TEST:DOUBLE", 777.0)
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
// It's possible one update slipped through before cancel was processed.
|
|
// Accept it, but not a second one.
|
|
t.Logf("received one update after unsub (value=%g), checking for second...", tv.Double)
|
|
select {
|
|
case <-ch:
|
|
t.Error("received second update after unsubscribe")
|
|
case <-time.After(100 * time.Millisecond):
|
|
// Good — no second update.
|
|
}
|
|
case <-time.After(150 * time.Millisecond):
|
|
// No update received — correct.
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Put tests //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func TestPutDouble(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Subscribe to observe the effect of Put.
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
defer unsub()
|
|
|
|
// Drain initial value.
|
|
select {
|
|
case <-ch:
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for initial value")
|
|
}
|
|
|
|
if err := cli.Put(ctx, "TEST:DOUBLE", float64(2.718)); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
if math.Abs(tv.Double-2.718) > 1e-6 {
|
|
t.Errorf("post-put Double = %g, want ~2.718", tv.Double)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for post-put monitor value")
|
|
}
|
|
}
|
|
|
|
func TestPutLong(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:LONG", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
defer unsub()
|
|
<-ch // drain initial
|
|
|
|
if err := cli.Put(ctx, "TEST:LONG", int(100)); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
if tv.Long != 100 {
|
|
t.Errorf("post-put Long = %d, want 100", tv.Long)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for post-put value")
|
|
}
|
|
}
|
|
|
|
func TestPutString(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ch := make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:STRING", ch)
|
|
if err != nil {
|
|
t.Fatalf("Subscribe: %v", err)
|
|
}
|
|
defer unsub()
|
|
<-ch // drain initial
|
|
|
|
if err := cli.Put(ctx, "TEST:STRING", "world"); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
select {
|
|
case tv := <-ch:
|
|
if tv.Str != "world" {
|
|
t.Errorf("post-put Str = %q, want %q", tv.Str, "world")
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout waiting for post-put value")
|
|
}
|
|
}
|
|
|
|
func TestPutReadOnly(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := cli.Put(ctx, "TEST:READONLY", float64(1.0))
|
|
if err == nil {
|
|
t.Fatal("expected error writing to read-only PV")
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Multiple subscribers on the same PV //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func TestMultipleSubscribers(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
const n = 3
|
|
chs := make([]chan proto.TimeValue, n)
|
|
for i := range chs {
|
|
chs[i] = make(chan proto.TimeValue, 8)
|
|
unsub, err := cli.Subscribe(ctx, "TEST:DOUBLE", chs[i])
|
|
if err != nil {
|
|
t.Fatalf("Subscribe[%d]: %v", i, err)
|
|
}
|
|
defer unsub()
|
|
}
|
|
|
|
// Drain initial values.
|
|
for i, ch := range chs {
|
|
select {
|
|
case <-ch:
|
|
case <-ctx.Done():
|
|
t.Fatalf("timeout waiting for initial value on subscriber %d", i)
|
|
}
|
|
}
|
|
|
|
// Push a new value.
|
|
srv.SetValue("TEST:DOUBLE", 55.5)
|
|
|
|
for i, ch := range chs {
|
|
select {
|
|
case tv := <-ch:
|
|
if math.Abs(tv.Double-55.5) > 1e-6 {
|
|
t.Errorf("subscriber %d: Double = %g, want 55.5", i, tv.Double)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("timeout waiting for update on subscriber %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Context cancellation //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func TestGetContextCancelled(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
cli := newTestClient(t, srv)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // already cancelled
|
|
|
|
_, err := cli.Get(ctx, "TEST:DOUBLE")
|
|
if err == nil {
|
|
t.Fatal("expected error with cancelled context")
|
|
}
|
|
}
|