Route executions through neutral snapshots
This commit is contained in:
13
app.go
13
app.go
@@ -3,14 +3,19 @@ package commandkit
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App owns one command registry and its application identity.
|
// App owns one command registry and its application identity.
|
||||||
type App struct {
|
type App struct {
|
||||||
identity Identity
|
identity Identity
|
||||||
commands map[string]*commandNode
|
commands map[string]*commandNode
|
||||||
commandTree *internalcobra.Command
|
snapshot *routingSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
type routingSnapshot struct {
|
||||||
|
metadata *commandmeta.Command
|
||||||
|
runners map[commandmeta.Key]commandPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
type commandNode struct {
|
type commandNode struct {
|
||||||
|
|||||||
30
builtins.go
30
builtins.go
@@ -6,9 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,31 +79,25 @@ func parseVersionFormat(value string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderHelpOutcome(path []string, streams Streams) Outcome {
|
func (a *App) renderHelpOutcome(path []string, streams Streams) Outcome {
|
||||||
session := internalcobra.Prepare(a.bridgeTree(), internalcobra.ParseOptions{})
|
session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{})
|
||||||
return a.renderHelpOutcomeWithSession(path, streams, session)
|
return a.renderHelpOutcomeWithSession(path, streams, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderHelpTargets(targets []string, streams Streams) Outcome {
|
func (a *App) renderHelpTargets(targets []string, streams Streams) Outcome {
|
||||||
session := internalcobra.Prepare(a.bridgeTree(), internalcobra.ParseOptions{})
|
session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{})
|
||||||
return a.renderHelpTargetsWithSession(targets, streams, session)
|
return a.renderHelpTargetsWithSession(targets, streams, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderHelpTargetsWithSession(targets []string, streams Streams, session *internalcobra.Session) Outcome {
|
func (a *App) renderHelpTargetsWithSession(targets []string, streams Streams, session *internalcobra.Session) Outcome {
|
||||||
target, err := a.helpCommandTarget(targets)
|
return a.renderHelpOutcomeWithSession(targets, streams, session)
|
||||||
if err != nil {
|
|
||||||
return Usage(err)
|
|
||||||
}
|
|
||||||
return a.renderHelpOutcomeWithSession(target, streams, session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderHelpOutcomeWithSession(path []string, streams Streams, session *internalcobra.Session) Outcome {
|
func (a *App) renderHelpOutcomeWithSession(path []string, streams Streams, session *internalcobra.Session) Outcome {
|
||||||
if len(path) > 0 {
|
|
||||||
if _, ok := a.commandAt(path); !ok {
|
|
||||||
return Usage(fmt.Errorf("unknown help path %q", strings.Join(path, " ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content, failure := session.RenderHelp(path)
|
content, failure := session.RenderHelp(path)
|
||||||
if failure != nil {
|
if failure != nil {
|
||||||
|
if failure.Usage {
|
||||||
|
return Usage(errors.New(failure.Message))
|
||||||
|
}
|
||||||
return Failure(errors.New(failure.Message))
|
return Failure(errors.New(failure.Message))
|
||||||
}
|
}
|
||||||
if err := writeReport(streams.Out, []byte(content)); err != nil {
|
if err := writeReport(streams.Out, []byte(content)); err != nil {
|
||||||
@@ -201,13 +195,3 @@ func commandKitBuildIdentity(info *debug.BuildInfo) Identity {
|
|||||||
}
|
}
|
||||||
return identity
|
return identity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) helpCommandTarget(targets []string) ([]string, error) {
|
|
||||||
if len(targets) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if _, ok := a.commandAt(targets); !ok {
|
|
||||||
return nil, fmt.Errorf("unknown help path %q", strings.Join(targets, " "))
|
|
||||||
}
|
|
||||||
return append([]string(nil), targets...), nil
|
|
||||||
}
|
|
||||||
|
|||||||
18
command.go
18
command.go
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command is an opaque, reusable declaration captured by NewCommand, Group,
|
// Command is an opaque, reusable declaration captured by NewCommand, Group,
|
||||||
@@ -117,8 +117,8 @@ type CommandSpec[I any] struct {
|
|||||||
type commandPayload interface {
|
type commandPayload interface {
|
||||||
validate() error
|
validate() error
|
||||||
hasRun() bool
|
hasRun() bool
|
||||||
bridgeFlags() []internalcobra.Flag
|
bridgeFlags() []commandmeta.Flag
|
||||||
bridgeArguments() []internalcobra.Argument
|
bridgeArguments() []commandmeta.Argument
|
||||||
prepare([][]string, []string, bool) (func(*execution) Outcome, error)
|
prepare([][]string, []string, bool) (func(*execution) Outcome, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@ func (c typedCommand[I]) hasRun() bool {
|
|||||||
return c.spec.Run != nil || c.privateRun != nil
|
return c.spec.Run != nil || c.privateRun != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c typedCommand[I]) bridgeFlags() []internalcobra.Flag {
|
func (c typedCommand[I]) bridgeFlags() []commandmeta.Flag {
|
||||||
flags := make([]internalcobra.Flag, len(c.spec.Flags))
|
flags := make([]commandmeta.Flag, len(c.spec.Flags))
|
||||||
for index, flag := range c.spec.Flags {
|
for index, flag := range c.spec.Flags {
|
||||||
binding := flag.binding
|
binding := flag.binding
|
||||||
flags[index] = internalcobra.Flag{
|
flags[index] = commandmeta.Flag{
|
||||||
Name: binding.meta.Name,
|
Name: binding.meta.Name,
|
||||||
Short: binding.meta.Short,
|
Short: binding.meta.Short,
|
||||||
Usage: binding.meta.Usage,
|
Usage: binding.meta.Usage,
|
||||||
@@ -152,11 +152,11 @@ func (c typedCommand[I]) bridgeFlags() []internalcobra.Flag {
|
|||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c typedCommand[I]) bridgeArguments() []internalcobra.Argument {
|
func (c typedCommand[I]) bridgeArguments() []commandmeta.Argument {
|
||||||
arguments := make([]internalcobra.Argument, len(c.spec.Arguments))
|
arguments := make([]commandmeta.Argument, len(c.spec.Arguments))
|
||||||
for index, argument := range c.spec.Arguments {
|
for index, argument := range c.spec.Arguments {
|
||||||
binding := argument.binding
|
binding := argument.binding
|
||||||
arguments[index] = internalcobra.Argument{
|
arguments[index] = commandmeta.Argument{
|
||||||
Name: binding.meta.Name,
|
Name: binding.meta.Name,
|
||||||
Usage: binding.meta.Usage,
|
Usage: binding.meta.Usage,
|
||||||
Min: binding.min,
|
Min: binding.min,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# Implement the optional integration modules and captured registration API
|
||||||
|
|
||||||
Status: implementation in progress. Stages 1 through 6 are complete; subsequent work follows the ordered roadmap.
|
Status: implementation in progress. Stages 1 through 7 are complete; subsequent work follows the ordered roadmap.
|
||||||
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
||||||
assignment. This document authorizes no publication or changes to sibling
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -357,10 +357,16 @@ help/usage presentation.
|
|||||||
7. Preserve local flags after the complete command path, interspersed operands,
|
7. Preserve local flags after the complete command path, interspersed operands,
|
||||||
`--`, short help, malformed-input behavior, and public error isolation.
|
`--`, short help, malformed-input behavior, and public error isolation.
|
||||||
|
|
||||||
**Completion evidence:** run the now-complete core and adapter suites. Exercise
|
**Completion evidence — complete:** neutral command, binding, parse, and failure
|
||||||
registration followed by help, callback-driven metadata refresh without replay,
|
records now live in `internal/commandmeta`; Cobra owns only its mutable session
|
||||||
runnable parents, repeated execution with fresh values, cancellation, and public
|
and adapter logic. One cached snapshot projects sorted metadata and keys to its
|
||||||
outcome delivery. No concurrency or snapshot-isolation guarantee is added.
|
runner table. Parsing returns a snapshot-local key, and execution selects from
|
||||||
|
that same snapshot without a second path lookup. Each invocation creates one
|
||||||
|
session reused by parse/help/usage; presentation refreshes after registration
|
||||||
|
without reinterpreting the original key or replaying callbacks. Core and adapter
|
||||||
|
tests pass, including registration/help refresh, runner-key pairing, runnable
|
||||||
|
parents, fresh input, cancellation, and outcome delivery. No concurrency or
|
||||||
|
snapshot-isolation guarantee is added.
|
||||||
|
|
||||||
## Stage 8: Implement configuration contracts and native ConfigKit adapters
|
## Stage 8: Implement configuration contracts and native ConfigKit adapters
|
||||||
|
|
||||||
|
|||||||
64
execute.go
64
execute.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Execute parses and dispatches one invocation. args excludes the executable
|
// Execute parses and dispatches one invocation. args excludes the executable
|
||||||
@@ -19,21 +20,22 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco
|
|||||||
}
|
}
|
||||||
streams = normalizeStreams(streams)
|
streams = normalizeStreams(streams)
|
||||||
current := newExecution(a, ctx, streams)
|
current := newExecution(a, ctx, streams)
|
||||||
|
routedSnapshot := current.snapshot
|
||||||
parsed := current.parse(args)
|
parsed := current.parse(args)
|
||||||
if parsed.Failure != nil {
|
if parsed.Failure != nil {
|
||||||
return current.present(Usage(errors.New(parsed.Failure.Message)), parsed.Path)
|
return current.present(Usage(errors.New(parsed.Failure.Message)), parsed.Path)
|
||||||
}
|
}
|
||||||
command, ok := a.commandAt(parsed.Path)
|
payload, ok := routedSnapshot.runners[parsed.Key]
|
||||||
if !ok || command.record == nil || command.record.payload == nil {
|
if !ok || payload == nil {
|
||||||
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
||||||
}
|
}
|
||||||
if parsed.HelpRequested || !command.record.payload.hasRun() {
|
if parsed.HelpRequested || !payload.hasRun() {
|
||||||
if _, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, false); err != nil {
|
if _, err := payload.prepare(parsed.FlagValues, parsed.Operands, false); err != nil {
|
||||||
return current.present(Usage(err), parsed.Path)
|
return current.present(Usage(err), parsed.Path)
|
||||||
}
|
}
|
||||||
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
||||||
}
|
}
|
||||||
run, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
run, err := payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return current.present(Usage(err), parsed.Path)
|
return current.present(Usage(err), parsed.Path)
|
||||||
}
|
}
|
||||||
@@ -44,8 +46,8 @@ type execution struct {
|
|||||||
app *App
|
app *App
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
streams Streams
|
streams Streams
|
||||||
options internalcobra.ParseOptions
|
options commandmeta.ParseOptions
|
||||||
metadata *internalcobra.Command
|
snapshot *routingSnapshot
|
||||||
session *internalcobra.Session
|
session *internalcobra.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,18 +72,18 @@ func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
|||||||
app: app,
|
app: app,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
streams: streams,
|
streams: streams,
|
||||||
options: internalcobra.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err},
|
options: commandmeta.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err},
|
||||||
}
|
}
|
||||||
if app != nil {
|
if app != nil {
|
||||||
current.metadata = app.bridgeTree()
|
current.snapshot = app.routeSnapshot()
|
||||||
current.session = internalcobra.Prepare(current.metadata, current.options)
|
current.session = internalcobra.Prepare(current.snapshot.metadata, current.options)
|
||||||
}
|
}
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
func (current *execution) parse(args []string) internalcobra.Result {
|
func (current *execution) parse(args []string) commandmeta.Result {
|
||||||
if current.app == nil {
|
if current.app == nil {
|
||||||
return internalcobra.Result{Failure: &internalcobra.Failure{Message: "cannot parse commands on a nil app"}}
|
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
|
||||||
}
|
}
|
||||||
return current.session.Parse(args)
|
return current.session.Parse(args)
|
||||||
}
|
}
|
||||||
@@ -90,10 +92,10 @@ func (current *execution) presentationSession() *internalcobra.Session {
|
|||||||
if current.app == nil {
|
if current.app == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
metadata := current.app.bridgeTree()
|
snapshot := current.app.routeSnapshot()
|
||||||
if current.session == nil || current.session.Metadata() != metadata {
|
if current.session == nil || current.snapshot != snapshot {
|
||||||
current.metadata = metadata
|
current.snapshot = snapshot
|
||||||
current.session = internalcobra.Prepare(metadata, current.options)
|
current.session = internalcobra.Prepare(snapshot.metadata, current.options)
|
||||||
}
|
}
|
||||||
return current.session
|
return current.session
|
||||||
}
|
}
|
||||||
@@ -139,34 +141,6 @@ func normalizeStreams(streams Streams) Streams {
|
|||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) commandAt(path []string) (Command, bool) {
|
|
||||||
if a == nil {
|
|
||||||
return Command{}, false
|
|
||||||
}
|
|
||||||
if len(path) == 0 {
|
|
||||||
return Command{}, true
|
|
||||||
}
|
|
||||||
command, ok := a.commands[path[0]]
|
|
||||||
if !ok {
|
|
||||||
return Command{}, false
|
|
||||||
}
|
|
||||||
current := command.command
|
|
||||||
for _, name := range path[1:] {
|
|
||||||
found := false
|
|
||||||
for _, child := range current.record.children {
|
|
||||||
if child.record != nil && child.record.name == name {
|
|
||||||
current = child
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return Command{}, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return current, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome {
|
func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome {
|
||||||
return a.presentWithUsageLine(outcome, path, streams, a.usageLine)
|
return a.presentWithUsageLine(outcome, path, streams, a.usageLine)
|
||||||
}
|
}
|
||||||
@@ -222,7 +196,7 @@ func (a *App) presentWithUsageLine(outcome Outcome, path []string, streams Strea
|
|||||||
|
|
||||||
func (a *App) usageLine(path []string) string {
|
func (a *App) usageLine(path []string) string {
|
||||||
if a != nil {
|
if a != nil {
|
||||||
if usage, failure := internalcobra.UsageLine(a.bridgeTree(), path); failure == nil {
|
if usage, failure := internalcobra.UsageLine(a.routeSnapshot().metadata, path); failure == nil {
|
||||||
return "Usage: " + usage
|
return "Usage: " + usage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,72 +10,21 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
|
|
||||||
spfcobra "github.com/spf13/cobra"
|
spfcobra "github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command describes one node in a per-parse command tree.
|
|
||||||
type Command struct {
|
|
||||||
Name string
|
|
||||||
Summary string
|
|
||||||
Description string
|
|
||||||
Runnable bool
|
|
||||||
Flags []Flag
|
|
||||||
Arguments []Argument
|
|
||||||
Children []*Command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Argument describes one positional input for usage rendering.
|
|
||||||
type Argument struct {
|
|
||||||
Name string
|
|
||||||
Usage string
|
|
||||||
Min int
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag describes one local option without exposing a pflag value.
|
|
||||||
type Flag struct {
|
|
||||||
Name string
|
|
||||||
Short string
|
|
||||||
Usage string
|
|
||||||
Default string
|
|
||||||
Boolean bool
|
|
||||||
Repeated bool
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failure is a backend-owned parse failure translated to neutral message data.
|
|
||||||
type Failure struct {
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result contains the selected command and raw input collected by pflag.
|
|
||||||
type Result struct {
|
|
||||||
Path []string
|
|
||||||
HelpRequested bool
|
|
||||||
FlagValues [][]string
|
|
||||||
Operands []string
|
|
||||||
Failure *Failure
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseOptions makes the parser's context and streams explicit. Nil writers
|
|
||||||
// are replaced with io.Discard.
|
|
||||||
type ParseOptions struct {
|
|
||||||
Context context.Context
|
|
||||||
In io.Reader
|
|
||||||
Out io.Writer
|
|
||||||
Err io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session owns one mutable backend tree for a single command invocation.
|
// Session owns one mutable backend tree for a single command invocation.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
metadata *Command
|
metadata *commandmeta.Command
|
||||||
root *spfcobra.Command
|
root *spfcobra.Command
|
||||||
declarations map[*spfcobra.Command]*Command
|
declarations map[*spfcobra.Command]*commandmeta.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare constructs a fresh backend tree for one invocation.
|
// Prepare constructs a fresh backend tree for one invocation.
|
||||||
func Prepare(root *Command, options ParseOptions) *Session {
|
func Prepare(root *commandmeta.Command, options commandmeta.ParseOptions) *Session {
|
||||||
session := &Session{}
|
session := &Session{}
|
||||||
if root == nil {
|
if root == nil {
|
||||||
return session
|
return session
|
||||||
@@ -87,7 +36,7 @@ func Prepare(root *Command, options ParseOptions) *Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the neutral tree used to prepare this session.
|
// Metadata returns the neutral tree used to prepare this session.
|
||||||
func (session *Session) Metadata() *Command {
|
func (session *Session) Metadata() *commandmeta.Command {
|
||||||
if session == nil {
|
if session == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -95,8 +44,8 @@ func (session *Session) Metadata() *Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse routes leading command words and parses flags on this session's tree.
|
// Parse routes leading command words and parses flags on this session's tree.
|
||||||
func (session *Session) Parse(argv []string) Result {
|
func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||||
result := Result{}
|
result := commandmeta.Result{}
|
||||||
if session == nil || session.root == nil {
|
if session == nil || session.root == nil {
|
||||||
return failure(result, "command tree is nil")
|
return failure(result, "command tree is nil")
|
||||||
}
|
}
|
||||||
@@ -112,6 +61,7 @@ func (session *Session) Parse(argv []string) Result {
|
|||||||
if selectedNode == nil {
|
if selectedNode == nil {
|
||||||
return failure(result, "selected command is not part of the declared tree")
|
return failure(result, "selected command is not part of the declared tree")
|
||||||
}
|
}
|
||||||
|
result.Key = selectedNode.Key
|
||||||
var pathOK bool
|
var pathOK bool
|
||||||
result.Path, pathOK = pathFor(selected, session.declarations)
|
result.Path, pathOK = pathFor(selected, session.declarations)
|
||||||
if !pathOK {
|
if !pathOK {
|
||||||
@@ -149,18 +99,18 @@ func (session *Session) Parse(argv []string) Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderHelp renders registered metadata for an exact path on this session.
|
// RenderHelp renders registered metadata for an exact path on this session.
|
||||||
func (session *Session) RenderHelp(path []string) (string, *Failure) {
|
func (session *Session) RenderHelp(path []string) (string, *commandmeta.Failure) {
|
||||||
if session == nil || session.root == nil {
|
if session == nil || session.root == nil {
|
||||||
return "", &Failure{Message: "command tree is nil"}
|
return "", &commandmeta.Failure{Message: "command tree is nil"}
|
||||||
}
|
}
|
||||||
selected, remaining, err := session.root.Find(path)
|
selected, remaining, err := session.root.Find(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", &Failure{Message: err.Error()}
|
return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", strings.Join(path, " ")), Usage: true}
|
||||||
}
|
}
|
||||||
declaration := session.declarations[selected]
|
declaration := session.declarations[selected]
|
||||||
selectedPath, pathOK := pathFor(selected, session.declarations)
|
selectedPath, pathOK := pathFor(selected, session.declarations)
|
||||||
if declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
|
if declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
|
||||||
return "", &Failure{Message: "help path is not registered"}
|
return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", strings.Join(path, " ")), Usage: true}
|
||||||
}
|
}
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
previousOut, previousErr := selected.OutOrStdout(), selected.ErrOrStderr()
|
previousOut, previousErr := selected.OutOrStdout(), selected.ErrOrStderr()
|
||||||
@@ -171,21 +121,21 @@ func (session *Session) RenderHelp(path []string) (string, *Failure) {
|
|||||||
selected.SetErr(previousErr)
|
selected.SetErr(previousErr)
|
||||||
}()
|
}()
|
||||||
if err := selected.Usage(); err != nil {
|
if err := selected.Usage(); err != nil {
|
||||||
return "", &Failure{Message: err.Error()}
|
return "", &commandmeta.Failure{Message: err.Error()}
|
||||||
}
|
}
|
||||||
return buffer.String(), nil
|
return buffer.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsageLine returns Cobra's usage line for an exact path on this session.
|
// UsageLine returns Cobra's usage line for an exact path on this session.
|
||||||
func (session *Session) UsageLine(path []string) (string, *Failure) {
|
func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) {
|
||||||
if session == nil || session.root == nil {
|
if session == nil || session.root == nil {
|
||||||
return "", &Failure{Message: "command tree is nil"}
|
return "", &commandmeta.Failure{Message: "command tree is nil"}
|
||||||
}
|
}
|
||||||
selected, remaining, err := session.root.Find(path)
|
selected, remaining, err := session.root.Find(path)
|
||||||
declaration := session.declarations[selected]
|
declaration := session.declarations[selected]
|
||||||
selectedPath, pathOK := pathFor(selected, session.declarations)
|
selectedPath, pathOK := pathFor(selected, session.declarations)
|
||||||
if err != nil || declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
|
if err != nil || declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
|
||||||
return "", &Failure{Message: "usage path is not registered"}
|
return "", &commandmeta.Failure{Message: "usage path is not registered"}
|
||||||
}
|
}
|
||||||
line := selected.UseLine()
|
line := selected.UseLine()
|
||||||
if selected.HasSubCommands() {
|
if selected.HasSubCommands() {
|
||||||
@@ -195,21 +145,21 @@ func (session *Session) UsageLine(path []string) (string, *Failure) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderHelp renders a fresh command tree for one help request.
|
// RenderHelp renders a fresh command tree for one help request.
|
||||||
func RenderHelp(root *Command, path []string) (string, *Failure) {
|
func RenderHelp(root *commandmeta.Command, path []string) (string, *commandmeta.Failure) {
|
||||||
return Prepare(root, ParseOptions{}).RenderHelp(path)
|
return Prepare(root, commandmeta.ParseOptions{}).RenderHelp(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsageLine resolves a path on a fresh command tree.
|
// UsageLine resolves a path on a fresh command tree.
|
||||||
func UsageLine(root *Command, path []string) (string, *Failure) {
|
func UsageLine(root *commandmeta.Command, path []string) (string, *commandmeta.Failure) {
|
||||||
return Prepare(root, ParseOptions{}).UsageLine(path)
|
return Prepare(root, commandmeta.ParseOptions{}).UsageLine(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse constructs a fresh command tree and parses one argv.
|
// Parse constructs a fresh command tree and parses one argv.
|
||||||
func Parse(root *Command, argv []string, options ParseOptions) Result {
|
func Parse(root *commandmeta.Command, argv []string, options commandmeta.ParseOptions) commandmeta.Result {
|
||||||
return Prepare(root, options).Parse(argv)
|
return Prepare(root, options).Parse(argv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeOptions(options ParseOptions) ParseOptions {
|
func normalizeOptions(options commandmeta.ParseOptions) commandmeta.ParseOptions {
|
||||||
if options.Context == nil {
|
if options.Context == nil {
|
||||||
options.Context = context.Background()
|
options.Context = context.Background()
|
||||||
}
|
}
|
||||||
@@ -225,10 +175,10 @@ func normalizeOptions(options ParseOptions) ParseOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTree(root *Command, options ParseOptions) (*spfcobra.Command, map[*spfcobra.Command]*Command) {
|
func buildTree(root *commandmeta.Command, options commandmeta.ParseOptions) (*spfcobra.Command, map[*spfcobra.Command]*commandmeta.Command) {
|
||||||
nodes := make(map[*spfcobra.Command]*Command)
|
nodes := make(map[*spfcobra.Command]*commandmeta.Command)
|
||||||
var build func(*Command) *spfcobra.Command
|
var build func(*commandmeta.Command) *spfcobra.Command
|
||||||
build = func(declaration *Command) *spfcobra.Command {
|
build = func(declaration *commandmeta.Command) *spfcobra.Command {
|
||||||
backend := &spfcobra.Command{
|
backend := &spfcobra.Command{
|
||||||
Use: commandUse(declaration),
|
Use: commandUse(declaration),
|
||||||
Short: declaration.Summary,
|
Short: declaration.Summary,
|
||||||
@@ -290,7 +240,7 @@ Flags:
|
|||||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
func commandUse(command *Command) string {
|
func commandUse(command *commandmeta.Command) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, argument := range command.Arguments {
|
for _, argument := range command.Arguments {
|
||||||
name := argument.Name
|
name := argument.Name
|
||||||
@@ -312,7 +262,7 @@ func commandUse(command *Command) string {
|
|||||||
return command.Name + " " + strings.Join(parts, " ")
|
return command.Name + " " + strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func helpDescription(command *Command) string {
|
func helpDescription(command *commandmeta.Command) string {
|
||||||
var sections []string
|
var sections []string
|
||||||
if description := strings.TrimSpace(command.Description); description != "" {
|
if description := strings.TrimSpace(command.Description); description != "" {
|
||||||
sections = append(sections, description)
|
sections = append(sections, description)
|
||||||
@@ -335,7 +285,7 @@ func helpDescription(command *Command) string {
|
|||||||
return strings.Join(sections, "\n\n")
|
return strings.Join(sections, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func flagUsage(flag Flag) string {
|
func flagUsage(flag commandmeta.Flag) string {
|
||||||
var details []string
|
var details []string
|
||||||
if flag.Required {
|
if flag.Required {
|
||||||
details = append(details, "required")
|
details = append(details, "required")
|
||||||
@@ -374,7 +324,7 @@ func splitRoutingPrefix(argv []string) ([]string, []string) {
|
|||||||
return append([]string(nil), argv...), nil
|
return append([]string(nil), argv...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*Command) ([]string, bool) {
|
func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*commandmeta.Command) ([]string, bool) {
|
||||||
var reverse []string
|
var reverse []string
|
||||||
for current := selected; current != nil; current = current.Parent() {
|
for current := selected; current != nil; current = current.Parent() {
|
||||||
declaration := declarations[current]
|
declaration := declarations[current]
|
||||||
@@ -391,8 +341,8 @@ func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*Com
|
|||||||
return reverse, true
|
return reverse, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func failure(result Result, message string) Result {
|
func failure(result commandmeta.Result, message string) commandmeta.Result {
|
||||||
result.Failure = &Failure{Message: message}
|
result.Failure = &commandmeta.Failure{Message: message}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
spfcobra "github.com/spf13/cobra"
|
spfcobra "github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Command = commandmeta.Command
|
||||||
|
type Argument = commandmeta.Argument
|
||||||
|
type Flag = commandmeta.Flag
|
||||||
|
type Failure = commandmeta.Failure
|
||||||
|
type Result = commandmeta.Result
|
||||||
|
type ParseOptions = commandmeta.ParseOptions
|
||||||
|
|
||||||
func TestParseRoutesBeforeOptionsAndKeepsFlagsLocal(t *testing.T) {
|
func TestParseRoutesBeforeOptionsAndKeepsFlagsLocal(t *testing.T) {
|
||||||
tree := &Command{Name: "app", Children: []*Command{
|
tree := &Command{Name: "app", Children: []*Command{
|
||||||
{Name: "config", Children: []*Command{
|
{Name: "config", Children: []*Command{
|
||||||
|
|||||||
66
internal/commandmeta/metadata.go
Normal file
66
internal/commandmeta/metadata.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Package commandmeta owns the neutral metadata exchanged by CommandKit and
|
||||||
|
// its command-line backend.
|
||||||
|
package commandmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key identifies one command inside one routing snapshot.
|
||||||
|
type Key uint64
|
||||||
|
|
||||||
|
// Command describes one node in a neutral command tree.
|
||||||
|
type Command struct {
|
||||||
|
Key Key
|
||||||
|
Name string
|
||||||
|
Summary string
|
||||||
|
Description string
|
||||||
|
Runnable bool
|
||||||
|
Flags []Flag
|
||||||
|
Arguments []Argument
|
||||||
|
Children []*Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argument describes one positional input for usage rendering.
|
||||||
|
type Argument struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
Min int
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag describes one local option without exposing a backend value.
|
||||||
|
type Flag struct {
|
||||||
|
Name string
|
||||||
|
Short string
|
||||||
|
Usage string
|
||||||
|
Default string
|
||||||
|
Boolean bool
|
||||||
|
Repeated bool
|
||||||
|
Required bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failure is a neutral parsing or presentation failure.
|
||||||
|
type Failure struct {
|
||||||
|
Message string
|
||||||
|
Usage bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result contains the selected command and raw input collected by the adapter.
|
||||||
|
type Result struct {
|
||||||
|
Key Key
|
||||||
|
Path []string
|
||||||
|
HelpRequested bool
|
||||||
|
FlagValues [][]string
|
||||||
|
Operands []string
|
||||||
|
Failure *Failure
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOptions makes parser context and streams explicit.
|
||||||
|
type ParseOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
In io.Reader
|
||||||
|
Out io.Writer
|
||||||
|
Err io.Writer
|
||||||
|
}
|
||||||
34
parser.go
34
parser.go
@@ -4,46 +4,52 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) parse(args []string, options internalcobra.ParseOptions) internalcobra.Result {
|
func (a *App) parse(args []string, options commandmeta.ParseOptions) commandmeta.Result {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return internalcobra.Result{Failure: &internalcobra.Failure{Message: "cannot parse commands on a nil app"}}
|
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
|
||||||
}
|
}
|
||||||
return internalcobra.Parse(a.bridgeTree(), args, options)
|
return internalcobra.Parse(a.routeSnapshot().metadata, args, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) bridgeTree() *internalcobra.Command {
|
func (a *App) routeSnapshot() *routingSnapshot {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if a.commandTree == nil {
|
if a.snapshot == nil {
|
||||||
a.commandTree = a.buildBridgeTree()
|
a.snapshot = a.buildRoutingSnapshot()
|
||||||
}
|
}
|
||||||
return a.commandTree
|
return a.snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) buildBridgeTree() *internalcobra.Command {
|
func (a *App) buildRoutingSnapshot() *routingSnapshot {
|
||||||
root := &internalcobra.Command{Name: a.identity.Name}
|
root := &commandmeta.Command{Name: a.identity.Name}
|
||||||
|
snapshot := &routingSnapshot{metadata: root, runners: make(map[commandmeta.Key]commandPayload)}
|
||||||
names := make([]string, 0, len(a.commands))
|
names := make([]string, 0, len(a.commands))
|
||||||
for name := range a.commands {
|
for name := range a.commands {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sort.Strings(names)
|
||||||
|
var key commandmeta.Key
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
root.Children = append(root.Children, bridgeCommand(a.commands[name].command))
|
root.Children = append(root.Children, bridgeCommand(a.commands[name].command, snapshot, &key))
|
||||||
}
|
}
|
||||||
return root
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func bridgeCommand(command Command) *internalcobra.Command {
|
func bridgeCommand(command Command, snapshot *routingSnapshot, next *commandmeta.Key) *commandmeta.Command {
|
||||||
record := command.record
|
record := command.record
|
||||||
declaration := &internalcobra.Command{
|
*next = *next + 1
|
||||||
|
declaration := &commandmeta.Command{
|
||||||
|
Key: *next,
|
||||||
Name: record.name,
|
Name: record.name,
|
||||||
Summary: record.summary,
|
Summary: record.summary,
|
||||||
Description: record.description,
|
Description: record.description,
|
||||||
}
|
}
|
||||||
if record.payload != nil {
|
if record.payload != nil {
|
||||||
|
snapshot.runners[declaration.Key] = record.payload
|
||||||
declaration.Runnable = record.payload.hasRun()
|
declaration.Runnable = record.payload.hasRun()
|
||||||
declaration.Flags = record.payload.bridgeFlags()
|
declaration.Flags = record.payload.bridgeFlags()
|
||||||
declaration.Arguments = record.payload.bridgeArguments()
|
declaration.Arguments = record.payload.bridgeArguments()
|
||||||
@@ -54,7 +60,7 @@ func bridgeCommand(command Command) *internalcobra.Command {
|
|||||||
if child.record == nil || child.record.absent {
|
if child.record == nil || child.record.absent {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
declaration.Children = append(declaration.Children, bridgeCommand(child))
|
declaration.Children = append(declaration.Children, bridgeCommand(child, snapshot, next))
|
||||||
}
|
}
|
||||||
return declaration
|
return declaration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
||||||
@@ -33,7 +34,7 @@ func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
options := internalcobra.ParseOptions{Context: context.Background()}
|
options := commandmeta.ParseOptions{Context: context.Background()}
|
||||||
result := app.parse([]string{"config", "show", "one", "--scope", "app"}, options)
|
result := app.parse([]string{"config", "show", "one", "--scope", "app"}, options)
|
||||||
if result.Failure != nil {
|
if result.Failure != nil {
|
||||||
t.Fatalf("parse failed: %s", result.Failure.Message)
|
t.Fatalf("parse failed: %s", result.Failure.Message)
|
||||||
@@ -47,6 +48,32 @@ func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRoutingSnapshotPairsParsedKeyWithItsRunner(t *testing.T) {
|
||||||
|
command := NewCommand(CommandSpec[struct{}]{Name: "run", Run: func(Invocation, struct{}) Outcome { return Success() }})
|
||||||
|
app, err := New(Identity{Name: "example"}, command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
snapshot := app.routeSnapshot()
|
||||||
|
session := internalcobra.Prepare(snapshot.metadata, commandmeta.ParseOptions{Context: context.Background()})
|
||||||
|
parsed := session.Parse([]string{"run"})
|
||||||
|
if parsed.Failure != nil {
|
||||||
|
t.Fatalf("parse failed: %s", parsed.Failure.Message)
|
||||||
|
}
|
||||||
|
if parsed.Key == 0 || snapshot.runners[parsed.Key] == nil || !snapshot.runners[parsed.Key].hasRun() {
|
||||||
|
t.Fatalf("parsed key %d has no matching runner in its snapshot", parsed.Key)
|
||||||
|
}
|
||||||
|
if app.routeSnapshot() != snapshot {
|
||||||
|
t.Fatal("unchanged registry rebuilt its routing snapshot")
|
||||||
|
}
|
||||||
|
if err := app.Register(NewCommand(CommandSpec[struct{}]{Name: "late"})); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if app.routeSnapshot() == snapshot {
|
||||||
|
t.Fatal("successful registration kept stale routing metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecutionRefreshesHelpMetadataAfterRegistration(t *testing.T) {
|
func TestExecutionRefreshesHelpMetadataAfterRegistration(t *testing.T) {
|
||||||
first := NewCommand(CommandSpec[struct{}]{
|
first := NewCommand(CommandSpec[struct{}]{
|
||||||
Name: "first",
|
Name: "first",
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ func (a *App) installBatch(commands ...Command) error {
|
|||||||
for name, node := range pending {
|
for name, node := range pending {
|
||||||
a.commands[name] = node
|
a.commands[name] = node
|
||||||
}
|
}
|
||||||
a.commandTree = nil
|
a.snapshot = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func TestRunnableCommandCapturesChildrenAndLeavesOriginalUnchanged(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("runnable parent with children rejected: %v", err)
|
t.Fatalf("runnable parent with children rejected: %v", err)
|
||||||
}
|
}
|
||||||
if _, ok := app.commandAt([]string{"parent", "child"}); !ok {
|
if len(app.commands["parent"].command.record.children) != 1 {
|
||||||
t.Fatal("child declaration was not installed")
|
t.Fatal("child declaration was not installed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ func TestGroupCapturesChildrenAndSkipsNestedAbsence(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, ok := app.commandAt([]string{"parent", "visible"}); !ok {
|
if len(app.commands["parent"].command.record.children) != 2 {
|
||||||
t.Fatal("captured child missing")
|
t.Fatal("captured child missing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,11 +160,11 @@ func TestNoOpOnlyRegistrationPreservesRoutingSnapshot(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
before := app.bridgeTree()
|
before := app.routeSnapshot()
|
||||||
if err := app.Register(NoCommand()); err != nil {
|
if err := app.Register(NoCommand()); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if after := app.bridgeTree(); after != before {
|
if after := app.routeSnapshot(); after != before {
|
||||||
t.Fatal("no-op-only registration invalidated derived metadata")
|
t.Fatal("no-op-only registration invalidated derived metadata")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,10 +179,10 @@ func TestCapturedDeclarationCanBeInstalledIntoMultipleApps(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, ok := first.commandAt([]string{"tools", "run"}); !ok {
|
if len(first.commands["tools"].command.record.children) != 1 {
|
||||||
t.Fatal("first app lost reused declaration")
|
t.Fatal("first app lost reused declaration")
|
||||||
}
|
}
|
||||||
if _, ok := second.commandAt([]string{"tools", "run"}); !ok {
|
if len(second.commands["tools"].command.record.children) != 1 {
|
||||||
t.Fatal("second app lost reused declaration")
|
t.Fatal("second app lost reused declaration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user