package sink import ( "context" "fmt" "strings" "testing" "github.com/peterbourgon/ff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "edge-infra.dev/pkg/lib/cli/rags" ) type testExtension struct { name string finishedBeforeRun bool finishedAfterRun bool stringFlag string boolFlag bool } func (ext *testExtension) RegisterFlags(rs *rags.RagSet) { if ext.name == "" { ext.name = "test-extension" } rs.StringVar(&ext.stringFlag, ext.name+"-string-flag", "default string value", "string usage message") rs.BoolVar(&ext.boolFlag, ext.name+"-bool-flag", false, "bool usage message") } type testCtxKey = struct{} func (ext *testExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) { ext.finishedBeforeRun = true return context.WithValue(ctx, &testCtxKey{}, "value"), r, nil } func (ext *testExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) { ext.finishedAfterRun = true return ctx, r, nil } type faultyExtension struct{} func (ext *faultyExtension) RegisterFlags(_ *rags.RagSet) {} func (ext *faultyExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) { return ctx, r, fmt.Errorf("Faulty extension BeforeRun") } func (ext *faultyExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) { return ctx, r, fmt.Errorf("Faulty extension AfterRun") } func TestNames(t *testing.T) { cmd := newTestCmd() // Force command tree to be computed. require.NoError(t, cmd.compute()) assert.Equal(t, cmd.Use, cmd.Name()) assert.Equal(t, cmd.Use, cmd.LongName()) for _, scmd := range cmd.Commands { assert.Equal(t, scmd.Use, scmd.Name()) assert.Equal(t, scmd.parent.LongName()+" "+scmd.Use, scmd.LongName()) } t.Run("Name", func(t *testing.T) { tcs := map[string]struct { use string exp string }{ "simple": {"root", "root"}, "options and args": {"foo [flags] ", "foo"}, "trailing space": {"foo ", "foo"}, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { c := &Command{Use: tc.use} assert.Equal(t, tc.exp, c.Name()) }) } }) } func TestUseLine(t *testing.T) { var ( root = &Command{Use: "root [flags]"} foo = &Command{Use: "foo [command]"} get = &Command{Use: "get [flags] "} del = &Command{Use: "delete [flags] "} ) root.Commands = []*Command{foo} foo.Commands = []*Command{get, del} // Force command tree to be computed. require.NoError(t, root.compute()) tcs := []struct { cmd *Command exp string }{ {root, root.Use}, {foo, "root " + foo.Use}, {get, "root foo " + get.Use}, {del, "root foo " + del.Use}, } for _, tc := range tcs { assert.Equal(t, tc.exp, useline(tc.cmd)) } } func TestExtensions(t *testing.T) { // TODO: should test context chaining (multiple ext modify context, verify // all changes are present) t.Run("extension errors", func(t *testing.T) { cmd := &Command{Use: "cli", Extensions: []Extension{&faultyExtension{}}} ctx := context.Background() r := newRun(cmd) var err error ctx, r, err = cmd.beforeRun(ctx, r) assert.Error(t, err) _, _, err = cmd.afterRun(ctx, r) assert.Error(t, err) }) t.Run("working extension", func(t *testing.T) { ext := &testExtension{} cmd := &Command{Use: "cli", Extensions: []Extension{ext}} ctx := context.Background() r := newRun(cmd) var err error ctx, r, err = cmd.beforeRun(ctx, r) assert.NoError(t, err) assert.True(t, ext.finishedBeforeRun) assert.Equal(t, "value", ctx.Value(&testCtxKey{})) ctx, _, err = cmd.afterRun(ctx, r) assert.NoError(t, err) assert.True(t, ext.finishedAfterRun) assert.Equal(t, "value", ctx.Value(&testCtxKey{})) }) } func TestDefaultUsageFunc(t *testing.T) { tcs := map[string]*Command{ "short usage": { Use: "foo", Short: "Foo the bar", }, "long usage": { Use: "foo", Long: "This is a paragraph about Fooing The Bar", }, "long usage overrides short": { Use: "foo", Short: "Foo the bar", Long: "This is a paragraph about Fooing The Bar", }, "subcommands": { Use: "foo", Short: "Foo the bar", Long: "This is a paragraph about Fooing The Bar", Commands: []*Command{{Use: "bar"}}, }, "flags": { Use: "foo", Short: "Foo the bar", Long: "This is a paragraph about Fooing The Bar", Commands: []*Command{{Use: "bar"}}, Flags: []*rags.Rag{ {Name: "you-see-me", Value: &rags.Bool{}}, }, }, "no help text": { Use: "foo", }, } for name, c := range tcs { name := name c := c t.Run(name, func(t *testing.T) { t.Parallel() // Force command to be computed require.NoError(t, c.compute()) usage := defaultUsageFn(c) lines := strings.Split(usage, "\n") t.Log("usage", "\n"+usage) for i, l := range lines { t.Log(i, l) } switch { case c.Long != "": assert.Equal(t, c.Long, lines[0]) case c.Short != "": assert.Equal(t, c.Short, lines[0]) default: assert.Equal(t, "Usage:", lines[0]) } if len(c.Commands) == 0 { assert.False(t, strings.Contains(usage, "\nCommands:")) } else { assert.True(t, strings.Contains(usage, "\nCommands:")) } if len(c.Flags) == 0 { assert.False(t, strings.Contains(usage, "\nFlags:")) } else { assert.True(t, strings.Contains(usage, "\nFlags:")) } assert.True(t, lines[len(lines)-1] == "") assert.True(t, lines[len(lines)-2] != "") }) } } func TestParse(t *testing.T) { t.Parallel() tcs := map[string]struct { cmdIdx []int args []string }{ "root": {[]int{}, []string{}}, "root foo": {[]int{0}, []string{"foo"}}, "root foo view": {[]int{0, 0}, []string{"foo", "view"}}, "root foo delete": {[]int{0, 1}, []string{"foo", "delete"}}, "root bar": {[]int{1}, []string{"bar"}}, "root bar baz": {[]int{1, 4}, []string{"bar", "baz"}}, "root bar baz delete": {[]int{1, 4, 1}, []string{"bar", "baz", "delete"}}, } for name, tc := range tcs { name := name tc := tc t.Run(name, func(t *testing.T) { t.Parallel() cmd := newTestCmd() assert.NoError(t, cmd.Parse(tc.args)) assert.ElementsMatch(t, cmd.args, tc.args) t.Log("args", cmd.args) expCmd := cmd cmds := expCmd.Commands for _, idx := range tc.cmdIdx { for i, s := range cmds { if i == idx { assert.NotNil(t, s.selected) } else { assert.Nil(t, s.selected) } } expCmd = cmds[idx] cmds = expCmd.Commands } }) } } func TestCommand_compute(t *testing.T) { t.Parallel() cmd := newTestCmd() // Force command tree to be computed. require.NoError(t, cmd.compute()) testComputedCmd(t, cmd) t.Run("parent as child", func(t *testing.T) { t.Parallel() cmd := newTestCmd() cmd.Commands = append(cmd.Commands, cmd) assert.Error(t, cmd.compute()) }) t.Run("duplicate children", func(t *testing.T) { t.Parallel() cmd := newTestCmd() cmd.Commands = append(cmd.Commands, cmd.Commands[0]) assert.Error(t, cmd.compute()) }) } func testComputedCmd(t *testing.T, cmd *Command) { t.Helper() a := assert.New(t) if cmd.HasParent() { p := cmd.Parent() switch { case len(p.AllParsingOptions()) > 0: exp := append(p.AllParsingOptions(), cmd.Options...) actual := cmd.AllParsingOptions() a.Len(actual, len(exp)) default: a.Len(cmd.AllParsingOptions(), len(cmd.Options)) } a.Equal(p.LongName()+" "+cmd.Name(), cmd.LongName()) } else { a.Equal(cmd.Name(), cmd.LongName()) a.ElementsMatch(cmd.Options, cmd.AllParsingOptions()) } a.NotNil(cmd.Exec) for _, scmd := range cmd.Commands { a.Same(cmd, scmd.parent) testComputedCmd(t, scmd) } } // add some extensions and options throughout func newTestCmd() *Command { return &Command{ Use: "root", Commands: []*Command{ { Use: "foo", Extensions: []Extension{&testExtension{}}, Commands: []*Command{ {Use: "view"}, {Use: "delete"}, {Use: "list"}, {Use: "create"}, }, }, { Use: "bar", Options: []ff.Option{ff.WithEnvVarNoPrefix()}, Commands: []*Command{ {Use: "view"}, {Use: "delete"}, {Use: "list"}, {Use: "create"}, {Use: "baz", Commands: []*Command{ {Use: "view", Options: []ff.Option{ff.WithIgnoreUndefined(true)}}, {Use: "delete"}, {Use: "list"}, {Use: "create"}, }, }, }, }, }, } }