This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git
commit e17ed97035cfb43581afd8c0e6462cbf2470cbdf Author: Szymon Janc <[email protected]> AuthorDate: Thu Oct 22 00:20:32 2026 +0200 newtmgr/config: Add unit tests for connstring and profile handling Cover the serial, BLE (blehostd), bll and Multitech LoRa connstring parsers: defaults, every supported key, values containing '=', bad values with the exact error text, unrecognized keys and malformed pairs. Exercise BuildBllSesnCfg() including the advertisement filter closures with a stub ble.Advertisement, and FillMtechLoraSesnCfg(). For connection profiles, test ConnType string and JSON conversions (documenting that an unknown type name silently maps to CONN_TYPE_NONE), and ConnProfileMgr loading and saving in a temporary home directory: missing file, add/replace/delete round trip, sorted listing, missing profile errors, corrupt and unreadable files, and a golden test of the on-disk JSON format. Assisted-by: Claude:claude-opus-5-5 --- newtmgr/config/ble_config_test.go | 132 ++++++++++++ newtmgr/config/bll_config_test.go | 209 ++++++++++++++++++ newtmgr/config/connprofile_test.go | 355 +++++++++++++++++++++++++++++++ newtmgr/config/mtech_lora_config_test.go | 140 ++++++++++++ newtmgr/config/serial_config_test.go | 100 +++++++++ newtmgr/config/util_test.go | 55 +++++ 6 files changed, 991 insertions(+) diff --git a/newtmgr/config/ble_config_test.go b/newtmgr/config/ble_config_test.go new file mode 100644 index 0000000..8b06523 --- /dev/null +++ b/newtmgr/config/ble_config_test.go @@ -0,0 +1,132 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + + "mynewt.apache.org/newtmgr/nmxact/bledefs" +) + +func mustParseBleAddr(t *testing.T, s string) bledefs.BleAddr { + t.Helper() + a, err := bledefs.ParseBleAddr(s) + if err != nil { + t.Fatalf("ParseBleAddr(%q): %v", s, err) + } + return a +} + +func TestParseBleConnStringDefaults(t *testing.T) { + setTimeout(t, 7.25) + setHciIdx(t, 2) + + bc, err := ParseBleConnString("peer_name=dev") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := BleConfig{ + PeerAddrType: bledefs.BLE_ADDR_TYPE_PUBLIC, + PeerName: "dev", + OwnAddrType: bledefs.BLE_ADDR_TYPE_RANDOM, + ConnTimeout: 7.25, + BlehostdPath: "blehostd", + HciIdx: 2, + } + if *bc != want { + t.Errorf("got %+v, want %+v", *bc, want) + } +} + +func TestParseBleConnStringKeys(t *testing.T) { + setHciIdx(t, 0) + + cs := "peer_addr_type=random,peer_addr=01:23:45:67:89:AB," + + "peer_name=a=b,own_addr_type=rpa_pub,own_addr=ff:ee:dd:cc:bb:aa," + + "bhd_path=/opt/blehostd,ctlr_path=/dev/ttyUSB1" + bc, err := ParseBleConnString(cs) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if bc.PeerAddrType != bledefs.BLE_ADDR_TYPE_RANDOM { + t.Errorf("PeerAddrType = %v", bc.PeerAddrType) + } + if bc.PeerAddr != mustParseBleAddr(t, "01:23:45:67:89:ab") { + t.Errorf("PeerAddr = %v", bc.PeerAddr) + } + if bc.PeerName != "a=b" { + t.Errorf("PeerName = %q, want \"a=b\"", bc.PeerName) + } + if bc.OwnAddrType != bledefs.BLE_ADDR_TYPE_RPA_PUB { + t.Errorf("OwnAddrType = %v", bc.OwnAddrType) + } + if bc.OwnAddr != mustParseBleAddr(t, "ff:ee:dd:cc:bb:aa") { + t.Errorf("OwnAddr = %v", bc.OwnAddr) + } + if bc.BlehostdPath != "/opt/blehostd" { + t.Errorf("BlehostdPath = %q", bc.BlehostdPath) + } + if bc.ControllerPath != "/dev/ttyUSB1" { + t.Errorf("ControllerPath = %q", bc.ControllerPath) + } +} + +func TestParseBleConnStringErrors(t *testing.T) { + tests := []struct { + cs string + err string + }{ + {"", + "Invalid BLE connstring; expected comma-separated " + + "key=value pairs; no '=' in: "}, + {"peer_name=x,bogus", + "Invalid BLE connstring; expected comma-separated " + + "key=value pairs; no '=' in: bogus"}, + {"peer_addr_type=foo", + "Invalid BLE connstring; Invalid peer_addr_type: foo"}, + {"own_addr_type=", + "Invalid BLE connstring; Invalid own_addr_type: "}, + {"peer_addr=01:02", + "Invalid BLE connstring; Invalid peer_addr; " + + "invalid BLE addr string: 01:02"}, + {"own_addr=1:2:3:4:5", + "Invalid BLE connstring; Invalid own_addr; " + + "invalid BLE addr string: 1:2:3:4:5"}, + {"foo=bar", + "Invalid BLE connstring; Unrecognized key: foo"}, + } + + for _, tt := range tests { + t.Run(tt.cs, func(t *testing.T) { + bc, err := ParseBleConnString(tt.cs) + if err == nil { + t.Fatalf("expected error, got %+v", bc) + } + if bc != nil { + t.Errorf("expected nil config on error") + } + if err.Error() != tt.err { + t.Errorf("error = %q, want %q", err.Error(), tt.err) + } + }) + } +} diff --git a/newtmgr/config/bll_config_test.go b/newtmgr/config/bll_config_test.go new file mode 100644 index 0000000..cc6474a --- /dev/null +++ b/newtmgr/config/bll_config_test.go @@ -0,0 +1,209 @@ +//go:build !windows + +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + "time" + + "github.com/JuulLabs-OSS/ble" + + "mynewt.apache.org/newtmgr/nmxact/bledefs" +) + +type fakeAddr string + +func (a fakeAddr) String() string { return string(a) } + +// fakeAdv is a stub ble.Advertisement; only the methods used by the +// bll advertisement filters are implemented. +type fakeAdv struct { + ble.Advertisement + name string + addr string +} + +func (a *fakeAdv) LocalName() string { return a.name } +func (a *fakeAdv) Addr() ble.Addr { return fakeAddr(a.addr) } + +func TestParseBllConnString(t *testing.T) { + setTimeout(t, 4) + setHciIdx(t, 1) + + tests := []struct { + name string + cs string + want BllConfig + }{ + {"peer_name", "peer_name=mydev", + BllConfig{PeerName: "mydev", ConnTimeout: 4, HciIdx: 1}}, + {"all keys", + "ctlr_name=hci0,own_addr_type=random,peer_id=aa:bb," + + "peer_name=x=y,conn_timeout=23.54", + BllConfig{ + CtlrName: "hci0", + OwnAddrType: bledefs.BLE_ADDR_TYPE_RANDOM, + PeerId: "aa:bb", + PeerName: "x=y", + ConnTimeout: 23.54, + HciIdx: 1, + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bc, err := ParseBllConnString(tt.cs) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if *bc != tt.want { + t.Errorf("got %+v, want %+v", *bc, tt.want) + } + }) + } +} + +func TestParseBllConnStringErrors(t *testing.T) { + tests := []struct { + cs string + err string + }{ + {"peer_name", + "Invalid BLE connstring; expected comma-separated " + + "key=value pairs; no '=' in: peer_name"}, + {"peer_name=a,", + "Invalid BLE connstring; expected comma-separated " + + "key=value pairs; no '=' in: "}, + {"own_addr_type=bad", + "Invalid BLE connstring; Invalid own_addr_type: bad"}, + {"conn_timeout=soon", + "Invalid BLE connstring; Invalid conn_timeout: soon"}, + {"peer_addr=01:02:03:04:05:06", + "Invalid BLE connstring; Unrecognized key: peer_addr"}, + } + + for _, tt := range tests { + t.Run(tt.cs, func(t *testing.T) { + bc, err := ParseBllConnString(tt.cs) + if err == nil { + t.Fatalf("expected error, got %+v", bc) + } + if bc != nil { + t.Errorf("expected nil config on error") + } + if err.Error() != tt.err { + t.Errorf("error = %q, want %q", err.Error(), tt.err) + } + }) + } +} + +func TestBuildBllSesnCfg(t *testing.T) { + type advCase struct { + adv fakeAdv + want bool + } + + tests := []struct { + name string + devName string + bc BllConfig + advs []advCase + wantPeerNm string + }{ + { + name: "peer_name", + bc: BllConfig{PeerName: "dev1", PeerId: "11:22", ConnTimeout: 1.5}, + advs: []advCase{ + {fakeAdv{name: "dev1", addr: "00:00"}, true}, + {fakeAdv{name: "dev2", addr: "11:22"}, false}, + {fakeAdv{name: "", addr: ""}, false}, + }, + wantPeerNm: "dev1", + }, + { + name: "peer_id", + bc: BllConfig{PeerId: "11:22", ConnTimeout: 1.5}, + advs: []advCase{ + {fakeAdv{name: "dev1", addr: "11:22"}, true}, + {fakeAdv{name: "11:22", addr: "33:44"}, false}, + }, + }, + { + name: "device name overrides", + devName: "global", + bc: BllConfig{PeerName: "dev1", PeerId: "11:22", ConnTimeout: 1.5}, + advs: []advCase{ + {fakeAdv{name: "global", addr: "00:00"}, true}, + {fakeAdv{name: "dev1", addr: "11:22"}, false}, + }, + wantPeerNm: "global", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setDeviceName(t, tt.devName) + setBleWriteRsp(t, true) + + bc := tt.bc + sc, err := BuildBllSesnCfg(&bc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if bc.PeerName != tt.wantPeerNm { + t.Errorf("PeerName = %q, want %q", bc.PeerName, tt.wantPeerNm) + } + if sc.ConnTimeout != 1500*time.Millisecond { + t.Errorf("ConnTimeout = %v, want 1.5s", sc.ConnTimeout) + } + if !sc.WriteRsp { + t.Errorf("WriteRsp = false, want true") + } + if sc.PreferredMtu != 512 || sc.ConnTries != 3 { + t.Errorf("defaults not kept: %+v", sc) + } + if sc.AdvFilter == nil { + t.Fatalf("AdvFilter is nil") + } + for i, ac := range tt.advs { + adv := ac.adv + if got := sc.AdvFilter(&adv); got != ac.want { + t.Errorf("adv %d (%+v): filter = %v, want %v", + i, ac.adv, got, ac.want) + } + } + }) + } +} + +func TestBuildBllSesnCfgNoPeer(t *testing.T) { + setDeviceName(t, "") + + _, err := BuildBllSesnCfg(&BllConfig{CtlrName: "hci0"}) + if err == nil { + t.Fatalf("expected error") + } + if err.Error() != "bll session lacks a peer specifier" { + t.Errorf("error = %q", err.Error()) + } +} diff --git a/newtmgr/config/connprofile_test.go b/newtmgr/config/connprofile_test.go new file mode 100644 index 0000000..df76d67 --- /dev/null +++ b/newtmgr/config/connprofile_test.go @@ -0,0 +1,355 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mitchellh/go-homedir" + + "mynewt.apache.org/newtmgr/newtmgr/nmutil" +) + +func TestConnTypeStrings(t *testing.T) { + tests := []struct { + ct ConnType + s string + }{ + {CONN_TYPE_SERIAL_PLAIN, "serial"}, + {CONN_TYPE_SERIAL_OIC, "oic_serial"}, + {CONN_TYPE_BLL_PLAIN, "ble"}, + {CONN_TYPE_BLL_OIC, "oic_ble"}, + {CONN_TYPE_BLE_PLAIN, "bhd"}, + {CONN_TYPE_BLE_OIC, "oic_bhd"}, + {CONN_TYPE_UDP_PLAIN, "udp"}, + {CONN_TYPE_UDP_OIC, "oic_udp"}, + {CONN_TYPE_MTECH_LORA_OIC, "oic_mtech"}, + {CONN_TYPE_NONE, "???"}, + } + + for _, tt := range tests { + if got := ConnTypeToString(tt.ct); got != tt.s { + t.Errorf("ConnTypeToString(%d) = %q, want %q", tt.ct, got, tt.s) + } + got, err := ConnTypeFromString(tt.s) + if err != nil { + t.Errorf("ConnTypeFromString(%q): %v", tt.s, err) + } else if got != tt.ct { + t.Errorf("ConnTypeFromString(%q) = %d, want %d", tt.s, got, tt.ct) + } + } + + if len(connTypeNameMap) != len(tests) { + t.Errorf("connTypeNameMap has %d entries, test covers %d", + len(connTypeNameMap), len(tests)) + } + + if s := ConnTypeToString(ConnType(100)); s != "" { + t.Errorf("ConnTypeToString(100) = %q, want \"\"", s) + } +} + +func TestConnTypeFromStringInvalid(t *testing.T) { + for _, s := range []string{"", "SERIAL", "foo"} { + ct, err := ConnTypeFromString(s) + if err == nil { + t.Errorf("ConnTypeFromString(%q): expected error", s) + continue + } + if ct != CONN_TYPE_NONE { + t.Errorf("ConnTypeFromString(%q) = %d, want NONE", s, ct) + } + want := "Invalid connection type: " + s + if err.Error() != want { + t.Errorf("error = %q, want %q", err.Error(), want) + } + } +} + +func TestConnTypeJSON(t *testing.T) { + ct := CONN_TYPE_SERIAL_OIC + b, err := json.Marshal(&ct) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if string(b) != `"oic_serial"` { + t.Errorf("Marshal = %s, want \"oic_serial\"", b) + } + + var back ConnType + if err := json.Unmarshal(b, &back); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if back != ct { + t.Errorf("Unmarshal = %d, want %d", back, ct) + } + + // Non-string JSON is rejected. + if err := json.Unmarshal([]byte("3"), &back); err == nil { + t.Errorf("Unmarshal(3): expected error") + } +} + +// An unknown connection type name in the profile file is not an error; it +// is silently mapped to CONN_TYPE_NONE. This documents current behaviour. +func TestConnTypeUnmarshalUnknown(t *testing.T) { + ct := CONN_TYPE_UDP_PLAIN + if err := json.Unmarshal([]byte(`"carrier_pigeon"`), &ct); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if ct != CONN_TYPE_NONE { + t.Errorf("Unmarshal = %d, want CONN_TYPE_NONE", ct) + } +} + +func TestConnProfileString(t *testing.T) { + p := &ConnProfile{ + Name: "p1", + Type: CONN_TYPE_SERIAL_PLAIN, + ConnString: "dev=/dev/ttyUSB0", + } + want := "name=p1 type=serial connstring=dev=/dev/ttyUSB0" + if p.String() != want { + t.Errorf("String() = %q, want %q", p.String(), want) + } +} + +// fakeHome points the home directory at a fresh temporary directory and +// returns the path of the connection profile file within it. +func fakeHome(t *testing.T) string { + t.Helper() + + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv("USERPROFILE", dir) + homedir.Reset() + t.Cleanup(homedir.Reset) + + old := nmutil.ToolInfo + nmutil.ToolInfo.CfgFilename = ".newtmgr.cp.json" + t.Cleanup(func() { nmutil.ToolInfo = old }) + + return filepath.Join(dir, ".newtmgr.cp.json") +} + +func TestConnProfileMgrMissingFile(t *testing.T) { + path := fakeHome(t) + + cpm, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("NewConnProfileMgr: %v", err) + } + list, err := cpm.GetConnProfileList() + if err != nil { + t.Fatalf("GetConnProfileList: %v", err) + } + if len(list) != 0 { + t.Errorf("expected no profiles, got %d", len(list)) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("profile file should not be created on load: %v", err) + } +} + +func TestConnProfileMgrRoundTrip(t *testing.T) { + fakeHome(t) + + cpm, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("NewConnProfileMgr: %v", err) + } + + profs := []*ConnProfile{ + {Name: "zeta", Type: CONN_TYPE_UDP_PLAIN, ConnString: "127.0.0.1:5683"}, + {Name: "alpha", Type: CONN_TYPE_SERIAL_PLAIN, + ConnString: "dev=/dev/ttyUSB0,baud=115200"}, + {Name: "mid", Type: CONN_TYPE_BLL_OIC, ConnString: "peer_name=a=b"}, + } + for _, p := range profs { + if err := cpm.AddConnProfile(p); err != nil { + t.Fatalf("AddConnProfile(%s): %v", p.Name, err) + } + } + + cpm2, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("reload: %v", err) + } + list, _ := cpm2.GetConnProfileList() + names := []string{} + for _, p := range list { + names = append(names, p.Name) + } + if strings.Join(names, ",") != "alpha,mid,zeta" { + t.Errorf("profile list = %v, want sorted alpha,mid,zeta", names) + } + + for _, want := range profs { + got, err := cpm2.GetConnProfile(want.Name) + if err != nil { + t.Fatalf("GetConnProfile(%s): %v", want.Name, err) + } + if *got != *want { + t.Errorf("profile %s = %+v, want %+v", want.Name, *got, *want) + } + } + + // Replace an existing profile. + repl := &ConnProfile{Name: "mid", Type: CONN_TYPE_SERIAL_OIC, + ConnString: "COM1"} + if err := cpm2.AddConnProfile(repl); err != nil { + t.Fatalf("AddConnProfile(replace): %v", err) + } + + // Delete a profile and reload. + if err := cpm2.DeleteConnProfile("zeta"); err != nil { + t.Fatalf("DeleteConnProfile: %v", err) + } + + cpm3, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("reload: %v", err) + } + list, _ = cpm3.GetConnProfileList() + if len(list) != 2 || list[0].Name != "alpha" || list[1].Name != "mid" { + t.Fatalf("unexpected profiles after delete: %v", list) + } + if *list[1] != *repl { + t.Errorf("replaced profile = %+v, want %+v", *list[1], *repl) + } + if _, err := cpm3.GetConnProfile("zeta"); err == nil { + t.Errorf("deleted profile still present") + } +} + +func TestConnProfileMgrMissingProfile(t *testing.T) { + fakeHome(t) + + cpm, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("NewConnProfileMgr: %v", err) + } + + _, err = cpm.GetConnProfile("nope") + if err == nil || err.Error() != `connection profile "nope" doesn't exist` { + t.Errorf("GetConnProfile error = %v", err) + } + + err = cpm.DeleteConnProfile("nope") + if err == nil || err.Error() != `connection profile "nope" doesn't exist` { + t.Errorf("DeleteConnProfile error = %v", err) + } +} + +func TestConnProfileMgrCorruptFile(t *testing.T) { + path := fakeHome(t) + + if err := os.WriteFile(path, []byte("{not json"), 0644); err != nil { + t.Fatal(err) + } + + cpm, err := NewConnProfileMgr() + if err == nil { + t.Fatalf("expected error, got %+v", cpm) + } + prefix := "error reading connection profile config (" + path + "): " + if !strings.HasPrefix(err.Error(), prefix) { + t.Errorf("error = %q, want prefix %q", err.Error(), prefix) + } +} + +func TestConnProfileMgrUnreadableFile(t *testing.T) { + path := fakeHome(t) + + // A directory in place of the file cannot be read. + if err := os.Mkdir(path, 0755); err != nil { + t.Fatal(err) + } + + if _, err := NewConnProfileMgr(); err == nil { + t.Fatalf("expected error") + } +} + +const goldenProfiles = `[ + { + "MyName": "a", + "MyType": "oic_serial", + "MyConnString": "dev=/dev/ttyACM0" + }, + { + "MyName": "b", + "MyType": "oic_mtech", + "MyConnString": "" + } +]` + +func TestConnProfileMgrFileFormat(t *testing.T) { + path := fakeHome(t) + + cpm, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("NewConnProfileMgr: %v", err) + } + if err := cpm.AddConnProfile(&ConnProfile{Name: "b", + Type: CONN_TYPE_MTECH_LORA_OIC}); err != nil { + t.Fatalf("AddConnProfile: %v", err) + } + if err := cpm.AddConnProfile(&ConnProfile{Name: "a", + Type: CONN_TYPE_SERIAL_OIC, ConnString: "dev=/dev/ttyACM0"}); err != nil { + t.Fatalf("AddConnProfile: %v", err) + } + + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(b) != goldenProfiles { + t.Errorf("file contents:\n%s\nwant:\n%s", b, goldenProfiles) + } + + // The golden file must also load back. + if err := os.WriteFile(path, []byte(goldenProfiles), 0644); err != nil { + t.Fatal(err) + } + cpm2, err := NewConnProfileMgr() + if err != nil { + t.Fatalf("reload: %v", err) + } + p, err := cpm2.GetConnProfile("b") + if err != nil || p.Type != CONN_TYPE_MTECH_LORA_OIC { + t.Errorf("GetConnProfile(b) = %+v, %v", p, err) + } +} + +func TestSortConnProfs(t *testing.T) { + in := []*ConnProfile{{Name: "c"}, {Name: "a"}, {Name: "b"}} + out := SortConnProfs(in) + if out[0].Name != "a" || out[1].Name != "b" || out[2].Name != "c" { + t.Errorf("unexpected order: %v", out) + } + if in[0].Name != "c" { + t.Errorf("input slice was modified") + } +} diff --git a/newtmgr/config/mtech_lora_config_test.go b/newtmgr/config/mtech_lora_config_test.go new file mode 100644 index 0000000..a003860 --- /dev/null +++ b/newtmgr/config/mtech_lora_config_test.go @@ -0,0 +1,140 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + + "mynewt.apache.org/newtmgr/nmxact/lora" + "mynewt.apache.org/newtmgr/nmxact/mtech_lora" + "mynewt.apache.org/newtmgr/nmxact/sesn" +) + +func TestParseMtechLoraConnString(t *testing.T) { + def := mtech_lora.LoraConfig{Port: lora.COAP_LORA_PORT} + + tests := []struct { + name string + cs string + want mtech_lora.LoraConfig + }{ + {"empty", "", def}, + {"addr", "addr=01:02:03:04:05:06:07:08", + mtech_lora.LoraConfig{ + Addr: "01:02:03:04:05:06:07:08", + Port: lora.COAP_LORA_PORT, + }}, + {"all keys", "addr=a=b,segsz=64,confirmedtx=true,port=10", + mtech_lora.LoraConfig{ + Addr: "a=b", + SegSz: 64, + ConfirmedTx: true, + Port: 10, + }}, + {"confirmedtx numeric", "confirmedtx=1", + mtech_lora.LoraConfig{ + ConfirmedTx: true, + Port: lora.COAP_LORA_PORT, + }}, + {"port bounds", "port=255", + mtech_lora.LoraConfig{Port: 255}}, + {"port zero", "port=0", + mtech_lora.LoraConfig{Port: 0}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc, err := ParseMtechLoraConnString(tt.cs) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if *mc != tt.want { + t.Errorf("got %+v, want %+v", *mc, tt.want) + } + }) + } +} + +func TestParseMtechLoraConnStringErrors(t *testing.T) { + tests := []struct { + cs string + err string + }{ + {"addr", + "expected comma-separated key=value pairs; no '=' in: addr"}, + {"addr=x,", + "expected comma-separated key=value pairs; no '=' in: "}, + {"segsz=big", "Invalid SegSz: big"}, + {"confirmedtx=maybe", "Invalid confirmedtx: maybe"}, + {"port=256", "Invalid port number: 256"}, + {"port=-1", "Invalid port number: -1"}, + {"port=x", "Invalid port number: x"}, + {"foo=1", "Unrecognized key: foo"}, + } + + for _, tt := range tests { + t.Run(tt.cs, func(t *testing.T) { + _, err := ParseMtechLoraConnString(tt.cs) + if err == nil { + t.Fatalf("expected error") + } + if err.Error() != tt.err { + t.Errorf("error = %q, want %q", err.Error(), tt.err) + } + }) + } +} + +func TestFillMtechLoraSesnCfg(t *testing.T) { + mc := &mtech_lora.LoraConfig{ + Addr: "aa", + SegSz: 32, + ConfirmedTx: true, + Port: 7, + } + + t.Run("from config", func(t *testing.T) { + setDeviceName(t, "") + var sc sesn.SesnCfg + if err := FillMtechLoraSesnCfg(mc, &sc); err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := sesn.SesnCfgLora{ + Addr: "aa", SegSz: 32, ConfirmedTx: true, Port: 7, + } + if sc.Lora != want { + t.Errorf("got %+v, want %+v", sc.Lora, want) + } + }) + + t.Run("device name overrides addr", func(t *testing.T) { + setDeviceName(t, "bb") + var sc sesn.SesnCfg + if err := FillMtechLoraSesnCfg(mc, &sc); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sc.Lora.Addr != "bb" { + t.Errorf("Addr = %q, want \"bb\"", sc.Lora.Addr) + } + if mc.Addr != "aa" { + t.Errorf("config modified: Addr = %q", mc.Addr) + } + }) +} diff --git a/newtmgr/config/serial_config_test.go b/newtmgr/config/serial_config_test.go new file mode 100644 index 0000000..ff713be --- /dev/null +++ b/newtmgr/config/serial_config_test.go @@ -0,0 +1,100 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + "time" +) + +func TestParseSerialConnString(t *testing.T) { + setTimeout(t, 3.5) + + tests := []struct { + name string + cs string + dev string + baud int + mtu int + }{ + // Defaults: baud 115200 (set by the parser), mtu 128 (from + // nmserial.NewXportCfg). + {"empty", "", "", 115200, 128}, + {"old style dev only", "/dev/ttyUSB0", "/dev/ttyUSB0", 115200, 128}, + {"dev key", "dev=/dev/ttyACM0", "/dev/ttyACM0", 115200, 128}, + {"windows dev", "COM3", "COM3", 115200, 128}, + {"all keys", "dev=/dev/x,baud=9600,mtu=256", "/dev/x", 9600, 256}, + {"order independent", "mtu=128,baud=1000000,dev=/dev/y", + "/dev/y", 1000000, 128}, + {"value with equals", "dev=/dev/a=b", "/dev/a=b", 115200, 128}, + {"last key wins", "baud=1,baud=2", "", 2, 128}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc, err := ParseSerialConnString(tt.cs) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sc.DevPath != tt.dev { + t.Errorf("DevPath = %q, want %q", sc.DevPath, tt.dev) + } + if sc.Baud != tt.baud { + t.Errorf("Baud = %d, want %d", sc.Baud, tt.baud) + } + if sc.Mtu != tt.mtu { + t.Errorf("Mtu = %d, want %d", sc.Mtu, tt.mtu) + } + if sc.ReadTimeout != 3500*time.Millisecond { + t.Errorf("ReadTimeout = %v, want 3.5s", sc.ReadTimeout) + } + }) + } +} + +func TestParseSerialConnStringErrors(t *testing.T) { + tests := []struct { + cs string + err string + }{ + {"baud=fast", + "Invalid serial connstring; Invalid baud: fast"}, + {"baud=", + "Invalid serial connstring; Invalid baud: "}, + {"mtu=big", + "Invalid serial connstring; Invalid mtu: big"}, + {"dev=/dev/x,speed=9600", + "Invalid serial connstring; Unrecognized key: speed"}, + {"=foo", + "Invalid serial connstring; Unrecognized key: "}, + } + + for _, tt := range tests { + t.Run(tt.cs, func(t *testing.T) { + _, err := ParseSerialConnString(tt.cs) + if err == nil { + t.Fatalf("expected error") + } + if err.Error() != tt.err { + t.Errorf("error = %q, want %q", err.Error(), tt.err) + } + }) + } +} diff --git a/newtmgr/config/util_test.go b/newtmgr/config/util_test.go new file mode 100644 index 0000000..4a0fe94 --- /dev/null +++ b/newtmgr/config/util_test.go @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + + "mynewt.apache.org/newtmgr/newtmgr/nmutil" +) + +// setTimeout overrides nmutil.Timeout for the duration of a test. +func setTimeout(t *testing.T, secs float64) { + t.Helper() + old := nmutil.Timeout + nmutil.Timeout = secs + t.Cleanup(func() { nmutil.Timeout = old }) +} + +func setHciIdx(t *testing.T, idx int) { + t.Helper() + old := nmutil.HciIdx + nmutil.HciIdx = idx + t.Cleanup(func() { nmutil.HciIdx = old }) +} + +func setDeviceName(t *testing.T, name string) { + t.Helper() + old := nmutil.DeviceName + nmutil.DeviceName = name + t.Cleanup(func() { nmutil.DeviceName = old }) +} + +func setBleWriteRsp(t *testing.T, v bool) { + t.Helper() + old := nmutil.BleWriteRsp + nmutil.BleWriteRsp = v + t.Cleanup(func() { nmutil.BleWriteRsp = old }) +}
