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 }