wshub: persist calibration alongside sources; add config reload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
66efd74dd5
commit
dfd257cfd9
@@ -0,0 +1,131 @@
|
||||
package wshub
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newTestManager builds a hub + manager pair with no goroutines running.
|
||||
func newTestManager(t *testing.T) (*Hub, *SourceManager, string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "sources.json")
|
||||
h := NewHub()
|
||||
sm := NewSourceManager(h, path)
|
||||
h.SetSourceManager(sm)
|
||||
return h, sm, path
|
||||
}
|
||||
|
||||
func TestSaveWritesSourcesAndCalibration(t *testing.T) {
|
||||
h, sm, path := newTestManager(t)
|
||||
|
||||
// Register two sources without starting any UDP client.
|
||||
sm.mu.Lock()
|
||||
sm.sources["s1"] = &managedSource{id: "s1", label: "wave", addr: "127.0.0.1:44500"}
|
||||
sm.sources["s2"] = &managedSource{
|
||||
id: "s2", label: "mc", addr: "127.0.0.1:44501",
|
||||
multicastGroup: "239.0.0.1", dataPort: 44502,
|
||||
}
|
||||
sm.mu.Unlock()
|
||||
|
||||
if !h.cal.Set(CalConfig{Source: "wave", Signal: "Adc", Scale: 0.5, Offset: -1.25, Unit: "V"}) {
|
||||
t.Fatal("calibration rejected")
|
||||
}
|
||||
if err := sm.Save(); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
srcs, cals, err := parseConfigFile(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parseConfigFile: %v\n%s", err, data)
|
||||
}
|
||||
if len(srcs) != 2 {
|
||||
t.Fatalf("got %d sources, want 2\n%s", len(srcs), data)
|
||||
}
|
||||
// Save sorts by label so the file is byte-stable across runs.
|
||||
if srcs[0].Label != "mc" || srcs[1].Label != "wave" {
|
||||
t.Errorf("source order = %q,%q, want mc,wave", srcs[0].Label, srcs[1].Label)
|
||||
}
|
||||
if len(cals) != 1 || cals[0].Signal != "Adc" || cals[0].Scale != 0.5 {
|
||||
t.Fatalf("calibration round-trip failed: %+v\n%s", cals, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveWithoutFilePathFails(t *testing.T) {
|
||||
h := NewHub()
|
||||
sm := NewSourceManager(h, "")
|
||||
h.SetSourceManager(sm)
|
||||
if err := sm.Save(); err == nil {
|
||||
t.Error("Save() with no path = nil error, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSeedsCalibrationTable(t *testing.T) {
|
||||
h, sm, path := newTestManager(t)
|
||||
// No "addr" blocks: Load must not start any UDP client during the test.
|
||||
if err := os.WriteFile(path, []byte(`[
|
||||
{"source":"wave","signal":"Adc","scale":0.25,"offset":2,"unit":"mV"},
|
||||
{"source":"wave","signal":"Dac","scale":2}
|
||||
]`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sm.Load(path); err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
got := h.cal.List()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("List() = %d entries, want 2", len(got))
|
||||
}
|
||||
if got[0].Signal != "Adc" || got[0].Unit != "mV" || got[0].Offset != 2 {
|
||||
t.Errorf("Adc = %+v", got[0])
|
||||
}
|
||||
if sm.Path() != path {
|
||||
t.Errorf("Path() = %q, want %q", sm.Path(), path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadReplacesCalibrationAndKeepsLiveSources(t *testing.T) {
|
||||
h, sm, path := newTestManager(t)
|
||||
|
||||
// A live source that the file does not mention must survive the reload.
|
||||
sm.mu.Lock()
|
||||
sm.sources["s1"] = &managedSource{id: "s1", label: "live", addr: "127.0.0.1:44999"}
|
||||
sm.mu.Unlock()
|
||||
|
||||
// A stale calibration that the file does not mention must be dropped.
|
||||
h.cal.Set(CalConfig{Source: "stale", Signal: "Old", Scale: 9})
|
||||
|
||||
if err := os.WriteFile(path, []byte(`[
|
||||
{"source":"wave","signal":"Adc","scale":0.5}
|
||||
]`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sm.Reload(); err != nil {
|
||||
t.Fatalf("Reload: %v", err)
|
||||
}
|
||||
|
||||
got := h.cal.List()
|
||||
if len(got) != 1 || got[0].Source != "wave" {
|
||||
t.Fatalf("after Reload, calibration = %+v, want only wave/Adc", got)
|
||||
}
|
||||
sm.mu.RLock()
|
||||
_, alive := sm.sources["s1"]
|
||||
n := len(sm.sources)
|
||||
sm.mu.RUnlock()
|
||||
if !alive || n != 1 {
|
||||
t.Errorf("live source count = %d (s1 alive=%v), want 1 / true", n, alive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadWithoutFilePathFails(t *testing.T) {
|
||||
h := NewHub()
|
||||
sm := NewSourceManager(h, "")
|
||||
h.SetSourceManager(sm)
|
||||
if err := sm.Reload(); err == nil {
|
||||
t.Error("Reload() with no path = nil error, want error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user