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

Reply via email to