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