github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/command/agent/consul/script_test.go (about) 1 package consul 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "os/exec" 8 "testing" 9 "time" 10 11 "github.com/hashicorp/consul/api" 12 "github.com/hashicorp/nomad/helper/testtask" 13 "github.com/hashicorp/nomad/nomad/structs" 14 ) 15 16 func TestMain(m *testing.M) { 17 if !testtask.Run() { 18 os.Exit(m.Run()) 19 } 20 } 21 22 // blockingScriptExec implements ScriptExec by running a subcommand that never 23 // exits. 24 type blockingScriptExec struct { 25 // running is ticked before blocking to allow synchronizing operations 26 running chan struct{} 27 28 // set to true if Exec is called and has exited 29 exited bool 30 } 31 32 func newBlockingScriptExec() *blockingScriptExec { 33 return &blockingScriptExec{running: make(chan struct{})} 34 } 35 36 func (b *blockingScriptExec) Exec(ctx context.Context, _ string, _ []string) ([]byte, int, error) { 37 b.running <- struct{}{} 38 cmd := exec.CommandContext(ctx, testtask.Path(), "sleep", "9000h") 39 err := cmd.Run() 40 code := 0 41 if exitErr, ok := err.(*exec.ExitError); ok { 42 if !exitErr.Success() { 43 code = 1 44 } 45 } 46 b.exited = true 47 return []byte{}, code, err 48 } 49 50 // TestConsulScript_Exec_Cancel asserts cancelling a script check shortcircuits 51 // any running scripts. 52 func TestConsulScript_Exec_Cancel(t *testing.T) { 53 serviceCheck := structs.ServiceCheck{ 54 Name: "sleeper", 55 Interval: time.Hour, 56 Timeout: time.Hour, 57 } 58 exec := newBlockingScriptExec() 59 60 // pass nil for heartbeater as it shouldn't be called 61 check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, nil, testLogger(), nil) 62 handle := check.run() 63 64 // wait until Exec is called 65 <-exec.running 66 67 // cancel now that we're blocked in exec 68 handle.cancel() 69 70 select { 71 case <-handle.wait(): 72 case <-time.After(3 * time.Second): 73 t.Fatalf("timed out waiting for script check to exit") 74 } 75 if !exec.exited { 76 t.Errorf("expected script executor to run and exit but it has not") 77 } 78 } 79 80 type execStatus struct { 81 checkID string 82 output string 83 status string 84 } 85 86 // fakeHeartbeater implements the heartbeater interface to allow mocking out 87 // Consul in script executor tests. 88 type fakeHeartbeater struct { 89 updates chan execStatus 90 } 91 92 func (f *fakeHeartbeater) UpdateTTL(checkID, output, status string) error { 93 f.updates <- execStatus{checkID: checkID, output: output, status: status} 94 return nil 95 } 96 97 func newFakeHeartbeater() *fakeHeartbeater { 98 return &fakeHeartbeater{updates: make(chan execStatus)} 99 } 100 101 // TestConsulScript_Exec_Timeout asserts a script will be killed when the 102 // timeout is reached. 103 func TestConsulScript_Exec_Timeout(t *testing.T) { 104 t.Parallel() // run the slow tests in parallel 105 serviceCheck := structs.ServiceCheck{ 106 Name: "sleeper", 107 Interval: time.Hour, 108 Timeout: time.Second, 109 } 110 exec := newBlockingScriptExec() 111 112 hb := newFakeHeartbeater() 113 check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), nil) 114 handle := check.run() 115 defer handle.cancel() // just-in-case cleanup 116 <-exec.running 117 118 // Check for UpdateTTL call 119 select { 120 case update := <-hb.updates: 121 if update.status != api.HealthCritical { 122 t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update) 123 } 124 case <-time.After(3 * time.Second): 125 t.Fatalf("timed out waiting for script check to exit") 126 } 127 if !exec.exited { 128 t.Errorf("expected script executor to run and exit but it has not") 129 } 130 131 // Cancel and watch for exit 132 handle.cancel() 133 select { 134 case <-handle.wait(): 135 // ok! 136 case update := <-hb.updates: 137 t.Errorf("unexpected UpdateTTL call on exit with status=%q", update) 138 case <-time.After(3 * time.Second): 139 t.Fatalf("timed out waiting for script check to exit") 140 } 141 } 142 143 // sleeperExec sleeps for 100ms but returns successfully to allow testing timeout conditions 144 type sleeperExec struct{} 145 146 func (sleeperExec) Exec(context.Context, string, []string) ([]byte, int, error) { 147 time.Sleep(100 * time.Millisecond) 148 return []byte{}, 0, nil 149 } 150 151 // TestConsulScript_Exec_TimeoutCritical asserts a script will be killed when 152 // the timeout is reached and always set a critical status regardless of what 153 // Exec returns. 154 func TestConsulScript_Exec_TimeoutCritical(t *testing.T) { 155 t.Parallel() // run the slow tests in parallel 156 serviceCheck := structs.ServiceCheck{ 157 Name: "sleeper", 158 Interval: time.Hour, 159 Timeout: time.Nanosecond, 160 } 161 hb := newFakeHeartbeater() 162 check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, sleeperExec{}, hb, testLogger(), nil) 163 handle := check.run() 164 defer handle.cancel() // just-in-case cleanup 165 166 // Check for UpdateTTL call 167 select { 168 case update := <-hb.updates: 169 if update.status != api.HealthCritical { 170 t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update) 171 } 172 if update.output != context.DeadlineExceeded.Error() { 173 t.Errorf("expected output=%q but found: %q", context.DeadlineExceeded.Error(), update.output) 174 } 175 case <-time.After(3 * time.Second): 176 t.Fatalf("timed out waiting for script check to timeout") 177 } 178 } 179 180 // simpleExec is a fake ScriptExecutor that returns whatever is specified. 181 type simpleExec struct { 182 code int 183 err error 184 } 185 186 func (s simpleExec) Exec(context.Context, string, []string) ([]byte, int, error) { 187 return []byte(fmt.Sprintf("code=%d err=%v", s.code, s.err)), s.code, s.err 188 } 189 190 // newSimpleExec creates a new ScriptExecutor that returns the given code and err. 191 func newSimpleExec(code int, err error) simpleExec { 192 return simpleExec{code: code, err: err} 193 } 194 195 // TestConsulScript_Exec_Shutdown asserts a script will be executed once more 196 // when told to shutdown. 197 func TestConsulScript_Exec_Shutdown(t *testing.T) { 198 serviceCheck := structs.ServiceCheck{ 199 Name: "sleeper", 200 Interval: time.Hour, 201 Timeout: 3 * time.Second, 202 } 203 204 hb := newFakeHeartbeater() 205 shutdown := make(chan struct{}) 206 exec := newSimpleExec(0, nil) 207 check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), shutdown) 208 handle := check.run() 209 defer handle.cancel() // just-in-case cleanup 210 211 // Tell scriptCheck to exit 212 close(shutdown) 213 214 select { 215 case update := <-hb.updates: 216 if update.status != api.HealthPassing { 217 t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update) 218 } 219 case <-time.After(3 * time.Second): 220 t.Fatalf("timed out waiting for script check to exit") 221 } 222 223 select { 224 case <-handle.wait(): 225 // ok! 226 case <-time.After(3 * time.Second): 227 t.Fatalf("timed out waiting for script check to exit") 228 } 229 } 230 231 func TestConsulScript_Exec_Codes(t *testing.T) { 232 run := func(code int, err error, expected string) func(t *testing.T) { 233 return func(t *testing.T) { 234 t.Parallel() 235 serviceCheck := structs.ServiceCheck{ 236 Name: "test", 237 Interval: time.Hour, 238 Timeout: 3 * time.Second, 239 } 240 241 hb := newFakeHeartbeater() 242 shutdown := make(chan struct{}) 243 exec := newSimpleExec(code, err) 244 check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), shutdown) 245 handle := check.run() 246 defer handle.cancel() 247 248 select { 249 case update := <-hb.updates: 250 if update.status != expected { 251 t.Errorf("expected %q but received %q", expected, update) 252 } 253 // assert output is being reported 254 expectedOutput := fmt.Sprintf("code=%d err=%v", code, err) 255 if err != nil { 256 expectedOutput = err.Error() 257 } 258 if update.output != expectedOutput { 259 t.Errorf("expected output=%q but found: %q", expectedOutput, update.output) 260 } 261 case <-time.After(3 * time.Second): 262 t.Fatalf("timed out waiting for script check to exec") 263 } 264 } 265 } 266 267 // Test exit codes with errors 268 t.Run("Passing", run(0, nil, api.HealthPassing)) 269 t.Run("Warning", run(1, nil, api.HealthWarning)) 270 t.Run("Critical-2", run(2, nil, api.HealthCritical)) 271 t.Run("Critical-9000", run(9000, nil, api.HealthCritical)) 272 273 // Errors should always cause Critical status 274 err := fmt.Errorf("test error") 275 t.Run("Error-0", run(0, err, api.HealthCritical)) 276 t.Run("Error-1", run(1, err, api.HealthCritical)) 277 t.Run("Error-2", run(2, err, api.HealthCritical)) 278 t.Run("Error-9000", run(9000, err, api.HealthCritical)) 279 }