github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/cmd/gosbom/cli/eventloop/event_loop_test.go (about) 1 package eventloop 2 3 import ( 4 "fmt" 5 "os" 6 "syscall" 7 "testing" 8 "time" 9 10 "github.com/nextlinux/gosbom/gosbom/event" 11 "github.com/nextlinux/gosbom/internal/ui" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/mock" 14 "github.com/wagoodman/go-partybus" 15 ) 16 17 var _ ui.UI = (*uiMock)(nil) 18 19 type uiMock struct { 20 t *testing.T 21 finalEvent partybus.Event 22 unsubscribe func() error 23 mock.Mock 24 } 25 26 func (u *uiMock) Setup(unsubscribe func() error) error { 27 u.t.Logf("UI Setup called") 28 u.unsubscribe = unsubscribe 29 return u.Called(unsubscribe).Error(0) 30 } 31 32 func (u *uiMock) Handle(event partybus.Event) error { 33 u.t.Logf("UI Handle called: %+v", event.Type) 34 if event == u.finalEvent { 35 assert.NoError(u.t, u.unsubscribe()) 36 } 37 return u.Called(event).Error(0) 38 } 39 40 func (u *uiMock) Teardown(_ bool) error { 41 u.t.Logf("UI Teardown called") 42 return u.Called().Error(0) 43 } 44 45 func Test_EventLoop_gracefulExit(t *testing.T) { 46 test := func(t *testing.T) { 47 48 testBus := partybus.NewBus() 49 subscription := testBus.Subscribe() 50 t.Cleanup(testBus.Close) 51 52 finalEvent := partybus.Event{ 53 Type: event.Exit, 54 } 55 56 worker := func() <-chan error { 57 ret := make(chan error) 58 go func() { 59 t.Log("worker running") 60 // send an empty item (which is ignored) ensuring we've entered the select statement, 61 // then close (a partial shutdown). 62 ret <- nil 63 t.Log("worker sent nothing") 64 close(ret) 65 t.Log("worker closed") 66 // do the other half of the shutdown 67 testBus.Publish(finalEvent) 68 t.Log("worker published final event") 69 }() 70 return ret 71 } 72 73 signaler := func() <-chan os.Signal { 74 return nil 75 } 76 77 ux := &uiMock{ 78 t: t, 79 finalEvent: finalEvent, 80 } 81 82 // ensure the mock sees at least the final event 83 ux.On("Handle", finalEvent).Return(nil) 84 // ensure the mock sees basic setup/teardown events 85 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 86 ux.On("Teardown").Return(nil) 87 88 var cleanupCalled bool 89 cleanupFn := func() { 90 t.Log("cleanup called") 91 cleanupCalled = true 92 } 93 94 assert.NoError(t, 95 EventLoop( 96 worker(), 97 signaler(), 98 subscription, 99 cleanupFn, 100 ux, 101 ), 102 ) 103 104 assert.True(t, cleanupCalled, "cleanup function not called") 105 ux.AssertExpectations(t) 106 } 107 108 // if there is a bug, then there is a risk of the event loop never returning 109 testWithTimeout(t, 5*time.Second, test) 110 } 111 112 func Test_EventLoop_workerError(t *testing.T) { 113 test := func(t *testing.T) { 114 115 testBus := partybus.NewBus() 116 subscription := testBus.Subscribe() 117 t.Cleanup(testBus.Close) 118 119 workerErr := fmt.Errorf("worker error") 120 121 worker := func() <-chan error { 122 ret := make(chan error) 123 go func() { 124 t.Log("worker running") 125 // send an empty item (which is ignored) ensuring we've entered the select statement, 126 // then close (a partial shutdown). 127 ret <- nil 128 t.Log("worker sent nothing") 129 ret <- workerErr 130 t.Log("worker sent error") 131 close(ret) 132 t.Log("worker closed") 133 // note: NO final event is fired 134 }() 135 return ret 136 } 137 138 signaler := func() <-chan os.Signal { 139 return nil 140 } 141 142 ux := &uiMock{ 143 t: t, 144 } 145 146 // ensure the mock sees basic setup/teardown events 147 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 148 ux.On("Teardown").Return(nil) 149 150 var cleanupCalled bool 151 cleanupFn := func() { 152 t.Log("cleanup called") 153 cleanupCalled = true 154 } 155 156 // ensure we see an error returned 157 assert.ErrorIs(t, 158 EventLoop( 159 worker(), 160 signaler(), 161 subscription, 162 cleanupFn, 163 ux, 164 ), 165 workerErr, 166 "should have seen a worker error, but did not", 167 ) 168 169 assert.True(t, cleanupCalled, "cleanup function not called") 170 ux.AssertExpectations(t) 171 } 172 173 // if there is a bug, then there is a risk of the event loop never returning 174 testWithTimeout(t, 5*time.Second, test) 175 } 176 177 func Test_EventLoop_unsubscribeError(t *testing.T) { 178 test := func(t *testing.T) { 179 180 testBus := partybus.NewBus() 181 subscription := testBus.Subscribe() 182 t.Cleanup(testBus.Close) 183 184 finalEvent := partybus.Event{ 185 Type: event.Exit, 186 } 187 188 worker := func() <-chan error { 189 ret := make(chan error) 190 go func() { 191 t.Log("worker running") 192 // send an empty item (which is ignored) ensuring we've entered the select statement, 193 // then close (a partial shutdown). 194 ret <- nil 195 t.Log("worker sent nothing") 196 close(ret) 197 t.Log("worker closed") 198 // do the other half of the shutdown 199 testBus.Publish(finalEvent) 200 t.Log("worker published final event") 201 }() 202 return ret 203 } 204 205 signaler := func() <-chan os.Signal { 206 return nil 207 } 208 209 ux := &uiMock{ 210 t: t, 211 finalEvent: finalEvent, 212 } 213 214 // ensure the mock sees at least the final event... note the unsubscribe error here 215 ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe) 216 // ensure the mock sees basic setup/teardown events 217 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 218 ux.On("Teardown").Return(nil) 219 220 var cleanupCalled bool 221 cleanupFn := func() { 222 t.Log("cleanup called") 223 cleanupCalled = true 224 } 225 226 // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that 227 // this case is handled as a controlled shutdown (this test should not timeout) 228 assert.NoError(t, 229 EventLoop( 230 worker(), 231 signaler(), 232 subscription, 233 cleanupFn, 234 ux, 235 ), 236 ) 237 238 assert.True(t, cleanupCalled, "cleanup function not called") 239 ux.AssertExpectations(t) 240 } 241 242 // if there is a bug, then there is a risk of the event loop never returning 243 testWithTimeout(t, 5*time.Second, test) 244 } 245 246 func Test_EventLoop_handlerError(t *testing.T) { 247 test := func(t *testing.T) { 248 249 testBus := partybus.NewBus() 250 subscription := testBus.Subscribe() 251 t.Cleanup(testBus.Close) 252 253 finalEvent := partybus.Event{ 254 Type: event.Exit, 255 Error: fmt.Errorf("an exit error occured"), 256 } 257 258 worker := func() <-chan error { 259 ret := make(chan error) 260 go func() { 261 t.Log("worker running") 262 // send an empty item (which is ignored) ensuring we've entered the select statement, 263 // then close (a partial shutdown). 264 ret <- nil 265 t.Log("worker sent nothing") 266 close(ret) 267 t.Log("worker closed") 268 // do the other half of the shutdown 269 testBus.Publish(finalEvent) 270 t.Log("worker published final event") 271 }() 272 return ret 273 } 274 275 signaler := func() <-chan os.Signal { 276 return nil 277 } 278 279 ux := &uiMock{ 280 t: t, 281 finalEvent: finalEvent, 282 } 283 284 // ensure the mock sees at least the final event... note the event error is propagated 285 ux.On("Handle", finalEvent).Return(finalEvent.Error) 286 // ensure the mock sees basic setup/teardown events 287 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 288 ux.On("Teardown").Return(nil) 289 290 var cleanupCalled bool 291 cleanupFn := func() { 292 t.Log("cleanup called") 293 cleanupCalled = true 294 } 295 296 // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is 297 // handled as a controlled shutdown (this test should not timeout) 298 assert.ErrorIs(t, 299 EventLoop( 300 worker(), 301 signaler(), 302 subscription, 303 cleanupFn, 304 ux, 305 ), 306 finalEvent.Error, 307 "should have seen a event error, but did not", 308 ) 309 310 assert.True(t, cleanupCalled, "cleanup function not called") 311 ux.AssertExpectations(t) 312 } 313 314 // if there is a bug, then there is a risk of the event loop never returning 315 testWithTimeout(t, 5*time.Second, test) 316 } 317 318 func Test_EventLoop_signalsStopExecution(t *testing.T) { 319 test := func(t *testing.T) { 320 321 testBus := partybus.NewBus() 322 subscription := testBus.Subscribe() 323 t.Cleanup(testBus.Close) 324 325 worker := func() <-chan error { 326 // the worker will never return work and the event loop will always be waiting... 327 return make(chan error) 328 } 329 330 signaler := func() <-chan os.Signal { 331 ret := make(chan os.Signal) 332 go func() { 333 ret <- syscall.SIGINT 334 // note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit 335 }() 336 return ret 337 } 338 339 ux := &uiMock{ 340 t: t, 341 } 342 343 // ensure the mock sees basic setup/teardown events 344 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 345 ux.On("Teardown").Return(nil) 346 347 var cleanupCalled bool 348 cleanupFn := func() { 349 t.Log("cleanup called") 350 cleanupCalled = true 351 } 352 353 assert.NoError(t, 354 EventLoop( 355 worker(), 356 signaler(), 357 subscription, 358 cleanupFn, 359 ux, 360 ), 361 ) 362 363 assert.True(t, cleanupCalled, "cleanup function not called") 364 ux.AssertExpectations(t) 365 } 366 367 // if there is a bug, then there is a risk of the event loop never returning 368 testWithTimeout(t, 5*time.Second, test) 369 } 370 371 func Test_EventLoop_uiTeardownError(t *testing.T) { 372 test := func(t *testing.T) { 373 374 testBus := partybus.NewBus() 375 subscription := testBus.Subscribe() 376 t.Cleanup(testBus.Close) 377 378 finalEvent := partybus.Event{ 379 Type: event.Exit, 380 } 381 382 worker := func() <-chan error { 383 ret := make(chan error) 384 go func() { 385 t.Log("worker running") 386 // send an empty item (which is ignored) ensuring we've entered the select statement, 387 // then close (a partial shutdown). 388 ret <- nil 389 t.Log("worker sent nothing") 390 close(ret) 391 t.Log("worker closed") 392 // do the other half of the shutdown 393 testBus.Publish(finalEvent) 394 t.Log("worker published final event") 395 }() 396 return ret 397 } 398 399 signaler := func() <-chan os.Signal { 400 return nil 401 } 402 403 ux := &uiMock{ 404 t: t, 405 finalEvent: finalEvent, 406 } 407 408 teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down") 409 410 // ensure the mock sees at least the final event... note the event error is propagated 411 ux.On("Handle", finalEvent).Return(nil) 412 // ensure the mock sees basic setup/teardown events 413 ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) 414 ux.On("Teardown").Return(teardownError) 415 416 var cleanupCalled bool 417 cleanupFn := func() { 418 t.Log("cleanup called") 419 cleanupCalled = true 420 } 421 422 // ensure we see an error returned 423 assert.ErrorIs(t, 424 EventLoop( 425 worker(), 426 signaler(), 427 subscription, 428 cleanupFn, 429 ux, 430 ), 431 teardownError, 432 "should have seen a UI teardown error, but did not", 433 ) 434 435 assert.True(t, cleanupCalled, "cleanup function not called") 436 ux.AssertExpectations(t) 437 } 438 439 // if there is a bug, then there is a risk of the event loop never returning 440 testWithTimeout(t, 5*time.Second, test) 441 } 442 443 func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { 444 done := make(chan bool) 445 go func() { 446 test(t) 447 done <- true 448 }() 449 450 select { 451 case <-time.After(timeout): 452 t.Fatal("test timed out") 453 case <-done: 454 } 455 }