package bonfire import ( "bytes" "context" "errors" "strings" "testing" ) func newTestRoot(commands []Command, opts ...RootOption) error { var buf bytes.Buffer _, err := NewRootIO("acme", commands, strings.NewReader(""), &buf, &buf, opts...) return err } // TestReservedFlags: a command flag that reuses a global option's name or // shorthand fails NewRoot with an error naming the command and the flag, // without a panic; Call refuses it the same way. func TestReservedFlags(t *testing.T) { for _, name := range []string{"verbose", "quiet", "ansi", "no-ansi", "no-interaction", "env", "help"} { cmd := Command{Name: "acme:loud", Flags: []Flag{{Name: name}}} err := newTestRoot([]Command{cmd}) if !errors.Is(err, ErrReservedFlag) || !strings.Contains(err.Error(), `"acme:loud"`) || !strings.Contains(err.Error(), "--"+name) { t.Errorf("--%s: err = %v", name, err) } if err := Call(context.Background(), []Command{cmd}, "acme:loud", nil, nil); !errors.Is(err, ErrReservedFlag) { t.Errorf("Call with --%s: err = %v", name, err) } } for _, short := range []string{"v", "q", "n", "h"} { cmd := Command{Name: "acme:loud", Flags: []Flag{{Name: "volume", Shorthand: short}}} err := newTestRoot([]Command{cmd}) if !errors.Is(err, ErrReservedFlag) || !strings.Contains(err.Error(), "-"+short+" for --volume") || !strings.Contains(err.Error(), `"acme:loud"`) { t.Errorf("-%s: err = %v", short, err) } } // Other names and shorthands are fine, repeatable and bare ones too. ok := Command{Name: "acme:loud", Flags: []Flag{ {Name: "volume", Shorthand: "V"}, {Name: "environment", Shorthand: "e"}, {Name: "tag", Shorthand: "t", Repeatable: true}, {Name: "force", Shorthand: "f", Bare: true}, }} if err := newTestRoot([]Command{ok}); err != nil { t.Errorf("unreserved flags refused: %v", err) } } // TestDuplicateNames: two commands with one name, or a command named list // or help, fail NewRoot with ErrDuplicateCommand naming it. func TestDuplicateNames(t *testing.T) { cases := map[string][]Command{ "acme:sync": {{Name: "acme:sync"}, {Name: "acme:other"}, {Name: "acme:sync"}}, "list": {{Name: "list"}}, "help": {{Name: "acme:x"}, {Name: "help"}}, } for name, commands := range cases { err := newTestRoot(commands) if !errors.Is(err, ErrDuplicateCommand) || !strings.Contains(err.Error(), `"`+name+`"`) { t.Errorf("%s: err = %v", name, err) } } if err := newTestRoot([]Command{{Name: "acme:list"}, {Name: "list:all"}, {Name: "acme:help"}}); err != nil { t.Errorf("names that only contain list or help refused: %v", err) } if err := newTestRoot([]Command{{Name: "nonamespace"}}); !errors.Is(err, ErrCommandName) { t.Errorf("a plugin command without a namespace: err = %v", err) } } // TestCallNoGlobalsHook: a root built by Call runs no OnGlobals hook, even // when called from a command of a root that has one, and the called // command still sees the globals passed to it. func TestCallNoGlobalsHook(t *testing.T) { var hooked []Globals var inner Globals target := Command{Name: "acme:inner", Description: "Inner", Run: func(_ context.Context, _ Input, out Output) error { inner = GlobalsOf(out) return nil }} outer := Command{Name: "acme:outer", Description: "Outer", Run: func(ctx context.Context, _ Input, out Output) error { return NewCatalog([]Command{target}).Call(ctx, "acme:inner", []string{"-vvv", "--env=other"}, out) }} var buf bytes.Buffer root, err := NewRootIO("acme", []Command{outer, target}, strings.NewReader(""), &buf, &buf, OnGlobals(func(g Globals) { hooked = append(hooked, g) })) if err != nil { t.Fatal(err) } if code := Execute(context.Background(), root, []string{"-v", "acme:outer"}); code != ExitOK { t.Fatalf("exit %d:\n%s", code, buf.String()) } if len(hooked) != 1 || hooked[0].Verbosity != 1 || hooked[0].Env != "" { t.Errorf("hooks = %+v, want one call with the outer -v", hooked) } if inner.Verbosity != 3 || inner.Env != "other" { t.Errorf("called command saw %+v, want -vvv --env=other", inner) } if err := Call(context.Background(), []Command{target}, "acme:missing", nil, nil); !errors.Is(err, ErrUnknownCommand) { t.Errorf("Call of a missing command: %v", err) } var nilCatalog *Catalog if nilCatalog.Has("acme:inner") || !errors.Is(nilCatalog.Call(context.Background(), "acme:inner", nil, nil), ErrUnknownCommand) { t.Error("a nil Catalog has commands") } }