171 lines
8.1 KiB
Go
171 lines
8.1 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
internalcobra "go.maximumdirect.net/commandkit/internal/cobra"
|
|
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
|
)
|
|
|
|
func TestCompletionQueryDispatch(t *testing.T) {
|
|
var factories, converters, handlers int
|
|
command := NewCommand(CommandSpec[struct{}]{
|
|
Name: "run", Summary: "Run a command.",
|
|
NewInput: func() struct{} { factories++; return struct{}{} },
|
|
Flags: []Flag[struct{}]{
|
|
ValueFlag(FlagMeta{Name: "color", Short: "c", Completion: CompleteChoices(CompletionChoice{Value: "blue", Description: "cool\ncolor"}, CompletionChoice{Value: "green"}, CompletionChoice{Value: "foo=bar"})}, (*string)(nil),
|
|
func(value string) (string, error) { converters++; return value, nil }, func(*struct{}, Supplied[string]) {}),
|
|
ValueFlag(FlagMeta{Name: "file", Short: "f", Completion: CompleteFiles()}, (*string)(nil),
|
|
func(value string) (string, error) { converters++; return value, nil }, func(*struct{}, Supplied[string]) {}),
|
|
},
|
|
Run: func(Invocation, struct{}) Outcome { handlers++; return Success() },
|
|
})
|
|
app, err := New(Identity{Name: "example"}, command, Completion("example"), Alias("shortcut", "run"), FlagAliasFor("run", "--colour", "--color"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
emptyReply, err := internalcobra.CompletionReply(commandmeta.CompletionResult{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
empty bool
|
|
}{
|
|
{"empty word", []string{"__complete", ""}, "run\tRun a command.", false},
|
|
{"alias value", []string{"__complete", "shortcut", "--colour=bl"}, "blue\tcool color\n", false},
|
|
{"quiet is data", []string{"__complete", "run", "--quiet", "--color=bl"}, "blue\tcool color\n", false},
|
|
{"debug is data", []string{"__complete", "run", "--debug", "--color=bl"}, "blue\tcool color\n", false},
|
|
{"format value", []string{"__complete", "run", "--format", "j"}, "json\n", false},
|
|
{"short attached choice", []string{"__complete", "run", "-cfoo=ba"}, "foo=bar\n", false},
|
|
{"unknown route", []string{"__complete", "missing", ""}, emptyReply, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var output, diagnostic bytes.Buffer
|
|
outcome := app.Execute(context.Background(), tc.args, Streams{Out: &output, Err: &diagnostic})
|
|
if outcome.Status != 0 || diagnostic.Len() != 0 || (tc.empty && output.String() != tc.want) || (!tc.empty && !strings.Contains(output.String(), tc.want)) {
|
|
t.Fatalf("outcome=%#v output=%q diagnostics=%q", outcome, output.String(), diagnostic.String())
|
|
}
|
|
})
|
|
}
|
|
fileReply, err := internalcobra.CompletionReply(commandmeta.CompletionResult{Kind: commandmeta.CompletionFiles, Prefix: "-f"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fileOutput bytes.Buffer
|
|
fileOutcome := app.Execute(context.Background(), []string{"__complete", "run", "-f./part"}, Streams{Out: &fileOutput})
|
|
if fileOutcome.Status != 0 || fileOutput.String() != fileReply {
|
|
t.Fatalf("file query outcome=%#v reply=%q", fileOutcome, fileOutput.String())
|
|
}
|
|
if factories != 0 || converters != 0 || handlers != 0 {
|
|
t.Fatalf("query invoked application callbacks: %d %d %d", factories, converters, handlers)
|
|
}
|
|
if err := app.Register(simpleRunnable("later")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output bytes.Buffer
|
|
if outcome := app.Execute(context.Background(), []string{"__complete", "lat"}, Streams{Out: &output}); outcome.Status != 0 || !strings.Contains(output.String(), "later") {
|
|
t.Fatalf("later command outcome=%#v reply=%q", outcome, output.String())
|
|
}
|
|
output.Reset()
|
|
if outcome := app.Execute(context.Background(), []string{"__complete", "later", "--qui"}, Streams{Out: &output}); outcome.Status != 0 || !strings.Contains(output.String(), "--quiet") {
|
|
t.Fatalf("later command flag outcome=%#v reply=%q", outcome, output.String())
|
|
}
|
|
}
|
|
|
|
func TestCompletionQueryErrorsAndDelivery(t *testing.T) {
|
|
app, err := New(Identity{Name: "example"}, Completion("example"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output, diagnostics bytes.Buffer
|
|
outcome := app.Execute(context.Background(), []string{"__complete"}, Streams{Out: &output, Err: &diagnostics})
|
|
if outcome.Status != 1 || outcome.Cause == nil || output.String() != internalcobra.CompletionErrorReply() || diagnostics.Len() == 0 {
|
|
t.Fatalf("malformed query outcome=%#v reply=%q diagnostics=%q", outcome, output.String(), diagnostics.String())
|
|
}
|
|
writeErr := errors.New("reply destination failed")
|
|
writer := &completionCountingWriter{err: writeErr}
|
|
outcome = app.Execute(context.Background(), []string{"__complete", ""}, Streams{Out: writer, Err: io.Discard})
|
|
if outcome.Status != 1 || !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || writer.calls != 1 {
|
|
t.Fatalf("reply write failure: %#v", outcome)
|
|
}
|
|
}
|
|
|
|
type completionCountingWriter struct {
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (writer *completionCountingWriter) Write([]byte) (int, error) {
|
|
writer.calls++
|
|
return 0, writer.err
|
|
}
|
|
|
|
func TestCompletionQueryDiscovery(t *testing.T) {
|
|
app, err := New(Identity{Name: "example"}, Completion("example"),
|
|
Group("workspace", "", simpleRunnable("inspect")), Alias("legacy inspect", "workspace inspect"), FlagAlias("--silent", "--quiet"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output bytes.Buffer
|
|
outcome := app.Execute(context.Background(), []string{"__complete", "legacy", "--"}, Streams{Out: &output})
|
|
if outcome.Status != 0 || !strings.Contains(output.String(), "--quiet") || strings.Contains(output.String(), "inspect") || strings.Contains(output.String(), "--silent") {
|
|
t.Fatalf("discovery outcome=%#v reply=%q", outcome, output.String())
|
|
}
|
|
}
|
|
|
|
func TestSharedDebugHelpAndCompletion(t *testing.T) {
|
|
var factories, setters, checks, handlers int
|
|
command := NewCommand(CommandSpec[struct{}]{
|
|
Name: "run",
|
|
NewInput: func() struct{} { factories++; return struct{}{} },
|
|
Flags: []Flag[struct{}]{BoolFlag(FlagMeta{Name: "debug", Short: "d", Usage: "application tracing", Required: true, Completion: CompleteChoices(CompletionChoice{Value: "true"}, CompletionChoice{Value: "trace"})}, true, func(*struct{}, Supplied[bool]) { setters++ })},
|
|
CheckInput: func(struct{}) error { checks++; return nil },
|
|
Run: func(Invocation, struct{}) Outcome { handlers++; return Success() },
|
|
})
|
|
app, err := New(Identity{Name: "example"}, command, Completion("example"), Alias("shortcut", "run"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, args := range [][]string{{"run", "--help"}, {"help", "run"}, {"shortcut", "--help"}, {"help", "shortcut"}, {"run", "--unknown"}, {"shortcut", "--unknown"}} {
|
|
var output, diagnostic bytes.Buffer
|
|
result := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostic})
|
|
text := output.String() + diagnostic.String()
|
|
if strings.Count(text, "--debug") != 1 || !strings.Contains(text, "-d") || !strings.Contains(text, "application tracing") || !strings.Contains(text, "application input defaults to true") || !strings.Contains(text, "required") || !strings.Contains(text, "default: false") {
|
|
t.Fatalf("%v: outcome=%#v text=%q", args, result, text)
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
count int
|
|
}{
|
|
{[]string{"__complete", "run", "--deb"}, "--debug", 1},
|
|
{[]string{"__complete", "run", "-"}, "\n-d\t", 1},
|
|
{[]string{"__complete", "run", "--debug=tr"}, "trace", 1},
|
|
{[]string{"__complete", "run", "-d=tr"}, "trace", 1},
|
|
} {
|
|
var output bytes.Buffer
|
|
result := app.Execute(context.Background(), tc.args, Streams{Out: &output, Err: io.Discard})
|
|
if result.Kind != OutcomeSuccess || strings.Count(output.String(), tc.want) != tc.count {
|
|
t.Fatalf("%v: %#v %q", tc.args, result, output.String())
|
|
}
|
|
}
|
|
if factories != 0 || setters != 0 || checks != 0 || handlers != 0 {
|
|
t.Fatalf("callbacks: %d %d %d %d", factories, setters, checks, handlers)
|
|
}
|
|
if result := app.Execute(context.Background(), []string{"run", "--debug=invalid", "--help"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
|
|
t.Fatalf("help skipped boolean validation: %#v", result)
|
|
}
|
|
if result := app.Execute(context.Background(), []string{"run", "--debug=trace"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
|
|
t.Fatalf("suggestion bypassed boolean parsing: %#v", result)
|
|
}
|
|
}
|