decred.org/dcrdex@v1.0.5/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 return nil 127 } 128 129 msg, _ = msgjson.NewRequest(123, "bad", `stuff`) 130 wsLink.handleMessage(msg) 131 } 132 133 func TestWSLink_send(t *testing.T) { 134 defer os.Stdout.Sync() 135 136 handlerChan := make(chan struct{}, 1) 137 inMsgHandler := func(msg *msgjson.Message) *msgjson.Error { 138 handlerChan <- struct{}{} 139 return nil 140 } 141 142 conn := &ConnStub{ 143 inMsg: make(chan []byte, 1), 144 inErr: make(chan error, 1), 145 } 146 ipk := dex.IPKey{16, 16, 120, 120 /* ipv6 1010:7878:: */} 147 wsLink := NewWSLink(ipk.String(), conn, time.Second, inMsgHandler, tLogger) 148 ctx, cancel := context.WithCancel(context.Background()) 149 defer cancel() 150 151 // start the in/out/pingHandlers, and the initial read deadline 152 wg, err := wsLink.Connect(ctx) 153 if err != nil { 154 t.Fatalf("Connect: %v", err) 155 } 156 157 defer wg.Wait() 158 159 // hit the inHandler once before testing sends 160 msg, _ := msgjson.NewRequest(12, "beep", "boop") 161 b, err := json.Marshal(msg) 162 if err != nil { 163 t.Fatal(err) 164 } 165 // give something for inHandler to read 166 conn.inMsg <- b 167 // ensure that the handler was called 168 select { 169 case <-handlerChan: 170 case <-time.NewTimer(time.Second).C: 171 t.Fatal("in handler not called") 172 } 173 174 // sends 175 stuff := struct { 176 Thing int `json:"thing"` 177 Blah string `json:"stuff"` 178 }{ 179 12, "asdf", 180 } 181 msg, _ = msgjson.NewNotification("blah", stuff) 182 183 err = wsLink.SendNow(msg) 184 if err != nil { 185 t.Error(err) 186 } 187 msg.ID++ // not normally used for ntfns, just in this test 188 189 // slightly slower than send rate, bouncing off of 0 queue length 190 sendStdDev := stdDev * 20 / 19 191 sendMin := minInterval 192 193 sendCount := 10000 194 if testing.Short() { 195 sendCount = 1000 196 } 197 t.Logf("Sending %v messages at the same average rate as write latency.", sendCount) 198 for i := 0; i < sendCount; i++ { 199 err = wsLink.Send(msg) 200 if err != nil { 201 t.Fatal(err) 202 } 203 msg.ID++ 204 time.Sleep(microSecDelay(sendStdDev, sendMin)) 205 } 206 207 // send much faster briefly, building queue up a bit to be drained on disconnect 208 sendStdDev = stdDev * 4 / 5 209 sendMin = minInterval / 2 210 t.Logf("Sending %v messages faster than the average write latency.", sendCount) 211 for i := 0; i < sendCount; i++ { 212 err = wsLink.Send(msg) 213 if err != nil { 214 t.Fatal(err) 215 } 216 msg.ID++ 217 time.Sleep(microSecDelay(sendStdDev, sendMin)) 218 } 219 220 // NOTE: The following message may be sent by the main write loop in 221 // (*WSLink).outHandler, or in the deferred drain/send of queued messages. 222 // _ = wsLink.Send(msg) 223 224 wsLink.Disconnect() 225 226 // Make like a good connection manager and wait for the wsLink to shutdown. 227 wg.Wait() 228 229 if lastID != int64(msg.ID-1) { 230 t.Errorf("final message %d not sent, last ID is %d", msg.ID-1, lastID) 231 } 232 } 233 234 func TestSendRaw(t *testing.T) { 235 tests := []struct { 236 name string 237 on uint32 238 stopped chan struct{} 239 outChan chan *sendData 240 wantErr error 241 }{{ 242 name: "ok", 243 stopped: make(chan struct{}), 244 outChan: make(chan *sendData, 1), 245 on: 1, 246 }, { 247 name: "client stopped", 248 stopped: make(chan struct{}), 249 outChan: make(chan *sendData, 1), 250 wantErr: ErrPeerDisconnected, 251 }, { 252 name: "client stopped before sending", 253 stopped: func() chan struct{} { 254 ch := make(chan struct{}) 255 close(ch) 256 return ch 257 }(), 258 outChan: make(chan *sendData), 259 on: 1, 260 wantErr: ErrPeerDisconnected, 261 }} 262 for _, test := range tests { 263 link := WSLink{ 264 stopped: test.stopped, 265 on: test.on, 266 outChan: test.outChan, 267 } 268 err := link.SendRaw(nil) 269 if test.wantErr != nil { 270 if !errors.Is(err, test.wantErr) { 271 t.Fatalf("%q: expected error %T but got %T", test.name, test.wantErr, err) 272 } 273 continue 274 } 275 if err != nil { 276 t.Fatalf("%q: unexpected error %v", test.name, err) 277 } 278 } 279 }