github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/dockercomposeservice/disable_watcher_test.go (about) 1 package dockercomposeservice 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/jonboulle/clockwork" 12 "github.com/pkg/errors" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 "github.com/tilt-dev/tilt/internal/dockercompose" 17 "github.com/tilt-dev/tilt/internal/testutils/bufsync" 18 "github.com/tilt-dev/tilt/internal/testutils/manifestbuilder" 19 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 20 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 21 "github.com/tilt-dev/tilt/pkg/logger" 22 "github.com/tilt-dev/tilt/pkg/model" 23 ) 24 25 // https://app.shortcut.com/windmill/story/13147/docker-compose-down-messages-for-disabled-resources-may-be-confusing 26 func TestDockerComposeIgnoresGoingToRemoveMessage(t *testing.T) { 27 f := newDWFixture(t) 28 f.dcClient.RmOutput = `Stopping servantes_fortune_1 ... 29 Stopping servantes_fortune_1 ... done 30 servantes_fortune_1 exited with code 137 31 Removing servantes_fortune_1 ... 32 Removing servantes_fortune_1 ... done 33 Going to remove servantes_fortune_1 34 ` 35 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 36 f.clock.BlockUntil(1) 37 f.clock.Advance(20 * disableDebounceDelay) 38 f.startTime = f.clock.Now() 39 40 f.log.AssertEventuallyContains(t, "Stopping servantes", time.Second) 41 expectedOutput := strings.Replace(f.dcClient.RmOutput, "Going to remove servantes_fortune_1\n", "", -1) 42 require.Equal(t, expectedOutput, f.log.String()) 43 } 44 45 func TestDockerComposeDebounce(t *testing.T) { 46 f := newDWFixture(t) 47 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 48 f.updateQueue("m2", v1alpha1.DisableStateEnabled) 49 require.Len(t, f.dcClient.RmCalls(), 0) 50 51 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 52 53 f.clock.BlockUntil(2) 54 f.clock.Advance(20 * disableDebounceDelay) 55 56 call := f.rmCall(1) 57 58 require.Equal(t, []string{"m1", "m2"}, stoppedServices(call)) 59 } 60 61 func TestDockerComposeDontRetryOnSameStartTime(t *testing.T) { 62 f := newDWFixture(t) 63 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 64 f.updateQueue("m2", v1alpha1.DisableStateEnabled) 65 require.Len(t, f.dcClient.RmCalls(), 0) 66 67 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 68 69 f.clock.BlockUntil(2) 70 f.clock.Advance(2 * disableDebounceDelay) 71 72 call := f.rmCall(1) 73 require.Equal(t, []string{"m1", "m2"}, stoppedServices(call)) 74 75 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 76 77 require.Neverf(t, func() bool { 78 return len(f.dcClient.RmCalls()) > 1 79 }, 20*time.Millisecond, time.Millisecond, "docker-compose should not be called again") 80 } 81 82 func TestDockerComposeRetryIfStartTimeChanges(t *testing.T) { 83 f := newDWFixture(t) 84 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 85 f.updateQueue("m2", v1alpha1.DisableStateEnabled) 86 require.Len(t, f.dcClient.RmCalls(), 0) 87 f.clock.BlockUntil(1) 88 89 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 90 f.clock.BlockUntil(2) 91 92 f.clock.Advance(2 * disableDebounceDelay) 93 94 require.Eventually(t, func() bool { 95 return len(f.dcClient.RmCalls()) == 1 96 }, time.Second, 10*time.Millisecond, "docker-compose rm called") 97 98 call := f.rmCall(1) 99 require.Equal(t, []string{"m1", "m2"}, stoppedServices(call)) 100 101 // simulate restarting m2 by bumping its start time 102 f.startTime = f.clock.Now() 103 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 104 105 f.clock.BlockUntil(1) 106 f.clock.Advance(2 * disableDebounceDelay) 107 108 call = f.rmCall(2) 109 require.Equal(t, []string{"m2"}, stoppedServices(call)) 110 } 111 112 func TestDockerComposeDontDisableIfReenabledDuringDebounce(t *testing.T) { 113 f := newDWFixture(t) 114 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 115 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 116 117 f.clock.BlockUntil(2) 118 119 // reenable m2 during debounce 120 f.updateQueue("m2", v1alpha1.DisableStateEnabled) 121 122 f.clock.Advance(2 * disableDebounceDelay) 123 124 call := f.rmCall(1) 125 126 require.Equal(t, []string{"m1"}, stoppedServices(call)) 127 } 128 129 func TestDisableError(t *testing.T) { 130 f := newDWFixture(t) 131 132 f.dcClient.RmError = errors.New("fake dc error") 133 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 134 135 f.clock.BlockUntil(1) 136 f.clock.Advance(2 * disableDebounceDelay) 137 138 require.Eventually(t, func() bool { 139 return strings.Contains(f.log.String(), "fake dc error") 140 }, 20*time.Millisecond, time.Millisecond) 141 } 142 143 // Iterations of this subscriber have spawned goroutines for every update call, so try to 144 // verify it's not doing that. 145 func TestDontSpawnRedundantGoroutines(t *testing.T) { 146 f := newDWFixture(t) 147 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 148 f.updateQueue("m2", v1alpha1.DisableStateDisabled) 149 150 for i := 0; i < 10; i++ { 151 f.updateQueue("m1", v1alpha1.DisableStateDisabled) 152 } 153 154 if !assert.Never(t, func() bool { 155 f.watcher.mu.Lock() 156 defer f.watcher.mu.Unlock() 157 return f.watcher.goroutinesSpawnedForTesting > 1 158 }, 20*time.Millisecond, 1*time.Millisecond) { 159 f.watcher.mu.Lock() 160 defer f.watcher.mu.Unlock() 161 require.Equal(t, 1, f.watcher.goroutinesSpawnedForTesting, "goroutines spawned") 162 } 163 164 f.clock.Advance(20 * disableDebounceDelay) 165 166 call := f.rmCall(1) 167 168 require.Equal(t, []string{"m1", "m2"}, stoppedServices(call)) 169 } 170 171 type dwFixture struct { 172 *tempdir.TempDirFixture 173 t *testing.T 174 ctx context.Context 175 dcClient *dockercompose.FakeDCClient 176 watcher *DisableSubscriber 177 clock clockwork.FakeClock 178 log *bufsync.ThreadSafeBuffer 179 startTime time.Time 180 } 181 182 func newDWFixture(t *testing.T) *dwFixture { 183 log := bufsync.NewThreadSafeBuffer() 184 out := io.MultiWriter(log, os.Stdout) 185 ctx := logger.WithLogger(context.Background(), logger.NewTestLogger(out)) 186 ctx, cancel := context.WithCancel(ctx) 187 t.Cleanup(cancel) 188 dcClient := dockercompose.NewFakeDockerComposeClient(t, ctx) 189 clock := clockwork.NewFakeClock() 190 watcher := NewDisableSubscriber(ctx, dcClient, clock) 191 192 return &dwFixture{ 193 TempDirFixture: tempdir.NewTempDirFixture(t), 194 t: t, 195 ctx: ctx, 196 dcClient: dcClient, 197 watcher: watcher, 198 clock: clock, 199 log: log, 200 startTime: clock.Now(), 201 } 202 } 203 204 func (f *dwFixture) updateQueue(mn model.ManifestName, disableState v1alpha1.DisableState) { 205 m := manifestbuilder.New(f, mn).WithDockerCompose().Build() 206 f.watcher.UpdateQueue(resourceState{ 207 Name: mn.String(), 208 Spec: m.DockerComposeTarget().Spec, 209 NeedsCleanup: disableState == v1alpha1.DisableStateDisabled, 210 StartTime: f.startTime, 211 }) 212 } 213 214 // waits for and returns the {num}th RmCall (1-based) 215 func (f *dwFixture) rmCall(num int) dockercompose.RmCall { 216 require.Eventuallyf(f.t, func() bool { 217 return len(f.dcClient.RmCalls()) >= num 218 }, 20*time.Millisecond, time.Millisecond, "waiting for dc rm call #%d", num) 219 return f.dcClient.RmCalls()[num-1] 220 } 221 222 // returns the names of the services stopped by the given call 223 func stoppedServices(call dockercompose.RmCall) []string { 224 var result []string 225 for _, spec := range call.Specs { 226 result = append(result, spec.Service) 227 } 228 return result 229 }