Files
summercms/modules/bonfire/root_test.go
Jakub Zych eeeee59ead test(bonfire): flags, exit codes and the widget matrix
- 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
2026-10-21 19:22:00 +02:00

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")
}
}