1 package rags
2
3 import (
4 "bytes"
5 "flag"
6 "fmt"
7 "strconv"
8 "strings"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 )
13
14 type tType struct {
15 person string
16 age int
17 }
18
19 func (t *tType) Set(s string) error {
20 v := strings.Split(s, ":")
21 age, err := strconv.Atoi(v[1])
22 if err != nil {
23 return err
24 }
25 *t = tType{
26 person: v[0],
27 age: age,
28 }
29 return nil
30 }
31
32 func (t *tType) String() string {
33 if t.person == "" || t.age == 0 {
34 return ""
35 }
36 return fmt.Sprintf("%s is %d years old", t.person, t.age)
37 }
38 func (t *tType) Get() any { return *t }
39
40
41 type tTypedValue struct{ *tType }
42
43 func (t *tTypedValue) Get() any { return *t.tType }
44 func (t *tTypedValue) Type() string { return "tvalue" }
45
46
47 func TestRagSet_FlagSet_Binding(t *testing.T) {
48 t.Parallel()
49
50 flags := []*Rag{
51 {
52 Name: "help",
53 Short: "h",
54 Usage: "display help information for a command",
55 Value: &Bool{},
56 },
57 {
58 Name: "log-level",
59 Short: "v",
60 Usage: "control verbosity. a higher number means chattier logs",
61 Value: &Bool{},
62 },
63 {
64 Name: "log-json",
65 Usage: "emit json logs",
66 Value: &Bool{},
67 },
68 {
69 Name: "foo",
70 Usage: "i foo the bar. see --bar",
71 Value: &String{},
72 },
73 {
74 Name: "bar",
75 Usage: "dont see me",
76 Value: &Int{},
77 },
78 {
79 Name: "def-value",
80 Short: "d",
81 Value: NewValueDefault(new(string), "foo"),
82 },
83 {
84 Name: "def-value-ptr",
85 Value: NewValueDefault(new(int), 100),
86 },
87 {
88 Name: "custom-value",
89 Value: &tType{},
90 },
91 }
92
93 testFlagSetBinding := func(t *testing.T, rs *RagSet, flags ...*Rag) {
94 t.Helper()
95
96 fs := rs.FlagSet()
97
98 var expGoFlags = []string{}
99
100 for _, f := range flags {
101 for _, n := range f.Names() {
102 expGoFlags = append(expGoFlags, n)
103
104 gof := fs.Lookup(n)
105 assert.NotNil(t, gof)
106
107 fval := f.Value
108 assert.Same(t, fval, gof.Value)
109 assert.Implements(t, (*flag.Getter)(nil), gof.Value)
110 getter, _ := gof.Value.(flag.Getter)
111 assert.Equal(t, fval.Get(), getter.Get())
112 }
113 }
114
115 actualCount := 0
116 fs.VisitAll(func(f *flag.Flag) {
117 actualCount++
118 found := false
119 for _, name := range expGoFlags {
120 if name == f.Name {
121 found = true
122 break
123 }
124 }
125 assert.True(t, found, "%s wasnt in expected flags %s", f.Name, expGoFlags)
126 })
127 assert.Equal(t, len(expGoFlags), actualCount)
128 }
129
130 t.Run("New_Add_Flags", func(t *testing.T) {
131 rs := New("test-add-on-create", flag.ContinueOnError, flags...)
132 assert.Len(t, rs.rags, 8)
133 testFlagSetBinding(t, rs, flags...)
134 })
135
136 t.Run("Add", func(t *testing.T) {
137 rs := New("test-add", flag.ContinueOnError)
138 rs.Add(flags...)
139 assert.Len(t, rs.rags, 8)
140 testFlagSetBinding(t, rs, flags...)
141 })
142 }
143
144 func TestRagUsageLine(t *testing.T) {
145 var (
146 valDst = &tType{person: "tom", age: 100}
147 typedValDst = &tTypedValue{&tType{person: "betty", age: 24}}
148 )
149
150 tcs := map[string]struct {
151 flag *Rag
152 exp string
153 }{
154 "categorized bool": {
155 &Rag{
156 Name: "flag-name",
157 Category: "special bools",
158 Usage: "this is my special bool flag, note the category",
159 Value: &Bool{},
160 },
161 "\t \t--flag-name\tthis is my special bool flag, note the category\t",
162 },
163 "categorized required bool": {
164 &Rag{
165 Name: "flag-name",
166 Category: "special bools",
167 Required: true,
168 Usage: "this is my special bool flag, note the category",
169 Value: &Bool{},
170 },
171 "\t \t--flag-name\t[required] this is my special bool flag, note the category\t",
172 },
173 "categorized required bool with shorthand": {
174 &Rag{
175 Name: "flag-name",
176 Category: "special bools",
177 Required: true,
178 Short: "f",
179 Usage: "this is my special bool flag, note the category",
180 Value: &Bool{},
181 },
182 "\t-f,\t--flag-name\t[required] this is my special bool flag, note the category\t",
183 },
184 "default true bool": {
185 &Rag{
186 Name: "flag-name",
187 Category: "special bools",
188 Required: true,
189 Usage: "this is my special bool flag, note the category",
190 Value: &Bool{Default: true},
191 },
192 "\t \t--flag-name\t[required] this is my special bool flag, note the category [default: true]\t",
193 },
194 "categorized required var": {
195
196 &Rag{
197 Name: "flag-name",
198 Category: "special vars",
199 Required: true,
200 Usage: "special var flag with category",
201 Value: valDst,
202 },
203 "\t \t--flag-name value\t[required] special var flag with category [default: tom is 100 years old]\t",
204 },
205 "categorized typed var with shorthand": {
206
207 &Rag{
208 Name: "flag-name",
209 Category: "special vars",
210 Usage: "special var flag with category",
211 Value: typedValDst,
212 Short: "f",
213 },
214 "\t-f,\t--flag-name tvalue\tspecial var flag with category [default: betty is 24 years old]\t",
215 },
216
217
218
219 }
220
221 for name, tc := range tcs {
222 tc := tc
223 name := name
224 t.Run(name, func(t *testing.T) {
225 t.Parallel()
226 rs := New(name, flag.ContinueOnError, tc.flag)
227 actual := usageLine(tc.flag, rs.FlagSet())
228 assert.Equal(t, tc.exp, actual)
229 })
230 }
231 }
232
233 func TestHasZeroDefault(t *testing.T) {
234 var (
235 emptyVar = &tTypedValue{new(tType)}
236 customVar = &tTypedValue{&tType{person: "tom", age: 100}}
237 )
238
239 tcs := map[string]struct {
240 v flag.Getter
241 exp bool
242 }{
243 "empty string": {&String{}, true},
244 "string": {&String{Default: "foo"}, false},
245 "uint 0": {&Uint{}, true},
246 "uint": {&Uint{Default: 10}, false},
247 "uint8 0": {&Uint8{}, true},
248 "uint8": {&Uint8{Default: 10}, false},
249 "uint16 0": {&Uint16{}, true},
250 "uint16": {&Uint16{Default: 10}, false},
251 "uint32 0": {&Uint32{}, true},
252 "uint32": {&Uint32{Default: 10}, false},
253 "uint64 0": {&Uint64{}, true},
254 "uint64": {&Uint64{Default: 10}, false},
255 "int 0": {&Int{}, true},
256 "int": {&Int{Default: 10}, false},
257 "int8 0": {&Int8{}, true},
258 "int8": {&Int8{Default: 10}, false},
259 "int16 0": {&Int16{}, true},
260 "int16": {&Int16{Default: 10}, false},
261 "int32 0": {&Int32{}, true},
262 "int32": {&Int32{Default: 10}, false},
263 "int64 0": {&Int64{}, true},
264 "int64": {&Int64{Default: 10}, false},
265 "float32 0": {&Float32{}, true},
266 "float32": {&Float32{Default: 10}, false},
267 "float64 0": {&Float64{}, true},
268 "float64": {&Float64{Default: 10}, false},
269 "complex64 0": {&Complex64{}, true},
270 "complex64": {&Complex64{Default: 10}, false},
271 "complex128 0": {&Complex128{}, true},
272 "complex128": {&Complex128{Default: complex(3, -5)}, false},
273 "var empty": {emptyVar, true},
274 "var": {customVar, false},
275 }
276
277 for name, tc := range tcs {
278 t.Run(name, func(t *testing.T) {
279 rs := New(name, flag.ContinueOnError)
280 r := &Rag{Name: strings.ReplaceAll(name, " ", "-"), Value: tc.v}
281 assert.NotPanics(t, func() { rs.Add(r) })
282 f := rs.FlagSet().Lookup(r.Name)
283 assert.Equal(t, tc.exp, hasZeroValueDefault(f))
284 })
285 }
286 }
287
288 func TestPanicsWithBigShort(t *testing.T) {
289 var (
290 bigShort string
291 )
292 lsRag := &Rag{
293 Name: "big-short",
294 Value: &String{Var: &bigShort},
295 Short: "bs",
296 Required: true,
297 }
298
299 assert.Panics(t, func() { New("bigShort Set", flag.ContinueOnError, lsRag) })
300 }
301
302 func TestUnquoteUsage(t *testing.T) {
303 var (
304 strDst string
305 int64Dst int64
306 intDst int
307 float64Dst float64
308 boolDst bool
309 untypedDst = &tType{}
310 typedDst = &tTypedValue{new(tType)}
311 )
312
313 tcs := map[string]struct {
314 usage string
315 val flag.Getter
316 expName string
317 expUsage string
318 }{
319 "backticked name": {
320 "load configuration from `file`",
321 &String{Default: "~/.config/file", Var: &strDst},
322 "file", "load configuration from file",
323 },
324 "int64 is int": {
325 "weird verbosity level",
326 &Int64{Default: 100, Var: &int64Dst},
327 "int", "weird verbosity level",
328 },
329 "int is int": {
330 "weird verbosity level",
331 &Int{Default: 100, Var: &intDst},
332 "int", "weird verbosity level",
333 },
334 "float64 is float": {
335 "weird verbosity level",
336 &Float64{Default: 100, Var: &float64Dst},
337 "float", "weird verbosity level",
338 },
339 "bool is empty": {
340 "weird verbosity toggle",
341 &Bool{Var: &boolDst},
342 "", "weird verbosity toggle",
343 },
344 "backtick takes precedence": {
345 "weird verbosity `level`",
346 &Int64{Default: 100, Var: &int64Dst},
347 "level", "weird verbosity level",
348 },
349 "implements TypedValue": {
350 "weird verbosity level",
351 typedDst,
352 "tvalue", "weird verbosity level",
353 },
354 "fallback to value": {
355 "weird verbosity level",
356 untypedDst,
357 "value", "weird verbosity level",
358 },
359 "single backtick is skipped": {
360 "weird verbosity `level",
361 &Int64{Default: 100, Var: &int64Dst},
362 "int", "weird verbosity `level",
363 },
364 }
365
366 for name, tc := range tcs {
367 t.Run(name, func(t *testing.T) {
368 f := &flag.Flag{Usage: tc.usage, Value: tc.val}
369 varname, usage := UnquoteUsage(f)
370 assert.Equal(t, tc.expName, varname)
371 assert.Equal(t, tc.expUsage, usage)
372 })
373 }
374 }
375
376 func TestUsage(t *testing.T) {
377 tcs := map[string]struct {
378 rs func() *RagSet
379 exp string
380 }{
381 "simple": {
382 func() *RagSet {
383 rs := New("simple", flag.ContinueOnError)
384 rs.Bool("force", false, "force operation in event of conflict")
385 rs.String("target", "", "action target")
386 rs.String("config", "", "config `file` to load", WithShort("c"))
387 return rs
388 },
389 `Flags:
390 --force force operation in event of conflict
391 --target string action target
392 -c, --config file config file to load
393 `,
394 },
395 "categories": {
396 func() *RagSet {
397 rs := New("simple", flag.ContinueOnError)
398 rs.Bool("force", false, "force operation in event of conflict")
399 rs.String("target", "", "action target")
400 rs.String("config", "", "config `file` to load", WithShort("c"))
401 rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
402 rs.Bool("notify", false, "send a slack notification",
403 WithShort("n"), WithCategory("slack"))
404 return rs
405 },
406 `Flags:
407 --force force operation in event of conflict
408 --target string action target
409 -c, --config file config file to load
410
411 Slack Flags:
412 --channel int slack channel ID to notify
413 -n, --notify send a slack notification
414 `,
415 },
416 "only categories": {
417 func() *RagSet {
418 rs := New("simple", flag.ContinueOnError)
419 rs.Bool("force", false, "force operation in event of conflict", WithCategory("action"))
420 rs.String("target", "", "action target", WithCategory("action"))
421 rs.String("config", "", "config `file` to load", WithShort("c"), WithCategory("action"))
422 rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
423 rs.Bool("notify", false, "send a slack notification",
424 WithShort("n"), WithCategory("slack"))
425 return rs
426 },
427 `Action Flags:
428 --force force operation in event of conflict
429 --target string action target
430 -c, --config file config file to load
431
432 Slack Flags:
433 --channel int slack channel ID to notify
434 -n, --notify send a slack notification
435 `,
436 },
437 "categories without shorts": {
438 func() *RagSet {
439 rs := New("simple", flag.ContinueOnError)
440 rs.Bool("force", false, "force operation in event of conflict")
441 rs.String("target", "", "action target")
442 rs.String("config", "", "config `file` to load", WithShort("c"))
443 rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
444 rs.Bool("notify", false, "send a slack notification", WithCategory("slack"))
445 return rs
446 },
447 `Flags:
448 --force force operation in event of conflict
449 --target string action target
450 -c, --config file config file to load
451
452 Slack Flags:
453 --channel int slack channel ID to notify
454 --notify send a slack notification
455 `,
456 },
457 }
458
459 for name, tc := range tcs {
460 name := name
461 tc := tc
462 t.Run(name, func(t *testing.T) {
463 t.Parallel()
464
465
466 for i := 0; i < 5; i++ {
467 b := new(bytes.Buffer)
468 rs := tc.rs()
469 rs.SetOutput(b)
470 rs.Usage()
471 assert.Equal(t, tc.exp, b.String())
472 }
473 })
474 }
475 }
476
View as plain text