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