1package command
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"os"
10	"reflect"
11	"strings"
12	"testing"
13	"time"
14
15	"github.com/hashicorp/nomad/api"
16	"github.com/hashicorp/nomad/helper"
17	"github.com/hashicorp/nomad/helper/flatmap"
18	"github.com/kr/pretty"
19	"github.com/mitchellh/cli"
20	"github.com/stretchr/testify/require"
21)
22
23func TestHelpers_FormatKV(t *testing.T) {
24	t.Parallel()
25	in := []string{"alpha|beta", "charlie|delta", "echo|"}
26	out := formatKV(in)
27
28	expect := "alpha   = beta\n"
29	expect += "charlie = delta\n"
30	expect += "echo    = <none>"
31
32	if out != expect {
33		t.Fatalf("expect: %s, got: %s", expect, out)
34	}
35}
36
37func TestHelpers_FormatList(t *testing.T) {
38	t.Parallel()
39	in := []string{"alpha|beta||delta"}
40	out := formatList(in)
41
42	expect := "alpha  beta  <none>  delta"
43
44	if out != expect {
45		t.Fatalf("expect: %s, got: %s", expect, out)
46	}
47}
48
49func TestHelpers_NodeID(t *testing.T) {
50	t.Parallel()
51	srv, _, _ := testServer(t, false, nil)
52	defer srv.Shutdown()
53
54	meta := Meta{Ui: new(cli.MockUi)}
55	client, err := meta.Client()
56	if err != nil {
57		t.FailNow()
58	}
59
60	// This is because there is no client
61	if _, err := getLocalNodeID(client); err == nil {
62		t.Fatalf("getLocalNodeID() should fail")
63	}
64}
65
66func TestHelpers_LineLimitReader_NoTimeLimit(t *testing.T) {
67	t.Parallel()
68	helloString := `hello
69world
70this
71is
72a
73test`
74
75	noLines := "jskdfhjasdhfjkajkldsfdlsjkahfkjdsafa"
76
77	cases := []struct {
78		Input       string
79		Output      string
80		Lines       int
81		SearchLimit int
82	}{
83		{
84			Input:       helloString,
85			Output:      helloString,
86			Lines:       6,
87			SearchLimit: 1000,
88		},
89		{
90			Input: helloString,
91			Output: `world
92this
93is
94a
95test`,
96			Lines:       5,
97			SearchLimit: 1000,
98		},
99		{
100			Input:       helloString,
101			Output:      `test`,
102			Lines:       1,
103			SearchLimit: 1000,
104		},
105		{
106			Input:       helloString,
107			Output:      "",
108			Lines:       0,
109			SearchLimit: 1000,
110		},
111		{
112			Input:       helloString,
113			Output:      helloString,
114			Lines:       6,
115			SearchLimit: 1, // Exceed the limit
116		},
117		{
118			Input:       noLines,
119			Output:      noLines,
120			Lines:       10,
121			SearchLimit: 1000,
122		},
123		{
124			Input:       noLines,
125			Output:      noLines,
126			Lines:       10,
127			SearchLimit: 2,
128		},
129	}
130
131	for i, c := range cases {
132		in := ioutil.NopCloser(strings.NewReader(c.Input))
133		limit := NewLineLimitReader(in, c.Lines, c.SearchLimit, 0)
134		outBytes, err := ioutil.ReadAll(limit)
135		if err != nil {
136			t.Fatalf("case %d failed: %v", i, err)
137		}
138
139		out := string(outBytes)
140		if out != c.Output {
141			t.Fatalf("case %d: got %q; want %q", i, out, c.Output)
142		}
143	}
144}
145
146type testReadCloser struct {
147	data chan []byte
148}
149
150func (t *testReadCloser) Read(p []byte) (n int, err error) {
151	select {
152	case b, ok := <-t.data:
153		if !ok {
154			return 0, io.EOF
155		}
156
157		return copy(p, b), nil
158	case <-time.After(10 * time.Millisecond):
159		return 0, nil
160	}
161}
162
163func (t *testReadCloser) Close() error {
164	close(t.data)
165	return nil
166}
167
168func TestHelpers_LineLimitReader_TimeLimit(t *testing.T) {
169	t.Parallel()
170	// Create the test reader
171	in := &testReadCloser{data: make(chan []byte)}
172
173	// Set up the reader such that it won't hit the line/buffer limit and could
174	// only terminate if it hits the time limit
175	limit := NewLineLimitReader(in, 1000, 1000, 100*time.Millisecond)
176
177	expected := []byte("hello world")
178
179	errCh := make(chan error)
180	resultCh := make(chan []byte)
181	go func() {
182		defer close(resultCh)
183		defer close(errCh)
184		outBytes, err := ioutil.ReadAll(limit)
185		if err != nil {
186			errCh <- fmt.Errorf("ReadAll failed: %v", err)
187			return
188		}
189		resultCh <- outBytes
190	}()
191
192	// Send the data
193	in.data <- expected
194	in.Close()
195
196	select {
197	case err := <-errCh:
198		if err != nil {
199			t.Fatalf("ReadAll: %v", err)
200		}
201	case outBytes := <-resultCh:
202		if !reflect.DeepEqual(outBytes, expected) {
203			t.Fatalf("got:%s, expected,%s", string(outBytes), string(expected))
204		}
205	case <-time.After(1 * time.Second):
206		t.Fatalf("did not exit by time limit")
207	}
208}
209
210const (
211	job = `job "job1" {
212        type = "service"
213        datacenters = [ "dc1" ]
214        group "group1" {
215                count = 1
216                task "task1" {
217                        driver = "exec"
218                        resources = {}
219                }
220                restart{
221                        attempts = 10
222                        mode = "delay"
223						interval = "15s"
224                }
225        }
226}`
227)
228
229var (
230	expectedApiJob = &api.Job{
231		ID:          helper.StringToPtr("job1"),
232		Name:        helper.StringToPtr("job1"),
233		Type:        helper.StringToPtr("service"),
234		Datacenters: []string{"dc1"},
235		TaskGroups: []*api.TaskGroup{
236			{
237				Name:  helper.StringToPtr("group1"),
238				Count: helper.IntToPtr(1),
239				RestartPolicy: &api.RestartPolicy{
240					Attempts: helper.IntToPtr(10),
241					Interval: helper.TimeToPtr(15 * time.Second),
242					Mode:     helper.StringToPtr("delay"),
243				},
244
245				Tasks: []*api.Task{
246					{
247						Driver:    "exec",
248						Name:      "task1",
249						Resources: &api.Resources{},
250					},
251				},
252			},
253		},
254	}
255)
256
257// Test APIJob with local jobfile
258func TestJobGetter_LocalFile(t *testing.T) {
259	t.Parallel()
260	fh, err := ioutil.TempFile("", "nomad")
261	if err != nil {
262		t.Fatalf("err: %s", err)
263	}
264	defer os.Remove(fh.Name())
265	_, err = fh.WriteString(job)
266	if err != nil {
267		t.Fatalf("err: %s", err)
268	}
269
270	j := &JobGetter{}
271	aj, err := j.ApiJob(fh.Name())
272	if err != nil {
273		t.Fatalf("err: %s", err)
274	}
275
276	if !reflect.DeepEqual(expectedApiJob, aj) {
277		eflat := flatmap.Flatten(expectedApiJob, nil, false)
278		aflat := flatmap.Flatten(aj, nil, false)
279		t.Fatalf("got:\n%v\nwant:\n%v", aflat, eflat)
280	}
281}
282
283// Test StructJob with jobfile from HTTP Server
284func TestJobGetter_HTTPServer(t *testing.T) {
285	t.Parallel()
286	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
287		fmt.Fprintf(w, job)
288	})
289	go http.ListenAndServe("127.0.0.1:12345", nil)
290
291	// Wait until HTTP Server starts certainly
292	time.Sleep(100 * time.Millisecond)
293
294	j := &JobGetter{}
295	aj, err := j.ApiJob("http://127.0.0.1:12345/")
296	if err != nil {
297		t.Fatalf("err: %s", err)
298	}
299	if !reflect.DeepEqual(expectedApiJob, aj) {
300		for _, d := range pretty.Diff(expectedApiJob, aj) {
301			t.Logf(d)
302		}
303		t.Fatalf("Unexpected file")
304	}
305}
306
307func TestPrettyTimeDiff(t *testing.T) {
308	// Grab the time and truncate to the nearest second. This allows our tests
309	// to be deterministic since we don't have to worry about rounding.
310	now := time.Now().Truncate(time.Second)
311
312	test_cases := []struct {
313		t1  time.Time
314		t2  time.Time
315		exp string
316	}{
317		{now, time.Unix(0, 0), ""}, // This is the upgrade path case
318		{now, now.Add(-10 * time.Millisecond), "0s ago"},
319		{now, now.Add(-740 * time.Second), "12m20s ago"},
320		{now, now.Add(-12 * time.Minute), "12m ago"},
321		{now, now.Add(-60 * time.Minute), "1h ago"},
322		{now, now.Add(-80 * time.Minute), "1h20m ago"},
323		{now, now.Add(-6 * time.Hour), "6h ago"},
324		{now.Add(-6 * time.Hour), now, "6h from now"},
325		{now, now.Add(-22165 * time.Second), "6h9m ago"},
326		{now, now.Add(-100 * time.Hour), "4d4h ago"},
327		{now, now.Add(-438000 * time.Minute), "10mo4d ago"},
328		{now, now.Add(-20460 * time.Hour), "2y4mo ago"},
329	}
330	for _, tc := range test_cases {
331		t.Run(tc.exp, func(t *testing.T) {
332			out := prettyTimeDiff(tc.t2, tc.t1)
333			if out != tc.exp {
334				t.Fatalf("expected :%v but got :%v", tc.exp, out)
335			}
336		})
337	}
338
339	var t1 time.Time
340	out := prettyTimeDiff(t1, time.Now())
341
342	if out != "" {
343		t.Fatalf("Expected empty output but got:%v", out)
344	}
345
346}
347
348// TestUiErrorWriter asserts that writer buffers and
349func TestUiErrorWriter(t *testing.T) {
350	t.Parallel()
351
352	var outBuf, errBuf bytes.Buffer
353	ui := &cli.BasicUi{
354		Writer:      &outBuf,
355		ErrorWriter: &errBuf,
356	}
357
358	w := &uiErrorWriter{ui: ui}
359
360	inputs := []string{
361		"some line\n",
362		"multiple\nlines\r\nhere",
363		" with  followup\nand",
364		" more lines ",
365		" without new line ",
366		"until here\nand then",
367		"some more",
368	}
369
370	partialAcc := ""
371	for _, in := range inputs {
372		n, err := w.Write([]byte(in))
373		require.NoError(t, err)
374		require.Equal(t, len(in), n)
375
376		// assert that writer emits partial result until last new line
377		partialAcc += strings.ReplaceAll(in, "\r\n", "\n")
378		lastNL := strings.LastIndex(partialAcc, "\n")
379		require.Equal(t, partialAcc[:lastNL+1], errBuf.String())
380	}
381
382	require.Empty(t, outBuf.String())
383
384	// note that the \r\n got replaced by \n
385	expectedErr := "some line\nmultiple\nlines\nhere with  followup\nand more lines  without new line until here\n"
386	require.Equal(t, expectedErr, errBuf.String())
387
388	// close emits the final line
389	err := w.Close()
390	require.NoError(t, err)
391
392	expectedErr += "and thensome more\n"
393	require.Equal(t, expectedErr, errBuf.String())
394}
395