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:34 UTC

(mynewt-newtmgr) 10/20: nmxact/nmble: Add unit tests for dispatcher and listeners

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 34382fa910b64ed5f8b5dba1fe5a06e291d4bfb7
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:30:48 2026 +0200

    nmxact/nmble: Add unit tests for dispatcher and listeners
    
    Cover BLE message decoding for every op/type pair in msgCtorMap and
    rejection of malformed or unknown messages, dispatcher routing
    priority (sequence key, then type plus connection handle, then type
    with any handle), delivery of conn_cancel_evt to the connect listener
    (regression test for c2b0518), listener add/remove, ErrorAll, the
    ListenerMap operations and listener timeout/close behaviour.
    
    Assisted-by: Claude:claude-opus-5-5
---
 nmxact/nmble/dispatch_test.go | 556 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 556 insertions(+)

diff --git a/nmxact/nmble/dispatch_test.go b/nmxact/nmble/dispatch_test.go
new file mode 100644
index 0000000..2cb814a
--- /dev/null
+++ b/nmxact/nmble/dispatch_test.go
@@ -0,0 +1,556 @@
+/**
+ * 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 nmble
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+)
+
+const testTimeout = 5 * time.Second
+
+func testMsgJson(t *testing.T, op MsgOp, typ MsgType, seq BleSeq,
+	connHandle int) []byte {
+
+	t.Helper()
+
+	m := map[string]interface{}{
+		"op":   MsgOpToString(op),
+		"type": MsgTypeToString(typ),
+		"seq":  seq,
+	}
+	if connHandle >= 0 {
+		m["conn_handle"] = connHandle
+	}
+
+	b, err := json.Marshal(m)
+	if err != nil {
+		t.Fatalf("json.Marshal failed: %v", err)
+	}
+	return b
+}
+
+func TestDecodeMsgAllTypes(t *testing.T) {
+	tests := []struct {
+		op   MsgOp
+		typ  MsgType
+		want Msg
+	}{
+		{MSG_OP_RSP, MSG_TYPE_ERR, &BleErrRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SYNC, &BleSyncRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_CONNECT, &BleConnectRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_TERMINATE, &BleTerminateRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_DISC_ALL_SVCS, &BleDiscAllSvcsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_DISC_SVC_UUID, &BleDiscSvcUuidRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_DISC_ALL_CHRS, &BleDiscAllChrsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_DISC_CHR_UUID, &BleDiscChrUuidRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_DISC_ALL_DSCS, &BleDiscAllDscsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_WRITE, &BleWriteRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_WRITE_CMD, &BleWriteCmdRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_EXCHANGE_MTU, &BleExchangeMtuRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_GEN_RAND_ADDR, &BleGenRandAddrRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SET_RAND_ADDR, &BleSetRandAddrRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_CONN_CANCEL, &BleConnCancelRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SCAN, &BleScanRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SCAN_CANCEL, &BleScanCancelRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SET_PREFERRED_MTU, &BleSetPreferredMtuRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SECURITY_INITIATE, &BleSecurityInitiateRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_CONN_FIND, &BleConnFindRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_RESET, &BleResetRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADV_START, &BleAdvStartRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADV_STOP, &BleAdvStopRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADV_SET_DATA, &BleAdvSetDataRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADV_RSP_SET_DATA, &BleAdvRspSetDataRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADV_FIELDS, &BleAdvFieldsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_CLEAR_SVCS, &BleClearSvcsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ADD_SVCS, &BleAddSvcsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_COMMIT_SVCS, &BleCommitSvcsRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_ACCESS_STATUS, &BleAccessStatusRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_NOTIFY, &BleNotifyRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_FIND_CHR, &BleFindChrRsp{}},
+		{MSG_OP_RSP, MSG_TYPE_SM_INJECT_IO, &BleSmInjectIoRsp{}},
+		{MSG_OP_EVT, MSG_TYPE_SYNC_EVT, &BleSyncEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_CONNECT_EVT, &BleConnectEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_CONN_CANCEL_EVT, &BleConnCancelEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_DISCONNECT_EVT, &BleDisconnectEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_DISC_SVC_EVT, &BleDiscSvcEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_DISC_CHR_EVT, &BleDiscChrEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_DISC_DSC_EVT, &BleDiscDscEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_WRITE_ACK_EVT, &BleWriteAckEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_NOTIFY_RX_EVT, &BleNotifyRxEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_MTU_CHANGE_EVT, &BleMtuChangeEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_SCAN_EVT, &BleScanEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_SCAN_COMPLETE_EVT, &BleScanCompleteEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_ADV_COMPLETE_EVT, &BleAdvCompleteEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_ENC_CHANGE_EVT, &BleEncChangeEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_RESET_EVT, &BleResetEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_ACCESS_EVT, &BleAccessEvt{}},
+		{MSG_OP_EVT, MSG_TYPE_PASSKEY_EVT, &BlePasskeyEvt{}},
+	}
+
+	if len(tests) != len(msgCtorMap) {
+		t.Errorf("table has %d entries, msgCtorMap has %d",
+			len(tests), len(msgCtorMap))
+	}
+
+	for _, tt := range tests {
+		name := MsgOpToString(tt.op) + "/" + MsgTypeToString(tt.typ)
+		t.Run(name, func(t *testing.T) {
+			data := testMsgJson(t, tt.op, tt.typ, 1234, 7)
+
+			base, msg, err := decodeMsg(data)
+			if err != nil {
+				t.Fatalf("decodeMsg failed: %v", err)
+			}
+			if base.Op != tt.op || base.Type != tt.typ {
+				t.Errorf("base op/type = %d/%d, want %d/%d",
+					base.Op, base.Type, tt.op, tt.typ)
+			}
+			if base.Seq != 1234 {
+				t.Errorf("base seq = %d, want 1234", base.Seq)
+			}
+			if base.ConnHandle != 7 {
+				t.Errorf("base conn handle = %d, want 7", base.ConnHandle)
+			}
+			if reflect.TypeOf(msg) != reflect.TypeOf(tt.want) {
+				t.Errorf("decoded %T, want %T", msg, tt.want)
+			}
+		})
+	}
+}
+
+func TestDecodeMsgFields(t *testing.T) {
+	data := []byte(`{"op":"event","type":"notify_rx_evt","seq":3,` +
+		`"conn_handle":9,"attr_handle":17,"indication":true,` +
+		`"data":"0x01:0x02:0x03"}`)
+
+	_, msg, err := decodeMsg(data)
+	if err != nil {
+		t.Fatalf("decodeMsg failed: %v", err)
+	}
+
+	evt, ok := msg.(*BleNotifyRxEvt)
+	if !ok {
+		t.Fatalf("decoded %T, want *BleNotifyRxEvt", msg)
+	}
+	if evt.Seq != 3 || evt.ConnHandle != 9 || evt.AttrHandle != 17 ||
+		!evt.Indication {
+
+		t.Errorf("unexpected decoded event: %+v", evt)
+	}
+	if !reflect.DeepEqual(evt.Data.Bytes, []byte{1, 2, 3}) {
+		t.Errorf("data = %x, want 010203", evt.Data.Bytes)
+	}
+}
+
+func TestDecodeMsgInvalid(t *testing.T) {
+	tests := []struct {
+		name string
+		data string
+	}{
+		{"not json", `{"op":`},
+		{"unknown op", `{"op":"bogus","type":"connect","seq":1}`},
+		{"unknown type", `{"op":"response","type":"bogus","seq":1}`},
+		{"request op", `{"op":"request","type":"connect","seq":1}`},
+		{"event type as response",
+			`{"op":"response","type":"connect_evt","seq":1}`},
+		{"bad field type",
+			`{"op":"response","type":"connect","seq":1,"status":"x"}`},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if _, msg, err := decodeMsg([]byte(tt.data)); err == nil {
+				t.Errorf("decodeMsg succeeded: %T %+v", msg, msg)
+			}
+		})
+	}
+}
+
+func recvMsg(t *testing.T, l *Listener) Msg {
+	t.Helper()
+
+	select {
+	case m := <-l.MsgChan:
+		return m
+	case err := <-l.ErrChan:
+		t.Fatalf("unexpected error: %v", err)
+	case <-time.After(testTimeout):
+		t.Fatalf("timeout waiting for BLE message")
+	}
+	return nil
+}
+
+func addTestListener(t *testing.T, d *Dispatcher, key ListenerKey) *Listener {
+	t.Helper()
+
+	l := NewListener()
+	if err := d.AddListener(key, l); err != nil {
+		t.Fatalf("AddListener(%+v) failed: %v", key, err)
+	}
+	return l
+}
+
+func TestDispatcherRouting(t *testing.T) {
+	const typ = MSG_TYPE_NOTIFY_RX_EVT
+
+	tests := []struct {
+		name       string
+		seq        BleSeq
+		typ        MsgType
+		connHandle int
+		want       string
+	}{
+		{"seq key wins", 5, typ, 3, "seq"},
+		{"seq key any type", 5, MSG_TYPE_DISCONNECT_EVT, 3, "seq"},
+		{"type and conn handle", BLE_SEQ_NONE, typ, 3, "tch"},
+		{"type with any conn handle", BLE_SEQ_NONE, typ, 4, "type"},
+		{"no conn handle in message", 6, typ, -1, "type"},
+		{"no listener", 6, MSG_TYPE_DISCONNECT_EVT, 3, ""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := NewDispatcher()
+			lners := map[string]*Listener{
+				"seq":  addTestListener(t, d, SeqKey(5)),
+				"tch":  addTestListener(t, d, TchKey(typ, 3)),
+				"type": addTestListener(t, d, TchKey(typ, -1)),
+			}
+
+			d.Dispatch(testMsgJson(t, MSG_OP_EVT, tt.typ, tt.seq,
+				tt.connHandle))
+
+			for name, l := range lners {
+				if name != tt.want {
+					if n := len(l.MsgChan); n != 0 {
+						t.Errorf("listener %q got %d messages", name, n)
+					}
+					continue
+				}
+
+				m := recvMsg(t, l)
+				want := msgCtorMap[OpTypePair{MSG_OP_EVT, tt.typ}]()
+				if reflect.TypeOf(m) != reflect.TypeOf(want) {
+					t.Errorf("got %T, want %T", m, want)
+				}
+			}
+		})
+	}
+}
+
+// Regression test for c2b0518: blehostd sends conn_cancel_evt with the
+// sequence number of the connect request, so it must reach the listener
+// registered for the connect procedure.
+func TestDispatcherConnCancelEvtUsesConnectSeq(t *testing.T) {
+	const connectSeq BleSeq = 100
+	const cancelSeq BleSeq = 101
+
+	d := NewDispatcher()
+	connL := addTestListener(t, d, SeqKey(connectSeq))
+	cancelL := addTestListener(t, d, SeqKey(cancelSeq))
+
+	d.Dispatch(testMsgJson(t, MSG_OP_RSP, MSG_TYPE_CONN_CANCEL, cancelSeq,
+		-1))
+	d.Dispatch(testMsgJson(t, MSG_OP_EVT, MSG_TYPE_CONN_CANCEL_EVT,
+		connectSeq, -1))
+
+	if m, ok := recvMsg(t, cancelL).(*BleConnCancelRsp); !ok {
+		t.Errorf("cancel listener got %T, want *BleConnCancelRsp", m)
+	}
+	if m, ok := recvMsg(t, connL).(*BleConnCancelEvt); !ok {
+		t.Errorf("connect listener got %T, want *BleConnCancelEvt", m)
+	}
+	if n := len(cancelL.MsgChan); n != 0 {
+		t.Errorf("cancel listener got %d extra messages", n)
+	}
+}
+
+func TestDispatcherDropsInvalid(t *testing.T) {
+	d := NewDispatcher()
+	l := addTestListener(t, d, SeqKey(1))
+
+	for _, data := range []string{
+		`garbage`,
+		`{"op":"bogus","type":"connect","seq":1}`,
+		`{"op":"response","type":"bogus","seq":1}`,
+	} {
+		d.Dispatch([]byte(data))
+	}
+
+	if n := len(l.MsgChan); n != 0 {
+		t.Errorf("listener got %d messages from invalid input", n)
+	}
+}
+
+func TestDispatcherAddRemove(t *testing.T) {
+	d := NewDispatcher()
+	key := SeqKey(9)
+	l := addTestListener(t, d, key)
+
+	if err := d.AddListener(key, NewListener()); err == nil {
+		t.Errorf("duplicate key accepted")
+	}
+	if err := d.AddListener(SeqKey(10), l); err == nil {
+		t.Errorf("duplicate listener accepted")
+	}
+
+	got := d.RemoveListener(l)
+	if got == nil || *got != key {
+		t.Fatalf("RemoveListener returned %v, want %+v", got, key)
+	}
+	if _, ok := <-l.MsgChan; ok {
+		t.Errorf("MsgChan not closed")
+	}
+	if got := d.RemoveListener(l); got != nil {
+		t.Errorf("second RemoveListener returned %+v", *got)
+	}
+
+	l2 := addTestListener(t, d, key)
+	if got := d.RemoveKey(key); got != l2 {
+		t.Errorf("RemoveKey returned %p, want %p", got, l2)
+	}
+	if got := d.RemoveKey(key); got != nil {
+		t.Errorf("second RemoveKey returned %p", got)
+	}
+
+	d.Dispatch(testMsgJson(t, MSG_OP_RSP, MSG_TYPE_CONNECT, 9, -1))
+}
+
+func TestDispatcherErrorAll(t *testing.T) {
+	d := NewDispatcher()
+
+	var lners []*Listener
+	for i := 0; i < 3; i++ {
+		lners = append(lners, addTestListener(t, d, SeqKey(BleSeq(i))))
+	}
+	lners = append(lners, addTestListener(t, d,
+		TchKey(MSG_TYPE_DISCONNECT_EVT, -1)))
+
+	want := errors.New("xport stopped")
+	d.ErrorAll(want)
+
+	for i, l := range lners {
+		// The error is delivered before the listener is closed.
+		select {
+		case err, ok := <-l.ErrChan:
+			if ok && err != want {
+				t.Errorf("listener %d: got %v, want %v", i, err, want)
+			}
+		case <-time.After(testTimeout):
+			t.Fatalf("listener %d: ErrChan neither written nor closed", i)
+		}
+		if _, ok := <-l.MsgChan; ok {
+			t.Errorf("listener %d: MsgChan not closed", i)
+		}
+	}
+
+	// All listeners were removed; keys can be reused.
+	addTestListener(t, d, SeqKey(0))
+}
+
+func TestListenerMapFind(t *testing.T) {
+	lm := NewListenerMap()
+	seqL := NewListener()
+	tchL := NewListener()
+	anyL := NewListener()
+
+	for _, e := range []struct {
+		key ListenerKey
+		l   *Listener
+	}{
+		{SeqKey(1), seqL},
+		{TchKey(MSG_TYPE_WRITE_ACK_EVT, 2), tchL},
+		{TchKey(MSG_TYPE_WRITE_ACK_EVT, -1), anyL},
+	} {
+		if err := lm.AddListener(e.key, e.l); err != nil {
+			t.Fatalf("AddListener failed: %v", err)
+		}
+	}
+
+	tests := []struct {
+		seq     BleSeq
+		typ     MsgType
+		conn    int
+		wantKey ListenerKey
+		want    *Listener
+	}{
+		{1, MSG_TYPE_WRITE_ACK_EVT, 2, SeqKey(1), seqL},
+		{2, MSG_TYPE_WRITE_ACK_EVT, 2,
+			TchKey(MSG_TYPE_WRITE_ACK_EVT, 2), tchL},
+		{2, MSG_TYPE_WRITE_ACK_EVT, 3,
+			TchKey(MSG_TYPE_WRITE_ACK_EVT, -1), anyL},
+		{2, MSG_TYPE_NOTIFY_RX_EVT, 2,
+			TchKey(MSG_TYPE_NOTIFY_RX_EVT, -1), nil},
+	}
+
+	for i, tt := range tests {
+		key, l := lm.FindListener(tt.seq, tt.typ, tt.conn)
+		if l != tt.want {
+			t.Errorf("%d: listener = %p, want %p", i, l, tt.want)
+		}
+		if key != tt.wantKey {
+			t.Errorf("%d: key = %+v, want %+v", i, key, tt.wantKey)
+		}
+	}
+}
+
+func TestListenerMapAddRemove(t *testing.T) {
+	lm := NewListenerMap()
+	l1 := NewListener()
+	l2 := NewListener()
+	k1 := SeqKey(1)
+	k2 := TchKey(MSG_TYPE_CONNECT_EVT, 5)
+
+	if err := lm.AddListener(k1, l1); err != nil {
+		t.Fatal(err)
+	}
+	if err := lm.AddListener(k1, l2); err == nil {
+		t.Errorf("duplicate key accepted")
+	}
+	if err := lm.AddListener(k2, l1); err == nil {
+		t.Errorf("duplicate listener accepted")
+	}
+	if err := lm.AddListener(k2, l2); err != nil {
+		t.Fatal(err)
+	}
+
+	if got := lm.RemoveListener(l1); got == nil || *got != k1 {
+		t.Errorf("RemoveListener(l1) = %v, want %+v", got, k1)
+	}
+	if got := lm.RemoveListener(l1); got != nil {
+		t.Errorf("second RemoveListener(l1) = %+v", *got)
+	}
+	if got := lm.RemoveKey(k2); got != l2 {
+		t.Errorf("RemoveKey(k2) = %p, want %p", got, l2)
+	}
+	if got := lm.RemoveKey(k2); got != nil {
+		t.Errorf("second RemoveKey(k2) = %p", got)
+	}
+	if _, l := lm.FindListener(1, MSG_TYPE_CONNECT_EVT, 5); l != nil {
+		t.Errorf("found removed listener")
+	}
+
+	// Both can be added again.
+	if err := lm.AddListener(k1, l1); err != nil {
+		t.Errorf("re-adding l1 failed: %v", err)
+	}
+	if err := lm.AddListener(k2, l2); err != nil {
+		t.Errorf("re-adding l2 failed: %v", err)
+	}
+}
+
+func TestListenerMapExtractAll(t *testing.T) {
+	lm := NewListenerMap()
+	want := map[*Listener]bool{}
+	for i := 0; i < 4; i++ {
+		l := NewListener()
+		if err := lm.AddListener(SeqKey(BleSeq(i)), l); err != nil {
+			t.Fatal(err)
+		}
+		want[l] = true
+	}
+
+	got := lm.ExtractAll()
+	if len(got) != len(want) {
+		t.Fatalf("ExtractAll returned %d listeners, want %d",
+			len(got), len(want))
+	}
+	for _, l := range got {
+		if !want[l] {
+			t.Errorf("unexpected listener %p", l)
+		}
+	}
+
+	if got := lm.ExtractAll(); len(got) != 0 {
+		t.Errorf("second ExtractAll returned %d listeners", len(got))
+	}
+	if _, l := lm.FindListener(0, MSG_TYPE_CONNECT_EVT, 0); l != nil {
+		t.Errorf("listener still present after ExtractAll")
+	}
+}
+
+func TestListenerTimeout(t *testing.T) {
+	l := NewListener()
+
+	select {
+	case <-l.AfterTimeout(time.Millisecond):
+	case <-time.After(testTimeout):
+		t.Fatalf("timeout did not fire")
+	}
+
+	l.Close()
+	for name, closed := range map[string]bool{
+		"MsgChan": isClosedMsg(l.MsgChan),
+		"ErrChan": isClosedErr(l.ErrChan),
+		"TmoChan": isClosedTmo(l.TmoChan),
+	} {
+		if !closed {
+			t.Errorf("%s not closed", name)
+		}
+	}
+}
+
+func TestListenerCloseDrains(t *testing.T) {
+	l := NewListener()
+	l.MsgChan <- &BleSyncEvt{}
+	l.ErrChan <- fmt.Errorf("pending")
+	l.AfterTimeout(time.Hour)
+
+	l.Close()
+
+	if !isClosedMsg(l.MsgChan) || !isClosedErr(l.ErrChan) {
+		t.Errorf("channels not drained and closed")
+	}
+	if l.timer.Stop() {
+		t.Errorf("timer still active after Close")
+	}
+}
+
+func isClosedMsg(c chan Msg) bool {
+	select {
+	case _, ok := <-c:
+		return !ok
+	default:
+		return false
+	}
+}
+
+func isClosedErr(c chan error) bool {
+	select {
+	case _, ok := <-c:
+		return !ok
+	default:
+		return false
+	}
+}
+
+func isClosedTmo(c chan time.Time) bool {
+	select {
+	case _, ok := <-c:
+		return !ok
+	default:
+		return false
+	}
+}