| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- package fwh_mqtt
- import (
- "context"
- "encoding/json"
- "fwh-toolbox-server/config"
- "sync"
- "testing"
- "time"
- )
- // fakeTransport 内存实现,记录订阅和发送,并可模拟 broker 推送
- type fakeTransport struct {
- mu sync.Mutex
- connected bool
- subs map[string]func([]byte)
- published []string
- subCalls int
- unsubCalls int
- disconnects int
- }
- func newFake() *fakeTransport { return &fakeTransport{subs: map[string]func([]byte){}} }
- func (f *fakeTransport) Connect() error { f.mu.Lock(); f.connected = true; f.mu.Unlock(); return nil }
- func (f *fakeTransport) IsConnected() bool {
- f.mu.Lock()
- defer f.mu.Unlock()
- return f.connected
- }
- func (f *fakeTransport) Disconnect() {
- f.mu.Lock()
- f.connected = false
- f.disconnects++
- f.mu.Unlock()
- }
- func (f *fakeTransport) SubscribeMessage(topic string, cb func([]byte)) error {
- f.mu.Lock()
- f.subs[topic] = cb
- f.subCalls++
- f.mu.Unlock()
- return nil
- }
- func (f *fakeTransport) UnSubscribeMessage(topic string) error {
- f.mu.Lock()
- delete(f.subs, topic)
- f.unsubCalls++
- f.mu.Unlock()
- return nil
- }
- func (f *fakeTransport) SendMessageOnce(topic string, msg []byte) error {
- f.mu.Lock()
- f.published = append(f.published, topic)
- f.mu.Unlock()
- return nil
- }
- func (f *fakeTransport) GetClientStatus() string { return "fake" }
- // push 模拟 broker 推送
- func (f *fakeTransport) push(topic string, cmdType uint) bool {
- f.mu.Lock()
- cb := f.subs[topic]
- f.mu.Unlock()
- if cb == nil {
- return false
- }
- b, _ := json.Marshal(FwhMqttMsg{CmdType: cmdType, EquipNo: "SN", DataArea: map[string]int{"cmd": 2}})
- cb(b)
- return true
- }
- func (f *fakeTransport) counts() (sub, unsub int) {
- f.mu.Lock()
- defer f.mu.Unlock()
- return f.subCalls, f.unsubCalls
- }
- func waitFor(t *testing.T, cond func() bool) {
- t.Helper()
- deadline := time.Now().Add(2 * time.Second)
- for !cond() {
- if time.Now().After(deadline) {
- t.Fatal("condition not met in time")
- }
- time.Sleep(5 * time.Millisecond)
- }
- }
- func TestParseEnv(t *testing.T) {
- cases := []struct {
- in string
- want Env
- wantErr bool
- }{
- {"", EnvProd, false},
- {"prod", EnvProd, false},
- {"test", EnvTest, false},
- {"TEST", "", true},
- {"dev", "", true},
- }
- for _, c := range cases {
- got, err := ParseEnv(c.in)
- if (err != nil) != c.wantErr || got != c.want {
- t.Errorf("ParseEnv(%q) = %q, %v; want %q, err=%v", c.in, got, err, c.want, c.wantErr)
- }
- }
- }
- func TestInitWithoutBrokerFails(t *testing.T) {
- if err := InitFwhMqttClient(EnvTest, &config.FwhMqttConfig{}); err == nil {
- t.Fatal("expected error when broker not configured")
- }
- if _, err := GetClient(EnvTest); err == nil {
- t.Fatal("client should not be registered after failed init")
- }
- }
- func TestSubscribeFanOutAndRefCount(t *testing.T) {
- f := newFake()
- c := newFwhMqttClient(EnvTest, f)
- ctx := context.Background()
- var mu sync.Mutex
- got := map[string]int{}
- h := func(name string) Handler {
- return func(m Message) {
- mu.Lock()
- got[name]++
- mu.Unlock()
- }
- }
- cancelA, err := c.Subscribe(ctx, "t", h("a"))
- if err != nil {
- t.Fatal(err)
- }
- cancelB, err := c.Subscribe(ctx, "t", h("b"))
- if err != nil {
- t.Fatal(err)
- }
- if sub, _ := f.counts(); sub != 1 {
- t.Fatalf("broker subscribe calls = %d, want 1", sub)
- }
- f.push("t", 379)
- waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return got["a"] == 1 && got["b"] == 1 })
- cancelA()
- cancelA() // 重复取消无副作用
- f.push("t", 379)
- waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return got["b"] == 2 })
- mu.Lock()
- if got["a"] != 1 {
- t.Errorf("cancelled subscriber received message")
- }
- mu.Unlock()
- if _, unsub := f.counts(); unsub != 0 {
- t.Errorf("should not unsubscribe while subscribers remain")
- }
- cancelB()
- waitFor(t, func() bool { _, unsub := f.counts(); return unsub == 1 })
- if c.hasTopics() {
- t.Error("topics should be empty")
- }
- }
- func TestRequestMatchesResponse(t *testing.T) {
- f := newFake()
- c := newFwhMqttClient(EnvTest, f)
- go func() {
- waitFor(t, func() bool {
- f.mu.Lock()
- defer f.mu.Unlock()
- return len(f.published) == 1
- })
- f.push(C2STopic("mng/SN"), 379) // 不匹配,应被忽略
- f.push(C2STopic("mng/SN"), 378)
- }()
- res, err := c.Request(context.Background(), RequestOpt{
- Topic: S2CTopic("mng/SN"),
- Msg: FwhMqttMsg{CmdType: 377},
- ResTopic: C2STopic("mng/SN"),
- Match: func(m Message) bool { return m.Envelope.CmdType == 378 },
- Timeout: 2 * time.Second,
- })
- if err != nil {
- t.Fatal(err)
- }
- if res.Envelope.CmdType != 378 || len(res.Raw) == 0 {
- t.Errorf("unexpected response: %+v", res.Envelope)
- }
- waitFor(t, func() bool { _, unsub := f.counts(); return unsub == 1 })
- }
- func TestRequestTimeout(t *testing.T) {
- c := newFwhMqttClient(EnvTest, newFake())
- _, err := c.Request(context.Background(), RequestOpt{
- Topic: "a", ResTopic: "b", Timeout: 50 * time.Millisecond,
- })
- if err == nil {
- t.Fatal("expected timeout")
- }
- }
- func TestNewEnvelope(t *testing.T) {
- m, err := NewEnvelope(377, "SN", map[string]any{"cmd": 4, "data": map[string]any{}})
- if err != nil {
- t.Fatal(err)
- }
- // {"cmd":4,"data":{}} 共 19 字节
- if m.Len != 19 || len(m.Crc) != 8 || m.EquipNo != "SN" || m.CmdType != 377 {
- t.Errorf("envelope: %+v", m)
- }
- }
|