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  })