Files
uopi/pkg/ca/client_test.go
2026-04-28 00:09:22 +02:00

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")
}
}