Skip to content

Commit bc670ad

Browse files
authored
Add comprehensive edge case tests for CLI library (#5)
1 parent 163b6b3 commit bc670ad

File tree

5 files changed

+1089
-1
lines changed

5 files changed

+1089
-1
lines changed

parse_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,222 @@ func TestParse(t *testing.T) {
396396
err := Parse(cmd, nil)
397397
require.NoError(t, err)
398398
})
399+
t.Run("underscore in command name", func(t *testing.T) {
400+
t.Parallel()
401+
cmd := &Command{
402+
Name: "root",
403+
Exec: func(ctx context.Context, s *State) error { return nil },
404+
SubCommands: []*Command{
405+
{Name: "sub_command", Exec: func(ctx context.Context, s *State) error { return nil }},
406+
},
407+
}
408+
err := Parse(cmd, []string{"sub_command"})
409+
require.NoError(t, err)
410+
})
411+
t.Run("command name starting with number", func(t *testing.T) {
412+
t.Parallel()
413+
cmd := &Command{
414+
Name: "root",
415+
SubCommands: []*Command{
416+
{Name: "1command"},
417+
},
418+
}
419+
err := Parse(cmd, nil)
420+
require.Error(t, err)
421+
require.ErrorContains(t, err, `name must start with a letter`)
422+
})
423+
t.Run("command name with special characters", func(t *testing.T) {
424+
t.Parallel()
425+
cmd := &Command{
426+
Name: "root",
427+
SubCommands: []*Command{
428+
{Name: "sub@command"},
429+
},
430+
}
431+
err := Parse(cmd, nil)
432+
require.Error(t, err)
433+
require.ErrorContains(t, err, `name must start with a letter and contain only letters, numbers, dashes (-) or underscores (_)`)
434+
})
435+
t.Run("very long command name", func(t *testing.T) {
436+
t.Parallel()
437+
longName := "very-long-command-name-that-exceeds-normal-expectations-and-continues-for-a-while-to-test-edge-cases"
438+
cmd := &Command{
439+
Name: "root",
440+
Exec: func(ctx context.Context, s *State) error { return nil },
441+
SubCommands: []*Command{
442+
{Name: longName, Exec: func(ctx context.Context, s *State) error { return nil }},
443+
},
444+
}
445+
err := Parse(cmd, []string{longName})
446+
require.NoError(t, err)
447+
})
448+
t.Run("empty args list", func(t *testing.T) {
449+
t.Parallel()
450+
cmd := &Command{
451+
Name: "root",
452+
Exec: func(ctx context.Context, s *State) error { return nil },
453+
}
454+
err := Parse(cmd, []string{})
455+
require.NoError(t, err)
456+
require.Len(t, cmd.state.Args, 0)
457+
})
458+
t.Run("args with whitespace only", func(t *testing.T) {
459+
t.Parallel()
460+
cmd := &Command{
461+
Name: "root",
462+
Exec: func(ctx context.Context, s *State) error { return nil },
463+
}
464+
err := Parse(cmd, []string{" ", "\t", ""})
465+
require.NoError(t, err)
466+
require.Equal(t, []string{" ", "\t", ""}, cmd.state.Args)
467+
})
468+
t.Run("flag with empty value", func(t *testing.T) {
469+
t.Parallel()
470+
cmd := &Command{
471+
Name: "root",
472+
Flags: FlagsFunc(func(fset *flag.FlagSet) {
473+
fset.String("config", "", "config file")
474+
}),
475+
Exec: func(ctx context.Context, s *State) error { return nil },
476+
}
477+
err := Parse(cmd, []string{"--config="})
478+
require.NoError(t, err)
479+
require.Equal(t, "", GetFlag[string](cmd.state, "config"))
480+
})
481+
t.Run("boolean flag with explicit false", func(t *testing.T) {
482+
t.Parallel()
483+
cmd := &Command{
484+
Name: "root",
485+
Flags: FlagsFunc(func(fset *flag.FlagSet) {
486+
fset.Bool("verbose", true, "verbose mode")
487+
}),
488+
Exec: func(ctx context.Context, s *State) error { return nil },
489+
}
490+
err := Parse(cmd, []string{"--verbose=false"})
491+
require.NoError(t, err)
492+
require.False(t, GetFlag[bool](cmd.state, "verbose"))
493+
})
494+
t.Run("deeply nested command hierarchy", func(t *testing.T) {
495+
t.Parallel()
496+
level5 := &Command{
497+
Name: "level5",
498+
Exec: func(ctx context.Context, s *State) error { return nil },
499+
}
500+
level4 := &Command{
501+
Name: "level4",
502+
SubCommands: []*Command{level5},
503+
}
504+
level3 := &Command{
505+
Name: "level3",
506+
SubCommands: []*Command{level4},
507+
}
508+
level2 := &Command{
509+
Name: "level2",
510+
SubCommands: []*Command{level3},
511+
}
512+
level1 := &Command{
513+
Name: "level1",
514+
SubCommands: []*Command{level2},
515+
}
516+
root := &Command{
517+
Name: "root",
518+
SubCommands: []*Command{level1},
519+
}
520+
err := Parse(root, []string{"level1", "level2", "level3", "level4", "level5"})
521+
require.NoError(t, err)
522+
terminal := root.terminal()
523+
require.Equal(t, level5, terminal)
524+
})
525+
t.Run("many subcommands", func(t *testing.T) {
526+
t.Parallel()
527+
var subcommands []*Command
528+
for i := 0; i < 25; i++ {
529+
subcommands = append(subcommands, &Command{
530+
Name: "cmd" + string(rune('a'+i%26)),
531+
Exec: func(ctx context.Context, s *State) error { return nil },
532+
})
533+
}
534+
root := &Command{
535+
Name: "root",
536+
SubCommands: subcommands,
537+
}
538+
err := Parse(root, []string{"cmda"})
539+
require.NoError(t, err)
540+
terminal := root.terminal()
541+
require.Equal(t, "cmda", terminal.Name)
542+
})
543+
t.Run("duplicate subcommand names", func(t *testing.T) {
544+
t.Parallel()
545+
cmd := &Command{
546+
Name: "root",
547+
SubCommands: []*Command{
548+
{Name: "duplicate", Exec: func(ctx context.Context, s *State) error { return nil }},
549+
{Name: "duplicate", Exec: func(ctx context.Context, s *State) error { return nil }},
550+
},
551+
}
552+
// This library may not check for duplicate names, so just verify it works
553+
err := Parse(cmd, []string{"duplicate"})
554+
require.NoError(t, err)
555+
// Just ensure it doesn't crash and can parse the first match
556+
})
557+
t.Run("flag metadata for non-existent flag", func(t *testing.T) {
558+
t.Parallel()
559+
cmd := &Command{
560+
Name: "root",
561+
Flags: FlagsFunc(func(fset *flag.FlagSet) {
562+
fset.String("existing", "", "existing flag")
563+
}),
564+
FlagsMetadata: []FlagMetadata{
565+
{Name: "existing", Required: true},
566+
{Name: "nonexistent", Required: true},
567+
},
568+
Exec: func(ctx context.Context, s *State) error { return nil },
569+
}
570+
err := Parse(cmd, []string{"--existing=value"})
571+
require.Error(t, err)
572+
require.ErrorContains(t, err, "required flag -nonexistent not found in flag set")
573+
})
574+
t.Run("args with special characters", func(t *testing.T) {
575+
t.Parallel()
576+
cmd := &Command{
577+
Name: "root",
578+
Exec: func(ctx context.Context, s *State) error { return nil },
579+
}
580+
specialArgs := []string{"file with spaces.txt", "file@symbol.txt", "file\"quote.txt", "file'apostrophe.txt"}
581+
err := Parse(cmd, specialArgs)
582+
require.NoError(t, err)
583+
require.Equal(t, specialArgs, cmd.state.Args)
584+
})
585+
t.Run("very long argument list", func(t *testing.T) {
586+
t.Parallel()
587+
cmd := &Command{
588+
Name: "root",
589+
Exec: func(ctx context.Context, s *State) error { return nil },
590+
}
591+
var longArgList []string
592+
for i := 0; i < 100; i++ {
593+
longArgList = append(longArgList, "arg"+string(rune('0'+i%10)))
594+
}
595+
err := Parse(cmd, longArgList)
596+
require.NoError(t, err)
597+
require.Equal(t, longArgList, cmd.state.Args)
598+
})
599+
t.Run("mixed flags and args in various orders", func(t *testing.T) {
600+
t.Parallel()
601+
cmd := &Command{
602+
Name: "root",
603+
Flags: FlagsFunc(func(fset *flag.FlagSet) {
604+
fset.String("flag1", "", "first flag")
605+
fset.String("flag2", "", "second flag")
606+
}),
607+
Exec: func(ctx context.Context, s *State) error { return nil },
608+
}
609+
err := Parse(cmd, []string{"arg1", "--flag1=val1", "arg2", "--flag2", "val2", "arg3"})
610+
require.NoError(t, err)
611+
require.Equal(t, "val1", GetFlag[string](cmd.state, "flag1"))
612+
require.Equal(t, "val2", GetFlag[string](cmd.state, "flag2"))
613+
require.Equal(t, []string{"arg1", "arg2", "arg3"}, cmd.state.Args)
614+
})
399615
}
400616

401617
func getCommand(t *testing.T, c *Command) *Command {

0 commit comments

Comments
 (0)