Repository: hashicorp/nomad
File: command/job_start_test.go (Go)

Code before:

	// check multiple cycles of starting/stopping a job result in the correct version selected
	for _ = range 3 {

		code := stopCmd.Run(args)
		must.Zero(t, code,
			must.Sprintf("job stop stdout: %s", ui.OutputWriter.String()),
			must.Sprintf("job stop stderr: %s", ui.ErrorWriter.String()),
		)

		code = startCmd.Run(args)
		must.Zero(t, code,
			must.Sprintf("job start stdout: %s", ui.OutputWriter.String()),
			must.Sprintf("job start stderr: %s", ui.ErrorWriter.String()),
		)
		if newCode := waitForSuccess(ui, client, fullId, t, resp.EvalID); newCode != 0 {
			t.Fatalf("status code non zero saw %d", newCode)
		}

		allocs, _, err = client.Jobs().Allocations("job-start-test", true, nil)
		must.NoError(t, err)
		waitForAllocRunning(t, client, allocs[0].ID)

		// check the version selected
		versionSelected := <-startCmd.versionCh
		must.Eq(t, 0, versionSelected)
	}

}

func TestStartCommand_AutocompleteArgs(t *testing.T) {
	ci.Parallel(t)

	srv, _, url := testServer(t, true, nil)
	defer srv.Shutdown()

	ui := cli.NewMockUi()
	cmd := &JobStartCommand{Meta: Meta{Ui: ui, flagAddress: url}}

	// Create a fake job
	state := srv.Agent.Server().State()
	j := mock.Job()
	must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j))

	prefix := j.ID[:len(j.ID)-5]
	args := complete.Args{Last: prefix}
	predictor := cmd.AutocompleteArgs()

	res := predictor.Predict(args)
	must.Len(t, 1, res)
	must.Eq(t, j.ID, res[0])

Review comment (suggestion):
A key issue I noticed when testing this feature is that when stopping a job for the first time and starting it again, version 0 is set to be “running” and therefore it is selected as the version to revert to (since version 1 is the version that has stop set to true), however, when a job is stopped, version 2 (the most recent version before calling stop) is marked as “dead” so when that job is started again we will be reverting to version 0 again because it is the only running and non-stopped version available, and this will happen for all the next iterations of stop/start commands. One issue that arises is that when version zero is left behind (as after the size of the versions list reaches a limit we take the oldest version out of the versions list), we have no more running and non-stopped versions to revert to. 

For example, if I run a service job, stop it, and start it again, the job will be reverted to version 0, since that is the most recent running and non-stopped version :
<img width="760" alt="Screenshot 2024-12-05 at 12 41 49 PM 1" src="https://github.com/user-attachments/assets/1800b155-9195-4222-941c-29066762819f">
When I stop this job and start it again, version 0 will once again be selected to revert to, because version 2 has been marked as dead after the second stop.
<img width="951" alt="Screenshot 2024-12-05 at 12 42 49 PM" src="https://github.com/user-attachments/assets/6ca987c9-ffcd-470c-a883-d3e29d7a2de2">



Code after:
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package command

import (
	"testing"

	"github.com/hashicorp/cli"
	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/command/agent"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/posener/complete"
	"github.com/shoenig/test/must"
)

var _ cli.Command = (*JobStartCommand)(nil)

func TestStartCommand(t *testing.T) {
	ci.Parallel(t)

	srv, _, addr := testServer(t, true, func(c *agent.Config) {
		c.DevMode = true
	})
	defer srv.Shutdown()

	ui := cli.NewMockUi()
	cmd := &JobStartCommand{
		Meta: Meta{
			Ui:          ui,
			flagAddress: addr,
		},
	}

	t.Run("succeeds when starting a stopped job", func(t *testing.T) {
		job := testJob(uuid.Generate())

		client, err := cmd.Meta.Client()
		must.NoError(t, err)

		_, _, err = client.Jobs().Register(job, nil)
		must.NoError(t, err)

		waitForJobAllocsStatus(t, client, *job.ID, api.AllocClientStatusRunning, "")

		_, _, err = client.Jobs().Deregister(*job.ID, false, nil)
		must.Nil(t, err)

		waitForJobAllocsStatus(t, client, *job.ID, api.AllocClientStatusComplete, "")

		res := cmd.Run([]string{"-address", addr, *job.ID})
		must.Zero(t, res)
	})

	t.Run("fails to start a job not previously stopped", func(t *testing.T) {
		job := testJob(uuid.Generate())

		client, err := cmd.Meta.Client()
		must.NoError(t, err)

		_, _, err = client.Jobs().Register(job, nil)
		must.NoError(t, err)

		waitForJobAllocsStatus(t, client, *job.ID, api.AllocClientStatusRunning, "")

		res := cmd.Run([]string{"-address", addr, *job.ID})
		must.Eq(t, 1, res)
	})

	t.Run("fails to start a non-existant job", func(t *testing.T) {
		res := cmd.Run([]string{"-address", addr, "non-existant"})
		must.Eq(t, 1, res)
	})
}

func TestStartCommand_Arguments(t *testing.T) {
	ci.Parallel(t)

	t.Run("fails if client request fails", func(t *testing.T) {
		ui := cli.NewMockUi()
		cmd := &JobStartCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		code := cmd.Run([]string{"-address=nope", "foo"})
		must.Eq(t, code, 1)

		out := ui.ErrorWriter.String()
		must.StrContains(t, out, "Error querying job prefix")
	})
	t.Run("fails if given more than 1 argument", func(t *testing.T) {
		ui := cli.NewMockUi()
		cmd := &JobStartCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		code := cmd.Run([]string{"foo1", "foo2"})
		must.Eq(t, code, 1)

		out := ui.ErrorWriter.String()
		must.StrContains(t, out, "This command takes one argument: <job>")
	})
	t.Run("fails if given less than 1 argument", func(t *testing.T) {
		ui := cli.NewMockUi()
		cmd := &JobStartCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		code := cmd.Run([]string{})
		must.Eq(t, code, 1)

		out := ui.ErrorWriter.String()
		must.StrContains(t, out, "This command takes one argument: <job>")
	})
}

func TestStartCommand_AutocompleteArgs(t *testing.T) {
	ci.Parallel(t)

	srv, _, url := testServer(t, true, nil)
	defer srv.Shutdown()

	ui := cli.NewMockUi()
	cmd := &JobStartCommand{Meta: Meta{Ui: ui, flagAddress: url}}

	// Create a fake job
	state := srv.Agent.Server().State()
	j := mock.Job()
	must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j))

	prefix := j.ID[:len(j.ID)-5]
	args := complete.Args{Last: prefix}
	predictor := cmd.AutocompleteArgs()

	res := predictor.Predict(args)
	must.Len(t, 1, res)
	must.Eq(t, j.ID, res[0])
}