- PeekGlobals table, Args round trip, hook and PeekGlobals agree (clusters too) - reserved flags, duplicate names, Call runs no OnGlobals hook - exit codes 0/1/2, Reported silence, scrubbed errors, wrong-directory hint - widget matrix: not a terminal byte for byte, -q, --no-ansi, -n, terminal - live models through Update and View and one program run into a buffer - prompts under -n, Secret from piped stdin, ConfirmProceed, PrintAbove
106 lines
4.3 KiB
Go
106 lines
4.3 KiB
Go
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")
|
|
}
|
|
}
|