init
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// optBool is the minimal stand-in for clicmd's optional flags: a flag.Value
|
||||
// that announces it takes no argument.
|
||||
type optBool struct{ v bool }
|
||||
|
||||
func (o *optBool) String() string { return "" }
|
||||
func (o *optBool) Set(string) error { o.v = true; return nil }
|
||||
func (o *optBool) IsBoolFlag() bool { return true }
|
||||
|
||||
func TestPermute(t *testing.T) {
|
||||
newFS := func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
fs.String("name", "", "")
|
||||
fs.Bool("yes", false, "")
|
||||
fs.Var(&optBool{}, "spa", "")
|
||||
return fs
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
in []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
desc: "flags after the positional, which is what people type",
|
||||
in: []string{"demo", "--name", "Demo", "--spa"},
|
||||
want: []string{"--name", "Demo", "--spa", "--", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "already in flag package order, unchanged apart from the separator",
|
||||
in: []string{"--name", "Demo", "demo"},
|
||||
want: []string{"--name", "Demo", "--", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "a bool flag must not swallow the positional that follows it",
|
||||
in: []string{"--yes", "demo"},
|
||||
want: []string{"--yes", "--", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "single-dash spelling is the same flag",
|
||||
in: []string{"demo", "-name", "Demo"},
|
||||
want: []string{"-name", "Demo", "--", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "--flag=value carries its own argument",
|
||||
in: []string{"demo", "--name=Demo", "other"},
|
||||
want: []string{"--name=Demo", "--", "demo", "other"},
|
||||
},
|
||||
{
|
||||
desc: "a value that looks like a flag is still the flag's value",
|
||||
in: []string{"demo", "--name", "-weird"},
|
||||
want: []string{"--name", "-weird", "--", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "everything after -- stays positional",
|
||||
in: []string{"--yes", "--", "--name", "demo"},
|
||||
want: []string{"--yes", "--", "--name", "demo"},
|
||||
},
|
||||
{
|
||||
desc: "a lone dash is a positional, not a flag",
|
||||
in: []string{"-"},
|
||||
want: []string{"--", "-"},
|
||||
},
|
||||
{
|
||||
desc: "an unknown flag is passed through alone so flag.Parse reports it",
|
||||
in: []string{"--nope", "demo"},
|
||||
want: []string{"--nope", "--", "demo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := permute(newFS(), tc.in)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("permute(%q)\n got %q\nwant %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermutedFlagsReachExec is the property the permutation exists for: the
|
||||
// command sees the same flags and the same positionals either way round.
|
||||
func TestPermutedFlagsReachExec(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"create", "demo", "--name", "Demo", "--spa"},
|
||||
{"create", "--name", "Demo", "--spa", "demo"},
|
||||
{"create", "--name", "Demo", "demo", "--spa"},
|
||||
} {
|
||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
||||
var (
|
||||
name string
|
||||
spa optBool
|
||||
rest []string
|
||||
)
|
||||
root := &Command{
|
||||
Name: "test",
|
||||
Sub: []*Command{{
|
||||
Name: "create",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.StringVar(&name, "name", "", "")
|
||||
fs.Var(&spa, "spa", "")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
rest = args
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
}
|
||||
if err := Run(context.Background(), root, args, io.Discard, nil); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if name != "Demo" || !spa.v {
|
||||
t.Errorf("flags: name=%q spa=%v, want Demo/true", name, spa.v)
|
||||
}
|
||||
if !reflect.DeepEqual(rest, []string{"demo"}) {
|
||||
t.Errorf("positionals: %q, want [demo]", rest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParentDoesNotPermute guards the other half of the rule: a parent must
|
||||
// leave a child's flags alone, or "pages project create --spa" would try to
|
||||
// parse --spa against the project command and fail.
|
||||
func TestParentDoesNotPermute(t *testing.T) {
|
||||
var spa bool
|
||||
root := &Command{
|
||||
Name: "test",
|
||||
Sub: []*Command{{
|
||||
Name: "project",
|
||||
Sub: []*Command{{
|
||||
Name: "create",
|
||||
Flags: func(fs *flag.FlagSet) { fs.BoolVar(&spa, "spa", false, "") },
|
||||
Exec: func(context.Context, []string) error { return nil },
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if err := Run(context.Background(), root, []string{"project", "create", "--spa"}, io.Discard, nil); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if !spa {
|
||||
t.Error("--spa did not reach the leaf command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageErrors(t *testing.T) {
|
||||
root := &Command{
|
||||
Name: "test",
|
||||
Sub: []*Command{{
|
||||
Name: "leaf",
|
||||
Exec: func(context.Context, []string) error { return UsageErrorf("expected %d args", 1) },
|
||||
}},
|
||||
}
|
||||
|
||||
t.Run("unknown command", func(t *testing.T) {
|
||||
var out strings.Builder
|
||||
err := Run(context.Background(), root, []string{"nope"}, &out, nil)
|
||||
if !errors.Is(err, ErrUsage) {
|
||||
t.Fatalf("err = %v, want ErrUsage", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), `unknown command "nope"`) {
|
||||
t.Errorf("output did not name the unknown command:\n%s", out.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a parent with no child named is usage, not a crash", func(t *testing.T) {
|
||||
err := Run(context.Background(), root, nil, io.Discard, nil)
|
||||
if !errors.Is(err, ErrUsage) {
|
||||
t.Fatalf("err = %v, want ErrUsage", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UsageErrorf prints the command's usage and keeps its message", func(t *testing.T) {
|
||||
var out strings.Builder
|
||||
err := Run(context.Background(), root, []string{"leaf"}, &out, nil)
|
||||
if !errors.Is(err, ErrUsage) {
|
||||
t.Fatalf("err = %v, want ErrUsage", err)
|
||||
}
|
||||
if err.Error() != "expected 1 args" {
|
||||
t.Errorf("message = %q", err.Error())
|
||||
}
|
||||
if !strings.Contains(out.String(), "Usage:\n test leaf") {
|
||||
t.Errorf("usage text missing or misnamed:\n%s", out.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user