You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by ja...@apache.org on 2026/10/23 22:02:06 UTC
(mynewt-newtmgr) branch master updated (3e852ce -> d512da4)
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git
from 3e852ce ci: Add Dependabot configuration
new 11f4793 nmxact/nmp: Add unit tests for codec and reassembly
new 37ca926 nmxact/omp: Add unit tests for OMP encoding and decoding
new 26b4cf3 nmxact/nmcoap: Add unit tests for reassembly, listeners and messages
new d4a9509 nmxact/nmcoap: Fix loss of back-to-back CoAP-TCP messages
new 48a9922 nmxact/nmxutil: Add unit tests for pure helpers
new 6f67cee nmxact/nmxutil: Fix Fragment hang for non-positive MTU
new bdefc99 nmxact/bledefs: Add unit tests for addresses, UUIDs and enums
new 8faec83 nmxact/nmble: Add unit tests for protocol and utility helpers
new e816408 nmxact/nmble: Fix ErrCodeToString for ATT and SM error codes
new ec4fb8e nmxact/mtech_lora: Add unit tests for listeners and address helpers
new 92c4de9 nmxact/mtech_lora: Fix NormalizeAddr rejecting colon separated addrs
new e17ed97 newtmgr/config: Add unit tests for connstring and profile handling
new 32f5262 newtmgr/config: Apply --hci index with an empty bll connstring
new 7793220 newtmgr/cli: Add unit tests for res, log and image helpers
new 6ce79d2 newtmgr/cli: Fix panic on a lone double quote resource value
new f698e60 newtmgr/cli: Fix panic in conn add on a variable without '='
new b92f1ad newtmgr/core: Add unit tests for corefile to ELF conversion
new e2365e1 newtmgr/core: Fix panic converting a corefile without segments
new 9620557 newtmgr/bll: Add unit tests for MTU exchange and UUID helpers
new d512da4 newtmgr/nmutil: Add unit tests for ErrorCausedBy and TxOptions
The 20 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
newtmgr/bll/bll_test.go | 239 +++++++++++
newtmgr/cli/connprofile.go | 43 +-
newtmgr/cli/connprofile_test.go | 89 ++++
nmxact/mgmt/mgmt.go => newtmgr/cli/image_test.go | 40 +-
newtmgr/cli/log_test.go | 123 ++++++
newtmgr/cli/res.go | 3 +-
newtmgr/cli/res_test.go | 432 +++++++++++++++++++
newtmgr/config/ble_config_test.go | 132 ++++++
newtmgr/config/bll_config.go | 3 +-
newtmgr/config/bll_config_test.go | 213 ++++++++++
newtmgr/config/connprofile_test.go | 355 ++++++++++++++++
newtmgr/config/mtech_lora_config_test.go | 140 +++++++
newtmgr/config/serial_config_test.go | 100 +++++
newtmgr/{cli/reset.go => config/util_test.go} | 51 ++-
newtmgr/core/core_convert.go | 2 +-
newtmgr/core/core_convert_test.go | 353 ++++++++++++++++
newtmgr/nmutil/nmutil_test.go | 71 ++++
nmxact/bledefs/bledefs_test.go | 499 ++++++++++++++++++++++
nmxact/mtech_lora/listen_test.go | 208 ++++++++++
nmxact/mtech_lora/mtech_lora_xport.go | 2 +-
nmxact/mtech_lora/mtech_lora_xport_test.go | 88 ++++
nmxact/nmble/ble_proto.go | 6 +
nmxact/nmble/ble_proto_test.go | 326 +++++++++++++++
nmxact/nmble/ble_util_test.go | 501 +++++++++++++++++++++++
nmxact/nmcoap/dispatch.go | 16 +-
nmxact/nmcoap/dispatch_test.go | 65 +++
nmxact/nmcoap/frag.go | 5 +-
nmxact/nmcoap/frag_test.go | 211 ++++++++++
nmxact/nmcoap/listener_test.go | 200 +++++++++
nmxact/nmcoap/nmcoap_test.go | 251 ++++++++++++
nmxact/nmcoap/receiver.go | 15 +
nmxact/nmp/decode_test.go | 240 +++++++++++
nmxact/nmp/frag_test.go | 121 ++++++
nmxact/nmp/log_test.go | 174 ++++++++
nmxact/nmp/nmp_test.go | 401 ++++++++++++++++++
nmxact/nmxutil/nmxerr_test.go | 134 ++++++
nmxact/nmxutil/nmxutil.go | 9 +
nmxact/nmxutil/nmxutil_test.go | 190 +++++++++
nmxact/omp/omp_test.go | 456 +++++++++++++++++++++
39 files changed, 6439 insertions(+), 68 deletions(-)
create mode 100644 newtmgr/bll/bll_test.go
create mode 100644 newtmgr/cli/connprofile_test.go
copy nmxact/mgmt/mgmt.go => newtmgr/cli/image_test.go (54%)
create mode 100644 newtmgr/cli/log_test.go
create mode 100644 newtmgr/cli/res_test.go
create mode 100644 newtmgr/config/ble_config_test.go
create mode 100644 newtmgr/config/bll_config_test.go
create mode 100644 newtmgr/config/connprofile_test.go
create mode 100644 newtmgr/config/mtech_lora_config_test.go
create mode 100644 newtmgr/config/serial_config_test.go
copy newtmgr/{cli/reset.go => config/util_test.go} (55%)
create mode 100644 newtmgr/core/core_convert_test.go
create mode 100644 newtmgr/nmutil/nmutil_test.go
create mode 100644 nmxact/bledefs/bledefs_test.go
create mode 100644 nmxact/mtech_lora/listen_test.go
create mode 100644 nmxact/mtech_lora/mtech_lora_xport_test.go
create mode 100644 nmxact/nmble/ble_proto_test.go
create mode 100644 nmxact/nmble/ble_util_test.go
create mode 100644 nmxact/nmcoap/dispatch_test.go
create mode 100644 nmxact/nmcoap/frag_test.go
create mode 100644 nmxact/nmcoap/listener_test.go
create mode 100644 nmxact/nmcoap/nmcoap_test.go
create mode 100644 nmxact/nmp/decode_test.go
create mode 100644 nmxact/nmp/frag_test.go
create mode 100644 nmxact/nmp/log_test.go
create mode 100644 nmxact/nmp/nmp_test.go
create mode 100644 nmxact/nmxutil/nmxerr_test.go
create mode 100644 nmxact/nmxutil/nmxutil_test.go
create mode 100644 nmxact/omp/omp_test.go
(mynewt-newtmgr) 09/20: nmxact/nmble: Fix ErrCodeToString for ATT and SM error codes
Posted by ja...@apache.org.
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 e81640820e32c47119b6a9c43e202dcdb6ae0487
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:29:21 2026 +0200
nmxact/nmble: Fix ErrCodeToString for ATT and SM error codes
ErrCodeToString had empty cases for the ATT, SM (local) and SM (peer)
error code ranges, so every such status was reported as "unknown"
even though AttErrCodeStringMap and SmErrCodeStringMap define names
for them. Users saw e.g. "indicates error: unknown (261)" instead of
"insufficient authentication" when a GATT operation or pairing
failed.
Look the codes up in the corresponding maps. L2CAP codes still have
no names and are reported as "unknown" as before.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmble/ble_proto.go | 6 ++++++
nmxact/nmble/ble_proto_test.go | 24 ++++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/nmxact/nmble/ble_proto.go b/nmxact/nmble/ble_proto.go
index 37243ae..ca8c3d0 100644
--- a/nmxact/nmble/ble_proto.go
+++ b/nmxact/nmble/ble_proto.go
@@ -1463,12 +1463,18 @@ func ErrCodeToString(e int) string {
switch {
case e >= ERR_CODE_SM_PEER_BASE:
+ s = SmErrCodeStringMap[e-ERR_CODE_SM_PEER_BASE]
+
case e >= ERR_CODE_SM_US_BASE:
+ s = SmErrCodeStringMap[e-ERR_CODE_SM_US_BASE]
+
case e >= ERR_CODE_L2C_BASE:
case e >= ERR_CODE_HCI_BASE:
s = HciErrCodeStringMap[e-ERR_CODE_HCI_BASE]
case e >= ERR_CODE_ATT_BASE:
+ s = AttErrCodeStringMap[e-ERR_CODE_ATT_BASE]
+
default:
s = ErrCodeStringMap[e]
}
diff --git a/nmxact/nmble/ble_proto_test.go b/nmxact/nmble/ble_proto_test.go
index 0da845b..b6b6a17 100644
--- a/nmxact/nmble/ble_proto_test.go
+++ b/nmxact/nmble/ble_proto_test.go
@@ -279,6 +279,14 @@ func TestErrCodeToString(t *testing.T) {
HciErrCodeStringMap[ERR_CODE_HCI_REM_USER_CONN_TERM]},
{ERR_CODE_HCI_BASE + 0xfe, "unknown"},
{ERR_CODE_L2C_BASE + 1, "unknown"},
+ {ERR_CODE_ATT_BASE + ERR_CODE_ATT_INSUFFICIENT_ENC,
+ "insufficient encryption"},
+ {ERR_CODE_ATT_BASE, "unknown"},
+ {ERR_CODE_ATT_BASE + 0xfe, "unknown"},
+ {ERR_CODE_SM_US_BASE + ERR_CODE_SM_ERR_PASSKEY, "passkey"},
+ {ERR_CODE_SM_PEER_BASE + ERR_CODE_SM_ERR_DHKEY,
+ "dhkey check failed"},
+ {ERR_CODE_SM_PEER_BASE + 0x100, "unknown"},
}
for _, tt := range tests {
@@ -293,6 +301,22 @@ func TestErrCodeToString(t *testing.T) {
t.Errorf("ErrCodeToString(%d) = %q, want %q", code, got, name)
}
}
+ for code, name := range AttErrCodeStringMap {
+ if got := ErrCodeToString(ERR_CODE_ATT_BASE + code); got != name {
+ t.Errorf("ErrCodeToString(att %d) = %q, want %q", code, got,
+ name)
+ }
+ }
+ for code, name := range SmErrCodeStringMap {
+ for _, base := range []int{ERR_CODE_SM_US_BASE,
+ ERR_CODE_SM_PEER_BASE} {
+
+ if got := ErrCodeToString(base + code); got != name {
+ t.Errorf("ErrCodeToString(%#x) = %q, want %q", base+code,
+ got, name)
+ }
+ }
+ }
for code, name := range HciErrCodeStringMap {
if got := ErrCodeToString(ERR_CODE_HCI_BASE + code); got != name {
t.Errorf("ErrCodeToString(hci %d) = %q, want %q", code, got,
(mynewt-newtmgr) 06/20: nmxact/nmxutil: Fix Fragment hang for non-positive MTU
Posted by ja...@apache.org.
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 6f67ceebee820e159ec3b99fbf0fe37c1da93c3b
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:25:48 2026 +0200
nmxact/nmxutil: Fix Fragment hang for non-positive MTU
Fragment advanced its offset by mtu, so an mtu of 0 never terminated
and kept appending empty fragments until memory ran out, and a
negative mtu panicked with an out of range slice. The mtu comes from
Sesn.MtuOut(), which can be 0 or negative, e.g. a BLE session whose
ATT MTU is not known yet (AttMtu - 3) or a serial session configured
with a very small MTU, so a request could hang newtmgr forever.
Return the whole buffer as a single fragment when mtu <= 0. For
datagram transports the transceiver already rejects such a request
as "Request too big" before fragmenting; elsewhere the buffer is
handed to the transport unfragmented, which can report an error,
instead of hanging. An empty buffer still yields no fragments.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmxutil/nmxutil.go | 9 +++++++++
nmxact/nmxutil/nmxutil_test.go | 5 +++++
2 files changed, 14 insertions(+)
diff --git a/nmxact/nmxutil/nmxutil.go b/nmxact/nmxutil/nmxutil.go
index e917c34..fa25112 100644
--- a/nmxact/nmxutil/nmxutil.go
+++ b/nmxact/nmxutil/nmxutil.go
@@ -158,6 +158,15 @@ func StopAndDrainTimer(timer *time.Timer) {
func Fragment(b []byte, mtu int) [][]byte {
frags := [][]byte{}
+ // Without a usable MTU, do not fragment at all. An mtu of 0 used to
+ // loop forever and a negative one used to panic.
+ if mtu <= 0 {
+ if len(b) > 0 {
+ frags = append(frags, b)
+ }
+ return frags
+ }
+
for off := 0; off < len(b); off += mtu {
fragEnd := off + mtu
if fragEnd > len(b) {
diff --git a/nmxact/nmxutil/nmxutil_test.go b/nmxact/nmxutil/nmxutil_test.go
index 13967cd..49526a4 100644
--- a/nmxact/nmxutil/nmxutil_test.go
+++ b/nmxact/nmxutil/nmxutil_test.go
@@ -44,6 +44,11 @@ func TestFragment(t *testing.T) {
[][]byte{{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}}},
{"exact", data, 10, [][]byte{data}},
{"larger mtu", data, 100, [][]byte{data}},
+
+ // No usable MTU: the buffer is not fragmented.
+ {"mtu 0", data, 0, [][]byte{data}},
+ {"negative mtu", data, -5, [][]byte{data}},
+ {"mtu 0 empty", []byte{}, 0, [][]byte{}},
}
for _, tt := range tests {
(mynewt-newtmgr) 20/20: newtmgr/nmutil: Add unit tests for ErrorCausedBy and TxOptions
Posted by ja...@apache.org.
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 d512da4476894e9b8bb4207bf470c600d0aa3162
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:26:00 2026 +0200
newtmgr/nmutil: Add unit tests for ErrorCausedBy and TxOptions
Check that ErrorCausedBy() matches an error itself and the root cause
of errors wrapped with github.com/pkg/errors, and that TxOptions()
converts the global timeout in seconds to a time.Duration.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/nmutil/nmutil_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/newtmgr/nmutil/nmutil_test.go b/newtmgr/nmutil/nmutil_test.go
new file mode 100644
index 0000000..ed3494a
--- /dev/null
+++ b/newtmgr/nmutil/nmutil_test.go
@@ -0,0 +1,71 @@
+/**
+ * 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 nmutil
+
+import (
+ "testing"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+func TestErrorCausedBy(t *testing.T) {
+ root := errors.New("root")
+ other := errors.New("other")
+
+ tests := []struct {
+ name string
+ err error
+ cause error
+ want bool
+ }{
+ {"same", root, root, true},
+ {"nil both", nil, nil, true},
+ {"nil err", nil, root, false},
+ {"unrelated", other, root, false},
+ {"wrapped", errors.Wrap(root, "ctx"), root, true},
+ {"wrapped twice", errors.Wrap(errors.WithMessage(root, "a"), "b"),
+ root, true},
+ {"wrapped other", errors.Wrap(other, "ctx"), root, false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ErrorCausedBy(tt.err, tt.cause); got != tt.want {
+ t.Errorf("ErrorCausedBy = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestTxOptions(t *testing.T) {
+ oldTmo, oldTries := Timeout, Tries
+ t.Cleanup(func() { Timeout, Tries = oldTmo, oldTries })
+
+ Timeout = 2.5
+ Tries = 3
+ opt := TxOptions()
+ if opt.Timeout != 2500*time.Millisecond {
+ t.Errorf("Timeout = %v, want 2.5s", opt.Timeout)
+ }
+ if opt.Tries != 3 {
+ t.Errorf("Tries = %d, want 3", opt.Tries)
+ }
+}
(mynewt-newtmgr) 03/20: nmxact/nmcoap: Add unit tests for reassembly, listeners and messages
Posted by ja...@apache.org.
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 26b4cf3f61cd25b1334fa481de0439bb7e14a858
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:23:21 2026 +0200
nmxact/nmcoap: Add unit tests for reassembly, listeners and messages
Cover the CoAP-TCP Reassembler (single, fragmented with extended
length, recovery after an invalid message), the Receiver for both UDP
and TCP, listener criteria comparison, matching (including the
nil/empty token equivalence from "treat 0-length and nil tokens as
equal") and sort order, ParseOp, ObserveCode.Spec and CreateMsg (path,
URI query split, observe option, token, TCP vs datagram).
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmcoap/frag_test.go | 161 ++++++++++++++++++++++++++
nmxact/nmcoap/listener_test.go | 200 ++++++++++++++++++++++++++++++++
nmxact/nmcoap/nmcoap_test.go | 251 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 612 insertions(+)
diff --git a/nmxact/nmcoap/frag_test.go b/nmxact/nmcoap/frag_test.go
new file mode 100644
index 0000000..7a3a3ff
--- /dev/null
+++ b/nmxact/nmcoap/frag_test.go
@@ -0,0 +1,161 @@
+/**
+ * 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 nmcoap
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/runtimeco/go-coap"
+)
+
+// Invalid CoAP-TCP message: 1-byte body containing a reserved option
+// delta (15) that is not the payload marker.
+var garbageTcp = []byte{0x10, 0x00, 0xf0}
+
+func tcpMsg(t *testing.T, token []byte, path string,
+ payload []byte) []byte {
+
+ t.Helper()
+
+ m := coap.NewTcpMessage(coap.MessageParams{
+ Type: coap.Confirmable,
+ Code: coap.Content,
+ Token: token,
+ Payload: payload,
+ })
+ if path != "" {
+ m.SetPathString(path)
+ }
+
+ b, err := m.MarshalBinary()
+ if err != nil {
+ t.Fatalf("failed to encode CoAP: %v", err)
+ }
+ return b
+}
+
+func checkTcpMsg(t *testing.T, m *coap.TcpMessage, token []byte,
+ payload []byte) {
+
+ t.Helper()
+
+ if m == nil {
+ t.Fatalf("expected a message, got nil")
+ }
+ if !bytes.Equal(m.Token(), token) {
+ t.Fatalf("token = % x, want % x", m.Token(), token)
+ }
+ if !bytes.Equal(m.Payload(), payload) {
+ t.Fatalf("payload = % x, want % x", m.Payload(), payload)
+ }
+}
+
+func TestReassemblerSingle(t *testing.T) {
+ pl := []byte("hello world")
+ b := tcpMsg(t, []byte{1, 2}, "omgr", pl)
+
+ r := NewReassembler()
+ m := r.RxFrag(b)
+ checkTcpMsg(t, m, []byte{1, 2}, pl)
+ if m.PathString() != "omgr" {
+ t.Fatalf("path = %q, want omgr", m.PathString())
+ }
+}
+
+func TestReassemblerFragmented(t *testing.T) {
+ // Large enough to use the extended length field.
+ pl := bytes.Repeat([]byte{0x5a}, 300)
+ b := tcpMsg(t, []byte{7}, "omgr", pl)
+
+ for _, fragSz := range []int{1, 2, 7, 20, len(b) - 1} {
+ r := NewReassembler()
+ var m *coap.TcpMessage
+ for off := 0; off < len(b); off += fragSz {
+ end := off + fragSz
+ if end > len(b) {
+ end = len(b)
+ }
+
+ m = r.RxFrag(b[off:end])
+ if end < len(b) && m != nil {
+ t.Fatalf("fragSz=%d: message returned early at off %d",
+ fragSz, off)
+ }
+ }
+ checkTcpMsg(t, m, []byte{7}, pl)
+ }
+}
+
+func TestReassemblerGarbageRecovery(t *testing.T) {
+ r := NewReassembler()
+ if m := r.RxFrag(garbageTcp); m != nil {
+ t.Fatalf("garbage produced a message: %v", m)
+ }
+
+ // The invalid data must have been dropped; the next valid message is
+ // received normally.
+ pl := []byte{1, 2, 3}
+ m := r.RxFrag(tcpMsg(t, []byte{9}, "", pl))
+ checkTcpMsg(t, m, []byte{9}, pl)
+}
+
+func TestReceiverUdp(t *testing.T) {
+ rx := NewReceiver(false)
+
+ m := coap.NewDgramMessage(coap.MessageParams{
+ Type: coap.Confirmable,
+ Code: coap.Changed,
+ MessageID: 0x1234,
+ Token: []byte{4},
+ Payload: []byte("x"),
+ })
+ b, err := m.MarshalBinary()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got := rx.Rx(b)
+ if got == nil {
+ t.Fatalf("expected message")
+ }
+ if got.Code() != coap.Changed || got.MessageID() != 0x1234 ||
+ !bytes.Equal(got.Token(), []byte{4}) {
+
+ t.Fatalf("unexpected message %+v", got)
+ }
+
+ if got := rx.Rx([]byte{0x00}); got != nil {
+ t.Fatalf("garbage produced a message: %v", got)
+ }
+}
+
+func TestReceiverTcp(t *testing.T) {
+ rx := NewReceiver(true)
+ b := tcpMsg(t, []byte{3}, "a/b", []byte("p"))
+
+ if got := rx.Rx(b[:2]); got != nil {
+ t.Fatalf("partial message returned: %v", got)
+ }
+ got := rx.Rx(b[2:])
+ if got == nil || got.PathString() != "a/b" {
+ t.Fatalf("unexpected message %v", got)
+ }
+}
diff --git a/nmxact/nmcoap/listener_test.go b/nmxact/nmcoap/listener_test.go
new file mode 100644
index 0000000..e815540
--- /dev/null
+++ b/nmxact/nmcoap/listener_test.go
@@ -0,0 +1,200 @@
+/**
+ * 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 nmcoap
+
+import (
+ "testing"
+ "time"
+
+ "github.com/runtimeco/go-coap"
+)
+
+func sign(i int) int {
+ switch {
+ case i < 0:
+ return -1
+ case i > 0:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func TestCompareMsgCriteria(t *testing.T) {
+ tests := []struct {
+ a, b MsgCriteria
+ want int
+ }{
+ {MsgCriteria{}, MsgCriteria{}, 0},
+ {MsgCriteria{Path: "a"}, MsgCriteria{Path: "a"}, 0},
+ {MsgCriteria{Path: "a"}, MsgCriteria{Path: "b"}, -1},
+ {MsgCriteria{Path: "b"}, MsgCriteria{Path: "a"}, 1},
+ {MsgCriteria{Path: ""}, MsgCriteria{Path: "a"}, -1},
+
+ // Path is the primary key.
+ {MsgCriteria{Path: "a", Token: []byte{9}},
+ MsgCriteria{Path: "b", Token: []byte{1}}, -1},
+
+ {MsgCriteria{Token: []byte{1}}, MsgCriteria{Token: []byte{1}}, 0},
+ {MsgCriteria{Token: []byte{1}}, MsgCriteria{Token: []byte{2}}, -1},
+ {MsgCriteria{Token: []byte{2}}, MsgCriteria{Token: []byte{1}}, 1},
+ {MsgCriteria{Token: []byte{1}}, MsgCriteria{Token: []byte{1, 0}},
+ -1},
+
+ // A nil token is a wildcard and sorts before any concrete token,
+ // including an empty one.
+ {MsgCriteria{Token: nil}, MsgCriteria{Token: []byte{1}}, -1},
+ {MsgCriteria{Token: []byte{1}}, MsgCriteria{Token: nil}, 1},
+ {MsgCriteria{Token: nil}, MsgCriteria{Token: []byte{}}, -1},
+ {MsgCriteria{Token: []byte{}}, MsgCriteria{Token: []byte{}}, 0},
+ }
+
+ for _, tt := range tests {
+ got := sign(CompareMsgCriteria(tt.a, tt.b))
+ if got != tt.want {
+ t.Errorf("Compare(%s, %s) = %d, want %d",
+ tt.a.String(), tt.b.String(), got, tt.want)
+ }
+ }
+}
+
+func TestMatchMsgCriteria(t *testing.T) {
+ tests := []struct {
+ name string
+ listen MsgCriteria
+ msg MsgCriteria
+ expected bool
+ }{
+ {"wildcard", MsgCriteria{},
+ MsgCriteria{Path: "x", Token: []byte{1}}, true},
+ {"path match", MsgCriteria{Path: "x"},
+ MsgCriteria{Path: "x", Token: []byte{1}}, true},
+ {"path mismatch", MsgCriteria{Path: "x"},
+ MsgCriteria{Path: "y"}, false},
+ {"token match", MsgCriteria{Token: []byte{1, 2}},
+ MsgCriteria{Path: "x", Token: []byte{1, 2}}, true},
+ {"token mismatch", MsgCriteria{Token: []byte{1, 2}},
+ MsgCriteria{Token: []byte{1, 3}}, false},
+ {"token missing", MsgCriteria{Token: []byte{1}},
+ MsgCriteria{}, false},
+ {"path and token", MsgCriteria{Path: "x", Token: []byte{1}},
+ MsgCriteria{Path: "x", Token: []byte{1}}, true},
+ {"path ok token bad", MsgCriteria{Path: "x", Token: []byte{1}},
+ MsgCriteria{Path: "x", Token: []byte{2}}, false},
+
+ // A 0-length token and a nil token are equal ("nmxact: treat
+ // 0-length and nil tokens as equal").
+ {"empty listener nil msg", MsgCriteria{Token: []byte{}},
+ MsgCriteria{Token: nil}, true},
+ {"empty listener empty msg", MsgCriteria{Token: []byte{}},
+ MsgCriteria{Token: []byte{}}, true},
+ {"empty listener token msg", MsgCriteria{Token: []byte{}},
+ MsgCriteria{Token: []byte{1}}, false},
+ }
+
+ for _, tt := range tests {
+ if got := MatchMsgCriteria(tt.listen, tt.msg); got != tt.expected {
+ t.Errorf("%s: Match(%s, %s) = %v, want %v", tt.name,
+ tt.listen.String(), tt.msg.String(), got, tt.expected)
+ }
+ }
+}
+
+func TestSortListeners(t *testing.T) {
+ wild := NewListener(MsgCriteria{})
+ pathA := NewListener(MsgCriteria{Path: "a"})
+ pathATok1 := NewListener(MsgCriteria{Path: "a", Token: []byte{1}})
+ pathATok2 := NewListener(MsgCriteria{Path: "a", Token: []byte{2}})
+ pathB := NewListener(MsgCriteria{Path: "b"})
+ tok1 := NewListener(MsgCriteria{Token: []byte{1}})
+
+ ls := []*Listener{wild, pathATok1, tok1, pathB, pathA, pathATok2}
+ SortListeners(ls)
+
+ want := []*Listener{pathB, pathATok2, pathATok1, pathA, tok1, wild}
+ for i := range want {
+ if ls[i] != want[i] {
+ for j, l := range ls {
+ t.Logf("[%d] %s", j, l.Criteria.String())
+ }
+ t.Fatalf("index %d: got %s, want %s", i,
+ ls[i].Criteria.String(), want[i].Criteria.String())
+ }
+ }
+
+ // After sorting, the first matching listener is the most specific one.
+ msg := MsgCriteria{Path: "a", Token: []byte{1}}
+ for _, l := range ls {
+ if MatchMsgCriteria(l.Criteria, msg) {
+ if l != pathATok1 {
+ t.Fatalf("first match = %s, want %s",
+ l.Criteria.String(), pathATok1.Criteria.String())
+ }
+ break
+ }
+ }
+}
+
+func TestMsgCriteriaString(t *testing.T) {
+ tests := []struct {
+ mc MsgCriteria
+ want string
+ }{
+ {MsgCriteria{}, "token=nil path=nil"},
+ {MsgCriteria{Token: []byte{0xab, 0x01}, Path: "omgr"},
+ "token=ab01 path=omgr"},
+ {MsgCriteria{Token: []byte{}}, "token= path=nil"},
+ }
+
+ for _, tt := range tests {
+ if got := tt.mc.String(); got != tt.want {
+ t.Errorf("String() = %q, want %q", got, tt.want)
+ }
+ }
+}
+
+func TestCriteriaFromMsg(t *testing.T) {
+ m := coap.NewDgramMessage(coap.MessageParams{
+ Code: coap.Content,
+ Token: []byte{5, 6},
+ })
+ m.SetPathString("/a/b")
+
+ mc := CriteriaFromMsg(m)
+ if mc.Path != "a/b" || string(mc.Token) != "\x05\x06" {
+ t.Fatalf("unexpected criteria %s", mc.String())
+ }
+}
+
+func TestListenerClose(t *testing.T) {
+ l := NewListener(MsgCriteria{})
+ tmo := l.AfterTimeout(time.Hour)
+ l.Close()
+
+ if _, ok := <-l.RspChan; ok {
+ t.Fatalf("RspChan not closed")
+ }
+ if _, ok := <-l.ErrChan; ok {
+ t.Fatalf("ErrChan not closed")
+ }
+ if _, ok := <-tmo; ok {
+ t.Fatalf("timeout chan not closed")
+ }
+}
diff --git a/nmxact/nmcoap/nmcoap_test.go b/nmxact/nmcoap/nmcoap_test.go
new file mode 100644
index 0000000..494eb2b
--- /dev/null
+++ b/nmxact/nmcoap/nmcoap_test.go
@@ -0,0 +1,251 @@
+/**
+ * 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 nmcoap
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/runtimeco/go-coap"
+)
+
+func TestParseOp(t *testing.T) {
+ tests := []struct {
+ in string
+ want coap.COAPCode
+ }{
+ {"get", coap.GET},
+ {"GET", coap.GET},
+ {"Put", coap.PUT},
+ {"post", coap.POST},
+ {"DeLeTe", coap.DELETE},
+ }
+
+ for _, tt := range tests {
+ got, err := ParseOp(tt.in)
+ if err != nil {
+ t.Errorf("ParseOp(%q): unexpected error: %v", tt.in, err)
+ continue
+ }
+ if got != tt.want {
+ t.Errorf("ParseOp(%q) = %v, want %v", tt.in, got, tt.want)
+ }
+ }
+
+ for _, bad := range []string{"", "fetch", "gett", " get"} {
+ if _, err := ParseOp(bad); err == nil {
+ t.Errorf("ParseOp(%q): expected error", bad)
+ }
+ }
+}
+
+func TestObserveCodeSpec(t *testing.T) {
+ tests := []struct {
+ o ObserveCode
+ want int
+ }{
+ {OBSERVE_NONE, -1},
+ {OBSERVE_START, 0},
+ {OBSERVE_STOP, 1},
+ {ObserveCode(42), -1},
+ }
+
+ for _, tt := range tests {
+ if got := tt.o.Spec(); got != tt.want {
+ t.Errorf("ObserveCode(%d).Spec() = %d, want %d",
+ int(tt.o), got, tt.want)
+ }
+ }
+}
+
+func TestNextMessageId(t *testing.T) {
+ a := NextMessageId()
+ b := NextMessageId()
+ if b != a+1 {
+ t.Fatalf("NextMessageId() = %d after %d, want %d", b, a, a+1)
+ }
+}
+
+func TestValidateToken(t *testing.T) {
+ for n := 0; n <= 8; n++ {
+ if err := validateToken(make([]byte, n)); err != nil {
+ t.Errorf("len=%d: unexpected error: %v", n, err)
+ }
+ }
+ if err := validateToken(make([]byte, 9)); err == nil {
+ t.Errorf("len=9: expected error")
+ }
+}
+
+// encodeDecode encodes m and parses it back with the matching transport
+// parser.
+func encodeDecode(t *testing.T, isTcp bool, m coap.Message) coap.Message {
+ t.Helper()
+
+ b, err := Encode(m)
+ if err != nil {
+ t.Fatalf("Encode(): %v", err)
+ }
+
+ if isTcp {
+ tm, rest, err := coap.PullTcp(b)
+ if err != nil || tm == nil || len(rest) != 0 {
+ t.Fatalf("PullTcp() = %v, % x, %v", tm, rest, err)
+ }
+ return tm
+ }
+
+ dm, err := coap.ParseDgramMessage(b)
+ if err != nil {
+ t.Fatalf("ParseDgramMessage(): %v", err)
+ }
+ return dm
+}
+
+func TestCreateMsg(t *testing.T) {
+ tests := []struct {
+ name string
+ mp MsgParams
+ path string
+ query []string
+ observe interface{}
+ }{
+ {
+ name: "plain",
+ mp: MsgParams{Code: coap.GET, Uri: "/a/b",
+ Token: []byte{1, 2}},
+ path: "a/b",
+ },
+ {
+ name: "query",
+ mp: MsgParams{Code: coap.PUT, Uri: "res?x=1&y=2",
+ Token: []byte{3}, Payload: []byte("pl")},
+ path: "res",
+ query: []string{"x=1&y=2"},
+ },
+ {
+ name: "question mark in query",
+ mp: MsgParams{Code: coap.POST, Uri: "res?a?b",
+ Token: []byte{4}},
+ path: "res",
+ query: []string{"a?b"},
+ },
+ {
+ name: "observe start",
+ mp: MsgParams{Code: coap.GET, Uri: "obs",
+ Observe: OBSERVE_START, Token: []byte{5}},
+ path: "obs",
+ observe: uint32(0),
+ },
+ {
+ name: "observe stop",
+ mp: MsgParams{Code: coap.GET, Uri: "obs",
+ Observe: OBSERVE_STOP, Token: []byte{6}},
+ path: "obs",
+ observe: uint32(1),
+ },
+ }
+
+ for _, tt := range tests {
+ for _, isTcp := range []bool{false, true} {
+ m, err := CreateMsg(isTcp, tt.mp)
+ if err != nil {
+ t.Fatalf("%s: unexpected error: %v", tt.name, err)
+ }
+
+ _, gotTcp := m.(*coap.TcpMessage)
+ if gotTcp != isTcp {
+ t.Fatalf("%s: got %T, isTcp=%v", tt.name, m, isTcp)
+ }
+
+ m = encodeDecode(t, isTcp, m)
+
+ if m.Code() != tt.mp.Code {
+ t.Errorf("%s: code = %v, want %v", tt.name, m.Code(),
+ tt.mp.Code)
+ }
+ if !bytes.Equal(m.Token(), tt.mp.Token) {
+ t.Errorf("%s: token = % x, want % x", tt.name, m.Token(),
+ tt.mp.Token)
+ }
+ if !bytes.Equal(m.Payload(), tt.mp.Payload) {
+ t.Errorf("%s: payload = % x, want % x", tt.name,
+ m.Payload(), tt.mp.Payload)
+ }
+ if m.PathString() != tt.path {
+ t.Errorf("%s: path = %q, want %q", tt.name,
+ m.PathString(), tt.path)
+ }
+
+ var q []string
+ for _, o := range m.Options(coap.URIQuery) {
+ q = append(q, o.(string))
+ }
+ if len(q) != len(tt.query) ||
+ (len(q) > 0 && q[0] != tt.query[0]) {
+
+ t.Errorf("%s: query = %v, want %v", tt.name, q, tt.query)
+ }
+
+ if obs := m.Option(coap.Observe); obs != tt.observe {
+ t.Errorf("%s: observe = %#v, want %#v", tt.name, obs,
+ tt.observe)
+ }
+ }
+ }
+}
+
+func TestCreateMsgDefaultToken(t *testing.T) {
+ m1, err := CreateMsg(false, MsgParams{Code: coap.GET, Uri: "a"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m2, err := CreateMsg(false, MsgParams{Code: coap.GET, Uri: "a"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if len(m1.Token()) != 1 || len(m2.Token()) != 1 {
+ t.Fatalf("tokens = % x, % x; want 1 byte each",
+ m1.Token(), m2.Token())
+ }
+ if m2.Token()[0] != m1.Token()[0]+1 {
+ t.Fatalf("tokens = % x, % x; want consecutive",
+ m1.Token(), m2.Token())
+ }
+}
+
+func TestRxFilterFunc(t *testing.T) {
+ in := coap.NewDgramMessage(coap.MessageParams{Code: coap.GET})
+ out := coap.NewDgramMessage(coap.MessageParams{Code: coap.PUT})
+
+ f := RxFilterFunc(func(m coap.Message) (coap.Message, error) {
+ if m != in {
+ t.Fatalf("filter got unexpected message")
+ }
+ return out, nil
+ })
+
+ var rf RxMsgFilter = f
+ got, err := rf.Filter(in)
+ if err != nil || got != out {
+ t.Fatalf("Filter() = %v, %v", got, err)
+ }
+}
(mynewt-newtmgr) 15/20: newtmgr/cli: Fix panic on a lone double quote resource value
Posted by ja...@apache.org.
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 6ce79d255e7161f495f6e73aa2597ebbea177079
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:23:22 2026 +0200
newtmgr/cli: Fix panic on a lone double quote resource value
extractResKv() treats a value that starts and ends with '"' as a
quoted string and strips the quotes. A value consisting of a single
'"' matched both checks, and slicing [1:0] panicked. For example
newtmgr res put /r 'a="' crashed with "slice bounds out of range"
instead of sending the string. Require at least two characters
before treating a value as quoted, so a lone quote is sent as-is.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/cli/res.go | 3 ++-
newtmgr/cli/res_test.go | 3 +++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/newtmgr/cli/res.go b/newtmgr/cli/res.go
index 7205be3..7b75e5b 100644
--- a/newtmgr/cli/res.go
+++ b/newtmgr/cli/res.go
@@ -81,7 +81,8 @@ func extractResKv(params []string) (map[string]interface{}, error) {
var val interface{}
// If value is quoted, parse it as a string.
- if strings.HasPrefix(parts[1], "\"") &&
+ if len(parts[1]) >= 2 &&
+ strings.HasPrefix(parts[1], "\"") &&
strings.HasSuffix(parts[1], "\"") {
val = parts[1][1 : len(parts[1])-1]
diff --git a/newtmgr/cli/res_test.go b/newtmgr/cli/res_test.go
index 239bdb2..bde621b 100644
--- a/newtmgr/cli/res_test.go
+++ b/newtmgr/cli/res_test.go
@@ -50,6 +50,9 @@ func TestExtractResKv(t *testing.T) {
map[string]interface{}{"a": ""}},
{"only leading quote", []string{`a="x`},
map[string]interface{}{"a": `"x`}},
+ // A lone quote is not a quoted string; it used to panic.
+ {"single quote", []string{`a="`},
+ map[string]interface{}{"a": `"`}},
{"bool true", []string{"a=true"}, map[string]interface{}{"a": true}},
{"bool mixed case", []string{"a=FaLsE"},
map[string]interface{}{"a": false}},
(mynewt-newtmgr) 11/20: nmxact/mtech_lora: Fix NormalizeAddr rejecting colon separated addrs
Posted by ja...@apache.org.
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 92c4de90c4c9bb4e13f78d74e81949eae127c479
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:30:12 2026 +0200
nmxact/mtech_lora: Fix NormalizeAddr rejecting colon separated addrs
NormalizeAddr stripped colons into a local copy but then stripped
dashes from the original argument again, discarding the first
replacement. As a result an EUI-64 given as "00:11:22:33:44:55:66:77"
(e.g. as the LoRa session address) was rejected with "Invalid addr",
and only plain or dash separated addresses worked.
Apply the second replacement to the already processed string.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/mtech_lora/mtech_lora_xport.go | 2 +-
nmxact/mtech_lora/mtech_lora_xport_test.go | 3 +++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/nmxact/mtech_lora/mtech_lora_xport.go b/nmxact/mtech_lora/mtech_lora_xport.go
index 0c7f2b9..6e2b23d 100644
--- a/nmxact/mtech_lora/mtech_lora_xport.go
+++ b/nmxact/mtech_lora/mtech_lora_xport.go
@@ -114,7 +114,7 @@ func (lx *LoraXport) minMtu() int {
func NormalizeAddr(addr string) (string, error) {
a := strings.Replace(addr, ":", "", -1)
- a = strings.Replace(addr, "-", "", -1)
+ a = strings.Replace(a, "-", "", -1)
// XXX check that there's 8 components, 2 chars each, which are in [0-9,a-f]
if len(a) != 16 {
return "", fmt.Errorf("Invalid addr")
diff --git a/nmxact/mtech_lora/mtech_lora_xport_test.go b/nmxact/mtech_lora/mtech_lora_xport_test.go
index 7300e73..8fcfd2e 100644
--- a/nmxact/mtech_lora/mtech_lora_xport_test.go
+++ b/nmxact/mtech_lora/mtech_lora_xport_test.go
@@ -30,6 +30,8 @@ func TestNormalizeAddr(t *testing.T) {
}{
{"0011223344556677", "0011223344556677"},
{"00-11-22-33-44-55-66-77", "0011223344556677"},
+ {"00:11:22:33:44:55:66:77", "0011223344556677"},
+ {"00:11-22:33-44:55-66:77", "0011223344556677"},
}
for _, tt := range tests {
@@ -48,6 +50,7 @@ func TestNormalizeAddr(t *testing.T) {
"00112233445566",
"001122334455667788",
"00-11-22-33-44-55-66",
+ "00:11:22:33:44:55:66",
}
for _, s := range bad {
if got, err := NormalizeAddr(s); err == nil {
(mynewt-newtmgr) 08/20: nmxact/nmble: Add unit tests for protocol and utility helpers
Posted by ja...@apache.org.
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 8faec838093fcf999811745fac3d6bb8dd833a45
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:29:00 2026 +0200
nmxact/nmble: Add unit tests for protocol and utility helpers
Cover the blehostd JSON conversions in ble_proto.go (MsgOp and MsgType
round trips over every map entry, BleHdr, BleBytes encoding and
decoding including empty, upper case and invalid input), the
ErrCodeTo* range helpers and ErrCodeToString for base and HCI codes.
For ble_util.go, cover NextSeq wrap-around before the event range,
the request constructors, StatusError, BhdTimeoutError,
BleAdvReportFromScanEvt, BleAdvFieldsToReq, BleSvcToAddSvc,
BuildMgmtChrs, IsSecErr and ToSecurityErr.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmble/ble_proto_test.go | 302 +++++++++++++++++++++++++
nmxact/nmble/ble_util_test.go | 501 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 803 insertions(+)
diff --git a/nmxact/nmble/ble_proto_test.go b/nmxact/nmble/ble_proto_test.go
new file mode 100644
index 0000000..0da845b
--- /dev/null
+++ b/nmxact/nmble/ble_proto_test.go
@@ -0,0 +1,302 @@
+/**
+ * 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 nmble
+
+import (
+ "bytes"
+ "encoding/json"
+ "testing"
+)
+
+func TestMsgOpStrings(t *testing.T) {
+ for op, name := range MsgOpStringMap {
+ if got := MsgOpToString(op); got != name {
+ t.Errorf("MsgOpToString(%d) = %q, want %q", int(op), got, name)
+ }
+
+ got, err := MsgOpFromString(name)
+ if err != nil || got != op {
+ t.Errorf("MsgOpFromString(%q) = %d, %v", name, int(got), err)
+ }
+
+ b, err := json.Marshal(op)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != `"`+name+`"` {
+ t.Errorf("Marshal(%d) = %s", int(op), b)
+ }
+
+ var u MsgOp
+ if err := json.Unmarshal(b, &u); err != nil || u != op {
+ t.Errorf("Unmarshal(%s) = %d, %v", b, int(u), err)
+ }
+ }
+
+ if got := MsgOpToString(MsgOp(99)); got != "???" {
+ t.Errorf("MsgOpToString(99) = %q", got)
+ }
+ if _, err := MsgOpFromString("bogus"); err == nil {
+ t.Errorf("MsgOpFromString(bogus): expected error")
+ }
+ var u MsgOp
+ for _, bad := range []string{`"bogus"`, `1`} {
+ if err := json.Unmarshal([]byte(bad), &u); err == nil {
+ t.Errorf("Unmarshal(%s): expected error", bad)
+ }
+ }
+}
+
+func TestMsgTypeStrings(t *testing.T) {
+ seen := map[string]bool{}
+ for typ, name := range MsgTypeStringMap {
+ if seen[name] {
+ t.Errorf("duplicate MsgType name %q", name)
+ }
+ seen[name] = true
+
+ if got := MsgTypeToString(typ); got != name {
+ t.Errorf("MsgTypeToString(%d) = %q, want %q",
+ int(typ), got, name)
+ }
+
+ got, err := MsgTypeFromString(name)
+ if err != nil || got != typ {
+ t.Errorf("MsgTypeFromString(%q) = %d, %v", name, int(got), err)
+ }
+
+ b, err := json.Marshal(typ)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != `"`+name+`"` {
+ t.Errorf("Marshal(%d) = %s", int(typ), b)
+ }
+
+ var u MsgType
+ if err := json.Unmarshal(b, &u); err != nil || u != typ {
+ t.Errorf("Unmarshal(%s) = %d, %v", b, int(u), err)
+ }
+ }
+
+ if got := MsgTypeToString(MsgType(0)); got != "???" {
+ t.Errorf("MsgTypeToString(0) = %q", got)
+ }
+ if _, err := MsgTypeFromString("bogus"); err == nil {
+ t.Errorf("MsgTypeFromString(bogus): expected error")
+ }
+ var u MsgType
+ for _, bad := range []string{`"bogus"`, `3`} {
+ if err := json.Unmarshal([]byte(bad), &u); err == nil {
+ t.Errorf("Unmarshal(%s): expected error", bad)
+ }
+ }
+}
+
+func TestBleHdrJSON(t *testing.T) {
+ hdr := BleHdr{Op: MSG_OP_RSP, Type: MSG_TYPE_CONNECT, Seq: 42}
+
+ b, err := json.Marshal(&hdr)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ want := `{"op":"response","type":"connect","seq":42}`
+ if string(b) != want {
+ t.Fatalf("Marshal = %s, want %s", b, want)
+ }
+
+ var got BleHdr
+ if err := json.Unmarshal(b, &got); err != nil || got != hdr {
+ t.Fatalf("Unmarshal = %+v, %v", got, err)
+ }
+}
+
+func TestBleBytesMarshal(t *testing.T) {
+ tests := []struct {
+ in []byte
+ want string
+ }{
+ {nil, `""`},
+ {[]byte{}, `""`},
+ {[]byte{0x01}, `"0x01"`},
+ {[]byte{0x01, 0xab, 0x00, 0xff}, `"0x01:0xab:0x00:0xff"`},
+ }
+
+ for _, tt := range tests {
+ bb := BleBytes{tt.in}
+ b, err := json.Marshal(&bb)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != tt.want {
+ t.Errorf("Marshal(% x) = %s, want %s", tt.in, b, tt.want)
+ }
+ }
+}
+
+func TestBleBytesUnmarshal(t *testing.T) {
+ tests := []struct {
+ in string
+ want []byte
+ }{
+ {`""`, nil},
+ {`"0x01"`, []byte{0x01}},
+ {`"0x01:0xab"`, []byte{0x01, 0xab}},
+ {`"0x01:0xAB:0XFF"`, []byte{0x01, 0xab, 0xff}},
+ {`"0x1:0x2"`, []byte{0x01, 0x02}},
+ }
+
+ for _, tt := range tests {
+ var bb BleBytes
+ if err := json.Unmarshal([]byte(tt.in), &bb); err != nil {
+ t.Errorf("Unmarshal(%s): %v", tt.in, err)
+ continue
+ }
+ if !bytes.Equal(bb.Bytes, tt.want) {
+ t.Errorf("Unmarshal(%s) = % x, want % x", tt.in, bb.Bytes,
+ tt.want)
+ }
+ }
+
+ bad := []string{
+ `"01:ab"`,
+ `"0x01:ab"`,
+ `"0x100"`,
+ `"0xzz"`,
+ `"0x01:"`,
+ `"0x01,0x02"`,
+ `5`,
+ `[1,2]`,
+ }
+ for _, s := range bad {
+ var bb BleBytes
+ if err := json.Unmarshal([]byte(s), &bb); err == nil {
+ t.Errorf("Unmarshal(%s): expected error, got % x", s, bb.Bytes)
+ }
+ }
+}
+
+func TestBleBytesRoundTrip(t *testing.T) {
+ in := BleBytes{[]byte{0x00, 0x10, 0x7f, 0x80, 0xff}}
+ b, err := json.Marshal(&in)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+
+ var out BleBytes
+ if err := json.Unmarshal(b, &out); err != nil {
+ t.Fatalf("Unmarshal: %v", err)
+ }
+ if !bytes.Equal(in.Bytes, out.Bytes) {
+ t.Fatalf("round trip = % x, want % x", out.Bytes, in.Bytes)
+ }
+}
+
+// BleBytes fields of a request are encoded in the blehostd format when the
+// request is marshalled through a pointer.
+func TestBleBytesInMsg(t *testing.T) {
+ r := NewBleWriteCmdReq()
+ r.Seq = 1
+ r.Data = BleBytes{[]byte{0xde, 0xad}}
+
+ b, err := json.Marshal(r)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+
+ m := map[string]interface{}{}
+ if err := json.Unmarshal(b, &m); err != nil {
+ t.Fatalf("Unmarshal: %v", err)
+ }
+ if m["data"] != "0xde:0xad" || m["op"] != "request" ||
+ m["type"] != "write_cmd" {
+
+ t.Fatalf("unexpected JSON %s", b)
+ }
+}
+
+func TestErrCodeTo(t *testing.T) {
+ tests := []struct {
+ name string
+ fn func(int) int
+ base int
+ }{
+ {"att", ErrCodeToAtt, ERR_CODE_ATT_BASE},
+ {"hci", ErrCodeToHci, ERR_CODE_HCI_BASE},
+ {"l2c", ErrCodeToL2c, ERR_CODE_L2C_BASE},
+ {"sm us", ErrCodeToSmUs, ERR_CODE_SM_US_BASE},
+ {"sm peer", ErrCodeToSmPeer, ERR_CODE_SM_PEER_BASE},
+ }
+
+ for _, tt := range tests {
+ checks := []struct {
+ in int
+ want int
+ }{
+ {tt.base, 0},
+ {tt.base + 0x13, 0x13},
+ {tt.base + 0xff, 0xff},
+ {tt.base - 1, -1},
+ {tt.base + 0x100, -1},
+ {0, -1},
+ }
+ for _, c := range checks {
+ if got := tt.fn(c.in); got != c.want {
+ t.Errorf("%s(%#x) = %d, want %d", tt.name, c.in, got, c.want)
+ }
+ }
+ }
+}
+
+func TestErrCodeToString(t *testing.T) {
+ tests := []struct {
+ in int
+ want string
+ }{
+ {ERR_CODE_EAGAIN, "eagain"},
+ {ERR_CODE_ETIMEOUT, "etimeout"},
+ {0, "unknown"},
+ {0xff, "unknown"},
+ {-1, "unknown"},
+ {ERR_CODE_HCI_BASE + ERR_CODE_HCI_REM_USER_CONN_TERM,
+ HciErrCodeStringMap[ERR_CODE_HCI_REM_USER_CONN_TERM]},
+ {ERR_CODE_HCI_BASE + 0xfe, "unknown"},
+ {ERR_CODE_L2C_BASE + 1, "unknown"},
+ }
+
+ for _, tt := range tests {
+ if got := ErrCodeToString(tt.in); got != tt.want {
+ t.Errorf("ErrCodeToString(%#x) = %q, want %q", tt.in, got,
+ tt.want)
+ }
+ }
+
+ for code, name := range ErrCodeStringMap {
+ if got := ErrCodeToString(code); got != name {
+ t.Errorf("ErrCodeToString(%d) = %q, want %q", code, got, name)
+ }
+ }
+ for code, name := range HciErrCodeStringMap {
+ if got := ErrCodeToString(ERR_CODE_HCI_BASE + code); got != name {
+ t.Errorf("ErrCodeToString(hci %d) = %q, want %q", code, got,
+ name)
+ }
+ }
+}
diff --git a/nmxact/nmble/ble_util_test.go b/nmxact/nmble/ble_util_test.go
new file mode 100644
index 0000000..8cf3081
--- /dev/null
+++ b/nmxact/nmble/ble_util_test.go
@@ -0,0 +1,501 @@
+/**
+ * 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 nmble
+
+import (
+ "errors"
+ "reflect"
+ "strings"
+ "testing"
+
+ . "mynewt.apache.org/newtmgr/nmxact/bledefs"
+ "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+ "mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+func TestNextSeqWrap(t *testing.T) {
+ seqMtx.Lock()
+ saved := nextSeq
+ nextSeq = BLE_SEQ_EVT_MIN - 2
+ seqMtx.Unlock()
+
+ defer func() {
+ seqMtx.Lock()
+ nextSeq = saved
+ seqMtx.Unlock()
+ }()
+
+ // Sequence numbers never enter the event range.
+ want := []BleSeq{BLE_SEQ_EVT_MIN - 2, BLE_SEQ_EVT_MIN - 1, BLE_SEQ_MIN,
+ BLE_SEQ_MIN + 1}
+ for _, w := range want {
+ if got := NextSeq(); got != w {
+ t.Fatalf("NextSeq() = %#x, want %#x", got, w)
+ }
+ }
+}
+
+func TestBleUtilNewReqs(t *testing.T) {
+ tests := []struct {
+ req interface{}
+ typ MsgType
+ }{
+ {NewBleConnectReq(), MSG_TYPE_CONNECT},
+ {NewBleTerminateReq(), MSG_TYPE_TERMINATE},
+ {NewBleConnCancelReq(), MSG_TYPE_CONN_CANCEL},
+ {NewBleDiscAllSvcsReq(), MSG_TYPE_DISC_ALL_SVCS},
+ {NewBleDiscSvcUuidReq(), MSG_TYPE_DISC_SVC_UUID},
+ {NewBleDiscAllChrsReq(), MSG_TYPE_DISC_ALL_CHRS},
+ {NewBleDiscAllDscsReq(), MSG_TYPE_DISC_ALL_DSCS},
+ {NewBleExchangeMtuReq(), MSG_TYPE_EXCHANGE_MTU},
+ {NewBleGenRandAddrReq(), MSG_TYPE_GEN_RAND_ADDR},
+ {NewBleSetRandAddrReq(), MSG_TYPE_SET_RAND_ADDR},
+ {NewBleWriteCmdReq(), MSG_TYPE_WRITE_CMD},
+ {NewBleWriteReq(), MSG_TYPE_WRITE},
+ {NewBleScanReq(), MSG_TYPE_SCAN},
+ {NewBleScanCancelReq(), MSG_TYPE_SCAN_CANCEL},
+ {NewBleSetPreferredMtuReq(), MSG_TYPE_SET_PREFERRED_MTU},
+ {NewBleConnFindReq(), MSG_TYPE_CONN_FIND},
+ {NewResetReq(), MSG_TYPE_RESET},
+ {NewBleSecurityInitiateReq(), MSG_TYPE_SECURITY_INITIATE},
+ {NewBleAdvFieldsReq(), MSG_TYPE_ADV_FIELDS},
+ {NewBleAdvSetDataReq(), MSG_TYPE_ADV_SET_DATA},
+ {NewBleAdvRspSetDataReq(), MSG_TYPE_ADV_RSP_SET_DATA},
+ {NewBleAdvStartReq(), MSG_TYPE_ADV_START},
+ {NewBleAdvStopReq(), MSG_TYPE_ADV_STOP},
+ {NewBleClearSvcsReq(), MSG_TYPE_CLEAR_SVCS},
+ {NewBleAddSvcsReq(), MSG_TYPE_ADD_SVCS},
+ {NewBleCommitSvcsReq(), MSG_TYPE_COMMIT_SVCS},
+ {NewAccessStatusReq(), MSG_TYPE_ACCESS_STATUS},
+ {NewNotifyReq(), MSG_TYPE_NOTIFY},
+ {NewFindChrReq(), MSG_TYPE_FIND_CHR},
+ {NewSyncReq(), MSG_TYPE_SYNC},
+ {NewBleSmInjectIoReq(), MSG_TYPE_SM_INJECT_IO},
+ }
+
+ for _, tt := range tests {
+ v := reflect.ValueOf(tt.req).Elem()
+ op := MsgOp(v.FieldByName("Op").Int())
+ typ := MsgType(v.FieldByName("Type").Int())
+ seq := BleSeq(v.FieldByName("Seq").Uint())
+
+ if op != MSG_OP_REQ {
+ t.Errorf("%T: op = %s, want request", tt.req, MsgOpToString(op))
+ }
+ if typ != tt.typ {
+ t.Errorf("%T: type = %s, want %s", tt.req,
+ MsgTypeToString(typ), MsgTypeToString(tt.typ))
+ }
+ if seq >= BLE_SEQ_EVT_MIN {
+ t.Errorf("%T: seq %#x in event range", tt.req, seq)
+ }
+ }
+}
+
+func TestStatusError(t *testing.T) {
+ err := StatusError(MSG_OP_RSP, MSG_TYPE_CONNECT, ERR_CODE_EBUSY)
+
+ bhe := nmxutil.ToBleHost(err)
+ if bhe == nil {
+ t.Fatalf("StatusError() returned %T, want *BleHostError", err)
+ }
+ if bhe.Status != ERR_CODE_EBUSY {
+ t.Fatalf("Status = %d, want %d", bhe.Status, ERR_CODE_EBUSY)
+ }
+ for _, s := range []string{"response", "connect", "ebusy"} {
+ if !strings.Contains(err.Error(), s) {
+ t.Errorf("error %q does not contain %q", err.Error(), s)
+ }
+ }
+}
+
+func TestBhdTimeoutError(t *testing.T) {
+ err := BhdTimeoutError(MSG_TYPE_SCAN, 17)
+ if !nmxutil.IsXport(err) {
+ t.Fatalf("BhdTimeoutError() returned %T, want *XportError", err)
+ }
+ if !strings.Contains(err.Error(), "scan") ||
+ !strings.Contains(err.Error(), "seq=17") {
+
+ t.Fatalf("unexpected error text %q", err.Error())
+ }
+}
+
+func TestBleAdvReportFromScanEvt(t *testing.T) {
+ u8 := uint8(6)
+ i8 := int8(-4)
+ u16a := uint16(10)
+ u16b := uint16(20)
+ u16c := uint16(0x0340)
+ u16d := uint16(160)
+ name := "dev"
+ uri := "http://x"
+ addr, _ := ParseBleAddr("01:02:03:04:05:06")
+ u128, _ := ParseUuid128(NmpPlainSvcUuid)
+
+ e := &BleScanEvt{
+ EventType: BLE_ADV_EVENT_IND,
+ AddrType: BLE_ADDR_TYPE_RANDOM,
+ Addr: addr,
+ Rssi: -70,
+ Data: BleBytes{[]byte{1, 2, 3}},
+ DataFlags: &u8,
+ DataUuids16: []BleUuid16{0x1800, 0xfe18},
+ DataUuids16IsComplete: true,
+ DataUuids32: []uint32{0x12345678},
+ DataUuids32IsComplete: true,
+ DataUuids128: []BleUuid128{u128},
+ DataUuids128IsComplete: true,
+ DataName: &name,
+ DataNameIsComplete: true,
+ DataTxPwrLvl: &i8,
+ DataSlaveItvlMin: &u16a,
+ DataSlaveItvlMax: &u16b,
+ DataSvcDataUuid16: BleBytes{[]byte{4}},
+ DataPublicTgtAddrs: []BleAddr{addr},
+ DataAppearance: &u16c,
+ DataAdvItvl: &u16d,
+ DataSvcDataUuid32: BleBytes{[]byte{5}},
+ DataSvcDataUuid128: BleBytes{[]byte{6}},
+ DataUri: &uri,
+ DataMfgData: BleBytes{[]byte{7, 8}},
+ }
+
+ want := BleAdvReport{
+ EventType: BLE_ADV_EVENT_IND,
+ Sender: BleDev{AddrType: BLE_ADDR_TYPE_RANDOM, Addr: addr},
+ Rssi: -70,
+ Fields: BleAdvFields{
+ Data: []byte{1, 2, 3},
+ Flags: &u8,
+ Uuids16: []BleUuid16{0x1800, 0xfe18},
+ Uuids16IsComplete: true,
+ Uuids32: []uint32{0x12345678},
+ Uuids32IsComplete: true,
+ Uuids128: []BleUuid128{u128},
+ Uuids128IsComplete: true,
+ Name: &name,
+ NameIsComplete: true,
+ TxPwrLvl: &i8,
+ SlaveItvlMin: &u16a,
+ SlaveItvlMax: &u16b,
+ SvcDataUuid16: []byte{4},
+ PublicTgtAddrs: []BleAddr{addr},
+ Appearance: &u16c,
+ AdvItvl: &u16d,
+ SvcDataUuid32: []byte{5},
+ SvcDataUuid128: []byte{6},
+ Uri: &uri,
+ MfgData: []byte{7, 8},
+ },
+ }
+
+ got := BleAdvReportFromScanEvt(e)
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("BleAdvReportFromScanEvt() =\n%+v\nwant\n%+v", got, want)
+ }
+}
+
+func TestBleAdvFieldsToReq(t *testing.T) {
+ u8 := uint8(6)
+ i8 := int8(3)
+ u16a := uint16(10)
+ u16b := uint16(20)
+ u16c := uint16(0x0340)
+ u16d := uint16(160)
+ name := "dev"
+ uri := "http://x"
+ addr, _ := ParseBleAddr("01:02:03:04:05:06")
+ u128, _ := ParseUuid128(NmpPlainSvcUuid)
+
+ f := BleAdvFields{
+ // Raw data is not part of the request.
+ Data: []byte{0xee},
+ Flags: &u8,
+ Uuids16: []BleUuid16{0x1800},
+ Uuids16IsComplete: true,
+ Uuids32: []uint32{1},
+ Uuids32IsComplete: true,
+ Uuids128: []BleUuid128{u128},
+ Uuids128IsComplete: true,
+ Name: &name,
+ NameIsComplete: true,
+ TxPwrLvl: &i8,
+ SlaveItvlMin: &u16a,
+ SlaveItvlMax: &u16b,
+ SvcDataUuid16: []byte{4},
+ PublicTgtAddrs: []BleAddr{addr},
+ Appearance: &u16c,
+ AdvItvl: &u16d,
+ SvcDataUuid32: []byte{5},
+ SvcDataUuid128: []byte{6},
+ Uri: &uri,
+ MfgData: []byte{7, 8},
+ }
+
+ r := BleAdvFieldsToReq(f)
+
+ if r.Op != MSG_OP_REQ || r.Type != MSG_TYPE_ADV_FIELDS {
+ t.Fatalf("unexpected header op=%d type=%d", r.Op, r.Type)
+ }
+
+ want := *NewBleAdvFieldsReq()
+ want.Seq = r.Seq
+ want.Flags = &u8
+ want.Uuids16 = []BleUuid16{0x1800}
+ want.Uuids16IsComplete = true
+ want.Uuids32 = []uint32{1}
+ want.Uuids32IsComplete = true
+ want.Uuids128 = []BleUuid128{u128}
+ want.Uuids128IsComplete = true
+ want.Name = &name
+ want.NameIsComplete = true
+ want.TxPwrLvl = &i8
+ want.SlaveItvlMin = &u16a
+ want.SlaveItvlMax = &u16b
+ want.SvcDataUuid16 = BleBytes{[]byte{4}}
+ want.PublicTgtAddrs = []BleAddr{addr}
+ want.Appearance = &u16c
+ want.AdvItvl = &u16d
+ want.SvcDataUuid32 = BleBytes{[]byte{5}}
+ want.SvcDataUuid128 = BleBytes{[]byte{6}}
+ want.Uri = &uri
+ want.MfgData = BleBytes{[]byte{7, 8}}
+
+ if !reflect.DeepEqual(*r, want) {
+ t.Fatalf("BleAdvFieldsToReq() =\n%+v\nwant\n%+v", *r, want)
+ }
+}
+
+func TestBleSvcToAddSvc(t *testing.T) {
+ svc := BleSvc{
+ Uuid: NewBleUuid16(0x1234),
+ SvcType: BLE_SVC_TYPE_SECONDARY,
+ Chrs: []BleChr{
+ {
+ Uuid: NewBleUuid16(0x2a00),
+ Flags: BLE_GATT_F_READ | BLE_GATT_F_NOTIFY,
+ MinKeySize: 16,
+ AccessCb: func(a BleGattAccess) (uint8, []byte) {
+ return 0, nil
+ },
+ Dscs: []BleDsc{
+ {
+ Uuid: NewBleUuid16(0x2901),
+ AttFlags: BLE_ATT_F_READ,
+ MinKeySize: 7,
+ },
+ },
+ },
+ {
+ Uuid: NewBleUuid16(0x2a01),
+ Flags: BLE_GATT_F_WRITE,
+ },
+ },
+ }
+
+ want := BleAddSvc{
+ Uuid: NewBleUuid16(0x1234),
+ SvcType: BLE_SVC_TYPE_SECONDARY,
+ Chrs: []BleAddChr{
+ {
+ Uuid: NewBleUuid16(0x2a00),
+ Flags: BLE_GATT_F_READ | BLE_GATT_F_NOTIFY,
+ MinKeySize: 16,
+ Dscs: []BleAddDsc{
+ {
+ Uuid: NewBleUuid16(0x2901),
+ AttFlags: BLE_ATT_F_READ,
+ MinKeySize: 7,
+ },
+ },
+ },
+ {
+ Uuid: NewBleUuid16(0x2a01),
+ Flags: BLE_GATT_F_WRITE,
+ },
+ },
+ }
+
+ got := BleSvcToAddSvc(svc)
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("BleSvcToAddSvc() =\n%+v\nwant\n%+v", got, want)
+ }
+
+ // A service without characteristics.
+ got = BleSvcToAddSvc(BleSvc{Uuid: NewBleUuid16(1)})
+ if got.Chrs != nil || CompareUuids(got.Uuid, NewBleUuid16(1)) != 0 {
+ t.Fatalf("unexpected result %+v", got)
+ }
+}
+
+func TestBuildMgmtChrs(t *testing.T) {
+ mustUuid := func(s string) BleUuid {
+ u, err := ParseUuid(s)
+ if err != nil {
+ t.Fatalf("ParseUuid(%q): %v", s, err)
+ }
+ return u
+ }
+
+ chrId := func(svc string, chr string) BleChrId {
+ return BleChrId{SvcUuid: mustUuid(svc), ChrUuid: mustUuid(chr)}
+ }
+
+ resReq := chrId(IotivitySvcUuid, IotivityReqChrUuid)
+ resRsp := chrId(IotivitySvcUuid, IotivityRspChrUuid)
+
+ tests := []struct {
+ proto sesn.MgmtProto
+ nmpReq BleChrId
+ nmpRsp BleChrId
+ }{
+ {
+ sesn.MGMT_PROTO_NMP,
+ chrId(NmpPlainSvcUuid, NmpPlainChrUuid),
+ chrId(NmpPlainSvcUuid, NmpPlainChrUuid),
+ },
+ {
+ sesn.MGMT_PROTO_OMP,
+ chrId(OmpUnsecSvcUuid, OmpUnsecReqChrUuid),
+ chrId(OmpUnsecSvcUuid, OmpUnsecRspChrUuid),
+ },
+ }
+
+ for _, tt := range tests {
+ mc, err := BuildMgmtChrs(tt.proto)
+ if err != nil {
+ t.Fatalf("proto %d: unexpected error: %v", tt.proto, err)
+ }
+
+ checks := []struct {
+ name string
+ got *BleChrId
+ want BleChrId
+ }{
+ {"NmpReqChr", mc.NmpReqChr, tt.nmpReq},
+ {"NmpRspChr", mc.NmpRspChr, tt.nmpRsp},
+ {"ResReqChr", mc.ResReqChr, resReq},
+ {"ResRspChr", mc.ResRspChr, resRsp},
+ }
+ for _, c := range checks {
+ if c.got == nil {
+ t.Errorf("proto %d: %s is nil", tt.proto, c.name)
+ continue
+ }
+ if CompareChrIds(*c.got, c.want) != 0 {
+ t.Errorf("proto %d: %s = %s, want %s", tt.proto, c.name,
+ c.got.String(), c.want.String())
+ }
+ }
+ }
+
+ if _, err := BuildMgmtChrs(sesn.MGMT_PROTO_COAP_SERVER); err == nil {
+ t.Errorf("COAP_SERVER: expected error")
+ }
+}
+
+func TestIsSecErr(t *testing.T) {
+ tests := []struct {
+ err error
+ want bool
+ }{
+ {nil, false},
+ {errors.New("x"), false},
+ {nmxutil.NewXportError("x"), false},
+ }
+
+ for code := 0; code < 0x100; code++ {
+ var want bool
+ switch code {
+ case ERR_CODE_ATT_INSUFFICIENT_AUTHEN,
+ ERR_CODE_ATT_INSUFFICIENT_AUTHOR,
+ ERR_CODE_ATT_INSUFFICIENT_KEY_SZ,
+ ERR_CODE_ATT_INSUFFICIENT_ENC:
+ want = true
+ }
+ tests = append(tests, struct {
+ err error
+ want bool
+ }{nmxutil.NewBleHostError(ERR_CODE_ATT_BASE+code, "x"), want})
+ }
+
+ // The same low bits in other error ranges are not security errors.
+ tests = append(tests, struct {
+ err error
+ want bool
+ }{nmxutil.NewBleHostError(
+ ERR_CODE_HCI_BASE+ERR_CODE_ATT_INSUFFICIENT_AUTHEN, "x"), false})
+
+ for _, tt := range tests {
+ if got := IsSecErr(tt.err); got != tt.want {
+ t.Errorf("IsSecErr(%#v) = %v, want %v", tt.err, got, tt.want)
+ }
+ }
+}
+
+func TestToSecurityErr(t *testing.T) {
+ secCodes := map[int]bool{
+ ERR_CODE_SM_ERR_PASSKEY: true,
+ ERR_CODE_SM_ERR_OOB: true,
+ ERR_CODE_SM_ERR_CONFIRM_MISMATCH: true,
+ ERR_CODE_SM_ERR_UNSPECIFIED: true,
+ ERR_CODE_SM_ERR_DHKEY: true,
+ ERR_CODE_SM_ERR_NUMCMP: true,
+ }
+
+ for _, base := range []int{ERR_CODE_SM_US_BASE, ERR_CODE_SM_PEER_BASE} {
+ for code := 0; code < 0x100; code++ {
+ in := nmxutil.NewBleHostError(base+code, "pairing failed")
+ got := ToSecurityErr(in)
+
+ if !secCodes[code] {
+ if got != nil {
+ t.Errorf("ToSecurityErr(%#x) = %v, want nil",
+ base+code, got)
+ }
+ continue
+ }
+
+ if !nmxutil.IsBleSecurity(got) {
+ t.Errorf("ToSecurityErr(%#x) = %T, want *BleSecurityError",
+ base+code, got)
+ continue
+ }
+ if got.Error() != in.Error() {
+ t.Errorf("ToSecurityErr(%#x) text = %q, want %q",
+ base+code, got.Error(), in.Error())
+ }
+ }
+ }
+
+ others := []error{
+ nil,
+ errors.New("x"),
+ nmxutil.NewBleSecurityError("x"),
+ nmxutil.NewBleHostError(ERR_CODE_ATT_BASE+ERR_CODE_SM_ERR_PASSKEY,
+ "x"),
+ }
+ for _, e := range others {
+ if got := ToSecurityErr(e); got != nil {
+ t.Errorf("ToSecurityErr(%#v) = %v, want nil", e, got)
+ }
+ }
+}
(mynewt-newtmgr) 02/20: nmxact/omp: Add unit tests for OMP encoding and decoding
Posted by ja...@apache.org.
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 37ca926ca7886e31384113ef37c39ae9ed3d6e33
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:22:02 2026 +0200
nmxact/omp: Add unit tests for OMP encoding and decoding
Check that EncodeOmpDgram and EncodeOmpTcp produce a confirmable CoAP
PUT to /omgr with the NMP sequence as token and the NMP header in the
"_h" payload field, and that the TX filter is applied. Also add a
regression test for the "omitempty" fix: the struct-to-map conversion
must drop empty optional fields and must not include NmpBase.
For DecodeOmp, cover all accepted response codes, ignored request
codes, error codes, missing or short "_h", bad CBOR, unknown NMP ids
and the RX filter.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/omp/omp_test.go | 456 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 456 insertions(+)
diff --git a/nmxact/omp/omp_test.go b/nmxact/omp/omp_test.go
new file mode 100644
index 0000000..39d7155
--- /dev/null
+++ b/nmxact/omp/omp_test.go
@@ -0,0 +1,456 @@
+/**
+ * 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 omp
+
+import (
+ "bytes"
+ "errors"
+ "reflect"
+ "sort"
+ "testing"
+
+ "github.com/runtimeco/go-coap"
+ "github.com/ugorji/go/codec"
+
+ "mynewt.apache.org/newtmgr/nmxact/nmcoap"
+ "mynewt.apache.org/newtmgr/nmxact/nmp"
+)
+
+func cborMap(t *testing.T, b []byte) map[string]interface{} {
+ t.Helper()
+
+ m := map[string]interface{}{}
+ if err := codec.NewDecoderBytes(b, new(codec.CborHandle)).
+ Decode(&m); err != nil {
+
+ t.Fatalf("failed to decode CBOR: %v", err)
+ }
+ return m
+}
+
+func cborBytes(t *testing.T, v interface{}) []byte {
+ t.Helper()
+
+ b := []byte{}
+ if err := codec.NewEncoderBytes(&b, new(codec.CborHandle)).
+ Encode(v); err != nil {
+
+ t.Fatalf("failed to encode CBOR: %v", err)
+ }
+ return b
+}
+
+func keys(m map[string]interface{}) []string {
+ ks := []string{}
+ for k := range m {
+ ks = append(ks, k)
+ }
+ sort.Strings(ks)
+ return ks
+}
+
+// checkOmpReq verifies the CoAP envelope of an encoded OMP request and
+// returns the decoded CBOR payload.
+func checkOmpReq(t *testing.T, m coap.Message,
+ hdr nmp.NmpHdr) map[string]interface{} {
+
+ t.Helper()
+
+ if m.Code() != coap.PUT {
+ t.Fatalf("code = %v, want PUT", m.Code())
+ }
+ if m.PathString() != "omgr" {
+ t.Fatalf("path = %q, want omgr", m.PathString())
+ }
+ if !bytes.Equal(m.Token(), []byte{hdr.Seq}) {
+ t.Fatalf("token = % x, want %02x", m.Token(), hdr.Seq)
+ }
+
+ pl := cborMap(t, m.Payload())
+ h, ok := pl["_h"].([]byte)
+ if !ok {
+ t.Fatalf("_h missing or wrong type: %#v", pl["_h"])
+ }
+ if !bytes.Equal(h, hdr.Bytes()) {
+ t.Fatalf("_h = % x, want % x", h, hdr.Bytes())
+ }
+
+ return pl
+}
+
+func TestEncodeOmpDgram(t *testing.T) {
+ req := nmp.NewEchoReq()
+ req.Payload = "hello"
+ msg := req.Msg()
+
+ data, err := EncodeOmpDgram(nil, msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ m, err := coap.ParseDgramMessage(data)
+ if err != nil {
+ t.Fatalf("failed to parse CoAP: %v", err)
+ }
+ if m.Type() != coap.Confirmable {
+ t.Fatalf("type = %v, want Confirmable", m.Type())
+ }
+
+ pl := checkOmpReq(t, m, msg.Hdr)
+ if want := []string{"_h", "d"}; !reflect.DeepEqual(keys(pl), want) {
+ t.Fatalf("payload keys = %v, want %v", keys(pl), want)
+ }
+ if pl["d"] != "hello" {
+ t.Fatalf("d = %v, want hello", pl["d"])
+ }
+}
+
+func TestEncodeOmpTcp(t *testing.T) {
+ req := nmp.NewEchoReq()
+ req.Payload = "hello"
+ msg := req.Msg()
+
+ data, err := EncodeOmpTcp(nil, msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ m, rest, err := coap.PullTcp(data)
+ if err != nil {
+ t.Fatalf("failed to parse CoAP: %v", err)
+ }
+ if m == nil || len(rest) != 0 {
+ t.Fatalf("PullTcp() = %v, rest % x", m, rest)
+ }
+
+ pl := checkOmpReq(t, m, msg.Hdr)
+ if want := []string{"_h", "d"}; !reflect.DeepEqual(keys(pl), want) {
+ t.Fatalf("payload keys = %v, want %v", keys(pl), want)
+ }
+}
+
+// The OMP encoder builds a field map from the request struct; it must
+// honour "omitempty" and must not include the NmpBase header. Regression
+// test for 'nmxact: Fix "omitempty" handling in NMP requests'.
+func TestEncodeOmpOmitEmpty(t *testing.T) {
+ tests := []struct {
+ name string
+ req nmp.NmpReq
+ keys []string
+ }{
+ {
+ name: "image upload first chunk",
+ req: func() nmp.NmpReq {
+ r := nmp.NewImageUploadReq()
+ r.Len = 100
+ r.DataSha = []byte{1, 2}
+ r.Upgrade = true
+ r.Data = []byte{0xaa}
+ return r
+ }(),
+ keys: []string{"_h", "data", "image", "len", "off", "sha",
+ "upgrade"},
+ },
+ {
+ name: "image upload next chunk",
+ req: func() nmp.NmpReq {
+ r := nmp.NewImageUploadReq()
+ r.Off = 50
+ r.Data = []byte{0xbb}
+ return r
+ }(),
+ keys: []string{"_h", "data", "image", "off"},
+ },
+ {
+ name: "config write save only",
+ req: func() nmp.NmpReq {
+ r := nmp.NewConfigWriteReq()
+ r.Save = true
+ return r
+ }(),
+ keys: []string{"_h", "save"},
+ },
+ {
+ name: "config write name val",
+ req: func() nmp.NmpReq {
+ r := nmp.NewConfigWriteReq()
+ r.Name = "a/b"
+ r.Val = "1"
+ return r
+ }(),
+ keys: []string{"_h", "name", "val"},
+ },
+ {
+ name: "fs upload",
+ req: func() nmp.NmpReq {
+ r := nmp.NewFsUploadReq()
+ r.Name = "/f"
+ r.Off = 10
+ r.Data = []byte{1}
+ return r
+ }(),
+ keys: []string{"_h", "data", "len", "name", "off"},
+ },
+ {
+ name: "reset",
+ req: nmp.NewResetReq(),
+ keys: []string{"_h"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ msg := tt.req.Msg()
+ data, err := EncodeOmpDgram(nil, msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m, err := coap.ParseDgramMessage(data)
+ if err != nil {
+ t.Fatalf("failed to parse CoAP: %v", err)
+ }
+
+ pl := checkOmpReq(t, m, msg.Hdr)
+ if !reflect.DeepEqual(keys(pl), tt.keys) {
+ t.Fatalf("payload keys = %v, want %v", keys(pl), tt.keys)
+ }
+ })
+ }
+}
+
+type testTxFilter struct {
+ called int
+ err error
+}
+
+func (f *testTxFilter) Filter(m coap.Message) (coap.Message, error) {
+ f.called++
+ if f.err != nil {
+ return nil, f.err
+ }
+ m.SetURIQuery("filtered")
+ return m, nil
+}
+
+func (f *testTxFilter) Freeze() {}
+func (f *testTxFilter) Unfreeze() {}
+
+func TestEncodeOmpTxFilter(t *testing.T) {
+ f := &testTxFilter{}
+ data, err := EncodeOmpDgram(f, nmp.NewResetReq().Msg())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if f.called != 1 {
+ t.Fatalf("filter called %d times, want 1", f.called)
+ }
+
+ m, err := coap.ParseDgramMessage(data)
+ if err != nil {
+ t.Fatalf("failed to parse CoAP: %v", err)
+ }
+ if q := m.Option(coap.URIQuery); q != "filtered" {
+ t.Fatalf("query = %v, want filtered", q)
+ }
+
+ f = &testTxFilter{err: errors.New("boom")}
+ if _, err := EncodeOmpTcp(f, nmp.NewResetReq().Msg()); err == nil {
+ t.Fatalf("expected filter error")
+ }
+}
+
+func echoRspMsg(t *testing.T, code coap.COAPCode, withHdr bool,
+ seq uint8) coap.Message {
+
+ t.Helper()
+
+ hdr := nmp.NmpHdr{
+ Op: nmp.NMP_OP_WRITE_RSP,
+ Group: nmp.NMP_GROUP_DEFAULT,
+ Id: nmp.NMP_ID_DEF_ECHO,
+ Seq: seq,
+ }
+
+ pl := map[string]interface{}{
+ "r": "pong",
+ "rc": 0,
+ }
+ if withHdr {
+ pl["_h"] = hdr.Bytes()
+ }
+
+ return coap.NewDgramMessage(coap.MessageParams{
+ Type: coap.Acknowledgement,
+ Code: code,
+ Token: []byte{seq},
+ Payload: cborBytes(t, pl),
+ })
+}
+
+func TestDecodeOmp(t *testing.T) {
+ codes := []coap.COAPCode{
+ coap.Created, coap.Deleted, coap.Valid, coap.Changed, coap.Content,
+ }
+
+ for _, c := range codes {
+ rsp, err := DecodeOmp(echoRspMsg(t, c, true, 0x21), nil)
+ if err != nil {
+ t.Fatalf("%v: unexpected error: %v", c, err)
+ }
+
+ er, ok := rsp.(*nmp.EchoRsp)
+ if !ok {
+ t.Fatalf("%v: got %T, want *nmp.EchoRsp", c, rsp)
+ }
+ if er.Payload != "pong" || er.Hdr().Seq != 0x21 {
+ t.Fatalf("%v: unexpected rsp %+v hdr %+v", c, er, *er.Hdr())
+ }
+ }
+}
+
+func TestDecodeOmpIgnoresRequests(t *testing.T) {
+ for _, c := range []coap.COAPCode{coap.GET, coap.PUT, coap.POST,
+ coap.DELETE} {
+
+ rsp, err := DecodeOmp(echoRspMsg(t, c, true, 1), nil)
+ if rsp != nil || err != nil {
+ t.Errorf("%v: DecodeOmp() = (%v, %v), want (nil, nil)",
+ c, rsp, err)
+ }
+ }
+}
+
+func TestDecodeOmpErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ msg coap.Message
+ }{
+ {"error code", echoRspMsg(t, coap.NotFound, true, 1)},
+ {"internal error code", echoRspMsg(t, coap.InternalServerError,
+ true, 1)},
+ {"missing _h", echoRspMsg(t, coap.Changed, false, 1)},
+ {"bad cbor", coap.NewDgramMessage(coap.MessageParams{
+ Code: coap.Changed,
+ Payload: []byte{0xa1, 0x62},
+ })},
+ {"short _h", coap.NewDgramMessage(coap.MessageParams{
+ Code: coap.Changed,
+ Payload: cborBytes(t, map[string]interface{}{
+ "_h": []byte{1, 2, 3},
+ }),
+ })},
+ {"unknown nmp id", coap.NewDgramMessage(coap.MessageParams{
+ Code: coap.Changed,
+ Payload: cborBytes(t, map[string]interface{}{
+ "_h": (&nmp.NmpHdr{Op: nmp.NMP_OP_WRITE_RSP,
+ Group: 99, Id: 99}).Bytes(),
+ }),
+ })},
+ }
+
+ for _, tt := range tests {
+ rsp, err := DecodeOmp(tt.msg, nil)
+ if err == nil {
+ t.Errorf("%s: expected error, got %+v", tt.name, rsp)
+ }
+ }
+}
+
+func TestDecodeOmpRxFilter(t *testing.T) {
+ // Filter is applied before the code check.
+ var called bool
+ f := nmcoap.RxFilterFunc(func(m coap.Message) (coap.Message, error) {
+ called = true
+ return echoRspMsg(t, coap.Changed, true, 5), nil
+ })
+
+ rsp, err := DecodeOmp(echoRspMsg(t, coap.NotFound, false, 5), f)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !called || rsp == nil || rsp.Hdr().Seq != 5 {
+ t.Fatalf("unexpected rsp %+v (filter called=%v)", rsp, called)
+ }
+
+ // Filter error is propagated.
+ f = nmcoap.RxFilterFunc(func(m coap.Message) (coap.Message, error) {
+ return nil, errors.New("boom")
+ })
+ if _, err := DecodeOmp(echoRspMsg(t, coap.Changed, true, 5),
+ f); err == nil {
+
+ t.Fatalf("expected filter error")
+ }
+
+ // Requests are ignored before the filter runs.
+ called = false
+ f = nmcoap.RxFilterFunc(func(m coap.Message) (coap.Message, error) {
+ called = true
+ return m, nil
+ })
+ if rsp, err := DecodeOmp(echoRspMsg(t, coap.PUT, true, 5),
+ f); rsp != nil || err != nil || called {
+
+ t.Fatalf("request: rsp=%v err=%v called=%v", rsp, err, called)
+ }
+}
+
+func TestEncodeDecodeRoundTrip(t *testing.T) {
+ // A device answering an OMP request echoes the request header with the
+ // response op.
+ req := nmp.NewEchoReq()
+ req.Payload = "x"
+ msg := req.Msg()
+
+ data, err := EncodeOmpTcp(nil, msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m, _, err := coap.PullTcp(data)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ pl := cborMap(t, m.Payload())
+ hdr, err := nmp.DecodeNmpHdr(pl["_h"].([]byte))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ hdr.Op = nmp.NMP_OP_WRITE_RSP
+
+ rm := coap.NewTcpMessage(coap.MessageParams{
+ Code: coap.Changed,
+ Token: m.Token(),
+ Payload: cborBytes(t, map[string]interface{}{
+ "_h": hdr.Bytes(),
+ "r": pl["d"],
+ }),
+ })
+
+ rsp, err := DecodeOmp(rm, nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if er := rsp.(*nmp.EchoRsp); er.Payload != "x" ||
+ er.Hdr().Seq != msg.Hdr.Seq {
+
+ t.Fatalf("unexpected rsp %+v", er)
+ }
+}
(mynewt-newtmgr) 07/20: nmxact/bledefs: Add unit tests for addresses, UUIDs and enums
Posted by ja...@apache.org.
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 bdefc99e18ce0619bea5e2784b3120f5a422047f
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:27:02 2026 +0200
nmxact/bledefs: Add unit tests for addresses, UUIDs and enums
Cover BLE address parsing, formatting and JSON (including invalid
input), 16-bit and 128-bit UUID parsing, formatting and JSON,
CompareUuids and CompareChrIds ordering, and a table test over every
enum string map checking ToString/FromString/MarshalJSON/
UnmarshalJSON round trips and the handling of unknown values.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/bledefs/bledefs_test.go | 499 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 499 insertions(+)
diff --git a/nmxact/bledefs/bledefs_test.go b/nmxact/bledefs/bledefs_test.go
new file mode 100644
index 0000000..d2b3240
--- /dev/null
+++ b/nmxact/bledefs/bledefs_test.go
@@ -0,0 +1,499 @@
+/**
+ * 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 bledefs
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestParseBleAddr(t *testing.T) {
+ tests := []struct {
+ in string
+ want [6]byte
+ str string
+ }{
+ {"00:00:00:00:00:00", [6]byte{}, "00:00:00:00:00:00"},
+ {"01:23:45:67:89:ab", [6]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab},
+ "01:23:45:67:89:ab"},
+ {"FF:EE:DD:CC:BB:AA", [6]byte{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa},
+ "ff:ee:dd:cc:bb:aa"},
+ {"1:2:3:4:5:6", [6]byte{1, 2, 3, 4, 5, 6}, "01:02:03:04:05:06"},
+ }
+
+ for _, tt := range tests {
+ ba, err := ParseBleAddr(tt.in)
+ if err != nil {
+ t.Errorf("ParseBleAddr(%q): unexpected error: %v", tt.in, err)
+ continue
+ }
+ if ba.Bytes != tt.want {
+ t.Errorf("ParseBleAddr(%q) = % x, want % x", tt.in, ba.Bytes,
+ tt.want)
+ }
+ if s := ba.String(); s != tt.str {
+ t.Errorf("String() = %q, want %q", s, tt.str)
+ }
+
+ // String output parses back to the same address.
+ ba2, err := ParseBleAddr(ba.String())
+ if err != nil || ba2 != ba {
+ t.Errorf("round trip of %q: %v, %v", tt.in, ba2, err)
+ }
+ }
+}
+
+func TestParseBleAddrInvalid(t *testing.T) {
+ bad := []string{
+ "",
+ "01:23:45:67:89",
+ "01:23:45:67:89:ab:cd",
+ "01-23-45-67-89-ab",
+ "01:23:45:67:89:xy",
+ "01:23:45:67:89:100",
+ "01:23:45:67::ab",
+ "0x1:23:45:67:89:ab",
+ }
+
+ for _, s := range bad {
+ if ba, err := ParseBleAddr(s); err == nil {
+ t.Errorf("ParseBleAddr(%q) = %v, expected error", s, ba)
+ }
+ }
+}
+
+func TestBleAddrJSON(t *testing.T) {
+ ba, _ := ParseBleAddr("01:02:03:04:05:06")
+
+ b, err := json.Marshal(&ba)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != `"01:02:03:04:05:06"` {
+ t.Fatalf("Marshal = %s", b)
+ }
+
+ var got BleAddr
+ if err := json.Unmarshal(b, &got); err != nil {
+ t.Fatalf("Unmarshal: %v", err)
+ }
+ if got != ba {
+ t.Fatalf("Unmarshal = %v, want %v", got, ba)
+ }
+
+ for _, bad := range []string{`"zz"`, `5`, `null`} {
+ if err := json.Unmarshal([]byte(bad), &got); err == nil {
+ t.Errorf("Unmarshal(%s): expected error", bad)
+ }
+ }
+}
+
+func TestBleDevString(t *testing.T) {
+ ba, _ := ParseBleAddr("aa:bb:cc:dd:ee:ff")
+ d := BleDev{AddrType: BLE_ADDR_TYPE_RANDOM, Addr: ba}
+ if s := d.String(); s != "random,aa:bb:cc:dd:ee:ff" {
+ t.Fatalf("String() = %q", s)
+ }
+}
+
+func TestParseUuid16(t *testing.T) {
+ tests := []struct {
+ in string
+ want BleUuid16
+ str string
+ }{
+ {"0x1800", 0x1800, "0x1800"},
+ {"0xfe18", 0xfe18, "0xfe18"},
+ {"0XFFFF", 0xffff, "0xffff"},
+ // Base is inferred from the prefix; no prefix means decimal.
+ {"10", 10, "0x000a"},
+ }
+
+ for _, tt := range tests {
+ got, err := ParseUuid16(tt.in)
+ if err != nil {
+ t.Errorf("ParseUuid16(%q): %v", tt.in, err)
+ continue
+ }
+ if got != tt.want {
+ t.Errorf("ParseUuid16(%q) = %#x, want %#x", tt.in, got, tt.want)
+ }
+ if s := got.String(); s != tt.str {
+ t.Errorf("String() = %q, want %q", s, tt.str)
+ }
+ }
+
+ for _, bad := range []string{"", "0x10000", "-1", "0xzz",
+ "ade3d529-c784-4f63-a987-eb69f70ee816"} {
+
+ if _, err := ParseUuid16(bad); err == nil {
+ t.Errorf("ParseUuid16(%q): expected error", bad)
+ }
+ }
+}
+
+func TestParseUuid128(t *testing.T) {
+ s := "ade3d529-c784-4f63-a987-eb69f70ee816"
+ want := BleUuid128{
+ 0xad, 0xe3, 0xd5, 0x29, 0xc7, 0x84, 0x4f, 0x63,
+ 0xa9, 0x87, 0xeb, 0x69, 0xf7, 0x0e, 0xe8, 0x16,
+ }
+
+ got, err := ParseUuid128(s)
+ if err != nil {
+ t.Fatalf("ParseUuid128: %v", err)
+ }
+ if got != want {
+ t.Fatalf("ParseUuid128 = % x, want % x", got, want)
+ }
+ if got.String() != s {
+ t.Fatalf("String() = %q, want %q", got.String(), s)
+ }
+
+ // Upper case input is accepted; output is lower case.
+ got, err = ParseUuid128("ADE3D529-C784-4F63-A987-EB69F70EE816")
+ if err != nil || got != want {
+ t.Fatalf("upper case: % x, %v", got, err)
+ }
+
+ bad := []string{
+ "",
+ "ade3d529c7844f63a987eb69f70ee816",
+ "ade3d529-c784-4f63-a987-eb69f70ee81",
+ "ade3d529-c784-4f63-a987-eb69f70ee8166",
+ "ade3d529_c784-4f63-a987-eb69f70ee816",
+ "ade3d529-c784-4f63-a987-eb69f70ee8zz",
+ "ade3d52-9c784-4f63-a987-eb69f70ee816",
+ }
+ for _, b := range bad {
+ if _, err := ParseUuid128(b); err == nil {
+ t.Errorf("ParseUuid128(%q): expected error", b)
+ }
+ }
+}
+
+func TestBleUuid128JSON(t *testing.T) {
+ u, _ := ParseUuid128("0c08c213-98ed-4e43-a499-7e1137c39567")
+
+ b, err := json.Marshal(&u)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != `"0c08c213-98ed-4e43-a499-7e1137c39567"` {
+ t.Fatalf("Marshal = %s", b)
+ }
+
+ var got BleUuid128
+ if err := json.Unmarshal(b, &got); err != nil || got != u {
+ t.Fatalf("Unmarshal = % x, %v", got, err)
+ }
+ if err := json.Unmarshal([]byte(`"bad"`), &got); err == nil {
+ t.Fatalf("Unmarshal(bad): expected error")
+ }
+}
+
+func TestParseUuid(t *testing.T) {
+ u, err := ParseUuid("0x2902")
+ if err != nil {
+ t.Fatalf("ParseUuid: %v", err)
+ }
+ if u.U16 != CccdUuid || u.U128 != (BleUuid128{}) {
+ t.Fatalf("ParseUuid(0x2902) = %+v", u)
+ }
+ if u.String() != "0x2902" {
+ t.Fatalf("String() = %q", u.String())
+ }
+
+ u, err = ParseUuid(NmpPlainSvcUuid)
+ if err != nil {
+ t.Fatalf("ParseUuid: %v", err)
+ }
+ if u.U16 != 0 || u.String() != NmpPlainSvcUuid {
+ t.Fatalf("ParseUuid(%s) = %+v", NmpPlainSvcUuid, u)
+ }
+
+ if _, err := ParseUuid("not-a-uuid"); err == nil {
+ t.Fatalf("expected error")
+ }
+
+ if n := NewBleUuid16(0x1801); n.U16 != 0x1801 || n.String() != "0x1801" {
+ t.Fatalf("NewBleUuid16 = %+v", n)
+ }
+}
+
+func TestBleUuidJSON(t *testing.T) {
+ u16 := NewBleUuid16(0x2a00)
+ b, err := json.Marshal(&u16)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != "10752" {
+ t.Fatalf("Marshal(u16) = %s, want 10752", b)
+ }
+
+ u128, _ := ParseUuid(OmpUnsecSvcUuid)
+ b, err = json.Marshal(&u128)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if string(b) != `"`+OmpUnsecSvcUuid+`"` {
+ t.Fatalf("Marshal(u128) = %s", b)
+ }
+
+ tests := []struct {
+ in string
+ want BleUuid
+ }{
+ {"10752", u16},
+ {`"0x2a00"`, u16},
+ {`"` + OmpUnsecSvcUuid + `"`, u128},
+ }
+ for _, tt := range tests {
+ var got BleUuid
+ if err := json.Unmarshal([]byte(tt.in), &got); err != nil {
+ t.Errorf("Unmarshal(%s): %v", tt.in, err)
+ continue
+ }
+ if CompareUuids(got, tt.want) != 0 {
+ t.Errorf("Unmarshal(%s) = %s, want %s", tt.in, got.String(),
+ tt.want.String())
+ }
+ }
+
+ for _, bad := range []string{`"zzz"`, `70000`, `true`} {
+ var got BleUuid
+ if err := json.Unmarshal([]byte(bad), &got); err == nil {
+ t.Errorf("Unmarshal(%s): expected error", bad)
+ }
+ }
+}
+
+func TestCompareUuids(t *testing.T) {
+ a16 := NewBleUuid16(0x1800)
+ b16 := NewBleUuid16(0x1801)
+ a128, _ := ParseUuid("00000000-0000-0000-0000-000000000001")
+ b128, _ := ParseUuid("00000000-0000-0000-0000-000000000002")
+
+ tests := []struct {
+ a, b BleUuid
+ want int
+ }{
+ {a16, a16, 0},
+ {a16, b16, -1},
+ {b16, a16, 1},
+ {a128, a128, 0},
+ {a128, b128, -1},
+ {b128, a128, 1},
+ // 16-bit UUIDs sort after 128-bit ones.
+ {a16, a128, 1},
+ {a128, a16, -1},
+ }
+
+ for _, tt := range tests {
+ got := CompareUuids(tt.a, tt.b)
+ if (got < 0) != (tt.want < 0) || (got > 0) != (tt.want > 0) {
+ t.Errorf("CompareUuids(%s, %s) = %d, want sign %d",
+ tt.a.String(), tt.b.String(), got, tt.want)
+ }
+ }
+}
+
+func TestCompareChrIds(t *testing.T) {
+ s1 := NewBleUuid16(1)
+ s2 := NewBleUuid16(2)
+ c1 := NewBleUuid16(10)
+ c2 := NewBleUuid16(20)
+
+ tests := []struct {
+ a, b BleChrId
+ want int
+ }{
+ {BleChrId{s1, c1}, BleChrId{s1, c1}, 0},
+ {BleChrId{s1, c1}, BleChrId{s1, c2}, -1},
+ {BleChrId{s1, c2}, BleChrId{s1, c1}, 1},
+ // Service UUID is the primary key.
+ {BleChrId{s1, c2}, BleChrId{s2, c1}, -1},
+ {BleChrId{s2, c1}, BleChrId{s1, c2}, 1},
+ }
+
+ for _, tt := range tests {
+ got := CompareChrIds(tt.a, tt.b)
+ if (got < 0) != (tt.want < 0) || (got > 0) != (tt.want > 0) {
+ t.Errorf("CompareChrIds(%s, %s) = %d, want sign %d",
+ tt.a.String(), tt.b.String(), got, tt.want)
+ }
+ }
+
+ id := BleChrId{s1, c2}
+ if s := id.String(); s != "s=0x0001 c=0x0014" {
+ t.Errorf("String() = %q", s)
+ }
+}
+
+// checkEnum verifies the string and JSON conversions of an enum type
+// against its string map.
+func checkEnum[T ~int](t *testing.T, m map[T]string,
+ toStr func(T) string, fromStr func(string) (T, error), hasJSON bool) {
+
+ t.Helper()
+
+ if len(m) == 0 {
+ t.Fatalf("empty string map")
+ }
+
+ seen := map[string]bool{}
+ for v, name := range m {
+ if seen[name] {
+ t.Errorf("duplicate name %q", name)
+ }
+ seen[name] = true
+
+ if got := toStr(v); got != name {
+ t.Errorf("ToString(%d) = %q, want %q", int(v), got, name)
+ }
+
+ got, err := fromStr(name)
+ if err != nil {
+ t.Errorf("FromString(%q): %v", name, err)
+ } else if got != v {
+ t.Errorf("FromString(%q) = %d, want %d", name, int(got), int(v))
+ }
+
+ if !hasJSON {
+ continue
+ }
+
+ b, err := json.Marshal(v)
+ if err != nil {
+ t.Errorf("Marshal(%d): %v", int(v), err)
+ continue
+ }
+ if want, _ := json.Marshal(name); string(b) != string(want) {
+ t.Errorf("Marshal(%d) = %s, want %s", int(v), b, want)
+ }
+
+ var u T
+ if err := json.Unmarshal(b, &u); err != nil {
+ t.Errorf("Unmarshal(%s): %v", b, err)
+ } else if u != v {
+ t.Errorf("Unmarshal(%s) = %d, want %d", b, int(u), int(v))
+ }
+ }
+
+ if got := toStr(T(12345)); got != "???" {
+ t.Errorf("ToString(unknown) = %q, want ???", got)
+ }
+ if _, err := fromStr("no-such-name"); err == nil {
+ t.Errorf("FromString(unknown): expected error")
+ }
+
+ if !hasJSON {
+ return
+ }
+
+ if b, err := json.Marshal(T(12345)); err != nil ||
+ string(b) != `"???"` {
+
+ t.Errorf("Marshal(unknown) = %s, %v", b, err)
+ }
+
+ for _, bad := range []string{`"no-such-name"`, `0`, `null`} {
+ var u T
+ if err := json.Unmarshal([]byte(bad), &u); err == nil {
+ t.Errorf("Unmarshal(%s): expected error", bad)
+ }
+ }
+}
+
+func TestEnumStrings(t *testing.T) {
+ t.Run("BleAddrType", func(t *testing.T) {
+ checkEnum(t, BleAddrTypeStringMap,
+ BleAddrTypeToString, BleAddrTypeFromString, true)
+ })
+ t.Run("BleScanFilterPolicy", func(t *testing.T) {
+ checkEnum(t, BleScanFilterPolicyStringMap,
+ BleScanFilterPolicyToString, BleScanFilterPolicyFromString, true)
+ })
+ t.Run("BleAdvEventType", func(t *testing.T) {
+ checkEnum(t, BleAdvEventTypeStringMap,
+ BleAdvEventTypeToString, BleAdvEventTypeFromString, true)
+ })
+ t.Run("BleAdvConnMode", func(t *testing.T) {
+ checkEnum(t, BleAdvConnModeStringMap,
+ BleAdvConnModeToString, BleAdvConnModeFromString, true)
+ })
+ t.Run("BleAdvDiscMode", func(t *testing.T) {
+ checkEnum(t, BleAdvDiscModeStringMap,
+ BleAdvDiscModeToString, BleAdvDiscModeFromString, true)
+ })
+ t.Run("BleAdvFilterPolicy", func(t *testing.T) {
+ checkEnum(t, BleAdvFilterPolicyStringMap,
+ BleAdvFilterPolicyToString, BleAdvFilterPolicyFromString, true)
+ })
+ t.Run("BleGattOp", func(t *testing.T) {
+ checkEnum(t, BleGattOpStringMap,
+ BleGattOpToString, BleGattOpFromString, false)
+ })
+ t.Run("BleSvcType", func(t *testing.T) {
+ checkEnum(t, BleSvcTypeStringMap,
+ BleSvcTypeToString, BleSvcTypeFromString, true)
+ })
+ t.Run("BleSmAction", func(t *testing.T) {
+ checkEnum(t, BleSmActionStringMap,
+ BleSmActionToString, BleSmActionFromString, true)
+ })
+ t.Run("BleSmIoCap", func(t *testing.T) {
+ checkEnum(t, BleSmIoCapStringMap,
+ BleSmIoCapToString, BleSmIoCapFromString, true)
+ })
+ t.Run("BleSmKeyDist", func(t *testing.T) {
+ checkEnum(t, BleSmKeyDistStringMap,
+ BleSmKeyDistToString, BleSmKeyDistFromString, true)
+ })
+ t.Run("BleSmAuthReq", func(t *testing.T) {
+ checkEnum(t, BleSmAuthReqStringMap,
+ BleSmAuthReqToString, BleSmAuthReqFromString, true)
+ })
+}
+
+func TestEnumStringMethods(t *testing.T) {
+ tests := []struct {
+ s interface{ String() string }
+ want string
+ }{
+ {BLE_SM_ACTION_NUMCMP, "numcmp"},
+ {BLE_SM_IO_CAP_NO_IO, "no_io"},
+ {BLE_SM_KEY_DIST_ID, "id"},
+ {BleSmAction(99), "???"},
+ }
+
+ for _, tt := range tests {
+ if got := tt.s.String(); got != tt.want {
+ t.Errorf("String() = %q, want %q", got, tt.want)
+ }
+ }
+
+ for v, name := range BleSmAuthReqStringMap {
+ if got := v.String(); got != name {
+ t.Errorf("BleSmAuthReq(%d).String() = %q, want %q",
+ int(v), got, name)
+ }
+ }
+}
(mynewt-newtmgr) 17/20: newtmgr/core: Add unit tests for corefile to ELF conversion
Posted by ja...@apache.org.
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 b92f1adaeadb25dd5d866f420b70c71a685e5f97
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:24:52 2026 +0200
newtmgr/core: Add unit tests for corefile to ELF conversion
Build corefiles in memory, convert them with ConvertFilenames() and
parse the result with debug/elf. Check the ELF header (ET_CORE,
EM_ARM), the PT_NOTE segment carrying the registers in an
NT_PRSTATUS note (including truncation to 18 registers and short
register areas), PT_LOAD segments with their addresses, sizes and
contents, file offsets with padding of odd-length memory areas, and
the image hash. Also cover error handling: empty file, short header,
bad magic, short TLV header, unknown TLV type, truncated TLV data,
register area size not a multiple of 4, and unopenable files.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/core/core_convert_test.go | 312 ++++++++++++++++++++++++++++++++++++++
1 file changed, 312 insertions(+)
diff --git a/newtmgr/core/core_convert_test.go b/newtmgr/core/core_convert_test.go
new file mode 100644
index 0000000..aa38611
--- /dev/null
+++ b/newtmgr/core/core_convert_test.go
@@ -0,0 +1,312 @@
+/**
+ * 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 core
+
+import (
+ "bytes"
+ "debug/elf"
+ "encoding/binary"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+type testTlv struct {
+ typ uint8
+ off uint32
+ data []byte
+}
+
+// buildCore builds a corefile image consisting of a header followed by the
+// given TLVs.
+func buildCore(magic uint32, tlvs []testTlv) []byte {
+ var b bytes.Buffer
+
+ binary.Write(&b, binary.LittleEndian, magic)
+ binary.Write(&b, binary.LittleEndian, uint32(0))
+ for _, t := range tlvs {
+ b.WriteByte(t.typ)
+ b.WriteByte(0)
+ binary.Write(&b, binary.LittleEndian, uint16(len(t.data)))
+ binary.Write(&b, binary.LittleEndian, t.off)
+ b.Write(t.data)
+ }
+
+ return b.Bytes()
+}
+
+// convert writes the corefile to a temporary directory, converts it and
+// returns the converter and the path of the resulting ELF file.
+func convert(t *testing.T, core []byte) (*CoreConvert, string, error) {
+ t.Helper()
+
+ dir := t.TempDir()
+ src := filepath.Join(dir, "core")
+ dst := filepath.Join(dir, "core.elf")
+ if err := os.WriteFile(src, core, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ cc, err := ConvertFilenames(src, dst)
+ return cc, dst, err
+}
+
+func seq(n int, start byte) []byte {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = start + byte(i)
+ }
+ return b
+}
+
+func readProg(t *testing.T, p *elf.Prog) []byte {
+ t.Helper()
+ b, err := io.ReadAll(p.Open())
+ if err != nil {
+ t.Fatalf("reading segment: %v", err)
+ }
+ return b
+}
+
+func TestConvert(t *testing.T) {
+ hash := seq(32, 0xa0)
+ mem1 := seq(16, 0x10)
+ mem2 := seq(5, 0x50) // odd length; padded in the file
+ regs := make([]byte, 4*20)
+ for i := 0; i < 20; i++ {
+ binary.LittleEndian.PutUint32(regs[i*4:], 0x1000+uint32(i))
+ }
+
+ core := buildCore(COREDUMP_MAGIC, []testTlv{
+ {COREDUMP_TLV_IMAGE, 0, hash},
+ {COREDUMP_TLV_REGS, 0, regs},
+ {COREDUMP_TLV_MEM, 0x20000000, mem1},
+ {COREDUMP_TLV_MEM, 0x20001000, mem2},
+ })
+
+ cc, dst, err := convert(t, core)
+ if err != nil {
+ t.Fatalf("ConvertFilenames: %v", err)
+ }
+ if !bytes.Equal(cc.ImageHash, hash) {
+ t.Errorf("ImageHash = %x, want %x", cc.ImageHash, hash)
+ }
+
+ f, err := elf.Open(dst)
+ if err != nil {
+ t.Fatalf("elf.Open: %v", err)
+ }
+ defer f.Close()
+
+ if f.Class != elf.ELFCLASS32 || f.Data != elf.ELFDATA2LSB {
+ t.Errorf("class/data = %v/%v", f.Class, f.Data)
+ }
+ if f.Type != elf.ET_CORE {
+ t.Errorf("Type = %v, want ET_CORE", f.Type)
+ }
+ if f.Machine != elf.EM_ARM {
+ t.Errorf("Machine = %v, want EM_ARM", f.Machine)
+ }
+ if len(f.Sections) != 0 {
+ t.Errorf("expected no sections, got %d", len(f.Sections))
+ }
+ if len(f.Progs) != 3 {
+ t.Fatalf("expected 3 program headers, got %d", len(f.Progs))
+ }
+
+ // Program headers appear in TLV order.
+ note := f.Progs[0]
+ if note.Type != elf.PT_NOTE {
+ t.Errorf("phdr 0 type = %v, want PT_NOTE", note.Type)
+ }
+ nb := readProg(t, note)
+ if len(nb) != 12+8+148 {
+ t.Fatalf("note size = %d, want %d", len(nb), 12+8+148)
+ }
+ if n := binary.LittleEndian.Uint32(nb[0:]); n != 5 {
+ t.Errorf("note namesz = %d, want 5", n)
+ }
+ if n := binary.LittleEndian.Uint32(nb[4:]); n != 148 {
+ t.Errorf("note descsz = %d, want 148", n)
+ }
+ if n := binary.LittleEndian.Uint32(nb[8:]); n != uint32(elf.NT_PRSTATUS) {
+ t.Errorf("note type = %d, want NT_PRSTATUS", n)
+ }
+ if !bytes.Equal(nb[12:20], []byte(".reg\x00\x00\x00\x00")) {
+ t.Errorf("note name = %q", nb[12:20])
+ }
+ // prstatus: 18 words of padding, then 18 registers. Registers beyond
+ // the 18th are dropped.
+ desc := nb[20:]
+ if !bytes.Equal(desc[:72], make([]byte, 72)) {
+ t.Errorf("prstatus padding not zero: %x", desc[:72])
+ }
+ for i := 0; i < 18; i++ {
+ r := binary.LittleEndian.Uint32(desc[72+i*4:])
+ if r != 0x1000+uint32(i) {
+ t.Errorf("reg %d = %#x, want %#x", i, r, 0x1000+i)
+ }
+ }
+ if r := binary.LittleEndian.Uint32(desc[144:]); r != 0 {
+ t.Errorf("trailing word = %#x, want 0", r)
+ }
+
+ wantLoads := []struct {
+ vaddr uint32
+ data []byte
+ }{
+ {0x20000000, mem1},
+ {0x20001000, mem2},
+ }
+ for i, w := range wantLoads {
+ p := f.Progs[i+1]
+ if p.Type != elf.PT_LOAD {
+ t.Errorf("phdr %d type = %v, want PT_LOAD", i+1, p.Type)
+ }
+ if p.Vaddr != uint64(w.vaddr) {
+ t.Errorf("phdr %d vaddr = %#x, want %#x", i+1, p.Vaddr, w.vaddr)
+ }
+ if p.Filesz != uint64(len(w.data)) || p.Memsz != p.Filesz {
+ t.Errorf("phdr %d filesz/memsz = %d/%d, want %d", i+1,
+ p.Filesz, p.Memsz, len(w.data))
+ }
+ if p.Flags != elf.PF_R || p.Align != 4 {
+ t.Errorf("phdr %d flags/align = %v/%d", i+1, p.Flags, p.Align)
+ }
+ if p.Off%4 != 0 {
+ t.Errorf("phdr %d offset %#x not 4-byte aligned", i+1, p.Off)
+ }
+ if got := readProg(t, p); !bytes.Equal(got, w.data) {
+ t.Errorf("phdr %d data = %x, want %x", i+1, got, w.data)
+ }
+ }
+
+ // Segment data is laid out back to back after the program headers,
+ // with the odd-length segment padded to a multiple of 4.
+ const ehdrSz, phdrSz = 52, 32
+ off := uint64(ehdrSz + 3*phdrSz)
+ for i, p := range f.Progs {
+ if p.Off != off {
+ t.Errorf("phdr %d offset = %#x, want %#x", i, p.Off, off)
+ }
+ off += (p.Filesz + 3) &^ 3
+ }
+ st, err := os.Stat(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if uint64(st.Size()) != off {
+ t.Errorf("file size = %d, want %d", st.Size(), off)
+ }
+}
+
+func TestConvertShortRegs(t *testing.T) {
+ regs := make([]byte, 8)
+ binary.LittleEndian.PutUint32(regs[0:], 0xdeadbeef)
+ binary.LittleEndian.PutUint32(regs[4:], 0xcafef00d)
+
+ _, dst, err := convert(t, buildCore(COREDUMP_MAGIC, []testTlv{
+ {COREDUMP_TLV_REGS, 0, regs},
+ }))
+ if err != nil {
+ t.Fatalf("ConvertFilenames: %v", err)
+ }
+
+ f, err := elf.Open(dst)
+ if err != nil {
+ t.Fatalf("elf.Open: %v", err)
+ }
+ defer f.Close()
+
+ if len(f.Progs) != 1 || f.Progs[0].Type != elf.PT_NOTE {
+ t.Fatalf("unexpected program headers: %v", f.Progs)
+ }
+ desc := readProg(t, f.Progs[0])[20:]
+ want := []uint32{0xdeadbeef, 0xcafef00d, 0}
+ for i, w := range want {
+ if r := binary.LittleEndian.Uint32(desc[72+i*4:]); r != w {
+ t.Errorf("reg %d = %#x, want %#x", i, r, w)
+ }
+ }
+}
+
+func TestConvertErrors(t *testing.T) {
+ valid := buildCore(COREDUMP_MAGIC, []testTlv{
+ {COREDUMP_TLV_MEM, 0x100, seq(8, 0)},
+ })
+
+ tests := []struct {
+ name string
+ core []byte
+ err string
+ }{
+ {"empty file", nil, "Error reading: EOF"},
+ {"short header", []byte{0xc3, 0x47, 0x0c, 0x69}, "Short read"},
+ {"bad magic", buildCore(0x12345678, nil),
+ "Source file is not corefile"},
+ {"short tlv header", append(buildCore(COREDUMP_MAGIC, nil),
+ COREDUMP_TLV_MEM, 0, 4), "Short read"},
+ {"unknown tlv", buildCore(COREDUMP_MAGIC, []testTlv{
+ {9, 0, seq(4, 0)},
+ }), "Unknown TLV type"},
+ {"tlv data truncated", valid[:len(valid)-3], "Short file"},
+ {"tlv data missing", valid[:len(valid)-8], "Error reading: EOF"},
+ {"regs not multiple of 4", buildCore(COREDUMP_MAGIC, []testTlv{
+ {COREDUMP_TLV_REGS, 0, seq(6, 0)},
+ }), "Invalid register area size"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, _, err := convert(t, tt.core)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if err.Error() != tt.err {
+ t.Errorf("error = %q, want %q", err.Error(), tt.err)
+ }
+ })
+ }
+}
+
+func TestConvertMissingFiles(t *testing.T) {
+ dir := t.TempDir()
+
+ _, err := ConvertFilenames(filepath.Join(dir, "nope"),
+ filepath.Join(dir, "out"))
+ if err == nil {
+ t.Errorf("expected error for missing source")
+ }
+
+ src := filepath.Join(dir, "core")
+ if err := os.WriteFile(src, buildCore(COREDUMP_MAGIC, nil), 0644); err != nil {
+ t.Fatal(err)
+ }
+ _, err = ConvertFilenames(src, filepath.Join(dir, "nodir", "out"))
+ if err == nil {
+ t.Errorf("expected error for unwritable target")
+ }
+
+ if err := NewCoreConvert().Convert(); err == nil ||
+ err.Error() != "Missing file parameters" {
+ t.Errorf("Convert() without files: err = %v", err)
+ }
+}
(mynewt-newtmgr) 05/20: nmxact/nmxutil: Add unit tests for pure helpers
Posted by ja...@apache.org.
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 48a992209d0ade35b856bca12e6205ffbb364bad
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:25:31 2026 +0200
nmxact/nmxutil: Add unit tests for pure helpers
Cover Fragment for positive MTUs, NMP sequence and CoAP token
wrap-around, SeqToToken, GetNextId, the CBOR encode/decode helpers
(including invalid input), StopAndDrainTimer and the nmxerr error
types with their Is*/To* helpers.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmxutil/nmxerr_test.go | 134 +++++++++++++++++++++++++++++
nmxact/nmxutil/nmxutil_test.go | 185 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 319 insertions(+)
diff --git a/nmxact/nmxutil/nmxerr_test.go b/nmxact/nmxutil/nmxerr_test.go
new file mode 100644
index 0000000..c47d539
--- /dev/null
+++ b/nmxact/nmxutil/nmxerr_test.go
@@ -0,0 +1,134 @@
+/**
+ * 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 nmxutil
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestErrorPredicates(t *testing.T) {
+ type pred struct {
+ name string
+ fn func(error) bool
+ }
+
+ preds := []pred{
+ {"IsRspTimeout", IsRspTimeout},
+ {"IsBleSesnDisconnect", IsBleSesnDisconnect},
+ {"IsSesnAlreadyOpen", IsSesnAlreadyOpen},
+ {"IsSesnClosed", IsSesnClosed},
+ {"IsScanTmo", IsScanTmo},
+ {"IsXport", IsXport},
+ {"IsBleHost", IsBleHost},
+ {"IsAlready", IsAlready},
+ {"IsBleSecurity", IsBleSecurity},
+ }
+
+ // errs[i] must satisfy exactly preds[i].
+ errs := []error{
+ FmtRspTimeoutError("tmo %d", 1),
+ NewBleSesnDisconnectError(0x13, "disc"),
+ NewSesnAlreadyOpenError("open"),
+ NewSesnClosedError("closed"),
+ NewScanTmoError("scan"),
+ NewXportError("xport"),
+ FmtBleHostError(5, "host %s", "err"),
+ NewAlreadyError("already"),
+ NewBleSecurityError("sec"),
+ }
+
+ for i, err := range errs {
+ for j, p := range preds {
+ want := i == j
+ if got := p.fn(err); got != want {
+ t.Errorf("%s(%T) = %v, want %v", p.name, err, got, want)
+ }
+ }
+ }
+
+ for _, p := range preds {
+ if p.fn(nil) {
+ t.Errorf("%s(nil) = true", p.name)
+ }
+ if p.fn(errors.New("plain")) {
+ t.Errorf("%s(plain error) = true", p.name)
+ }
+ }
+}
+
+func TestErrorText(t *testing.T) {
+ tests := []struct {
+ err error
+ want string
+ }{
+ {FmtRspTimeoutError("a=%d b=%s", 1, "x"), "a=1 b=x"},
+ {NewBleSesnDisconnectError(1, "disc"), "disc"},
+ {NewSesnAlreadyOpenError("open"), "open"},
+ {NewSesnClosedError("closed"), "closed"},
+ {NewScanTmoError("scan"), "scan"},
+ {NewXportError("xport"), "xport"},
+ {FmtBleHostError(1, "%s-%d", "h", 2), "h-2"},
+ {NewAlreadyError("already"), "already"},
+ {NewBleSecurityError("sec"), "sec"},
+ }
+
+ for _, tt := range tests {
+ if got := tt.err.Error(); got != tt.want {
+ t.Errorf("%T.Error() = %q, want %q", tt.err, got, tt.want)
+ }
+ }
+}
+
+func TestToBleHost(t *testing.T) {
+ e := NewBleHostError(0x205, "x")
+ if got := ToBleHost(e); got != e {
+ t.Fatalf("ToBleHost() = %v, want %v", got, e)
+ }
+ if got := ToBleHost(e); got.Status != 0x205 {
+ t.Fatalf("Status = %#x, want 0x205", got.Status)
+ }
+ if got := ToBleHost(NewXportError("x")); got != nil {
+ t.Fatalf("ToBleHost(xport) = %v, want nil", got)
+ }
+ if got := ToBleHost(nil); got != nil {
+ t.Fatalf("ToBleHost(nil) = %v, want nil", got)
+ }
+}
+
+func TestToBleSecurity(t *testing.T) {
+ e := NewBleSecurityError("x")
+ if got := ToBleSecurity(e); got != e {
+ t.Fatalf("ToBleSecurity() = %v, want %v", got, e)
+ }
+ if got := ToBleSecurity(NewBleHostError(1, "x")); got != nil {
+ t.Fatalf("ToBleSecurity(host) = %v, want nil", got)
+ }
+ if got := ToBleSecurity(nil); got != nil {
+ t.Fatalf("ToBleSecurity(nil) = %v, want nil", got)
+ }
+}
+
+func TestBleSesnDisconnectReason(t *testing.T) {
+ e := NewBleSesnDisconnectError(0x13, "disc")
+ if e.Reason != 0x13 {
+ t.Fatalf("Reason = %#x, want 0x13", e.Reason)
+ }
+}
diff --git a/nmxact/nmxutil/nmxutil_test.go b/nmxact/nmxutil/nmxutil_test.go
new file mode 100644
index 0000000..13967cd
--- /dev/null
+++ b/nmxact/nmxutil/nmxutil_test.go
@@ -0,0 +1,185 @@
+/**
+ * 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 nmxutil
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestFragment(t *testing.T) {
+ data := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
+
+ tests := []struct {
+ name string
+ b []byte
+ mtu int
+ want [][]byte
+ }{
+ {"empty", []byte{}, 4, [][]byte{}},
+ {"nil", nil, 4, [][]byte{}},
+ {"mtu 1", data[:3], 1, [][]byte{{0}, {1}, {2}}},
+ {"uneven", data, 4,
+ [][]byte{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9}}},
+ {"even", data, 5,
+ [][]byte{{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}}},
+ {"exact", data, 10, [][]byte{data}},
+ {"larger mtu", data, 100, [][]byte{data}},
+ }
+
+ for _, tt := range tests {
+ got := Fragment(tt.b, tt.mtu)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("%s: Fragment() = %v, want %v", tt.name, got, tt.want)
+ }
+
+ // Fragments always reassemble into the original buffer.
+ if j := bytes.Join(got, nil); !bytes.Equal(j, tt.b) {
+ t.Errorf("%s: joined fragments = %v, want %v", tt.name, j, tt.b)
+ }
+ }
+}
+
+func TestNextNmpSeqWrap(t *testing.T) {
+ seqMutex.Lock()
+ nextNmpSeq = 0xfe
+ nmpSeqBeenRead = true
+ seqMutex.Unlock()
+
+ for _, want := range []uint8{0xfe, 0xff, 0x00, 0x01} {
+ if got := NextNmpSeq(); got != want {
+ t.Fatalf("NextNmpSeq() = %#x, want %#x", got, want)
+ }
+ }
+}
+
+func TestNextTokenWrap(t *testing.T) {
+ seqMutex.Lock()
+ nextOicSeq = 0xff
+ oicSeqBeenRead = true
+ seqMutex.Unlock()
+
+ for _, want := range []uint8{0xff, 0x00, 0x01} {
+ got := NextToken()
+ if !bytes.Equal(got, []byte{want}) {
+ t.Fatalf("NextToken() = % x, want %02x", got, want)
+ }
+ }
+}
+
+func TestSeqToToken(t *testing.T) {
+ for _, seq := range []uint8{0, 1, 0x7f, 0xff} {
+ if got := SeqToToken(seq); !bytes.Equal(got, []byte{seq}) {
+ t.Errorf("SeqToToken(%d) = % x", seq, got)
+ }
+ }
+}
+
+func TestGetNextId(t *testing.T) {
+ a := GetNextId()
+ b := GetNextId()
+ if b != a+1 {
+ t.Fatalf("GetNextId() = %d after %d", b, a)
+ }
+}
+
+func TestCborMapRoundTrip(t *testing.T) {
+ in := map[string]interface{}{
+ "s": "str",
+ "b": []byte{1, 2},
+ "t": true,
+ }
+
+ b, err := EncodeCborMap(in)
+ if err != nil {
+ t.Fatalf("EncodeCborMap(): %v", err)
+ }
+
+ out, err := DecodeCborMap(b)
+ if err != nil {
+ t.Fatalf("DecodeCborMap(): %v", err)
+ }
+ if !reflect.DeepEqual(in, out) {
+ t.Fatalf("round trip = %#v, want %#v", out, in)
+ }
+}
+
+func TestCborRoundTrip(t *testing.T) {
+ in := []interface{}{"a", uint64(5), int64(-3)}
+
+ b, err := EncodeCbor(in)
+ if err != nil {
+ t.Fatalf("EncodeCbor(): %v", err)
+ }
+
+ out, err := DecodeCbor(b)
+ if err != nil {
+ t.Fatalf("DecodeCbor(): %v", err)
+ }
+ if !reflect.DeepEqual(in, out) {
+ t.Fatalf("round trip = %#v, want %#v", out, in)
+ }
+}
+
+func TestDecodeCborInvalid(t *testing.T) {
+ bad := [][]byte{
+ // Truncated map.
+ {0xa1, 0x61},
+ // Reserved additional info.
+ {0x1c},
+ }
+
+ for _, b := range bad {
+ if _, err := DecodeCbor(b); err == nil {
+ t.Errorf("DecodeCbor(% x): expected error", b)
+ }
+ if _, err := DecodeCborMap(b); err == nil {
+ t.Errorf("DecodeCborMap(% x): expected error", b)
+ }
+ }
+
+ // Valid CBOR, but not a map.
+ if _, err := DecodeCborMap([]byte{0x01}); err == nil {
+ t.Errorf("DecodeCborMap(int): expected error")
+ }
+}
+
+func TestStopAndDrainTimer(t *testing.T) {
+ // Timer still running.
+ tm := time.NewTimer(time.Hour)
+ StopAndDrainTimer(tm)
+
+ // Timer already expired but its value not received; must not block.
+ tm = time.NewTimer(time.Millisecond)
+ time.Sleep(20 * time.Millisecond)
+
+ done := make(chan struct{})
+ go func() {
+ StopAndDrainTimer(tm)
+ close(done)
+ }()
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatalf("StopAndDrainTimer blocked")
+ }
+}
(mynewt-newtmgr) 18/20: newtmgr/core: Fix panic converting a corefile without segments
Posted by ja...@apache.org.
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 e2365e10cccfdfdc1ef69d1bffef5e3944d78063
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:25:05 2026 +0200
newtmgr/core: Fix panic converting a corefile without segments
setProgHdrOff() took the program header size from cc.phdrs[0], so a
corefile that contains only the header, or only an IMAGE TLV, made
"newtmgr image coreconvert" (and "image coredownload -e") crash with
"index out of range" instead of producing output.
Use the size of the elf.Prog32 type instead. The result is a valid,
if empty, ELF core file with zero program headers, and the image
hash is still reported to the user, which is more useful than an
error for a dump that carries only the image hash.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/core/core_convert.go | 2 +-
newtmgr/core/core_convert_test.go | 41 +++++++++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/newtmgr/core/core_convert.go b/newtmgr/core/core_convert.go
index 4b52a31..453011a 100644
--- a/newtmgr/core/core_convert.go
+++ b/newtmgr/core/core_convert.go
@@ -238,7 +238,7 @@ func (cc *CoreConvert) makeRegInfo(regs []byte) {
func (cc *CoreConvert) setProgHdrOff() {
off := binary.Size(cc.elfHdr)
- off += len(cc.phdrs) * binary.Size(cc.phdrs[0])
+ off += len(cc.phdrs) * binary.Size(elf.Prog32{})
for idx, phdr := range cc.phdrs {
phdr.Off = uint32(off)
diff --git a/newtmgr/core/core_convert_test.go b/newtmgr/core/core_convert_test.go
index aa38611..e286a59 100644
--- a/newtmgr/core/core_convert_test.go
+++ b/newtmgr/core/core_convert_test.go
@@ -310,3 +310,44 @@ func TestConvertMissingFiles(t *testing.T) {
t.Errorf("Convert() without files: err = %v", err)
}
}
+
+// A corefile without memory or register TLVs results in an ELF file with
+// no program headers. This used to panic.
+func TestConvertNoProgHdrs(t *testing.T) {
+ hash := seq(32, 1)
+
+ tests := []struct {
+ name string
+ tlvs []testTlv
+ hash []byte
+ }{
+ {"header only", nil, nil},
+ {"image only", []testTlv{{COREDUMP_TLV_IMAGE, 0, hash}}, hash},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cc, dst, err := convert(t, buildCore(COREDUMP_MAGIC, tt.tlvs))
+ if err != nil {
+ t.Fatalf("ConvertFilenames: %v", err)
+ }
+ if !bytes.Equal(cc.ImageHash, tt.hash) {
+ t.Errorf("ImageHash = %x, want %x", cc.ImageHash, tt.hash)
+ }
+
+ f, err := elf.Open(dst)
+ if err != nil {
+ t.Fatalf("elf.Open: %v", err)
+ }
+ defer f.Close()
+
+ if f.Type != elf.ET_CORE || f.Machine != elf.EM_ARM {
+ t.Errorf("type/machine = %v/%v", f.Type, f.Machine)
+ }
+ if len(f.Progs) != 0 {
+ t.Errorf("expected no program headers, got %d",
+ len(f.Progs))
+ }
+ })
+ }
+}
(mynewt-newtmgr) 01/20: nmxact/nmp: Add unit tests for codec and reassembly
Posted by ja...@apache.org.
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 11f4793943a6cefc0781e892cba7f6ed0cd36a3d
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:21:07 2026 +0200
nmxact/nmp: Add unit tests for codec and reassembly
Cover NMP header encoding and decoding (big-endian Len/Group, short
input), EncodeNmpPlain length handling, the Reassembler (byte by byte,
split header, exact packet, oversize and back-to-back packets) and
DecodeRspBody for every registered response type, including unknown
op/group/id and invalid CBOR.
Also add regression tests for past fixes: echoed requests are ignored
by decodeRsp instead of failing, the NmpBase header never leaks into
the CBOR body, "omitempty" fields are dropped from requests, and
LogEntryType is (un)marshalled as a raw string.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmp/decode_test.go | 240 +++++++++++++++++++++++++++
nmxact/nmp/frag_test.go | 121 ++++++++++++++
nmxact/nmp/log_test.go | 174 ++++++++++++++++++++
nmxact/nmp/nmp_test.go | 401 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 936 insertions(+)
diff --git a/nmxact/nmp/decode_test.go b/nmxact/nmp/decode_test.go
new file mode 100644
index 0000000..dab3fd5
--- /dev/null
+++ b/nmxact/nmp/decode_test.go
@@ -0,0 +1,240 @@
+/**
+ * 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 nmp
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+)
+
+func TestDecodeRspBodyAllTypes(t *testing.T) {
+ body, err := BodyBytes(map[string]interface{}{"rc": 7})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ for ogi, ctor := range rspCtorMap {
+ hdr := &NmpHdr{
+ Op: ogi.Op,
+ Group: ogi.Group,
+ Id: ogi.Id,
+ Seq: 0x5a,
+ Len: uint16(len(body)),
+ }
+
+ rsp, err := DecodeRspBody(hdr, body)
+ if err != nil {
+ t.Errorf("%+v: unexpected error: %v", ogi, err)
+ continue
+ }
+
+ if reflect.TypeOf(rsp) != reflect.TypeOf(ctor()) {
+ t.Errorf("%+v: got type %T, want %T", ogi, rsp, ctor())
+ }
+ if *rsp.Hdr() != *hdr {
+ t.Errorf("%+v: hdr = %+v, want %+v", ogi, *rsp.Hdr(), *hdr)
+ }
+
+ // Every response that has an rc field must have it decoded.
+ f := reflect.ValueOf(rsp).Elem().FieldByName("Rc")
+ if f.IsValid() && f.Int() != 7 {
+ t.Errorf("%+v: Rc = %d, want 7", ogi, f.Int())
+ }
+ }
+}
+
+func TestDecodeRspBodyRoundTrip(t *testing.T) {
+ src := NewImageStateRsp()
+ src.Rc = 0
+ src.SplitStatus = MATCHING
+ src.Images = []ImageStateEntry{
+ {
+ Image: 0,
+ Slot: 1,
+ Version: "1.2.3",
+ Hash: []byte{0xde, 0xad},
+ Bootable: true,
+ Confirmed: true,
+ },
+ }
+
+ body, err := BodyBytes(src)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ hdr := &NmpHdr{
+ Op: NMP_OP_READ_RSP,
+ Group: NMP_GROUP_IMAGE,
+ Id: NMP_ID_IMAGE_STATE,
+ }
+ rsp, err := DecodeRspBody(hdr, body)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, ok := rsp.(*ImageStateRsp)
+ if !ok {
+ t.Fatalf("got type %T, want *ImageStateRsp", rsp)
+ }
+ if got.SplitStatus != MATCHING || len(got.Images) != 1 {
+ t.Fatalf("unexpected rsp %+v", got)
+ }
+ e := got.Images[0]
+ if e.Slot != 1 || e.Version != "1.2.3" ||
+ !bytes.Equal(e.Hash, []byte{0xde, 0xad}) ||
+ !e.Bootable || !e.Confirmed || e.Active {
+
+ t.Fatalf("unexpected image entry %+v", e)
+ }
+}
+
+func TestDecodeRspBodyUnknown(t *testing.T) {
+ body, _ := BodyBytes(map[string]interface{}{"rc": 0})
+
+ hdrs := []NmpHdr{
+ {Op: NMP_OP_READ_RSP, Group: NMP_GROUP_PERUSER, Id: 0},
+ {Op: NMP_OP_READ_RSP, Group: NMP_GROUP_DEFAULT, Id: 0xff},
+ // Echo is only defined for write.
+ {Op: NMP_OP_READ_RSP, Group: NMP_GROUP_DEFAULT, Id: NMP_ID_DEF_ECHO},
+ // Requests never decode as responses.
+ {Op: NMP_OP_WRITE, Group: NMP_GROUP_DEFAULT, Id: NMP_ID_DEF_ECHO},
+ }
+
+ for _, h := range hdrs {
+ h := h
+ rsp, err := DecodeRspBody(&h, body)
+ if err == nil {
+ t.Errorf("%+v: expected error, got %+v", h, rsp)
+ }
+ if rsp != nil {
+ t.Errorf("%+v: expected nil rsp, got %+v", h, rsp)
+ }
+ }
+}
+
+func TestDecodeRspBodyInvalidCbor(t *testing.T) {
+ hdr := &NmpHdr{
+ Op: NMP_OP_WRITE_RSP,
+ Group: NMP_GROUP_DEFAULT,
+ Id: NMP_ID_DEF_ECHO,
+ }
+
+ bodies := [][]byte{
+ // Truncated map.
+ {0xa1, 0x62, 'r', 'c'},
+ // Not a map.
+ {0x18},
+ }
+ for _, b := range bodies {
+ if rsp, err := DecodeRspBody(hdr, b); err == nil {
+ t.Errorf("% x: expected error, got %+v", b, rsp)
+ }
+ }
+}
+
+func TestDecodeRsp(t *testing.T) {
+ rsp := NewEchoRsp()
+ rsp.Payload = "hi"
+ body, err := BodyBytes(rsp)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ hdr := NmpHdr{
+ Op: NMP_OP_WRITE_RSP,
+ Group: NMP_GROUP_DEFAULT,
+ Id: NMP_ID_DEF_ECHO,
+ Seq: 9,
+ Len: uint16(len(body)),
+ }
+ pkt := append(hdr.Bytes(), body...)
+
+ got, err := decodeRsp(pkt)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ er, ok := got.(*EchoRsp)
+ if !ok {
+ t.Fatalf("got type %T, want *EchoRsp", got)
+ }
+ if er.Payload != "hi" || er.Hdr().Seq != 9 {
+ t.Fatalf("unexpected rsp %+v hdr %+v", er, *er.Hdr())
+ }
+}
+
+// Devices that echo received requests (e.g., over serial) must not cause a
+// crash or an error; requests are silently ignored. Regression test for
+// "nmxact; fix crash when sent requests are being echoed".
+func TestDecodeRspIgnoresRequests(t *testing.T) {
+ for _, op := range []uint8{NMP_OP_READ, NMP_OP_WRITE} {
+ req := NewEchoReq()
+ req.Payload = "echoed"
+ msg := req.Msg()
+ msg.Hdr.Op = op
+
+ pkt, err := EncodeNmpPlain(msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ rsp, err := decodeRsp(pkt)
+ if err != nil || rsp != nil {
+ t.Fatalf("op=%d: decodeRsp() = (%v, %v), want (nil, nil)",
+ op, rsp, err)
+ }
+ }
+}
+
+func TestDecodeRspShort(t *testing.T) {
+ if rsp, err := decodeRsp([]byte{1, 2, 3}); err == nil {
+ t.Fatalf("expected error, got %+v", rsp)
+ }
+}
+
+func TestRegisterResponseHandler(t *testing.T) {
+ ogi := Ogi{NMP_OP_WRITE_RSP, NMP_GROUP_PERUSER + 1, 0x77}
+ defer delete(rspCtorMap, ogi)
+
+ ctor := func() NmpRsp { return NewEchoRsp() }
+ RegisterResponseHandler(ogi, ctor)
+
+ body, _ := BodyBytes(map[string]interface{}{"r": "x"})
+ hdr := &NmpHdr{Op: ogi.Op, Group: ogi.Group, Id: ogi.Id}
+ rsp, err := DecodeRspBody(hdr, body)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if er, ok := rsp.(*EchoRsp); !ok || er.Payload != "x" {
+ t.Fatalf("unexpected rsp %#v", rsp)
+ }
+
+ // An existing handler is not replaced.
+ RegisterResponseHandler(ogi,
+ func() NmpRsp { return NewResetRsp() })
+ rsp, err = DecodeRspBody(hdr, body)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if _, ok := rsp.(*EchoRsp); !ok {
+ t.Fatalf("handler was replaced; got %T", rsp)
+ }
+}
diff --git a/nmxact/nmp/frag_test.go b/nmxact/nmp/frag_test.go
new file mode 100644
index 0000000..d775a17
--- /dev/null
+++ b/nmxact/nmp/frag_test.go
@@ -0,0 +1,121 @@
+/**
+ * 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 nmp
+
+import (
+ "bytes"
+ "testing"
+)
+
+// buildPkt builds a raw NMP packet with the given body.
+func buildPkt(seq uint8, body []byte) []byte {
+ hdr := NmpHdr{
+ Op: NMP_OP_READ_RSP,
+ Group: NMP_GROUP_DEFAULT,
+ Id: NMP_ID_DEF_ECHO,
+ Seq: seq,
+ Len: uint16(len(body)),
+ }
+ return append(hdr.Bytes(), body...)
+}
+
+func TestReassemblerExact(t *testing.T) {
+ pkt := buildPkt(1, []byte{1, 2, 3, 4})
+
+ r := NewReassembler()
+ got := r.RxFrag(pkt)
+ if !bytes.Equal(got, pkt) {
+ t.Fatalf("RxFrag() = % x, want % x", got, pkt)
+ }
+}
+
+func TestReassemblerEmptyBody(t *testing.T) {
+ pkt := buildPkt(1, nil)
+
+ r := NewReassembler()
+ got := r.RxFrag(pkt)
+ if !bytes.Equal(got, pkt) {
+ t.Fatalf("RxFrag() = % x, want % x", got, pkt)
+ }
+}
+
+func TestReassemblerByteByByte(t *testing.T) {
+ pkt := buildPkt(7, []byte("0123456789abcdef"))
+
+ r := NewReassembler()
+ for i := 0; i < len(pkt)-1; i++ {
+ if got := r.RxFrag(pkt[i : i+1]); got != nil {
+ t.Fatalf("byte %d: unexpected packet % x", i, got)
+ }
+ }
+
+ got := r.RxFrag(pkt[len(pkt)-1:])
+ if !bytes.Equal(got, pkt) {
+ t.Fatalf("RxFrag() = % x, want % x", got, pkt)
+ }
+}
+
+func TestReassemblerSplitHeader(t *testing.T) {
+ pkt := buildPkt(3, []byte{9, 8, 7, 6, 5})
+
+ for split := 1; split < len(pkt); split++ {
+ r := NewReassembler()
+ if got := r.RxFrag(pkt[:split]); got != nil {
+ t.Fatalf("split=%d: unexpected packet after first frag", split)
+ }
+ got := r.RxFrag(pkt[split:])
+ if !bytes.Equal(got, pkt) {
+ t.Fatalf("split=%d: RxFrag() = % x, want % x", split, got, pkt)
+ }
+ }
+}
+
+func TestReassemblerOversize(t *testing.T) {
+ pkt := buildPkt(1, []byte{1, 2})
+ bad := append(append([]byte{}, pkt...), 0xff)
+
+ r := NewReassembler()
+ if got := r.RxFrag(bad); got != nil {
+ t.Fatalf("oversize packet must be discarded, got % x", got)
+ }
+
+ // The reassembler must have been reset; a following valid packet is
+ // received correctly.
+ got := r.RxFrag(pkt)
+ if !bytes.Equal(got, pkt) {
+ t.Fatalf("RxFrag() = % x, want % x", got, pkt)
+ }
+}
+
+func TestReassemblerBackToBack(t *testing.T) {
+ pkt1 := buildPkt(1, []byte{1, 2, 3})
+ pkt2 := buildPkt(2, []byte{4, 5, 6, 7})
+
+ r := NewReassembler()
+ if got := r.RxFrag(pkt1); !bytes.Equal(got, pkt1) {
+ t.Fatalf("pkt1: RxFrag() = % x, want % x", got, pkt1)
+ }
+ if got := r.RxFrag(pkt2[:5]); got != nil {
+ t.Fatalf("pkt2: unexpected packet % x", got)
+ }
+ if got := r.RxFrag(pkt2[5:]); !bytes.Equal(got, pkt2) {
+ t.Fatalf("pkt2: RxFrag() = % x, want % x", got, pkt2)
+ }
+}
diff --git a/nmxact/nmp/log_test.go b/nmxact/nmp/log_test.go
new file mode 100644
index 0000000..b4bc17a
--- /dev/null
+++ b/nmxact/nmp/log_test.go
@@ -0,0 +1,174 @@
+/**
+ * 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 nmp
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestLogEntryTypeString(t *testing.T) {
+ for typ, name := range LogEntryTypeStringMap {
+ if got := LogEntryTypeToString(typ); got != name {
+ t.Errorf("LogEntryTypeToString(%d) = %q, want %q",
+ int(typ), got, name)
+ }
+ if got := typ.String(); got != name {
+ t.Errorf("String(%d) = %q, want %q", int(typ), got, name)
+ }
+
+ got, err := LogEntryTypeFromString(name)
+ if err != nil {
+ t.Errorf("LogEntryTypeFromString(%q): %v", name, err)
+ }
+ if got != typ {
+ t.Errorf("LogEntryTypeFromString(%q) = %d, want %d",
+ name, int(got), int(typ))
+ }
+ }
+
+ if got := LogEntryTypeToString(LogEntryType(42)); got != "???" {
+ t.Errorf("unknown type: got %q, want ???", got)
+ }
+ if _, err := LogEntryTypeFromString("bogus"); err == nil {
+ t.Errorf("expected error for unknown string")
+ }
+}
+
+// LogEntryType must be marshalled as the raw string, not as a nested CBOR
+// string. Regression test for "Fix [un]marshal of log-entry-type".
+func TestLogEntryTypeBinary(t *testing.T) {
+ for typ, name := range LogEntryTypeStringMap {
+ b, err := typ.MarshalBinary()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !bytes.Equal(b, []byte(name)) {
+ t.Errorf("MarshalBinary(%d) = % x, want %q", int(typ), b, name)
+ }
+
+ var got LogEntryType
+ if err := got.UnmarshalBinary([]byte(name)); err != nil {
+ t.Errorf("UnmarshalBinary(%q): %v", name, err)
+ }
+ if got != typ {
+ t.Errorf("UnmarshalBinary(%q) = %d, want %d",
+ name, int(got), int(typ))
+ }
+ }
+
+ var l LogEntryType
+ if err := l.UnmarshalBinary([]byte("nope")); err == nil {
+ t.Errorf("expected error for unknown type")
+ }
+}
+
+func TestLogShowRspDecode(t *testing.T) {
+ // Body as sent by a device: the entry type is a CBOR text string.
+ body, err := BodyBytes(map[string]interface{}{
+ "rc": 0,
+ "next_index": 3,
+ "logs": []interface{}{
+ map[string]interface{}{
+ "name": "reboot_log",
+ "type": 1,
+ "entries": []interface{}{
+ map[string]interface{}{
+ "index": 1,
+ "ts": 1000,
+ "module": 2,
+ "level": 1,
+ "type": "cbor",
+ "msg": []byte{0xa0},
+ },
+ map[string]interface{}{
+ "index": 2,
+ "ts": 2000,
+ "module": 3,
+ "level": 0,
+ "type": "str",
+ "msg": []byte("hello"),
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ hdr := &NmpHdr{Op: NMP_OP_READ_RSP, Group: NMP_GROUP_LOG,
+ Id: NMP_ID_LOG_SHOW}
+ rsp, err := DecodeRspBody(hdr, body)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ r := rsp.(*LogShowRsp)
+ if r.NextIndex != 3 || len(r.Logs) != 1 ||
+ len(r.Logs[0].Entries) != 2 {
+
+ t.Fatalf("unexpected rsp %+v", r)
+ }
+ e := r.Logs[0].Entries
+ if e[0].Type != LOG_ENTRY_TYPE_CBOR || e[1].Type != LOG_ENTRY_TYPE_STRING {
+ t.Fatalf("entry types = %v, %v; want cbor, str", e[0].Type, e[1].Type)
+ }
+ if string(e[1].Msg) != "hello" || e[0].Timestamp != 1000 {
+ t.Fatalf("unexpected entries %+v", e)
+ }
+
+ // Encoding the decoded response and decoding it again yields the same
+ // entry types.
+ body2, err := BodyBytes(r)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ rsp2, err := DecodeRspBody(hdr, body2)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ e2 := rsp2.(*LogShowRsp).Logs[0].Entries
+ if e2[0].Type != LOG_ENTRY_TYPE_CBOR || e2[1].Type != LOG_ENTRY_TYPE_STRING {
+ t.Fatalf("round trip entry types = %v, %v", e2[0].Type, e2[1].Type)
+ }
+}
+
+func TestLogNameHelpers(t *testing.T) {
+ tests := []struct {
+ fn func(int) string
+ in int
+ want string
+ }{
+ {LogModuleToString, MODULE_NEWTMGR, "NEWTMGR"},
+ {LogModuleToString, MODULE_TEST, "TEST"},
+ {LogModuleToString, 7, "CUSTOM"},
+ {LogLevelToString, LEVEL_WARN, "WARN"},
+ {LogLevelToString, 100, "CUSTOM"},
+ {LogTypeToString, STORAGE_LOG, "STORAGE"},
+ {LogTypeToString, 9, "UNDEFINED"},
+ }
+
+ for _, tt := range tests {
+ if got := tt.fn(tt.in); got != tt.want {
+ t.Errorf("%d: got %q, want %q", tt.in, got, tt.want)
+ }
+ }
+}
diff --git a/nmxact/nmp/nmp_test.go b/nmxact/nmp/nmp_test.go
new file mode 100644
index 0000000..bca2a2f
--- /dev/null
+++ b/nmxact/nmp/nmp_test.go
@@ -0,0 +1,401 @@
+/**
+ * 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 nmp
+
+import (
+ "bytes"
+ "reflect"
+ "sort"
+ "testing"
+
+ "github.com/ugorji/go/codec"
+)
+
+func decodeCborMap(t *testing.T, b []byte) map[string]interface{} {
+ t.Helper()
+
+ m := map[string]interface{}{}
+ dec := codec.NewDecoderBytes(b, new(codec.CborHandle))
+ if err := dec.Decode(&m); err != nil {
+ t.Fatalf("failed to decode CBOR body: %v", err)
+ }
+ return m
+}
+
+func sortedKeys(m map[string]interface{}) []string {
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+func TestNmpHdrBytes(t *testing.T) {
+ hdr := NmpHdr{
+ Op: NMP_OP_WRITE,
+ Flags: 0xa5,
+ Len: 0x1234,
+ Group: 0xabcd,
+ Seq: 0x42,
+ Id: 0x07,
+ }
+
+ want := []byte{0x02, 0xa5, 0x12, 0x34, 0xab, 0xcd, 0x42, 0x07}
+ got := hdr.Bytes()
+ if !bytes.Equal(got, want) {
+ t.Fatalf("Bytes() = % x, want % x", got, want)
+ }
+ if len(got) != NMP_HDR_SIZE {
+ t.Fatalf("len(Bytes()) = %d, want %d", len(got), NMP_HDR_SIZE)
+ }
+}
+
+func TestDecodeNmpHdr(t *testing.T) {
+ data := []byte{0x03, 0x01, 0x00, 0x10, 0x00, 0x40, 0xff, 0x05}
+ hdr, err := DecodeNmpHdr(data)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ want := NmpHdr{
+ Op: NMP_OP_WRITE_RSP,
+ Flags: 0x01,
+ Len: 0x0010,
+ Group: NMP_GROUP_PERUSER,
+ Seq: 0xff,
+ Id: 0x05,
+ }
+ if *hdr != want {
+ t.Fatalf("DecodeNmpHdr() = %+v, want %+v", *hdr, want)
+ }
+
+ // Trailing body bytes are ignored.
+ hdr2, err := DecodeNmpHdr(append(data, 0xaa, 0xbb))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if *hdr2 != want {
+ t.Fatalf("DecodeNmpHdr() = %+v, want %+v", *hdr2, want)
+ }
+}
+
+func TestDecodeNmpHdrShort(t *testing.T) {
+ for n := 0; n < NMP_HDR_SIZE; n++ {
+ hdr, err := DecodeNmpHdr(make([]byte, n))
+ if err == nil {
+ t.Errorf("len=%d: expected error, got hdr %+v", n, hdr)
+ }
+ if hdr != nil {
+ t.Errorf("len=%d: expected nil hdr, got %+v", n, hdr)
+ }
+ }
+}
+
+func TestNmpHdrRoundTrip(t *testing.T) {
+ hdrs := []NmpHdr{
+ {},
+ {Op: 3, Flags: 0xff, Len: 0xffff, Group: 0xffff, Seq: 0xff, Id: 0xff},
+ {Op: 1, Flags: 0, Len: 1, Group: 256, Seq: 1, Id: 1},
+ }
+
+ for _, h := range hdrs {
+ got, err := DecodeNmpHdr(h.Bytes())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if *got != h {
+ t.Errorf("round trip: got %+v, want %+v", *got, h)
+ }
+ }
+}
+
+func TestEncodeNmpPlain(t *testing.T) {
+ req := NewEchoReq()
+ req.Payload = "hello"
+ msg := req.Msg()
+
+ // Garbage length must be overwritten by the encoder.
+ msg.Hdr.Len = 0xbeef
+
+ data, err := EncodeNmpPlain(msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ hdr, err := DecodeNmpHdr(data)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ body := data[NMP_HDR_SIZE:]
+ if int(hdr.Len) != len(body) {
+ t.Fatalf("hdr.Len=%d, body len=%d", hdr.Len, len(body))
+ }
+ if msg.Hdr.Len != hdr.Len {
+ t.Fatalf("msg.Hdr.Len=%d not updated, want %d", msg.Hdr.Len, hdr.Len)
+ }
+ if hdr.Op != NMP_OP_WRITE || hdr.Group != NMP_GROUP_DEFAULT ||
+ hdr.Id != NMP_ID_DEF_ECHO || hdr.Seq != req.Hdr().Seq {
+ t.Fatalf("unexpected header: %+v", *hdr)
+ }
+
+ m := decodeCborMap(t, body)
+ if !reflect.DeepEqual(sortedKeys(m), []string{"d"}) {
+ t.Fatalf("unexpected body keys: %v", sortedKeys(m))
+ }
+ if m["d"] != "hello" {
+ t.Fatalf("d=%v, want hello", m["d"])
+ }
+}
+
+func TestFillNmpReqWithSeq(t *testing.T) {
+ r := NewImageUploadReqWithSeq(0x42)
+ want := NmpHdr{
+ Op: NMP_OP_WRITE,
+ Group: NMP_GROUP_IMAGE,
+ Id: NMP_ID_IMAGE_UPLOAD,
+ Seq: 0x42,
+ }
+ if *r.Hdr() != want {
+ t.Fatalf("hdr = %+v, want %+v", *r.Hdr(), want)
+ }
+}
+
+func TestNmpBaseSetHdr(t *testing.T) {
+ var b NmpBase
+ h := NmpHdr{Op: 1, Group: 2, Id: 3, Seq: 4}
+ b.SetHdr(&h)
+ h.Seq = 5
+ if b.Hdr().Seq != 4 {
+ t.Fatalf("SetHdr must copy the header")
+ }
+}
+
+// Every request constructor must set the correct op/group/id, and encoding
+// a request must only produce the request's own CBOR fields: the NmpBase
+// header must never leak into the body, and "omitempty" fields must be
+// left out when empty.
+func TestNewReqs(t *testing.T) {
+ tests := []struct {
+ name string
+ req NmpReq
+ op uint8
+ group uint16
+ id uint8
+ keys []string
+ }{
+ {"echo", NewEchoReq(),
+ NMP_OP_WRITE, NMP_GROUP_DEFAULT, NMP_ID_DEF_ECHO,
+ []string{"d"}},
+ {"taskstat", NewTaskStatReq(),
+ NMP_OP_READ, NMP_GROUP_DEFAULT, NMP_ID_DEF_TASKSTAT,
+ []string{}},
+ {"mpstat", NewMempoolStatReq(),
+ NMP_OP_READ, NMP_GROUP_DEFAULT, NMP_ID_DEF_MPSTAT,
+ []string{}},
+ {"datetime read", NewDateTimeReadReq(),
+ NMP_OP_READ, NMP_GROUP_DEFAULT, NMP_ID_DEF_DATETIME_STR,
+ []string{}},
+ {"datetime write", NewDateTimeWriteReq(),
+ NMP_OP_WRITE, NMP_GROUP_DEFAULT, NMP_ID_DEF_DATETIME_STR,
+ []string{"datetime"}},
+ {"reset", NewResetReq(),
+ NMP_OP_WRITE, NMP_GROUP_DEFAULT, NMP_ID_DEF_RESET,
+ []string{}},
+ {"image upload", NewImageUploadReq(),
+ NMP_OP_WRITE, NMP_GROUP_IMAGE, NMP_ID_IMAGE_UPLOAD,
+ []string{"data", "image", "off"}},
+ {"image state read", NewImageStateReadReq(),
+ NMP_OP_READ, NMP_GROUP_IMAGE, NMP_ID_IMAGE_STATE,
+ []string{}},
+ {"image state write", NewImageStateWriteReq(),
+ NMP_OP_WRITE, NMP_GROUP_IMAGE, NMP_ID_IMAGE_STATE,
+ []string{"confirm", "hash"}},
+ {"core list", NewCoreListReq(),
+ NMP_OP_READ, NMP_GROUP_IMAGE, NMP_ID_IMAGE_CORELIST,
+ []string{}},
+ {"core load", NewCoreLoadReq(),
+ NMP_OP_READ, NMP_GROUP_IMAGE, NMP_ID_IMAGE_CORELOAD,
+ []string{"off"}},
+ {"core erase", NewCoreEraseReq(),
+ NMP_OP_WRITE, NMP_GROUP_IMAGE, NMP_ID_IMAGE_CORELOAD,
+ []string{}},
+ {"image erase", NewImageEraseReq(),
+ NMP_OP_WRITE, NMP_GROUP_IMAGE, NMP_ID_IMAGE_ERASE,
+ []string{}},
+ {"stat read", NewStatReadReq(),
+ NMP_OP_READ, NMP_GROUP_STAT, NMP_ID_STAT_READ,
+ []string{"name"}},
+ {"stat list", NewStatListReq(),
+ NMP_OP_READ, NMP_GROUP_STAT, NMP_ID_STAT_LIST,
+ []string{}},
+ {"log show", NewLogShowReq(),
+ NMP_OP_READ, NMP_GROUP_LOG, NMP_ID_LOG_SHOW,
+ []string{"index", "log_name", "ts"}},
+ {"log list", NewLogListReq(),
+ NMP_OP_READ, NMP_GROUP_LOG, NMP_ID_LOG_LIST,
+ []string{}},
+ {"log module list", NewLogModuleListReq(),
+ NMP_OP_READ, NMP_GROUP_LOG, NMP_ID_LOG_MODULE_LIST,
+ []string{}},
+ {"log level list", NewLogLevelListReq(),
+ NMP_OP_READ, NMP_GROUP_LOG, NMP_ID_LOG_LEVEL_LIST,
+ []string{}},
+ {"log clear", NewLogClearReq(),
+ NMP_OP_WRITE, NMP_GROUP_LOG, NMP_ID_LOG_CLEAR,
+ []string{}},
+ {"crash", NewCrashReq(),
+ NMP_OP_WRITE, NMP_GROUP_CRASH, NMP_ID_CRASH_TRIGGER,
+ []string{"t"}},
+ {"run test", NewRunTestReq(),
+ NMP_OP_WRITE, NMP_GROUP_RUN, NMP_ID_RUN_TEST,
+ []string{"testname", "token"}},
+ {"run list", NewRunListReq(),
+ NMP_OP_READ, NMP_GROUP_RUN, NMP_ID_RUN_LIST,
+ []string{}},
+ {"fs download", NewFsDownloadReq(),
+ NMP_OP_READ, NMP_GROUP_FS, NMP_ID_FS_FILE,
+ []string{"name", "off"}},
+ {"fs upload", NewFsUploadReq(),
+ NMP_OP_WRITE, NMP_GROUP_FS, NMP_ID_FS_FILE,
+ []string{"data", "len", "name", "off"}},
+ {"config read", NewConfigReadReq(),
+ NMP_OP_READ, NMP_GROUP_CONFIG, NMP_ID_CONFIG_VAL,
+ []string{"name"}},
+ {"config write", NewConfigWriteReq(),
+ NMP_OP_WRITE, NMP_GROUP_CONFIG, NMP_ID_CONFIG_VAL,
+ []string{}},
+ {"shell exec", NewShellExecReq(),
+ NMP_OP_WRITE, NMP_GROUP_SHELL, NMP_ID_SHELL_EXEC,
+ []string{"argv"}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ hdr := tt.req.Hdr()
+ if hdr.Op != tt.op || hdr.Group != tt.group || hdr.Id != tt.id {
+ t.Fatalf("hdr = %+v, want op=%d group=%d id=%d",
+ *hdr, tt.op, tt.group, tt.id)
+ }
+ if hdr.Flags != 0 || hdr.Len != 0 {
+ t.Fatalf("hdr = %+v, want flags=0 len=0", *hdr)
+ }
+
+ msg := tt.req.Msg()
+ if msg.Hdr != *hdr {
+ t.Fatalf("Msg().Hdr = %+v, want %+v", msg.Hdr, *hdr)
+ }
+
+ data, err := EncodeNmpPlain(msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ m := decodeCborMap(t, data[NMP_HDR_SIZE:])
+ if got := sortedKeys(m); !reflect.DeepEqual(got, tt.keys) {
+ t.Fatalf("body keys = %v, want %v", got, tt.keys)
+ }
+ })
+ }
+}
+
+func TestImageUploadReqOmitEmpty(t *testing.T) {
+ // First chunk: len, sha and upgrade are present.
+ r := NewImageUploadReq()
+ r.Off = 0
+ r.Len = 1000
+ r.DataSha = []byte{1, 2, 3}
+ r.Upgrade = true
+ r.Data = []byte{0xaa}
+
+ data, err := EncodeNmpPlain(r.Msg())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m := decodeCborMap(t, data[NMP_HDR_SIZE:])
+ want := []string{"data", "image", "len", "off", "sha", "upgrade"}
+ if got := sortedKeys(m); !reflect.DeepEqual(got, want) {
+ t.Fatalf("body keys = %v, want %v", got, want)
+ }
+
+ // Subsequent chunk: only image, off and data.
+ r = NewImageUploadReq()
+ r.Off = 500
+ r.Data = []byte{0xbb}
+
+ data, err = EncodeNmpPlain(r.Msg())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m = decodeCborMap(t, data[NMP_HDR_SIZE:])
+ want = []string{"data", "image", "off"}
+ if got := sortedKeys(m); !reflect.DeepEqual(got, want) {
+ t.Fatalf("body keys = %v, want %v", got, want)
+ }
+}
+
+func TestConfigWriteReqOmitEmpty(t *testing.T) {
+ tests := []struct {
+ name string
+ val string
+ save bool
+ keys []string
+ }{
+ {"", "", true, []string{"save"}},
+ {"foo/bar", "1", false, []string{"name", "val"}},
+ {"foo/bar", "1", true, []string{"name", "save", "val"}},
+ }
+
+ for _, tt := range tests {
+ r := NewConfigWriteReq()
+ r.Name = tt.name
+ r.Val = tt.val
+ r.Save = tt.save
+
+ data, err := EncodeNmpPlain(r.Msg())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ m := decodeCborMap(t, data[NMP_HDR_SIZE:])
+ if got := sortedKeys(m); !reflect.DeepEqual(got, tt.keys) {
+ t.Errorf("%+v: body keys = %v, want %v", tt, got, tt.keys)
+ }
+ }
+}
+
+func TestSplitStatusString(t *testing.T) {
+ tests := []struct {
+ s SplitStatus
+ want string
+ }{
+ {NOT_APPLICABLE, "N/A"},
+ {NOT_MATCHING, "non-matching"},
+ {MATCHING, "matching"},
+ {SplitStatus(99), "Unknown!"},
+ }
+
+ for _, tt := range tests {
+ if got := tt.s.String(); got != tt.want {
+ t.Errorf("SplitStatus(%d).String() = %q, want %q",
+ int(tt.s), got, tt.want)
+ }
+ }
+}
(mynewt-newtmgr) 19/20: newtmgr/bll: Add unit tests for MTU exchange and UUID helpers
Posted by ja...@apache.org.
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 9620557f94a847ce5c9c4ccca5260c7c944798f4
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:25:47 2026 +0200
newtmgr/bll: Add unit tests for MTU exchange and UUID helpers
Test exchangeMtu() with a fake ble.Client: the reported MTU is used
as-is, a bogus value below the ATT default is replaced by 185, the
-m override takes precedence and is range checked, and client errors
are propagated. The macOS retry path (which sleeps) is skipped on
darwin.
Also test UuidFromBllUuid() for 16-bit, 128-bit and invalid UUIDs,
cross-checked against bledefs.ParseUuid(), and findChr() lookups in
a ble.Profile literal.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/bll/bll_test.go | 239 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 239 insertions(+)
diff --git a/newtmgr/bll/bll_test.go b/newtmgr/bll/bll_test.go
new file mode 100644
index 0000000..432eae6
--- /dev/null
+++ b/newtmgr/bll/bll_test.go
@@ -0,0 +1,239 @@
+//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 bll
+
+import (
+ "errors"
+ "runtime"
+ "testing"
+
+ "github.com/JuulLabs-OSS/ble"
+
+ "mynewt.apache.org/newtmgr/newtmgr/nmutil"
+ "mynewt.apache.org/newtmgr/nmxact/bledefs"
+)
+
+// fakeClient is a ble.Client whose ExchangeMTU reports a fixed MTU. Any
+// other method panics (nil embedded interface).
+type fakeClient struct {
+ ble.Client
+ mtu int
+ err error
+ calls []int
+}
+
+func (c *fakeClient) ExchangeMTU(rxMTU int) (int, error) {
+ c.calls = append(c.calls, rxMTU)
+ return c.mtu, c.err
+}
+
+func setMtuOverride(t *testing.T, mtu int) {
+ t.Helper()
+ old := nmutil.MtuOverride
+ nmutil.MtuOverride = mtu
+ t.Cleanup(func() { nmutil.MtuOverride = old })
+}
+
+func TestExchangeMtu(t *testing.T) {
+ tests := []struct {
+ name string
+ override int
+ reported int
+ want uint16
+ wantErr string
+ }{
+ {"reported", 0, 247, 247, ""},
+ {"reported default", 0, 23, 23, ""},
+ {"too small clamped", 0, 17, 185, ""},
+ {"override", 100, 247, 100, ""},
+ {"override small reported", 100, 17, 100, ""},
+ {"override minimum", 23, 247, 23, ""},
+ {"override too small", 20, 247, 0, "MTU should be at least 23"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if runtime.GOOS == "darwin" &&
+ tt.reported <= bledefs.BLE_ATT_MTU_DFLT {
+ // macOS retries with one second sleeps.
+ t.Skip("skipping macOS MTU retry path")
+ }
+ setMtuOverride(t, tt.override)
+
+ cln := &fakeClient{mtu: tt.reported}
+ mtu, err := exchangeMtu(cln, 512)
+ if tt.wantErr != "" {
+ if err == nil || err.Error() != tt.wantErr {
+ t.Fatalf("err = %v, want %q", err, tt.wantErr)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if mtu != tt.want {
+ t.Errorf("mtu = %d, want %d", mtu, tt.want)
+ }
+ if len(cln.calls) != 1 || cln.calls[0] != 512 {
+ t.Errorf("ExchangeMTU calls = %v, want [512]", cln.calls)
+ }
+ })
+ }
+}
+
+func TestExchangeMtuError(t *testing.T) {
+ setMtuOverride(t, 100)
+
+ want := errors.New("exchange failed")
+ cln := &fakeClient{mtu: 247, err: want}
+ mtu, err := exchangeMtu(cln, 256)
+ if err != want {
+ t.Errorf("err = %v, want %v", err, want)
+ }
+ if mtu != 0 {
+ t.Errorf("mtu = %d, want 0", mtu)
+ }
+ if len(cln.calls) != 1 {
+ t.Errorf("ExchangeMTU called %d times, want 1", len(cln.calls))
+ }
+}
+
+const (
+ testSvcUuid = "8d53dc1d-1db7-4cd3-868b-8a527460aa84"
+ testChrUuid = "da2e7828-fbce-4e01-ae9e-261174997c48"
+)
+
+func mustParseUuid(t *testing.T, s string) bledefs.BleUuid {
+ t.Helper()
+ u, err := bledefs.ParseUuid(s)
+ if err != nil {
+ t.Fatalf("ParseUuid(%q): %v", s, err)
+ }
+ return u
+}
+
+func TestUuidFromBllUuid(t *testing.T) {
+ u, err := UuidFromBllUuid(ble.UUID16(0x180a))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if u != (bledefs.BleUuid{U16: 0x180a}) {
+ t.Errorf("16-bit: got %+v", u)
+ }
+
+ for _, s := range []string{testSvcUuid, testChrUuid} {
+ u, err := UuidFromBllUuid(ble.MustParse(s))
+ if err != nil {
+ t.Fatalf("%s: unexpected error: %v", s, err)
+ }
+ if want := mustParseUuid(t, s); u != want {
+ t.Errorf("%s: got %+v, want %+v", s, u, want)
+ }
+ }
+
+ for _, b := range []ble.UUID{nil, {1}, {1, 2, 3, 4}} {
+ if _, err := UuidFromBllUuid(b); err == nil {
+ t.Errorf("%#v: expected error", b)
+ }
+ }
+}
+
+func TestFindChr(t *testing.T) {
+ smpChr := &ble.Characteristic{UUID: ble.MustParse(testChrUuid)}
+ otherChr := &ble.Characteristic{UUID: ble.UUID16(0x2a29)}
+
+ profile := &ble.Profile{
+ Services: []*ble.Service{
+ {
+ UUID: ble.UUID16(0x180a),
+ Characteristics: []*ble.Characteristic{otherChr},
+ },
+ {
+ UUID: ble.MustParse(testSvcUuid),
+ Characteristics: []*ble.Characteristic{
+ {UUID: ble.UUID16(0x2a00)},
+ smpChr,
+ },
+ },
+ },
+ }
+
+ svc := mustParseUuid(t, testSvcUuid)
+ chr := mustParseUuid(t, testChrUuid)
+
+ tests := []struct {
+ name string
+ id bledefs.BleChrId
+ want *ble.Characteristic
+ }{
+ {"128-bit", bledefs.BleChrId{SvcUuid: svc, ChrUuid: chr}, smpChr},
+ {"16-bit", bledefs.BleChrId{
+ SvcUuid: bledefs.BleUuid{U16: 0x180a},
+ ChrUuid: bledefs.BleUuid{U16: 0x2a29},
+ }, otherChr},
+ {"chr in other service", bledefs.BleChrId{
+ SvcUuid: bledefs.BleUuid{U16: 0x180a},
+ ChrUuid: chr,
+ }, nil},
+ {"unknown service", bledefs.BleChrId{
+ SvcUuid: bledefs.BleUuid{U16: 0x1800},
+ ChrUuid: bledefs.BleUuid{U16: 0x2a00},
+ }, nil},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ id := tt.id
+ c, err := findChr(profile, &id)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if c != tt.want {
+ t.Errorf("got %+v, want %+v", c, tt.want)
+ }
+ })
+ }
+}
+
+func TestFindChrErrors(t *testing.T) {
+ if _, err := findChr(&ble.Profile{}, nil); err == nil {
+ t.Errorf("nil chrId: expected error")
+ }
+
+ bad := &ble.Profile{
+ Services: []*ble.Service{{UUID: ble.UUID{1, 2, 3}}},
+ }
+ id := &bledefs.BleChrId{SvcUuid: bledefs.BleUuid{U16: 0x180a}}
+ if _, err := findChr(bad, id); err == nil {
+ t.Errorf("invalid service UUID: expected error")
+ }
+
+ badChr := &ble.Profile{
+ Services: []*ble.Service{{
+ UUID: ble.UUID16(0x180a),
+ Characteristics: []*ble.Characteristic{{UUID: ble.UUID{1}}},
+ }},
+ }
+ if _, err := findChr(badChr, id); err == nil {
+ t.Errorf("invalid characteristic UUID: expected error")
+ }
+}
(mynewt-newtmgr) 16/20: newtmgr/cli: Fix panic in conn add on a variable without '='
Posted by ja...@apache.org.
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 f698e60c3acacf438e83044c4f3f677c407b6b63
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:23:54 2026 +0200
newtmgr/cli: Fix panic in conn add on a variable without '='
"newtmgr conn add p1 type" (or "connstring" without a value) indexed
the missing value of the split key=value token and crashed with
"index out of range". Report a usage error instead, like for other
bad input.
To make this testable without going through nmUsage(), which exits
the process, move the parsing of the variable list into a small
helper, connProfileParseVars(), that returns an error.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/cli/connprofile.go | 43 +++++++++++++-------
newtmgr/cli/connprofile_test.go | 89 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 118 insertions(+), 14 deletions(-)
diff --git a/newtmgr/cli/connprofile.go b/newtmgr/cli/connprofile.go
index c3a614d..d69fc72 100644
--- a/newtmgr/cli/connprofile.go
+++ b/newtmgr/cli/connprofile.go
@@ -30,6 +30,33 @@ import (
"github.com/spf13/cobra"
)
+// connProfileParseVars applies "key=value" variable definitions from the
+// "conn add" command line to the given connection profile.
+func connProfileParseVars(cp *config.ConnProfile, vdefs []string) error {
+ for _, vdef := range vdefs {
+ s := strings.SplitN(vdef, "=", 2)
+ if len(s) != 2 {
+ return util.FmtNewtError(
+ "Invalid variable definition (expected key=value): %s", vdef)
+ }
+
+ switch s[0] {
+ case "type":
+ var err error
+ cp.Type, err = config.ConnTypeFromString(s[1])
+ if err != nil {
+ return err
+ }
+ case "connstring":
+ cp.ConnString = s[1]
+ default:
+ return util.NewNewtError("Unknown variable " + s[0])
+ }
+ }
+
+ return nil
+}
+
func connProfileAddCmd(cmd *cobra.Command, args []string) {
cpm := config.GlobalConnProfileMgr()
@@ -43,20 +70,8 @@ func connProfileAddCmd(cmd *cobra.Command, args []string) {
cp.Name = name
cp.Type = config.CONN_TYPE_NONE
- for _, vdef := range args[1:] {
- s := strings.SplitN(vdef, "=", 2)
- switch s[0] {
- case "type":
- var err error
- cp.Type, err = config.ConnTypeFromString(s[1])
- if err != nil {
- nmUsage(cmd, err)
- }
- case "connstring":
- cp.ConnString = s[1]
- default:
- nmUsage(cmd, util.NewNewtError("Unknown variable "+s[0]))
- }
+ if err := connProfileParseVars(cp, args[1:]); err != nil {
+ nmUsage(cmd, err)
}
// Check that a type is specified.
diff --git a/newtmgr/cli/connprofile_test.go b/newtmgr/cli/connprofile_test.go
new file mode 100644
index 0000000..6daec36
--- /dev/null
+++ b/newtmgr/cli/connprofile_test.go
@@ -0,0 +1,89 @@
+/**
+ * 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 cli
+
+import (
+ "testing"
+
+ "mynewt.apache.org/newtmgr/newtmgr/config"
+)
+
+func TestConnProfileParseVars(t *testing.T) {
+ tests := []struct {
+ name string
+ vdefs []string
+ want config.ConnProfile
+ }{
+ {"none", nil, config.ConnProfile{Name: "p"}},
+ {"type", []string{"type=serial"},
+ config.ConnProfile{Name: "p", Type: config.CONN_TYPE_SERIAL_PLAIN}},
+ {"type and connstring",
+ []string{"type=oic_ble", "connstring=peer_name=x,conn_timeout=5"},
+ config.ConnProfile{
+ Name: "p",
+ Type: config.CONN_TYPE_BLL_OIC,
+ ConnString: "peer_name=x,conn_timeout=5",
+ }},
+ {"empty connstring", []string{"connstring="},
+ config.ConnProfile{Name: "p"}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cp := &config.ConnProfile{Name: "p"}
+ if err := connProfileParseVars(cp, tt.vdefs); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if *cp != tt.want {
+ t.Errorf("got %+v, want %+v", *cp, tt.want)
+ }
+ })
+ }
+}
+
+func TestConnProfileParseVarsErrors(t *testing.T) {
+ tests := []struct {
+ vdefs []string
+ err string
+ }{
+ // Regression: a token without '=' used to panic.
+ {[]string{"type"},
+ "Invalid variable definition (expected key=value): type"},
+ {[]string{"type=serial", "connstring"},
+ "Invalid variable definition (expected key=value): connstring"},
+ {[]string{""},
+ "Invalid variable definition (expected key=value): "},
+ {[]string{"type=bogus"}, "Invalid connection type: bogus"},
+ {[]string{"type="}, "Invalid connection type: "},
+ {[]string{"foo=bar"}, "Unknown variable foo"},
+ }
+
+ for _, tt := range tests {
+ cp := config.NewConnProfile()
+ err := connProfileParseVars(cp, tt.vdefs)
+ if err == nil {
+ t.Errorf("%q: expected error", tt.vdefs)
+ continue
+ }
+ if err.Error() != tt.err {
+ t.Errorf("%q: error = %q, want %q", tt.vdefs, err.Error(), tt.err)
+ }
+ }
+}
(mynewt-newtmgr) 14/20: newtmgr/cli: Add unit tests for res, log and image helpers
Posted by ja...@apache.org.
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 7793220d7a9526e6cee06949b84feb410ec61163
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:23:09 2026 +0200
newtmgr/cli: Add unit tests for res, log and image helpers
Cover the pure helper functions used by the res, log and image
commands: k=v resource specifier parsing, payload encoding from k=v
pairs, JSON arguments and raw/JSON/binary payload files, float to int
conversion for --int, CoAP response formatting, log show argument
parsing, CBOR log message to JSON conversion, and image flag strings.
Include regression tests for earlier res fixes: k=v pairs must not be
discarded (80e5c28), and no payload arguments must yield no payload
rather than an empty CBOR map (d988f6d, 0d6802e).
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/cli/image_test.go | 51 ++++++
newtmgr/cli/log_test.go | 123 +++++++++++++
newtmgr/cli/res_test.go | 429 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 603 insertions(+)
diff --git a/newtmgr/cli/image_test.go b/newtmgr/cli/image_test.go
new file mode 100644
index 0000000..453ecdd
--- /dev/null
+++ b/newtmgr/cli/image_test.go
@@ -0,0 +1,51 @@
+/**
+ * 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 cli
+
+import (
+ "testing"
+
+ "mynewt.apache.org/newtmgr/nmxact/nmp"
+)
+
+func TestImageFlagsStr(t *testing.T) {
+ tests := []struct {
+ img nmp.ImageStateEntry
+ want string
+ }{
+ {nmp.ImageStateEntry{}, ""},
+ {nmp.ImageStateEntry{Active: true}, "active"},
+ {nmp.ImageStateEntry{Active: true, Confirmed: true},
+ "active confirmed"},
+ {nmp.ImageStateEntry{Pending: true, Permanent: true},
+ "pending permanent"},
+ {nmp.ImageStateEntry{Permanent: true, Pending: true,
+ Confirmed: true, Active: true, Bootable: true},
+ "active confirmed pending permanent"},
+ {nmp.ImageStateEntry{Bootable: true}, ""},
+ }
+
+ for _, tt := range tests {
+ if got := imageFlagsStr(tt.img); got != tt.want {
+ t.Errorf("imageFlagsStr(%+v) = %q, want %q", tt.img, got,
+ tt.want)
+ }
+ }
+}
diff --git a/newtmgr/cli/log_test.go b/newtmgr/cli/log_test.go
new file mode 100644
index 0000000..f28a94d
--- /dev/null
+++ b/newtmgr/cli/log_test.go
@@ -0,0 +1,123 @@
+/**
+ * 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 cli
+
+import (
+ "strings"
+ "testing"
+
+ "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+)
+
+func TestLogShowParseArgs(t *testing.T) {
+ tests := []struct {
+ name string
+ args []string
+ want logShowCfg
+ }{
+ {"none", nil, logShowCfg{}},
+ {"name", []string{"reboot_log"}, logShowCfg{Name: "reboot_log"}},
+ {"index", []string{"log", "10"}, logShowCfg{Name: "log", Index: 10}},
+ {"hex index", []string{"log", "0x10"},
+ logShowCfg{Name: "log", Index: 16}},
+ {"max index", []string{"log", "4294967295"},
+ logShowCfg{Name: "log", Index: 0xffffffff}},
+ {"last", []string{"log", "last"},
+ logShowCfg{Name: "log", Timestamp: -1}},
+ {"last ignores timestamp", []string{"log", "last", "junk"},
+ logShowCfg{Name: "log", Timestamp: -1}},
+ {"index and timestamp", []string{"log", "3", "123456"},
+ logShowCfg{Name: "log", Index: 3, Timestamp: 123456}},
+ {"negative timestamp", []string{"log", "3", "-7"},
+ logShowCfg{Name: "log", Index: 3, Timestamp: -7}},
+ {"extra args ignored", []string{"log", "1", "2", "3"},
+ logShowCfg{Name: "log", Index: 1, Timestamp: 2}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cfg, err := logShowParseArgs(tt.args)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if *cfg != tt.want {
+ t.Errorf("got %+v, want %+v", *cfg, tt.want)
+ }
+ })
+ }
+}
+
+func TestLogShowParseArgsErrors(t *testing.T) {
+ tests := []struct {
+ args []string
+ err string
+ }{
+ {[]string{"log", "4294967296"}, "index out of range"},
+ {[]string{"log", "-1"}, ""},
+ {[]string{"log", "abc"}, ""},
+ {[]string{"log", "1", "ts"}, ""},
+ }
+
+ for _, tt := range tests {
+ cfg, err := logShowParseArgs(tt.args)
+ if err == nil {
+ t.Errorf("%v: expected error, got %+v", tt.args, cfg)
+ continue
+ }
+ if tt.err != "" && err.Error() != tt.err {
+ t.Errorf("%v: error = %q, want %q", tt.args, err.Error(), tt.err)
+ }
+ }
+}
+
+func TestLogCborMsgText(t *testing.T) {
+ b, err := nmxutil.EncodeCbor(map[string]interface{}{
+ "msg": "hello",
+ "n": 7,
+ "neg": -1,
+ "l": []interface{}{1, "x"},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := logCborMsgText(b)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // encoding/json sorts map keys.
+ want := `{"l":[1,"x"],"msg":"hello","n":7,"neg":-1}`
+ if got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+func TestLogCborMsgTextInvalid(t *testing.T) {
+ // Not a map.
+ if _, err := logCborMsgText([]byte{0x01}); err == nil {
+ t.Errorf("expected error for non-map CBOR")
+ }
+
+ // Truncated map.
+ _, err := logCborMsgText([]byte{0xa1, 0x61})
+ if err == nil || !strings.HasPrefix(err.Error(), "failure decoding cbor") {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/newtmgr/cli/res_test.go b/newtmgr/cli/res_test.go
new file mode 100644
index 0000000..239bdb2
--- /dev/null
+++ b/newtmgr/cli/res_test.go
@@ -0,0 +1,429 @@
+/**
+ * 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 cli
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/runtimeco/go-coap"
+
+ "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+)
+
+func TestExtractResKv(t *testing.T) {
+ tests := []struct {
+ name string
+ params []string
+ want map[string]interface{}
+ }{
+ {"none", nil, map[string]interface{}{}},
+ {"int", []string{"a=1"}, map[string]interface{}{"a": 1}},
+ {"negative int", []string{"a=-5"}, map[string]interface{}{"a": -5}},
+ {"string", []string{"a=hello"},
+ map[string]interface{}{"a": "hello"}},
+ {"quoted int stays string", []string{`a="12"`},
+ map[string]interface{}{"a": "12"}},
+ {"quoted bool stays string", []string{`a="true"`},
+ map[string]interface{}{"a": "true"}},
+ {"empty quoted string", []string{`a=""`},
+ map[string]interface{}{"a": ""}},
+ {"only leading quote", []string{`a="x`},
+ map[string]interface{}{"a": `"x`}},
+ {"bool true", []string{"a=true"}, map[string]interface{}{"a": true}},
+ {"bool mixed case", []string{"a=FaLsE"},
+ map[string]interface{}{"a": false}},
+ {"float stays string", []string{"a=1.5"},
+ map[string]interface{}{"a": "1.5"}},
+ {"empty value", []string{"a="}, map[string]interface{}{"a": ""}},
+ {"value with equals", []string{"a=b=c"},
+ map[string]interface{}{"a": "b=c"}},
+ {"empty key", []string{"=1"}, map[string]interface{}{"": 1}},
+ {"multiple", []string{"x=1", "y=two", "z=false"},
+ map[string]interface{}{"x": 1, "y": "two", "z": false}},
+ {"duplicate key last wins", []string{"x=1", "x=2"},
+ map[string]interface{}{"x": 2}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m, err := extractResKv(tt.params)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !reflect.DeepEqual(m, tt.want) {
+ t.Errorf("got %#v, want %#v", m, tt.want)
+ }
+ })
+ }
+}
+
+func TestExtractResKvErrors(t *testing.T) {
+ for _, p := range []string{"", "abc"} {
+ m, err := extractResKv([]string{"ok=1", p})
+ if err == nil {
+ t.Errorf("%q: expected error, got %v", p, m)
+ continue
+ }
+ want := "invalid resource specifier: " + p
+ if err.Error() != want {
+ t.Errorf("error = %q, want %q", err.Error(), want)
+ }
+ }
+}
+
+// normCbor converts a decoded CBOR value into a form that is easy to compare:
+// maps get string keys and integers become int64.
+func normCbor(v interface{}) interface{} {
+ switch x := v.(type) {
+ case map[interface{}]interface{}:
+ m := map[string]interface{}{}
+ for k, e := range x {
+ m[k.(string)] = normCbor(e)
+ }
+ return m
+ case []interface{}:
+ for i := range x {
+ x[i] = normCbor(x[i])
+ }
+ return x
+ case uint64:
+ return int64(x)
+ default:
+ return x
+ }
+}
+
+func decodeCbor(t *testing.T, b []byte) interface{} {
+ t.Helper()
+ v, err := nmxutil.DecodeCbor(b)
+ if err != nil {
+ t.Fatalf("DecodeCbor(%x): %v", b, err)
+ }
+ return normCbor(v)
+}
+
+// setResGlobals sets the res command flag variables for the duration of a
+// test.
+func setResGlobals(t *testing.T, json, toInt bool, jsonFile, rawFile,
+ binFile string) {
+
+ t.Helper()
+
+ oJson, oInt := resJson, resInt
+ oJf, oRf, oBf := resJsonFilename, resRawFilename, resBinFilename
+ t.Cleanup(func() {
+ resJson, resInt = oJson, oInt
+ resJsonFilename, resRawFilename, resBinFilename = oJf, oRf, oBf
+ })
+
+ resJson, resInt = json, toInt
+ resJsonFilename, resRawFilename, resBinFilename = jsonFile, rawFile,
+ binFile
+}
+
+func TestParsePayloadNoArgs(t *testing.T) {
+ // Regression (d988f6d, 0d6802e): no payload arguments must produce no
+ // payload at all, not an empty CBOR map.
+ for _, json := range []bool{false, true} {
+ setResGlobals(t, json, false, "", "", "")
+
+ for _, args := range [][]string{nil, {}} {
+ b, err := parsePayload(args)
+ if err != nil {
+ t.Fatalf("json=%v: unexpected error: %v", json, err)
+ }
+ if b != nil {
+ t.Errorf("json=%v: payload = %x, want nil", json, b)
+ }
+
+ b, err = calcCborPayload(args)
+ if err != nil {
+ t.Fatalf("json=%v: unexpected error: %v", json, err)
+ }
+ if b != nil {
+ t.Errorf("json=%v: payload = %x, want nil", json, b)
+ }
+ }
+ }
+}
+
+func TestParsePayloadKv(t *testing.T) {
+ setResGlobals(t, false, false, "", "", "")
+
+ // Regression (80e5c28): k=v pairs must not be discarded.
+ b, err := parsePayload([]string{"a=1", "b=str", "c=true", `d="7"`})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := map[string]interface{}{
+ "a": int64(1), "b": "str", "c": true, "d": "7",
+ }
+ if got := decodeCbor(t, b); !reflect.DeepEqual(got, want) {
+ t.Errorf("payload = %#v, want %#v", got, want)
+ }
+
+ if _, err := parsePayload([]string{"novalue"}); err == nil {
+ t.Errorf("expected error for invalid specifier")
+ }
+}
+
+func TestParsePayloadJsonArg(t *testing.T) {
+ tests := []struct {
+ name string
+ toInt bool
+ arg string
+ want interface{}
+ }{
+ {"object float", false, `{"a": 2, "b": [1.5, "x"]}`,
+ map[string]interface{}{
+ "a": float64(2),
+ "b": []interface{}{1.5, "x"},
+ }},
+ {"object int", true, `{"a": 2, "b": [1.5, 3.0, {"c": -4}]}`,
+ map[string]interface{}{
+ "a": int64(2),
+ "b": []interface{}{1.5, int64(3),
+ map[string]interface{}{"c": int64(-4)}},
+ }},
+ {"scalar", true, `5`, int64(5)},
+ {"string", false, `"hi"`, "hi"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setResGlobals(t, true, tt.toInt, "", "", "")
+
+ b, err := parsePayload([]string{tt.arg, "ignored"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got := decodeCbor(t, b); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("payload = %#v, want %#v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParsePayloadJsonNull(t *testing.T) {
+ setResGlobals(t, true, false, "", "", "")
+
+ // A JSON null is treated as "no payload".
+ b, err := parsePayload([]string{"null"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if b != nil {
+ t.Errorf("payload = %x, want nil", b)
+ }
+}
+
+func TestParsePayloadJsonInvalid(t *testing.T) {
+ setResGlobals(t, true, false, "", "", "")
+
+ if _, err := parsePayload([]string{"{bad"}); err == nil {
+ t.Errorf("expected error for invalid JSON")
+ }
+ if _, err := parsePayloadJson(""); err == nil {
+ t.Errorf("expected error for empty JSON")
+ }
+}
+
+func TestRemoveFloats(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ want interface{}
+ }{
+ {float64(2), int64(2)},
+ {float64(-3), int64(-3)},
+ {float64(0), int64(0)},
+ {1.5, 1.5},
+ {float32(4), int64(4)},
+ {float32(0.5), float64(0.5)},
+ {"s", "s"},
+ {true, true},
+ {nil, nil},
+ {[]interface{}{1.0, 2.5, "x"}, []interface{}{int64(1), 2.5, "x"}},
+ {map[string]interface{}{"a": 1.0, "b": []interface{}{7.0}},
+ map[string]interface{}{"a": int64(1),
+ "b": []interface{}{int64(7)}}},
+ }
+
+ for _, tt := range tests {
+ got := removeFloats(tt.in)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("removeFloats(%#v) = %#v, want %#v", tt.in, got, tt.want)
+ }
+ }
+}
+
+func writeTemp(t *testing.T, name string, data []byte) string {
+ t.Helper()
+ p := filepath.Join(t.TempDir(), name)
+ if err := os.WriteFile(p, data, 0644); err != nil {
+ t.Fatal(err)
+ }
+ return p
+}
+
+func TestCalcCborPayloadRawFile(t *testing.T) {
+ raw := []byte{0xa1, 0x61, 0x61, 0x01, 0xff, 0x00}
+ setResGlobals(t, false, false, "", writeTemp(t, "raw", raw), "")
+
+ // The raw file is sent verbatim and arguments are ignored.
+ b, err := calcCborPayload([]string{"a=2"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !reflect.DeepEqual(b, raw) {
+ t.Errorf("payload = %x, want %x", b, raw)
+ }
+}
+
+func TestCalcCborPayloadJsonFile(t *testing.T) {
+ p := writeTemp(t, "j.json", []byte(`{"a": 3.0, "b": "x"}`))
+ setResGlobals(t, false, true, p, "", "")
+
+ b, err := calcCborPayload(nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := map[string]interface{}{"a": int64(3), "b": "x"}
+ if got := decodeCbor(t, b); !reflect.DeepEqual(got, want) {
+ t.Errorf("payload = %#v, want %#v", got, want)
+ }
+
+ bad := writeTemp(t, "bad.json", []byte(`{`))
+ setResGlobals(t, false, false, bad, "", "")
+ if _, err := calcCborPayload(nil); err == nil {
+ t.Errorf("expected error for invalid JSON file")
+ }
+}
+
+func TestCalcCborPayloadBinFile(t *testing.T) {
+ data := []byte{0, 1, 2, 0xfe}
+ setResGlobals(t, false, false, "", "", writeTemp(t, "bin", data))
+
+ b, err := calcCborPayload(nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Encoded as a CBOR byte string.
+ want := append([]byte{0x44}, data...)
+ if !reflect.DeepEqual(b, want) {
+ t.Errorf("payload = %x, want %x", b, want)
+ }
+}
+
+func TestCalcCborPayloadMissingFile(t *testing.T) {
+ missing := filepath.Join(t.TempDir(), "missing")
+
+ cases := []struct {
+ name string
+ jsonF, rawF, binF string
+ }{
+ {"json", missing, "", ""},
+ {"raw", "", missing, ""},
+ {"bin", "", "", missing},
+ }
+ for _, c := range cases {
+ setResGlobals(t, false, false, c.jsonF, c.rawF, c.binF)
+ if _, err := calcCborPayload(nil); err == nil {
+ t.Errorf("%s: expected error for missing file", c.name)
+ }
+ }
+}
+
+func TestCborValStr(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ want string
+ }{
+ {"abc", "abc"},
+ {uint64(5), "0x5"},
+ {int64(-5), "-5"},
+ {true, "true"},
+ {[]byte{0x01, 0x02},
+ "00000000 01 02 |..|"},
+ }
+
+ for _, tt := range tests {
+ if got := cborValStr(tt.in); got != tt.want {
+ t.Errorf("cborValStr(%#v) = %q, want %q", tt.in, got, tt.want)
+ }
+ }
+}
+
+func TestPrintCode(t *testing.T) {
+ tests := []struct {
+ code coap.COAPCode
+ want string
+ }{
+ {coap.Content, "CoAP Response Code: 2.05 Content\n"},
+ {coap.NotFound, "CoAP Response Code: 4.04 NotFound\n"},
+ {coap.Created, "CoAP Response Code: 2.01 Created\n"},
+ }
+
+ for _, tt := range tests {
+ if got := printCode(tt.code); got != tt.want {
+ t.Errorf("printCode(%d) = %q, want %q", tt.code, got, tt.want)
+ }
+ }
+}
+
+func TestCleanUpMapValue(t *testing.T) {
+ in := map[interface{}]interface{}{
+ "s": "str",
+ uint64(1): []interface{}{int64(-1), true, nil},
+ "m": map[interface{}]interface{}{"x": 1.5},
+ }
+ want := map[string]interface{}{
+ "s": "str",
+ "1": []interface{}{"-1", "true", "<nil>"},
+ "m": map[string]interface{}{"x": "1.5"},
+ }
+
+ if got := cleanUpMapValue(in); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func TestResResponseStr(t *testing.T) {
+ if got := resResponseStr("/a/b", nil); got != "/a/b\n <empty>" {
+ t.Errorf("empty payload: got %q", got)
+ }
+
+ b, err := nmxutil.EncodeCbor(map[string]interface{}{"k": 1})
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "/a/b\n{\n \"k\": \"1\"\n}"
+ if got := resResponseStr("/a/b", b); got != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
+
+ got := resResponseStr("/a/b", []byte{0xff, 0xff})
+ if !strings.HasPrefix(got, "/a/b\n invalid incoming cbor:") {
+ t.Errorf("invalid cbor: got %q", got)
+ }
+}
(mynewt-newtmgr) 04/20: nmxact/nmcoap: Fix loss of back-to-back CoAP-TCP messages
Posted by ja...@apache.org.
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 d4a9509200d4709968c00eb1b261ddfc77510293
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:24:39 2026 +0200
nmxact/nmcoap: Fix loss of back-to-back CoAP-TCP messages
Reassembler.RxFrag stored the unread remainder returned by
coap.PullTcp but then unconditionally cleared it after extracting a
message. If a received chunk contained the end of one message and the
start (or all) of the next one, the second message was silently
dropped, and with it the response a caller was waiting for, which then
timed out.
Keep the remainder after a successful pull. Since RxFrag returns only
one message per call, also add Receiver.RxBuffered and make
Dispatcher.Dispatch keep dispatching until no complete message is left
in the buffer, so a second complete message is delivered right away
instead of waiting for the next fragment. Only CoAP-TCP (BLE) buffers
data; UDP behaviour is unchanged.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmcoap/dispatch.go | 16 ++++++++---
nmxact/nmcoap/dispatch_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++
nmxact/nmcoap/frag.go | 5 +++-
nmxact/nmcoap/frag_test.go | 50 ++++++++++++++++++++++++++++++++
nmxact/nmcoap/receiver.go | 15 ++++++++++
5 files changed, 146 insertions(+), 5 deletions(-)
diff --git a/nmxact/nmcoap/dispatch.go b/nmxact/nmcoap/dispatch.go
index 4048cd9..c8a3e71 100644
--- a/nmxact/nmcoap/dispatch.go
+++ b/nmxact/nmcoap/dispatch.go
@@ -104,12 +104,20 @@ func (d *Dispatcher) RemoveListener(mc MsgCriteria) *Listener {
// Returns true if the response was dispatched.
func (d *Dispatcher) Dispatch(data []byte) bool {
- // See if this fragment completes a packet.
- msg := d.rxer.Rx(data)
- if msg == nil {
- return false
+ dispatched := false
+
+ // See if this fragment completes a packet. With TCP, a fragment may
+ // complete more than one packet; dispatch all of them.
+ for msg := d.rxer.Rx(data); msg != nil; msg = d.rxer.RxBuffered() {
+ if d.dispatchMsg(msg) {
+ dispatched = true
+ }
}
+ return dispatched
+}
+
+func (d *Dispatcher) dispatchMsg(msg coap.Message) bool {
d.mtx.Lock()
defer d.mtx.Unlock()
diff --git a/nmxact/nmcoap/dispatch_test.go b/nmxact/nmcoap/dispatch_test.go
new file mode 100644
index 0000000..a8df620
--- /dev/null
+++ b/nmxact/nmcoap/dispatch_test.go
@@ -0,0 +1,65 @@
+/**
+ * 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 nmcoap
+
+import (
+ "testing"
+)
+
+// A single TCP chunk containing two complete messages must dispatch both
+// of them immediately, not just the first one.
+func TestDispatcherTcpBackToBack(t *testing.T) {
+ d := NewDispatcher(true, 0)
+
+ l1, err := d.AddListener(MsgCriteria{Token: []byte{1}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ l2, err := d.AddListener(MsgCriteria{Token: []byte{2}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer d.RemoveListener(l1.Criteria)
+ defer d.RemoveListener(l2.Criteria)
+
+ b1 := tcpMsg(t, []byte{1}, "", []byte("one"))
+ b2 := tcpMsg(t, []byte{2}, "", []byte("two"))
+ if !d.Dispatch(append(append([]byte{}, b1...), b2...)) {
+ t.Fatalf("Dispatch() = false, want true")
+ }
+
+ for i, l := range []*Listener{l1, l2} {
+ select {
+ case m := <-l.RspChan:
+ if m.Token()[0] != byte(i+1) {
+ t.Fatalf("listener %d got token % x", i+1, m.Token())
+ }
+ default:
+ t.Fatalf("listener %d did not receive its message", i+1)
+ }
+ }
+}
+
+func TestDispatcherNoListener(t *testing.T) {
+ d := NewDispatcher(true, 0)
+ if d.Dispatch(tcpMsg(t, []byte{1}, "", nil)) {
+ t.Fatalf("Dispatch() = true without a listener")
+ }
+}
diff --git a/nmxact/nmcoap/frag.go b/nmxact/nmcoap/frag.go
index a7371d7..673da9d 100644
--- a/nmxact/nmcoap/frag.go
+++ b/nmxact/nmcoap/frag.go
@@ -48,6 +48,9 @@ func (r *Reassembler) RxFrag(frag []byte) *coap.TcpMessage {
return nil
}
- r.cur = nil
+ // Keep any trailing bytes; they are the start of the next message.
+ if len(r.cur) == 0 {
+ r.cur = nil
+ }
return tm
}
diff --git a/nmxact/nmcoap/frag_test.go b/nmxact/nmcoap/frag_test.go
index 7a3a3ff..7aefb98 100644
--- a/nmxact/nmcoap/frag_test.go
+++ b/nmxact/nmcoap/frag_test.go
@@ -159,3 +159,53 @@ func TestReceiverTcp(t *testing.T) {
t.Fatalf("unexpected message %v", got)
}
}
+
+// Two messages received in a single buffer must both be returned, one per
+// call. Previously the second message was silently dropped.
+func TestReassemblerBackToBack(t *testing.T) {
+ b1 := tcpMsg(t, []byte{1}, "", []byte("first"))
+ b2 := tcpMsg(t, []byte{2}, "", []byte("second"))
+ b3 := tcpMsg(t, []byte{3}, "", []byte("third"))
+
+ r := NewReassembler()
+ buf := append(append([]byte{}, b1...), b2...)
+
+ checkTcpMsg(t, r.RxFrag(buf), []byte{1}, []byte("first"))
+ checkTcpMsg(t, r.RxFrag(nil), []byte{2}, []byte("second"))
+ if m := r.RxFrag(nil); m != nil {
+ t.Fatalf("unexpected third message: %v", m)
+ }
+
+ // First message complete plus the start of the next one.
+ buf = append(append([]byte{}, b1...), b3[:3]...)
+ checkTcpMsg(t, r.RxFrag(buf), []byte{1}, []byte("first"))
+ checkTcpMsg(t, r.RxFrag(b3[3:]), []byte{3}, []byte("third"))
+ if m := r.RxFrag(nil); m != nil {
+ t.Fatalf("unexpected extra message: %v", m)
+ }
+}
+
+func TestReceiverRxBuffered(t *testing.T) {
+ udp := NewReceiver(false)
+ if m := udp.RxBuffered(); m != nil {
+ t.Fatalf("UDP receiver returned buffered message %v", m)
+ }
+
+ tcp := NewReceiver(true)
+ if m := tcp.RxBuffered(); m != nil {
+ t.Fatalf("empty TCP receiver returned message %v", m)
+ }
+
+ b1 := tcpMsg(t, []byte{1}, "", nil)
+ b2 := tcpMsg(t, []byte{2}, "", nil)
+ if m := tcp.Rx(append(append([]byte{}, b1...), b2...)); m == nil {
+ t.Fatalf("expected first message")
+ }
+ m := tcp.RxBuffered()
+ if m == nil || m.Token()[0] != 2 {
+ t.Fatalf("expected second message, got %v", m)
+ }
+ if m := tcp.RxBuffered(); m != nil {
+ t.Fatalf("unexpected message %v", m)
+ }
+}
diff --git a/nmxact/nmcoap/receiver.go b/nmxact/nmcoap/receiver.go
index c8587ee..0835535 100644
--- a/nmxact/nmcoap/receiver.go
+++ b/nmxact/nmcoap/receiver.go
@@ -57,3 +57,18 @@ func (r *Receiver) Rx(data []byte) coap.Message {
return m
}
}
+
+// RxBuffered returns the next complete message that is already buffered
+// from previously received data, or nil if there is none. Only TCP can
+// buffer messages.
+func (r *Receiver) RxBuffered() coap.Message {
+ if r.reassembler == nil {
+ return nil
+ }
+
+ tm := r.reassembler.RxFrag(nil)
+ if tm == nil {
+ return nil
+ }
+ return tm
+}
(mynewt-newtmgr) 13/20: newtmgr/config: Apply --hci index with an empty bll connstring
Posted by ja...@apache.org.
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 32f52623a4666ad6188dc392306a1734d67e3af8
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:20:49 2026 +0200
newtmgr/config: Apply --hci index with an empty bll connstring
ParseBllConnString() returned early for an empty connstring before
copying nmutil.HciIdx into the config. Running e.g.
"newtmgr --conntype ble -n mydev -i 1 echo hi" without a connstring
therefore silently used HCI controller 0 instead of the requested
one. Set the HCI index before the early return.
Assisted-by: Claude:claude-opus-5-5
---
newtmgr/config/bll_config.go | 3 +--
newtmgr/config/bll_config_test.go | 4 ++++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/newtmgr/config/bll_config.go b/newtmgr/config/bll_config.go
index fe39a09..4fe2f12 100644
--- a/newtmgr/config/bll_config.go
+++ b/newtmgr/config/bll_config.go
@@ -60,6 +60,7 @@ func einvalBllConnString(f string, args ...interface{}) error {
func ParseBllConnString(cs string) (*BllConfig, error) {
bc := NewBllConfig()
+ bc.HciIdx = nmutil.HciIdx
if strings.TrimSpace(cs) == "" {
return bc, nil
@@ -101,8 +102,6 @@ func ParseBllConnString(cs string) (*BllConfig, error) {
}
}
- bc.HciIdx = nmutil.HciIdx
-
return bc, nil
}
diff --git a/newtmgr/config/bll_config_test.go b/newtmgr/config/bll_config_test.go
index cc6474a..d3b1b65 100644
--- a/newtmgr/config/bll_config_test.go
+++ b/newtmgr/config/bll_config_test.go
@@ -54,6 +54,10 @@ func TestParseBllConnString(t *testing.T) {
cs string
want BllConfig
}{
+ // The --hci index must be applied even without a connstring
+ // (e.g. when the peer is given with -n).
+ {"empty", "", BllConfig{ConnTimeout: 4, HciIdx: 1}},
+ {"whitespace", " ", BllConfig{ConnTimeout: 4, HciIdx: 1}},
{"peer_name", "peer_name=mydev",
BllConfig{PeerName: "mydev", ConnTimeout: 4, HciIdx: 1}},
{"all keys",
(mynewt-newtmgr) 10/20: nmxact/mtech_lora: Add unit tests for listeners and address helpers
Posted by ja...@apache.org.
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 ec4fb8e9b71dd5584ad5b84ea9efe08b56cda9f6
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:30:06 2026 +0200
nmxact/mtech_lora: Add unit tests for listeners and address helpers
Cover the listener key helpers, ListenerMap and ListenerSlice lookup
precedence (target specific before type only), duplicate detection
and removal, Listener reference counting on Close, and the
NormalizeAddr/DenormalizeAddr conversions for plain and dash
separated EUI-64 addresses.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/mtech_lora/listen_test.go | 208 +++++++++++++++++++++++++++++
nmxact/mtech_lora/mtech_lora_xport_test.go | 85 ++++++++++++
2 files changed, 293 insertions(+)
diff --git a/nmxact/mtech_lora/listen_test.go b/nmxact/mtech_lora/listen_test.go
new file mode 100644
index 0000000..08d2284
--- /dev/null
+++ b/nmxact/mtech_lora/listen_test.go
@@ -0,0 +1,208 @@
+/**
+ * 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 mtech_lora
+
+import (
+ "testing"
+)
+
+func TestListenerKeys(t *testing.T) {
+ k := TgtPortKey("0011223344556677", 5, "rx")
+ if k.Target != "00112233445566775" || k.Type != "rx" {
+ t.Fatalf("TgtPortKey() = %+v", k)
+ }
+
+ k = TgtKey("0011223344556677", "req")
+ if k.Target != "0011223344556677" || k.Type != "req" {
+ t.Fatalf("TgtKey() = %+v", k)
+ }
+
+ k = TypeKey("rx")
+ if k.Target != "" || k.Type != "rx" {
+ t.Fatalf("TypeKey() = %+v", k)
+ }
+}
+
+func TestListenerMap(t *testing.T) {
+ lm := NewListenerMap()
+
+ tgt := "0011223344556677"
+ specific := NewListener()
+ generic := NewListener()
+
+ if _, l := lm.FindListener(tgt, 1, "rx"); l != nil {
+ t.Fatalf("empty map returned listener")
+ }
+
+ if err := lm.AddListener(TypeKey("rx"), generic); err != nil {
+ t.Fatalf("AddListener: %v", err)
+ }
+
+ // Falls back to the type-only key.
+ key, l := lm.FindListener(tgt, 1, "rx")
+ if l != generic || key != TypeKey("rx") {
+ t.Fatalf("FindListener() = %+v, %p; want generic", key, l)
+ }
+
+ if err := lm.AddListener(TgtPortKey(tgt, 1, "rx"), specific); err != nil {
+ t.Fatalf("AddListener: %v", err)
+ }
+
+ // Target+port key takes precedence.
+ key, l = lm.FindListener(tgt, 1, "rx")
+ if l != specific || key != TgtPortKey(tgt, 1, "rx") {
+ t.Fatalf("FindListener() = %+v, %p; want specific", key, l)
+ }
+
+ // Other port or type.
+ if _, l := lm.FindListener(tgt, 2, "rx"); l != generic {
+ t.Fatalf("other port: got %p, want generic", l)
+ }
+ if _, l := lm.FindListener(tgt, 1, "tx"); l != nil {
+ t.Fatalf("other type: got %p, want nil", l)
+ }
+
+ // Duplicates are rejected.
+ if err := lm.AddListener(TypeKey("rx"), NewListener()); err == nil {
+ t.Fatalf("duplicate key accepted")
+ }
+ if err := lm.AddListener(TypeKey("other"), generic); err == nil {
+ t.Fatalf("duplicate listener accepted")
+ }
+
+ // Removal by listener.
+ if k := lm.RemoveListener(specific); k == nil ||
+ *k != TgtPortKey(tgt, 1, "rx") {
+
+ t.Fatalf("RemoveListener() = %v", k)
+ }
+ if k := lm.RemoveListener(specific); k != nil {
+ t.Fatalf("second RemoveListener() = %v, want nil", k)
+ }
+ if _, l := lm.FindListener(tgt, 1, "rx"); l != generic {
+ t.Fatalf("after removal: got %p, want generic", l)
+ }
+
+ // Removal by key.
+ if l := lm.RemoveKey(TypeKey("rx")); l != generic {
+ t.Fatalf("RemoveKey() = %p, want generic", l)
+ }
+ if l := lm.RemoveKey(TypeKey("rx")); l != nil {
+ t.Fatalf("second RemoveKey() = %p, want nil", l)
+ }
+ if _, l := lm.FindListener(tgt, 1, "rx"); l != nil {
+ t.Fatalf("empty map returned listener")
+ }
+
+ // A removed listener can be added again.
+ if err := lm.AddListener(TypeKey("rx"), generic); err != nil {
+ t.Fatalf("re-add: %v", err)
+ }
+}
+
+func TestListenerSlice(t *testing.T) {
+ ls := NewListenerSlice()
+ tgt := "0011223344556677"
+
+ if _, s := ls.FindListener(tgt, "req"); s != nil {
+ t.Fatalf("empty slice map returned %v", s)
+ }
+
+ g1 := NewListener()
+ g2 := NewListener()
+ spec := NewListener()
+
+ for _, l := range []*Listener{g1, g2} {
+ if err := ls.AddListener(TypeKey("req"), l); err != nil {
+ t.Fatalf("AddListener: %v", err)
+ }
+ }
+
+ key, s := ls.FindListener(tgt, "req")
+ if key != TypeKey("req") || len(s) != 2 {
+ t.Fatalf("FindListener() = %+v, %v", key, s)
+ }
+
+ if err := ls.AddListener(TgtKey(tgt, "req"), spec); err != nil {
+ t.Fatalf("AddListener: %v", err)
+ }
+ key, s = ls.FindListener(tgt, "req")
+ if key != TgtKey(tgt, "req") || len(s) != 1 || s[0] != spec {
+ t.Fatalf("FindListener() = %+v, %v; want specific", key, s)
+ }
+
+ // The same listener cannot be added twice.
+ if err := ls.AddListener(TypeKey("other"), g1); err == nil {
+ t.Fatalf("duplicate listener accepted")
+ }
+
+ // Removing one of several listeners keeps the others.
+ if k := ls.RemoveListener(g1); k == nil || *k != TypeKey("req") {
+ t.Fatalf("RemoveListener() = %v", k)
+ }
+ if k := ls.RemoveListener(g1); k != nil {
+ t.Fatalf("second RemoveListener() = %v, want nil", k)
+ }
+ if _, s := ls.FindListener("other", "req"); len(s) != 1 || s[0] != g2 {
+ t.Fatalf("after removal: %v, want [g2]", s)
+ }
+
+ // Removing the last listener removes the key.
+ ls.RemoveListener(g2)
+ if _, s := ls.FindListener("other", "req"); s != nil {
+ t.Fatalf("after removing all: %v, want nil", s)
+ }
+ ls.RemoveListener(spec)
+ if _, s := ls.FindListener(tgt, "req"); s != nil {
+ t.Fatalf("after removing all: %v, want nil", s)
+ }
+}
+
+func TestListenerRefCnt(t *testing.T) {
+ l := NewListener()
+ l.RefCnt++
+
+ l.MsgChan <- []byte{1}
+
+ // First Close only drops a reference.
+ l.Close()
+ select {
+ case b, ok := <-l.MsgChan:
+ if !ok || len(b) != 1 {
+ t.Fatalf("MsgChan closed too early")
+ }
+ default:
+ t.Fatalf("queued message lost")
+ }
+
+ // Second Close closes and drains the channels.
+ l.MsgChan <- []byte{2}
+ l.MtuChan <- 100
+ l.Close()
+ if _, ok := <-l.MsgChan; ok {
+ t.Fatalf("MsgChan not closed")
+ }
+ if _, ok := <-l.MtuChan; ok {
+ t.Fatalf("MtuChan not closed")
+ }
+ if _, ok := <-l.ConnChan; ok {
+ t.Fatalf("ConnChan not closed")
+ }
+}
diff --git a/nmxact/mtech_lora/mtech_lora_xport_test.go b/nmxact/mtech_lora/mtech_lora_xport_test.go
new file mode 100644
index 0000000..7300e73
--- /dev/null
+++ b/nmxact/mtech_lora/mtech_lora_xport_test.go
@@ -0,0 +1,85 @@
+/**
+ * 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 mtech_lora
+
+import (
+ "testing"
+)
+
+func TestNormalizeAddr(t *testing.T) {
+ tests := []struct {
+ in string
+ want string
+ }{
+ {"0011223344556677", "0011223344556677"},
+ {"00-11-22-33-44-55-66-77", "0011223344556677"},
+ }
+
+ for _, tt := range tests {
+ got, err := NormalizeAddr(tt.in)
+ if err != nil {
+ t.Errorf("NormalizeAddr(%q): unexpected error: %v", tt.in, err)
+ continue
+ }
+ if got != tt.want {
+ t.Errorf("NormalizeAddr(%q) = %q, want %q", tt.in, got, tt.want)
+ }
+ }
+
+ bad := []string{
+ "",
+ "00112233445566",
+ "001122334455667788",
+ "00-11-22-33-44-55-66",
+ }
+ for _, s := range bad {
+ if got, err := NormalizeAddr(s); err == nil {
+ t.Errorf("NormalizeAddr(%q) = %q, expected error", s, got)
+ }
+ }
+}
+
+func TestDenormalizeAddr(t *testing.T) {
+ tests := []struct {
+ in string
+ want string
+ }{
+ {"0011223344556677", "00-11-22-33-44-55-66-77"},
+ {"aabbccddeeff0011", "aa-bb-cc-dd-ee-ff-00-11"},
+ {"", "<invalid>"},
+ {"001122", "<invalid>"},
+ {"00-11-22-33-44-55-66-77", "<invalid>"},
+ }
+
+ for _, tt := range tests {
+ if got := DenormalizeAddr(tt.in); got != tt.want {
+ t.Errorf("DenormalizeAddr(%q) = %q, want %q", tt.in, got,
+ tt.want)
+ }
+ }
+}
+
+func TestNormalizeDenormalizeRoundTrip(t *testing.T) {
+ addr := "0123456789abcdef"
+ n, err := NormalizeAddr(DenormalizeAddr(addr))
+ if err != nil || n != addr {
+ t.Fatalf("round trip = %q, %v; want %q", n, err, addr)
+ }
+}
(mynewt-newtmgr) 12/20: newtmgr/config: Add unit tests for connstring and profile handling
Posted by ja...@apache.org.
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 <sz...@codecoup.pl>
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 })
+}