fwh_mqtt_client_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package fwh_mqtt
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fwh-toolbox-server/config"
  6. "sync"
  7. "testing"
  8. "time"
  9. )
  10. // fakeTransport 内存实现,记录订阅和发送,并可模拟 broker 推送
  11. type fakeTransport struct {
  12. mu sync.Mutex
  13. connected bool
  14. subs map[string]func([]byte)
  15. published []string
  16. subCalls int
  17. unsubCalls int
  18. disconnects int
  19. }
  20. func newFake() *fakeTransport { return &fakeTransport{subs: map[string]func([]byte){}} }
  21. func (f *fakeTransport) Connect() error { f.mu.Lock(); f.connected = true; f.mu.Unlock(); return nil }
  22. func (f *fakeTransport) IsConnected() bool {
  23. f.mu.Lock()
  24. defer f.mu.Unlock()
  25. return f.connected
  26. }
  27. func (f *fakeTransport) Disconnect() {
  28. f.mu.Lock()
  29. f.connected = false
  30. f.disconnects++
  31. f.mu.Unlock()
  32. }
  33. func (f *fakeTransport) SubscribeMessage(topic string, cb func([]byte)) error {
  34. f.mu.Lock()
  35. f.subs[topic] = cb
  36. f.subCalls++
  37. f.mu.Unlock()
  38. return nil
  39. }
  40. func (f *fakeTransport) UnSubscribeMessage(topic string) error {
  41. f.mu.Lock()
  42. delete(f.subs, topic)
  43. f.unsubCalls++
  44. f.mu.Unlock()
  45. return nil
  46. }
  47. func (f *fakeTransport) SendMessageOnce(topic string, msg []byte) error {
  48. f.mu.Lock()
  49. f.published = append(f.published, topic)
  50. f.mu.Unlock()
  51. return nil
  52. }
  53. func (f *fakeTransport) GetClientStatus() string { return "fake" }
  54. // push 模拟 broker 推送
  55. func (f *fakeTransport) push(topic string, cmdType uint) bool {
  56. f.mu.Lock()
  57. cb := f.subs[topic]
  58. f.mu.Unlock()
  59. if cb == nil {
  60. return false
  61. }
  62. b, _ := json.Marshal(FwhMqttMsg{CmdType: cmdType, EquipNo: "SN", DataArea: map[string]int{"cmd": 2}})
  63. cb(b)
  64. return true
  65. }
  66. func (f *fakeTransport) counts() (sub, unsub int) {
  67. f.mu.Lock()
  68. defer f.mu.Unlock()
  69. return f.subCalls, f.unsubCalls
  70. }
  71. func waitFor(t *testing.T, cond func() bool) {
  72. t.Helper()
  73. deadline := time.Now().Add(2 * time.Second)
  74. for !cond() {
  75. if time.Now().After(deadline) {
  76. t.Fatal("condition not met in time")
  77. }
  78. time.Sleep(5 * time.Millisecond)
  79. }
  80. }
  81. func TestParseEnv(t *testing.T) {
  82. cases := []struct {
  83. in string
  84. want Env
  85. wantErr bool
  86. }{
  87. {"", EnvProd, false},
  88. {"prod", EnvProd, false},
  89. {"test", EnvTest, false},
  90. {"TEST", "", true},
  91. {"dev", "", true},
  92. }
  93. for _, c := range cases {
  94. got, err := ParseEnv(c.in)
  95. if (err != nil) != c.wantErr || got != c.want {
  96. t.Errorf("ParseEnv(%q) = %q, %v; want %q, err=%v", c.in, got, err, c.want, c.wantErr)
  97. }
  98. }
  99. }
  100. func TestInitWithoutBrokerFails(t *testing.T) {
  101. if err := InitFwhMqttClient(EnvTest, &config.FwhMqttConfig{}); err == nil {
  102. t.Fatal("expected error when broker not configured")
  103. }
  104. if _, err := GetClient(EnvTest); err == nil {
  105. t.Fatal("client should not be registered after failed init")
  106. }
  107. }
  108. func TestSubscribeFanOutAndRefCount(t *testing.T) {
  109. f := newFake()
  110. c := newFwhMqttClient(EnvTest, f)
  111. ctx := context.Background()
  112. var mu sync.Mutex
  113. got := map[string]int{}
  114. h := func(name string) Handler {
  115. return func(m Message) {
  116. mu.Lock()
  117. got[name]++
  118. mu.Unlock()
  119. }
  120. }
  121. cancelA, err := c.Subscribe(ctx, "t", h("a"))
  122. if err != nil {
  123. t.Fatal(err)
  124. }
  125. cancelB, err := c.Subscribe(ctx, "t", h("b"))
  126. if err != nil {
  127. t.Fatal(err)
  128. }
  129. if sub, _ := f.counts(); sub != 1 {
  130. t.Fatalf("broker subscribe calls = %d, want 1", sub)
  131. }
  132. f.push("t", 379)
  133. waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return got["a"] == 1 && got["b"] == 1 })
  134. cancelA()
  135. cancelA() // 重复取消无副作用
  136. f.push("t", 379)
  137. waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return got["b"] == 2 })
  138. mu.Lock()
  139. if got["a"] != 1 {
  140. t.Errorf("cancelled subscriber received message")
  141. }
  142. mu.Unlock()
  143. if _, unsub := f.counts(); unsub != 0 {
  144. t.Errorf("should not unsubscribe while subscribers remain")
  145. }
  146. cancelB()
  147. waitFor(t, func() bool { _, unsub := f.counts(); return unsub == 1 })
  148. if c.hasTopics() {
  149. t.Error("topics should be empty")
  150. }
  151. }
  152. func TestRequestMatchesResponse(t *testing.T) {
  153. f := newFake()
  154. c := newFwhMqttClient(EnvTest, f)
  155. go func() {
  156. waitFor(t, func() bool {
  157. f.mu.Lock()
  158. defer f.mu.Unlock()
  159. return len(f.published) == 1
  160. })
  161. f.push(C2STopic("mng/SN"), 379) // 不匹配,应被忽略
  162. f.push(C2STopic("mng/SN"), 378)
  163. }()
  164. res, err := c.Request(context.Background(), RequestOpt{
  165. Topic: S2CTopic("mng/SN"),
  166. Msg: FwhMqttMsg{CmdType: 377},
  167. ResTopic: C2STopic("mng/SN"),
  168. Match: func(m Message) bool { return m.Envelope.CmdType == 378 },
  169. Timeout: 2 * time.Second,
  170. })
  171. if err != nil {
  172. t.Fatal(err)
  173. }
  174. if res.Envelope.CmdType != 378 || len(res.Raw) == 0 {
  175. t.Errorf("unexpected response: %+v", res.Envelope)
  176. }
  177. waitFor(t, func() bool { _, unsub := f.counts(); return unsub == 1 })
  178. }
  179. func TestRequestTimeout(t *testing.T) {
  180. c := newFwhMqttClient(EnvTest, newFake())
  181. _, err := c.Request(context.Background(), RequestOpt{
  182. Topic: "a", ResTopic: "b", Timeout: 50 * time.Millisecond,
  183. })
  184. if err == nil {
  185. t.Fatal("expected timeout")
  186. }
  187. }
  188. func TestNewEnvelope(t *testing.T) {
  189. m, err := NewEnvelope(377, "SN", map[string]any{"cmd": 4, "data": map[string]any{}})
  190. if err != nil {
  191. t.Fatal(err)
  192. }
  193. // {"cmd":4,"data":{}} 共 19 字节
  194. if m.Len != 19 || len(m.Crc) != 8 || m.EquipNo != "SN" || m.CmdType != 377 {
  195. t.Errorf("envelope: %+v", m)
  196. }
  197. }