580 lines
29 KiB
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(), "ERROR: Command failed.") {
|
|
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)
|
|
}
|
|
}
|