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:28:42 UTC
(mynewt-newtmgr) 18/20: nmxact/udp: Add unit tests for UDP session
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 a040631682424c9e5c0bba34d38ee86f860f5691
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:37:29 2026 +0200
nmxact/udp: Add unit tests for UDP session
Drive UdpSesn against a fake device listening on an ephemeral
127.0.0.1 UDP port that answers NMP or OMP echo requests: Open,
TxRxMgmt, Close, reopening a closed session, Open/Close twice,
TxRxMgmtAsync (regression test for 8473977), response timeouts,
oversized requests, an invalid peer address and UdpXport start/stop.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/udp/udp_test.go | 446 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 446 insertions(+)
diff --git a/nmxact/udp/udp_test.go b/nmxact/udp/udp_test.go
new file mode 100644
index 0000000..4b1b12f
--- /dev/null
+++ b/nmxact/udp/udp_test.go
@@ -0,0 +1,446 @@
+/**
+ * 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 udp
+
+import (
+ "fmt"
+ "net"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/runtimeco/go-coap"
+ "github.com/ugorji/go/codec"
+
+ "mynewt.apache.org/newtmgr/nmxact/nmp"
+ "mynewt.apache.org/newtmgr/nmxact/nmxutil"
+ "mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+const testTimeout = 5 * time.Second
+
+// fakeUdpDevice is a management server on a loopback UDP socket that
+// answers NMP or OMP echo requests.
+type fakeUdpDevice struct {
+ conn *net.UDPConn
+ proto sesn.MgmtProto
+ respond bool
+
+ mtx sync.Mutex
+ errs []error
+ reqs int
+
+ wg sync.WaitGroup
+}
+
+func newFakeUdpDevice(t *testing.T, proto sesn.MgmtProto,
+ respond bool) *fakeUdpDevice {
+
+ t.Helper()
+
+ conn, err := net.ListenUDP("udp",
+ &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+ if err != nil {
+ t.Fatalf("ListenUDP failed: %v", err)
+ }
+
+ f := &fakeUdpDevice{
+ conn: conn,
+ proto: proto,
+ respond: respond,
+ }
+
+ f.wg.Add(1)
+ go f.serve()
+
+ t.Cleanup(func() {
+ conn.Close()
+ f.wg.Wait()
+
+ f.mtx.Lock()
+ defer f.mtx.Unlock()
+ for _, err := range f.errs {
+ t.Errorf("fake device: %v", err)
+ }
+ })
+
+ return f
+}
+
+func (f *fakeUdpDevice) addr() string {
+ return f.conn.LocalAddr().String()
+}
+
+func (f *fakeUdpDevice) numReqs() int {
+ f.mtx.Lock()
+ defer f.mtx.Unlock()
+
+ return f.reqs
+}
+
+func (f *fakeUdpDevice) serve() {
+ defer f.wg.Done()
+
+ buf := make([]byte, 4096)
+ for {
+ n, src, err := f.conn.ReadFromUDP(buf)
+ if err != nil {
+ return
+ }
+
+ f.mtx.Lock()
+ f.reqs++
+ f.mtx.Unlock()
+
+ rsp, err := f.process(buf[:n])
+ if err != nil {
+ f.mtx.Lock()
+ f.errs = append(f.errs, err)
+ f.mtx.Unlock()
+ continue
+ }
+
+ if f.respond {
+ f.conn.WriteToUDP(rsp, src)
+ }
+ }
+}
+
+func cborDecodeMap(b []byte) (map[string]interface{}, error) {
+ m := map[string]interface{}{}
+ dec := codec.NewDecoderBytes(b, new(codec.CborHandle))
+ if err := dec.Decode(&m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func echoRspHdr(seq uint8) nmp.NmpHdr {
+ return nmp.NmpHdr{
+ Op: nmp.NMP_OP_WRITE_RSP,
+ Group: nmp.NMP_GROUP_DEFAULT,
+ Id: nmp.NMP_ID_DEF_ECHO,
+ Seq: seq,
+ }
+}
+
+func (f *fakeUdpDevice) process(req []byte) ([]byte, error) {
+ if f.proto == sesn.MGMT_PROTO_NMP {
+ hdr, err := nmp.DecodeNmpHdr(req)
+ if err != nil {
+ return nil, err
+ }
+ body, err := cborDecodeMap(req[nmp.NMP_HDR_SIZE:])
+ if err != nil {
+ return nil, err
+ }
+ return nmp.EncodeNmpPlain(&nmp.NmpMsg{
+ Hdr: echoRspHdr(hdr.Seq),
+ Body: map[string]interface{}{"r": body["d"], "rc": 0},
+ })
+ }
+
+ m, err := coap.ParseDgramMessage(req)
+ if err != nil {
+ return nil, err
+ }
+ body, err := cborDecodeMap(m.Payload())
+ if err != nil {
+ return nil, err
+ }
+ hb, ok := body["_h"].([]byte)
+ if !ok {
+ return nil, fmt.Errorf("OMP request without NMP header")
+ }
+ hdr, err := nmp.DecodeNmpHdr(hb)
+ if err != nil {
+ return nil, err
+ }
+
+ rh := echoRspHdr(hdr.Seq)
+ var payload []byte
+ enc := codec.NewEncoderBytes(&payload, new(codec.CborHandle))
+ err = enc.Encode(map[string]interface{}{
+ "_h": rh.Bytes(),
+ "r": body["d"],
+ "rc": 0,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ rsp := coap.NewDgramMessage(coap.MessageParams{
+ Type: coap.Acknowledgement,
+ Code: coap.Content,
+ MessageID: m.MessageID(),
+ Token: m.Token(),
+ Payload: payload,
+ })
+ return rsp.MarshalBinary()
+}
+
+func newTestSesn(t *testing.T, proto sesn.MgmtProto,
+ addr string) *UdpSesn {
+
+ t.Helper()
+
+ cfg := sesn.NewSesnCfg()
+ cfg.MgmtProto = proto
+ cfg.PeerSpec.Udp = addr
+
+ s, err := NewUdpSesn(cfg)
+ if err != nil {
+ t.Fatalf("NewUdpSesn failed: %v", err)
+ }
+ return s
+}
+
+func echoMsg(payload string) *nmp.NmpMsg {
+ r := nmp.NewEchoReq()
+ r.Payload = payload
+ return r.Msg()
+}
+
+func checkEcho(t *testing.T, rsp nmp.NmpRsp, seq uint8, payload string) {
+ t.Helper()
+
+ echo, ok := rsp.(*nmp.EchoRsp)
+ if !ok {
+ t.Fatalf("got %T, want *nmp.EchoRsp", rsp)
+ }
+ if echo.Payload != payload || echo.Hdr().Seq != seq {
+ t.Errorf("got payload %q seq %d, want %q seq %d",
+ echo.Payload, echo.Hdr().Seq, payload, seq)
+ }
+}
+
+var udpProtos = []sesn.MgmtProto{sesn.MGMT_PROTO_NMP, sesn.MGMT_PROTO_OMP}
+
+func TestUdpSesnTxRxMgmt(t *testing.T) {
+ for _, proto := range udpProtos {
+ t.Run(proto.String(), func(t *testing.T) {
+ dev := newFakeUdpDevice(t, proto, true)
+ s := newTestSesn(t, proto, dev.addr())
+
+ if s.IsOpen() {
+ t.Fatalf("new session is open")
+ }
+ if s.MgmtProto() != proto {
+ t.Errorf("MgmtProto() = %v, want %v", s.MgmtProto(), proto)
+ }
+ if err := s.Open(); err != nil {
+ t.Fatalf("Open failed: %v", err)
+ }
+ if !s.IsOpen() {
+ t.Fatalf("session not open after Open")
+ }
+
+ for i := 0; i < 3; i++ {
+ req := echoMsg(fmt.Sprintf("ping %d", i))
+ rsp, err := s.TxRxMgmt(req, testTimeout)
+ if err != nil {
+ t.Fatalf("TxRxMgmt failed: %v", err)
+ }
+ checkEcho(t, rsp, req.Hdr.Seq, fmt.Sprintf("ping %d", i))
+ }
+
+ if err := s.Close(); err != nil {
+ t.Fatalf("Close failed: %v", err)
+ }
+ if s.IsOpen() {
+ t.Errorf("session open after Close")
+ }
+ })
+ }
+}
+
+func TestUdpSesnOpenCloseTwice(t *testing.T) {
+ dev := newFakeUdpDevice(t, sesn.MGMT_PROTO_NMP, true)
+ s := newTestSesn(t, sesn.MGMT_PROTO_NMP, dev.addr())
+
+ if err := s.Close(); !nmxutil.IsSesnClosed(err) {
+ t.Errorf("Close of unopened session: got %v", err)
+ }
+ if err := s.Open(); err != nil {
+ t.Fatalf("Open failed: %v", err)
+ }
+ if err := s.Open(); !nmxutil.IsSesnAlreadyOpen(err) {
+ t.Errorf("second Open: got %v", err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatalf("Close failed: %v", err)
+ }
+ if err := s.Close(); !nmxutil.IsSesnClosed(err) {
+ t.Errorf("second Close: got %v", err)
+ }
+
+ if _, err := s.TxRxMgmt(echoMsg("x"), testTimeout); err == nil {
+ t.Errorf("TxRxMgmt on closed session succeeded")
+ }
+}
+
+func TestUdpSesnReopen(t *testing.T) {
+ for _, proto := range udpProtos {
+ t.Run(proto.String(), func(t *testing.T) {
+ dev := newFakeUdpDevice(t, proto, true)
+ s := newTestSesn(t, proto, dev.addr())
+
+ for i := 0; i < 2; i++ {
+ if err := s.Open(); err != nil {
+ t.Fatalf("Open %d failed: %v", i, err)
+ }
+ req := echoMsg("again")
+ rsp, err := s.TxRxMgmt(req, testTimeout)
+ if err != nil {
+ t.Fatalf("TxRxMgmt %d failed: %v", i, err)
+ }
+ checkEcho(t, rsp, req.Hdr.Seq, "again")
+ if err := s.Close(); err != nil {
+ t.Fatalf("Close %d failed: %v", i, err)
+ }
+ }
+ })
+ }
+}
+
+// Regression test for 8473977: TxRxMgmtAsync must deliver the response
+// on ch (UDP sessions implement it synchronously).
+func TestUdpSesnTxRxMgmtAsync(t *testing.T) {
+ for _, proto := range udpProtos {
+ t.Run(proto.String(), func(t *testing.T) {
+ dev := newFakeUdpDevice(t, proto, true)
+ s := newTestSesn(t, proto, dev.addr())
+ if err := s.Open(); err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+
+ ch := make(chan nmp.NmpRsp, 1)
+ errc := make(chan error, 1)
+ req := echoMsg("async")
+
+ if err := s.TxRxMgmtAsync(req, testTimeout, ch, errc); err != nil {
+ t.Fatalf("TxRxMgmtAsync failed: %v", err)
+ }
+
+ select {
+ case rsp := <-ch:
+ checkEcho(t, rsp, req.Hdr.Seq, "async")
+ case err := <-errc:
+ t.Fatalf("unexpected error: %v", err)
+ case <-time.After(testTimeout):
+ t.Fatalf("timeout waiting for async response")
+ }
+ })
+ }
+}
+
+func TestUdpSesnTimeout(t *testing.T) {
+ for _, proto := range udpProtos {
+ t.Run(proto.String(), func(t *testing.T) {
+ dev := newFakeUdpDevice(t, proto, false)
+ s := newTestSesn(t, proto, dev.addr())
+ if err := s.Open(); err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+
+ _, err := s.TxRxMgmt(echoMsg("x"), 50*time.Millisecond)
+ if !nmxutil.IsRspTimeout(err) {
+ t.Fatalf("got %v (%T), want RspTimeoutError", err, err)
+ }
+
+ // The async variant reports the timeout on errc.
+ ch := make(chan nmp.NmpRsp, 1)
+ errc := make(chan error, 1)
+ s.TxRxMgmtAsync(echoMsg("y"), 50*time.Millisecond, ch, errc)
+ select {
+ case err := <-errc:
+ if !nmxutil.IsRspTimeout(err) {
+ t.Errorf("got %v, want RspTimeoutError", err)
+ }
+ case rsp := <-ch:
+ t.Fatalf("unexpected response %+v", rsp)
+ case <-time.After(testTimeout):
+ t.Fatalf("timeout waiting for async error")
+ }
+
+ if n := dev.numReqs(); n != 2 {
+ t.Errorf("device saw %d requests, want 2", n)
+ }
+ })
+ }
+}
+
+func TestUdpSesnRequestTooBig(t *testing.T) {
+ dev := newFakeUdpDevice(t, sesn.MGMT_PROTO_NMP, true)
+ s := newTestSesn(t, sesn.MGMT_PROTO_NMP, dev.addr())
+ if err := s.Open(); err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+
+ req := echoMsg(strings.Repeat("x", MAX_PACKET_SIZE))
+ if _, err := s.TxRxMgmt(req, testTimeout); err == nil {
+ t.Errorf("oversized request succeeded")
+ }
+}
+
+func TestUdpSesnBadPeer(t *testing.T) {
+ s := newTestSesn(t, sesn.MGMT_PROTO_NMP, "not an address")
+ if err := s.Open(); err == nil {
+ s.Close()
+ t.Fatalf("Open with invalid peer succeeded")
+ }
+ if s.IsOpen() {
+ t.Errorf("session open after failed Open")
+ }
+}
+
+func TestUdpXportStartStop(t *testing.T) {
+ x := NewUdpXport()
+
+ if err := x.Stop(); err == nil {
+ t.Errorf("Stop of unstarted xport succeeded")
+ }
+ if err := x.Start(); err != nil {
+ t.Fatalf("Start failed: %v", err)
+ }
+ if err := x.Start(); err == nil {
+ t.Errorf("second Start succeeded")
+ }
+ if err := x.Stop(); err != nil {
+ t.Fatalf("Stop failed: %v", err)
+ }
+ if err := x.Tx([]byte{1}); err == nil {
+ t.Errorf("Tx succeeded")
+ }
+
+ cfg := sesn.NewSesnCfg()
+ cfg.PeerSpec.Udp = "127.0.0.1:1"
+ s, err := x.BuildSesn(cfg)
+ if err != nil {
+ t.Fatalf("BuildSesn failed: %v", err)
+ }
+ if _, ok := s.(*UdpSesn); !ok {
+ t.Errorf("BuildSesn returned %T, want *UdpSesn", s)
+ }
+}