Files
nomad/command/eval_status_test.go
Tim Gross 0ae5b3f39b eval status: sort plan annotations by task group (#26428)
The plan annotations table isn't sorted by task group, which makes for a less
beautiful UX and a flaky test.
2025-08-05 09:36:12 -04:00

242 lines
7.3 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"testing"
"time"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pointer"
"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"
)
func TestEvalStatusCommand_Implements(t *testing.T) {
ci.Parallel(t)
var _ cli.Command = &EvalStatusCommand{}
}
func TestEvalStatusCommand_Fails(t *testing.T) {
ci.Parallel(t)
srv, _, url := testServer(t, false, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui}}
// Fails on misuse
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
t.Fatalf("expected help output, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on eval lookup failure
if code := cmd.Run([]string{"-address=" + url, "3E55C771-76FC-423B-BCED-3E5314F433B1"}); code != 1 {
t.Fatalf("expect exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No evaluation(s) with prefix or id") {
t.Fatalf("expect not found error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on connection failure
if code := cmd.Run([]string{"-address=nope", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying evaluation") {
t.Fatalf("expected failed query error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Failed on both -json and -t options are specified
if code := cmd.Run([]string{"-address=" + url, "-json", "-t", "{{.ID}}",
"12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Both json and template formatting are not allowed") {
t.Fatalf("expected getting formatter error, got: %s", out)
}
}
func TestEvalStatusCommand_AutocompleteArgs(t *testing.T) {
ci.Parallel(t)
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
// Create a fake eval
state := srv.Agent.Server().State()
e := mock.Eval()
must.NoError(t, state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{e}))
prefix := e.ID[:5]
args := complete.Args{Last: prefix}
predictor := cmd.AutocompleteArgs()
res := predictor.Predict(args)
must.SliceLen(t, 1, res)
must.Eq(t, e.ID, res[0])
}
func TestEvalStatusCommand_Format(t *testing.T) {
now := time.Now().UTC()
ui := cli.NewMockUi()
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui}}
eval := &api.Evaluation{
ID: uuid.Generate(),
Priority: 50,
Type: api.JobTypeService,
TriggeredBy: structs.EvalTriggerAllocStop,
Namespace: api.DefaultNamespace,
JobID: "example",
JobModifyIndex: 0,
DeploymentID: uuid.Generate(),
Status: api.EvalStatusComplete,
StatusDescription: "complete",
NextEval: "",
PreviousEval: uuid.Generate(),
BlockedEval: uuid.Generate(),
RelatedEvals: []*api.EvaluationStub{{
ID: uuid.Generate(),
Priority: 50,
Type: "service",
TriggeredBy: "queued-allocs",
Namespace: api.DefaultNamespace,
JobID: "example",
DeploymentID: "",
Status: "pending",
StatusDescription: "",
WaitUntil: time.Time{},
NextEval: "",
PreviousEval: uuid.Generate(),
BlockedEval: "",
CreateIndex: 0,
ModifyIndex: 0,
CreateTime: 0,
ModifyTime: 0,
}},
FailedTGAllocs: map[string]*api.AllocationMetric{"web": {
NodesEvaluated: 6,
NodesFiltered: 4,
NodesInPool: 10,
NodesAvailable: map[string]int{},
ClassFiltered: map[string]int{},
ConstraintFiltered: map[string]int{"${attr.kernel.name} = linux": 2},
NodesExhausted: 2,
ClassExhausted: map[string]int{},
DimensionExhausted: map[string]int{"memory": 2},
QuotaExhausted: []string{},
ResourcesExhausted: map[string]*api.Resources{"web": {
Cores: pointer.Of(3),
}},
Scores: map[string]float64{},
AllocationTime: 0,
CoalescedFailures: 0,
ScoreMetaData: []*api.NodeScoreMeta{},
}},
PlanAnnotations: &api.PlanAnnotations{
DesiredTGUpdates: map[string]*api.DesiredUpdates{"web": {Place: 10}},
PreemptedAllocs: []*api.AllocationListStub{
{
ID: uuid.Generate(),
JobID: "another",
NodeID: uuid.Generate(),
TaskGroup: "db",
DesiredStatus: "evict",
JobVersion: 3,
ClientStatus: "complete",
CreateTime: now.Add(-10 * time.Minute).UnixNano(),
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
},
},
},
ClassEligibility: map[string]bool{},
EscapedComputedClass: true,
QuotaLimitReached: "",
QueuedAllocations: map[string]int{},
SnapshotIndex: 1001,
CreateIndex: 999,
ModifyIndex: 1003,
CreateTime: now.UnixNano(),
ModifyTime: now.Add(time.Second).UnixNano(),
}
placed := []*api.AllocationListStub{
{
ID: uuid.Generate(),
NodeID: uuid.Generate(),
TaskGroup: "web",
DesiredStatus: "run",
JobVersion: 2,
ClientStatus: "running",
CreateTime: now.Add(-10 * time.Second).UnixNano(),
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
},
{
ID: uuid.Generate(),
NodeID: uuid.Generate(),
TaskGroup: "web",
JobVersion: 2,
DesiredStatus: "run",
ClientStatus: "pending",
CreateTime: now.Add(-3 * time.Second).UnixNano(),
ModifyTime: now.Add(-1 * time.Second).UnixNano(),
},
{
ID: uuid.Generate(),
NodeID: uuid.Generate(),
TaskGroup: "web",
JobVersion: 2,
DesiredStatus: "run",
ClientStatus: "pending",
CreateTime: now.Add(-4 * time.Second).UnixNano(),
ModifyTime: now.UnixNano(),
},
}
cmd.formatEvalStatus(eval, placed, false, shortId)
out := ui.OutputWriter.String()
// there isn't much logic here, so this is just a smoke test
must.StrContains(t, out, `
Failed Placements
Task Group "web" (failed to place 1 allocation):
* Constraint "${attr.kernel.name} = linux": 2 nodes excluded by filter
* Resources exhausted on 2 nodes
* Dimension "memory" exhausted on 2 nodes`)
must.StrContains(t, out, `Related Evaluations`)
must.StrContains(t, out, `Placed Allocations`)
must.StrContains(t, out, `Plan Annotations`)
must.StrContains(t, out, `Preempted Allocations`)
}
func TestEvalStatus_FormatPlanAnnotations(t *testing.T) {
updates := map[string]*api.DesiredUpdates{
"foo": {Place: 1, Ignore: 2, Canary: 1},
"bar": {Place: 1, Stop: 3, Reconnect: 2},
}
out := formatPlanAnnotations(updates, false)
must.Eq(t, `Task Group Ignore Place Stop InPlace Destructive Canary Reconnect
bar 0 1 3 0 0 0 2
foo 2 1 0 0 0 1 0`, out)
}