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) + } + } +}
