github.com/decred/dcrlnd@v0.7.6/healthcheck/healthcheck_test.go (about) 1 package healthcheck 2 3 import ( 4 "errors" 5 "testing" 6 "time" 7 8 "github.com/decred/dcrlnd/ticker" 9 "github.com/stretchr/testify/require" 10 ) 11 12 var ( 13 errNonNil = errors.New("non-nil test error") 14 timeout = time.Second 15 testTime = time.Unix(1, 2) 16 ) 17 18 type mockedCheck struct { 19 t *testing.T 20 errChan chan error 21 } 22 23 // newMockCheck creates a new mock. 24 func newMockCheck(t *testing.T) *mockedCheck { 25 return &mockedCheck{ 26 t: t, 27 errChan: make(chan error), 28 } 29 } 30 31 // call returns our mock's error channel, which we can send responses on. 32 func (m *mockedCheck) call() chan error { 33 return m.errChan 34 } 35 36 // sendError sends an error into our mock's error channel, mocking the sending 37 // of a response from our check function. 38 func (m *mockedCheck) sendError(err error) { 39 select { 40 case m.errChan <- err: 41 case <-time.After(timeout): 42 m.t.Fatalf("could not send error: %v", err) 43 } 44 } 45 46 // TestMonitor tests creation and triggering of a monitor with a health check. 47 func TestMonitor(t *testing.T) { 48 intervalTicker := ticker.NewForce(time.Hour) 49 50 mock := newMockCheck(t) 51 shutdown := make(chan struct{}) 52 53 // Create our config for monitoring. We will use a 0 back off so that 54 // out test does not need to wait. 55 cfg := &Config{ 56 Checks: []*Observation{ 57 { 58 Check: mock.call, 59 Interval: intervalTicker, 60 Attempts: 2, 61 Backoff: 0, 62 Timeout: time.Hour, 63 }, 64 }, 65 Shutdown: func(string, ...interface{}) { 66 shutdown <- struct{}{} 67 }, 68 } 69 monitor := NewMonitor(cfg) 70 71 require.NoError(t, monitor.Start(), "could not start monitor") 72 73 // Tick is a helper we will use to tick our interval. 74 tick := func() { 75 select { 76 case intervalTicker.Force <- testTime: 77 case <-time.After(timeout): 78 t.Fatal("could not tick timer") 79 } 80 } 81 82 // Tick our timer and provide our error channel with a nil error. This 83 // mocks our check function succeeding on the first call. 84 tick() 85 mock.sendError(nil) 86 87 // Now we tick our timer again. This time send a non-nil error, followed 88 // by a nil error. This tests our retry logic, because we allow 2 89 // retries, so should recover without needing to shutdown. 90 tick() 91 mock.sendError(errNonNil) 92 mock.sendError(nil) 93 94 // Finally, we tick our timer once more, and send two non-nil errors 95 // into our error channel. This mocks our check function failing twice. 96 tick() 97 mock.sendError(errNonNil) 98 mock.sendError(errNonNil) 99 100 // Since we have failed within our allowed number of retries, we now 101 // expect a call to our shutdown function. 102 select { 103 case <-shutdown: 104 case <-time.After(timeout): 105 t.Fatal("expected shutdown") 106 } 107 108 require.NoError(t, monitor.Stop(), "could not stop monitor") 109 } 110 111 // TestRetryCheck tests our retry logic. It does not include a test for exiting 112 // during the back off period. 113 func TestRetryCheck(t *testing.T) { 114 tests := []struct { 115 name string 116 117 // errors provides an in-order list of errors that we expect our 118 // health check to respond with. The number of errors in this 119 // list indicates the number of times we expect our check to 120 // be called, because our test will fail if we do not consume 121 // every error. 122 errors []error 123 124 // attempts is the number of times we call a check before 125 // failing. 126 attempts int 127 128 // timeout is the time we allow our check to take before we 129 // fail them. 130 timeout time.Duration 131 132 // expectedShutdown is true if we expect a shutdown to be 133 // triggered because all of our calls failed. 134 expectedShutdown bool 135 136 // maxAttemptsReached specifies whether the max allowed 137 // attempts are reached from calling retryCheck. 138 maxAttemptsReached bool 139 }{ 140 { 141 name: "first call succeeds", 142 errors: []error{nil}, 143 attempts: 2, 144 timeout: time.Hour, 145 expectedShutdown: false, 146 maxAttemptsReached: false, 147 }, 148 { 149 name: "first call fails", 150 errors: []error{errNonNil}, 151 attempts: 1, 152 timeout: time.Hour, 153 expectedShutdown: true, 154 maxAttemptsReached: true, 155 }, 156 { 157 name: "fail then recover", 158 errors: []error{errNonNil, nil}, 159 attempts: 2, 160 timeout: time.Hour, 161 expectedShutdown: false, 162 maxAttemptsReached: false, 163 }, 164 { 165 name: "always fail", 166 errors: []error{errNonNil, errNonNil}, 167 attempts: 2, 168 timeout: time.Hour, 169 expectedShutdown: true, 170 maxAttemptsReached: true, 171 }, 172 { 173 name: "no calls", 174 errors: nil, 175 attempts: 0, 176 timeout: time.Hour, 177 expectedShutdown: false, 178 maxAttemptsReached: false, 179 }, 180 { 181 name: "call times out", 182 errors: nil, 183 attempts: 1, 184 timeout: 1, 185 expectedShutdown: true, 186 maxAttemptsReached: true, 187 }, 188 } 189 190 for _, test := range tests { 191 test := test 192 193 t.Run(test.name, func(t *testing.T) { 194 var shutdown bool 195 shutdownFunc := func(string, ...interface{}) { 196 shutdown = true 197 } 198 199 mock := newMockCheck(t) 200 201 // Create an observation that calls our call counting 202 // function. We set a zero back off so that the test 203 // will not wait. 204 observation := &Observation{ 205 Check: mock.call, 206 Attempts: test.attempts, 207 Timeout: test.timeout, 208 Backoff: 0, 209 } 210 quit := make(chan struct{}) 211 212 // Run our retry check in a goroutine because it blocks 213 // on us sending errors into the mocked caller's error 214 // channel. 215 done := make(chan struct{}) 216 retryResult := false 217 go func() { 218 retryResult = observation.retryCheck( 219 quit, shutdownFunc, 220 ) 221 close(done) 222 }() 223 224 // Prompt our mock caller to send responses for calls 225 // to our call function. 226 for _, err := range test.errors { 227 mock.sendError(err) 228 } 229 230 // Make sure that we have finished running our retry 231 // check function before we start checking results. 232 <-done 233 234 require.Equal(t, test.maxAttemptsReached, retryResult, 235 "retryCheck returned unexpected error") 236 require.Equal(t, test.expectedShutdown, shutdown, 237 "unexpected shutdown state") 238 }) 239 } 240 }