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