1 package sink
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "testing"
8
9 "github.com/peterbourgon/ff/v3"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "edge-infra.dev/pkg/lib/cli/rags"
14 )
15
16 type testExtension struct {
17 name string
18 finishedBeforeRun bool
19 finishedAfterRun bool
20 stringFlag string
21 boolFlag bool
22 }
23
24 func (ext *testExtension) RegisterFlags(rs *rags.RagSet) {
25 if ext.name == "" {
26 ext.name = "test-extension"
27 }
28 rs.StringVar(&ext.stringFlag, ext.name+"-string-flag", "default string value", "string usage message")
29 rs.BoolVar(&ext.boolFlag, ext.name+"-bool-flag", false, "bool usage message")
30 }
31
32 type testCtxKey = struct{}
33
34 func (ext *testExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) {
35 ext.finishedBeforeRun = true
36 return context.WithValue(ctx, &testCtxKey{}, "value"), r, nil
37 }
38
39 func (ext *testExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) {
40 ext.finishedAfterRun = true
41 return ctx, r, nil
42 }
43
44 type faultyExtension struct{}
45
46 func (ext *faultyExtension) RegisterFlags(_ *rags.RagSet) {}
47
48 func (ext *faultyExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) {
49 return ctx, r, fmt.Errorf("Faulty extension BeforeRun")
50 }
51
52 func (ext *faultyExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) {
53 return ctx, r, fmt.Errorf("Faulty extension AfterRun")
54 }
55
56 func TestNames(t *testing.T) {
57 cmd := newTestCmd()
58
59
60 require.NoError(t, cmd.compute())
61
62 assert.Equal(t, cmd.Use, cmd.Name())
63 assert.Equal(t, cmd.Use, cmd.LongName())
64 for _, scmd := range cmd.Commands {
65 assert.Equal(t, scmd.Use, scmd.Name())
66 assert.Equal(t, scmd.parent.LongName()+" "+scmd.Use, scmd.LongName())
67 }
68
69 t.Run("Name", func(t *testing.T) {
70 tcs := map[string]struct {
71 use string
72 exp string
73 }{
74 "simple": {"root", "root"},
75 "options and args": {"foo [flags] <argument> <argument>", "foo"},
76 "trailing space": {"foo ", "foo"},
77 }
78
79 for name, tc := range tcs {
80 t.Run(name, func(t *testing.T) {
81 c := &Command{Use: tc.use}
82 assert.Equal(t, tc.exp, c.Name())
83 })
84 }
85 })
86 }
87
88 func TestUseLine(t *testing.T) {
89 var (
90 root = &Command{Use: "root [flags]"}
91 foo = &Command{Use: "foo [command]"}
92 get = &Command{Use: "get [flags] <object>"}
93 del = &Command{Use: "delete [flags] <object>"}
94 )
95 root.Commands = []*Command{foo}
96 foo.Commands = []*Command{get, del}
97
98
99 require.NoError(t, root.compute())
100
101 tcs := []struct {
102 cmd *Command
103 exp string
104 }{
105 {root, root.Use},
106 {foo, "root " + foo.Use},
107 {get, "root foo " + get.Use},
108 {del, "root foo " + del.Use},
109 }
110
111 for _, tc := range tcs {
112 assert.Equal(t, tc.exp, useline(tc.cmd))
113 }
114 }
115
116 func TestExtensions(t *testing.T) {
117
118
119 t.Run("extension errors", func(t *testing.T) {
120 cmd := &Command{Use: "cli", Extensions: []Extension{&faultyExtension{}}}
121 ctx := context.Background()
122 r := newRun(cmd)
123 var err error
124 ctx, r, err = cmd.beforeRun(ctx, r)
125 assert.Error(t, err)
126 _, _, err = cmd.afterRun(ctx, r)
127 assert.Error(t, err)
128 })
129 t.Run("working extension", func(t *testing.T) {
130 ext := &testExtension{}
131 cmd := &Command{Use: "cli", Extensions: []Extension{ext}}
132 ctx := context.Background()
133 r := newRun(cmd)
134 var err error
135 ctx, r, err = cmd.beforeRun(ctx, r)
136 assert.NoError(t, err)
137 assert.True(t, ext.finishedBeforeRun)
138 assert.Equal(t, "value", ctx.Value(&testCtxKey{}))
139 ctx, _, err = cmd.afterRun(ctx, r)
140 assert.NoError(t, err)
141 assert.True(t, ext.finishedAfterRun)
142 assert.Equal(t, "value", ctx.Value(&testCtxKey{}))
143 })
144 }
145
146 func TestDefaultUsageFunc(t *testing.T) {
147 tcs := map[string]*Command{
148 "short usage": {
149 Use: "foo",
150 Short: "Foo the bar",
151 },
152 "long usage": {
153 Use: "foo",
154 Long: "This is a paragraph about Fooing The Bar",
155 },
156 "long usage overrides short": {
157 Use: "foo",
158 Short: "Foo the bar",
159 Long: "This is a paragraph about Fooing The Bar",
160 },
161 "subcommands": {
162 Use: "foo",
163 Short: "Foo the bar",
164 Long: "This is a paragraph about Fooing The Bar",
165 Commands: []*Command{{Use: "bar"}},
166 },
167 "flags": {
168 Use: "foo",
169 Short: "Foo the bar",
170 Long: "This is a paragraph about Fooing The Bar",
171 Commands: []*Command{{Use: "bar"}},
172 Flags: []*rags.Rag{
173 {Name: "you-see-me", Value: &rags.Bool{}},
174 },
175 },
176 "no help text": {
177 Use: "foo",
178 },
179 }
180
181 for name, c := range tcs {
182 name := name
183 c := c
184 t.Run(name, func(t *testing.T) {
185 t.Parallel()
186
187
188 require.NoError(t, c.compute())
189
190 usage := defaultUsageFn(c)
191 lines := strings.Split(usage, "\n")
192
193 t.Log("usage", "\n"+usage)
194 for i, l := range lines {
195 t.Log(i, l)
196 }
197
198 switch {
199 case c.Long != "":
200 assert.Equal(t, c.Long, lines[0])
201 case c.Short != "":
202 assert.Equal(t, c.Short, lines[0])
203 default:
204 assert.Equal(t, "Usage:", lines[0])
205 }
206
207 if len(c.Commands) == 0 {
208 assert.False(t, strings.Contains(usage, "\nCommands:"))
209 } else {
210 assert.True(t, strings.Contains(usage, "\nCommands:"))
211 }
212
213 if len(c.Flags) == 0 {
214 assert.False(t, strings.Contains(usage, "\nFlags:"))
215 } else {
216 assert.True(t, strings.Contains(usage, "\nFlags:"))
217 }
218
219 assert.True(t, lines[len(lines)-1] == "")
220 assert.True(t, lines[len(lines)-2] != "")
221 })
222 }
223 }
224
225 func TestParse(t *testing.T) {
226 t.Parallel()
227
228 tcs := map[string]struct {
229 cmdIdx []int
230 args []string
231 }{
232 "root": {[]int{}, []string{}},
233 "root foo": {[]int{0}, []string{"foo"}},
234 "root foo view": {[]int{0, 0}, []string{"foo", "view"}},
235 "root foo delete": {[]int{0, 1}, []string{"foo", "delete"}},
236 "root bar": {[]int{1}, []string{"bar"}},
237 "root bar baz": {[]int{1, 4}, []string{"bar", "baz"}},
238 "root bar baz delete": {[]int{1, 4, 1}, []string{"bar", "baz", "delete"}},
239 }
240
241 for name, tc := range tcs {
242 name := name
243 tc := tc
244 t.Run(name, func(t *testing.T) {
245 t.Parallel()
246
247 cmd := newTestCmd()
248 assert.NoError(t, cmd.Parse(tc.args))
249 assert.ElementsMatch(t, cmd.args, tc.args)
250 t.Log("args", cmd.args)
251
252 expCmd := cmd
253 cmds := expCmd.Commands
254 for _, idx := range tc.cmdIdx {
255 for i, s := range cmds {
256 if i == idx {
257 assert.NotNil(t, s.selected)
258 } else {
259 assert.Nil(t, s.selected)
260 }
261 }
262 expCmd = cmds[idx]
263 cmds = expCmd.Commands
264 }
265 })
266 }
267 }
268
269 func TestCommand_compute(t *testing.T) {
270 t.Parallel()
271 cmd := newTestCmd()
272
273
274 require.NoError(t, cmd.compute())
275
276 testComputedCmd(t, cmd)
277
278 t.Run("parent as child", func(t *testing.T) {
279 t.Parallel()
280 cmd := newTestCmd()
281 cmd.Commands = append(cmd.Commands, cmd)
282 assert.Error(t, cmd.compute())
283 })
284
285 t.Run("duplicate children", func(t *testing.T) {
286 t.Parallel()
287 cmd := newTestCmd()
288 cmd.Commands = append(cmd.Commands, cmd.Commands[0])
289 assert.Error(t, cmd.compute())
290 })
291 }
292
293 func testComputedCmd(t *testing.T, cmd *Command) {
294 t.Helper()
295 a := assert.New(t)
296
297 if cmd.HasParent() {
298 p := cmd.Parent()
299
300 switch {
301 case len(p.AllParsingOptions()) > 0:
302 exp := append(p.AllParsingOptions(), cmd.Options...)
303 actual := cmd.AllParsingOptions()
304 a.Len(actual, len(exp))
305 default:
306 a.Len(cmd.AllParsingOptions(), len(cmd.Options))
307 }
308
309 a.Equal(p.LongName()+" "+cmd.Name(), cmd.LongName())
310 } else {
311 a.Equal(cmd.Name(), cmd.LongName())
312 a.ElementsMatch(cmd.Options, cmd.AllParsingOptions())
313 }
314
315 a.NotNil(cmd.Exec)
316
317 for _, scmd := range cmd.Commands {
318 a.Same(cmd, scmd.parent)
319 testComputedCmd(t, scmd)
320 }
321 }
322
323
324 func newTestCmd() *Command {
325 return &Command{
326 Use: "root",
327 Commands: []*Command{
328 {
329 Use: "foo",
330 Extensions: []Extension{&testExtension{}},
331 Commands: []*Command{
332 {Use: "view"},
333 {Use: "delete"},
334 {Use: "list"},
335 {Use: "create"},
336 },
337 },
338 {
339 Use: "bar",
340 Options: []ff.Option{ff.WithEnvVarNoPrefix()},
341 Commands: []*Command{
342 {Use: "view"},
343 {Use: "delete"},
344 {Use: "list"},
345 {Use: "create"},
346 {Use: "baz",
347 Commands: []*Command{
348 {Use: "view", Options: []ff.Option{ff.WithIgnoreUndefined(true)}},
349 {Use: "delete"},
350 {Use: "list"},
351 {Use: "create"},
352 },
353 },
354 },
355 },
356 },
357 }
358 }
359
View as plain text