Phase 6
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package synthetic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/uopi/internal/broker"
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
)
|
||||
|
||||
func newTestSynthetic(t *testing.T) (*Synthetic, *broker.Broker, context.CancelFunc) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
brk := broker.New(ctx, log)
|
||||
syn := New(dir, brk, log)
|
||||
return syn, brk, cancel
|
||||
}
|
||||
|
||||
// TestNameAndConnect verifies basic construction and Connect.
|
||||
func TestNameAndConnect(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if syn.Name() != "synthetic" {
|
||||
t.Errorf("Name: want %q, got %q", "synthetic", syn.Name())
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := syn.Connect(ctx); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectCreatesEmptyFile verifies that Connect creates an empty
|
||||
// synthetic.json when none exists.
|
||||
func TestConnectCreatesEmptyFile(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path := filepath.Join(syn.storePath, "synthetic.json")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("expected file to be created: %v", err)
|
||||
}
|
||||
if string(data) == "" {
|
||||
t.Error("file should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddAndRemoveSignal exercises CRUD at runtime.
|
||||
func TestAddAndRemoveSignal(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
def := SignalDef{
|
||||
Name: "test_gain",
|
||||
DS: "stub",
|
||||
Signal: "sine_1hz",
|
||||
Pipeline: []NodeDef{
|
||||
{Type: "gain", Params: map[string]any{"gain": 2.0}},
|
||||
},
|
||||
Meta: MetaOverride{Unit: "V"},
|
||||
}
|
||||
|
||||
if err := syn.AddSignal(def); err != nil {
|
||||
t.Fatalf("AddSignal: %v", err)
|
||||
}
|
||||
|
||||
// Duplicate should fail.
|
||||
if err := syn.AddSignal(def); err == nil {
|
||||
t.Error("expected error for duplicate signal")
|
||||
}
|
||||
|
||||
// Should be listed.
|
||||
metas, err := syn.ListSignals(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(metas) != 1 || metas[0].Name != "test_gain" {
|
||||
t.Errorf("unexpected metas: %v", metas)
|
||||
}
|
||||
|
||||
// GetMetadata should work.
|
||||
meta, err := syn.GetMetadata(context.Background(), "test_gain")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata: %v", err)
|
||||
}
|
||||
if meta.Unit != "V" {
|
||||
t.Errorf("unit: want V, got %q", meta.Unit)
|
||||
}
|
||||
|
||||
// Remove it.
|
||||
if err := syn.RemoveSignal("test_gain"); err != nil {
|
||||
t.Fatalf("RemoveSignal: %v", err)
|
||||
}
|
||||
|
||||
// Should be gone.
|
||||
if _, err := syn.GetMetadata(context.Background(), "test_gain"); err != datasource.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound after remove, got %v", err)
|
||||
}
|
||||
|
||||
// Remove non-existing should return ErrNotFound.
|
||||
if err := syn.RemoveSignal("nonexistent"); err != datasource.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddSignalEmptyName validates that empty name is rejected.
|
||||
func TestAddSignalEmptyName(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := syn.AddSignal(SignalDef{}); err == nil {
|
||||
t.Error("expected error for empty signal name")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMetadataNotFound verifies ErrNotFound for unknown signal.
|
||||
func TestGetMetadataNotFound(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := syn.GetMetadata(context.Background(), "no_such_signal")
|
||||
if err != datasource.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteNotSupported ensures Write returns ErrNotWritable.
|
||||
func TestWriteNotSupported(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
err := syn.Write(context.Background(), "any", 1.0)
|
||||
if err != datasource.ErrNotWritable {
|
||||
t.Errorf("expected ErrNotWritable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistoryNotSupported ensures History returns ErrHistoryUnavailable.
|
||||
func TestHistoryNotSupported(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
_, err := syn.History(context.Background(), "any", time.Now(), time.Now(), 10)
|
||||
if err != datasource.ErrHistoryUnavailable {
|
||||
t.Errorf("expected ErrHistoryUnavailable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPersistence verifies that definitions survive a restart (New+Connect).
|
||||
func TestPersistence(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
brk := broker.New(ctx, log)
|
||||
syn := New(dir, brk, log)
|
||||
|
||||
if err := syn.Connect(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
def := SignalDef{
|
||||
Name: "persisted",
|
||||
DS: "stub",
|
||||
Signal: "sine_1hz",
|
||||
Pipeline: []NodeDef{
|
||||
{Type: "offset", Params: map[string]any{"offset": 5.0}},
|
||||
},
|
||||
}
|
||||
if err := syn.AddSignal(def); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
|
||||
// Reload.
|
||||
ctx2 := t.Context()
|
||||
brk2 := broker.New(ctx2, log)
|
||||
syn2 := New(dir, brk2, log)
|
||||
if err := syn2.Connect(ctx2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
meta, err := syn2.GetMetadata(ctx2, "persisted")
|
||||
if err != nil {
|
||||
t.Fatalf("after reload, GetMetadata: %v", err)
|
||||
}
|
||||
if meta.Name != "persisted" {
|
||||
t.Errorf("expected name %q, got %q", "persisted", meta.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubscribeUnknownSignal verifies that subscribing to unknown signal returns error.
|
||||
func TestSubscribeUnknownSignal(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ch := make(chan datasource.Value, 1)
|
||||
_, err := syn.Subscribe(context.Background(), "nonexistent", ch)
|
||||
if err != datasource.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetDefs verifies GetDefs returns current definitions.
|
||||
func TestGetDefs(t *testing.T) {
|
||||
syn, _, cancel := newTestSynthetic(t)
|
||||
defer cancel()
|
||||
|
||||
if err := syn.Connect(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defs := syn.GetDefs()
|
||||
if len(defs) != 0 {
|
||||
t.Errorf("expected empty defs, got %d", len(defs))
|
||||
}
|
||||
|
||||
def := SignalDef{
|
||||
Name: "test",
|
||||
DS: "stub",
|
||||
Signal: "noise",
|
||||
}
|
||||
if err := syn.AddSignal(def); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defs = syn.GetDefs()
|
||||
if len(defs) != 1 || defs[0].Name != "test" {
|
||||
t.Errorf("unexpected defs: %v", defs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user