package wshub import ( "math" "testing" ) func TestCalConfigNormalise(t *testing.T) { cases := []struct { name string in CalConfig want bool wantUnit string }{ {"plain", CalConfig{Source: "wave", Signal: "Adc", Scale: 2, Offset: -1, Unit: "V"}, true, "V"}, {"trims", CalConfig{Source: " wave ", Signal: " Adc ", Scale: 1, Unit: " V "}, true, "V"}, {"emptySource", CalConfig{Signal: "Adc", Scale: 1}, false, ""}, {"emptySignal", CalConfig{Source: "wave", Scale: 1}, false, ""}, {"zeroScale", CalConfig{Source: "wave", Signal: "Adc", Scale: 0}, false, ""}, {"nanScale", CalConfig{Source: "wave", Signal: "Adc", Scale: math.NaN()}, false, ""}, {"infScale", CalConfig{Source: "wave", Signal: "Adc", Scale: math.Inf(1)}, false, ""}, {"nanOffset", CalConfig{Source: "wave", Signal: "Adc", Scale: 1, Offset: math.NaN()}, false, ""}, {"infOffset", CalConfig{Source: "wave", Signal: "Adc", Scale: 1, Offset: math.Inf(-1)}, false, ""}, {"negScaleOK", CalConfig{Source: "wave", Signal: "Adc", Scale: -1}, true, ""}, {"longUnit", CalConfig{Source: "wave", Signal: "Adc", Scale: 1, Unit: "0123456789abcdefGHIJ"}, true, "0123456789abcdef"}, } for _, c := range cases { got := c.in if ok := got.Normalise(); ok != c.want { t.Errorf("%s: Normalise() = %v, want %v", c.name, ok, c.want) continue } if c.want && got.Unit != c.wantUnit { t.Errorf("%s: Unit = %q, want %q", c.name, got.Unit, c.wantUnit) } } if len("0123456789abcdef") != maxUnitLen { t.Fatalf("test assumes maxUnitLen == 16, got %d", maxUnitLen) } } func TestCalTableSetListAndIdentityRemoval(t *testing.T) { tab := newCalTable() if !tab.Set(CalConfig{Source: "b", Signal: "Y", Scale: 3, Offset: 1, Unit: "A"}) { t.Fatal("Set(b/Y) rejected") } if !tab.Set(CalConfig{Source: "a", Signal: "X", Scale: 2}) { t.Fatal("Set(a/X) rejected") } if tab.Set(CalConfig{Source: "a", Signal: "Z", Scale: 0}) { t.Error("Set with scale=0 accepted, want rejected") } got := tab.List() if len(got) != 2 { t.Fatalf("List() = %d entries, want 2", len(got)) } // Sorted by source then signal. if got[0].Source != "a" || got[1].Source != "b" { t.Errorf("List() order = %q,%q, want a,b", got[0].Source, got[1].Source) } // An identity entry removes the stored one. if !tab.Set(CalConfig{Source: "a", Signal: "X", Scale: 1, Offset: 0, Unit: ""}) { t.Fatal("identity Set rejected") } if got := tab.List(); len(got) != 1 || got[0].Source != "b" { t.Errorf("after identity Set, List() = %+v, want only b/Y", got) } } func TestCalTableReplace(t *testing.T) { tab := newCalTable() tab.Set(CalConfig{Source: "old", Signal: "X", Scale: 5}) tab.Replace([]CalConfig{ {Source: "new", Signal: "Y", Scale: 2}, {Source: "bad", Signal: "Z", Scale: 0}, // invalid → dropped {Source: "id", Signal: "W", Scale: 1, Offset: 0, Unit: ""}, // identity → dropped }) got := tab.List() if len(got) != 1 || got[0].Source != "new" { t.Fatalf("List() = %+v, want only new/Y", got) } } func TestParseConfigFileCurrentFormat(t *testing.T) { // A file written by the current binaries — sources only, spaces after colons. data := []byte(`[ { "label": "wave", "addr": "127.0.0.1:44500" }, { "label": "mc", "addr": "127.0.0.1:44501", "multicastGroup": "239.0.0.1", "dataPort": 44502 } ]`) srcs, cals, err := parseConfigFile(data) if err != nil { t.Fatalf("parseConfigFile: %v", err) } if len(srcs) != 2 || len(cals) != 0 { t.Fatalf("got %d sources / %d cals, want 2 / 0", len(srcs), len(cals)) } if srcs[1].MulticastGroup != "239.0.0.1" || srcs[1].DataPort != 44502 { t.Errorf("multicast source = %+v", srcs[1]) } } func TestParseConfigFileMixed(t *testing.T) { data := []byte(`[ {"label":"wave","addr":"127.0.0.1:44500"}, {"source":"wave","signal":"Adc","scale":0.00030518,"offset":-1.25,"unit":"V"}, {"source":"wave","signal":"Bare"}, {"source":"wave","signal":"Bad","scale":0}, {"nonsense":true} ]`) srcs, cals, err := parseConfigFile(data) if err != nil { t.Fatalf("parseConfigFile: %v", err) } if len(srcs) != 1 { t.Fatalf("got %d sources, want 1", len(srcs)) } if len(cals) != 2 { t.Fatalf("got %d cals, want 2 (Adc and Bare; Bad is invalid)", len(cals)) } if cals[0].Scale != 0.00030518 || cals[0].Offset != -1.25 || cals[0].Unit != "V" { t.Errorf("Adc = %+v", cals[0]) } // Absent scale/offset default to the identity values, not to zero. if cals[1].Signal != "Bare" || cals[1].Scale != 1 || cals[1].Offset != 0 { t.Errorf("Bare = %+v, want scale 1 / offset 0", cals[1]) } } func TestParseConfigFileMalformed(t *testing.T) { if _, _, err := parseConfigFile([]byte("not json")); err == nil { t.Error("parseConfigFile(garbage) = nil error, want error") } } func TestEncodeConfigFileRoundTrip(t *testing.T) { srcs := []SourceConfig{ {Label: "wave", Addr: "127.0.0.1:44500"}, {Label: "mc", Addr: "127.0.0.1:44501", MulticastGroup: "239.0.0.1", DataPort: 44502}, } cals := []CalConfig{ {Source: "wave", Signal: "Adc", Scale: 0.5, Offset: 0, Unit: "V"}, } data, err := encodeConfigFile(srcs, cals) if err != nil { t.Fatalf("encodeConfigFile: %v", err) } gotSrcs, gotCals, err := parseConfigFile(data) if err != nil { t.Fatalf("parseConfigFile(encoded): %v\n%s", err, data) } if len(gotSrcs) != 2 || len(gotCals) != 1 { t.Fatalf("round-trip gave %d sources / %d cals, want 2 / 1\n%s", len(gotSrcs), len(gotCals), data) } if gotSrcs[1] != srcs[1] { t.Errorf("source round-trip: got %+v, want %+v", gotSrcs[1], srcs[1]) } if gotCals[0] != cals[0] { t.Errorf("cal round-trip: got %+v, want %+v", gotCals[0], cals[0]) } // offset 0 must survive as an explicit field, not be dropped by omitempty. if !bytesContains(data, []byte(`"offset": 0`)) { t.Errorf("encoded file lost the zero offset:\n%s", data) } } func bytesContains(hay, needle []byte) bool { for i := 0; i+len(needle) <= len(hay); i++ { if string(hay[i:i+len(needle)]) == string(needle) { return true } } return false }