Reconcile reporting tests and boundaries
This commit is contained in:
@@ -32,7 +32,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) {
|
|||||||
for _, fragment := range []string{
|
for _, fragment := range []string{
|
||||||
"resolved command alias \"shortcut\" to \"run\"",
|
"resolved command alias \"shortcut\" to \"run\"",
|
||||||
"selected debug reporting mode using --trace (alias for --debug)",
|
"selected debug reporting mode using --trace (alias for --debug)",
|
||||||
"INFO: deferred summary",
|
"deferred summary",
|
||||||
"handler completed for \"run\" with operational outcome and status 1",
|
"handler completed for \"run\" with operational outcome and status 1",
|
||||||
"ERROR: operation failed",
|
"ERROR: operation failed",
|
||||||
} {
|
} {
|
||||||
@@ -46,7 +46,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) {
|
|||||||
ordered := []string{
|
ordered := []string{
|
||||||
"resolved command alias",
|
"resolved command alias",
|
||||||
"selected debug reporting mode",
|
"selected debug reporting mode",
|
||||||
"INFO: deferred summary",
|
"deferred summary",
|
||||||
"handler completed",
|
"handler completed",
|
||||||
"ERROR: operation failed",
|
"ERROR: operation failed",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Invocation reporting implementation plan
|
# Invocation reporting implementation plan
|
||||||
|
|
||||||
Status: Stages 1–17 complete. Stage 18 is next.
|
Status: Stages 1–18 complete. Stage 19 is next.
|
||||||
|
|
||||||
## Objective and authority
|
## Objective and authority
|
||||||
|
|
||||||
@@ -676,7 +676,7 @@ ConfigKit, aliased quiet help, and offline PromptKit smoke commands also pass.
|
|||||||
|
|
||||||
## Stage 18: Reconcile the full test suite and architectural boundaries
|
## Stage 18: Reconcile the full test suite and architectural boundaries
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
1. Find and migrate remaining callers of removed constructors, raw invocation
|
1. Find and migrate remaining callers of removed constructors, raw invocation
|
||||||
streams, terminal `SeverityError`, outcome diagnostic submission, and old
|
streams, terminal `SeverityError`, outcome diagnostic submission, and old
|
||||||
@@ -704,6 +704,19 @@ Validation: the suite should now build as a whole using the local three-module
|
|||||||
workspace. Focused reporter race testing is justified by the selected concurrent
|
workspace. Focused reporter race testing is justified by the selected concurrent
|
||||||
reporter contract; it does not establish a new permanent repository-wide gate.
|
reporter contract; it does not establish a new permanent repository-wide gate.
|
||||||
|
|
||||||
|
Completion: migrated the remaining core tests and benchmark caller to reporter,
|
||||||
|
explicit explanation/cause, and presentation-result APIs. Updated reporting
|
||||||
|
expectations for unprefixed Info, routed flag-alias parsing, quiet selection on
|
||||||
|
routing failures, and causes remaining separate from terminal explanations.
|
||||||
|
Reporter tests cover delivery states, retained failures, ignored errors,
|
||||||
|
finalization, domain statuses, quiet late handles, independent healthy
|
||||||
|
destinations, complete concurrent operations, and caller-owned writers.
|
||||||
|
`GOTOOLCHAIN=local go test -count=1 ./...` passes in the root, ckconfig, and
|
||||||
|
ckprompt modules; `GOTOOLCHAIN=local go test -race -run '^TestReporter' .` passes.
|
||||||
|
Boundary checks confirm Cobra/pflag imports stay under `internal/cobra`, public
|
||||||
|
Go documentation exposes no backend or internal types, and core does not depend
|
||||||
|
on either optional integration module.
|
||||||
|
|
||||||
## Stage 19: Update canonical documentation and compatibility guidance
|
## Stage 19: Update canonical documentation and compatibility guidance
|
||||||
|
|
||||||
Status: pending.
|
Status: pending.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
var inputs []input
|
var inputs []input
|
||||||
var contexts []context.Context
|
var contexts []context.Context
|
||||||
var streams []commandkit.Streams
|
var reporters []*commandkit.Reporter
|
||||||
firstContext := context.WithValue(context.Background(), struct{}{}, "first")
|
firstContext := context.WithValue(context.Background(), struct{}{}, "first")
|
||||||
secondContext := context.WithValue(context.Background(), struct{}{}, "second")
|
secondContext := context.WithValue(context.Background(), struct{}{}, "second")
|
||||||
var firstOut, secondOut, firstDiagnostic, secondDiagnostic bytes.Buffer
|
var firstOut, secondOut, firstDiagnostic, secondDiagnostic bytes.Buffer
|
||||||
@@ -32,7 +32,12 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
|
|||||||
Run: func(invocation commandkit.Invocation, value input) commandkit.Outcome {
|
Run: func(invocation commandkit.Invocation, value input) commandkit.Outcome {
|
||||||
inputs = append(inputs, value)
|
inputs = append(inputs, value)
|
||||||
contexts = append(contexts, invocation.Context)
|
contexts = append(contexts, invocation.Context)
|
||||||
streams = append(streams, invocation.Streams)
|
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()
|
return commandkit.Success()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -52,11 +57,11 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
|
|||||||
if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set {
|
if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set {
|
||||||
t.Fatalf("second invocation inherited state: %#v", inputs[1])
|
t.Fatalf("second invocation inherited state: %#v", inputs[1])
|
||||||
}
|
}
|
||||||
if streams[2].Out != io.Discard || streams[2].Err != io.Discard {
|
if reporters[0] == nil || reporters[1] == nil || reporters[2] == nil || reporters[0] == reporters[1] || reporters[0] == reporters[2] || reporters[1] == reporters[2] {
|
||||||
t.Fatalf("nil streams were not replaced with io.Discard: %#v", streams[2])
|
t.Fatalf("invocations did not receive fresh reporters: %#v", reporters)
|
||||||
}
|
}
|
||||||
if contexts[0] != firstContext || contexts[1] != secondContext || streams[0].Out != &firstOut || streams[0].Err != &firstDiagnostic || streams[1].Out != &secondOut || streams[1].Err != &secondDiagnostic {
|
if firstOut.String() != "first\n" || secondOut.String() != "\n" || contexts[0] != firstContext || contexts[1] != secondContext {
|
||||||
t.Fatalf("handler invocation contexts or streams were not forwarded independently")
|
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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +70,7 @@ func TestOmittedInputFactoryUsesExactPointerZeroValue(t *testing.T) {
|
|||||||
Name: "run",
|
Name: "run",
|
||||||
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
|
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
return commandkit.Failure(errors.New("omitted input factory allocated a pointer"))
|
return commandkit.Failure("omitted input factory allocated a pointer")
|
||||||
}
|
}
|
||||||
return commandkit.Success()
|
return commandkit.Success()
|
||||||
},
|
},
|
||||||
@@ -136,9 +141,9 @@ func TestExecutePresentsHandlerUsageAfterRegisteringCommand(t *testing.T) {
|
|||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||||
calls++
|
calls++
|
||||||
if err := app.Register(late); err != nil {
|
if err := app.Register(late); err != nil {
|
||||||
return commandkit.Failure(err)
|
return commandkit.Failure(err.Error()).WithCause(err)
|
||||||
}
|
}
|
||||||
return commandkit.Usage(usageCause)
|
return commandkit.Usage("The handler requests usage.").WithCause(usageCause)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
||||||
@@ -195,12 +200,11 @@ func TestExecuteReturnsUsageForParsingRequirementsAndInputChecks(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
|
func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
|
||||||
domainCause := errors.New("domain rejected the request")
|
domainCause := errors.New("domain rejected the request")
|
||||||
warning := commandkit.MessageDiagnostic(commandkit.SeverityWarning, "review needed")
|
|
||||||
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
|
||||||
outcome := commandkit.Domain("sample", 2, domainCause)
|
_ = invocation.Reporter().Warn("review needed")
|
||||||
outcome.Diagnostics = []commandkit.Diagnostic{warning}
|
outcome := commandkit.Domain("sample", 2).WithExplanation("The request was rejected.").WithCause(domainCause)
|
||||||
return outcome
|
return outcome
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -209,19 +213,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var diagnostic bytes.Buffer
|
var diagnostic bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
|
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) {
|
if outcome.Kind != commandkit.OutcomeDomain || outcome.Domain != "sample" || outcome.Status != 2 || !errors.Is(outcome.Cause, domainCause) {
|
||||||
t.Fatalf("domain outcome changed: %#v", outcome)
|
t.Fatalf("domain outcome changed: %#v", outcome)
|
||||||
}
|
}
|
||||||
if diagnostic.String() != "review needed\ndomain rejected the request\n" || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
|
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)
|
t.Fatalf("warning delivery = %q, %#v", diagnostic.String(), outcome)
|
||||||
}
|
}
|
||||||
warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
Name: "warn",
|
Name: "warn",
|
||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
|
||||||
result := commandkit.Success()
|
_ = invocation.Reporter().Warn("review needed")
|
||||||
result.Diagnostics = []commandkit.Diagnostic{warning}
|
return commandkit.Success()
|
||||||
return result
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
|
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
|
||||||
@@ -229,23 +232,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
diagnostic.Reset()
|
diagnostic.Reset()
|
||||||
outcome = app.Execute(context.Background(), []string{"warn"}, commandkit.Streams{Err: &diagnostic})
|
outcome = app.Execute(context.Background(), []string{"warn", "--verbose"}, commandkit.Streams{Err: &diagnostic})
|
||||||
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "review needed\n" {
|
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "WARN: review needed\n" {
|
||||||
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
|
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T) {
|
func TestExecuteDoesNotPresentOperationCauseWithoutExplanation(t *testing.T) {
|
||||||
cause := errors.New("already shown")
|
cause := errors.New("private operation detail")
|
||||||
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||||
outcome := commandkit.Domain("sample", 2, cause)
|
return commandkit.Domain("sample", 2).WithCause(cause)
|
||||||
outcome.Diagnostics = []commandkit.Diagnostic{{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error {
|
|
||||||
_, err := io.WriteString(writer, "presented\n")
|
|
||||||
return err
|
|
||||||
}}}
|
|
||||||
return outcome
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
||||||
@@ -254,21 +252,11 @@ func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T)
|
|||||||
}
|
}
|
||||||
var diagnostic bytes.Buffer
|
var diagnostic bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
|
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
|
||||||
if outcome.Kind != commandkit.OutcomeDomain || diagnostic.String() != "presented\n" {
|
if outcome.Kind != commandkit.OutcomeDomain || !errors.Is(outcome.Cause, cause) {
|
||||||
t.Fatalf("presented cause was duplicated: %#v, %q", outcome, diagnostic.String())
|
t.Fatalf("domain outcome changed: %#v", outcome)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(diagnostic.String(), cause.Error()) || !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
|
||||||
diagnostic.Reset()
|
t.Fatalf("cause was presented instead of the fallback explanation: %#v, %q", outcome, diagnostic.String())
|
||||||
completed := commandkit.Domain("sample", 2, cause)
|
|
||||||
completed.DiagnosticDelivery = commandkit.DeliveryComplete
|
|
||||||
command = commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "done", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return completed }})
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,9 +266,7 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
|
|||||||
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||||
outcome := commandkit.Failure(operationFailure)
|
return commandkit.Failure("The operation was canceled.").WithCause(operationFailure)
|
||||||
outcome.ReportIncomplete = true
|
|
||||||
return outcome
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
||||||
@@ -288,12 +274,12 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{err: writeFailure}})
|
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) {
|
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)
|
t.Fatalf("combined operation and delivery failure = %#v", outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
short := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: shortWriter{}})
|
short := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: shortWriter{}})
|
||||||
if !errors.Is(short.DeliveryError, io.ErrShortWrite) || short.DiagnosticDelivery != commandkit.DeliveryIncomplete {
|
if !errors.Is(short.Presentation.Diagnostics.WriteError, io.ErrShortWrite) || short.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete {
|
||||||
t.Fatalf("short write was not detected: %#v", short)
|
t.Fatalf("short write was not detected: %#v", short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +325,7 @@ func TestExecuteRunsApplicationOwnedNestedHelp(t *testing.T) {
|
|||||||
)},
|
)},
|
||||||
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
|
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
|
||||||
received = append(received, value)
|
received = append(received, value)
|
||||||
return commandkit.Domain("session-help", 7, domainCause)
|
return commandkit.Domain("session-help", 7).WithExplanation("Session help is unavailable.").WithCause(domainCause)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
session := commandkit.Group("session", "Session commands.", help)
|
session := commandkit.Group("session", "Session commands.", help)
|
||||||
@@ -463,7 +449,7 @@ func TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
|
|||||||
t.Fatalf("version invoked an application command: %#v calls=%d/%d", versionOutcome, factoryCalls, runCalls)
|
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})
|
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) {
|
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)
|
t.Fatalf("failed help delivery = %#v", failed)
|
||||||
}
|
}
|
||||||
out.Reset()
|
out.Reset()
|
||||||
@@ -503,7 +489,7 @@ func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
|
|||||||
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
|
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
|
||||||
}
|
}
|
||||||
failed := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
|
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) {
|
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)
|
t.Fatalf("failed version delivery = %#v", failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,11 +518,11 @@ func TestHelpEntryFormsPresentReportAndDiagnosticFailures(t *testing.T) {
|
|||||||
for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} {
|
for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} {
|
||||||
var diagnostics bytes.Buffer
|
var diagnostics bytes.Buffer
|
||||||
result := app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: &diagnostics})
|
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 {
|
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())
|
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}})
|
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) {
|
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)
|
t.Fatalf("%v: result=%#v", args, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,46 +546,34 @@ func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
|
var diagnostic bytes.Buffer
|
||||||
wantDelivery := commandkit.DeliveryNone
|
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
|
||||||
if tc.cause != nil {
|
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause {
|
||||||
wantDelivery = commandkit.DeliveryComplete
|
|
||||||
}
|
|
||||||
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause || result.DiagnosticDelivery != wantDelivery {
|
|
||||||
t.Fatalf("result=%#v expected=%#v", result, tc)
|
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 TestNilDiagnosticWritersHaveNoOutputAndDoNotHideCauses(t *testing.T) {
|
func TestNilDiagnosticWriterDoesNotHideOperationCause(t *testing.T) {
|
||||||
cause := errors.New("operation failed")
|
cause := errors.New("operation failed")
|
||||||
for _, base := range []commandkit.Outcome{commandkit.Success(), commandkit.Domain("needs action", 2, nil), commandkit.Failure(cause)} {
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
base.Diagnostics = []commandkit.Diagnostic{{}, {Severity: commandkit.SeverityWarning}, {Severity: commandkit.SeverityError}}
|
Name: "run",
|
||||||
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return base }})
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
return commandkit.Failure("The operation failed.").WithCause(cause)
|
||||||
if err != nil {
|
},
|
||||||
t.Fatal(err)
|
})
|
||||||
}
|
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
||||||
var diagnostics bytes.Buffer
|
if err != nil {
|
||||||
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostics})
|
t.Fatal(err)
|
||||||
want := commandkit.DeliveryNone
|
}
|
||||||
if base.Cause != nil {
|
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
|
||||||
want = commandkit.DeliveryComplete
|
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)
|
||||||
if result.Kind != base.Kind || result.Status != base.Status || result.Domain != base.Domain || result.Cause != base.Cause || result.DeliveryError != nil || result.DiagnosticDelivery != want {
|
|
||||||
t.Fatalf("result=%#v base=%#v", result, base)
|
|
||||||
}
|
|
||||||
if base.Cause == nil && diagnostics.Len() != 0 {
|
|
||||||
t.Fatalf("unexpected output=%q", diagnostics.String())
|
|
||||||
}
|
|
||||||
if base.Cause != nil && strings.Count(diagnostics.String(), cause.Error()) != 1 {
|
|
||||||
t.Fatalf("cause output=%q", diagnostics.String())
|
|
||||||
}
|
|
||||||
if base.Cause != nil {
|
|
||||||
result = app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{io.ErrClosedPipe}})
|
|
||||||
if !errors.Is(result.Cause, cause) || !errors.Is(result.DeliveryError, io.ErrClosedPipe) || result.DiagnosticDelivery != commandkit.DeliveryIncomplete {
|
|
||||||
t.Fatalf("failed delivery=%#v", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ func newReportingParseApp(t *testing.T) *App {
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseReportingArgs(app *App, args []string) commandmeta.Result {
|
||||||
|
route := resolveEnteredRoute(args, app.routeSnapshot())
|
||||||
|
current := &execution{app: app, route: route}
|
||||||
|
return current.parse(route.args)
|
||||||
|
}
|
||||||
|
|
||||||
func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) {
|
func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
@@ -37,7 +43,7 @@ func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) {
|
|||||||
{"disabled quiet leaves verbose", []string{"run", "--verbose", "--quiet=false"}, commandmeta.ReportingModeVerbose},
|
{"disabled quiet leaves verbose", []string{"run", "--verbose", "--quiet=false"}, commandmeta.ReportingModeVerbose},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
result := newReportingParseApp(t).parse(test.args, commandmeta.ParseOptions{})
|
result := parseReportingArgs(newReportingParseApp(t), test.args)
|
||||||
if result.Failure != nil || result.Reporting.Mode != test.mode {
|
if result.Failure != nil || result.Reporting.Mode != test.mode {
|
||||||
t.Fatalf("parse result = %#v, want mode %d", result, test.mode)
|
t.Fatalf("parse result = %#v, want mode %d", result, test.mode)
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,7 @@ func TestProgressSelectionIsIndependentAndUsesLastOccurrence(t *testing.T) {
|
|||||||
{"unset by default", []string{"run", "--verbose"}, commandmeta.ReportingModeVerbose, false, false},
|
{"unset by default", []string{"run", "--verbose"}, commandmeta.ReportingModeVerbose, false, false},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
result := newReportingParseApp(t).parse(test.args, commandmeta.ParseOptions{})
|
result := parseReportingArgs(newReportingParseApp(t), test.args)
|
||||||
selection := result.Reporting
|
selection := result.Reporting
|
||||||
if result.Failure != nil || selection.Mode != test.mode || selection.Progress != test.progress || selection.ProgressSet != test.progressSet {
|
if result.Failure != nil || selection.Mode != test.mode || selection.Progress != test.progress || selection.ProgressSet != test.progressSet {
|
||||||
t.Fatalf("parse selection = %#v, failure = %v", selection, result.Failure)
|
t.Fatalf("parse selection = %#v, failure = %v", selection, result.Failure)
|
||||||
@@ -71,9 +77,9 @@ func TestProgressSelectionIsIndependentAndUsesLastOccurrence(t *testing.T) {
|
|||||||
|
|
||||||
func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
|
func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
|
||||||
app := newReportingParseApp(t)
|
app := newReportingParseApp(t)
|
||||||
result := app.parse([]string{"run", "--verbose", "-q"}, commandmeta.ParseOptions{})
|
result := parseReportingArgs(app, []string{"run", "--verbose", "-q"})
|
||||||
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
|
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
|
||||||
t.Fatalf("aliased selection = %#v", result)
|
t.Fatalf("aliased selection = %#v, failure=%+v", result, result.Failure)
|
||||||
}
|
}
|
||||||
want := []commandmeta.FlagOccurrence{
|
want := []commandmeta.FlagOccurrence{
|
||||||
{Name: "verbose", Spelling: "--verbose", RawValue: "true"},
|
{Name: "verbose", Spelling: "--verbose", RawValue: "true"},
|
||||||
@@ -83,12 +89,12 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
|
|||||||
t.Fatalf("occurrences = %#v, want %#v", result.FlagOccurrences, want)
|
t.Fatalf("occurrences = %#v, want %#v", result.FlagOccurrences, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = app.parse([]string{"run", "--verbose", "--unknown", "--quiet"}, commandmeta.ParseOptions{})
|
result = parseReportingArgs(app, []string{"run", "--verbose", "--unknown", "--quiet"})
|
||||||
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 1 {
|
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 1 {
|
||||||
t.Fatalf("unknown flag partial selection = %#v", result)
|
t.Fatalf("unknown flag partial selection = %#v", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = app.parse([]string{"run", "--verbose", "--quiet=invalid", "--debug"}, commandmeta.ParseOptions{})
|
result = parseReportingArgs(app, []string{"run", "--verbose", "--quiet=invalid", "--debug"})
|
||||||
if result.Failure == nil || !result.Failure.Usage || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 2 {
|
if result.Failure == nil || !result.Failure.Usage || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 2 {
|
||||||
t.Fatalf("invalid boolean partial selection = %#v", result)
|
t.Fatalf("invalid boolean partial selection = %#v", result)
|
||||||
}
|
}
|
||||||
@@ -96,7 +102,7 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
|
|||||||
|
|
||||||
func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) {
|
func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) {
|
||||||
app := newReportingParseApp(t)
|
app := newReportingParseApp(t)
|
||||||
result := app.parse([]string{"run", "--message=--quiet", "--verbose", "--", "--debug"}, commandmeta.ParseOptions{})
|
result := parseReportingArgs(app, []string{"run", "--message=--quiet", "--verbose", "--", "--debug"})
|
||||||
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || !reflect.DeepEqual(result.Operands, []string{"--debug"}) {
|
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || !reflect.DeepEqual(result.Operands, []string{"--debug"}) {
|
||||||
t.Fatalf("option-looking value or terminator = %#v", result)
|
t.Fatalf("option-looking value or terminator = %#v", result)
|
||||||
}
|
}
|
||||||
@@ -104,17 +110,17 @@ func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) {
|
|||||||
t.Fatalf("reporting-like value became a flag occurrence: %#v", result.FlagOccurrences)
|
t.Fatalf("reporting-like value became a flag occurrence: %#v", result.FlagOccurrences)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = app.parse([]string{"run", "--help=bad", "--quiet"}, commandmeta.ParseOptions{})
|
result = parseReportingArgs(app, []string{"run", "--help=bad", "--quiet"})
|
||||||
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeNormal || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "help" {
|
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeNormal || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "help" {
|
||||||
t.Fatalf("invalid help boolean consumed later flags: %#v", result)
|
t.Fatalf("invalid help boolean consumed later flags: %#v", result)
|
||||||
}
|
}
|
||||||
result = app.parse([]string{"run", "--help=false", "--quiet"}, commandmeta.ParseOptions{})
|
result = parseReportingArgs(app, []string{"run", "--help=false", "--quiet"})
|
||||||
if result.Failure != nil || result.HelpRequested || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
|
if result.Failure != nil || result.HelpRequested || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
|
||||||
t.Fatalf("valid false help boolean changed handling: %#v", result)
|
t.Fatalf("valid false help boolean changed handling: %#v", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = app.parse([]string{"missing", "--quiet"}, commandmeta.ParseOptions{})
|
result = parseReportingArgs(app, []string{"missing", "--quiet"})
|
||||||
if result.Failure == nil || len(result.FlagOccurrences) != 0 {
|
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "quiet" {
|
||||||
t.Fatalf("malformed path produced flag occurrences: %#v", result)
|
t.Fatalf("routing failure did not retain reporting selection: %#v", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func performanceFlatCommands(tb testing.TB, count int, prefix string) []commandk
|
|||||||
run := func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }
|
run := func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }
|
||||||
if prefix == "cmd" && index == 4 {
|
if prefix == "cmd" && index == 4 {
|
||||||
run = func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
run = func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||||
return commandkit.Usage(performanceUsageCause)
|
return commandkit.Usage("The benchmark command needs usage.").WithCause(performanceUsageCause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: name, Summary: "Benchmark command.", Flags: flags, Run: run})
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: name, Summary: "Benchmark command.", Flags: flags, Run: run})
|
||||||
|
|||||||
@@ -49,6 +49,31 @@ func TestReporterOutputSubmitsOnWrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type callerOwnedWriter struct {
|
||||||
|
bytes.Buffer
|
||||||
|
closeCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *callerOwnedWriter) Close() error {
|
||||||
|
w.closeCalls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReporterLeavesCallerWriterOpen(t *testing.T) {
|
||||||
|
destination := &callerOwnedWriter{}
|
||||||
|
reporter := newReporter(Streams{Out: destination})
|
||||||
|
_, _ = reporter.Output().Write([]byte("report"))
|
||||||
|
_ = reporter.RenderOutput(func(writer io.Writer) error {
|
||||||
|
_, err := io.WriteString(writer, " rendered")
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
_ = reporter.finalize(Success(), nil)
|
||||||
|
|
||||||
|
if destination.String() != "report rendered" || destination.closeCalls != 0 {
|
||||||
|
t.Fatalf("destination = %q, Close calls = %d", destination.String(), destination.closeCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
|
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
|
||||||
var destination bytes.Buffer
|
var destination bytes.Buffer
|
||||||
reporter := newReporter(Streams{Out: &destination})
|
reporter := newReporter(Streams{Out: &destination})
|
||||||
|
|||||||
@@ -1020,7 +1020,7 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) {
|
|||||||
|
|
||||||
var diagnostics bytes.Buffer
|
var diagnostics bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), []string{"list"}, Streams{Out: io.Discard, Err: &diagnostics})
|
outcome := app.Execute(context.Background(), []string{"list"}, Streams{Out: io.Discard, Err: &diagnostics})
|
||||||
if outcome.Kind != OutcomeUsage || !strings.Contains(outcome.Cause.Error(), "--help") {
|
if outcome.Kind != OutcomeUsage || !strings.Contains(outcome.Explanation.Message, "--help") {
|
||||||
t.Fatalf("bare discovery prefix = %#v, diagnostics %q; want usage pointing to list --help", outcome, diagnostics.String())
|
t.Fatalf("bare discovery prefix = %#v, diagnostics %q; want usage pointing to list --help", outcome, diagnostics.String())
|
||||||
}
|
}
|
||||||
if inputBuilds != 0 || promptRuns != 0 {
|
if inputBuilds != 0 || promptRuns != 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user