Files
summercms/modules/diary/bootquiet_smoke_test.go
Jakub Zych 6c606c666b 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
2026-10-28 21:47:42 +01:00

198 lines
6.5 KiB
Go

package diary_test
import (
"bytes"
"context"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/diary"
"git.golem15.com/golem15/summercms/modules/party"
)
// The records the boot-logging test plugin writes.
const (
bootRegisterInfo = "operators registered"
bootBootInfo = "registries collected"
bootBootWarn = "registry without operators"
)
// bootLogPlugin logs from Register through slog.Default, the way a plugin
// written before diary does, and from Boot through a component logger.
type bootLogPlugin struct{}
func (bootLogPlugin) ID() string { return "acme.bootlog" }
func (bootLogPlugin) Requires() []string { return nil }
func (bootLogPlugin) Register(*backpack.App) error {
slog.Default().Info(bootRegisterInfo, "count", 2)
return nil
}
func (bootLogPlugin) Boot(*backpack.App) error {
log := diary.For("bootlog")
log.Info(bootBootInfo, "count", 3)
log.Warn(bootBootWarn, "registry", "refunds")
return nil
}
func init() {
party.Register(bootLogPlugin{})
}
// bootLogYAML is a console plus a daily file at info, as in the tracer test.
const bootLogYAML = `default: stack
level: info
channels:
stack:
driver: stack
channels: [console, daily]
console:
driver: console
format: auto
daily:
driver: daily
path: storage/logs/summer.log
days: 14
format: json
level: info
`
// TestBootQuietSmoke runs the generated main's sequence (PeekGlobals,
// Bootstrap and Setup with FromGlobals, party.Activate, a root with
// OnGlobals(diary.ApplyGlobals), Execute) with a plugin that logs while it
// registers and boots: an invocation that only asks for help keeps the boot
// Info records off the console, every other one prints them, and the daily
// file and the console's warnings are never affected.
func TestBootQuietSmoke(t *testing.T) {
rows := []struct {
name string
args []string
literal bool // Options as a struct literal, without Args
wantInfo bool // the boot Info records reach the console
}{
{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) {
t.Cleanup(diary.Reset)
root := t.TempDir()
configDir := filepath.Join(root, "config")
if err := os.MkdirAll(configDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "log.yaml"), []byte(bootLogYAML), 0o644); err != nil {
t.Fatal(err)
}
var console, stdout, stderr bytes.Buffer
now := func() time.Time { return time.Date(2026, 10, 28, 12, 0, 0, 0, time.Local) }
g := bonfire.PeekGlobals(row.args)
var opts diary.Options
if row.literal {
opts = diary.Options{Verbosity: g.Verbosity, Quiet: g.Quiet, Color: g.Color}
} else {
opts = diary.FromGlobals(g)
opts.Args = slices.Clone(row.args)
if opts.Args == nil {
opts.Args = []string{}
}
}
opts.Root, opts.Stderr, opts.Environ, opts.Now = root, &console, []string{}, now
diary.Bootstrap(&console, opts)
cfg, err := compass.Open(compass.Options{Dir: configDir, Env: g.Env, Environ: []string{}})
if err != nil {
t.Fatal(err)
}
if err := diary.Setup(cfg, opts); err != nil {
t.Fatalf("Setup: %v", err)
}
app := backpack.New(cfg)
if _, err := party.Activate(app, []string{"acme.bootlog"}); err != nil {
t.Fatalf("Activate: %v", err)
}
commands := []bonfire.Command{
{Name: "serve", Description: "Start the HTTP server", Run: func(context.Context, bonfire.Input, bonfire.Output) error { return nil }},
{Name: "queue:work", Description: "Run the job workers", Run: func(context.Context, bonfire.Input, bonfire.Output) error { return nil }},
}
rootCmd, err := bonfire.NewRootIO("acme", commands, strings.NewReader(""), &stdout, &stderr,
bonfire.WithVersion(bonfire.Version{App: "v1.0.0", Framework: "v1.0.0"}), bonfire.OnGlobals(diary.ApplyGlobals))
if err != nil {
t.Fatal(err)
}
if code := bonfire.Execute(context.Background(), rootCmd, row.args); code != bonfire.ExitOK {
t.Fatalf("Execute(%q) = %d, want 0; stderr:\n%s", row.args, code, stderr.String())
}
if slices.Contains(row.args, "--help") && !strings.Contains(stdout.String(), "serve") {
t.Errorf("Execute(%q) printed no help:\n%s", row.args, stdout.String())
}
out := console.String()
for _, msg := range []string{bootRegisterInfo, bootBootInfo} {
if got := strings.Contains(out, msg); got != row.wantInfo {
t.Errorf("console holds %q = %v, want %v:\n%s", msg, got, row.wantInfo, out)
}
}
if !strings.Contains(out, bootBootWarn) {
t.Errorf("console lacks the boot warning %q:\n%s", bootBootWarn, out)
}
if err := diary.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
files, err := filepath.Glob(filepath.Join(root, "storage", "logs", "*.log"))
if err != nil || len(files) != 1 {
t.Fatalf("daily files = %v (%v), want one", files, err)
}
raw, err := os.ReadFile(files[0])
if err != nil {
t.Fatal(err)
}
for _, msg := range []string{bootRegisterInfo, bootBootInfo, bootBootWarn} {
if !bytes.Contains(raw, []byte(msg)) {
t.Errorf("daily file lacks %q:\n%s", msg, raw)
}
}
})
}
}
// TestFromGlobalsArgs: FromGlobals records a copy of the process arguments
// without the program name, never nil.
func TestFromGlobalsArgs(t *testing.T) {
saved := os.Args
t.Cleanup(func() { os.Args = saved })
os.Args = []string{"acme", "serve", "--help"}
opts := diary.FromGlobals(bonfire.Globals{})
if want := []string{"serve", "--help"}; !slices.Equal(opts.Args, want) {
t.Fatalf("Args = %q, want %q", opts.Args, want)
}
opts.Args[0] = "migrate"
if os.Args[1] != "serve" {
t.Errorf("changing Args changed os.Args to %q", os.Args)
}
os.Args = []string{"acme"}
if opts := diary.FromGlobals(bonfire.Globals{}); opts.Args == nil || len(opts.Args) != 0 {
t.Errorf("Args = %#v, want an empty non-nil slice", opts.Args)
}
}