1package command
2
3import (
4	"strings"
5	"testing"
6	"time"
7
8	"github.com/hashicorp/nomad/nomad/structs"
9	"github.com/mitchellh/cli"
10)
11
12func TestMonitor_Update_Eval(t *testing.T) {
13	t.Parallel()
14	ui := new(cli.MockUi)
15	mon := newMonitor(ui, nil, fullId)
16
17	// Evals triggered by jobs log
18	state := &evalState{
19		status: structs.EvalStatusPending,
20		job:    "job1",
21	}
22	mon.update(state)
23
24	out := ui.OutputWriter.String()
25	if !strings.Contains(out, "job1") {
26		t.Fatalf("missing job\n\n%s", out)
27	}
28	ui.OutputWriter.Reset()
29
30	// Evals triggered by nodes log
31	state = &evalState{
32		status: structs.EvalStatusPending,
33		node:   "12345678-abcd-efab-cdef-123456789abc",
34	}
35	mon.update(state)
36
37	out = ui.OutputWriter.String()
38	if !strings.Contains(out, "12345678-abcd-efab-cdef-123456789abc") {
39		t.Fatalf("missing node\n\n%s", out)
40	}
41
42	// Transition to pending should not be logged
43	if strings.Contains(out, structs.EvalStatusPending) {
44		t.Fatalf("should skip status\n\n%s", out)
45	}
46	ui.OutputWriter.Reset()
47
48	// No logs sent if no update
49	mon.update(state)
50	if out := ui.OutputWriter.String(); out != "" {
51		t.Fatalf("expected no output\n\n%s", out)
52	}
53
54	// Status change sends more logs
55	state = &evalState{
56		status: structs.EvalStatusComplete,
57		node:   "12345678-abcd-efab-cdef-123456789abc",
58	}
59	mon.update(state)
60	out = ui.OutputWriter.String()
61	if !strings.Contains(out, structs.EvalStatusComplete) {
62		t.Fatalf("missing status\n\n%s", out)
63	}
64}
65
66func TestMonitor_Update_Allocs(t *testing.T) {
67	t.Parallel()
68	ui := new(cli.MockUi)
69	mon := newMonitor(ui, nil, fullId)
70
71	// New allocations write new logs
72	state := &evalState{
73		allocs: map[string]*allocState{
74			"alloc1": {
75				id:      "87654321-abcd-efab-cdef-123456789abc",
76				group:   "group1",
77				node:    "12345678-abcd-efab-cdef-123456789abc",
78				desired: structs.AllocDesiredStatusRun,
79				client:  structs.AllocClientStatusPending,
80				index:   1,
81			},
82		},
83	}
84	mon.update(state)
85
86	// Logs were output
87	out := ui.OutputWriter.String()
88	if !strings.Contains(out, "87654321-abcd-efab-cdef-123456789abc") {
89		t.Fatalf("missing alloc\n\n%s", out)
90	}
91	if !strings.Contains(out, "group1") {
92		t.Fatalf("missing group\n\n%s", out)
93	}
94	if !strings.Contains(out, "12345678-abcd-efab-cdef-123456789abc") {
95		t.Fatalf("missing node\n\n%s", out)
96	}
97	if !strings.Contains(out, "created") {
98		t.Fatalf("missing created\n\n%s", out)
99	}
100	ui.OutputWriter.Reset()
101
102	// No change yields no logs
103	mon.update(state)
104	if out := ui.OutputWriter.String(); out != "" {
105		t.Fatalf("expected no output\n\n%s", out)
106	}
107	ui.OutputWriter.Reset()
108
109	// Alloc updates cause more log lines
110	state = &evalState{
111		allocs: map[string]*allocState{
112			"alloc1": {
113				id:      "87654321-abcd-efab-cdef-123456789abc",
114				group:   "group1",
115				node:    "12345678-abcd-efab-cdef-123456789abc",
116				desired: structs.AllocDesiredStatusRun,
117				client:  structs.AllocClientStatusRunning,
118				index:   2,
119			},
120		},
121	}
122	mon.update(state)
123
124	// Updates were logged
125	out = ui.OutputWriter.String()
126	if !strings.Contains(out, "87654321-abcd-efab-cdef-123456789abc") {
127		t.Fatalf("missing alloc\n\n%s", out)
128	}
129	if !strings.Contains(out, "pending") {
130		t.Fatalf("missing old status\n\n%s", out)
131	}
132	if !strings.Contains(out, "running") {
133		t.Fatalf("missing new status\n\n%s", out)
134	}
135}
136
137func TestMonitor_Update_AllocModification(t *testing.T) {
138	t.Parallel()
139	ui := new(cli.MockUi)
140	mon := newMonitor(ui, nil, fullId)
141
142	// New allocs with a create index lower than the
143	// eval create index are logged as modifications
144	state := &evalState{
145		index: 2,
146		allocs: map[string]*allocState{
147			"alloc3": {
148				id:    "87654321-abcd-bafe-cdef-123456789abc",
149				node:  "12345678-abcd-efab-cdef-123456789abc",
150				group: "group2",
151				index: 1,
152			},
153		},
154	}
155	mon.update(state)
156
157	// Modification was logged
158	out := ui.OutputWriter.String()
159	if !strings.Contains(out, "87654321-abcd-bafe-cdef-123456789abc") {
160		t.Fatalf("missing alloc\n\n%s", out)
161	}
162	if !strings.Contains(out, "group2") {
163		t.Fatalf("missing group\n\n%s", out)
164	}
165	if !strings.Contains(out, "12345678-abcd-efab-cdef-123456789abc") {
166		t.Fatalf("missing node\n\n%s", out)
167	}
168	if !strings.Contains(out, "modified") {
169		t.Fatalf("missing modification\n\n%s", out)
170	}
171}
172
173func TestMonitor_Monitor(t *testing.T) {
174	t.Parallel()
175	srv, client, _ := testServer(t, false, nil)
176	defer srv.Shutdown()
177
178	// Create the monitor
179	ui := new(cli.MockUi)
180	mon := newMonitor(ui, client, fullId)
181
182	// Submit a job - this creates a new evaluation we can monitor
183	job := testJob("job1")
184	resp, _, err := client.Jobs().Register(job, nil)
185	if err != nil {
186		t.Fatalf("err: %s", err)
187	}
188
189	// Start monitoring the eval
190	var code int
191	doneCh := make(chan struct{})
192	go func() {
193		defer close(doneCh)
194		code = mon.monitor(resp.EvalID, false)
195	}()
196
197	// Wait for completion
198	select {
199	case <-doneCh:
200	case <-time.After(5 * time.Second):
201		t.Fatalf("eval monitor took too long")
202	}
203
204	// Check the return code. We should get exit code 2 as there
205	// would be a scheduling problem on the test server (no clients).
206	if code != 2 {
207		t.Fatalf("expect exit 2, got: %d", code)
208	}
209
210	// Check the output
211	out := ui.OutputWriter.String()
212	if !strings.Contains(out, resp.EvalID) {
213		t.Fatalf("missing eval\n\n%s", out)
214	}
215	if !strings.Contains(out, "finished with status") {
216		t.Fatalf("missing final status\n\n%s", out)
217	}
218}
219
220func TestMonitor_MonitorWithPrefix(t *testing.T) {
221	t.Parallel()
222	srv, client, _ := testServer(t, false, nil)
223	defer srv.Shutdown()
224
225	// Create the monitor
226	ui := new(cli.MockUi)
227	mon := newMonitor(ui, client, shortId)
228
229	// Submit a job - this creates a new evaluation we can monitor
230	job := testJob("job1")
231	resp, _, err := client.Jobs().Register(job, nil)
232	if err != nil {
233		t.Fatalf("err: %s", err)
234	}
235
236	// Start monitoring the eval
237	var code int
238	doneCh := make(chan struct{})
239	go func() {
240		defer close(doneCh)
241		code = mon.monitor(resp.EvalID[:13], true)
242	}()
243
244	// Wait for completion
245	select {
246	case <-doneCh:
247	case <-time.After(5 * time.Second):
248		t.Fatalf("eval monitor took too long")
249	}
250
251	// Check the return code. We should get exit code 2 as there
252	// would be a scheduling problem on the test server (no clients).
253	if code != 2 {
254		t.Fatalf("expect exit 2, got: %d", code)
255	}
256
257	// Check the output
258	out := ui.OutputWriter.String()
259	if !strings.Contains(out, resp.EvalID[:8]) {
260		t.Fatalf("missing eval\n\n%s", out)
261	}
262	if strings.Contains(out, resp.EvalID) {
263		t.Fatalf("expected truncated eval id, got: %s", out)
264	}
265	if !strings.Contains(out, "finished with status") {
266		t.Fatalf("missing final status\n\n%s", out)
267	}
268
269	// Fail on identifier with too few characters
270	code = mon.monitor(resp.EvalID[:1], true)
271	if code != 1 {
272		t.Fatalf("expect exit 1, got: %d", code)
273	}
274	if out := ui.ErrorWriter.String(); !strings.Contains(out, "must contain at least two characters.") {
275		t.Fatalf("expected too few characters error, got: %s", out)
276	}
277	ui.ErrorWriter.Reset()
278
279	code = mon.monitor(resp.EvalID[:3], true)
280	if code != 2 {
281		t.Fatalf("expect exit 2, got: %d", code)
282	}
283	if out := ui.OutputWriter.String(); !strings.Contains(out, "Monitoring evaluation") {
284		t.Fatalf("expected evaluation monitoring output, got: %s", out)
285	}
286
287}
288