- Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n and --env on every command; PeekGlobals reads them before config loads, OnGlobals hooks re-apply them after parsing (diary.ApplyGlobals in the generated main) - Bare binary, --help and list print the Artisan list with the version header from BuildVersion and namespace blocks - Execute runs the root through fang (no version, man page or completions) and returns 0, 1 or 2; Fail renders errors before the root exists - NewRoot rejects duplicate command names (ErrDuplicateCommand) and flags that reuse a global option (ErrReservedFlag) - Generated main and summer use PeekGlobals, compass.Open with --env, diary.FromGlobals and bonfire.Execute; docs checker knows the globals
143 lines
5.7 KiB
Go
143 lines
5.7 KiB
Go
package bonfire
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestListTracerSmoke: a bare root prints the Artisan list (header, usage,
|
|
// global options, top-level commands then namespaces), the global options
|
|
// reach the OnGlobals hook the same way PeekGlobals reads them, and Execute
|
|
// returns 0, 1 or 2.
|
|
func TestListTracerSmoke(t *testing.T) {
|
|
var recorded []Globals
|
|
commands := func() []Command {
|
|
ok := func(context.Context, Input, Output) error { return nil }
|
|
return []Command{
|
|
{Name: "migrate", Description: "Run the database migrations", Run: ok},
|
|
{Name: "admin:create", Description: "Create a backend administrator", Run: ok},
|
|
{Name: "admin:token:create", Description: "Create a personal API token", Run: ok},
|
|
{Name: "make:model", Description: "Create a new model and migration", Run: ok},
|
|
{Name: "acme:fail", Description: "Fail on purpose", Run: func(context.Context, Input, Output) error {
|
|
return errors.New("boom")
|
|
}},
|
|
}
|
|
}
|
|
newRoot := func(t *testing.T, out *bytes.Buffer) func(args ...string) int {
|
|
t.Helper()
|
|
root, err := NewRootIO("acme", commands(), strings.NewReader(""), out, out,
|
|
WithVersion(Version{App: "v1.4.0", Framework: "v0.3.2"}),
|
|
OnGlobals(func(g Globals) { recorded = append(recorded, g) }))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return func(args ...string) int {
|
|
recorded = nil
|
|
out.Reset()
|
|
return Execute(context.Background(), root, args)
|
|
}
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
run := newRoot(t, &out)
|
|
if code := run(); code != ExitOK {
|
|
t.Fatalf("bare root exit = %d\n%s", code, out.String())
|
|
}
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"SummerCMS acme v1.4.0 (framework v0.3.2)\n",
|
|
"Usage:\n bin/acme <command> [options] [arguments]\n",
|
|
" -h, --help Display help for the given command\n",
|
|
" -q, --quiet Do not output any message\n",
|
|
" -v|vv|vvv Increase verbosity\n",
|
|
" --ansi|no-ansi Force (or disable) ANSI output\n",
|
|
" -n, --no-interaction Do not ask any interactive question\n",
|
|
" --env The environment the command runs under\n",
|
|
"Available commands:\n",
|
|
" migrate Run the database migrations\n",
|
|
" admin:token:create Create a personal API token\n",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("list missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
order := []string{"\n list ", "\n migrate ", "\n acme\n", "\n admin\n", "\n admin:create ", "\n admin:token:create ", "\n make\n", "\n make:model "}
|
|
last := -1
|
|
for _, s := range order {
|
|
i := strings.Index(got, s)
|
|
if i <= last {
|
|
t.Fatalf("%q out of order (at %d, previous at %d):\n%s", s, i, last, got)
|
|
}
|
|
last = i
|
|
}
|
|
if strings.Contains(got, "\x1b[") {
|
|
t.Fatalf("non-tty list has ansi:\n%q", got)
|
|
}
|
|
if code := run("--help"); code != ExitOK || !strings.Contains(out.String(), "Available commands:") {
|
|
t.Fatalf("--help exit = %d\n%s", code, out.String())
|
|
}
|
|
|
|
cases := []struct {
|
|
args []string
|
|
want Globals
|
|
}{
|
|
{[]string{"-vvv", "migrate"}, Globals{Verbosity: 3}},
|
|
{[]string{"migrate", "-q"}, Globals{Quiet: true, NoInteraction: true}},
|
|
{[]string{"migrate", "--env", "staging", "-n", "--ansi", "--no-ansi"}, Globals{NoInteraction: true, Env: "staging", Color: ColorNever}},
|
|
{[]string{"--env=dev", "-v", "migrate", "--verbose"}, Globals{Verbosity: 2, Env: "dev"}},
|
|
}
|
|
for _, c := range cases {
|
|
if code := run(c.args...); code != ExitOK {
|
|
t.Fatalf("%v exit = %d\n%s", c.args, code, out.String())
|
|
}
|
|
if len(recorded) != 1 || recorded[0] != c.want {
|
|
t.Fatalf("%v: hook got %+v, want %+v", c.args, recorded, c.want)
|
|
}
|
|
if peek := PeekGlobals(c.args); peek != c.want {
|
|
t.Fatalf("%v: PeekGlobals = %+v, want %+v", c.args, peek, c.want)
|
|
}
|
|
}
|
|
|
|
if code := run("migrat"); code != ExitUsage {
|
|
t.Fatalf("unknown command exit = %d, want %d\n%s", code, ExitUsage, out.String())
|
|
}
|
|
if code := run("migrate", "--bogus"); code != ExitUsage {
|
|
t.Fatalf("unknown flag exit = %d, want %d", code, ExitUsage)
|
|
}
|
|
if code := run("acme:fail"); code != ExitFailure || !strings.Contains(out.String(), "ERROR") || !strings.Contains(out.String(), "boom") {
|
|
t.Fatalf("failing command exit = %d\n%s", code, out.String())
|
|
}
|
|
|
|
dup := append(commands(), Command{Name: "migrate", Description: "Again"})
|
|
if _, err := NewRootIO("acme", dup, strings.NewReader(""), &out, &out); !errors.Is(err, ErrDuplicateCommand) || !strings.Contains(err.Error(), `"migrate"`) {
|
|
t.Fatalf("duplicate name err = %v", err)
|
|
}
|
|
if _, err := NewRootIO("acme", []Command{{Name: "acme:list"}, {Name: "list"}}, strings.NewReader(""), &out, &out); !errors.Is(err, ErrDuplicateCommand) {
|
|
t.Fatalf("command named list err = %v", err)
|
|
}
|
|
reserved := []Command{{Name: "acme:loud", Flags: []Flag{{Name: "volume", Shorthand: "v"}}}}
|
|
if _, err := NewRootIO("acme", reserved, strings.NewReader(""), &out, &out); !errors.Is(err, ErrReservedFlag) || !strings.Contains(err.Error(), "acme:loud") {
|
|
t.Fatalf("reserved shorthand err = %v", err)
|
|
}
|
|
reservedName := []Command{{Name: "acme:loud", Flags: []Flag{{Name: "env"}}}}
|
|
if _, err := NewRootIO("acme", reservedName, strings.NewReader(""), &out, &out); !errors.Is(err, ErrReservedFlag) {
|
|
t.Fatalf("reserved name err = %v", err)
|
|
}
|
|
|
|
// Call builds its own root and never runs a hook.
|
|
recorded = nil
|
|
if err := Call(context.Background(), commands(), "migrate", []string{"-vv"}, &out); err != nil || len(recorded) != 0 {
|
|
t.Fatalf("Call: err %v, hooks %v", err, recorded)
|
|
}
|
|
|
|
if args := (Globals{Verbosity: 2, Quiet: true, NoInteraction: true, Color: ColorNever, Env: "staging"}).Args(); strings.Join(args, " ") != "-vv -q --no-ansi --env=staging" {
|
|
t.Fatalf("Globals.Args = %v", args)
|
|
}
|
|
if args := (Globals{}).Args(); len(args) != 0 {
|
|
t.Fatalf("zero Globals.Args = %v", args)
|
|
}
|
|
}
|