package stub_test import ( "context" "testing" "time" "github.com/uopi/uopi/internal/datasource" "github.com/uopi/uopi/internal/datasource/stub" ) func newStub(t *testing.T) *stub.Stub { t.Helper() s := stub.New() if err := s.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } return s } func TestName(t *testing.T) { if stub.New().Name() != "stub" { t.Error("Name() should return 'stub'") } } func TestListSignals(t *testing.T) { s := newStub(t) sigs, err := s.ListSignals(context.Background()) if err != nil { t.Fatalf("ListSignals: %v", err) } if len(sigs) == 0 { t.Fatal("expected at least one signal") } names := make(map[string]bool) for _, m := range sigs { names[m.Name] = true } for _, want := range []string{"sine_1hz", "ramp_10s", "toggle_1hz", "noise", "setpoint"} { if !names[want] { t.Errorf("signal %q not in ListSignals", want) } } } func TestGetMetadata(t *testing.T) { s := newStub(t) m, err := s.GetMetadata(context.Background(), "sine_1hz") if err != nil { t.Fatalf("GetMetadata: %v", err) } if m.Type != datasource.TypeFloat64 { t.Errorf("Type = %v, want TypeFloat64", m.Type) } if m.Unit == "" { t.Error("expected non-empty Unit") } } func TestGetMetadataNotFound(t *testing.T) { s := newStub(t) _, err := s.GetMetadata(context.Background(), "no:such:pv") if err != datasource.ErrNotFound { t.Errorf("want ErrNotFound, got %v", err) } } func TestSubscribeReceivesValues(t *testing.T) { s := newStub(t) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() ch := make(chan datasource.Value, 4) stop, err := s.Subscribe(ctx, "sine_1hz", ch) if err != nil { t.Fatalf("Subscribe: %v", err) } defer stop() select { case v := <-ch: if v.Quality != datasource.QualityGood { t.Errorf("Quality = %v, want Good", v.Quality) } if _, ok := v.Data.(float64); !ok { t.Errorf("Data type = %T, want float64", v.Data) } if v.Timestamp.IsZero() { t.Error("Timestamp is zero") } case <-ctx.Done(): t.Fatal("timeout waiting for first value") } } func TestSubscribeUnknown(t *testing.T) { s := newStub(t) _, err := s.Subscribe(context.Background(), "no:such", make(chan datasource.Value, 1)) if err != datasource.ErrNotFound { t.Errorf("want ErrNotFound, got %v", err) } } func TestSubscribeCancel(t *testing.T) { s := newStub(t) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() ch := make(chan datasource.Value, 4) stop, err := s.Subscribe(ctx, "ramp_10s", ch) if err != nil { t.Fatalf("Subscribe: %v", err) } // Drain one value to confirm it's running. select { case <-ch: case <-ctx.Done(): t.Fatal("timeout waiting for first value") } stop() // cancel subscription // Drain remaining buffered values then confirm channel goes quiet. time.Sleep(200 * time.Millisecond) for len(ch) > 0 { <-ch } time.Sleep(300 * time.Millisecond) if len(ch) > 0 { t.Error("channel still receiving after stop()") } } func TestWriteSetpoint(t *testing.T) { s := newStub(t) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := s.Write(context.Background(), "setpoint", float64(99.0)); err != nil { t.Fatalf("Write: %v", err) } ch := make(chan datasource.Value, 4) stop, err := s.Subscribe(ctx, "setpoint", ch) if err != nil { t.Fatalf("Subscribe: %v", err) } defer stop() select { case v := <-ch: f, ok := v.Data.(float64) if !ok { t.Fatalf("Data type = %T, want float64", v.Data) } if f != 99.0 { t.Errorf("setpoint value = %g, want 99.0", f) } case <-ctx.Done(): t.Fatal("timeout waiting for setpoint value") } } func TestWriteReadOnly(t *testing.T) { s := newStub(t) err := s.Write(context.Background(), "sine_1hz", float64(0)) if err != datasource.ErrNotWritable { t.Errorf("want ErrNotWritable, got %v", err) } } func TestWriteNotFound(t *testing.T) { s := newStub(t) err := s.Write(context.Background(), "no:such", float64(0)) if err != datasource.ErrNotFound { t.Errorf("want ErrNotFound, got %v", err) } } func TestHistory(t *testing.T) { s := newStub(t) _, err := s.History(context.Background(), "sine_1hz", time.Now().Add(-time.Hour), time.Now(), 100) if err != datasource.ErrHistoryUnavailable { t.Errorf("want ErrHistoryUnavailable, got %v", err) } } func TestToggleBool(t *testing.T) { s := newStub(t) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() ch := make(chan datasource.Value, 4) stop, err := s.Subscribe(ctx, "toggle_1hz", ch) if err != nil { t.Fatalf("Subscribe: %v", err) } defer stop() select { case v := <-ch: if _, ok := v.Data.(bool); !ok { t.Errorf("toggle_1hz Data type = %T, want bool", v.Data) } case <-ctx.Done(): t.Fatal("timeout") } }