github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/cf/commands/application/start_test.go (about) 1 package application_test 2 3 import ( 4 "os" 5 "time" 6 7 . "code.cloudfoundry.org/cli/cf/commands/application" 8 "code.cloudfoundry.org/cli/cf/commands/application/applicationfakes" 9 "code.cloudfoundry.org/cli/cf/requirements" 10 "code.cloudfoundry.org/cli/cf/requirements/requirementsfakes" 11 "code.cloudfoundry.org/cli/cf/trace/tracefakes" 12 13 "code.cloudfoundry.org/cli/cf/commandregistry" 14 "code.cloudfoundry.org/cli/cf/configuration/coreconfig" 15 "code.cloudfoundry.org/cli/cf/errors" 16 "code.cloudfoundry.org/cli/cf/models" 17 18 "code.cloudfoundry.org/cli/cf/api/appinstances/appinstancesfakes" 19 "code.cloudfoundry.org/cli/cf/api/applications/applicationsfakes" 20 "code.cloudfoundry.org/cli/cf/api/logs" 21 "code.cloudfoundry.org/cli/cf/api/logs/logsfakes" 22 testcmd "code.cloudfoundry.org/cli/util/testhelpers/commands" 23 testconfig "code.cloudfoundry.org/cli/util/testhelpers/configuration" 24 testterm "code.cloudfoundry.org/cli/util/testhelpers/terminal" 25 26 . "code.cloudfoundry.org/cli/util/testhelpers/matchers" 27 28 "sync" 29 30 "sync/atomic" 31 32 . "github.com/onsi/ginkgo" 33 . "github.com/onsi/gomega" 34 ) 35 36 var _ = Describe("start command", func() { 37 var ( 38 ui *testterm.FakeUI 39 configRepo coreconfig.Repository 40 defaultAppForStart models.Application 41 defaultInstanceResponses [][]models.AppInstanceFields 42 defaultInstanceErrorCodes []string 43 requirementsFactory *requirementsfakes.FakeFactory 44 logMessages atomic.Value 45 logRepo *logsfakes.FakeRepository 46 47 appInstancesRepo *appinstancesfakes.FakeAppInstancesRepository 48 appRepo *applicationsfakes.FakeRepository 49 originalAppCommand commandregistry.Command 50 deps commandregistry.Dependency 51 displayApp *applicationfakes.FakeAppDisplayer 52 ) 53 54 updateCommandDependency := func(logsRepo logs.Repository) { 55 deps.UI = ui 56 deps.Config = configRepo 57 deps.RepoLocator = deps.RepoLocator.SetLogsRepository(logsRepo) 58 deps.RepoLocator = deps.RepoLocator.SetApplicationRepository(appRepo) 59 deps.RepoLocator = deps.RepoLocator.SetAppInstancesRepository(appInstancesRepo) 60 61 //inject fake 'Start' into registry 62 commandregistry.Register(displayApp) 63 64 commandregistry.Commands.SetCommand(commandregistry.Commands.FindCommand("start").SetDependency(deps, false)) 65 } 66 67 getInstance := func(appGUID string) ([]models.AppInstanceFields, error) { 68 var apiErr error 69 var instances []models.AppInstanceFields 70 71 if len(defaultInstanceResponses) > 0 { 72 instances, defaultInstanceResponses = defaultInstanceResponses[0], defaultInstanceResponses[1:] 73 } 74 if len(defaultInstanceErrorCodes) > 0 { 75 var errorCode string 76 errorCode, defaultInstanceErrorCodes = defaultInstanceErrorCodes[0], defaultInstanceErrorCodes[1:] 77 78 if errorCode != "" { 79 apiErr = errors.NewHTTPError(400, errorCode, "Error staging app") 80 } 81 } 82 83 return instances, apiErr 84 } 85 86 AfterEach(func() { 87 commandregistry.Register(originalAppCommand) 88 }) 89 90 BeforeEach(func() { 91 deps = commandregistry.NewDependency(os.Stdout, new(tracefakes.FakePrinter), "") 92 ui = new(testterm.FakeUI) 93 requirementsFactory = new(requirementsfakes.FakeFactory) 94 95 configRepo = testconfig.NewRepository() 96 97 appInstancesRepo = new(appinstancesfakes.FakeAppInstancesRepository) 98 appRepo = new(applicationsfakes.FakeRepository) 99 100 displayApp = new(applicationfakes.FakeAppDisplayer) 101 102 //save original command dependency and restore later 103 originalAppCommand = commandregistry.Commands.FindCommand("app") 104 105 defaultInstanceErrorCodes = []string{"", ""} 106 107 defaultAppForStart = models.Application{ 108 ApplicationFields: models.ApplicationFields{ 109 Name: "my-app", 110 GUID: "my-app-guid", 111 InstanceCount: 2, 112 PackageState: "STAGED", 113 }, 114 } 115 116 defaultAppForStart.Routes = []models.RouteSummary{ 117 { 118 Host: "my-app", 119 Domain: models.DomainFields{ 120 Name: "example.com", 121 }, 122 }, 123 } 124 125 instance1 := models.AppInstanceFields{ 126 State: models.InstanceStarting, 127 } 128 129 instance2 := models.AppInstanceFields{ 130 State: models.InstanceStarting, 131 } 132 133 instance3 := models.AppInstanceFields{ 134 State: models.InstanceRunning, 135 } 136 137 instance4 := models.AppInstanceFields{ 138 State: models.InstanceStarting, 139 } 140 141 defaultInstanceResponses = [][]models.AppInstanceFields{ 142 {instance1, instance2}, 143 {instance1, instance2}, 144 {instance3, instance4}, 145 } 146 147 logRepo = new(logsfakes.FakeRepository) 148 logMessages.Store([]logs.Loggable{}) 149 150 closeWait := sync.WaitGroup{} 151 closeWait.Add(1) 152 153 logRepo.TailLogsForStub = func(appGUID string, onConnect func(), logChan chan<- logs.Loggable, errChan chan<- error) { 154 onConnect() 155 156 go func() { 157 for _, log := range logMessages.Load().([]logs.Loggable) { 158 logChan <- log 159 } 160 161 closeWait.Wait() 162 close(logChan) 163 }() 164 } 165 166 logRepo.CloseStub = func() { 167 closeWait.Done() 168 } 169 }) 170 171 callStart := func(args []string) bool { 172 updateCommandDependency(logRepo) 173 cmd := commandregistry.Commands.FindCommand("start").(*Start) 174 cmd.StagingTimeout = 100 * time.Millisecond 175 cmd.StartupTimeout = 500 * time.Millisecond 176 cmd.PingerThrottle = 10 * time.Millisecond 177 commandregistry.Register(cmd) 178 return testcmd.RunCLICommandWithoutDependency("start", args, requirementsFactory, ui) 179 } 180 181 callStartWithLoggingTimeout := func(args []string) bool { 182 183 logRepoWithTimeout := logsfakes.FakeRepository{} 184 updateCommandDependency(&logRepoWithTimeout) 185 186 cmd := commandregistry.Commands.FindCommand("start").(*Start) 187 cmd.LogServerConnectionTimeout = 100 * time.Millisecond 188 cmd.StagingTimeout = 100 * time.Millisecond 189 cmd.StartupTimeout = 200 * time.Millisecond 190 cmd.PingerThrottle = 10 * time.Millisecond 191 commandregistry.Register(cmd) 192 193 return testcmd.RunCLICommandWithoutDependency("start", args, requirementsFactory, ui) 194 } 195 196 startAppWithInstancesAndErrors := func(app models.Application, requirementsFactory *requirementsfakes.FakeFactory) (*testterm.FakeUI, *applicationsfakes.FakeRepository, *appinstancesfakes.FakeAppInstancesRepository) { 197 appRepo.UpdateReturns(app, nil) 198 appRepo.ReadReturns(app, nil) 199 appRepo.GetAppReturns(app, nil) 200 appInstancesRepo.GetInstancesStub = getInstance 201 202 args := []string{"my-app"} 203 204 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 205 applicationReq.GetApplicationReturns(app) 206 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 207 callStart(args) 208 return ui, appRepo, appInstancesRepo 209 } 210 211 It("fails requirements when not logged in", func() { 212 requirementsFactory.NewLoginRequirementReturns(requirements.Failing{Message: "not logged in"}) 213 214 Expect(callStart([]string{"some-app-name"})).To(BeFalse()) 215 }) 216 217 It("fails requirements when a space is not targeted", func() { 218 requirementsFactory.NewLoginRequirementReturns(requirements.Passing{}) 219 requirementsFactory.NewTargetedSpaceRequirementReturns(requirements.Failing{Message: "not targeting space"}) 220 221 Expect(callStart([]string{"some-app-name"})).To(BeFalse()) 222 }) 223 224 Describe("timeouts", func() { 225 It("has sane default timeout values", func() { 226 updateCommandDependency(logRepo) 227 cmd := commandregistry.Commands.FindCommand("start").(*Start) 228 Expect(cmd.StagingTimeout).To(Equal(15 * time.Minute)) 229 Expect(cmd.StartupTimeout).To(Equal(5 * time.Minute)) 230 }) 231 232 It("can read timeout values from environment variables", func() { 233 oldStaging := os.Getenv("CF_STAGING_TIMEOUT") 234 oldStart := os.Getenv("CF_STARTUP_TIMEOUT") 235 defer func() { 236 os.Setenv("CF_STAGING_TIMEOUT", oldStaging) 237 os.Setenv("CF_STARTUP_TIMEOUT", oldStart) 238 }() 239 240 os.Setenv("CF_STAGING_TIMEOUT", "6") 241 os.Setenv("CF_STARTUP_TIMEOUT", "3") 242 243 updateCommandDependency(logRepo) 244 cmd := commandregistry.Commands.FindCommand("start").(*Start) 245 Expect(cmd.StagingTimeout).To(Equal(6 * time.Minute)) 246 Expect(cmd.StartupTimeout).To(Equal(3 * time.Minute)) 247 }) 248 249 Describe("when the staging timeout is zero seconds", func() { 250 var ( 251 app models.Application 252 ) 253 254 BeforeEach(func() { 255 app = defaultAppForStart 256 257 appRepo.UpdateReturns(app, nil) 258 259 app.PackageState = "FAILED" 260 app.StagingFailedReason = "BLAH, FAILED" 261 appRepo.GetAppReturns(app, nil) 262 263 requirementsFactory.NewLoginRequirementReturns(requirements.Passing{}) 264 requirementsFactory.NewTargetedSpaceRequirementReturns(requirements.Passing{}) 265 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 266 applicationReq.GetApplicationReturns(app) 267 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 268 269 updateCommandDependency(logRepo) 270 cmd := commandregistry.Commands.FindCommand("start").(*Start) 271 cmd.StagingTimeout = 0 272 cmd.PingerThrottle = 1 273 cmd.StartupTimeout = 1 274 commandregistry.Register(cmd) 275 }) 276 277 It("can still respond to staging failures", func() { 278 testcmd.RunCLICommandWithoutDependency("start", []string{"my-app"}, requirementsFactory, ui) 279 280 Expect(ui.Outputs()).To(ContainSubstrings( 281 []string{"my-app"}, 282 []string{"FAILED"}, 283 []string{"BLAH, FAILED"}, 284 )) 285 }) 286 }) 287 288 Context("when the timeout happens exactly when the connection is established", func() { 289 var startWait *sync.WaitGroup 290 291 BeforeEach(func() { 292 requirementsFactory.NewLoginRequirementReturns(requirements.Passing{}) 293 requirementsFactory.NewTargetedSpaceRequirementReturns(requirements.Passing{}) 294 configRepo = testconfig.NewRepositoryWithDefaults() 295 logRepo.TailLogsForStub = func(appGUID string, onConnect func(), logChan chan<- logs.Loggable, errChan chan<- error) { 296 startWait.Wait() 297 onConnect() 298 } 299 }) 300 301 It("times out gracefully", func() { 302 updateCommandDependency(logRepo) 303 cmd := commandregistry.Commands.FindCommand("start").(*Start) 304 cmd.LogServerConnectionTimeout = 10 * time.Millisecond 305 startWait = new(sync.WaitGroup) 306 startWait.Add(1) 307 doneWait := new(sync.WaitGroup) 308 doneWait.Add(1) 309 cmd.TailStagingLogs(defaultAppForStart, make(chan bool, 1), startWait, doneWait) 310 }) 311 }) 312 313 Context("when the noaa library reconnects", func() { 314 var app models.Application 315 BeforeEach(func() { 316 app = defaultAppForStart 317 app.PackageState = "FAILED" 318 app.StagingFailedReason = "BLAH, FAILED" 319 320 requirementsFactory.NewLoginRequirementReturns(requirements.Passing{}) 321 requirementsFactory.NewTargetedSpaceRequirementReturns(requirements.Passing{}) 322 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 323 applicationReq.GetApplicationReturns(app) 324 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 325 326 appRepo.GetAppReturns(app, nil) 327 appRepo.UpdateReturns(app, nil) 328 329 cmd := commandregistry.Commands.FindCommand("start").(*Start) 330 cmd.StagingTimeout = 1 331 cmd.PingerThrottle = 1 332 cmd.StartupTimeout = 1 333 commandregistry.Register(cmd) 334 335 logRepo.TailLogsForStub = func(appGUID string, onConnect func(), logChan chan<- logs.Loggable, errChan chan<- error) { 336 onConnect() 337 onConnect() 338 onConnect() 339 } 340 updateCommandDependency(logRepo) 341 }) 342 343 It("it doesn't cause a negative wait group - github#1019", func() { 344 testcmd.RunCLICommandWithoutDependency("start", []string{"my-app"}, requirementsFactory, ui) 345 }) 346 }) 347 }) 348 349 Context("when logged in", func() { 350 BeforeEach(func() { 351 requirementsFactory.NewLoginRequirementReturns(requirements.Passing{}) 352 requirementsFactory.NewTargetedSpaceRequirementReturns(requirements.Passing{}) 353 configRepo = testconfig.NewRepositoryWithDefaults() 354 }) 355 356 It("fails with usage when not provided exactly one arg", func() { 357 callStart([]string{}) 358 Expect(ui.Outputs()).To(ContainSubstrings( 359 []string{"Incorrect Usage", "Requires an argument"}, 360 )) 361 }) 362 363 It("uses proper org name and space name", func() { 364 appRepo.ReadReturns(defaultAppForStart, nil) 365 appRepo.GetAppReturns(defaultAppForStart, nil) 366 appInstancesRepo.GetInstancesStub = getInstance 367 368 updateCommandDependency(logRepo) 369 cmd := commandregistry.Commands.FindCommand("start").(*Start) 370 cmd.PingerThrottle = 10 * time.Millisecond 371 372 //defaultAppForStart.State = "started" 373 cmd.ApplicationStart(defaultAppForStart, "some-org", "some-space") 374 375 Expect(ui.Outputs()).To(ContainSubstrings( 376 []string{"my-app", "some-org", "some-space", "my-user"}, 377 []string{"OK"}, 378 )) 379 }) 380 381 It("starts an app, when given the app's name", func() { 382 ui, appRepo, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 383 384 Expect(ui.Outputs()).To(ContainSubstrings( 385 []string{"my-app", "my-org", "my-space", "my-user"}, 386 []string{"OK"}, 387 []string{"0 of 2 instances running", "2 starting"}, 388 []string{"started"}, 389 )) 390 391 appGUID, _ := appRepo.UpdateArgsForCall(0) 392 Expect(appGUID).To(Equal("my-app-guid")) 393 Expect(displayApp.AppToDisplay).To(Equal(defaultAppForStart)) 394 }) 395 396 Context("when app instance count is zero", func() { 397 var zeroInstanceApp models.Application 398 BeforeEach(func() { 399 zeroInstanceApp = models.Application{ 400 ApplicationFields: models.ApplicationFields{ 401 Name: "my-app", 402 GUID: "my-app-guid", 403 InstanceCount: 0, 404 PackageState: "STAGED", 405 }, 406 } 407 defaultInstanceResponses = [][]models.AppInstanceFields{{}} 408 }) 409 410 It("exit without polling for the app, and warns the user", func() { 411 ui, _, _ := startAppWithInstancesAndErrors(zeroInstanceApp, requirementsFactory) 412 413 Expect(ui.Outputs()).To(ContainSubstrings( 414 []string{"my-app", "my-org", "my-space", "my-user"}, 415 []string{"OK"}, 416 []string{"App state changed to started, but note that it has 0 instances."}, 417 )) 418 419 Expect(appRepo.UpdateCallCount()).To(Equal(1)) 420 appGuid, appParams := appRepo.UpdateArgsForCall(0) 421 Expect(appGuid).To(Equal(zeroInstanceApp.GUID)) 422 startedState := "started" 423 Expect(appParams).To(Equal(models.AppParams{State: &startedState})) 424 425 zeroInstanceApp.State = startedState 426 ui, _, _ = startAppWithInstancesAndErrors(zeroInstanceApp, requirementsFactory) 427 Expect(ui.Outputs()).To(ContainSubstrings( 428 []string{"App my-app is already started"}, 429 )) 430 Expect(appRepo.UpdateCallCount()).To(Equal(1)) 431 }) 432 }) 433 It("displays the command start command instead of the detected start command when set", func() { 434 defaultAppForStart.Command = "command start command" 435 defaultAppForStart.DetectedStartCommand = "detected start command" 436 ui, appRepo, _ = startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 437 438 Expect(appRepo.GetAppCallCount()).To(Equal(1)) 439 Expect(appRepo.GetAppArgsForCall(0)).To(Equal("my-app-guid")) 440 Expect(ui.Outputs()).To(ContainSubstrings( 441 []string{"App my-app was started using this command `command start command`"}, 442 )) 443 }) 444 445 It("displays the detected start command when no other command is set", func() { 446 defaultAppForStart.DetectedStartCommand = "detected start command" 447 defaultAppForStart.Command = "" 448 ui, appRepo, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 449 450 Expect(appRepo.GetAppCallCount()).To(Equal(1)) 451 Expect(appRepo.GetAppArgsForCall(0)).To(Equal("my-app-guid")) 452 Expect(ui.Outputs()).To(ContainSubstrings( 453 []string{"App my-app was started using this command `detected start command`"}, 454 )) 455 }) 456 457 It("handles timeouts gracefully", func() { 458 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 459 applicationReq.GetApplicationReturns(defaultAppForStart) 460 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 461 appRepo.UpdateReturns(defaultAppForStart, nil) 462 appRepo.ReadReturns(defaultAppForStart, nil) 463 464 callStartWithLoggingTimeout([]string{"my-app"}) 465 Expect(ui.Outputs()).To(ContainSubstrings( 466 []string{"timeout connecting to log server"}, 467 )) 468 }) 469 470 It("only displays staging logs when an app is starting", func() { 471 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 472 applicationReq.GetApplicationReturns(defaultAppForStart) 473 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 474 appRepo.UpdateReturns(defaultAppForStart, nil) 475 appRepo.ReadReturns(defaultAppForStart, nil) 476 477 message1 := logsfakes.FakeLoggable{} 478 message1.ToSimpleLogReturns("Log Line 1") 479 480 message2 := logsfakes.FakeLoggable{} 481 message2.GetSourceNameReturns("STG") 482 message2.ToSimpleLogReturns("Log Line 2") 483 484 message3 := logsfakes.FakeLoggable{} 485 message3.GetSourceNameReturns("STG") 486 message3.ToSimpleLogReturns("Log Line 3") 487 488 message4 := logsfakes.FakeLoggable{} 489 message4.ToSimpleLogReturns("Log Line 4") 490 491 logMessages.Store([]logs.Loggable{ 492 &message1, 493 &message2, 494 &message3, 495 &message4, 496 }) 497 498 callStart([]string{"my-app"}) 499 500 Expect(ui.Outputs()).To(ContainSubstrings( 501 []string{"Log Line 2"}, 502 []string{"Log Line 3"}, 503 )) 504 Expect(ui.Outputs()).ToNot(ContainSubstrings( 505 []string{"Log Line 1"}, 506 []string{"Log Line 4"}, 507 )) 508 }) 509 510 It("gracefully handles starting an app that is still staging", func() { 511 closeWait := sync.WaitGroup{} 512 closeWait.Add(1) 513 514 logRepo.TailLogsForStub = func(appGUID string, onConnect func(), logChan chan<- logs.Loggable, errChan chan<- error) { 515 onConnect() 516 517 go func() { 518 message1 := logsfakes.FakeLoggable{} 519 message1.ToSimpleLogReturns("Before close") 520 message1.GetSourceNameReturns("STG") 521 522 logChan <- &message1 523 524 closeWait.Wait() 525 526 message2 := logsfakes.FakeLoggable{} 527 message2.ToSimpleLogReturns("After close 1") 528 message2.GetSourceNameReturns("STG") 529 530 message3 := logsfakes.FakeLoggable{} 531 message3.ToSimpleLogReturns("After close 2") 532 message3.GetSourceNameReturns("STG") 533 534 logChan <- &message2 535 logChan <- &message3 536 537 close(logChan) 538 }() 539 } 540 541 logRepo.CloseStub = func() { 542 closeWait.Done() 543 } 544 545 defaultInstanceResponses = [][]models.AppInstanceFields{ 546 {}, 547 {}, 548 {{State: models.InstanceDown}, {State: models.InstanceStarting}}, 549 {{State: models.InstanceStarting}, {State: models.InstanceStarting}}, 550 {{State: models.InstanceRunning}, {State: models.InstanceRunning}}, 551 } 552 553 defaultInstanceErrorCodes = []string{errors.NotStaged, errors.NotStaged, "", "", ""} 554 defaultAppForStart.PackageState = "PENDING" 555 ui, appRepo, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 556 557 Expect(appRepo.GetAppArgsForCall(0)).To(Equal("my-app-guid")) 558 559 Expect(ui.Outputs()).To(ContainSubstrings( 560 []string{"Before close"}, 561 []string{"After close 1"}, 562 []string{"After close 2"}, 563 []string{"my-app failed to stage within", "minutes"}, 564 )) 565 }) 566 567 It("displays an error message when staging fails", func() { 568 defaultAppForStart.PackageState = "FAILED" 569 defaultAppForStart.StagingFailedReason = "AWWW, FAILED" 570 571 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 572 573 Expect(ui.Outputs()).To(ContainSubstrings( 574 []string{"my-app"}, 575 []string{"FAILED"}, 576 []string{"AWWW, FAILED"}, 577 )) 578 }) 579 580 It("displays an TIP about needing to push from source directory when staging fails with NoAppDetectedError", func() { 581 defaultAppForStart.PackageState = "FAILED" 582 defaultAppForStart.StagingFailedReason = "NoAppDetectedError" 583 584 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 585 586 Expect(ui.Outputs()).To(ContainSubstrings( 587 []string{"my-app"}, 588 []string{"FAILED"}, 589 []string{"is executed from within the directory"}, 590 )) 591 }) 592 593 It("Display a TIP when starting the app timeout", func() { 594 appInstance := models.AppInstanceFields{} 595 appInstance.State = models.InstanceStarting 596 appInstance2 := models.AppInstanceFields{} 597 appInstance2.State = models.InstanceStarting 598 appInstance3 := models.AppInstanceFields{} 599 appInstance3.State = models.InstanceStarting 600 appInstance4 := models.AppInstanceFields{} 601 appInstance4.State = models.InstanceStarting 602 defaultInstanceResponses = [][]models.AppInstanceFields{ 603 {appInstance, appInstance2}, 604 {appInstance3, appInstance4}, 605 } 606 607 defaultInstanceErrorCodes = []string{"some error", ""} 608 609 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 610 611 Expect(ui.Outputs()).To(ContainSubstrings( 612 []string{"TIP: Application must be listening on the right port."}, 613 )) 614 }) 615 616 It("prints a warning when failing to fetch instance count", func() { 617 defaultInstanceResponses = [][]models.AppInstanceFields{} 618 defaultInstanceErrorCodes = []string{"an-error"} 619 620 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 621 622 Expect(ui.Outputs()).To(ContainSubstrings( 623 []string{"an-error"}, 624 )) 625 }) 626 627 Context("when an app instance is flapping", func() { 628 It("fails and alerts the user", func() { 629 appInstance := models.AppInstanceFields{} 630 appInstance.State = models.InstanceStarting 631 appInstance2 := models.AppInstanceFields{} 632 appInstance2.State = models.InstanceStarting 633 appInstance3 := models.AppInstanceFields{} 634 appInstance3.State = models.InstanceStarting 635 appInstance4 := models.AppInstanceFields{} 636 appInstance4.State = models.InstanceFlapping 637 defaultInstanceResponses = [][]models.AppInstanceFields{ 638 {appInstance, appInstance2}, 639 {appInstance3, appInstance4}, 640 } 641 642 defaultInstanceErrorCodes = []string{"", ""} 643 644 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 645 646 Expect(ui.Outputs()).To(ContainSubstrings( 647 []string{"my-app"}, 648 []string{"0 of 2 instances running", "1 starting", "1 failing"}, 649 []string{"FAILED"}, 650 []string{"Start unsuccessful"}, 651 )) 652 }) 653 }) 654 655 Context("when an app instance is crashed", func() { 656 It("fails and alerts the user", func() { 657 appInstance := models.AppInstanceFields{} 658 appInstance.State = models.InstanceStarting 659 appInstance2 := models.AppInstanceFields{} 660 appInstance2.State = models.InstanceStarting 661 appInstance3 := models.AppInstanceFields{} 662 appInstance3.State = models.InstanceStarting 663 appInstance4 := models.AppInstanceFields{} 664 appInstance4.State = models.InstanceCrashed 665 defaultInstanceResponses = [][]models.AppInstanceFields{ 666 {appInstance, appInstance2}, 667 {appInstance3, appInstance4}, 668 } 669 670 defaultInstanceErrorCodes = []string{"", ""} 671 672 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 673 674 Expect(ui.Outputs()).To(ContainSubstrings( 675 []string{"my-app"}, 676 []string{"0 of 2 instances running", "1 starting", "1 crashed"}, 677 []string{"FAILED"}, 678 []string{"Start unsuccessful"}, 679 )) 680 }) 681 }) 682 683 Context("when an app instance is starting", func() { 684 It("reports any additional details", func() { 685 appInstance := models.AppInstanceFields{ 686 State: models.InstanceStarting, 687 } 688 appInstance2 := models.AppInstanceFields{ 689 State: models.InstanceStarting, 690 } 691 692 appInstance3 := models.AppInstanceFields{ 693 State: models.InstanceDown, 694 } 695 appInstance4 := models.AppInstanceFields{ 696 State: models.InstanceStarting, 697 Details: "no compatible cell", 698 } 699 700 appInstance5 := models.AppInstanceFields{ 701 State: models.InstanceStarting, 702 Details: "insufficient resources", 703 } 704 appInstance6 := models.AppInstanceFields{ 705 State: models.InstanceStarting, 706 Details: "no compatible cell", 707 } 708 709 appInstance7 := models.AppInstanceFields{ 710 State: models.InstanceRunning, 711 } 712 appInstance8 := models.AppInstanceFields{ 713 State: models.InstanceRunning, 714 } 715 716 defaultInstanceResponses = [][]models.AppInstanceFields{ 717 {appInstance, appInstance2}, 718 {appInstance3, appInstance4}, 719 {appInstance5, appInstance6}, 720 {appInstance7, appInstance8}, 721 } 722 723 defaultInstanceErrorCodes = []string{"", ""} 724 725 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 726 Expect(ui.Outputs()).To(ContainSubstrings( 727 []string{"my-app"}, 728 []string{"0 of 2 instances running", "2 starting"}, 729 []string{"0 of 2 instances running", "1 starting (no compatible cell)", "1 down"}, 730 []string{"0 of 2 instances running", "2 starting (insufficient resources, no compatible cell)"}, 731 []string{"2 of 2 instances running"}, 732 []string{"App started"}, 733 )) 734 }) 735 }) 736 737 It("tells the user about the failure when waiting for the app to stage times out", func() { 738 defaultInstanceErrorCodes = []string{errors.NotStaged, errors.NotStaged, errors.NotStaged} 739 740 defaultAppForStart.PackageState = "PENDING" 741 ui, _, _ := startAppWithInstancesAndErrors(defaultAppForStart, requirementsFactory) 742 743 Expect(ui.Outputs()).To(ContainSubstrings( 744 []string{"Starting", "my-app"}, 745 []string{"FAILED"}, 746 []string{"my-app failed to stage within", "minutes"}, 747 )) 748 Expect(ui.Outputs()).ToNot(ContainSubstrings([]string{"instances running"})) 749 }) 750 751 It("tells the user about the failure when starting the app fails", func() { 752 app := models.Application{} 753 app.Name = "my-app" 754 app.GUID = "my-app-guid" 755 appRepo.UpdateReturns(models.Application{}, errors.New("Error updating app.")) 756 appRepo.ReadReturns(app, nil) 757 args := []string{"my-app"} 758 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 759 applicationReq.GetApplicationReturns(app) 760 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 761 callStart(args) 762 763 Expect(ui.Outputs()).To(ContainSubstrings( 764 []string{"my-app"}, 765 []string{"FAILED"}, 766 []string{"Error updating app."}, 767 )) 768 appGUID, _ := appRepo.UpdateArgsForCall(0) 769 Expect(appGUID).To(Equal("my-app-guid")) 770 }) 771 772 It("warns the user when the app is already running", func() { 773 app := models.Application{} 774 app.Name = "my-app" 775 app.GUID = "my-app-guid" 776 app.State = "started" 777 appRepo := new(applicationsfakes.FakeRepository) 778 appRepo.ReadReturns(app, nil) 779 780 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 781 applicationReq.GetApplicationReturns(app) 782 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 783 784 args := []string{"my-app"} 785 callStart(args) 786 787 Expect(ui.Outputs()).To(ContainSubstrings([]string{"my-app", "is already started"})) 788 789 Expect(appRepo.UpdateCallCount()).To(BeZero()) 790 }) 791 792 It("tells the user when connecting to the log server fails", func() { 793 appRepo.ReadReturns(defaultAppForStart, nil) 794 795 logRepo.TailLogsForStub = func(appGUID string, onConnect func(), logChan chan<- logs.Loggable, errChan chan<- error) { 796 errChan <- errors.New("Ooops") 797 } 798 799 applicationReq := new(requirementsfakes.FakeApplicationRequirement) 800 applicationReq.GetApplicationReturns(defaultAppForStart) 801 requirementsFactory.NewApplicationRequirementReturns(applicationReq) 802 803 callStart([]string{"my-app"}) 804 805 Expect(ui.Outputs()).To(ContainSubstrings( 806 []string{"error tailing logs"}, 807 []string{"Ooops"}, 808 )) 809 }) 810 }) 811 })