github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/service/api_task_test.go (about) 1 package service 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "github.com/evergreen-ci/evergreen" 15 "github.com/evergreen-ci/evergreen/apimodels" 16 "github.com/evergreen-ci/evergreen/db" 17 "github.com/evergreen-ci/evergreen/model" 18 "github.com/evergreen-ci/evergreen/model/build" 19 "github.com/evergreen-ci/evergreen/model/distro" 20 "github.com/evergreen-ci/evergreen/model/host" 21 "github.com/evergreen-ci/evergreen/model/task" 22 modelUtil "github.com/evergreen-ci/evergreen/model/testutil" 23 "github.com/evergreen-ci/evergreen/model/version" 24 "github.com/evergreen-ci/evergreen/taskrunner" 25 "github.com/evergreen-ci/evergreen/testutil" 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 var ( 30 hostSecret = "secret" 31 taskSecret = "tasksecret" 32 ) 33 34 func insertHostWithRunningTask(hostId, taskId string) (host.Host, error) { 35 h := host.Host{ 36 Id: hostId, 37 RunningTask: taskId, 38 } 39 return h, h.Insert() 40 } 41 42 func getNextTaskEndpoint(t *testing.T, as *APIServer, hostId string) *httptest.ResponseRecorder { 43 if err := os.MkdirAll(filepath.Join(evergreen.FindEvergreenHome(), evergreen.ClientDirectory), 0644); err != nil { 44 t.Fatal("could not create client directory required to start the API server:", err.Error()) 45 } 46 47 handler, err := as.Handler() 48 if err != nil { 49 t.Fatalf("creating test API handler: %v", err) 50 } 51 url := "/api/2/agent/next_task" 52 53 request, err := http.NewRequest("GET", url, nil) 54 if err != nil { 55 t.Fatalf("building request: %v", err) 56 } 57 request.Header.Add(evergreen.HostHeader, hostId) 58 request.Header.Add(evergreen.HostSecretHeader, hostSecret) 59 60 w := httptest.NewRecorder() 61 handler.ServeHTTP(w, request) 62 return w 63 } 64 65 func getEndTaskEndpoint(t *testing.T, as *APIServer, hostId, taskId string, details *apimodels.TaskEndDetail) *httptest.ResponseRecorder { 66 if err := os.MkdirAll(filepath.Join(evergreen.FindEvergreenHome(), evergreen.ClientDirectory), 0644); err != nil { 67 t.Fatal("could not create client directory required to start the API server:", err.Error()) 68 } 69 70 handler, err := as.Handler() 71 if err != nil { 72 t.Fatalf("creating test API handler: %v", err) 73 } 74 url := fmt.Sprintf("/api/2/task/%v/new_end", taskId) 75 76 request, err := http.NewRequest("POST", url, nil) 77 if err != nil { 78 t.Fatalf("building request: %v", err) 79 } 80 request.Header.Add(evergreen.HostHeader, hostId) 81 request.Header.Add(evergreen.HostSecretHeader, hostSecret) 82 request.Header.Add(evergreen.TaskSecretHeader, taskSecret) 83 84 jsonBytes, err := json.Marshal(*details) 85 testutil.HandleTestingErr(err, t, "error marshalling json") 86 request.Body = ioutil.NopCloser(bytes.NewReader(jsonBytes)) 87 88 w := httptest.NewRecorder() 89 handler.ServeHTTP(w, request) 90 return w 91 } 92 93 func TestAssignNextAvailableTask(t *testing.T) { 94 Convey("with a task queue and a host", t, func() { 95 if err := db.ClearCollections(host.Collection, task.Collection, model.TaskQueuesCollection); err != nil { 96 t.Fatalf("clearing db: %v", err) 97 } 98 if err := modelUtil.AddTestIndexes(host.Collection, true, true, host.RunningTaskKey); err != nil { 99 t.Fatalf("adding test indexes %v", err) 100 } 101 distroId := "testDistro" 102 103 tq := &model.TaskQueue{ 104 Distro: distroId, 105 Queue: []model.TaskQueueItem{ 106 {Id: "task1"}, 107 {Id: "task2"}, 108 }, 109 } 110 So(tq.Save(), ShouldBeNil) 111 sampleHost := host.Host{ 112 Id: "h1", 113 Distro: distro.Distro{ 114 Id: distroId, 115 }, 116 Secret: hostSecret, 117 } 118 So(sampleHost.Insert(), ShouldBeNil) 119 120 task1 := task.Task{ 121 Id: "task1", 122 Status: evergreen.TaskUndispatched, 123 Activated: true, 124 } 125 So(task1.Insert(), ShouldBeNil) 126 127 task2 := task.Task{ 128 Id: "task2", 129 Status: evergreen.TaskUndispatched, 130 Activated: true, 131 } 132 So(task2.Insert(), ShouldBeNil) 133 Convey("a host should get the task at the top of the queue", func() { 134 t, err := assignNextAvailableTask(tq, &sampleHost) 135 So(err, ShouldBeNil) 136 So(t, ShouldNotBeNil) 137 So(t.Id, ShouldEqual, "task1") 138 139 currentTq, err := model.FindTaskQueueForDistro(distroId) 140 So(err, ShouldBeNil) 141 So(len(currentTq.Queue), ShouldEqual, 1) 142 143 h, err := host.FindOne(host.ById(sampleHost.Id)) 144 So(err, ShouldBeNil) 145 So(h.RunningTask, ShouldEqual, "task1") 146 147 Convey("a task that is not undispatched should not be updated in the host", func() { 148 tq.Queue = []model.TaskQueueItem{ 149 {Id: "undispatchedTask"}, 150 {Id: "task2"}, 151 } 152 So(tq.Save(), ShouldBeNil) 153 undispatchedTask := task.Task{ 154 Id: "undispatchedTask", 155 Status: evergreen.TaskStarted, 156 } 157 So(undispatchedTask.Insert(), ShouldBeNil) 158 t, err := assignNextAvailableTask(tq, &sampleHost) 159 So(err, ShouldBeNil) 160 So(t.Id, ShouldEqual, "task2") 161 162 currentTq, err := model.FindTaskQueueForDistro(distroId) 163 So(err, ShouldBeNil) 164 So(len(currentTq.Queue), ShouldEqual, 0) 165 }) 166 Convey("an empty task queue should return a nil task", func() { 167 tq.Queue = []model.TaskQueueItem{} 168 So(tq.Save(), ShouldBeNil) 169 t, err := assignNextAvailableTask(tq, &sampleHost) 170 So(err, ShouldBeNil) 171 So(t, ShouldBeNil) 172 }) 173 Convey("a tasks queue with a task that does not exist should error", func() { 174 tq.Queue = []model.TaskQueueItem{{Id: "notatask"}} 175 So(tq.Save(), ShouldBeNil) 176 _, err := assignNextAvailableTask(tq, h) 177 So(err, ShouldNotBeNil) 178 }) 179 Convey("with a host with a running task", func() { 180 anotherHost := host.Host{ 181 Id: "ahost", 182 RunningTask: "sampleTask", 183 Distro: distro.Distro{ 184 Id: distroId, 185 }, 186 Secret: hostSecret, 187 } 188 So(anotherHost.Insert(), ShouldBeNil) 189 h2 := host.Host{ 190 Id: "host2", 191 Distro: distro.Distro{ 192 Id: distroId, 193 }, 194 Secret: hostSecret, 195 } 196 So(h2.Insert(), ShouldBeNil) 197 198 t1 := task.Task{ 199 Id: "sampleTask", 200 Status: evergreen.TaskUndispatched, 201 Activated: true, 202 } 203 So(t1.Insert(), ShouldBeNil) 204 t2 := task.Task{ 205 Id: "another", 206 Status: evergreen.TaskUndispatched, 207 Activated: true, 208 } 209 So(t2.Insert(), ShouldBeNil) 210 211 tq.Queue = []model.TaskQueueItem{ 212 {Id: t1.Id}, 213 {Id: t2.Id}, 214 } 215 So(tq.Save(), ShouldBeNil) 216 Convey("the task that is in the other host should not be assigned to another host", func() { 217 t, err := assignNextAvailableTask(tq, &h2) 218 So(err, ShouldBeNil) 219 So(t.Id, ShouldEqual, t2.Id) 220 h, err := host.FindOne(host.ById(h2.Id)) 221 So(err, ShouldBeNil) 222 So(h.RunningTask, ShouldEqual, t2.Id) 223 }) 224 Convey("a host with a running task should return an error", func() { 225 _, err := assignNextAvailableTask(tq, &anotherHost) 226 So(err, ShouldNotBeNil) 227 }) 228 229 }) 230 }) 231 232 }) 233 } 234 235 func TestNextTask(t *testing.T) { 236 Convey("with tasks, a host, a build, and a task queue", t, func() { 237 if err := db.ClearCollections(host.Collection, task.Collection, model.TaskQueuesCollection, build.Collection); err != nil { 238 t.Fatalf("clearing db: %v", err) 239 } 240 if err := modelUtil.AddTestIndexes(host.Collection, true, true, host.RunningTaskKey); err != nil { 241 t.Fatalf("adding test indexes %v", err) 242 } 243 244 as, err := NewAPIServer(testutil.TestConfig(), nil) 245 if err != nil { 246 t.Fatalf("creating test API server: %v", err) 247 } 248 taskRunnerInstance := taskrunner.NewTaskRunner(&as.Settings) 249 agentRevision, err := taskRunnerInstance.HostGateway.GetAgentRevision() 250 So(err, ShouldBeNil) 251 252 distroId := "testDistro" 253 buildId := "buildId" 254 255 tq := &model.TaskQueue{ 256 Distro: distroId, 257 Queue: []model.TaskQueueItem{ 258 {Id: "task1"}, 259 {Id: "task2"}, 260 }, 261 } 262 So(tq.Save(), ShouldBeNil) 263 sampleHost := host.Host{ 264 Id: "h1", 265 Distro: distro.Distro{ 266 Id: distroId, 267 }, 268 Secret: hostSecret, 269 Status: evergreen.HostRunning, 270 AgentRevision: agentRevision, 271 } 272 So(sampleHost.Insert(), ShouldBeNil) 273 274 task1 := task.Task{ 275 Id: "task1", 276 Status: evergreen.TaskUndispatched, 277 Activated: true, 278 BuildId: buildId, 279 } 280 So(task1.Insert(), ShouldBeNil) 281 282 task2 := task.Task{ 283 Id: "task2", 284 Status: evergreen.TaskUndispatched, 285 Activated: true, 286 BuildId: buildId, 287 } 288 So(task2.Insert(), ShouldBeNil) 289 290 testBuild := build.Build{ 291 Id: buildId, 292 Tasks: []build.TaskCache{ 293 {Id: "task1"}, 294 {Id: "task2"}, 295 }, 296 } 297 So(testBuild.Insert(), ShouldBeNil) 298 Convey("getting the next task api endpoint should work", func() { 299 resp := getNextTaskEndpoint(t, as, sampleHost.Id) 300 So(resp, ShouldNotBeNil) 301 Convey("should return an http status ok", func() { 302 So(resp.Code, ShouldEqual, http.StatusOK) 303 Convey("and a task with id task1", func() { 304 taskResp := apimodels.NextTaskResponse{} 305 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 306 So(taskResp.TaskId, ShouldEqual, "task1") 307 nextTask, err := task.FindOne(task.ById(taskResp.TaskId)) 308 So(err, ShouldBeNil) 309 So(nextTask.Status, ShouldEqual, evergreen.TaskDispatched) 310 }) 311 }) 312 Convey("with a host that already has a running task", func() { 313 h2 := host.Host{ 314 Id: "anotherHost", 315 Secret: hostSecret, 316 RunningTask: "existingTask", 317 AgentRevision: agentRevision, 318 Status: evergreen.HostRunning, 319 } 320 So(h2.Insert(), ShouldBeNil) 321 322 existingTask := task.Task{ 323 Id: "existingTask", 324 Status: evergreen.TaskDispatched, 325 Activated: true, 326 } 327 So(existingTask.Insert(), ShouldBeNil) 328 Convey("getting the next task should return the existing task", func() { 329 resp := getNextTaskEndpoint(t, as, h2.Id) 330 So(resp, ShouldNotBeNil) 331 Convey("should return http status ok", func() { 332 So(resp.Code, ShouldEqual, http.StatusOK) 333 Convey("and a task with the existing task id", func() { 334 taskResp := apimodels.NextTaskResponse{} 335 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 336 So(taskResp.TaskId, ShouldEqual, "existingTask") 337 nextTask, err := task.FindOne(task.ById(taskResp.TaskId)) 338 So(err, ShouldBeNil) 339 So(nextTask.Status, ShouldEqual, evergreen.TaskDispatched) 340 }) 341 }) 342 }) 343 Convey("with an undispatched task but a host that has that task as running task", func() { 344 t1 := task.Task{ 345 Id: "t1", 346 Status: evergreen.TaskUndispatched, 347 Activated: true, 348 BuildId: "anotherBuild", 349 } 350 So(t1.Insert(), ShouldBeNil) 351 anotherHost := host.Host{ 352 Id: "sampleHost", 353 Secret: hostSecret, 354 RunningTask: t1.Id, 355 AgentRevision: agentRevision, 356 Status: evergreen.HostRunning, 357 } 358 anotherBuild := build.Build{ 359 Id: "anotherBuild", 360 Tasks: []build.TaskCache{ 361 {Id: t1.Id}, 362 }, 363 } 364 365 So(anotherBuild.Insert(), ShouldBeNil) 366 So(anotherHost.Insert(), ShouldBeNil) 367 Convey("t1 should be returned and should be set to dispatched", func() { 368 resp := getNextTaskEndpoint(t, as, anotherHost.Id) 369 So(resp, ShouldNotBeNil) 370 Convey("should return http status ok", func() { 371 So(resp.Code, ShouldEqual, http.StatusOK) 372 Convey("task should exist with the existing task id and be dispatched", func() { 373 taskResp := apimodels.NextTaskResponse{} 374 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 375 So(taskResp.TaskId, ShouldEqual, t1.Id) 376 nextTask, err := task.FindOne(task.ById(taskResp.TaskId)) 377 So(err, ShouldBeNil) 378 So(nextTask.Status, ShouldEqual, evergreen.TaskDispatched) 379 }) 380 }) 381 }) 382 Convey("an inactive task should not be dispatched even if it's assigned to a host", func() { 383 inactiveTask := task.Task{ 384 Id: "t2", 385 Status: evergreen.TaskUndispatched, 386 Activated: false, 387 BuildId: "anotherBuild", 388 } 389 So(inactiveTask.Insert(), ShouldBeNil) 390 h3 := host.Host{ 391 Id: "inactive", 392 Secret: hostSecret, 393 RunningTask: inactiveTask.Id, 394 Status: evergreen.HostRunning, 395 AgentRevision: agentRevision, 396 } 397 So(h3.Insert(), ShouldBeNil) 398 anotherBuild := build.Build{ 399 Id: "b", 400 Tasks: []build.TaskCache{ 401 {Id: inactiveTask.Id}, 402 }, 403 } 404 So(anotherBuild.Insert(), ShouldBeNil) 405 Convey("the inactive task should not be returned and the host running task should be unset", func() { 406 resp := getNextTaskEndpoint(t, as, h3.Id) 407 So(resp, ShouldNotBeNil) 408 Convey("should return http status ok", func() { 409 So(resp.Code, ShouldEqual, http.StatusOK) 410 Convey("task should exist with the existing task id and be dispatched", func() { 411 taskResp := apimodels.NextTaskResponse{} 412 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 413 So(taskResp.TaskId, ShouldEqual, "") 414 h, err := host.FindOne(host.ById(h3.Id)) 415 So(err, ShouldBeNil) 416 So(h.RunningTask, ShouldEqual, "") 417 }) 418 }) 419 }) 420 }) 421 }) 422 }) 423 }) 424 }) 425 } 426 427 func TestValidateTaskEndDetails(t *testing.T) { 428 Convey("With a set of end details with different statuses", t, func() { 429 details := apimodels.TaskEndDetail{} 430 details.Status = evergreen.TaskUndispatched 431 So(validateTaskEndDetails(&details), ShouldBeTrue) 432 details.Status = evergreen.TaskDispatched 433 So(validateTaskEndDetails(&details), ShouldBeFalse) 434 details.Status = evergreen.TaskFailed 435 So(validateTaskEndDetails(&details), ShouldBeTrue) 436 details.Status = evergreen.TaskSucceeded 437 So(validateTaskEndDetails(&details), ShouldBeTrue) 438 }) 439 } 440 441 func TestCheckHostHealth(t *testing.T) { 442 currentRevision := "abc" 443 Convey("With a host that has different statuses", t, func() { 444 h := &host.Host{ 445 Status: evergreen.HostRunning, 446 AgentRevision: currentRevision, 447 } 448 shouldExit, _ := checkHostHealth(h, currentRevision) 449 So(shouldExit, ShouldBeFalse) 450 h.Status = evergreen.HostDecommissioned 451 shouldExit, _ = checkHostHealth(h, currentRevision) 452 So(shouldExit, ShouldBeTrue) 453 h.Status = evergreen.HostQuarantined 454 shouldExit, _ = checkHostHealth(h, currentRevision) 455 So(shouldExit, ShouldBeTrue) 456 Convey("With a host that is running but has a different revision", func() { 457 shouldExit, _ := checkHostHealth(h, "bcd") 458 So(shouldExit, ShouldBeTrue) 459 }) 460 }) 461 } 462 463 func TestEndTaskEndpoint(t *testing.T) { 464 Convey("with tasks, a host, a build, and a task queue", t, func() { 465 if err := db.ClearCollections(host.Collection, task.Collection, model.TaskQueuesCollection, 466 build.Collection, model.ProjectRefCollection, version.Collection); err != nil { 467 t.Fatalf("clearing db: %v", err) 468 } 469 as, err := NewAPIServer(testutil.TestConfig(), nil) 470 if err != nil { 471 t.Fatalf("creating test API server: %v", err) 472 } 473 taskRunnerInstance := taskrunner.NewTaskRunner(&as.Settings) 474 agentRevision, err := taskRunnerInstance.HostGateway.GetAgentRevision() 475 So(err, ShouldBeNil) 476 477 if err := modelUtil.AddTestIndexes(host.Collection, true, true, host.RunningTaskKey); err != nil { 478 t.Fatalf("adding test indexes %v", err) 479 } 480 481 hostId := "h1" 482 projectId := "proj" 483 buildId := "b1" 484 versionId := "v1" 485 486 proj := model.ProjectRef{ 487 Identifier: projectId, 488 } 489 So(proj.Insert(), ShouldBeNil) 490 491 task1 := task.Task{ 492 Id: "task1", 493 Status: evergreen.TaskStarted, 494 Activated: true, 495 HostId: hostId, 496 Secret: taskSecret, 497 Project: projectId, 498 BuildId: buildId, 499 Version: versionId, 500 } 501 So(task1.Insert(), ShouldBeNil) 502 503 sampleHost := host.Host{ 504 Id: hostId, 505 Secret: hostSecret, 506 RunningTask: task1.Id, 507 Status: evergreen.HostRunning, 508 AgentRevision: agentRevision, 509 } 510 So(sampleHost.Insert(), ShouldBeNil) 511 512 testBuild := build.Build{ 513 Id: buildId, 514 Tasks: []build.TaskCache{ 515 {Id: "task1"}, 516 {Id: "task2"}, 517 }, 518 Project: projectId, 519 Version: versionId, 520 } 521 So(testBuild.Insert(), ShouldBeNil) 522 523 testVersion := version.Version{ 524 Id: versionId, 525 Branch: projectId, 526 } 527 So(testVersion.Insert(), ShouldBeNil) 528 Convey("with a set of task end details indicating that task has succeeded", func() { 529 details := &apimodels.TaskEndDetail{ 530 Status: evergreen.TaskSucceeded, 531 } 532 resp := getEndTaskEndpoint(t, as, hostId, task1.Id, details) 533 So(resp, ShouldNotBeNil) 534 Convey("should return http status ok", func() { 535 So(resp.Code, ShouldEqual, http.StatusOK) 536 Convey("task should exist with the existing task id and be dispatched", func() { 537 taskResp := apimodels.EndTaskResponse{} 538 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 539 So(taskResp.ShouldExit, ShouldBeFalse) 540 }) 541 }) 542 Convey("the host should no longer have the task set as its running task", func() { 543 h, err := host.FindOne(host.ById(hostId)) 544 So(err, ShouldBeNil) 545 So(h.RunningTask, ShouldEqual, "") 546 Convey("the task should be marked as succeeded and the task end details"+ 547 "should be added to the task document", func() { 548 t, err := task.FindOne(task.ById(task1.Id)) 549 So(err, ShouldBeNil) 550 So(t.Status, ShouldEqual, evergreen.TaskSucceeded) 551 So(t.Details.Status, ShouldEqual, evergreen.TaskSucceeded) 552 }) 553 }) 554 }) 555 Convey("with a set of task end details indicating that task has failed", func() { 556 details := &apimodels.TaskEndDetail{ 557 Status: evergreen.TaskFailed, 558 } 559 testTask, err := task.FindOne(task.ById(task1.Id)) 560 So(err, ShouldBeNil) 561 So(testTask.Status, ShouldEqual, evergreen.TaskStarted) 562 resp := getEndTaskEndpoint(t, as, hostId, task1.Id, details) 563 So(resp, ShouldNotBeNil) 564 Convey("should return http status ok", func() { 565 So(resp.Code, ShouldEqual, http.StatusOK) 566 Convey("task should exist with the existing task id and be dispatched", func() { 567 taskResp := apimodels.EndTaskResponse{} 568 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 569 So(taskResp.ShouldExit, ShouldBeFalse) 570 }) 571 }) 572 Convey("the host should no longer have the task set as its running task", func() { 573 h, err := host.FindOne(host.ById(hostId)) 574 So(err, ShouldBeNil) 575 So(h.RunningTask, ShouldEqual, "") 576 Convey("the task should be marked as succeeded and the task end details"+ 577 "should be added to the task document", func() { 578 t, err := task.FindOne(task.ById(task1.Id)) 579 So(err, ShouldBeNil) 580 So(t.Status, ShouldEqual, evergreen.TaskFailed) 581 So(t.Details.Status, ShouldEqual, evergreen.TaskFailed) 582 }) 583 }) 584 }) 585 Convey("with a set of task end details but a task that is inactive", func() { 586 task2 := task.Task{ 587 Id: "task2", 588 Status: evergreen.TaskUndispatched, 589 Activated: false, 590 HostId: "h2", 591 Secret: taskSecret, 592 Project: projectId, 593 BuildId: buildId, 594 Version: versionId, 595 } 596 So(task2.Insert(), ShouldBeNil) 597 598 sampleHost := host.Host{ 599 Id: "h2", 600 Secret: hostSecret, 601 RunningTask: task2.Id, 602 Status: evergreen.HostRunning, 603 AgentRevision: agentRevision, 604 } 605 So(sampleHost.Insert(), ShouldBeNil) 606 607 details := &apimodels.TaskEndDetail{ 608 Status: evergreen.TaskUndispatched, 609 } 610 testTask, err := task.FindOne(task.ById(task1.Id)) 611 So(err, ShouldBeNil) 612 So(testTask.Status, ShouldEqual, evergreen.TaskStarted) 613 resp := getEndTaskEndpoint(t, as, sampleHost.Id, task2.Id, details) 614 So(resp, ShouldNotBeNil) 615 Convey("should return http status ok", func() { 616 So(resp.Code, ShouldEqual, http.StatusOK) 617 Convey("task should exist with the existing task id and be dispatched", func() { 618 taskResp := apimodels.EndTaskResponse{} 619 So(json.NewDecoder(resp.Body).Decode(&taskResp), ShouldBeNil) 620 So(taskResp.ShouldExit, ShouldBeTrue) 621 }) 622 }) 623 }) 624 625 }) 626 }