decred.org/dcrdex@v1.0.3/dex/ws/wslink_test.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package ws 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "math" 13 "math/rand" 14 "os" 15 "sync/atomic" 16 "testing" 17 "time" 18 19 "decred.org/dcrdex/dex" 20 "decred.org/dcrdex/dex/msgjson" 21 "github.com/gorilla/websocket" 22 ) 23 24 var tLogger = dex.StdOutLogger("ws_TEST", dex.LevelTrace) 25 26 type ConnStub struct { 27 inMsg chan []byte 28 inErr chan error 29 closed int32 30 } 31 32 func (c *ConnStub) Close() error { 33 // make ReadMessage return with a close error 34 atomic.StoreInt32(&c.closed, 1) 35 c.inErr <- &websocket.CloseError{ 36 Code: websocket.CloseNormalClosure, 37 Text: "bye", 38 } 39 return nil 40 } 41 func (c *ConnStub) SetReadLimit(int64) {} 42 func (c *ConnStub) SetReadDeadline(t time.Time) error { 43 return nil 44 } 45 func (c *ConnStub) SetWriteDeadline(t time.Time) error { 46 return nil 47 } 48 func (c *ConnStub) ReadMessage() (int, []byte, error) { 49 if atomic.LoadInt32(&c.closed) == 1 { 50 return 0, nil, &websocket.CloseError{ 51 Code: websocket.CloseAbnormalClosure, 52 Text: io.ErrUnexpectedEOF.Error(), 53 } 54 } 55 select { 56 case msg := <-c.inMsg: 57 return len(msg), msg, nil 58 case err := <-c.inErr: 59 return 0, nil, err 60 } 61 } 62 63 const ( 64 stdDev = 500.0 65 minInterval = int64(50) 66 ) 67 68 func microSecDelay(stdDev float64, min int64) time.Duration { 69 return time.Microsecond * time.Duration(int64(stdDev*math.Abs(rand.NormFloat64()))+min) 70 } 71 72 var lastID int64 = -1 // first msg.ID should be 0 73 74 func (c *ConnStub) WriteMessage(_ int, b []byte) error { 75 if atomic.LoadInt32(&c.closed) == 1 { 76 return websocket.ErrCloseSent 77 } 78 msg, err := msgjson.DecodeMessage(b) 79 if err != nil { 80 return err 81 } 82 if msg.ID != uint64(lastID+1) { 83 return fmt.Errorf("sent out of sequence. got %d, want %v", msg.ID, uint64(lastID+1)) 84 } 85 lastID++ 86 writeDuration := microSecDelay(stdDev, minInterval) 87 time.Sleep(writeDuration) 88 return nil 89 } 90 func (c *ConnStub) WriteControl(messageType int, data []byte, deadline time.Time) error { 91 switch messageType { 92 case websocket.PingMessage: 93 fmt.Println(" <ConnStub> ping sent to peer") 94 case websocket.TextMessage: 95 fmt.Println(" <ConnStub> message sent to peer:", string(data)) 96 case websocket.CloseMessage: 97 fmt.Println(" <ConnStub> close control frame sent to peer") 98 default: 99 fmt.Printf(" <ConnStub> message type %d sent to peer\n", messageType) 100 } 101 return nil 102 } 103 104 func TestWSLink_handleMessageRecover(t *testing.T) { 105 defer func() { 106 if pv := recover(); pv != nil { 107 t.Errorf("A panic made it through: %v", pv) 108 } 109 }() 110 111 inMsgHandler := func(msg *msgjson.Message) *msgjson.Error { 112 panic("oh snap") 113 } 114 115 conn := &ConnStub{ 116 inMsg: make(chan []byte, 1), 117 inErr: make(chan error, 1), 118 } 119 ipk := dex.IPKey{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255 /* ipv4 */, 127, 0, 0, 1} 120 wsLink := NewWSLink(ipk.String(), conn, time.Second, inMsgHandler, tLogger) 121 122 msg := new(msgjson.Message) 123 wsLink.handleMessage(msg) 124 125 wsLink.handler = func(msg *msgjson.Message) *msgjson.Error { 126 var v []int 127 _ = v[0] 128 return nil 129 } 130 131 msg, _ = msgjson.NewRequest(123, "bad", `stuff`) 132 wsLink.handleMessage(msg) 133 } 134 135 func TestWSLink_send(t *testing.T) { 136 defer os.Stdout.Sync() 137 138 handlerChan := make(chan struct{}, 1) 139 inMsgHandler := func(msg *msgjson.Message) *msgjson.Error { 140 handlerChan <- struct{}{} 141 return nil 142 } 143 144 conn := &ConnStub{ 145 inMsg: make(chan []byte, 1), 146 inErr: make(chan error, 1), 147 } 148 ipk := dex.IPKey{16, 16, 120, 120 /* ipv6 1010:7878:: */} 149 wsLink := NewWSLink(ipk.String(), conn, time.Second, inMsgHandler, tLogger) 150 ctx, cancel := context.WithCancel(context.Background()) 151 defer cancel() 152 153 // start the in/out/pingHandlers, and the initial read deadline 154 wg, err := wsLink.Connect(ctx) 155 if err != nil { 156 t.Fatalf("Connect: %v", err) 157 } 158 159 defer wg.Wait() 160 161 // hit the inHandler once before testing sends 162 msg, _ := msgjson.NewRequest(12, "beep", "boop") 163 b, err := json.Marshal(msg) 164 if err != nil { 165 t.Fatal(err) 166 } 167 // give something for inHandler to read 168 conn.inMsg <- b 169 // ensure that the handler was called 170 select { 171 case <-handlerChan: 172 case <-time.NewTimer(time.Second).C: 173 t.Fatal("in handler not called") 174 } 175 176 // sends 177 stuff := struct { 178 Thing int `json:"thing"` 179 Blah string `json:"stuff"` 180 }{ 181 12, "asdf", 182 } 183 msg, _ = msgjson.NewNotification("blah", stuff) 184 185 err = wsLink.SendNow(msg) 186 if err != nil { 187 t.Error(err) 188 } 189 msg.ID++ // not normally used for ntfns, just in this test 190 191 // slightly slower than send rate, bouncing off of 0 queue length 192 sendStdDev := stdDev * 20 / 19 193 sendMin := minInterval 194 195 sendCount := 10000 196 if testing.Short() { 197 sendCount = 1000 198 } 199 t.Logf("Sending %v messages at the same average rate as write latency.", sendCount) 200 for i := 0; i < sendCount; i++ { 201 err = wsLink.Send(msg) 202 if err != nil { 203 t.Fatal(err) 204 } 205 msg.ID++ 206 time.Sleep(microSecDelay(sendStdDev, sendMin)) 207 } 208 209 // send much faster briefly, building queue up a bit to be drained on disconnect 210 sendStdDev = stdDev * 4 / 5 211 sendMin = minInterval / 2 212 t.Logf("Sending %v messages faster than the average write latency.", sendCount) 213 for i := 0; i < sendCount; i++ { 214 err = wsLink.Send(msg) 215 if err != nil { 216 t.Fatal(err) 217 } 218 msg.ID++ 219 time.Sleep(microSecDelay(sendStdDev, sendMin)) 220 } 221 222 // NOTE: The following message may be sent by the main write loop in 223 // (*WSLink).outHandler, or in the deferred drain/send of queued messages. 224 // _ = wsLink.Send(msg) 225 226 wsLink.Disconnect() 227 228 // Make like a good connection manager and wait for the wsLink to shutdown. 229 wg.Wait() 230 231 if lastID != int64(msg.ID-1) { 232 t.Errorf("final message %d not sent, last ID is %d", msg.ID-1, lastID) 233 } 234 } 235 236 func TestSendRaw(t *testing.T) { 237 tests := []struct { 238 name string 239 on uint32 240 stopped chan struct{} 241 outChan chan *sendData 242 wantErr error 243 }{{ 244 name: "ok", 245 stopped: make(chan struct{}), 246 outChan: make(chan *sendData, 1), 247 on: 1, 248 }, { 249 name: "client stopped", 250 stopped: make(chan struct{}), 251 outChan: make(chan *sendData, 1), 252 wantErr: ErrPeerDisconnected, 253 }, { 254 name: "client stopped before sending", 255 stopped: func() chan struct{} { 256 ch := make(chan struct{}) 257 close(ch) 258 return ch 259 }(), 260 outChan: make(chan *sendData), 261 on: 1, 262 wantErr: ErrPeerDisconnected, 263 }} 264 for _, test := range tests { 265 link := WSLink{ 266 stopped: test.stopped, 267 on: test.on, 268 outChan: test.outChan, 269 } 270 err := link.SendRaw(nil) 271 if test.wantErr != nil { 272 if !errors.Is(err, test.wantErr) { 273 t.Fatalf("%q: expected error %T but got %T", test.name, test.wantErr, err) 274 } 275 continue 276 } 277 if err != nil { 278 t.Fatalf("%q: unexpected error %v", test.name, err) 279 } 280 } 281 }