1package kingpin
2
3import (
4	"sort"
5	"strings"
6
7	"github.com/stretchr/testify/assert"
8
9	"testing"
10)
11
12func parseAndExecute(app *Application, context *ParseContext) (string, error) {
13	if err := parse(context, app); err != nil {
14		return "", err
15	}
16
17	selected, err := app.setValues(context)
18	if err != nil {
19		return "", err
20	}
21
22	return app.execute(context, selected)
23}
24
25func complete(t *testing.T, app *Application, args ...string) []string {
26	context, err := app.ParseContext(args)
27	assert.NoError(t, err)
28	if err != nil {
29		return nil
30	}
31
32	completions := app.completionOptions(context)
33	sort.Strings(completions)
34
35	return completions
36}
37
38func TestNestedCommands(t *testing.T) {
39	app := New("app", "")
40	sub1 := app.Command("sub1", "")
41	sub1.Flag("sub1", "")
42	subsub1 := sub1.Command("sub1sub1", "")
43	subsub1.Command("sub1sub1end", "")
44
45	sub2 := app.Command("sub2", "")
46	sub2.Flag("sub2", "")
47	sub2.Command("sub2sub1", "")
48
49	context := tokenize([]string{"sub1", "sub1sub1", "sub1sub1end"}, false)
50	selected, err := parseAndExecute(app, context)
51	assert.NoError(t, err)
52	assert.True(t, context.EOL())
53	assert.Equal(t, "sub1 sub1sub1 sub1sub1end", selected)
54}
55
56func TestNestedCommandsWithArgs(t *testing.T) {
57	app := New("app", "")
58	cmd := app.Command("a", "").Command("b", "")
59	a := cmd.Arg("a", "").String()
60	b := cmd.Arg("b", "").String()
61	context := tokenize([]string{"a", "b", "c", "d"}, false)
62	selected, err := parseAndExecute(app, context)
63	assert.NoError(t, err)
64	assert.True(t, context.EOL())
65	assert.Equal(t, "a b", selected)
66	assert.Equal(t, "c", *a)
67	assert.Equal(t, "d", *b)
68}
69
70func TestNestedCommandsWithFlags(t *testing.T) {
71	app := New("app", "")
72	cmd := app.Command("a", "").Command("b", "")
73	a := cmd.Flag("aaa", "").Short('a').String()
74	b := cmd.Flag("bbb", "").Short('b').String()
75	err := app.init()
76	assert.NoError(t, err)
77	context := tokenize(strings.Split("a b --aaa x -b x", " "), false)
78	selected, err := parseAndExecute(app, context)
79	assert.NoError(t, err)
80	assert.True(t, context.EOL())
81	assert.Equal(t, "a b", selected)
82	assert.Equal(t, "x", *a)
83	assert.Equal(t, "x", *b)
84}
85
86func TestNestedCommandWithMergedFlags(t *testing.T) {
87	app := New("app", "")
88	cmd0 := app.Command("a", "")
89	cmd0f0 := cmd0.Flag("aflag", "").Bool()
90	// cmd1 := app.Command("b", "")
91	// cmd1f0 := cmd0.Flag("bflag", "").Bool()
92	cmd00 := cmd0.Command("aa", "")
93	cmd00f0 := cmd00.Flag("aaflag", "").Bool()
94	err := app.init()
95	assert.NoError(t, err)
96	context := tokenize(strings.Split("a aa --aflag --aaflag", " "), false)
97	selected, err := parseAndExecute(app, context)
98	assert.NoError(t, err)
99	assert.True(t, *cmd0f0)
100	assert.True(t, *cmd00f0)
101	assert.Equal(t, "a aa", selected)
102}
103
104func TestNestedCommandWithDuplicateFlagErrors(t *testing.T) {
105	app := New("app", "")
106	app.Flag("test", "").Bool()
107	app.Command("cmd0", "").Flag("test", "").Bool()
108	err := app.init()
109	assert.Error(t, err)
110}
111
112func TestNestedCommandWithArgAndMergedFlags(t *testing.T) {
113	app := New("app", "")
114	cmd0 := app.Command("a", "")
115	cmd0f0 := cmd0.Flag("aflag", "").Bool()
116	// cmd1 := app.Command("b", "")
117	// cmd1f0 := cmd0.Flag("bflag", "").Bool()
118	cmd00 := cmd0.Command("aa", "")
119	cmd00a0 := cmd00.Arg("arg", "").String()
120	cmd00f0 := cmd00.Flag("aaflag", "").Bool()
121	err := app.init()
122	assert.NoError(t, err)
123	context := tokenize(strings.Split("a aa hello --aflag --aaflag", " "), false)
124	selected, err := parseAndExecute(app, context)
125	assert.NoError(t, err)
126	assert.True(t, *cmd0f0)
127	assert.True(t, *cmd00f0)
128	assert.Equal(t, "a aa", selected)
129	assert.Equal(t, "hello", *cmd00a0)
130}
131
132func TestDefaultSubcommandEOL(t *testing.T) {
133	app := newTestApp()
134	c0 := app.Command("c0", "").Default()
135	c0.Command("c01", "").Default()
136	c0.Command("c02", "")
137
138	cmd, err := app.Parse([]string{"c0"})
139	assert.NoError(t, err)
140	assert.Equal(t, "c0 c01", cmd)
141}
142
143func TestDefaultSubcommandWithArg(t *testing.T) {
144	app := newTestApp()
145	c0 := app.Command("c0", "").Default()
146	c01 := c0.Command("c01", "").Default()
147	c012 := c01.Command("c012", "").Default()
148	a0 := c012.Arg("a0", "").String()
149	c0.Command("c02", "")
150
151	cmd, err := app.Parse([]string{"c0", "hello"})
152	assert.NoError(t, err)
153	assert.Equal(t, "c0 c01 c012", cmd)
154	assert.Equal(t, "hello", *a0)
155}
156
157func TestDefaultSubcommandWithFlags(t *testing.T) {
158	app := newTestApp()
159	c0 := app.Command("c0", "").Default()
160	_ = c0.Flag("f0", "").Int()
161	c0c1 := c0.Command("c1", "").Default()
162	c0c1f1 := c0c1.Flag("f1", "").Int()
163	selected, err := app.Parse([]string{"--f1=2"})
164	assert.NoError(t, err)
165	assert.Equal(t, "c0 c1", selected)
166	assert.Equal(t, 2, *c0c1f1)
167	_, err = app.Parse([]string{"--f2"})
168	assert.Error(t, err)
169}
170
171func TestMultipleDefaultCommands(t *testing.T) {
172	app := newTestApp()
173	app.Command("c0", "").Default()
174	app.Command("c1", "").Default()
175	_, err := app.Parse([]string{})
176	assert.Error(t, err)
177}
178
179func TestAliasedCommand(t *testing.T) {
180	app := newTestApp()
181	app.Command("one", "").Alias("two")
182	selected, _ := app.Parse([]string{"one"})
183	assert.Equal(t, "one", selected)
184	selected, _ = app.Parse([]string{"two"})
185	assert.Equal(t, "one", selected)
186	// 2 due to "help" and "one"
187	assert.Equal(t, 2, len(app.Model().FlattenedCommands()))
188}
189
190func TestDuplicateAlias(t *testing.T) {
191	app := newTestApp()
192	app.Command("one", "")
193	app.Command("two", "").Alias("one")
194	_, err := app.Parse([]string{"one"})
195	assert.Error(t, err)
196}
197
198func TestFlagCompletion(t *testing.T) {
199	app := newTestApp()
200	app.Command("one", "")
201	two := app.Command("two", "")
202	two.Flag("flag-1", "")
203	two.Flag("flag-2", "").HintOptions("opt1", "opt2", "opt3")
204	two.Flag("flag-3", "")
205
206	cases := []struct {
207		target              cmdMixin
208		flagName            string
209		flagValue           string
210		expectedFlagMatch   bool
211		expectedOptionMatch bool
212		expectedFlags       []string
213	}{
214		{
215			// Test top level flags
216			target:              app.cmdMixin,
217			flagName:            "",
218			flagValue:           "",
219			expectedFlagMatch:   false,
220			expectedOptionMatch: false,
221			expectedFlags:       []string{"--help"},
222		},
223		{
224			// Test no flag passed
225			target:              two.cmdMixin,
226			flagName:            "",
227			flagValue:           "",
228			expectedFlagMatch:   false,
229			expectedOptionMatch: false,
230			expectedFlags:       []string{"--flag-1", "--flag-2", "--flag-3"},
231		},
232		{
233			// Test an incomplete flag. Should still give all options as if the flag wasn't given at all.
234			target:              two.cmdMixin,
235			flagName:            "flag-",
236			flagValue:           "",
237			expectedFlagMatch:   false,
238			expectedOptionMatch: false,
239			expectedFlags:       []string{"--flag-1", "--flag-2", "--flag-3"},
240		},
241		{
242			// Test with a complete flag. Should show available choices for the flag
243			// This flag has no options. No options should be produced.
244			// Should also report an option was matched
245			target:              two.cmdMixin,
246			flagName:            "flag-1",
247			flagValue:           "",
248			expectedFlagMatch:   true,
249			expectedOptionMatch: true,
250			expectedFlags:       []string(nil),
251		},
252		{
253			// Test with a complete flag. Should show available choices for the flag
254			target:              two.cmdMixin,
255			flagName:            "flag-2",
256			flagValue:           "",
257			expectedFlagMatch:   true,
258			expectedOptionMatch: false,
259			expectedFlags:       []string{"opt1", "opt2", "opt3"},
260		},
261		{
262			// Test with a complete flag and complete option for that flag.
263			target:              two.cmdMixin,
264			flagName:            "flag-2",
265			flagValue:           "opt1",
266			expectedFlagMatch:   true,
267			expectedOptionMatch: true,
268			expectedFlags:       []string{"opt1", "opt2", "opt3"},
269		},
270	}
271
272	for i, c := range cases {
273		choices, flagMatch, optionMatch := c.target.FlagCompletion(c.flagName, c.flagValue)
274		assert.Equal(t, c.expectedFlags, choices, "Test case %d: expectedFlags != actual flags", i+1)
275		assert.Equal(t, c.expectedFlagMatch, flagMatch, "Test case %d: expectedFlagMatch != flagMatch", i+1)
276		assert.Equal(t, c.expectedOptionMatch, optionMatch, "Test case %d: expectedOptionMatch != optionMatch", i+1)
277	}
278
279}
280
281func TestCmdCompletion(t *testing.T) {
282	app := newTestApp()
283	app.Command("one", "")
284	two := app.Command("two", "")
285	two.Command("sub1", "")
286	two.Command("sub2", "")
287
288	assert.Equal(t, []string{"help", "one", "two"}, complete(t, app))
289	assert.Equal(t, []string{"sub1", "sub2"}, complete(t, app, "two"))
290}
291
292func TestHiddenCmdCompletion(t *testing.T) {
293	app := newTestApp()
294
295	// top level visible & hidden cmds, with no sub-cmds
296	app.Command("visible1", "")
297	app.Command("hidden1", "").Hidden()
298
299	// visible cmd with visible & hidden sub-cmds
300	visible2 := app.Command("visible2", "")
301	visible2.Command("visible2-visible", "")
302	visible2.Command("visible2-hidden", "").Hidden()
303
304	// hidden cmd with visible & hidden sub-cmds
305	hidden2 := app.Command("hidden2", "").Hidden()
306	hidden2.Command("hidden2-visible", "")
307	hidden2.Command("hidden2-hidden", "").Hidden()
308
309	// Only top level visible cmds should show
310	assert.Equal(t, []string{"help", "visible1", "visible2"}, complete(t, app))
311
312	// Only visible sub-cmds should show
313	assert.Equal(t, []string{"visible2-visible"}, complete(t, app, "visible2"))
314
315	// Hidden commands should still complete visible sub-cmds
316	assert.Equal(t, []string{"hidden2-visible"}, complete(t, app, "hidden2"))
317}
318
319func TestDefaultCmdCompletion(t *testing.T) {
320	app := newTestApp()
321
322	cmd1 := app.Command("cmd1", "")
323
324	cmd1Sub1 := cmd1.Command("cmd1-sub1", "")
325	cmd1Sub1.Arg("cmd1-sub1-arg1", "").HintOptions("cmd1-arg1").String()
326
327	cmd2 := app.Command("cmd2", "").Default()
328
329	cmd2.Command("cmd2-sub1", "")
330
331	cmd2Sub2 := cmd2.Command("cmd2-sub2", "").Default()
332
333	cmd2Sub2Sub1 := cmd2Sub2.Command("cmd2-sub2-sub1", "").Default()
334	cmd2Sub2Sub1.Arg("cmd2-sub2-sub1-arg1", "").HintOptions("cmd2-sub2-sub1-arg1").String()
335	cmd2Sub2Sub1.Arg("cmd2-sub2-sub1-arg2", "").HintOptions("cmd2-sub2-sub1-arg2").String()
336
337	// Without args, should get:
338	//   - root cmds (including implicit "help")
339	//   - thread of default cmds
340	//   - first arg hints for the final default cmd
341	assert.Equal(t, []string{"cmd1", "cmd2", "cmd2-sub1", "cmd2-sub2", "cmd2-sub2-sub1", "cmd2-sub2-sub1-arg1", "help"}, complete(t, app))
342
343	// With a non-default cmd already listed, should get:
344	//   - sub cmds of that arg
345	assert.Equal(t, []string{"cmd1-sub1"}, complete(t, app, "cmd1"))
346
347	// With an explicit default cmd listed, should get:
348	//   - default child-cmds
349	//   - first arg hints for the final default cmd
350	assert.Equal(t, []string{"cmd2-sub1", "cmd2-sub2", "cmd2-sub2-sub1", "cmd2-sub2-sub1-arg1"}, complete(t, app, "cmd2"))
351
352	// Args should be completed when all preceding cmds are explicit, and when
353	// any of them are implicit (not listed). Check this by trying all possible
354	// combinations of choosing/excluding the three levels of cmds. This tests
355	// root-level default, middle default, and end default.
356	for i := 0; i < 8; i++ {
357		var cmdline []string
358
359		if i&1 != 0 {
360			cmdline = append(cmdline, "cmd2")
361		}
362		if i&2 != 0 {
363			cmdline = append(cmdline, "cmd2-sub2")
364		}
365		if i&4 != 0 {
366			cmdline = append(cmdline, "cmd2-sub2-sub1")
367		}
368
369		assert.Contains(t, complete(t, app, cmdline...), "cmd2-sub2-sub1-arg1", "with cmdline: %v", cmdline)
370	}
371
372	// With both args of a default sub cmd, should get no completions
373	assert.Empty(t, complete(t, app, "arg1", "arg2"))
374}
375