[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# Cobra Commands, Hooks, and Args Validators
|
||||
|
||||
## The Run\* lifecycle
|
||||
|
||||
Cobra commands have five run hooks. Cobra executes them in this fixed order:
|
||||
|
||||
```
|
||||
PersistentPreRunE → PreRunE → RunE → PostRunE → PersistentPostRunE
|
||||
```
|
||||
|
||||
Each `*E` hook returns `error`. The non-`*E` variants (`PersistentPreRun`, `PreRun`, `Run`, `PostRun`, `PersistentPostRun`) have signature `func(cmd *cobra.Command, args []string)` — they cannot signal failure without `os.Exit` or panic. **Always use the `*E` variants.**
|
||||
|
||||
### Which hook to use
|
||||
|
||||
| Hook | Scope | When to use |
|
||||
| --- | --- | --- |
|
||||
| `PersistentPreRunE` | Parent + all descendants | Config init, auth check, telemetry setup — must run before every subcommand |
|
||||
| `PreRunE` | This command only | Validation that runs only for this command before `RunE` |
|
||||
| `RunE` | This command only | Main handler — the primary business logic |
|
||||
| `PostRunE` | This command only | Cleanup that runs only if `RunE` succeeded |
|
||||
| `PersistentPostRunE` | Parent + all descendants | Global cleanup (close connections, flush buffers) |
|
||||
|
||||
### Inheritance rules
|
||||
|
||||
`PersistentPreRunE` defined on the root command runs before every subcommand. But if a child command defines **its own** `PersistentPreRunE`, it **replaces** (does not chain) the parent's hook. Call the parent explicitly if you need both:
|
||||
|
||||
```go
|
||||
var childCmd = &cobra.Command{
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// call parent's hook first
|
||||
if err := rootCmd.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
// child-specific logic
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Execution stops on first error
|
||||
|
||||
If `PersistentPreRunE` returns an error, cobra stops — `RunE` and later hooks never run. Use this for fail-fast auth checks.
|
||||
|
||||
## Args validators
|
||||
|
||||
Args validators run before `RunE`. Cobra prints a clear error message and exits without calling `RunE` when validation fails.
|
||||
|
||||
### Built-in validators
|
||||
|
||||
```go
|
||||
cobra.NoArgs // fails if any positional args provided
|
||||
cobra.ArbitraryArgs // accepts any number of args (default)
|
||||
cobra.ExactArgs(n int) // requires exactly n args
|
||||
cobra.MinimumNArgs(n int) // requires at least n args
|
||||
cobra.MaximumNArgs(n int) // requires at most n args
|
||||
cobra.RangeArgs(min, max int) // requires between min and max args
|
||||
cobra.OnlyValidArgs // all args must be in ValidArgs list
|
||||
cobra.ExactValidArgs(n int) // exactly n args, all in ValidArgs
|
||||
```
|
||||
|
||||
### Composing validators with MatchAll
|
||||
|
||||
```go
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete <resource>",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
ValidArgs: []string{"pod", "service", "deployment"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return doDelete(args[0])
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Custom validators
|
||||
|
||||
Signature: `func(cmd *cobra.Command, args []string) error`
|
||||
|
||||
```go
|
||||
func validatePositiveInt(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("requires exactly 1 arg, got %d", len(args))
|
||||
}
|
||||
n, err := strconv.Atoi(args[0])
|
||||
if err != nil || n <= 0 {
|
||||
return fmt.Errorf("argument must be a positive integer, got %q", args[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Args: validatePositiveInt,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Combine custom validators with built-in ones using `MatchAll`:
|
||||
|
||||
```go
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(1), validateAllPositive),
|
||||
```
|
||||
|
||||
## Command registration and ordering
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// groups must be registered before AddCommand
|
||||
rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands:"})
|
||||
rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"})
|
||||
|
||||
serveCmd.GroupID = "core"
|
||||
migrateCmd.GroupID = "management"
|
||||
|
||||
rootCmd.AddCommand(serveCmd, migrateCmd, versionCmd)
|
||||
}
|
||||
```
|
||||
|
||||
`versionCmd` has no `GroupID` — it appears in the default section.
|
||||
|
||||
## Annotations
|
||||
|
||||
Cobra supports arbitrary command annotations for framework-level metadata:
|
||||
|
||||
```go
|
||||
var serveCmd = &cobra.Command{
|
||||
Annotations: map[string]string{
|
||||
"category": "network",
|
||||
"requires-auth": "true",
|
||||
},
|
||||
}
|
||||
|
||||
// read in a middleware hook:
|
||||
if serveCmd.Annotations["requires-auth"] == "true" {
|
||||
// enforce auth
|
||||
}
|
||||
```
|
||||
|
||||
## Hidden and deprecated commands
|
||||
|
||||
```go
|
||||
var internalCmd = &cobra.Command{
|
||||
Hidden: true, // not shown in help, still executable
|
||||
}
|
||||
|
||||
var oldCmd = &cobra.Command{
|
||||
Deprecated: "use `newcmd` instead", // shown in help, prints warning on use
|
||||
}
|
||||
```
|
||||
|
||||
## cobra.CheckErr
|
||||
|
||||
`cobra.CheckErr(err)` is a convenience function: if `err != nil`, it prints the error to `cmd.ErrOrStderr()` and calls `os.Exit(1)`. Use it only in `main()` where you want a hard exit — not inside `RunE` where returning the error is preferred.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cobra.CheckErr(rootCmd.Execute())
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Cobra Shell Completions Reference
|
||||
|
||||
Cobra generates shell completion scripts for bash, zsh, fish, and PowerShell automatically. Subcommand names and flag names are completed for free. You add completions for flag values and positional arguments.
|
||||
|
||||
## Built-in completion command
|
||||
|
||||
Cobra registers a `completion` subcommand automatically:
|
||||
|
||||
```bash
|
||||
myapp completion bash # generate bash script
|
||||
myapp completion zsh # generate zsh script
|
||||
myapp completion fish # generate fish script
|
||||
myapp completion powershell
|
||||
|
||||
# Install (example for zsh):
|
||||
myapp completion zsh > "${fpath[1]}/_myapp"
|
||||
```
|
||||
|
||||
## ShellCompDirective
|
||||
|
||||
The `ShellCompDirective` controls shell behavior after your completion function returns:
|
||||
|
||||
| Directive | Meaning |
|
||||
| --- | --- |
|
||||
| `ShellCompDirectiveDefault` | Fall back to file completion after your results |
|
||||
| `ShellCompDirectiveNoFileComp` | Disable file completion fallback |
|
||||
| `ShellCompDirectiveNoSpace` | Don't add a space after the completion |
|
||||
| `ShellCompDirectiveFilterFileExt(exts)` | Only show files with given extensions |
|
||||
| `ShellCompDirectiveFilterDirs(dirs)` | Only show directories |
|
||||
| `ShellCompDirectiveError` | Signal an error (show no completions) |
|
||||
|
||||
Combine with bitwise OR: `cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace`.
|
||||
|
||||
Use `ShellCompDirectiveNoFileComp` whenever your list is exhaustive — it prevents the shell from appending irrelevant files.
|
||||
|
||||
## Static arg completions
|
||||
|
||||
```go
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <resource>",
|
||||
ValidArgs: []string{"pod", "service", "deployment", "configmap"},
|
||||
Args: cobra.OnlyValidArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic arg completions
|
||||
|
||||
```go
|
||||
var getCmd = &cobra.Command{
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
// first arg already provided — no more completions
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
resources, err := listResources(toComplete)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
return resources, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`toComplete` is the prefix the user has typed so far — filter your results by it for responsive completions.
|
||||
|
||||
## Flag value completions
|
||||
|
||||
```go
|
||||
func init() {
|
||||
rootCmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"json\tJSON output", "yaml\tYAML output", "table\tTable output"}, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
|
||||
rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ns, err := listNamespaces()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
return ns, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Descriptions after `\t` are shown in zsh and fish menus.
|
||||
|
||||
## Completion annotations
|
||||
|
||||
Mark a flag to complete as a file or directory:
|
||||
|
||||
```go
|
||||
cmd.Flags().String("config", "", "config file")
|
||||
cmd.MarkFlagFilename("config", "yaml", "yml", "json") // only those extensions
|
||||
|
||||
cmd.Flags().String("dir", "", "output directory")
|
||||
cmd.MarkFlagDirname("dir")
|
||||
```
|
||||
|
||||
## Testing completions
|
||||
|
||||
```go
|
||||
func TestCompletion(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"__complete", "get", ""})
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.Execute()
|
||||
assert.Contains(t, buf.String(), "pod")
|
||||
assert.Contains(t, buf.String(), "service")
|
||||
}
|
||||
```
|
||||
|
||||
`__complete` is cobra's internal completion request verb. Pass the partial args as additional arguments.
|
||||
|
||||
## Disabling the completion command
|
||||
|
||||
```go
|
||||
rootCmd.CompletionOptions.DisableDefaultCmd = true // remove the completion subcommand
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true // keep it but hide from help
|
||||
```
|
||||
@@ -0,0 +1,125 @@
|
||||
# Cobra Flags Reference
|
||||
|
||||
Cobra delegates all flag parsing to `github.com/spf13/pflag`. `cobra.Command` exposes two `*pflag.FlagSet`s:
|
||||
|
||||
- `cmd.Flags()` — local flags, only available on this command.
|
||||
- `cmd.PersistentFlags()` — inherited by all subcommands.
|
||||
|
||||
## Common flag types
|
||||
|
||||
```go
|
||||
// String
|
||||
cmd.Flags().String("name", "default", "description")
|
||||
cmd.Flags().StringP("name", "n", "default", "description") // with shorthand
|
||||
|
||||
// With pointer binding (no Lookup needed later)
|
||||
var name string
|
||||
cmd.Flags().StringVar(&name, "name", "default", "description")
|
||||
cmd.Flags().StringVarP(&name, "name", "n", "default", "description")
|
||||
|
||||
// Other types follow the same pattern:
|
||||
cmd.Flags().Int / IntVar / IntVarP
|
||||
cmd.Flags().Bool / BoolVar / BoolVarP
|
||||
cmd.Flags().Float64 / Float64Var
|
||||
cmd.Flags().Duration / DurationVar // parses "1h30m", "500ms"
|
||||
cmd.Flags().StringSlice / StringSliceVar // comma-separated or repeated flags
|
||||
cmd.Flags().StringArray / StringArrayVar // repeated flags only (no comma splitting)
|
||||
cmd.Flags().IntSlice / IntSliceVar
|
||||
cmd.Flags().StringToString // --label key=value --label k2=v2
|
||||
```
|
||||
|
||||
## StringSlice vs StringArray
|
||||
|
||||
| Flag type | Input | Result |
|
||||
| ------------- | --------------------- | --------------------------------- |
|
||||
| `StringSlice` | `--tags a,b --tags c` | `["a", "b", "c"]` — commas split |
|
||||
| `StringArray` | `--tags a,b --tags c` | `["a,b", "c"]` — commas NOT split |
|
||||
|
||||
Use `StringArray` when values may legitimately contain commas.
|
||||
|
||||
## Flag constraints
|
||||
|
||||
```go
|
||||
// Fail if flag not provided
|
||||
cmd.MarkFlagRequired("output")
|
||||
|
||||
// Fail if both provided
|
||||
cmd.MarkFlagsMutuallyExclusive("json", "yaml", "table")
|
||||
|
||||
// Fail if none provided
|
||||
cmd.MarkFlagsOneRequired("file", "stdin")
|
||||
|
||||
// Require flag only if another flag is set
|
||||
cmd.MarkFlagsMutuallyExclusive("tls", "no-tls")
|
||||
```
|
||||
|
||||
## Persistent flag patterns
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// global flags on root
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: $HOME/.myapp.yaml)")
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "log level (debug, info, warn, error)")
|
||||
|
||||
// bind to viper immediately after defining
|
||||
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
|
||||
viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
|
||||
}
|
||||
```
|
||||
|
||||
## Custom flag value types
|
||||
|
||||
Implement `pflag.Value` to parse arbitrary types:
|
||||
|
||||
```go
|
||||
type enumValue struct {
|
||||
val string
|
||||
allowed []string
|
||||
}
|
||||
|
||||
func (e *enumValue) String() string { return e.val }
|
||||
func (e *enumValue) Type() string { return "enum" }
|
||||
func (e *enumValue) Set(s string) error {
|
||||
for _, a := range e.allowed {
|
||||
if s == a {
|
||||
e.val = s
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("must be one of %v", e.allowed)
|
||||
}
|
||||
|
||||
var outputFmt = &enumValue{val: "table", allowed: []string{"table", "json", "yaml"}}
|
||||
cmd.Flags().Var(outputFmt, "output", "output format (table, json, yaml)")
|
||||
```
|
||||
|
||||
## Flag groups (required together)
|
||||
|
||||
Mark a set of flags that must all be provided if any one of them is provided:
|
||||
|
||||
```go
|
||||
cmd.Flags().String("tls-cert", "", "TLS certificate file")
|
||||
cmd.Flags().String("tls-key", "", "TLS key file")
|
||||
cmd.MarkFlagsRequiredTogether("tls-cert", "tls-key")
|
||||
```
|
||||
|
||||
## Accessing flag values
|
||||
|
||||
Prefer pointer binding (`StringVar`, `IntVar`, etc.) for type-safe access. When you need the flag post-parse:
|
||||
|
||||
```go
|
||||
port, err := cmd.Flags().GetInt("port")
|
||||
name, err := cmd.Flags().GetString("name")
|
||||
tags, err := cmd.Flags().GetStringSlice("tags")
|
||||
```
|
||||
|
||||
## Flag changed vs default
|
||||
|
||||
```go
|
||||
if cmd.Flags().Changed("port") {
|
||||
// user explicitly provided --port
|
||||
// useful when distinguishing "user set 0" from "flag not provided"
|
||||
}
|
||||
```
|
||||
|
||||
`Changed()` is also how viper knows which flags are explicit overrides — it only promotes a flag to the highest precedence layer if `Changed()` is true.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Cobra Documentation and Scaffolding Generators
|
||||
|
||||
## Doc generation
|
||||
|
||||
Cobra can generate documentation from your command tree in multiple formats. Import the `cobra/doc` sub-package:
|
||||
|
||||
```bash
|
||||
go get github.com/spf13/cobra/doc
|
||||
```
|
||||
|
||||
### Markdown
|
||||
|
||||
```go
|
||||
import "github.com/spf13/cobra/doc"
|
||||
|
||||
err := doc.GenMarkdownTree(rootCmd, "/tmp/docs/")
|
||||
// generates /tmp/docs/myapp.md, /tmp/docs/myapp_serve.md, etc.
|
||||
|
||||
// Single command
|
||||
var buf bytes.Buffer
|
||||
doc.GenMarkdown(rootCmd, &buf)
|
||||
```
|
||||
|
||||
### Man pages
|
||||
|
||||
```go
|
||||
header := &doc.GenManHeader{
|
||||
Title: "MYAPP",
|
||||
Section: "1",
|
||||
Date: &time.Time{},
|
||||
Source: "myapp v1.0.0",
|
||||
Manual: "User Commands",
|
||||
}
|
||||
err := doc.GenManTree(rootCmd, header, "/usr/local/share/man/man1/")
|
||||
```
|
||||
|
||||
### YAML
|
||||
|
||||
```go
|
||||
err := doc.GenYamlTree(rootCmd, "/tmp/docs/")
|
||||
```
|
||||
|
||||
### RST (reStructuredText)
|
||||
|
||||
```go
|
||||
err := doc.GenReSTTree(rootCmd, "/tmp/docs/")
|
||||
```
|
||||
|
||||
## cobra-cli scaffolder
|
||||
|
||||
`cobra-cli` generates command files and wires them into your project:
|
||||
|
||||
```bash
|
||||
go get -tool github.com/spf13/cobra-cli@latest
|
||||
|
||||
# Initialize a new cobra project
|
||||
go tool cobra-cli init myapp
|
||||
|
||||
# Add a subcommand
|
||||
go tool cobra-cli add serve
|
||||
go tool cobra-cli add migrate
|
||||
|
||||
# Add with a parent other than root
|
||||
cobra-cli add list --parent serve
|
||||
```
|
||||
|
||||
Generated files follow the standard pattern:
|
||||
|
||||
```go
|
||||
// cmd/serve.go
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "A brief description of your command",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
```
|
||||
|
||||
`cobra-cli` is optional — many teams write command files by hand following the same pattern.
|
||||
|
||||
## Help and usage template customization
|
||||
|
||||
Override the default help template:
|
||||
|
||||
```go
|
||||
rootCmd.SetHelpTemplate(`
|
||||
Usage: {{.UseLine}}
|
||||
{{if .HasAvailableSubCommands}}
|
||||
Commands:
|
||||
{{range .Commands}}{{if .IsAvailableCommand}} {{rpad .Name .NamePadding }} {{.Short}}
|
||||
{{end}}{{end}}{{end}}
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimRightSpace}}
|
||||
`)
|
||||
```
|
||||
|
||||
Override the usage function entirely:
|
||||
|
||||
```go
|
||||
rootCmd.SetUsageFunc(func(cmd *cobra.Command) error {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Custom usage for %s\n", cmd.Name())
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Common template functions available: `rpad`, `trimRightSpace`, `gt`, `eq`.
|
||||
@@ -0,0 +1,154 @@
|
||||
# Testing Cobra Commands
|
||||
|
||||
## Basic test pattern
|
||||
|
||||
```go
|
||||
func TestServeCmd(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
rootCmd.SetOut(stdout)
|
||||
rootCmd.SetErr(stderr)
|
||||
rootCmd.SetArgs([]string{"serve", "--port", "9090", "--dry-run"})
|
||||
|
||||
err := rootCmd.Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stdout.String(), "listening on :9090")
|
||||
assert.Empty(t, stderr.String())
|
||||
}
|
||||
```
|
||||
|
||||
## Isolation between tests
|
||||
|
||||
Cobra accumulates flag state across `Execute()` calls on the same command instance. Tests must be isolated.
|
||||
|
||||
### Option 1: Re-create the command tree per test (recommended for unit tests)
|
||||
|
||||
```go
|
||||
func newRootCmd() *cobra.Command {
|
||||
root := &cobra.Command{Use: "myapp", SilenceUsage: true, SilenceErrors: true}
|
||||
root.AddCommand(newServeCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
func TestServeCmd(t *testing.T) {
|
||||
root := newRootCmd()
|
||||
root.SetArgs([]string{"serve", "--port", "9090"})
|
||||
err := root.Execute()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Reset flags between tests
|
||||
|
||||
```go
|
||||
func TestWithReset(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
rootCmd.ResetFlags()
|
||||
// re-define flags if needed
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Re-creating is safer — `ResetFlags` only clears the flag set, not subcommand state.
|
||||
|
||||
## Testing commands that write output
|
||||
|
||||
Commands must use `cmd.OutOrStdout()` / `cmd.ErrOrStderr()` instead of `os.Stdout` / `os.Stderr` for this to work.
|
||||
|
||||
```go
|
||||
// In command handler:
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Server started")
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Debug: listening on port 8080")
|
||||
return nil
|
||||
}
|
||||
|
||||
// In test:
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.Execute()
|
||||
assert.Contains(t, buf.String(), "Server started")
|
||||
```
|
||||
|
||||
## Golden file tests
|
||||
|
||||
For commands with structured or lengthy output, use golden files:
|
||||
|
||||
```go
|
||||
func TestOutputFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.SetArgs([]string{"list", "--output", "json"})
|
||||
require.NoError(t, rootCmd.Execute())
|
||||
|
||||
golden := "testdata/list-json.golden"
|
||||
if *update { // -update flag
|
||||
os.WriteFile(golden, buf.Bytes(), 0644)
|
||||
}
|
||||
want, _ := os.ReadFile(golden)
|
||||
assert.Equal(t, string(want), buf.String())
|
||||
}
|
||||
```
|
||||
|
||||
Run with `-update` to regenerate golden files after intentional output changes.
|
||||
|
||||
## Testing error paths
|
||||
|
||||
```go
|
||||
func TestInvalidArgs(t *testing.T) {
|
||||
stderr := new(bytes.Buffer)
|
||||
rootCmd.SetErr(stderr)
|
||||
rootCmd.SetArgs([]string{"delete"}) // missing required arg
|
||||
|
||||
err := rootCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accepts 1 arg")
|
||||
}
|
||||
```
|
||||
|
||||
## Table-driven command tests
|
||||
|
||||
```go
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantOut string
|
||||
wantErr bool
|
||||
}{
|
||||
{"no flags", []string{"serve"}, "listening on :8080", false},
|
||||
{"custom port", []string{"serve", "--port", "9090"}, "listening on :9090", false},
|
||||
{"invalid port", []string{"serve", "--port", "abc"}, "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
root := newRootCmd() // fresh command tree per test
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetArgs(tt.args)
|
||||
err := root.Execute()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Testing completions
|
||||
|
||||
```go
|
||||
func TestCompletion(t *testing.T) {
|
||||
root := newRootCmd()
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetArgs([]string{"__complete", "delete", ""})
|
||||
root.Execute()
|
||||
|
||||
assert.Contains(t, buf.String(), "pod")
|
||||
assert.Contains(t, buf.String(), "service")
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user