351 lines
9.2 KiB
Go
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)
|
|
}
|
|
}
|