- calibration.js: fix baseSignalName('[0]') parity with Go/C++ (>= 0 not > 0)
- calibration.test.js: add assertions for '[0]' edge case in two existing tests
- app.js: remove stale typeof guard around refreshVScaleMenu (always defined)
- app.js: call refreshTrigThresholdField on trig-signal change (both assignment sites)
- index.html: drop maxlength='16' on unit input; normaliseCal is the sole enforcer
- configcheck/main.go: delete dead nextOneOf function (no callers)
- hub_calibration_test.go: delete orphaned waitBroadcast comment (function never existed)
- calibration.go: correct arrayIndexSuffix comment to document known Go/C++ difference
- Docs/StreamHub-API.md: add calibration entry count and unit byte limits to §5 table
- spec: fix configReloaded missing path field, '16 chars'→'16 UTF-8 bytes', StreamString→char[], chain scenario→configcheck program, four→five new frames
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
3.7 KiB
Go
133 lines
3.7 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
|
|
func sleepMillis(n int) { time.Sleep(time.Duration(n) * time.Millisecond) }
|