feat(diary): boot list, help and completion requests quietly (quick-261028-rqc)
- helpOnly also counts no command word, list, help, cobra's shell completion requests and --help=<bool>, read the way pflag does - TestHelpOnlyAgreesWithCobra pins the classification against bonfire's real dispatch, with the passthrough --help case as the exception - console and logging docs describe which invocations boot quietly
This commit is contained in:
@@ -34,7 +34,7 @@ Use cases:
|
||||
- Redaction: every channel leaf is wrapped by `sunscreen.Wrap`.
|
||||
- Runtime verbosity: `diary.SetVerbosity` lets info (1) or debug (2) through for every component and adds source locations on styled lines (3); quiet raises only the console channels to warn.
|
||||
- Console global options: `diary.FromGlobals` turns `-v`/`-vv`/`-vvv`, `-q` and `--ansi`/`--no-ansi` into `diary.Options`, and `diary.ApplyGlobals` applies them to the running setup (the generated main registers it with `bonfire.OnGlobals`).
|
||||
- Quiet boot for help: `diary.FromGlobals` also records the process arguments (`os.Args[1:]`) in `diary.Options`, and `diary.Bootstrap` and `diary.Setup` start the console channels at warnings, as `-q` would, when those arguments only ask for help (`-h` or `--help`) and no `-v` is given. Daily files and stderr channels keep their levels, warnings and errors still print, and an existing generated main gets it from a framework upgrade alone. A `diary.Options` literal without arguments does no check.
|
||||
- Quiet boot for help: `diary.FromGlobals` also records the process arguments (`os.Args[1:]`) in `diary.Options`, and `diary.Bootstrap` and `diary.Setup` start the console channels at warnings, as `-q` would, when those arguments only ask the binary to describe itself and no `-v` is given: no command, `list`, `help`, `-h` or `--help` (also after a command name) and shell completion requests. A command that passes its arguments through unparsed counts as asking for help when `--help` follows it; `diary.ApplyGlobals` restores the configured console levels before it runs. Daily files and stderr channels keep their levels, warnings and errors still print, and an existing generated main gets it from a framework upgrade alone. A `diary.Options` literal without arguments does no check.
|
||||
- The process default logger (`slog.Default`) and the standard `log` package log through the configured channels without a component.
|
||||
|
||||
## Usage
|
||||
@@ -158,4 +158,4 @@ Environment variables use the `SUMMER_LOG__` prefix with a double underscore bet
|
||||
go test ./modules/diary/...
|
||||
```
|
||||
|
||||
`TestDiaryTracerSmoke` loads a `config/log.yaml` through `compass`, logs one record from a component logger and checks that it reaches the console and the daily file redacted. `TestConfigEnvSmoke` parses `SUMMER_LOG__` overrides given as strings and checks that every invalid value fails with an error naming its key. `TestDailyDegradesSmoke` points the daily channel at a read-only directory and checks that it warns once and the console keeps logging. `TestBootQuietSmoke` runs the generated main's sequence with a plugin that logs while it registers and boots, and checks that `serve --help` keeps its info lines off the console (but not its warning, nor the daily file) while `serve` prints them. The tests write to temporary directories and need no external services.
|
||||
`TestDiaryTracerSmoke` loads a `config/log.yaml` through `compass`, logs one record from a component logger and checks that it reaches the console and the daily file redacted. `TestConfigEnvSmoke` parses `SUMMER_LOG__` overrides given as strings and checks that every invalid value fails with an error naming its key. `TestDailyDegradesSmoke` points the daily channel at a read-only directory and checks that it warns once and the console keeps logging. `TestBootQuietSmoke` runs the generated main's sequence with a plugin that logs while it registers and boots, and checks that `serve --help` keeps its info lines off the console (but not its warning, nor the daily file) while `serve` prints them. `TestHelpOnly` lists the argument shapes that count as asking for help, and `TestHelpOnlyAgreesWithCobra` runs each through a bonfire root and checks that a command runs exactly when they do not. The tests write to temporary directories and need no external services.
|
||||
|
||||
@@ -82,6 +82,12 @@ func TestBootQuietSmoke(t *testing.T) {
|
||||
{name: "serve --help", args: []string{"serve", "--help"}},
|
||||
{name: "serve", args: []string{"serve"}, wantInfo: true},
|
||||
{name: "literal options", args: []string{"serve", "--help"}, literal: true, wantInfo: true},
|
||||
{name: "no args", args: []string{}},
|
||||
{name: "list", args: []string{"list"}},
|
||||
{name: "help serve", args: []string{"help", "serve"}},
|
||||
{name: "-h", args: []string{"-h"}},
|
||||
{name: "-v serve --help", args: []string{"-v", "serve", "--help"}, wantInfo: true},
|
||||
{name: "queue:work", args: []string{"queue:work"}, wantInfo: true},
|
||||
}
|
||||
for _, row := range rows {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
|
||||
10
modules/diary/export_test.go
Normal file
10
modules/diary/export_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package diary
|
||||
|
||||
// HelpOnly exposes helpOnly to the external tests.
|
||||
var HelpOnly = helpOnly
|
||||
|
||||
// The completion command names helpOnly recognises, for the cobra check.
|
||||
const (
|
||||
CompleteRequestCmd = completeRequestCmd
|
||||
CompleteNoDescRequestCmd = completeNoDescRequestCmd
|
||||
)
|
||||
@@ -1,47 +1,100 @@
|
||||
package diary
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The hidden commands of cobra's shell completion (cobra.ShellCompRequestCmd
|
||||
// and cobra.ShellCompNoDescRequestCmd), named here so diary does not link
|
||||
// cobra; TestHelpOnlyAgreesWithCobra pins them.
|
||||
const (
|
||||
completeRequestCmd = "__complete"
|
||||
completeNoDescRequestCmd = "__completeNoDesc"
|
||||
)
|
||||
|
||||
// helpOnly reports whether args, a binary's command line without the
|
||||
// program name, only ask for help: before "--" they hold --help or a
|
||||
// single-dash cluster that reaches h. It is a tolerant pre-scan of the raw
|
||||
// arguments, made before plugins boot, and reads them the way
|
||||
// bonfire.PeekGlobals does: it stops at "--", skips the value of --env and
|
||||
// reads a single-dash cluster letter by letter, stopping at the first
|
||||
// letter that is not a global shorthand (v, q, n, h). It never fails and
|
||||
// cannot tell a passthrough command (whose --help reaches its Run) from
|
||||
// another, so "<passthrough> --help" counts; the root's OnGlobals hook
|
||||
// (ApplyGlobals) restores the console levels before that command runs.
|
||||
// program name, only ask the binary to describe itself, so no command runs:
|
||||
//
|
||||
// - there is no command word (the first token before "--" that is neither
|
||||
// a flag nor the value of --env, bonfire's rule under which Execute
|
||||
// prints the list): a bare invocation, only global options, -h or
|
||||
// --help;
|
||||
// - the command word is list, help or one of cobra's shell completion
|
||||
// requests (__complete, __completeNoDesc);
|
||||
// - the help flag is set before "--", also after the command word, the
|
||||
// way cobra honours it: --help, --help=<bool> read by strconv.ParseBool
|
||||
// the way pflag does (the last occurrence wins, an unparsable value is
|
||||
// ignored), or a single-dash cluster that reaches h (-h, -vh, -nh).
|
||||
//
|
||||
// It is a tolerant pre-scan of the raw arguments, made before plugins boot,
|
||||
// and reads them the way bonfire.PeekGlobals does: it stops at "--", skips
|
||||
// the value of --env and reads a single-dash cluster letter by letter,
|
||||
// stopping at the first letter that is not a global shorthand (v, q, n, h).
|
||||
// It never fails. An unknown or misspelled command word is not help-only
|
||||
// (that is unknowable before boot). It cannot tell a passthrough command
|
||||
// (whose --help reaches its Run) from another, so "<passthrough> --help"
|
||||
// counts; the root's OnGlobals hook (ApplyGlobals) restores the console
|
||||
// levels before that command runs.
|
||||
func helpOnly(args []string) bool {
|
||||
help := false
|
||||
word := ""
|
||||
scan:
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
switch {
|
||||
case a == "--":
|
||||
return false
|
||||
break scan
|
||||
case a == "--env":
|
||||
i++
|
||||
case a == "--help":
|
||||
return true
|
||||
case len(a) >= 2 && a[0] == '-' && a[1] != '-':
|
||||
if clusterHasHelp(a[1:]) {
|
||||
return true
|
||||
help = true
|
||||
case strings.HasPrefix(a, "--help="):
|
||||
if v, err := strconv.ParseBool(strings.TrimPrefix(a, "--help=")); err == nil {
|
||||
help = v
|
||||
}
|
||||
case len(a) >= 2 && a[0] == '-' && a[1] != '-':
|
||||
if v, ok := clusterHelp(a[1:]); ok {
|
||||
help = v
|
||||
}
|
||||
case strings.HasPrefix(a, "-"):
|
||||
case word == "":
|
||||
word = a
|
||||
}
|
||||
}
|
||||
return false
|
||||
switch word {
|
||||
case "", "list", "help", completeRequestCmd, completeNoDescRequestCmd:
|
||||
return true
|
||||
}
|
||||
return help
|
||||
}
|
||||
|
||||
// clusterHasHelp reports whether a shorthand cluster (the letters after the
|
||||
// dash) reaches h before a letter that is not a global shorthand.
|
||||
func clusterHasHelp(cluster string) bool {
|
||||
// clusterHelp reads a shorthand cluster (the letters after the dash) the
|
||||
// way bonfire.PeekGlobals does and reports the help value it sets: ok is
|
||||
// true when the cluster reaches h before a letter that is not a global
|
||||
// shorthand, and -h=<bool> (-vh=false) sets the value read by
|
||||
// strconv.ParseBool.
|
||||
func clusterHelp(cluster string) (help, ok bool) {
|
||||
for i := 0; i < len(cluster); i++ {
|
||||
switch cluster[i] {
|
||||
r := cluster[i]
|
||||
if i+1 < len(cluster) && cluster[i+1] == '=' {
|
||||
if r != 'h' {
|
||||
return help, ok
|
||||
}
|
||||
if v, err := strconv.ParseBool(cluster[i+2:]); err == nil {
|
||||
return v, true
|
||||
}
|
||||
return help, ok
|
||||
}
|
||||
switch r {
|
||||
case 'h':
|
||||
return true
|
||||
help, ok = true, true
|
||||
case 'v', 'q', 'n':
|
||||
default:
|
||||
return false
|
||||
return help, ok
|
||||
}
|
||||
}
|
||||
return false
|
||||
return help, ok
|
||||
}
|
||||
|
||||
// bootQuiet applies the help-only rule of Bootstrap and Setup: with Args
|
||||
|
||||
133
modules/diary/helponly_test.go
Normal file
133
modules/diary/helponly_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package diary_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/diary"
|
||||
)
|
||||
|
||||
// helpOnlyCases are the rows shared by TestHelpOnly and
|
||||
// TestHelpOnlyAgreesWithCobra. usage marks the rows cobra rejects as a
|
||||
// usage error, which the agreement check leaves out.
|
||||
var helpOnlyCases = []struct {
|
||||
args []string
|
||||
want bool
|
||||
usage bool
|
||||
}{
|
||||
{args: []string{}, want: true},
|
||||
{args: []string{"--help"}, want: true},
|
||||
{args: []string{"-h"}, want: true},
|
||||
{args: []string{"serve", "--help"}, want: true},
|
||||
{args: []string{"serve", "-h"}, want: true},
|
||||
{args: []string{"-h", "serve"}, want: true},
|
||||
{args: []string{"serve", "--help=true"}, want: true},
|
||||
{args: []string{"serve", "--help=1"}, want: true},
|
||||
{args: []string{"-nh", "serve"}, want: true},
|
||||
{args: []string{"-vh", "serve"}, want: true},
|
||||
{args: []string{"serve", "-vh=true"}, want: true},
|
||||
{args: []string{"list"}, want: true},
|
||||
{args: []string{"list", "--format=json"}, want: true},
|
||||
{args: []string{"help"}, want: true},
|
||||
{args: []string{"help", "serve"}, want: true},
|
||||
{args: []string{"--env", "staging"}, want: true},
|
||||
{args: []string{"--env=staging"}, want: true},
|
||||
{args: []string{"-v"}, want: true},
|
||||
{args: []string{"--help=false"}, want: true},
|
||||
{args: []string{"--env", "help"}, want: true},
|
||||
{args: []string{"__complete", "serve", ""}, want: true},
|
||||
{args: []string{"__completeNoDesc", "se"}, want: true},
|
||||
|
||||
{args: []string{"serve"}},
|
||||
{args: []string{"migrate", "-v"}},
|
||||
{args: []string{"queue:work"}},
|
||||
{args: []string{"--env", "list", "serve"}},
|
||||
{args: []string{"serve", "--", "--help"}},
|
||||
{args: []string{"serve", "--help=false"}},
|
||||
{args: []string{"serve", "--help", "--help=false"}},
|
||||
{args: []string{"serve", "-h=false"}},
|
||||
{args: []string{"serve", "-qv"}},
|
||||
{args: []string{"serve", "-xh"}, usage: true},
|
||||
{args: []string{"serve", "--help=yes"}, usage: true},
|
||||
{args: []string{"blog:import", "https://example.com/help"}},
|
||||
{args: []string{"blog:import", "help"}},
|
||||
{args: []string{"-q", "--env=list", "migrate"}},
|
||||
}
|
||||
|
||||
func TestHelpOnly(t *testing.T) {
|
||||
for _, tc := range helpOnlyCases {
|
||||
if got := diary.HelpOnly(tc.args); got != tc.want {
|
||||
t.Errorf("helpOnly(%q) = %v, want %v", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helpOnlyRoot builds a root whose commands record whether their Run ran.
|
||||
func helpOnlyRoot(t *testing.T, ran *bool) *cobra.Command {
|
||||
t.Helper()
|
||||
run := func(context.Context, bonfire.Input, bonfire.Output) error {
|
||||
*ran = true
|
||||
return nil
|
||||
}
|
||||
optional := []bonfire.Arg{{Name: "target", Description: "An optional argument"}}
|
||||
commands := []bonfire.Command{
|
||||
{Name: "serve", Description: "Start the HTTP server", Args: optional, Run: run},
|
||||
{Name: "migrate", Description: "Run the migrations", Run: run},
|
||||
{Name: "queue:work", Description: "Run the job workers", Run: run},
|
||||
{Name: "blog:import", Description: "Import posts", Args: optional, Run: run},
|
||||
{Name: "acme:tool", Description: "Run a tool with its own flags", Passthrough: true, Run: run},
|
||||
}
|
||||
root, err := bonfire.NewRootIO("acme", commands, strings.NewReader(""), io.Discard, io.Discard,
|
||||
bonfire.WithVersion(bonfire.Version{App: "v1.0.0", Framework: "v1.0.0"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// TestHelpOnlyAgreesWithCobra: for every row cobra accepts, a command runs
|
||||
// exactly when helpOnly reports false. A passthrough command is the
|
||||
// documented exception: its --help reaches its Run, while helpOnly, which
|
||||
// cannot know the command before boot, counts it.
|
||||
func TestHelpOnlyAgreesWithCobra(t *testing.T) {
|
||||
for _, tc := range helpOnlyCases {
|
||||
var ran bool
|
||||
code := bonfire.Execute(context.Background(), helpOnlyRoot(t, &ran), tc.args)
|
||||
if tc.usage {
|
||||
if code != bonfire.ExitUsage {
|
||||
t.Errorf("Execute(%q) = %d, want the usage error %d", tc.args, code, bonfire.ExitUsage)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if code != bonfire.ExitOK {
|
||||
t.Errorf("Execute(%q) = %d, want 0", tc.args, code)
|
||||
continue
|
||||
}
|
||||
if ran != !diary.HelpOnly(tc.args) {
|
||||
t.Errorf("Execute(%q) ran a command = %v, but helpOnly = %v", tc.args, ran, diary.HelpOnly(tc.args))
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("passthrough --help", func(t *testing.T) {
|
||||
args := []string{"acme:tool", "--help"}
|
||||
var ran bool
|
||||
if code := bonfire.Execute(context.Background(), helpOnlyRoot(t, &ran), args); code != bonfire.ExitOK {
|
||||
t.Fatalf("Execute(%q) = %d, want 0", args, code)
|
||||
}
|
||||
if !ran || !diary.HelpOnly(args) {
|
||||
t.Errorf("Execute(%q) ran = %v, helpOnly = %v; want both true (the documented exception)", args, ran, diary.HelpOnly(args))
|
||||
}
|
||||
})
|
||||
|
||||
if diary.CompleteRequestCmd != cobra.ShellCompRequestCmd {
|
||||
t.Errorf("completion command %q, cobra uses %q", diary.CompleteRequestCmd, cobra.ShellCompRequestCmd)
|
||||
}
|
||||
if diary.CompleteNoDescRequestCmd != cobra.ShellCompNoDescRequestCmd {
|
||||
t.Errorf("completion command %q, cobra uses %q", diary.CompleteNoDescRequestCmd, cobra.ShellCompNoDescRequestCmd)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user