Route help and version through the reporter
This commit is contained in:
97
builtins.go
97
builtins.go
@@ -1,8 +1,6 @@
|
|||||||
package commandkit
|
package commandkit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@@ -58,7 +56,7 @@ func (a *App) builtinCommands() []Declaration {
|
|||||||
if identity.Version == "" {
|
if identity.Version == "" {
|
||||||
identity.Version = "dev"
|
identity.Version = "dev"
|
||||||
}
|
}
|
||||||
return renderVersionOutcome(identity, input.Format, invocation.Streams)
|
return renderVersionOutcome(identity, input.Format, invocation.Reporter())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return []Declaration{help, version}
|
return []Declaration{help, version}
|
||||||
@@ -78,78 +76,41 @@ func parseVersionFormat(value string) (string, error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderHelpOutcome(path []string, streams Streams) Outcome {
|
func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, reporter *Reporter, session *internalcobra.Session) Outcome {
|
||||||
session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{})
|
|
||||||
return a.renderHelpOutcomeWithSession(path, streams, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderHelpTargets(targets []string, streams Streams) Outcome {
|
|
||||||
session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{})
|
|
||||||
return a.renderHelpTargetsWithSession(targets, streams, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderHelpTargetsWithSession(targets []string, streams Streams, session *internalcobra.Session) Outcome {
|
|
||||||
snapshot := a.routeSnapshot()
|
|
||||||
route := resolveEnteredRoute(targets, snapshot)
|
|
||||||
if route.kind == routeUnknown {
|
|
||||||
return a.renderHelpOutcomeWithSession(targets, streams, session)
|
|
||||||
}
|
|
||||||
options := commandmeta.ParseOptions{}
|
|
||||||
switch route.kind {
|
|
||||||
case routeCanonical:
|
|
||||||
options.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords()
|
|
||||||
case routeDiscovery:
|
|
||||||
options.FlagAliases = snapshot.discoveryFlags[route.discoveryPath.Key()].neutralRecords()
|
|
||||||
}
|
|
||||||
session = internalcobra.Prepare(snapshot.metadata, options)
|
|
||||||
return a.renderHelpViewOutcomeWithSession(a.helpView(route, snapshot), streams, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderHelpOutcomeWithSession(path []string, streams Streams, session *internalcobra.Session) Outcome {
|
|
||||||
return a.renderHelpViewOutcomeWithSession(commandmeta.HelpView{EnteredPath: path, CanonicalPath: path}, streams, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, streams Streams, session *internalcobra.Session) Outcome {
|
|
||||||
content, failure := session.RenderHelpView(view)
|
content, failure := session.RenderHelpView(view)
|
||||||
if failure != nil {
|
if failure != nil {
|
||||||
if failure.Usage {
|
if failure.Usage {
|
||||||
return Usage(errors.New(failure.Message))
|
return Usage(failure.Message)
|
||||||
}
|
}
|
||||||
return Failure(errors.New(failure.Message))
|
return Failure(failure.Message)
|
||||||
}
|
|
||||||
if err := writeReport(streams.Out, []byte(content)); err != nil {
|
|
||||||
return incompleteReport(err)
|
|
||||||
}
|
}
|
||||||
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
||||||
|
_, err := io.WriteString(w, content)
|
||||||
|
return err
|
||||||
|
})
|
||||||
return Success()
|
return Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderVersionOutcome(identity Identity, format string, streams Streams) Outcome {
|
func renderVersionOutcome(identity Identity, format string, reporter *Reporter) Outcome {
|
||||||
var content []byte
|
if identity.Version == "" {
|
||||||
|
identity.Version = "dev"
|
||||||
|
}
|
||||||
switch format {
|
switch format {
|
||||||
case "text":
|
case "text":
|
||||||
name := identity.Name
|
line := identity.Name + " " + identity.Version
|
||||||
if identity.Version == "" {
|
|
||||||
identity.Version = "dev"
|
|
||||||
}
|
|
||||||
line := name + " " + identity.Version
|
|
||||||
if identity.Revision != "" {
|
if identity.Revision != "" {
|
||||||
line += " (" + identity.Revision + ")"
|
line += " (" + identity.Revision + ")"
|
||||||
}
|
}
|
||||||
content = []byte(line + "\n")
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
||||||
|
_, err := io.WriteString(w, line+"\n")
|
||||||
|
return err
|
||||||
|
})
|
||||||
case "yaml":
|
case "yaml":
|
||||||
if identity.Version == "" {
|
_ = reporter.RenderOutput(func(w io.Writer) error {
|
||||||
identity.Version = "dev"
|
return encodeYAML(w, versionDocument{Name: identity.Name, Version: identity.Version, Revision: identity.Revision})
|
||||||
}
|
})
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := encodeYAML(&buffer, versionDocument{Name: identity.Name, Version: identity.Version, Revision: identity.Revision}); err != nil {
|
|
||||||
return Failure(err)
|
|
||||||
}
|
|
||||||
content = buffer.Bytes()
|
|
||||||
default:
|
default:
|
||||||
return Usage(fmt.Errorf("unsupported version format %q", format))
|
return Usage(fmt.Sprintf("unsupported version format %q", format))
|
||||||
}
|
|
||||||
if err := writeReport(streams.Out, content); err != nil {
|
|
||||||
return incompleteReport(err)
|
|
||||||
}
|
}
|
||||||
return Success()
|
return Success()
|
||||||
}
|
}
|
||||||
@@ -160,22 +121,6 @@ func encodeYAML(writer io.Writer, document any) error {
|
|||||||
return encoder.Encode(document)
|
return encoder.Encode(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeReport(writer io.Writer, content []byte) error {
|
|
||||||
checked := &checkedWriter{writer: writer}
|
|
||||||
_, err := checked.Write(content)
|
|
||||||
if err == nil {
|
|
||||||
err = checked.failure
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func incompleteReport(err error) Outcome {
|
|
||||||
outcome := Failure(err)
|
|
||||||
outcome.ReportIncomplete = true
|
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBuildInfo() *debug.BuildInfo {
|
func readBuildInfo() *debug.BuildInfo {
|
||||||
info, ok := debug.ReadBuildInfo()
|
info, ok := debug.ReadBuildInfo()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
110
builtins_reporting_test.go
Normal file
110
builtins_reporting_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func builtinReportingTestApp(t *testing.T) *App {
|
||||||
|
t.Helper()
|
||||||
|
command := NewCommand(CommandSpec[struct{}]{
|
||||||
|
Name: "run",
|
||||||
|
Run: func(Invocation, struct{}) Outcome { return Success() },
|
||||||
|
})
|
||||||
|
app, err := New(Identity{Name: "Example", Version: "1.2.3"}, command,
|
||||||
|
Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections"))),
|
||||||
|
Alias("r", "run"), Alias("list collections", "promptkit list collections"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpAndVersionUseManagedOutput(t *testing.T) {
|
||||||
|
app := builtinReportingTestApp(t)
|
||||||
|
for _, args := range [][]string{{"help", "--quiet"}, {"version", "--quiet"}, {"list", "--help", "--quiet"}} {
|
||||||
|
var output, diagnostics bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics})
|
||||||
|
if outcome.Kind != OutcomeSuccess || outcome.Status != 0 {
|
||||||
|
t.Errorf("Execute(%q) = %#v", args, outcome)
|
||||||
|
}
|
||||||
|
if output.Len() != 0 || diagnostics.Len() != 0 {
|
||||||
|
t.Errorf("Execute(%q) wrote output %q or diagnostics %q", args, output.String(), diagnostics.String())
|
||||||
|
}
|
||||||
|
if outcome.Presentation.Output.State != DeliverySuppressed {
|
||||||
|
t.Errorf("Execute(%q) output presentation = %#v", args, outcome.Presentation.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{args: []string{"help", "run"}, want: "run"},
|
||||||
|
{args: []string{"help", "r"}, want: "Alias for: Example run"},
|
||||||
|
{args: []string{"list", "--help"}, want: "Reporting Flags"},
|
||||||
|
{args: []string{"version"}, want: "Example 1.2.3"},
|
||||||
|
{args: []string{"version", "--format=yaml"}, want: "version: 1.2.3"},
|
||||||
|
} {
|
||||||
|
var output, diagnostics bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), test.args, Streams{Out: &output, Err: &diagnostics})
|
||||||
|
if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), test.want) {
|
||||||
|
t.Errorf("Execute(%q) = %#v, output %q", test.args, outcome, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpAndVersionDestinationFailuresUseReporterObservations(t *testing.T) {
|
||||||
|
app := builtinReportingTestApp(t)
|
||||||
|
writeErr := errors.New("help/version destination failed")
|
||||||
|
for _, args := range [][]string{{"help", "run"}, {"version"}} {
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, Streams{
|
||||||
|
Out: executeReportingFailureWriter{err: writeErr}, Err: &diagnostics,
|
||||||
|
})
|
||||||
|
if outcome.Kind != OutcomeOperational || outcome.Status != 1 {
|
||||||
|
t.Errorf("Execute(%q) = %#v", args, outcome)
|
||||||
|
}
|
||||||
|
if !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || outcome.Presentation.Output.State != DeliveryIncomplete {
|
||||||
|
t.Errorf("Execute(%q) presentation = %#v", args, outcome.Presentation)
|
||||||
|
}
|
||||||
|
if !strings.Contains(diagnostics.String(), "ERROR: Command failed.") {
|
||||||
|
t.Errorf("Execute(%q) diagnostics = %q", args, diagnostics.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidHelpAndVersionRequestsRemainUsageOutcomes(t *testing.T) {
|
||||||
|
app := builtinReportingTestApp(t)
|
||||||
|
for _, args := range [][]string{{"help", "unknown"}, {"version", "--scope=unknown"}, {"version", "--format=unknown"}} {
|
||||||
|
var output, diagnostics bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics})
|
||||||
|
if outcome.Kind != OutcomeUsage || outcome.Status != 2 {
|
||||||
|
t.Errorf("Execute(%q) = %#v", args, outcome)
|
||||||
|
}
|
||||||
|
if !strings.Contains(diagnostics.String(), "Usage: Example") {
|
||||||
|
t.Errorf("Execute(%q) diagnostics = %q", args, diagnostics.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuietBuiltinRenderingSuppressesDestinationWrites(t *testing.T) {
|
||||||
|
app := builtinReportingTestApp(t)
|
||||||
|
|
||||||
|
// A quiet successful run must not touch its supplied result destination.
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"version", "--quiet"}, Streams{
|
||||||
|
Out: executeReportingFailureWriter{err: io.ErrClosedPipe}, Err: &diagnostics,
|
||||||
|
})
|
||||||
|
if outcome.Kind != OutcomeSuccess || outcome.Presentation.Output.State != DeliverySuppressed {
|
||||||
|
t.Fatalf("quiet version outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
if diagnostics.Len() != 0 {
|
||||||
|
t.Fatalf("quiet version diagnostics = %q", diagnostics.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Invocation reporting implementation plan
|
# Invocation reporting implementation plan
|
||||||
|
|
||||||
Status: Stages 1–7 complete. Stage 8 is next.
|
Status: Stages 1–8 complete. Stage 9 is next.
|
||||||
|
|
||||||
## Objective and authority
|
## Objective and authority
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ selection, quiet usage guidance, early errors, and ignored output failures.
|
|||||||
|
|
||||||
## Stage 8: Migrate help and version presentation
|
## Stage 8: Migrate help and version presentation
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
Primary files: [builtins.go](../../builtins.go), help metadata/rendering,
|
Primary files: [builtins.go](../../builtins.go), help metadata/rendering,
|
||||||
and built-in consumer tests.
|
and built-in consumer tests.
|
||||||
@@ -340,6 +340,13 @@ alias-only discovery, shared reporting flags, invalid help/version requests,
|
|||||||
factory-free behavior, and help/version destination failures. Invalid requests
|
factory-free behavior, and help/version destination failures. Invalid requests
|
||||||
remain usage outcomes unless presentation subsequently fails.
|
remain usage outcomes unless presentation subsequently fails.
|
||||||
|
|
||||||
|
Help and version reports now render through the invocation reporter, including
|
||||||
|
quiet-mode rendering and destination-failure accounting. Tests cover quiet
|
||||||
|
success, canonical and alias help, alias-only discovery, invalid requests, and
|
||||||
|
destination failures. The internal suites pass; root-package execution remains
|
||||||
|
deferred because the existing external test package still references APIs
|
||||||
|
scheduled for the later suite reconciliation.
|
||||||
|
|
||||||
## Stage 9: Add core debug events
|
## Stage 9: Add core debug events
|
||||||
|
|
||||||
Status: pending.
|
Status: pending.
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func (current *execution) renderHelp(path []string) Outcome {
|
|||||||
}
|
}
|
||||||
session := current.presentationSession()
|
session := current.presentationSession()
|
||||||
view := current.app.helpView(current.route, current.snapshot)
|
view := current.app.helpView(current.route, current.snapshot)
|
||||||
return current.app.renderHelpViewOutcomeWithSession(view, current.streams, session)
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (current *execution) renderHelpTargets(targets []string) Outcome {
|
func (current *execution) renderHelpTargets(targets []string) Outcome {
|
||||||
@@ -184,7 +184,8 @@ func (current *execution) renderHelpTargets(targets []string) Outcome {
|
|||||||
// Discovery routes have no canonical path and retain only trailing options.
|
// Discovery routes have no canonical path and retain only trailing options.
|
||||||
if route.kind == routeUnknown || len(route.args) != len(route.canonicalPath) {
|
if route.kind == routeUnknown || len(route.args) != len(route.canonicalPath) {
|
||||||
session := internalcobra.Prepare(snapshot.metadata, current.options)
|
session := internalcobra.Prepare(snapshot.metadata, current.options)
|
||||||
return current.app.renderHelpOutcomeWithSession(targets, current.streams, session)
|
view := commandmeta.HelpView{EnteredPath: targets, CanonicalPath: targets}
|
||||||
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
||||||
}
|
}
|
||||||
options := current.options
|
options := current.options
|
||||||
switch route.kind {
|
switch route.kind {
|
||||||
@@ -195,7 +196,7 @@ func (current *execution) renderHelpTargets(targets []string) Outcome {
|
|||||||
}
|
}
|
||||||
session := internalcobra.Prepare(snapshot.metadata, options)
|
session := internalcobra.Prepare(snapshot.metadata, options)
|
||||||
view := current.app.helpView(route, snapshot)
|
view := current.app.helpView(route, snapshot)
|
||||||
return current.app.renderHelpViewOutcomeWithSession(view, current.streams, session)
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (current *execution) usageLine(path []string) string {
|
func (current *execution) usageLine(path []string) string {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestDiscoveryUsageRetainsEnteredPrefix(t *testing.T) {
|
|||||||
t.Run(prefix+"/"+test.name, func(t *testing.T) {
|
t.Run(prefix+"/"+test.name, func(t *testing.T) {
|
||||||
var output, diagnostics bytes.Buffer
|
var output, diagnostics bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), append(strings.Fields(prefix), test.tail...), Streams{Out: &output, Err: &diagnostics})
|
outcome := app.Execute(context.Background(), append(strings.Fields(prefix), test.tail...), Streams{Out: &output, Err: &diagnostics})
|
||||||
if outcome.Kind != OutcomeUsage || outcome.Status != 2 || outcome.DiagnosticDelivery != DeliveryComplete || output.Len() != 0 {
|
if outcome.Kind != OutcomeUsage || outcome.Status != 2 || outcome.Presentation.Diagnostics.State != DeliveryComplete || output.Len() != 0 {
|
||||||
t.Fatalf("outcome = %#v, output %q", outcome, output.String())
|
t.Fatalf("outcome = %#v, output %q", outcome, output.String())
|
||||||
}
|
}
|
||||||
for _, fragment := range []string{test.want, "Usage: Demo " + prefix} {
|
for _, fragment := range []string{test.want, "Usage: Demo " + prefix} {
|
||||||
@@ -828,7 +828,7 @@ func TestAliasUsageSurvivesRegistrationDuringExecution(t *testing.T) {
|
|||||||
Name: "inspect",
|
Name: "inspect",
|
||||||
Run: func(Invocation, struct{}) Outcome {
|
Run: func(Invocation, struct{}) Outcome {
|
||||||
registrationErr = test.register(app)
|
registrationErr = test.register(app)
|
||||||
return Usage(errors.New("operation rejected"))
|
return Usage("operation rejected").WithCause(errors.New("operation rejected"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
Reference in New Issue
Block a user