feat(bonfire): global flags, Artisan list and exit codes
- 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
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -48,8 +49,8 @@ func runHello(t *testing.T, env map[string]string) string {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := run([]string{"greeter:hello"}, &buf); err != nil {
|
||||
t.Fatalf("greeter:hello: %v", err)
|
||||
if code := run([]string{"greeter:hello"}, &buf); code != 0 {
|
||||
t.Fatalf("greeter:hello: exit %d\n%s", code, buf.String())
|
||||
}
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
@@ -231,11 +232,11 @@ func TestTypedItemRoute(t *testing.T) {
|
||||
|
||||
func TestHelpListsServe(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := run([]string{"--help"}, &buf); err != nil {
|
||||
t.Fatal(err)
|
||||
if code := run([]string{"--help"}, &buf); code != 0 {
|
||||
t.Fatalf("--help: exit %d\n%s", code, buf.String())
|
||||
}
|
||||
help := buf.String()
|
||||
for _, name := range []string{"serve", "migrate", "migrate:status", "migrate:rollback"} {
|
||||
for _, name := range []string{"Available commands:", "serve", "migrate", "migrate:status", "migrate:rollback"} {
|
||||
if !strings.Contains(help, name) {
|
||||
t.Fatalf("help missing %s:\n%s", name, help)
|
||||
}
|
||||
@@ -244,9 +245,19 @@ func TestHelpListsServe(t *testing.T) {
|
||||
|
||||
func TestMigrateWithoutDSNFailsLoudly(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := run([]string{"migrate"}, &buf)
|
||||
if err == nil || !strings.Contains(err.Error(), "database.dsn") {
|
||||
t.Fatalf("want empty DSN error, got %v\n%s", err, buf.String())
|
||||
if code := run([]string{"migrate"}, &buf); code != 1 {
|
||||
t.Fatalf("migrate without a DSN: exit %d, want 1\n%s", code, buf.String())
|
||||
}
|
||||
runSummerBuild(t)
|
||||
cmd := exec.Command("./bin/hello", "migrate")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 1 {
|
||||
t.Fatalf("bin/hello migrate: %v, want exit 1\n%s", err, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "database.dsn") {
|
||||
t.Fatalf("stderr does not name database.dsn:\n%s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +265,34 @@ func TestBuiltBinaryUnknownCommandExitsNonZero(t *testing.T) {
|
||||
runSummerBuild(t)
|
||||
cmd := exec.Command("./bin/hello", "does:not-exist")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("expected non-zero exit, output:\n%s", out)
|
||||
if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 2 {
|
||||
t.Fatalf("unknown command: %v, want exit 2, output:\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltBinaryListsCommands: the bare binary prints the Artisan list with
|
||||
// the version header and the plugin namespace, and global options are
|
||||
// accepted before a command.
|
||||
func TestBuiltBinaryListsCommands(t *testing.T) {
|
||||
runSummerBuild(t)
|
||||
cmd := exec.Command("./bin/hello")
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("bin/hello: %v\n%s%s", err, stdout.String(), stderr.String())
|
||||
}
|
||||
got := stdout.String()
|
||||
for _, want := range []string{"SummerCMS hello", "Available commands:", "greeter:hello"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("list missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(strings.Split(got, "\n"), " greeter") {
|
||||
t.Fatalf("list has no greeter namespace line:\n%s", got)
|
||||
}
|
||||
verbose := exec.Command("./bin/hello", "-vv", "list")
|
||||
if out, err := verbose.CombinedOutput(); err != nil {
|
||||
t.Fatalf("bin/hello -vv list: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -20,25 +20,23 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:], os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(run(os.Args[1:], os.Stdout))
|
||||
}
|
||||
|
||||
func run(args []string, out io.Writer) error {
|
||||
diary.Bootstrap(os.Stderr, diary.Options{})
|
||||
cfg, err := compass.Load("config")
|
||||
func run(args []string, out io.Writer) int {
|
||||
g := bonfire.PeekGlobals(args)
|
||||
diary.Bootstrap(os.Stderr, diary.FromGlobals(g))
|
||||
cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env})
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
if err := diary.Setup(cfg, diary.Options{}); err != nil {
|
||||
return err
|
||||
if err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
plugins, err := party.Activate(app, PluginIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
commands := lagoon.RuntimeCommands(app, plugins)
|
||||
commands = append(commands, conga.RuntimeCommands(app, plugins)...)
|
||||
@@ -51,12 +49,11 @@ func run(args []string, out io.Writer) error {
|
||||
}
|
||||
}
|
||||
if err := app.Publish(bonfire.NewCatalog(commands)); err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
root, err := bonfire.NewRoot("hello", commands, out)
|
||||
root, err := bonfire.NewRoot("hello", commands, out, bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
root.SetArgs(args)
|
||||
return root.Execute()
|
||||
return bonfire.Execute(context.Background(), root, args)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user