Files
commandkit/execute_external_test.go

440 lines
21 KiB
Go

package commandkit_test
import (
"bytes"
"context"
"errors"
"io"
"strconv"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
type input struct {
name commandkit.Supplied[string]
ids commandkit.Supplied[[]string]
}
var inputs []input
var contexts []context.Context
var streams []commandkit.Streams
ctx := context.WithValue(context.Background(), struct{}{}, "invocation")
var out, diagnostic bytes.Buffer
command, err := 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)
streams = append(streams, invocation.Streams)
return commandkit.Success()
},
})
if err != nil {
t.Fatal(err)
}
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
first := app.Execute(ctx, []string{"run", "--name=first", "--id=a,b", "--id=c"}, commandkit.Streams{Out: &out, Err: &diagnostic})
second := app.Execute(ctx, []string{"run"}, commandkit.Streams{Out: &out, Err: &diagnostic})
third := app.Execute(ctx, []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 streams[2].Out != io.Discard || streams[2].Err != io.Discard {
t.Fatalf("nil streams were not replaced with io.Discard: %#v", streams[2])
}
if contexts[0] != ctx || streams[0].Out != &out || streams[0].Err != &diagnostic {
t.Fatalf("handler invocation context or streams were not forwarded")
}
}
func TestExecuteReturnsUsageForParsingRequirementsAndInputChecks(t *testing.T) {
type input struct{ count commandkit.Supplied[int] }
parseFailure := errors.New("bad count")
checkFailure := errors.New("incompatible inputs")
command, err := 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() },
})
if err != nil {
t.Fatal(err)
}
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")
warning := commandkit.MessageDiagnostic(configkit.SeverityWarning, "review needed")
command, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, domainCause)
outcome.Diagnostics = []commandkit.Diagnostic{warning}
return outcome
},
})
if err != nil {
t.Fatal(err)
}
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 || outcome.Domain != "sample" || outcome.Status != 2 || !errors.Is(outcome.Cause, domainCause) {
t.Fatalf("domain outcome changed: %#v", outcome)
}
if diagnostic.String() != "review needed\ndomain rejected the request\n" || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
t.Fatalf("warning delivery = %q, %#v", diagnostic.String(), outcome)
}
warningOnly, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "warn",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
result := commandkit.Success()
result.Diagnostics = []commandkit.Diagnostic{warning}
return result
},
})
if err != nil {
t.Fatal(err)
}
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
if err != nil {
t.Fatal(err)
}
diagnostic.Reset()
outcome = app.Execute(context.Background(), []string{"warn"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "review needed\n" {
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
}
}
func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T) {
cause := errors.New("already shown")
command, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, cause)
outcome.Diagnostics = []commandkit.Diagnostic{{Severity: configkit.SeverityError, Write: func(writer io.Writer) error {
_, err := io.WriteString(writer, "presented\n")
return err
}}}
return outcome
},
})
if err != nil {
t.Fatal(err)
}
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 || diagnostic.String() != "presented\n" {
t.Fatalf("presented cause was duplicated: %#v, %q", outcome, diagnostic.String())
}
diagnostic.Reset()
completed := commandkit.Domain("sample", 2, cause)
completed.DiagnosticDelivery = commandkit.DeliveryComplete
command, err = commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "done", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return completed }})
if err != nil {
t.Fatal(err)
}
app, err = commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
outcome = app.Execute(context.Background(), []string{"done"}, commandkit.Streams{Err: &diagnostic})
if diagnostic.Len() != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
t.Fatalf("completed diagnostics were repeated: %#v, %q", outcome, diagnostic.String())
}
}
func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
operationFailure := context.Canceled
writeFailure := errors.New("writer failed")
command, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Failure(operationFailure)
outcome.ReportIncomplete = true
return outcome
},
})
if err != nil {
t.Fatal(err)
}
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.DiagnosticDelivery != commandkit.DeliveryIncomplete || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, operationFailure) || !errors.Is(outcome.DeliveryError, 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.DeliveryError, io.ErrShortWrite) || short.DiagnosticDelivery != 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, err := 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() },
})
if err != nil {
t.Fatal(err)
}
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 TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
type input struct {
count commandkit.Supplied[int]
tags commandkit.Supplied[[]string]
}
factoryCalls, runCalls := 0, 0
show, err := 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() },
})
if err != nil {
t.Fatal(err)
}
config, err := commandkit.Group("config", "Inspect configuration.", show)
if err != nil {
t.Fatal(err)
}
now, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "now", Summary: "Deploy immediately.", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }})
if err != nil {
t.Fatal(err)
}
deployParent, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "deploy", Summary: "Deploy artifacts.", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { runCalls++; return commandkit.Success() }})
if err != nil {
t.Fatal(err)
}
deploy, err := commandkit.WithChildren(deployParent, now)
if err != nil {
t.Fatal(err)
}
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.ReportIncomplete || !errors.Is(failed.DeliveryError, 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.ReportIncomplete || !errors.Is(failed.DeliveryError, 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, err := commandkit.Group("notes", "Notes metadata.")
if err != nil {
t.Fatal(err)
}
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.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || result.DiagnosticDelivery != commandkit.DeliveryComplete || strings.Count(diagnostics.String(), reportErr.Error()) != 1 {
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.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || !errors.Is(result.DeliveryError, diagnosticErr) {
t.Fatalf("%v: result=%#v", args, result)
}
}
}