github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/hud/server/websocket_test.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"runtime"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/types"
    15  
    16  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    17  	"github.com/tilt-dev/tilt/internal/store"
    18  	"github.com/tilt-dev/tilt/internal/testutils"
    19  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    20  	"github.com/tilt-dev/tilt/pkg/logger"
    21  )
    22  
    23  func TestWebsocketCloseOnReadErr(t *testing.T) {
    24  	if runtime.GOOS == "windows" {
    25  		t.Skip("TODO(nick): investigate")
    26  	}
    27  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
    28  	st, _ := store.NewStoreWithFakeReducer()
    29  	_ = st.SetUpSubscribersForTesting(ctx)
    30  
    31  	conn := newFakeConn()
    32  	ctrlClient := fake.NewFakeTiltClient()
    33  	ws := NewWebsocketSubscriber(ctx, ctrlClient, st, conn)
    34  	require.NoError(t, st.AddSubscriber(ctx, ws))
    35  
    36  	done := make(chan bool)
    37  	go func() {
    38  		ws.Stream(ctx)
    39  		_ = st.RemoveSubscriber(context.Background(), ws)
    40  		close(done)
    41  	}()
    42  
    43  	conn.AssertNextWriteMsg(t).Ack()
    44  
    45  	writeLogAndNotify(ctx, st)
    46  	conn.AssertNextWriteMsg(t).Ack()
    47  
    48  	writeLogAndNotify(ctx, st)
    49  	conn.AssertNextWriteMsg(t).Ack()
    50  
    51  	conn.readCh <- readerOrErr{err: fmt.Errorf("read error")}
    52  
    53  	conn.AssertClose(t, done)
    54  }
    55  
    56  func TestWebsocketReadErrDuringMsg(t *testing.T) {
    57  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
    58  	st, _ := store.NewStoreWithFakeReducer()
    59  	_ = st.SetUpSubscribersForTesting(ctx)
    60  
    61  	conn := newFakeConn()
    62  	ctrlClient := fake.NewFakeTiltClient()
    63  	ws := NewWebsocketSubscriber(ctx, ctrlClient, st, conn)
    64  	require.NoError(t, st.AddSubscriber(ctx, ws))
    65  
    66  	done := make(chan bool)
    67  	go func() {
    68  		ws.Stream(ctx)
    69  		_ = st.RemoveSubscriber(context.Background(), ws)
    70  		close(done)
    71  	}()
    72  
    73  	conn.AssertNextWriteMsg(t).Ack()
    74  
    75  	writeLogAndNotify(ctx, st)
    76  
    77  	m := conn.AssertNextWriteMsg(t)
    78  
    79  	// Send a read error, and make sure the connection
    80  	// doesn't close immediately.
    81  	conn.readCh <- readerOrErr{err: fmt.Errorf("read error")}
    82  	time.Sleep(10 * time.Millisecond)
    83  	assert.False(t, conn.closed)
    84  
    85  	// Finish the write
    86  	m.Ack()
    87  
    88  	conn.AssertClose(t, done)
    89  }
    90  
    91  func TestWebsocketNextWriterError(t *testing.T) {
    92  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
    93  	st, _ := store.NewStoreWithFakeReducer()
    94  	_ = st.SetUpSubscribersForTesting(ctx)
    95  
    96  	conn := newFakeConn()
    97  	conn.nextWriterError = fmt.Errorf("fake NextWriter error")
    98  	ctrlClient := fake.NewFakeTiltClient()
    99  	ws := NewWebsocketSubscriber(ctx, ctrlClient, st, conn)
   100  	require.NoError(t, st.AddSubscriber(ctx, ws))
   101  
   102  	done := make(chan bool)
   103  	go func() {
   104  		ws.Stream(ctx)
   105  		_ = st.RemoveSubscriber(context.Background(), ws)
   106  		close(done)
   107  	}()
   108  
   109  	writeLogAndNotify(ctx, st)
   110  	time.Sleep(10 * time.Millisecond)
   111  
   112  	conn.readCh <- readerOrErr{err: fmt.Errorf("read error")}
   113  	conn.AssertClose(t, done)
   114  }
   115  
   116  // It's possible to get a ChangeSummary where Log is true but all logs have already been processed,
   117  // in which case ToLogList returns [-1,-1).
   118  // Presumably this happens when:
   119  // 1. store writes logevent A to logstore
   120  // 2. store notifies subscribers with a changesummary indicating there are logs
   121  // 3. store writes logevent B to logstore
   122  // 4. subscriber gets the changesummary from (2) and reads logevents A and B
   123  // 5. store notifies subscribers of logevent B
   124  // 6. subscriber reads logevents, but its checkpoint is already all caught up
   125  // https://github.com/tilt-dev/tilt/issues/4604
   126  func TestWebsocketIgnoreEmptyLogList(t *testing.T) {
   127  	f := newWSFixture(t)
   128  	ctx := f.ctx
   129  	ws := f.ws
   130  	st := f.st
   131  	conn := f.conn
   132  
   133  	done := make(chan bool)
   134  	go func() {
   135  		ws.Stream(ctx)
   136  		_ = st.RemoveSubscriber(context.Background(), ws)
   137  		close(done)
   138  	}()
   139  
   140  	conn.AssertNextWriteMsg(t).Ack()
   141  
   142  	_ = ws.OnChange(ctx, st, store.ChangeSummary{Log: true})
   143  	require.NotEqual(t, -1, ws.clientCheckpoint)
   144  }
   145  
   146  func TestMergeUpdates(t *testing.T) {
   147  	f := newWSFixture(t)
   148  
   149  	f.ws.SendUISessionUpdate(f.ctx,
   150  		&v1alpha1.UISession{ObjectMeta: metav1.ObjectMeta{Name: "sa"}})
   151  	f.ws.SendUISessionUpdate(f.ctx,
   152  		&v1alpha1.UISession{ObjectMeta: metav1.ObjectMeta{Name: "sb"}})
   153  	f.ws.SendUIResourceUpdate(f.ctx, types.NamespacedName{Name: "ra"},
   154  		&v1alpha1.UIResource{ObjectMeta: metav1.ObjectMeta{Name: "ra"}})
   155  	f.ws.SendUIResourceUpdate(f.ctx, types.NamespacedName{Name: "ra"},
   156  		&v1alpha1.UIResource{ObjectMeta: metav1.ObjectMeta{Name: "ra"}})
   157  	f.ws.SendUIResourceUpdate(f.ctx, types.NamespacedName{Name: "rb"}, nil)
   158  	f.ws.SendUIButtonUpdate(f.ctx, types.NamespacedName{Name: "ba"},
   159  		&v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "ba"}})
   160  	f.ws.SendUIButtonUpdate(f.ctx, types.NamespacedName{Name: "ba"},
   161  		&v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "ba"}})
   162  	f.ws.SendUIButtonUpdate(f.ctx, types.NamespacedName{Name: "bb"}, nil)
   163  	f.ws.SendClusterUpdate(f.ctx, types.NamespacedName{Name: "ca"},
   164  		&v1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "ca"}})
   165  	f.ws.SendClusterUpdate(f.ctx, types.NamespacedName{Name: "ca"},
   166  		&v1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "ca"}})
   167  	f.ws.SendClusterUpdate(f.ctx, types.NamespacedName{Name: "cb"}, nil)
   168  
   169  	view := f.ws.toViewUpdate()
   170  	assert.Equal(t, "sb", view.UiSession.ObjectMeta.Name)
   171  	assert.Equal(t, 2, len(view.UiResources))
   172  	assert.Equal(t, 2, len(view.UiButtons))
   173  	assert.Len(t, view.Clusters, 2, "Cluster updates")
   174  
   175  	view2 := f.ws.toViewUpdate()
   176  	assert.Nil(t, view2)
   177  
   178  	f.ws.SendUIButtonUpdate(f.ctx, types.NamespacedName{Name: "bb"}, nil)
   179  	view3 := f.ws.toViewUpdate()
   180  	assert.Nil(t, view3.UiSession)
   181  	assert.Equal(t, 0, len(view3.UiResources))
   182  	assert.Equal(t, 1, len(view3.UiButtons))
   183  
   184  	f.ws.SendClusterUpdate(f.ctx, types.NamespacedName{Name: "cb"}, nil)
   185  	view4 := f.ws.toViewUpdate()
   186  	assert.Len(t, view4.Clusters, 1, "Cluster updates")
   187  }
   188  
   189  type wsFixture struct {
   190  	ws   *WebsocketSubscriber
   191  	ctx  context.Context
   192  	st   *store.Store
   193  	conn *fakeConn
   194  }
   195  
   196  func newWSFixture(t *testing.T) *wsFixture {
   197  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   198  	st, _ := store.NewStoreWithFakeReducer()
   199  	_ = st.SetUpSubscribersForTesting(ctx)
   200  
   201  	conn := newFakeConn()
   202  	ctrlClient := fake.NewFakeTiltClient()
   203  	ws := NewWebsocketSubscriber(ctx, ctrlClient, st, conn)
   204  	require.NoError(t, st.AddSubscriber(ctx, ws))
   205  	return &wsFixture{
   206  		ctx:  ctx,
   207  		st:   st,
   208  		ws:   ws,
   209  		conn: conn,
   210  	}
   211  }
   212  
   213  type readerOrErr struct {
   214  	reader io.Reader
   215  	err    error
   216  }
   217  type fakeConn struct {
   218  	// Write an error to this channel to stop the Read consumer
   219  	readCh chan readerOrErr
   220  
   221  	// Consume messages written to this channel. The caller should Ack() to acknowledge receipt.
   222  	writeCh chan msg
   223  
   224  	closed bool
   225  
   226  	nextWriterError error
   227  }
   228  
   229  func newFakeConn() *fakeConn {
   230  	return &fakeConn{
   231  		readCh:  make(chan readerOrErr),
   232  		writeCh: make(chan msg),
   233  	}
   234  }
   235  
   236  func (c *fakeConn) NextReader() (int, io.Reader, error) {
   237  	next := <-c.readCh
   238  	return 1, next.reader, next.err
   239  }
   240  
   241  func (c *fakeConn) Close() error {
   242  	c.closed = true
   243  	return nil
   244  }
   245  
   246  func (c *fakeConn) newMessageToRead(r io.Reader) {
   247  	c.readCh <- readerOrErr{reader: r}
   248  }
   249  
   250  func (c *fakeConn) AssertNextWriteMsg(t *testing.T) msg {
   251  	select {
   252  	case <-time.After(250 * time.Millisecond):
   253  		t.Fatal("timed out waiting for Writer to Close")
   254  	case msg := <-c.writeCh:
   255  		return msg
   256  	}
   257  	return msg{}
   258  }
   259  
   260  func (c *fakeConn) AssertClose(t *testing.T, done chan bool) {
   261  	t.Helper()
   262  	select {
   263  	case <-time.After(250 * time.Millisecond):
   264  		t.Fatal("timed out waiting for close")
   265  	case <-done:
   266  		assert.True(t, c.closed)
   267  	}
   268  }
   269  
   270  func (c *fakeConn) NextWriter(messagetype int) (io.WriteCloser, error) {
   271  	if c.nextWriterError != nil {
   272  		return nil, c.nextWriterError
   273  	}
   274  	return c.writer(), nil
   275  }
   276  
   277  func (c *fakeConn) writer() io.WriteCloser {
   278  	return &fakeConnWriter{c: c}
   279  }
   280  
   281  type fakeConnWriter struct {
   282  	c *fakeConn
   283  }
   284  
   285  func (f *fakeConnWriter) Write(p []byte) (int, error) {
   286  	return len(p), nil
   287  }
   288  
   289  func (f *fakeConnWriter) Close() error {
   290  	cb := make(chan error)
   291  	f.c.writeCh <- msg{callback: cb}
   292  	return <-cb
   293  }
   294  
   295  type msg struct {
   296  	callback chan error
   297  }
   298  
   299  func (m msg) Ack() {
   300  	m.callback <- nil
   301  	close(m.callback)
   302  }
   303  
   304  func writeLogAndNotify(ctx context.Context, st *store.Store) {
   305  	state := st.LockMutableStateForTesting()
   306  	state.LogStore.Append(store.NewGlobalLogAction(logger.InfoLvl, []byte("test")), nil)
   307  	st.UnlockMutableState()
   308  	st.NotifySubscribers(ctx, store.ChangeSummary{Log: true})
   309  }