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 <[email protected]>
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)
+               }
+       }
+}

Reply via email to