github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/subscriber_test.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	"k8s.io/apimachinery/pkg/types"
    13  
    14  	"github.com/tilt-dev/tilt/pkg/logger"
    15  )
    16  
    17  func newCtx() context.Context {
    18  	return logger.WithLogger(context.Background(), logger.NewTestLogger(os.Stderr))
    19  }
    20  
    21  func TestSubscriber(t *testing.T) {
    22  	st, _ := NewStoreWithFakeReducer()
    23  	ctx := newCtx()
    24  	s := newFakeSubscriber()
    25  	require.NoError(t, st.AddSubscriber(ctx, s))
    26  
    27  	st.NotifySubscribers(ctx, ChangeSummary{Legacy: true})
    28  	call := <-s.onChange
    29  	close(call.done)
    30  	assert.True(t, call.summary.Legacy)
    31  }
    32  
    33  func TestSubscriberInterleavedCalls(t *testing.T) {
    34  	st, _ := NewStoreWithFakeReducer()
    35  	ctx := newCtx()
    36  	s := newFakeSubscriber()
    37  	require.NoError(t, st.AddSubscriber(ctx, s))
    38  
    39  	st.NotifySubscribers(ctx, ChangeSummary{})
    40  	call := <-s.onChange
    41  	st.NotifySubscribers(ctx, LegacyChangeSummary())
    42  	st.NotifySubscribers(ctx, LegacyChangeSummary())
    43  	time.Sleep(10 * time.Millisecond)
    44  	close(call.done)
    45  
    46  	call = <-s.onChange
    47  	close(call.done)
    48  
    49  	select {
    50  	case <-s.onChange:
    51  		t.Fatal("Expected no more onChange calls")
    52  	case <-time.After(10 * time.Millisecond):
    53  	}
    54  }
    55  
    56  func TestSubscriberInterleavedCallsSummary(t *testing.T) {
    57  	st, _ := NewStoreWithFakeReducer()
    58  	ctx := newCtx()
    59  	s := newFakeSubscriber()
    60  	require.NoError(t, st.AddSubscriber(ctx, s))
    61  
    62  	nn1 := types.NamespacedName{Name: "spec-1"}
    63  
    64  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn1)})
    65  	call := <-s.onChange
    66  	assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1)})
    67  
    68  	nn2 := types.NamespacedName{Name: "spec-2"}
    69  	nn3 := types.NamespacedName{Name: "spec-3"}
    70  
    71  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn2)})
    72  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn3)})
    73  	time.Sleep(10 * time.Millisecond)
    74  	close(call.done)
    75  
    76  	call = <-s.onChange
    77  	assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn2, nn3)})
    78  	close(call.done)
    79  
    80  	select {
    81  	case <-s.onChange:
    82  		t.Fatal("Expected no more onChange calls")
    83  	case <-time.After(10 * time.Millisecond):
    84  	}
    85  }
    86  
    87  func TestAddSubscriberToAlreadySetUpListCallsSetUp(t *testing.T) {
    88  	st, _ := NewStoreWithFakeReducer()
    89  	ctx := newCtx()
    90  	_ = st.subscribers.SetUp(ctx, st)
    91  
    92  	s := newFakeSubscriber()
    93  	require.NoError(t, st.AddSubscriber(ctx, s))
    94  
    95  	assert.Equal(t, 1, s.setupCount)
    96  }
    97  
    98  func TestAddSubscriberBeforeSetupNoop(t *testing.T) {
    99  	st, _ := NewStoreWithFakeReducer()
   100  	ctx := newCtx()
   101  
   102  	s := newFakeSubscriber()
   103  	require.NoError(t, st.AddSubscriber(ctx, s))
   104  
   105  	// We haven't called SetUp on subscriber list as a whole, so won't call it on a new individual subscriber
   106  	assert.Equal(t, 0, s.setupCount)
   107  }
   108  
   109  func TestRemoveSubscriber(t *testing.T) {
   110  	st, _ := NewStoreWithFakeReducer()
   111  	ctx := newCtx()
   112  	s := newFakeSubscriber()
   113  
   114  	require.NoError(t, st.AddSubscriber(ctx, s))
   115  	st.NotifySubscribers(ctx, ChangeSummary{})
   116  	s.assertOnChangeCount(t, 1)
   117  
   118  	err := st.RemoveSubscriber(ctx, s)
   119  	assert.NoError(t, err)
   120  	st.NotifySubscribers(ctx, LegacyChangeSummary())
   121  	s.assertOnChangeCount(t, 0)
   122  }
   123  
   124  func TestRemoveSubscriberNotFound(t *testing.T) {
   125  	st, _ := NewStoreWithFakeReducer()
   126  	s := newFakeSubscriber()
   127  	ctx := newCtx()
   128  	err := st.RemoveSubscriber(ctx, s)
   129  	if assert.Error(t, err) {
   130  		assert.Contains(t, err.Error(), "Subscriber not found")
   131  	}
   132  }
   133  
   134  func TestSubscriberSetup(t *testing.T) {
   135  	st, _ := NewStoreWithFakeReducer()
   136  	ctx := newCtx()
   137  	s := newFakeSubscriber()
   138  	require.NoError(t, st.AddSubscriber(ctx, s))
   139  
   140  	_ = st.subscribers.SetUp(ctx, st)
   141  
   142  	assert.Equal(t, 1, s.setupCount)
   143  }
   144  
   145  func TestSubscriberTeardown(t *testing.T) {
   146  	st, _ := NewStoreWithFakeReducer()
   147  	ctx := newCtx()
   148  	s := newFakeSubscriber()
   149  	require.NoError(t, st.AddSubscriber(ctx, s))
   150  
   151  	go st.Dispatch(NewErrorAction(context.Canceled))
   152  	err := st.Loop(ctx)
   153  	if assert.Error(t, err) {
   154  		assert.Contains(t, err.Error(), "context canceled")
   155  	}
   156  
   157  	assert.Equal(t, 1, s.teardownCount)
   158  }
   159  
   160  func TestSubscriberTeardownOnRemove(t *testing.T) {
   161  	st, _ := NewStoreWithFakeReducer()
   162  	ctx := newCtx()
   163  	s := newFakeSubscriber()
   164  	require.NoError(t, st.AddSubscriber(ctx, s))
   165  
   166  	errChan := make(chan error)
   167  	go func() {
   168  		err := st.Loop(ctx)
   169  		errChan <- err
   170  	}()
   171  
   172  	// Make sure the loop has started.
   173  	st.NotifySubscribers(ctx, ChangeSummary{})
   174  	s.assertOnChangeCount(t, 1)
   175  
   176  	// Remove the subscriber and make sure it doesn't get a change.
   177  	_ = st.RemoveSubscriber(ctx, s)
   178  	st.NotifySubscribers(ctx, ChangeSummary{})
   179  	s.assertOnChangeCount(t, 0)
   180  
   181  	assert.Equal(t, 1, s.teardownCount)
   182  
   183  	st.Dispatch(NewErrorAction(context.Canceled))
   184  
   185  	err := <-errChan
   186  	if assert.Error(t, err) {
   187  		assert.Contains(t, err.Error(), "context canceled")
   188  	}
   189  	assert.Equal(t, 1, s.teardownCount)
   190  }
   191  
   192  type blockingSleeper struct {
   193  	SleepDur chan time.Duration
   194  }
   195  
   196  func newBlockingSleeper() blockingSleeper {
   197  	return blockingSleeper{SleepDur: make(chan time.Duration)}
   198  }
   199  
   200  func (s blockingSleeper) Sleep(ctx context.Context, d time.Duration) {
   201  	if d == actionBatchWindow {
   202  		return
   203  	}
   204  	s.SleepDur <- d
   205  }
   206  
   207  func TestSubscriberBackoff(t *testing.T) {
   208  	bs := newBlockingSleeper()
   209  	st, _ := NewStoreWithFakeReducer()
   210  	st.sleeper = bs
   211  
   212  	ctx := newCtx()
   213  	s := newFakeSubscriber()
   214  	require.NoError(t, st.AddSubscriber(ctx, s))
   215  
   216  	// Fire a change that fails
   217  	nn1 := types.NamespacedName{Name: "spec-1"}
   218  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn1)})
   219  
   220  	call := <-s.onChange
   221  	assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1)})
   222  	call.done <- fmt.Errorf("failed")
   223  
   224  	// Fire a second change while the first change is sleeping
   225  	nn2 := types.NamespacedName{Name: "spec-2"}
   226  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn2)})
   227  	time.Sleep(10 * time.Millisecond)
   228  
   229  	// Clear the sleeper, and process the retry.
   230  	assert.Equal(t, time.Second, <-bs.SleepDur)
   231  
   232  	call = <-s.onChange
   233  	assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1, nn2), LastBackoff: time.Second})
   234  	call.done <- fmt.Errorf("failed")
   235  
   236  	// Fire a third change while the retry is sleeping
   237  	nn3 := types.NamespacedName{Name: "spec-3"}
   238  	st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn3)})
   239  	time.Sleep(10 * time.Millisecond)
   240  
   241  	// Clear the sleeper, and process the second retry.
   242  	assert.Equal(t, 2*time.Second, <-bs.SleepDur)
   243  
   244  	call = <-s.onChange
   245  	assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1, nn2, nn3), LastBackoff: 2 * time.Second})
   246  	close(call.done)
   247  }
   248  
   249  type subscriberWithPointerReceiver struct {
   250  }
   251  
   252  func (s *subscriberWithPointerReceiver) OnChange(ctx context.Context, st RStore, summary ChangeSummary) error {
   253  	return nil
   254  }
   255  
   256  type subscriberWithNonPointerReceiver struct {
   257  }
   258  
   259  func (s subscriberWithNonPointerReceiver) OnChange(ctx context.Context, st RStore, summary ChangeSummary) error {
   260  	return nil
   261  }
   262  
   263  func TestSubscriberName(t *testing.T) {
   264  	require.Equal(t, "store.subscriberWithPointerReceiver", subscriberName(&subscriberWithPointerReceiver{}))
   265  	require.Equal(t, "store.subscriberWithNonPointerReceiver", subscriberName(subscriberWithNonPointerReceiver{}))
   266  }