package wshub import ( "encoding/json" "errors" "testing" "time" ) func TestBuildCalibrationMsg(t *testing.T) { tab := newCalTable() tab.Set(CalConfig{Source: "wave", Signal: "Adc", Scale: 0.5, Offset: -1.25, Unit: "V"}) var got struct { Type string `json:"type"` Cal []CalConfig `json:"cal"` } raw := buildCalibrationMsg(tab) if err := json.Unmarshal(raw, &got); err != nil { t.Fatalf("unmarshal %s: %v", raw, err) } if got.Type != "calibration" { t.Errorf("type = %q, want calibration", got.Type) } if len(got.Cal) != 1 || got.Cal[0] != (CalConfig{ Source: "wave", Signal: "Adc", Scale: 0.5, Offset: -1.25, Unit: "V"}) { t.Errorf("cal = %+v", got.Cal) } } func TestBuildCalibrationMsgEmptyTableIsEmptyArray(t *testing.T) { // The SPA replaces its table wholesale on every calibration message, so an // empty table must serialise as [] and not as null. raw := buildCalibrationMsg(newCalTable()) var got struct { Cal []CalConfig `json:"cal"` } if err := json.Unmarshal(raw, &got); err != nil { t.Fatalf("unmarshal %s: %v", raw, err) } if got.Cal == nil { t.Errorf("cal = null, want []; raw = %s", raw) } } func TestBuildConfigAckMsg(t *testing.T) { ok := buildConfigAckMsg("configSaved", "/tmp/x.json", nil) var m map[string]any if err := json.Unmarshal(ok, &m); err != nil { t.Fatal(err) } if m["type"] != "configSaved" || m["ok"] != true || m["path"] != "/tmp/x.json" { t.Errorf("success ack = %s", ok) } if _, has := m["error"]; has { t.Errorf("success ack carries an error field: %s", ok) } bad := buildConfigAckMsg("configReloaded", "", errors.New("boom")) m = nil if err := json.Unmarshal(bad, &m); err != nil { t.Fatal(err) } if m["type"] != "configReloaded" || m["ok"] != false || m["error"] != "boom" { t.Errorf("failure ack = %s", bad) } } func TestHubSetCalibrationCommand(t *testing.T) { h := NewHub() go h.Run() // Register a client before sending commands so broadcasts are observable. sendCh := make(chan wsMessage, 64) c := &wsClient{hub: h, send: sendCh} h.register <- c sleepMillis(20) // let Run() process the register and flush initial state msgs drainSendCh(sendCh) // discard state-sync messages (sources, trigger, cal, ...) h.commandCh <- hubCmd{op: "wsSetCalibration", cal: CalConfig{ Source: "wave", Signal: "Adc", Scale: 4, Offset: 1, Unit: "V"}} if raw := waitMsg(t, sendCh, "calibration"); raw == nil { t.Fatal("no calibration broadcast after a valid setCalibration") } if got := h.cal.List(); len(got) != 1 || got[0].Scale != 4 { t.Fatalf("table = %+v, want one entry with scale 4", got) } // An invalid entry is rejected and emits no broadcast at all. h.commandCh <- hubCmd{op: "wsSetCalibration", cal: CalConfig{ Source: "wave", Signal: "Adc", Scale: 0}} if raw := waitMsg(t, sendCh, "calibration"); raw != nil { t.Errorf("invalid setCalibration broadcast %s", raw) } if got := h.cal.List(); len(got) != 1 || got[0].Scale != 4 { t.Errorf("table changed after a rejected setCalibration: %+v", got) } h.unregister <- c } // drainSendCh reads all currently buffered messages from the channel. func drainSendCh(ch chan wsMessage) { for { select { case <-ch: default: return } } } // waitMsg waits up to ~250 ms for a message of the given type on sendCh. func waitMsg(t *testing.T, sendCh chan wsMessage, msgType string) []byte { t.Helper() deadline := time.After(250 * time.Millisecond) for { select { case msg := <-sendCh: var env struct { Type string `json:"type"` } if json.Unmarshal(msg.data, &env) == nil && env.Type == msgType { return msg.data } case <-deadline: return nil } } } // waitBroadcast is kept for compatibility with the test helper interface; // it delegates to waitMsg using a pre-registered client send channel. // Callers that need it should register a client and use waitMsg directly. func sleepMillis(n int) { time.Sleep(time.Duration(n) * time.Millisecond) }