Files
commandkit/execute_external_test.go

580 lines
29 KiB
Go

package commandkit_test
import (
"bytes"
"context"
"errors"
"io"
"strconv"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
type input struct {
name commandkit.Supplied[string]
ids commandkit.Supplied[[]string]
}
var inputs []input
var contexts []context.Context
var reporters []*commandkit.Reporter
firstContext := context.WithValue(context.Background(), struct{}{}, "first")
secondContext := context.WithValue(context.Background(), struct{}{}, "second")
var firstOut, secondOut, firstDiagnostic, secondDiagnostic bytes.Buffer
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
Flags: []commandkit.Flag[input]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "name"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[string]) { target.name = value }),
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "id"}, nil, func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[[]string]) { target.ids = value }),
},
Run: func(invocation commandkit.Invocation, value input) commandkit.Outcome {
inputs = append(inputs, value)
contexts = append(contexts, invocation.Context)
reporter := invocation.Reporter()
reporters = append(reporters, reporter)
_ = reporter.RenderOutput(func(writer io.Writer) error {
_, err := io.WriteString(writer, value.name.Value+"\n")
return err
})
return commandkit.Success()
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
first := app.Execute(firstContext, []string{"run", "--name=first", "--id=a,b", "--id=c"}, commandkit.Streams{Out: &firstOut, Err: &firstDiagnostic})
second := app.Execute(secondContext, []string{"run"}, commandkit.Streams{Out: &secondOut, Err: &secondDiagnostic})
third := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
if first.Kind != commandkit.OutcomeSuccess || second.Kind != commandkit.OutcomeSuccess || third.Kind != commandkit.OutcomeSuccess {
t.Fatalf("outcomes = %#v, %#v, and %#v", first, second, third)
}
if len(inputs) != 3 || inputs[0].name.Value != "first" || !inputs[0].name.Set || len(inputs[0].ids.Value) != 2 || inputs[0].ids.Value[0] != "a,b" || inputs[0].ids.Value[1] != "c" {
t.Fatalf("first input = %#v", inputs)
}
if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set {
t.Fatalf("second invocation inherited state: %#v", inputs[1])
}
if reporters[0] == nil || reporters[1] == nil || reporters[2] == nil || reporters[0] == reporters[1] || reporters[0] == reporters[2] || reporters[1] == reporters[2] {
t.Fatalf("invocations did not receive fresh reporters: %#v", reporters)
}
if firstOut.String() != "first\n" || secondOut.String() != "\n" || contexts[0] != firstContext || contexts[1] != secondContext {
t.Fatalf("handler invocation contexts or reporter destinations were not independent: out=%q/%q contexts=%v/%v", firstOut.String(), secondOut.String(), contexts[0], contexts[1])
}
}
func TestOmittedInputFactoryUsesExactPointerZeroValue(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[*int]{
Name: "run",
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
if value != nil {
return commandkit.Failure("omitted input factory allocated a pointer")
}
return commandkit.Success()
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
if outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{}); outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("pointer zero value outcome = %#v", outcome)
}
}
func TestExecuteUsesFreshSessionForHelpAfterFlagBearingInvocation(t *testing.T) {
type input struct {
name commandkit.Supplied[string]
enabled commandkit.Supplied[bool]
}
var received []input
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
Flags: []commandkit.Flag[input]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "name"}, (*string)(nil), func(value string) (string, error) {
return value, nil
}, func(target *input, value commandkit.Supplied[string]) { target.name = value }),
commandkit.BoolFlag(commandkit.FlagMeta{Name: "enabled"}, false, func(target *input, value commandkit.Supplied[bool]) { target.enabled = value }),
},
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
received = append(received, value)
return commandkit.Success()
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var firstOutput, secondOutput, diagnostic bytes.Buffer
first := app.Execute(context.Background(), []string{"run", "--name=chosen", "--enabled=false"}, commandkit.Streams{Out: &firstOutput, Err: &diagnostic})
if first.Kind != commandkit.OutcomeSuccess {
t.Fatalf("flag-bearing invocation = %#v", first)
}
help := app.Execute(context.Background(), []string{"run", "--name=", "--help"}, commandkit.Streams{Out: &secondOutput, Err: &diagnostic})
if help.Kind != commandkit.OutcomeSuccess || !strings.Contains(secondOutput.String(), "--enabled") {
t.Fatalf("help invocation = %#v, output %q", help, secondOutput.String())
}
last := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
if last.Kind != commandkit.OutcomeSuccess {
t.Fatalf("omitted-value invocation = %#v", last)
}
if len(received) != 2 || received[0].name.Value != "chosen" || !received[0].name.Set || received[0].enabled.Value || !received[0].enabled.Set {
t.Fatalf("first input = %#v", received)
}
if received[1].name.Set || received[1].name.Value != "" || received[1].enabled.Set {
t.Fatalf("later invocation inherited flag state: %#v", received[1])
}
}
func TestExecutePresentsHandlerUsageAfterRegisteringCommand(t *testing.T) {
usageCause := errors.New("handler requests usage")
var app *commandkit.App
var calls int
late := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "late",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() },
})
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
calls++
if err := app.Register(late); err != nil {
return commandkit.Failure(err.Error()).WithCause(err)
}
return commandkit.Usage("The handler requests usage.").WithCause(usageCause)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if result.Kind != commandkit.OutcomeUsage || !errors.Is(result.Cause, usageCause) || calls != 1 {
t.Fatalf("handler usage = %#v, calls %d", result, calls)
}
if !strings.Contains(diagnostic.String(), "Usage: example run") {
t.Fatalf("handler usage diagnostic = %q", diagnostic.String())
}
if result := app.Execute(context.Background(), []string{"late"}, commandkit.Streams{}); result.Kind != commandkit.OutcomeSuccess {
t.Fatalf("registered command execution = %#v", result)
}
}
func TestExecuteReturnsUsageForParsingRequirementsAndInputChecks(t *testing.T) {
type input struct{ count commandkit.Supplied[int] }
parseFailure := errors.New("bad count")
checkFailure := errors.New("incompatible inputs")
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
Flags: []commandkit.Flag[input]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Required: true}, nil, func(value string) (int, error) {
if value == "bad" {
return 0, parseFailure
}
return 0, nil
}, func(target *input, value commandkit.Supplied[int]) { target.count = value })},
Arguments: []commandkit.Argument[input]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(*input, commandkit.Supplied[string]) {})},
CheckInput: func(input) error { return checkFailure },
Run: func(commandkit.Invocation, input) commandkit.Outcome { return commandkit.Success() },
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
missing := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if missing.Kind != commandkit.OutcomeUsage || missing.Status != 2 || !strings.Contains(diagnostic.String(), "Usage: example run") {
t.Fatalf("missing required input = %#v, diagnostics %q", missing, diagnostic.String())
}
malformed := app.Execute(context.Background(), []string{"run", "--count=bad", "id"}, commandkit.Streams{Err: &diagnostic})
if malformed.Kind != commandkit.OutcomeUsage || !errors.Is(malformed.Cause, parseFailure) {
t.Fatalf("parser failure = %#v", malformed)
}
checked := app.Execute(context.Background(), []string{"run", "--count=1", "id"}, commandkit.Streams{Err: &diagnostic})
if checked.Kind != commandkit.OutcomeUsage || !errors.Is(checked.Cause, checkFailure) {
t.Fatalf("input check failure = %#v", checked)
}
}
func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
domainCause := errors.New("domain rejected the request")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
_ = invocation.Reporter().Warn("review needed")
outcome := commandkit.Domain("sample", 2).WithExplanation("The request was rejected.").WithCause(domainCause)
return outcome
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"run", "--verbose"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || outcome.Domain != "sample" || outcome.Status != 2 || !errors.Is(outcome.Cause, domainCause) {
t.Fatalf("domain outcome changed: %#v", outcome)
}
if diagnostic.String() != "WARN: review needed\nERROR: The request was rejected.\n" || outcome.Presentation.Diagnostics.State != commandkit.DeliveryComplete {
t.Fatalf("warning delivery = %q, %#v", diagnostic.String(), outcome)
}
warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "warn",
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
_ = invocation.Reporter().Warn("review needed")
return commandkit.Success()
},
})
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
if err != nil {
t.Fatal(err)
}
diagnostic.Reset()
outcome = app.Execute(context.Background(), []string{"warn", "--verbose"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "WARN: review needed\n" {
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
}
}
func TestExecuteDoesNotPresentOperationCauseWithoutExplanation(t *testing.T) {
cause := errors.New("private operation detail")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
return commandkit.Domain("sample", 2).WithCause(cause)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || !errors.Is(outcome.Cause, cause) {
t.Fatalf("domain outcome changed: %#v", outcome)
}
if strings.Contains(diagnostic.String(), cause.Error()) || !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
t.Fatalf("cause was presented instead of the fallback explanation: %#v, %q", outcome, diagnostic.String())
}
}
func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
operationFailure := context.Canceled
writeFailure := errors.New("writer failed")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
return commandkit.Failure("The operation was canceled.").WithCause(operationFailure)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{err: writeFailure}})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 || outcome.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete || !errors.Is(outcome.Cause, operationFailure) || !errors.Is(outcome.Presentation.Diagnostics.WriteError, writeFailure) {
t.Fatalf("combined operation and delivery failure = %#v", outcome)
}
short := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: shortWriter{}})
if !errors.Is(short.Presentation.Diagnostics.WriteError, io.ErrShortWrite) || short.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete {
t.Fatalf("short write was not detected: %#v", short)
}
}
func TestExecuteHelpSkipsRequiredInputsAndRejectsNilContext(t *testing.T) {
type input struct{ id commandkit.Supplied[string] }
factoryCalls, checkCalls, runCalls := 0, 0, 0
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
NewInput: func() input {
factoryCalls++
return input{}
},
Arguments: []commandkit.Argument[input]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[string]) { target.id = value })},
CheckInput: func(input) error { checkCalls++; return nil },
Run: func(commandkit.Invocation, input) commandkit.Outcome { runCalls++; return commandkit.Success() },
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
help := app.Execute(context.Background(), []string{"run", "--help"}, commandkit.Streams{})
if help.Kind != commandkit.OutcomeSuccess || factoryCalls != 0 || checkCalls != 0 || runCalls != 0 {
t.Fatalf("help request invoked execution path: %#v calls=%d/%d/%d", help, factoryCalls, checkCalls, runCalls)
}
invalid := app.Execute(nil, []string{"run"}, commandkit.Streams{})
if invalid.Kind != commandkit.OutcomeOperational || invalid.Status != 1 || invalid.Cause == nil {
t.Fatalf("nil context outcome = %#v", invalid)
}
}
func TestExecuteRunsApplicationOwnedNestedHelp(t *testing.T) {
type input struct{ topic commandkit.Supplied[string] }
var received []input
domainCause := errors.New("application help result")
help := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "help",
Description: "Explain a session topic.",
Arguments: []commandkit.Argument[input]{commandkit.ValueArgument(
commandkit.ArgumentMeta{Name: "TOPIC"}, true,
func(value string) (string, error) { return value, nil },
func(target *input, value commandkit.Supplied[string]) { target.topic = value },
)},
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
received = append(received, value)
return commandkit.Domain("session-help", 7).WithExplanation("Session help is unavailable.").WithCause(domainCause)
},
})
session := commandkit.Group("session", "Session commands.", help)
app, err := commandkit.New(commandkit.Identity{Name: "example"}, session)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"session", "help", "restore"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || outcome.Domain != "session-help" || outcome.Status != 7 || !errors.Is(outcome.Cause, domainCause) {
t.Fatalf("nested help handler outcome = %#v, diagnostics %q", outcome, diagnostic.String())
}
if len(received) != 1 || received[0].topic != (commandkit.Supplied[string]{Value: "restore", Set: true}) {
t.Fatalf("nested help handler input = %#v", received)
}
for _, args := range [][]string{{"session", "help", "--help"}, {"help", "session", "help"}} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
var output, diagnostics bytes.Buffer
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: &diagnostics})
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || diagnostics.Len() != 0 {
t.Fatalf("metadata help outcome = %#v, diagnostics %q", outcome, diagnostics.String())
}
if !strings.Contains(output.String(), "Explain a session topic.") || !strings.Contains(output.String(), "TOPIC") {
t.Fatalf("metadata help omitted nested command details: %q", output.String())
}
if len(received) != 1 {
t.Fatalf("metadata help invoked the application handler: %#v", received)
}
})
}
}
func TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
type input struct {
count commandkit.Supplied[int]
tags commandkit.Supplied[[]string]
}
factoryCalls, runCalls := 0, 0
show := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "show",
Summary: "Show one item.",
Description: "Display the selected item in detail.",
NewInput: func() input {
factoryCalls++
return input{}
},
Flags: []commandkit.Flag[input]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Usage: "number of items", Required: true}, nil, func(value string) (int, error) {
return strconv.Atoi(value)
}, func(target *input, value commandkit.Supplied[int]) { target.count = value }),
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag", Usage: "item tag"}, []string{"base"}, func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[[]string]) { target.tags = value }),
},
Arguments: []commandkit.Argument[input]{commandkit.RepeatArgument(commandkit.ArgumentMeta{Name: "ID", Usage: "item identifier"}, 1, 3, func(value string) (string, error) { return value, nil }, func(*input, commandkit.Supplied[[]string]) {})},
Run: func(commandkit.Invocation, input) commandkit.Outcome { runCalls++; return commandkit.Success() },
})
config := commandkit.Group("config", "Inspect configuration.", show)
now := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "now", Summary: "Deploy immediately.", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }})
deploy := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "deploy", Summary: "Deploy artifacts.", Children: []commandkit.Command{now}, Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { runCalls++; return commandkit.Success() }})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, config, deploy)
if err != nil {
t.Fatal(err)
}
var out, diagnostic bytes.Buffer
root := app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if root.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("root help = %#v, output %q", root, out.String())
}
for _, name := range []string{"config", "help", "version"} {
if !strings.Contains(out.String(), name) {
t.Fatalf("root help omitted %q: %q", name, out.String())
}
}
for _, earlier := range []string{"config", "deploy", "help"} {
if strings.Index(out.String(), earlier) > strings.Index(out.String(), "version") {
t.Fatalf("root help commands are not sorted: %q", out.String())
}
}
out.Reset()
bare := app.Execute(context.Background(), nil, commandkit.Streams{Out: &out, Err: &diagnostic})
if bare.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("bare root help = %#v, output %q", bare, out.String())
}
out.Reset()
helpCommand := app.Execute(context.Background(), []string{"help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if helpCommand.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("root help command = %#v, output %q", helpCommand, out.String())
}
out.Reset()
group := app.Execute(context.Background(), []string{"config"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if group.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "show") {
t.Fatalf("group help = %#v, output %q", group, out.String())
}
out.Reset()
parentHelp := app.Execute(context.Background(), []string{"deploy", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if parentHelp.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "now") || strings.Contains(out.String(), "Usage: example deploy now") {
t.Fatalf("runnable parent help = %#v, output %q", parentHelp, out.String())
}
out.Reset()
selected := app.Execute(context.Background(), []string{"help", "config", "show"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if selected.Kind != commandkit.OutcomeSuccess {
t.Fatalf("selected help = %#v, error %q", selected, diagnostic.String())
}
for _, expected := range []string{"Display the selected item in detail.", "Usage:", "ID{1..3}", "Arguments:", "min 1, max 3", "--count", "required", "default: 0", "--tag", "repeatable", "default: [base]", "--help"} {
if !strings.Contains(out.String(), expected) {
t.Fatalf("selected help omitted %q: %q", expected, out.String())
}
}
if factoryCalls != 0 || runCalls != 0 {
t.Fatalf("help invoked execution callbacks: factories=%d runs=%d", factoryCalls, runCalls)
}
badValue := app.Execute(context.Background(), []string{"config", "show", "--count=bad", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if badValue.Kind != commandkit.OutcomeUsage || factoryCalls != 0 {
t.Fatalf("help skipped conversion of supplied values: %#v", badValue)
}
out.Reset()
versionOutcome := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if versionOutcome.Kind != commandkit.OutcomeSuccess || factoryCalls != 0 || runCalls != 0 {
t.Fatalf("version invoked an application command: %#v calls=%d/%d", versionOutcome, factoryCalls, runCalls)
}
failed := app.Execute(context.Background(), []string{"config", "show", "--help"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || failed.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(failed.Presentation.Output.WriteError, io.ErrShortWrite) {
t.Fatalf("failed help delivery = %#v", failed)
}
out.Reset()
unknown := app.Execute(context.Background(), []string{"help", "config", "missing"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if unknown.Kind != commandkit.OutcomeUsage || !strings.Contains(diagnostic.String(), "unknown help path") {
t.Fatalf("unknown help path = %#v, diagnostics %q", unknown, diagnostic.String())
}
badFlag := app.Execute(context.Background(), []string{"config", "show", "--unknown", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if badFlag.Kind != commandkit.OutcomeUsage {
t.Fatalf("help accepted an unknown flag: %#v", badFlag)
}
}
func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
app, err := commandkit.New(commandkit.Identity{Name: "ExampleApp", Revision: "abc123"})
if err != nil {
t.Fatal(err)
}
var out, diagnostic bytes.Buffer
textOutcome := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if textOutcome.Kind != commandkit.OutcomeSuccess || out.String() != "ExampleApp dev (abc123)\n" {
t.Fatalf("default version = %#v, output %q", textOutcome, out.String())
}
out.Reset()
yamlOutcome := app.Execute(context.Background(), []string{"version", "--format=yaml"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if yamlOutcome.Kind != commandkit.OutcomeSuccess || out.String() != "name: ExampleApp\nversion: dev\nrevision: abc123\n" {
t.Fatalf("YAML version = %#v, output %q", yamlOutcome, out.String())
}
out.Reset()
component := app.Execute(context.Background(), []string{"version", "--scope=commandkit"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if component.Kind != commandkit.OutcomeSuccess || !strings.HasPrefix(out.String(), "CommandKit ") || !strings.HasSuffix(out.String(), "\n") {
t.Fatalf("CommandKit version = %#v, output %q", component, out.String())
}
out.Reset()
invalid := app.Execute(context.Background(), []string{"version", "--scope=unknown"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if invalid.Kind != commandkit.OutcomeUsage || out.Len() != 0 {
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
}
failed := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || failed.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(failed.Presentation.Output.WriteError, io.ErrShortWrite) {
t.Fatalf("failed version delivery = %#v", failed)
}
}
type failingWriter struct{ err error }
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
type shortWriter struct{}
func (shortWriter) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
return len(data) - 1, nil
}
func TestHelpEntryFormsPresentReportAndDiagnosticFailures(t *testing.T) {
group := commandkit.Group("notes", "Notes metadata.")
app, err := commandkit.New(commandkit.Identity{Name: "example"}, group)
if err != nil {
t.Fatal(err)
}
reportErr := errors.New("report failed")
diagnosticErr := errors.New("diagnostic failed")
for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} {
var diagnostics bytes.Buffer
result := app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: &diagnostics})
if result.Kind != commandkit.OutcomeOperational || result.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Output.WriteError, reportErr) || result.Presentation.Diagnostics.State != commandkit.DeliveryComplete || (!strings.Contains(diagnostics.String(), "deliver") || !strings.Contains(diagnostics.String(), "output")) {
t.Fatalf("%v: result=%#v diagnostics=%q", args, result, diagnostics.String())
}
result = app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: failingWriter{diagnosticErr}})
if result.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Output.WriteError, reportErr) || result.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Diagnostics.WriteError, diagnosticErr) {
t.Fatalf("%v: result=%#v", args, result)
}
}
}
func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T) {
cause := errors.New("domain cause")
for _, tc := range []struct {
outcome commandkit.Outcome
kind commandkit.OutcomeKind
status int
domain string
cause error
}{
{commandkit.Outcome{}, commandkit.OutcomeSuccess, 0, "", nil},
{commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 2, Domain: "needs action", Cause: cause}, commandkit.OutcomeDomain, 2, "needs action", cause},
{commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 0, Domain: "complete"}, commandkit.OutcomeDomain, 0, "complete", nil},
} {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return tc.outcome }})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostic bytes.Buffer
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause {
t.Fatalf("result=%#v expected=%#v", result, tc)
}
if tc.status != 0 && !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
t.Fatalf("failed outcome omitted its fallback explanation: %q", diagnostic.String())
}
if tc.cause != nil && strings.Contains(diagnostic.String(), tc.cause.Error()) {
t.Fatalf("operation cause was presented automatically: %q", diagnostic.String())
}
}
}
func TestNilDiagnosticWriterDoesNotHideOperationCause(t *testing.T) {
cause := errors.New("operation failed")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
return commandkit.Failure("The operation failed.").WithCause(cause)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, cause) || result.Presentation.Diagnostics.State != commandkit.DeliveryComplete {
t.Fatalf("nil diagnostic destination changed the outcome: %#v", result)
}
}