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