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 abaf5d41cad0c040b92fe9a261e82764e69c3d7e Author: Szymon Janc <[email protected]> AuthorDate: Thu Oct 22 00:23:16 2026 +0200 nmxact/nmcoap: Add unit tests for dispatcher Cover the CoAP dispatcher: duplicate listener rejection, matching of incoming messages by token and/or path including the most-specific listener ordering, garbage input, TCP reassembly, listener removal, ErrorOne/ErrorAll, and listener timeouts. Assisted-by: Claude:claude-opus-5-5 --- nmxact/nmcoap/dispatcher_test.go | 321 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) diff --git a/nmxact/nmcoap/dispatcher_test.go b/nmxact/nmcoap/dispatcher_test.go new file mode 100644 index 0000000..374ef16 --- /dev/null +++ b/nmxact/nmcoap/dispatcher_test.go @@ -0,0 +1,321 @@ +/** + * 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" + "errors" + "testing" + "time" + + "github.com/runtimeco/go-coap" +) + +const testTimeout = 5 * time.Second + +func encodeTestMsg(t *testing.T, isTcp bool, token []byte, + uri string, payload string) []byte { + + t.Helper() + + // Built by hand rather than with CreateMsg, which cannot create a + // message without a URI. + m := buildMessage(isTcp, coap.MessageParams{ + Type: coap.Acknowledgement, + Code: coap.Content, + Token: token, + Payload: []byte(payload), + }) + if uri != "" { + m.SetPathString(uri) + } + + b, err := Encode(m) + if err != nil { + t.Fatalf("Encode failed: %v", err) + } + return b +} + +func recvMsg(t *testing.T, l *Listener) coap.Message { + t.Helper() + + select { + case m := <-l.RspChan: + return m + case err := <-l.ErrChan: + t.Fatalf("unexpected error: %v", err) + case <-time.After(testTimeout): + t.Fatalf("timeout waiting for CoAP message") + } + return nil +} + +func recvErr(t *testing.T, l *Listener) error { + t.Helper() + + select { + case err := <-l.ErrChan: + return err + case m := <-l.RspChan: + t.Fatalf("unexpected message: %v", m) + case <-time.After(testTimeout): + t.Fatalf("timeout waiting for error") + } + return nil +} + +func assertIdle(t *testing.T, name string, l *Listener) { + t.Helper() + + if n := len(l.RspChan); n != 0 { + t.Errorf("%s: %d unexpected messages", name, n) + } + if n := len(l.ErrChan); n != 0 { + t.Errorf("%s: %d unexpected errors", name, n) + } +} + +func TestDispatcherAddListenerDuplicate(t *testing.T) { + d := NewDispatcher(false, 0) + + mc := MsgCriteria{Token: []byte{1, 2}, Path: "res"} + if _, err := d.AddListener(mc); err != nil { + t.Fatalf("AddListener failed: %v", err) + } + + // Same criteria, different token slice instance. + dup := MsgCriteria{Token: []byte{1, 2}, Path: "res"} + if _, err := d.AddListener(dup); err == nil { + t.Fatalf("duplicate AddListener succeeded") + } + + others := []MsgCriteria{ + {Token: []byte{1, 3}, Path: "res"}, + {Token: nil, Path: "res"}, + {Token: []byte{1, 2}, Path: ""}, + {Token: nil, Path: ""}, + } + for _, o := range others { + if _, err := d.AddListener(o); err != nil { + t.Errorf("AddListener(%s) failed: %v", o.String(), err) + } + } +} + +func TestDispatcherMatch(t *testing.T) { + tok1 := []byte{0xa1} + tok2 := []byte{0xa2} + tok3 := []byte{0xa3} + + tests := []struct { + name string + token []byte + uri string + want string // name of the listener expected to receive; "" none + }{ + {"token only", tok1, "", "tok1"}, + {"token with other path", tok1, "other", "tok1"}, + {"path and token most specific", tok2, "res", "res+tok2"}, + {"path with unknown token", tok3, "res", "res"}, + {"path wins over token-only", tok1, "res", "res"}, + {"no match", tok3, "other", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := NewDispatcher(false, 0) + + lners := map[string]*Listener{} + add := func(name string, mc MsgCriteria) { + l, err := d.AddListener(mc) + if err != nil { + t.Fatalf("AddListener(%s) failed: %v", name, err) + } + lners[name] = l + } + add("tok1", MsgCriteria{Token: tok1}) + add("res", MsgCriteria{Path: "res"}) + add("res+tok2", MsgCriteria{Token: tok2, Path: "res"}) + + ok := d.Dispatch(encodeTestMsg(t, false, tt.token, tt.uri, "p")) + if ok != (tt.want != "") { + t.Fatalf("Dispatch returned %v", ok) + } + + for name, l := range lners { + if name != tt.want { + assertIdle(t, name, l) + continue + } + + m := recvMsg(t, l) + if !bytes.Equal(m.Token(), tt.token) { + t.Errorf("token = %x, want %x", m.Token(), tt.token) + } + if string(m.Payload()) != "p" { + t.Errorf("payload = %q", m.Payload()) + } + } + }) + } +} + +func TestDispatcherDispatchGarbage(t *testing.T) { + d := NewDispatcher(false, 0) + l, err := d.AddListener(MsgCriteria{}) + if err != nil { + t.Fatal(err) + } + + if d.Dispatch([]byte{0x00, 0x01}) { + t.Errorf("Dispatch of garbage returned true") + } + assertIdle(t, "catch-all", l) +} + +func TestDispatcherDispatchTcp(t *testing.T) { + d := NewDispatcher(true, 0) + tok := []byte{0x10, 0x20} + l, err := d.AddListener(MsgCriteria{Token: tok}) + if err != nil { + t.Fatal(err) + } + + b := encodeTestMsg(t, true, tok, "", "tcp payload") + if d.Dispatch(b[:1]) { + t.Fatalf("Dispatch of partial TCP message returned true") + } + if !d.Dispatch(b[1:]) { + t.Fatalf("Dispatch of completed TCP message returned false") + } + if m := recvMsg(t, l); string(m.Payload()) != "tcp payload" { + t.Errorf("payload = %q", m.Payload()) + } +} + +func TestDispatcherRemoveListener(t *testing.T) { + d := NewDispatcher(false, 0) + tok := []byte{7} + mc := MsgCriteria{Token: tok, Path: "x"} + + l, err := d.AddListener(mc) + if err != nil { + t.Fatal(err) + } + + if got := d.RemoveListener(MsgCriteria{Token: []byte{7}, Path: "x"}); got != l { + t.Fatalf("RemoveListener returned %p, want %p", got, l) + } + if got := d.RemoveListener(mc); got != nil { + t.Fatalf("second RemoveListener returned %p, want nil", got) + } + + if _, ok := <-l.RspChan; ok { + t.Errorf("RspChan not closed") + } + if _, ok := <-l.ErrChan; ok { + t.Errorf("ErrChan not closed") + } + + if d.Dispatch(encodeTestMsg(t, false, tok, "x", "p")) { + t.Errorf("Dispatch to removed listener returned true") + } + + if _, err := d.AddListener(mc); err != nil { + t.Errorf("re-adding listener failed: %v", err) + } +} + +func TestDispatcherErrorOne(t *testing.T) { + d := NewDispatcher(false, 0) + l1, err := d.AddListener(MsgCriteria{Token: []byte{1}}) + if err != nil { + t.Fatal(err) + } + l2, err := d.AddListener(MsgCriteria{Token: []byte{2}}) + if err != nil { + t.Fatal(err) + } + + want := errors.New("one") + if err := d.ErrorOne(MsgCriteria{Token: []byte{1}}, want); err != nil { + t.Fatalf("ErrorOne failed: %v", err) + } + if got := recvErr(t, l1); got != want { + t.Errorf("got %v, want %v", got, want) + } + assertIdle(t, "l2", l2) + + if err := d.ErrorOne(MsgCriteria{Token: []byte{3}}, want); err == nil { + t.Errorf("ErrorOne without matching listener succeeded") + } +} + +func TestDispatcherErrorAll(t *testing.T) { + d := NewDispatcher(false, 0) + + var lners []*Listener + for _, mc := range []MsgCriteria{ + {Token: []byte{1}}, + {Path: "a"}, + {Token: []byte{2}, Path: "a"}, + } { + l, err := d.AddListener(mc) + if err != nil { + t.Fatal(err) + } + lners = append(lners, l) + } + + want := errors.New("all") + d.ErrorAll(want) + + for i, l := range lners { + if got := recvErr(t, l); got != want { + t.Errorf("listener %d: got %v, want %v", i, got, want) + } + } +} + +func TestListenerAfterTimeout(t *testing.T) { + l := NewListener(MsgCriteria{}) + + select { + case _, ok := <-l.AfterTimeout(time.Millisecond): + if !ok { + t.Fatalf("timeout channel closed") + } + case <-time.After(testTimeout): + t.Fatalf("timeout did not fire") + } + + l.Close() +} + +func TestListenerCloseBeforeTimeout(t *testing.T) { + l := NewListener(MsgCriteria{}) + l.AfterTimeout(time.Hour) + l.Close() + + if l.timer.Stop() { + t.Errorf("timer still active after Close") + } +}
