test(bonfire): goldens for list, help and errors
- list, list --all, list --format=json and an --ansi styled list (D-13, D-16)
- admin:token:create help and a passthrough help; help <cmd> equals --help (D-14)
- error blocks: hint with {bin}, unknown command, -v chain on stderr (D-18)
- CheckDocs gaps in order and sorted; the version header and BuildVersion
- goldens render with color detection cleared; -update rewrites them
This commit is contained in:
50
modules/bonfire/docs_test.go
Normal file
50
modules/bonfire/docs_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package bonfire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCheckDocs: every missing item is reported in a fixed order, blank
|
||||||
|
// text counts as missing, results are sorted by command name and a fully
|
||||||
|
// documented command reports nothing.
|
||||||
|
func TestCheckDocs(t *testing.T) {
|
||||||
|
complete := Command{
|
||||||
|
Name: "acme:sync", Description: "Sync", Long: "Syncs.",
|
||||||
|
Args: []Arg{{Name: "target", Description: "Where to"}},
|
||||||
|
Flags: []Flag{{Name: "dry-run", Description: "Only print", Bare: true}},
|
||||||
|
Examples: []Example{{Command: "acme:sync prod"}},
|
||||||
|
}
|
||||||
|
if got := CheckDocs([]Command{complete}); len(got) != 0 {
|
||||||
|
t.Fatalf("complete command reported: %v", got)
|
||||||
|
}
|
||||||
|
if got := CheckDocs(nil); len(got) != 0 {
|
||||||
|
t.Fatalf("no commands reported: %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
blank := complete
|
||||||
|
blank.Name, blank.Description, blank.Long = "acme:blank", " ", "\n\t"
|
||||||
|
noExamples := complete
|
||||||
|
noExamples.Name, noExamples.Examples = "acme:noex", nil
|
||||||
|
undocumented := Command{
|
||||||
|
Name: "acme:bare",
|
||||||
|
Args: []Arg{{Name: "a"}, {Name: "b", Description: "B"}},
|
||||||
|
Flags: []Flag{{Name: "x"}, {Name: "y", Description: "Y"}, {Name: "z", Description: " "}},
|
||||||
|
}
|
||||||
|
got := CheckDocs([]Command{noExamples, complete, undocumented, blank})
|
||||||
|
want := []DocProblem{
|
||||||
|
{Command: "acme:bare", Missing: []string{"description", "long help", "examples", "argument a description", "option --x description", "option --z description"}},
|
||||||
|
{Command: "acme:blank", Missing: []string{"description", "long help"}},
|
||||||
|
{Command: "acme:noex", Missing: []string{"examples"}},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("CheckDocs =\n %v\nwant\n %v", got, want)
|
||||||
|
}
|
||||||
|
if s := got[1].String(); s != "acme:blank: missing description, long help" {
|
||||||
|
t.Errorf("String() = %q", s)
|
||||||
|
}
|
||||||
|
// The builtins list and help are documented.
|
||||||
|
if got := CheckDocs(builtinCommands()); len(got) != 0 {
|
||||||
|
t.Errorf("builtin commands reported: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
268
modules/bonfire/golden_test.go
Normal file
268
modules/bonfire/golden_test.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
package bonfire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var update = flag.Bool("update", false, "rewrite golden files")
|
||||||
|
|
||||||
|
// goldenVersion is the fixed version line of every golden.
|
||||||
|
var goldenVersion = Version{App: "v1.4.0", Framework: "v0.3.2"}
|
||||||
|
|
||||||
|
// plainEnv clears the environment variables that force or detect color, so
|
||||||
|
// goldens render the same on every machine: no escape codes unless --ansi.
|
||||||
|
func plainEnv(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
for _, k := range []string{"FORCE_COLOR", "CLICOLOR_FORCE", "CLICOLOR", "COLORTERM", "NO_COLOR"} {
|
||||||
|
t.Setenv(k, "")
|
||||||
|
_ = os.Unsetenv(k)
|
||||||
|
}
|
||||||
|
t.Setenv("TERM", "dumb")
|
||||||
|
}
|
||||||
|
|
||||||
|
// goldenTokenCommand is the admin:token:create command of the D-14
|
||||||
|
// preview: value, bare and repeatable flags with defaults, long help and
|
||||||
|
// two examples. The values are fake.
|
||||||
|
func goldenTokenCommand() Command {
|
||||||
|
return Command{
|
||||||
|
Name: "admin:token:create",
|
||||||
|
Description: "Create a personal API token for a backend admin",
|
||||||
|
Long: "Creates a token for the admin with the given login and prints it once.\nStore it in the client right away; it cannot be shown again.",
|
||||||
|
Args: []Arg{{Name: "login", Description: "The admin's login", Required: true}},
|
||||||
|
Flags: []Flag{
|
||||||
|
{Name: "scopes", Description: "Comma-separated scopes", Default: "*"},
|
||||||
|
{Name: "expires", Description: "Days until expiry", Default: "90"},
|
||||||
|
{Name: "json", Description: "Print the token as JSON", Bare: true},
|
||||||
|
{Name: "tag", Shorthand: "t", Description: "A tag for the token", Repeatable: true},
|
||||||
|
},
|
||||||
|
Examples: []Example{
|
||||||
|
{Command: "admin:token:create jane --scopes=journal.*", Description: "Token for the MCP server, journal only"},
|
||||||
|
{Command: "admin:token:create jane --json", Description: "Script-friendly output"},
|
||||||
|
},
|
||||||
|
Run: func(context.Context, Input, Output) error { return nil },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// goldenCommands is the command set of the list goldens (D-13 preview plus
|
||||||
|
// a hidden and a passthrough command).
|
||||||
|
func goldenCommands() []Command {
|
||||||
|
ok := func(context.Context, Input, Output) error { return nil }
|
||||||
|
return []Command{
|
||||||
|
{Name: "migrate", Description: "Run the database migrations", Run: ok},
|
||||||
|
{Name: "serve", Description: "Serve the application", Run: ok},
|
||||||
|
{Name: "admin:create", Description: "Create a backend administrator", Run: ok},
|
||||||
|
goldenTokenCommand(),
|
||||||
|
{Name: "make:command", Description: "Create a new console command", Run: ok},
|
||||||
|
{Name: "make:model", Description: "Create a new model and migration", Run: ok},
|
||||||
|
{Name: "acme:secret", Description: "Rotate the internal signing secret", Hidden: true, Run: ok},
|
||||||
|
goldenPassthroughCommand(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// goldenPassthroughCommand forwards everything after its name.
|
||||||
|
func goldenPassthroughCommand() Command {
|
||||||
|
return Command{
|
||||||
|
Name: "tool:exec",
|
||||||
|
Description: "Run the bundled tool with its own arguments",
|
||||||
|
Long: "Everything after the command name reaches the tool unchanged, global flags included.",
|
||||||
|
Passthrough: true,
|
||||||
|
Args: []Arg{{Name: "args", Description: "The tool's arguments"}},
|
||||||
|
Examples: []Example{{Command: "tool:exec --version", Description: "The tool's own version"}},
|
||||||
|
Run: func(context.Context, Input, Output) error { return nil },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGolden executes args on a fresh root over commands and returns stdout
|
||||||
|
// and stderr.
|
||||||
|
func runGolden(t *testing.T, commands []Command, args ...string) (string, string, int) {
|
||||||
|
t.Helper()
|
||||||
|
var out, errW bytes.Buffer
|
||||||
|
root, err := NewRootIO("acme", commands, strings.NewReader(""), &out, &errW, WithVersion(goldenVersion))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
code := Execute(context.Background(), root, args)
|
||||||
|
return out.String(), errW.String(), code
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertGolden compares got with testdata/<name>.golden, rewriting the file
|
||||||
|
// first when -update is set.
|
||||||
|
func assertGolden(t *testing.T, name string, got []byte) {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join("testdata", name+".golden")
|
||||||
|
if *update {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, got, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
want, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v (run go test -run %s -update to create it)", name, err, t.Name())
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("%s differs from %s\n%s", name, path, firstDiff(want, got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstDiff prints the first differing line of want and got with a little
|
||||||
|
// context, in a unified-diff-like form.
|
||||||
|
func firstDiff(want, got []byte) string {
|
||||||
|
wl := strings.Split(string(want), "\n")
|
||||||
|
gl := strings.Split(string(got), "\n")
|
||||||
|
for i := 0; i < max(len(wl), len(gl)); i++ {
|
||||||
|
var w, g string
|
||||||
|
if i < len(wl) {
|
||||||
|
w = wl[i]
|
||||||
|
}
|
||||||
|
if i < len(gl) {
|
||||||
|
g = gl[i]
|
||||||
|
}
|
||||||
|
if w == g {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for j := max(0, i-2); j < i; j++ {
|
||||||
|
fmt.Fprintf(&b, " %q\n", wl[j])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "- %q\n+ %q\n", w, g)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
return "(same lines, different line endings)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListGolden: the bare binary prints the D-13 list.
|
||||||
|
func TestListGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, errOut, code := runGolden(t, goldenCommands())
|
||||||
|
if code != ExitOK || errOut != "" {
|
||||||
|
t.Fatalf("exit %d, stderr %q", code, errOut)
|
||||||
|
}
|
||||||
|
assertGolden(t, "list", []byte(out))
|
||||||
|
for _, args := range [][]string{{"list"}, {"--help"}, {"-h"}, {"help"}, {"-v"}, {"--no-ansi"}} {
|
||||||
|
again, _, code := runGolden(t, goldenCommands(), args...)
|
||||||
|
if code != ExitOK || again != out {
|
||||||
|
t.Errorf("%v: exit %d, output differs from the bare list:\n%s", args, code, firstDiff([]byte(out), []byte(again)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListAllGolden: list --all adds the hidden command.
|
||||||
|
func TestListAllGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, _, code := runGolden(t, goldenCommands(), "list", "--all")
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("exit %d", code)
|
||||||
|
}
|
||||||
|
assertGolden(t, "list_all", []byte(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListJSONGolden: list --format=json prints the catalog.
|
||||||
|
func TestListJSONGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, _, code := runGolden(t, goldenCommands(), "list", "--format=json")
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("exit %d", code)
|
||||||
|
}
|
||||||
|
assertGolden(t, "list.json", []byte(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListStyledGolden: --ansi styles the list in the palette (ANSI256),
|
||||||
|
// so a palette change shows up in review.
|
||||||
|
func TestListStyledGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, _, code := runGolden(t, goldenCommands(), "--ansi")
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("exit %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "\x1b[") {
|
||||||
|
t.Fatalf("--ansi list has no escape codes:\n%q", out)
|
||||||
|
}
|
||||||
|
assertGolden(t, "list_styled", []byte(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpGolden: admin:token:create --help in the D-14 layout.
|
||||||
|
func TestHelpGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, _, code := runGolden(t, goldenCommands(), "admin:token:create", "--help")
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("exit %d", code)
|
||||||
|
}
|
||||||
|
assertGolden(t, "help_token_create", []byte(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpPassthroughGolden: the help of a passthrough command, reached
|
||||||
|
// through help <command> because --help after its name is forwarded.
|
||||||
|
func TestHelpPassthroughGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
out, _, code := runGolden(t, goldenCommands(), "help", "tool:exec")
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("exit %d", code)
|
||||||
|
}
|
||||||
|
assertGolden(t, "help_passthrough", []byte(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpEqualsHelpFlag: help <command>, <command> --help and <command>
|
||||||
|
// -h print the same bytes, with or without global flags.
|
||||||
|
func TestHelpEqualsHelpFlag(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
for _, name := range []string{"admin:token:create", "migrate", "list", "help"} {
|
||||||
|
want, _, code := runGolden(t, goldenCommands(), "help", name)
|
||||||
|
if code != ExitOK || !strings.Contains(want, "Usage:\n") {
|
||||||
|
t.Fatalf("help %s: exit %d\n%s", name, code, want)
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{name, "--help"}, {name, "-h"}, {"-v", "help", name}, {"--no-ansi", name, "--help"}} {
|
||||||
|
got, _, code := runGolden(t, goldenCommands(), args...)
|
||||||
|
if code != ExitOK || got != want {
|
||||||
|
t.Errorf("%v: exit %d, differs from help %s:\n%s", args, code, name, firstDiff([]byte(want), []byte(got)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrorGolden: the error block of D-18 for a hint with {bin}, an
|
||||||
|
// unknown command with a suggestion and the -v chain, on stderr.
|
||||||
|
func TestErrorGolden(t *testing.T) {
|
||||||
|
plainEnv(t)
|
||||||
|
hinted := Command{Name: "make:plugin", Description: "Create a plugin", Run: func(context.Context, Input, Output) error {
|
||||||
|
return WithHint(errors.New("No application found in /home/jane/acme/plugins."),
|
||||||
|
"summer make:command needs an application root (summer.yaml).",
|
||||||
|
"cd ..; {bin} make:command acme.blog:publish")
|
||||||
|
}}
|
||||||
|
chained := Command{Name: "db:check", Description: "Check the database", Run: func(context.Context, Input, Output) error {
|
||||||
|
return fmt.Errorf("db:check: %w", fmt.Errorf("connect to 127.0.0.1:5432: %w", errors.New("connection refused")))
|
||||||
|
}}
|
||||||
|
commands := append(goldenCommands(), hinted, chained)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
golden string
|
||||||
|
args []string
|
||||||
|
code int
|
||||||
|
}{
|
||||||
|
{"error_hint", []string{"make:plugin"}, ExitFailure},
|
||||||
|
{"error_unknown_command", []string{"migrat"}, ExitUsage},
|
||||||
|
{"error_chain_verbose", []string{"-v", "db:check"}, ExitFailure},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.golden, func(t *testing.T) {
|
||||||
|
out, errOut, code := runGolden(t, commands, tc.args...)
|
||||||
|
if code != tc.code {
|
||||||
|
t.Fatalf("exit %d, want %d", code, tc.code)
|
||||||
|
}
|
||||||
|
if out != "" {
|
||||||
|
t.Errorf("the error block reached stdout:\n%s", out)
|
||||||
|
}
|
||||||
|
assertGolden(t, tc.golden, []byte(errOut))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
7
modules/bonfire/testdata/error_chain_verbose.golden
vendored
Normal file
7
modules/bonfire/testdata/error_chain_verbose.golden
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
ERROR db:check: connect to 127.0.0.1:5432: connection refused
|
||||||
|
|
||||||
|
Caused by:
|
||||||
|
- connect to 127.0.0.1:5432: connection refused
|
||||||
|
- connection refused
|
||||||
|
|
||||||
10
modules/bonfire/testdata/error_hint.golden
vendored
Normal file
10
modules/bonfire/testdata/error_hint.golden
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
ERROR No application found in /home/jane/acme/plugins.
|
||||||
|
|
||||||
|
summer make:command needs an application root (summer.yaml).
|
||||||
|
|
||||||
|
Try:
|
||||||
|
cd ..; bin/acme make:command acme.blog:publish
|
||||||
|
|
||||||
|
Run with -v for details.
|
||||||
|
|
||||||
8
modules/bonfire/testdata/error_unknown_command.golden
vendored
Normal file
8
modules/bonfire/testdata/error_unknown_command.golden
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
ERROR Command "migrat" is not defined.
|
||||||
|
|
||||||
|
Try:
|
||||||
|
bin/acme migrate
|
||||||
|
|
||||||
|
Run with -v for details.
|
||||||
|
|
||||||
23
modules/bonfire/testdata/help_passthrough.golden
vendored
Normal file
23
modules/bonfire/testdata/help_passthrough.golden
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Description:
|
||||||
|
Run the bundled tool with its own arguments
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tool:exec [args] [options] [--] <args...>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
args The tool's arguments
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
-h, --help Display help for the given command
|
||||||
|
-q, --quiet Do not output any message
|
||||||
|
-v|vv|vvv Increase verbosity
|
||||||
|
--ansi|no-ansi Force (or disable) ANSI output
|
||||||
|
-n, --no-interaction Do not ask any interactive question
|
||||||
|
--env The environment the command runs under
|
||||||
|
|
||||||
|
Help:
|
||||||
|
Everything after the command name reaches the tool unchanged, global flags included.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# The tool's own version
|
||||||
|
bin/acme tool:exec --version
|
||||||
33
modules/bonfire/testdata/help_token_create.golden
vendored
Normal file
33
modules/bonfire/testdata/help_token_create.golden
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Description:
|
||||||
|
Create a personal API token for a backend admin
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
admin:token:create <login> [options]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
login The admin's login
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--scopes=SCOPES Comma-separated scopes [default: "*"]
|
||||||
|
--expires=EXPIRES Days until expiry [default: 90]
|
||||||
|
--json Print the token as JSON
|
||||||
|
-t, --tag=TAG A tag for the token (multiple values allowed)
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
-h, --help Display help for the given command
|
||||||
|
-q, --quiet Do not output any message
|
||||||
|
-v|vv|vvv Increase verbosity
|
||||||
|
--ansi|no-ansi Force (or disable) ANSI output
|
||||||
|
-n, --no-interaction Do not ask any interactive question
|
||||||
|
--env The environment the command runs under
|
||||||
|
|
||||||
|
Help:
|
||||||
|
Creates a token for the admin with the given login and prints it once.
|
||||||
|
Store it in the client right away; it cannot be shown again.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Token for the MCP server, journal only
|
||||||
|
bin/acme admin:token:create jane --scopes=journal.*
|
||||||
|
|
||||||
|
# Script-friendly output
|
||||||
|
bin/acme admin:token:create jane --json
|
||||||
26
modules/bonfire/testdata/list.golden
vendored
Normal file
26
modules/bonfire/testdata/list.golden
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
SummerCMS acme v1.4.0 (framework v0.3.2)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
bin/acme <command> [options] [arguments]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Display help for the given command
|
||||||
|
-q, --quiet Do not output any message
|
||||||
|
-v|vv|vvv Increase verbosity
|
||||||
|
--ansi|no-ansi Force (or disable) ANSI output
|
||||||
|
-n, --no-interaction Do not ask any interactive question
|
||||||
|
--env The environment the command runs under
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
help Display help for a command
|
||||||
|
list List commands
|
||||||
|
migrate Run the database migrations
|
||||||
|
serve Serve the application
|
||||||
|
admin
|
||||||
|
admin:create Create a backend administrator
|
||||||
|
admin:token:create Create a personal API token for a backend admin
|
||||||
|
make
|
||||||
|
make:command Create a new console command
|
||||||
|
make:model Create a new model and migration
|
||||||
|
tool
|
||||||
|
tool:exec Run the bundled tool with its own arguments
|
||||||
273
modules/bonfire/testdata/list.json.golden
vendored
Normal file
273
modules/bonfire/testdata/list.json.golden
vendored
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
{
|
||||||
|
"application": {
|
||||||
|
"name": "acme",
|
||||||
|
"version": "v1.4.0",
|
||||||
|
"framework": "v0.3.2"
|
||||||
|
},
|
||||||
|
"global_options": [
|
||||||
|
{
|
||||||
|
"name": "help",
|
||||||
|
"shorthand": "h",
|
||||||
|
"description": "Display help for the given command",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quiet",
|
||||||
|
"shorthand": "q",
|
||||||
|
"description": "Do not output any message",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "verbose",
|
||||||
|
"shorthand": "v",
|
||||||
|
"description": "Increase verbosity (-v, -vv or -vvv)",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ansi",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Force ANSI output",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "no-ansi",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Disable ANSI output",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "no-interaction",
|
||||||
|
"shorthand": "n",
|
||||||
|
"description": "Do not ask any interactive question",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "env",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "The environment the command runs under",
|
||||||
|
"default": "",
|
||||||
|
"bare": false,
|
||||||
|
"repeatable": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "acme:secret",
|
||||||
|
"description": "Rotate the internal signing secret",
|
||||||
|
"long": "",
|
||||||
|
"usage": "acme:secret [options]",
|
||||||
|
"hidden": true,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin:create",
|
||||||
|
"description": "Create a backend administrator",
|
||||||
|
"long": "",
|
||||||
|
"usage": "admin:create [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin:token:create",
|
||||||
|
"description": "Create a personal API token for a backend admin",
|
||||||
|
"long": "Creates a token for the admin with the given login and prints it once.\nStore it in the client right away; it cannot be shown again.",
|
||||||
|
"usage": "admin:token:create \u003clogin\u003e [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "login",
|
||||||
|
"description": "The admin's login",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"name": "scopes",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Comma-separated scopes",
|
||||||
|
"default": "*",
|
||||||
|
"bare": false,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expires",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Days until expiry",
|
||||||
|
"default": "90",
|
||||||
|
"bare": false,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "json",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Print the token as JSON",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag",
|
||||||
|
"shorthand": "t",
|
||||||
|
"description": "A tag for the token",
|
||||||
|
"default": "",
|
||||||
|
"bare": false,
|
||||||
|
"repeatable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"command": "admin:token:create jane --scopes=journal.*",
|
||||||
|
"description": "Token for the MCP server, journal only"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "admin:token:create jane --json",
|
||||||
|
"description": "Script-friendly output"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "help",
|
||||||
|
"description": "Display help for a command",
|
||||||
|
"long": "Prints the description, usage, arguments, options, long help and examples of a command. Without a command name it prints the command list.",
|
||||||
|
"usage": "help [command_name] [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "command_name",
|
||||||
|
"description": "The command name",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"command": "help migrate",
|
||||||
|
"description": "Help for the migrate command"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "List commands",
|
||||||
|
"long": "Prints the commands grouped by namespace. --all includes hidden commands; --format=json prints every command with its arguments, options and examples.",
|
||||||
|
"usage": "list [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"name": "all",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "Show hidden commands too",
|
||||||
|
"default": "",
|
||||||
|
"bare": true,
|
||||||
|
"repeatable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "format",
|
||||||
|
"shorthand": "",
|
||||||
|
"description": "The output format (txt or json)",
|
||||||
|
"default": "txt",
|
||||||
|
"bare": false,
|
||||||
|
"repeatable": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"command": "list --all",
|
||||||
|
"description": "Every command, hidden ones included"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "list --format=json",
|
||||||
|
"description": "The catalog for scripts and agents"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "make:command",
|
||||||
|
"description": "Create a new console command",
|
||||||
|
"long": "",
|
||||||
|
"usage": "make:command [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "make:model",
|
||||||
|
"description": "Create a new model and migration",
|
||||||
|
"long": "",
|
||||||
|
"usage": "make:model [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "migrate",
|
||||||
|
"description": "Run the database migrations",
|
||||||
|
"long": "",
|
||||||
|
"usage": "migrate [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serve",
|
||||||
|
"description": "Serve the application",
|
||||||
|
"long": "",
|
||||||
|
"usage": "serve [options]",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [],
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tool:exec",
|
||||||
|
"description": "Run the bundled tool with its own arguments",
|
||||||
|
"long": "Everything after the command name reaches the tool unchanged, global flags included.",
|
||||||
|
"usage": "tool:exec [args] [options] [--] \u003cargs...\u003e",
|
||||||
|
"hidden": false,
|
||||||
|
"passthrough": true,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "args",
|
||||||
|
"description": "The tool's arguments",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"command": "tool:exec --version",
|
||||||
|
"description": "The tool's own version"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
28
modules/bonfire/testdata/list_all.golden
vendored
Normal file
28
modules/bonfire/testdata/list_all.golden
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
SummerCMS acme v1.4.0 (framework v0.3.2)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
bin/acme <command> [options] [arguments]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Display help for the given command
|
||||||
|
-q, --quiet Do not output any message
|
||||||
|
-v|vv|vvv Increase verbosity
|
||||||
|
--ansi|no-ansi Force (or disable) ANSI output
|
||||||
|
-n, --no-interaction Do not ask any interactive question
|
||||||
|
--env The environment the command runs under
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
help Display help for a command
|
||||||
|
list List commands
|
||||||
|
migrate Run the database migrations
|
||||||
|
serve Serve the application
|
||||||
|
acme
|
||||||
|
acme:secret Rotate the internal signing secret
|
||||||
|
admin
|
||||||
|
admin:create Create a backend administrator
|
||||||
|
admin:token:create Create a personal API token for a backend admin
|
||||||
|
make
|
||||||
|
make:command Create a new console command
|
||||||
|
make:model Create a new model and migration
|
||||||
|
tool
|
||||||
|
tool:exec Run the bundled tool with its own arguments
|
||||||
26
modules/bonfire/testdata/list_styled.golden
vendored
Normal file
26
modules/bonfire/testdata/list_styled.golden
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[1mSummerCMS acme v1.4.0 (framework v0.3.2)[m
|
||||||
|
|
||||||
|
[1mUsage:[m
|
||||||
|
bin/acme <command> [options] [arguments]
|
||||||
|
|
||||||
|
[1mOptions:[m
|
||||||
|
[32m-h, --help[m Display help for the given command
|
||||||
|
[32m-q, --quiet[m Do not output any message
|
||||||
|
[32m-v|vv|vvv[m Increase verbosity
|
||||||
|
[32m --ansi|no-ansi[m Force (or disable) ANSI output
|
||||||
|
[32m-n, --no-interaction[m Do not ask any interactive question
|
||||||
|
[32m --env[m The environment the command runs under
|
||||||
|
|
||||||
|
[1mAvailable commands:[m
|
||||||
|
[32mhelp[m Display help for a command
|
||||||
|
[32mlist[m List commands
|
||||||
|
[32mmigrate[m Run the database migrations
|
||||||
|
[32mserve[m Serve the application
|
||||||
|
[1madmin[m
|
||||||
|
[32madmin:create[m Create a backend administrator
|
||||||
|
[32madmin:token:create[m Create a personal API token for a backend admin
|
||||||
|
[1mmake[m
|
||||||
|
[32mmake:command[m Create a new console command
|
||||||
|
[32mmake:model[m Create a new model and migration
|
||||||
|
[1mtool[m
|
||||||
|
[32mtool:exec[m Run the bundled tool with its own arguments
|
||||||
75
modules/bonfire/version_test.go
Normal file
75
modules/bonfire/version_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package bonfire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime/debug"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestVersionHeader: the list header and BuildVersion for a tagged
|
||||||
|
// framework dependency, a replaced (local) one, a go.work build (devel)
|
||||||
|
// and the summer tool's own build, read through an injected build info.
|
||||||
|
func TestVersionHeader(t *testing.T) {
|
||||||
|
headers := []struct {
|
||||||
|
v Version
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{Version{App: "v1.4.0", Framework: "v0.3.2"}, "acme", "SummerCMS acme v1.4.0 (framework v0.3.2)"},
|
||||||
|
{Version{App: "devel", Framework: "v0.4.0", Local: true}, "acme", "SummerCMS acme devel (framework v0.4.0, local)"},
|
||||||
|
{Version{App: "v1.4.1-0.20261021120000-abcdef123456+dirty", Framework: "(devel)"}, "acme", "SummerCMS acme v1.4.1-0.20261021120000-abcdef123456+dirty (framework devel)"},
|
||||||
|
// Equal versions collapse to one, as for the summer tool's own build.
|
||||||
|
{Version{App: "(devel)", Framework: "(devel)"}, "acme", "SummerCMS acme devel"},
|
||||||
|
{Version{App: "v0.4.0", Framework: "v0.4.0"}, "summer", "SummerCMS summer v0.4.0"},
|
||||||
|
{Version{App: "v0.4.0", Framework: "v0.4.0", Local: true}, "acme", "SummerCMS acme v0.4.0 (framework v0.4.0, local)"},
|
||||||
|
{Version{}, "acme", "SummerCMS acme devel"},
|
||||||
|
}
|
||||||
|
for _, tc := range headers {
|
||||||
|
if got := tc.v.Header(tc.name); got != tc.want {
|
||||||
|
t.Errorf("%+v.Header(%q) = %q, want %q", tc.v, tc.name, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dep := func(version string, replaced bool) *debug.Module {
|
||||||
|
m := &debug.Module{Path: frameworkModule, Version: version}
|
||||||
|
if replaced {
|
||||||
|
m.Replace = &debug.Module{Path: "../summercms.go", Version: "(devel)"}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
builds := []struct {
|
||||||
|
name string
|
||||||
|
info *debug.BuildInfo
|
||||||
|
ok bool
|
||||||
|
want Version
|
||||||
|
}{
|
||||||
|
{"tagged", &debug.BuildInfo{Main: debug.Module{Path: "example.com/acme", Version: "v1.4.0"},
|
||||||
|
Deps: []*debug.Module{{Path: "example.com/other", Version: "v9.9.9"}, dep("v0.3.2", false)}},
|
||||||
|
true, Version{App: "v1.4.0", Framework: "v0.3.2"}},
|
||||||
|
{"replaced", &debug.BuildInfo{Main: debug.Module{Path: "example.com/acme", Version: "(devel)"},
|
||||||
|
Deps: []*debug.Module{dep("v0.4.0", true)}},
|
||||||
|
true, Version{App: "devel", Framework: "v0.4.0", Local: true}},
|
||||||
|
{"go.work", &debug.BuildInfo{Main: debug.Module{Path: "example.com/acme", Version: "v1.4.1-0.20261021120000-abcdef123456+dirty"},
|
||||||
|
Deps: []*debug.Module{dep("(devel)", false)}},
|
||||||
|
true, Version{App: "v1.4.1-0.20261021120000-abcdef123456+dirty", Framework: "devel"}},
|
||||||
|
{"no framework dep", &debug.BuildInfo{Main: debug.Module{Path: "example.com/acme", Version: "v1.0.0"}},
|
||||||
|
true, Version{App: "v1.0.0", Framework: "devel"}},
|
||||||
|
{"summer", &debug.BuildInfo{Main: debug.Module{Path: frameworkModule, Version: "v0.4.0"}},
|
||||||
|
true, Version{App: "v0.4.0", Framework: "v0.4.0"}},
|
||||||
|
{"no build info", nil, false, Version{App: "devel", Framework: "devel"}},
|
||||||
|
}
|
||||||
|
for _, tc := range builds {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
prev := readBuildInfo
|
||||||
|
t.Cleanup(func() { readBuildInfo = prev })
|
||||||
|
readBuildInfo = func() (*debug.BuildInfo, bool) { return tc.info, tc.ok }
|
||||||
|
if got := BuildVersion(); got != tc.want {
|
||||||
|
t.Errorf("BuildVersion() = %+v, want %+v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The test binary is built from the framework module itself.
|
||||||
|
if got := BuildVersion(); got.App != got.Framework || got.Local {
|
||||||
|
t.Errorf("the framework's own build = %+v, want App == Framework", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user