Preserve ordered flag occurrences
This commit is contained in:
76
binding.go
76
binding.go
@@ -3,6 +3,7 @@ package commandkit
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
||||
@@ -38,6 +39,48 @@ func ValueFlag[I, V any](meta FlagMeta, defaultValue *V, parse func(string) (V,
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// OccurrenceFlag declares a repeatable flag whose setter runs once for each
|
||||
// supplied occurrence. Occurrence setters run in command-line order across
|
||||
// participating bindings; an omitted flag does not call its setter.
|
||||
func OccurrenceFlag[I, V any](meta FlagMeta, parse func(string) (V, error), set func(*I, Supplied[V])) Flag[I] {
|
||||
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, repeated: true}
|
||||
if parse == nil || set == nil {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||
result := preparedValue[I]{identity: binding.identity, supplied: len(values) > 0}
|
||||
for index, raw := range values {
|
||||
value, err := parse(raw)
|
||||
if err != nil {
|
||||
return preparedValue[I]{}, &flagOccurrenceError{index: index, cause: err}
|
||||
}
|
||||
ordinal := index
|
||||
result.ordered = append(result.ordered, preparedOccurrence[I]{ordinal: &ordinal, apply: func(input *I) { set(input, Supplied[V]{Value: value, Set: true}) }})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// RenameFlag returns an independent binding with a new long name and no short
|
||||
// spelling. Parsing, defaults, completion, requiredness, and setters are retained.
|
||||
func RenameFlag[I any](flag Flag[I], name string) Flag[I] {
|
||||
binding := flag.binding
|
||||
binding.meta.Name = name
|
||||
binding.meta.Short = ""
|
||||
binding.identity = newBindingIdentity()
|
||||
if prepare := binding.prepare; prepare != nil {
|
||||
identity := binding.identity
|
||||
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||
value, err := prepare(values)
|
||||
value.identity = identity
|
||||
return value, err
|
||||
}
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// BoolFlag declares a boolean flag. A bare flag is supplied by the parser as
|
||||
// "true"; explicit values are converted with strconv.ParseBool. Set distinguishes
|
||||
// an omitted flag from an explicit false value.
|
||||
@@ -189,7 +232,9 @@ func (p preparedInput[I]) build() I {
|
||||
input = p.newInput()
|
||||
}
|
||||
for _, value := range p.values {
|
||||
value.apply(&input)
|
||||
if value.apply != nil {
|
||||
value.apply(&input)
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
@@ -202,6 +247,10 @@ func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []
|
||||
}
|
||||
|
||||
func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
||||
return prepareInputWithOccurrences(spec, flagValues, flagSpellings, nil, operands, enforceRequired)
|
||||
}
|
||||
|
||||
func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, occurrences []commandmeta.FlagOccurrence, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
||||
if len(flagValues) != len(spec.Flags) {
|
||||
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags))
|
||||
}
|
||||
@@ -212,6 +261,12 @@ func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpell
|
||||
return preparedInput[I]{}, fmt.Errorf("received %d flag spelling sets for %d bindings", len(flagSpellings), len(spec.Flags))
|
||||
}
|
||||
preparedFlags := make([]preparedValue[I], len(spec.Flags))
|
||||
ordinals := make(map[string][]int)
|
||||
for index, occurrence := range occurrences {
|
||||
ordinals[occurrence.Name] = append(ordinals[occurrence.Name], index)
|
||||
}
|
||||
var ordered []preparedOccurrence[I]
|
||||
fallbackOrdinal := 0
|
||||
missingRequired := ""
|
||||
for i, flag := range spec.Flags {
|
||||
binding := flag.binding
|
||||
@@ -231,6 +286,21 @@ func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpell
|
||||
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
||||
}
|
||||
preparedFlags[i] = value
|
||||
for index, occurrence := range value.ordered {
|
||||
if occurrences != nil {
|
||||
positions := ordinals[binding.meta.Name]
|
||||
if index >= len(positions) {
|
||||
return preparedInput[I]{}, fmt.Errorf("missing occurrence position for flag --%s", binding.meta.Name)
|
||||
}
|
||||
*occurrence.ordinal = positions[index]
|
||||
} else {
|
||||
*occurrence.ordinal = fallbackOrdinal
|
||||
fallbackOrdinal++
|
||||
}
|
||||
if occurrence.apply != nil {
|
||||
ordered = append(ordered, occurrence)
|
||||
}
|
||||
}
|
||||
if enforceRequired && binding.meta.Required && !binding.deferredRequired && !value.supplied && missingRequired == "" {
|
||||
missingRequired = binding.meta.Name
|
||||
}
|
||||
@@ -243,6 +313,10 @@ func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpell
|
||||
return preparedInput[I]{}, err
|
||||
}
|
||||
preparedValues := append([]preparedValue[I](nil), preparedFlags...)
|
||||
sort.SliceStable(ordered, func(i, j int) bool { return *ordered[i].ordinal < *ordered[j].ordinal })
|
||||
for _, occurrence := range ordered {
|
||||
preparedValues = append(preparedValues, preparedValue[I]{apply: occurrence.apply})
|
||||
}
|
||||
for i, argument := range spec.Arguments {
|
||||
binding := argument.binding
|
||||
value, err := binding.prepare(argumentValues[i])
|
||||
|
||||
13
command.go
13
command.go
@@ -127,6 +127,13 @@ type preparedValue[I any] struct {
|
||||
identity *bindingIdentity
|
||||
supplied bool
|
||||
apply func(*I)
|
||||
ordered []preparedOccurrence[I]
|
||||
}
|
||||
|
||||
type preparedOccurrence[I any] struct {
|
||||
// Captured bindings share this position with the outer prepared occurrence.
|
||||
ordinal *int
|
||||
apply func(*I)
|
||||
}
|
||||
|
||||
type flagBinding[I any] struct {
|
||||
@@ -186,7 +193,7 @@ type commandPayload interface {
|
||||
hasRun() bool
|
||||
bridgeFlags() []commandmeta.Flag
|
||||
bridgeArguments() []commandmeta.Argument
|
||||
prepare([][]string, [][]string, []string, bool) (func(*execution) Outcome, error)
|
||||
prepare([][]string, [][]string, []commandmeta.FlagOccurrence, []string, bool) (func(*execution) Outcome, error)
|
||||
}
|
||||
|
||||
type typedCommand[I any] struct {
|
||||
@@ -235,8 +242,8 @@ func (c typedCommand[I]) bridgeArguments() []commandmeta.Argument {
|
||||
return arguments
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) prepare(flagValues, flagSpellings [][]string, operands []string, enforceRequired bool) (func(*execution) Outcome, error) {
|
||||
prepared, err := prepareInputWithSpellings(c.spec, flagValues, flagSpellings, operands, enforceRequired)
|
||||
func (c typedCommand[I]) prepare(flagValues, flagSpellings [][]string, occurrences []commandmeta.FlagOccurrence, operands []string, enforceRequired bool) (func(*execution) Outcome, error) {
|
||||
prepared, err := prepareInputWithOccurrences(c.spec, flagValues, flagSpellings, occurrences, operands, enforceRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||
|
||||
Status: implementation in progress; Stages 1–5 complete.
|
||||
Status: implementation in progress; Stages 1–6 complete.
|
||||
|
||||
## Scope, authority, and execution
|
||||
|
||||
@@ -213,6 +213,10 @@ conversion API. Each integration will enforce its flag's outer shape.
|
||||
|
||||
## Stage 6: Ordered typed occurrences and reusable binding identity
|
||||
|
||||
**Complete.** Ordered typed occurrences and independently renamed bindings are
|
||||
implemented, including captured subset replay. Focused binding tests and the
|
||||
root workspace suite (`GOTOOLCHAIN=local go test ./...` with Go 1.27.0) passed.
|
||||
|
||||
Extend [binding.go](../../binding.go), [flag_values.go](../../flag_values.go),
|
||||
and the neutral preparation path to retain ordinal positions from existing
|
||||
`FlagOccurrences`. Add:
|
||||
|
||||
@@ -59,13 +59,13 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco
|
||||
if !ok || payload == nil {
|
||||
outcome = current.renderHelp(parsed.Path)
|
||||
} else if parsed.HelpRequested || !payload.hasRun() {
|
||||
if _, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, false); err != nil {
|
||||
if _, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.FlagOccurrences, parsed.Operands, false); err != nil {
|
||||
outcome = classifyInputError(err)
|
||||
} else {
|
||||
outcome = current.renderHelp(parsed.Path)
|
||||
}
|
||||
} else {
|
||||
run, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, true)
|
||||
run, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.FlagOccurrences, parsed.Operands, true)
|
||||
if err != nil {
|
||||
outcome = classifyInputError(err)
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ package commandkit
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// ErrFlagInput identifies a missing required captured flag selected for an input.
|
||||
@@ -13,7 +14,8 @@ var ErrFlagWiring = errors.New("flag wiring is invalid")
|
||||
|
||||
// FlagValues stores invocation-local converted values and presence for captured
|
||||
// flags. It does not retain an input instance; values can be applied to
|
||||
// independently initialized destinations.
|
||||
// independently initialized destinations. Captured occurrence flags retain
|
||||
// their relative command-line order across the selected bindings.
|
||||
type FlagValues[I any] struct {
|
||||
values map[*bindingIdentity]preparedValue[I]
|
||||
}
|
||||
@@ -42,7 +44,7 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
|
||||
if err != nil {
|
||||
return preparedValue[Outer]{}, err
|
||||
}
|
||||
return preparedValue[Outer]{
|
||||
result := preparedValue[Outer]{
|
||||
identity: identity,
|
||||
supplied: prepared.supplied,
|
||||
apply: func(outer *Outer) {
|
||||
@@ -55,7 +57,11 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
|
||||
}
|
||||
store.values[inner.binding.identity] = prepared
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
for _, occurrence := range prepared.ordered {
|
||||
result.ordered = append(result.ordered, preparedOccurrence[Outer]{ordinal: occurrence.ordinal})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
captured[index] = Flag[Outer]{binding: binding}
|
||||
@@ -66,11 +72,14 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
|
||||
// ApplyTo applies selected original captured flags to an initialized
|
||||
// destination. A missing captured binding or nil destination matches
|
||||
// ErrFlagWiring. A required selected value that was not supplied matches
|
||||
// ErrFlagInput. Repeated core-owned values are copied for each application.
|
||||
// ErrFlagInput. Ordinary selected values are applied first, followed by selected
|
||||
// occurrence values in command-line order. Repeated core-owned values are copied
|
||||
// for each application.
|
||||
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
||||
if target == nil {
|
||||
return fmt.Errorf("%w: destination is nil", ErrFlagWiring)
|
||||
}
|
||||
preparedValues := make([]preparedValue[I], 0, len(selected))
|
||||
for _, flag := range selected {
|
||||
binding := flag.binding
|
||||
if binding.identity == nil {
|
||||
@@ -83,7 +92,22 @@ func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
||||
if binding.meta.Required && !prepared.supplied {
|
||||
return fmt.Errorf("flag --%s: %w", binding.meta.Name, ErrFlagInput)
|
||||
}
|
||||
prepared.apply(target)
|
||||
preparedValues = append(preparedValues, prepared)
|
||||
}
|
||||
var ordered []preparedOccurrence[I]
|
||||
for _, prepared := range preparedValues {
|
||||
if prepared.apply != nil {
|
||||
prepared.apply(target)
|
||||
}
|
||||
for _, occurrence := range prepared.ordered {
|
||||
if occurrence.apply != nil {
|
||||
ordered = append(ordered, occurrence)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ordered, func(i, j int) bool { return *ordered[i].ordinal < *ordered[j].ordinal })
|
||||
for _, occurrence := range ordered {
|
||||
occurrence.apply(target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
152
occurrence_test.go
Normal file
152
occurrence_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type occurrenceInput struct{ events []string }
|
||||
|
||||
func occurrenceFor(name string, required bool, calls *int) Flag[occurrenceInput] {
|
||||
return OccurrenceFlag(FlagMeta{Name: name, Required: required, Short: "x"}, func(raw string) (string, error) {
|
||||
*calls++
|
||||
if raw == "bad" {
|
||||
return "", errors.New("invalid occurrence")
|
||||
}
|
||||
return raw, nil
|
||||
}, func(input *occurrenceInput, value Supplied[string]) {
|
||||
input.events = append(input.events, name+"="+value.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOccurrenceFlagsApplyInEnteredOrder(t *testing.T) {
|
||||
calls, factories := 0, 0
|
||||
first := occurrenceFor("first", false, &calls)
|
||||
second := RenameFlag(occurrenceFor("second", false, &calls), "second")
|
||||
ordinary := ValueFlag(FlagMeta{Name: "ordinary"}, strptr("default"), func(s string) (string, error) { return s, nil }, func(i *occurrenceInput, v Supplied[string]) { i.events = append(i.events, "ordinary="+v.Value) })
|
||||
arg := ValueArgument(ArgumentMeta{Name: "ARG"}, false, func(s string) (string, error) { return s, nil }, func(i *occurrenceInput, v Supplied[string]) { i.events = append(i.events, "operand="+v.Value) })
|
||||
var got []string
|
||||
cmd := NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{first, second, ordinary}, Arguments: []Argument[occurrenceInput]{arg}, NewInput: func() occurrenceInput { factories++; return occurrenceInput{} }, CheckInput: func(i occurrenceInput) error { got = append([]string(nil), i.events...); return nil }, Run: func(Invocation, occurrenceInput) Outcome { return Success() }})
|
||||
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--other", "--second"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := app.Execute(t.Context(), []string{"run", "--other=a", "--first=", "--second=b", "--ordinary=z", "tail"}, Streams{})
|
||||
want := []string{"ordinary=z", "second=a", "first=", "second=b", "operand=tail"}
|
||||
if result.Status != 0 || !reflect.DeepEqual(got, want) || calls != 3 || factories != 1 {
|
||||
t.Fatalf("%#v events=%q calls=%d factories=%d", result, got, calls, factories)
|
||||
}
|
||||
result = app.Execute(t.Context(), []string{"run", "--first=bad"}, Streams{})
|
||||
if result.Kind != OutcomeUsage || factories != 1 || !strings.Contains(result.Explanation.Message, "--first") {
|
||||
t.Fatalf("invalid result %#v factories=%d", result, factories)
|
||||
}
|
||||
result = app.Execute(t.Context(), []string{"run"}, Streams{})
|
||||
if result.Status != 0 || !reflect.DeepEqual(got, []string{"ordinary=default", "operand="}) {
|
||||
t.Fatalf("fresh %#v, %q", result, got)
|
||||
}
|
||||
}
|
||||
|
||||
type occurrenceOuter struct{ flags FlagValues[occurrenceInput] }
|
||||
|
||||
func TestCapturedOccurrencesReplaySelectedOrder(t *testing.T) {
|
||||
calls := 0
|
||||
a := occurrenceFor("alpha", false, &calls)
|
||||
b := RenameFlag(a, "beta")
|
||||
missing := RenameFlag(occurrenceFor("required", true, &calls), "required")
|
||||
originals := []Flag[occurrenceInput]{a, b, missing}
|
||||
captured := CaptureFlags(originals, func(o *occurrenceOuter) *FlagValues[occurrenceInput] { return &o.flags })
|
||||
var stored FlagValues[occurrenceInput]
|
||||
cmd := NewCommand(CommandSpec[occurrenceOuter]{Name: "run", Flags: captured, Run: func(_ Invocation, i occurrenceOuter) Outcome { stored = i.flags; return Success() }})
|
||||
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--b", "--beta"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := app.Execute(t.Context(), []string{"run", "--b=1", "--alpha=2", "--beta=3", "--alpha=4"}, Streams{})
|
||||
if result.Status != 0 || calls != 4 {
|
||||
t.Fatalf("%#v calls=%d", result, calls)
|
||||
}
|
||||
var got occurrenceInput
|
||||
if err := stored.ApplyTo(&got, a, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"alpha=1", "alpha=2", "alpha=3", "alpha=4"}
|
||||
if !reflect.DeepEqual(got.events, want) {
|
||||
t.Fatalf("events %q want %q", got.events, want)
|
||||
}
|
||||
var subset occurrenceInput
|
||||
if err := stored.ApplyTo(&subset, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(subset.events, []string{"alpha=1", "alpha=3"}) || calls != 4 {
|
||||
t.Fatalf("subset %q calls=%d", subset.events, calls)
|
||||
}
|
||||
var repeated occurrenceInput
|
||||
if err := stored.ApplyTo(&repeated, a, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got.events[0] = "changed"
|
||||
if !reflect.DeepEqual(repeated.events, want) {
|
||||
t.Fatalf("shared state %q", repeated.events)
|
||||
}
|
||||
if err := stored.ApplyTo(&repeated, missing); !errors.Is(err, ErrFlagInput) {
|
||||
t.Fatalf("required error %v", err)
|
||||
}
|
||||
other := RenameFlag(a, "other")
|
||||
if err := stored.ApplyTo(&repeated, other); !errors.Is(err, ErrFlagWiring) {
|
||||
t.Fatalf("identity error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameFlagKeepsBindingBehavior(t *testing.T) {
|
||||
parse := func(s string) (string, error) { return strings.ToUpper(s), nil }
|
||||
original := ValueFlag(FlagMeta{Name: "original", Short: "o", Required: true, Completion: CompleteChoices(CompletionChoice{Value: "one"})}, strptr("default"), parse, func(i *occurrenceInput, v Supplied[string]) {
|
||||
i.events = append(i.events, fmt.Sprint(v.Value, ":", v.Set))
|
||||
})
|
||||
renamed := RenameFlag(original, "renamed")
|
||||
if renamed.binding.identity == original.binding.identity || renamed.binding.meta.Short != "" || !renamed.binding.meta.Required || !reflect.DeepEqual(renamed.binding.meta.Completion, original.binding.meta.Completion) {
|
||||
t.Fatalf("renamed %#v", renamed.binding.meta)
|
||||
}
|
||||
value, err := renamed.binding.prepare(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var input occurrenceInput
|
||||
value.apply(&input)
|
||||
if !reflect.DeepEqual(input.events, []string{"default:false"}) {
|
||||
t.Fatalf("default %q", input.events)
|
||||
}
|
||||
if _, err := New(Identity{Name: "Example"}, NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{original, RenameFlag(original, "bad name")}})); err == nil {
|
||||
t.Fatal("invalid name accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccurrenceRequiredAndAliasedConversion(t *testing.T) {
|
||||
parseErr := errors.New("cannot parse")
|
||||
builds := 0
|
||||
flag := OccurrenceFlag(FlagMeta{Name: "item", Required: true}, func(raw string) (string, error) {
|
||||
if raw == "bad" {
|
||||
return "", parseErr
|
||||
}
|
||||
return raw, nil
|
||||
}, func(*occurrenceInput, Supplied[string]) {})
|
||||
cmd := NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{flag}, NewInput: func() occurrenceInput { builds++; return occurrenceInput{} }, Run: func(Invocation, occurrenceInput) Outcome { return Success() }})
|
||||
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--entry", "--item"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
missing := app.Execute(t.Context(), []string{"run"}, Streams{})
|
||||
if missing.Kind != OutcomeUsage || builds != 0 {
|
||||
t.Fatalf("missing: %#v", missing)
|
||||
}
|
||||
invalid := app.Execute(t.Context(), []string{"run", "--item=ok", "--entry=bad"}, Streams{})
|
||||
if invalid.Kind != OutcomeUsage || !errors.Is(invalid.Cause, parseErr) || !strings.Contains(invalid.Explanation.Message, "--entry occurrence 2") || builds != 0 {
|
||||
t.Fatalf("invalid: %#v", invalid)
|
||||
}
|
||||
help := app.Execute(t.Context(), []string{"run", "--help"}, Streams{})
|
||||
if help.Status != 0 || builds != 0 {
|
||||
t.Fatalf("help: %#v", help)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user