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  }