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  }