Files
2026-05-21 07:41:56 +02:00

351 lines
9.2 KiB
Go

//go:build !epics
// Integration tests for the pure-Go EPICS datasource adapter.
// These tests use the in-process fake CA server from the goca testca package
// and therefore do not require a real EPICS installation.
package epics_test
import (
"context"
"math"
"testing"
"time"
"github.com/uopi/goca/proto"
"github.com/uopi/goca/testca"
"github.com/uopi/uopi/internal/datasource"
"github.com/uopi/uopi/internal/datasource/epics"
)
// newTestDS creates a datasource.DataSource backed by a fake CA server.
func newTestDS(t *testing.T, pvs []testca.PVSpec) (datasource.DataSource, *testca.Server) {
t.Helper()
srv, err := testca.New(pvs)
if err != nil {
t.Fatalf("testca.New: %v", err)
}
t.Cleanup(srv.Close)
ds := epics.New(srv.UDPAddr(), "", "", "", false, nil)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)
if err := ds.Connect(ctx); err != nil {
t.Fatalf("Connect: %v", err)
}
return ds, srv
}
// -------------------------------------------------------------------------- //
// GetMetadata //
// -------------------------------------------------------------------------- //
func TestPureGo_GetMetadataDouble(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:DOUBLE", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
meta, err := ds.GetMetadata(ctx, "DS:DOUBLE")
if err != nil {
t.Fatalf("GetMetadata: %v", err)
}
if meta.Name != "DS:DOUBLE" {
t.Errorf("Name = %q, want %q", meta.Name, "DS:DOUBLE")
}
if meta.Type != datasource.TypeFloat64 {
t.Errorf("Type = %v, want TypeFloat64", meta.Type)
}
if !meta.Writable {
t.Error("expected Writable=true")
}
}
func TestPureGo_GetMetadataString(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:STR", DBFType: proto.DBFString, Count: 1, Value: "hello"},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
meta, err := ds.GetMetadata(ctx, "DS:STR")
if err != nil {
t.Fatalf("GetMetadata: %v", err)
}
if meta.Type != datasource.TypeString {
t.Errorf("Type = %v, want TypeString", meta.Type)
}
}
func TestPureGo_GetMetadataEnum(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:ENUM", DBFType: proto.DBFEnum, Count: 1, Value: int16(0)},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
meta, err := ds.GetMetadata(ctx, "DS:ENUM")
if err != nil {
t.Fatalf("GetMetadata: %v", err)
}
if meta.Type != datasource.TypeEnum {
t.Errorf("Type = %v, want TypeEnum", meta.Type)
}
}
func TestPureGo_GetMetadataReadOnly(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:RO", DBFType: proto.DBFDouble, Count: 1, Value: 0.0,
Access: proto.AccessRead},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
meta, err := ds.GetMetadata(ctx, "DS:RO")
if err != nil {
t.Fatalf("GetMetadata: %v", err)
}
if meta.Writable {
t.Error("expected Writable=false for read-only PV")
}
}
func TestPureGo_GetMetadataCached(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:CACHED", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
m1, err := ds.GetMetadata(ctx, "DS:CACHED")
if err != nil {
t.Fatalf("first GetMetadata: %v", err)
}
m2, err := ds.GetMetadata(ctx, "DS:CACHED")
if err != nil {
t.Fatalf("second GetMetadata: %v", err)
}
if m1.Name != m2.Name || m1.Type != m2.Type {
t.Error("second GetMetadata returned different result from cache")
}
}
// -------------------------------------------------------------------------- //
// Subscribe //
// -------------------------------------------------------------------------- //
func TestPureGo_SubscribeInitialValue(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:SUB", DBFType: proto.DBFDouble, Count: 1, Value: 7.5},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch := make(chan datasource.Value, 16)
unsub, err := ds.Subscribe(ctx, "DS:SUB", ch)
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
defer unsub()
// Drain MetaUpdate and value frames — first numeric value wins.
deadline := time.After(5 * time.Second)
for {
select {
case v := <-ch:
if v.MetaUpdate {
continue
}
f, ok := v.Data.(float64)
if !ok {
t.Fatalf("Data type = %T, want float64", v.Data)
}
if math.Abs(f-7.5) > 1e-6 {
t.Errorf("Data = %g, want 7.5", f)
}
return
case <-deadline:
t.Fatal("timeout waiting for initial value")
}
}
}
func TestPureGo_SubscribeUpdates(t *testing.T) {
ds, srv := newTestDS(t, []testca.PVSpec{
{Name: "DS:UPD", DBFType: proto.DBFDouble, Count: 1, Value: 1.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch := make(chan datasource.Value, 16)
unsub, err := ds.Subscribe(ctx, "DS:UPD", ch)
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
defer unsub()
// Drain initial frames.
deadline := time.After(3 * time.Second)
drainLoop:
for {
select {
case v := <-ch:
if !v.MetaUpdate {
break drainLoop
}
case <-deadline:
t.Fatal("timeout draining initial frames")
}
}
// Push new value via server.
srv.SetValue("DS:UPD", 99.9)
deadline = time.After(3 * time.Second)
for {
select {
case v := <-ch:
if v.MetaUpdate {
continue
}
f, ok := v.Data.(float64)
if !ok {
t.Fatalf("Data type = %T, want float64", v.Data)
}
if math.Abs(f-99.9) > 1e-6 {
t.Errorf("Data = %g, want 99.9", f)
}
return
case <-deadline:
t.Fatal("timeout waiting for updated value")
}
}
}
// -------------------------------------------------------------------------- //
// Write //
// -------------------------------------------------------------------------- //
func TestPureGo_WriteDouble(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:W", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch := make(chan datasource.Value, 16)
unsub, err := ds.Subscribe(ctx, "DS:W", ch)
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
defer unsub()
// Drain initial.
deadline := time.After(3 * time.Second)
drainLoop:
for {
select {
case v := <-ch:
if !v.MetaUpdate {
break drainLoop
}
case <-deadline:
t.Fatal("timeout draining initial frames")
}
}
if err := ds.Write(ctx, "DS:W", float64(3.14)); err != nil {
t.Fatalf("Write: %v", err)
}
deadline = time.After(3 * time.Second)
for {
select {
case v := <-ch:
if v.MetaUpdate {
continue
}
f, ok := v.Data.(float64)
if !ok {
continue
}
if math.Abs(f-3.14) > 1e-6 {
t.Errorf("post-write Data = %g, want 3.14", f)
}
return
case <-deadline:
t.Fatal("timeout waiting for post-write monitor update")
}
}
}
func TestPureGo_WriteReadOnly(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:WRO", DBFType: proto.DBFDouble, Count: 1, Value: 0.0,
Access: proto.AccessRead},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := ds.Write(ctx, "DS:WRO", float64(1.0))
if err == nil {
t.Fatal("expected error writing to read-only PV")
}
}
// -------------------------------------------------------------------------- //
// ListSignals //
// -------------------------------------------------------------------------- //
func TestPureGo_ListSignals(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:LS1", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
{Name: "DS:LS2", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// GetMetadata for each PV to populate the cache.
for _, name := range []string{"DS:LS1", "DS:LS2"} {
if _, err := ds.GetMetadata(ctx, name); err != nil {
t.Fatalf("GetMetadata(%q): %v", name, err)
}
}
sigs, err := ds.ListSignals(ctx)
if err != nil {
t.Fatalf("ListSignals: %v", err)
}
if len(sigs) < 2 {
t.Errorf("ListSignals returned %d signals, want ≥ 2", len(sigs))
}
}
// -------------------------------------------------------------------------- //
// History //
// -------------------------------------------------------------------------- //
func TestPureGo_HistoryUnavailable(t *testing.T) {
ds, _ := newTestDS(t, []testca.PVSpec{
{Name: "DS:HIST", DBFType: proto.DBFDouble, Count: 1, Value: 0.0},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := ds.History(ctx, "DS:HIST", time.Now().Add(-time.Hour), time.Now(), 100)
if err != datasource.ErrHistoryUnavailable {
t.Errorf("History with no archiveURL: want ErrHistoryUnavailable, got %v", err)
}
}