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  }