Files
kong/kong_test.go
T
Alec Thomas 6408010083 Clean up disparity between Context and Kong.
Previously, there was a confusing mix of functionality shared between
the two wherein you would need to use the Kong type for printing errors,
etc. but it did not have access to the context in order to print
context-sensitive usage information. This has been fixed.

Additionally, there are now fuzzy correction suggestions for flags and
commands

Also added a server example which shows how Kong can be used for parsing
in interactive shells. Run with:

    $ go run ./_examples/server/*.go

Then interact with:

    $ ssh -p 6740 127.0.0.1
2018-06-27 09:11:11 +10:00

515 lines
11 KiB
Go

package kong_test
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong {
t.Helper()
options = append([]kong.Option{
kong.Name("test"),
kong.Exit(func(int) {
t.Helper()
t.Fatalf("unexpected exit()")
}),
}, options...)
parser, err := kong.New(cli, options...)
require.NoError(t, err)
return parser
}
func TestPositionalArguments(t *testing.T) {
var cli struct {
User struct {
Create struct {
ID int `kong:"arg"`
First string `kong:"arg"`
Last string `kong:"arg"`
} `kong:"cmd"`
} `kong:"cmd"`
}
p := mustNew(t, &cli)
ctx, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
require.NoError(t, err)
require.Equal(t, "user create <id> <first> <last>", ctx.Command())
t.Run("Missing", func(t *testing.T) {
_, err := p.Parse([]string{"user", "create", "10"})
require.Error(t, err)
})
}
func TestBranchingArgument(t *testing.T) {
/*
app user create <id> <first> <last>
app user <id> delete
app user <id> rename <to>
*/
var cli struct {
User struct {
Create struct {
ID string `kong:"arg"`
First string `kong:"arg"`
Last string `kong:"arg"`
} `kong:"cmd"`
// Branching argument.
ID struct {
ID int `kong:"arg"`
Flag int
Delete struct{} `kong:"cmd"`
Rename struct {
To string
} `kong:"cmd"`
} `kong:"arg"`
} `kong:"cmd,help='User management.'"`
}
p := mustNew(t, &cli)
ctx, err := p.Parse([]string{"user", "10", "delete"})
require.NoError(t, err)
require.Equal(t, 10, cli.User.ID.ID)
require.Equal(t, "user <id> delete", ctx.Command())
t.Run("Missing", func(t *testing.T) {
_, err = p.Parse([]string{"user"})
require.Error(t, err)
})
}
func TestResetWithDefaults(t *testing.T) {
var cli struct {
Flag string
FlagWithDefault string `kong:"default='default'"`
}
cli.Flag = "BLAH"
cli.FlagWithDefault = "BLAH"
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "", cli.Flag)
require.Equal(t, "default", cli.FlagWithDefault)
}
func TestFlagSlice(t *testing.T) {
var cli struct {
Slice []int
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{"--slice=1,2,3"})
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, cli.Slice)
}
func TestFlagSliceWithSeparator(t *testing.T) {
var cli struct {
Slice []string
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{`--slice=a\,b,c`})
require.NoError(t, err)
require.Equal(t, []string{"a,b", "c"}, cli.Slice)
}
func TestArgSlice(t *testing.T) {
// nolint: govet
var cli struct {
Slice []int `arg`
Flag bool
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{"1", "2", "3", "--flag"})
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, cli.Slice)
require.Equal(t, true, cli.Flag)
}
func TestArgSliceWithSeparator(t *testing.T) {
// nolint: govet
var cli struct {
Slice []string `arg`
Flag bool
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{"a,b", "c", "--flag"})
require.NoError(t, err)
require.Equal(t, []string{"a,b", "c"}, cli.Slice)
require.Equal(t, true, cli.Flag)
}
func TestUnsupportedFieldErrors(t *testing.T) {
var cli struct {
Keys struct{}
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestMatchingArgField(t *testing.T) {
var cli struct {
ID struct {
NotID int `kong:"arg"`
} `kong:"arg"`
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestCantMixPositionalAndBranches(t *testing.T) {
var cli struct {
Arg string `kong:"arg"`
Command struct {
} `kong:"cmd"`
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestPropagatedFlags(t *testing.T) {
var cli struct {
Flag1 string
Command1 struct {
Flag2 bool
Command2 struct{} `kong:"cmd"`
} `kong:"cmd"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{"command-1", "command-2", "--flag-2", "--flag-1=moo"})
require.NoError(t, err)
require.Equal(t, "moo", cli.Flag1)
require.Equal(t, true, cli.Command1.Flag2)
}
func TestRequiredFlag(t *testing.T) {
var cli struct {
Flag string `kong:"required"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.Error(t, err)
}
func TestOptionalArg(t *testing.T) {
var cli struct {
Arg string `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
}
func TestOptionalArgWithDefault(t *testing.T) {
var cli struct {
Arg string `kong:"arg,optional,default='moo'"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "moo", cli.Arg)
}
func TestArgWithDefaultIsOptional(t *testing.T) {
var cli struct {
Arg string `kong:"arg,default='moo'"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "moo", cli.Arg)
}
func TestRequiredArg(t *testing.T) {
var cli struct {
Arg string `kong:"arg"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.Error(t, err)
}
func TestInvalidRequiredAfterOptional(t *testing.T) {
var cli struct {
ID int `kong:"arg,optional"`
Name string `kong:"arg"`
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestOptionalStructArg(t *testing.T) {
var cli struct {
Name struct {
Name string `kong:"arg,optional"`
Enabled bool
} `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
t.Run("WithFlag", func(t *testing.T) {
_, err := parser.Parse([]string{"gak", "--enabled"})
require.NoError(t, err)
require.Equal(t, "gak", cli.Name.Name)
require.Equal(t, true, cli.Name.Enabled)
})
t.Run("WithoutFlag", func(t *testing.T) {
_, err := parser.Parse([]string{"gak"})
require.NoError(t, err)
require.Equal(t, "gak", cli.Name.Name)
})
t.Run("WithNothing", func(t *testing.T) {
_, err := parser.Parse([]string{})
require.NoError(t, err)
})
}
func TestMixedRequiredArgs(t *testing.T) {
var cli struct {
Name string `kong:"arg"`
ID int `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
t.Run("SingleRequired", func(t *testing.T) {
_, err := parser.Parse([]string{"gak", "5"})
require.NoError(t, err)
require.Equal(t, "gak", cli.Name)
require.Equal(t, 5, cli.ID)
})
t.Run("ExtraOptional", func(t *testing.T) {
_, err := parser.Parse([]string{"gak"})
require.NoError(t, err)
require.Equal(t, "gak", cli.Name)
})
}
func TestInvalidDefaultErrors(t *testing.T) {
var cli struct {
Flag int `kong:"default='foo'"`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
require.Error(t, err)
}
func TestCommandMissingTagIsInvalid(t *testing.T) {
var cli struct {
One struct{}
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestDuplicateFlag(t *testing.T) {
var cli struct {
Flag bool
Cmd struct {
Flag bool
} `kong:"cmd"`
}
_, err := kong.New(&cli)
require.Error(t, err)
}
func TestDuplicateFlagOnPeerCommandIsOkay(t *testing.T) {
var cli struct {
Cmd1 struct {
Flag bool
} `kong:"cmd"`
Cmd2 struct {
Flag bool
} `kong:"cmd"`
}
_, err := kong.New(&cli)
require.NoError(t, err)
}
func TestTraceErrorPartiallySucceeds(t *testing.T) {
var cli struct {
One struct {
Two struct {
} `kong:"cmd"`
} `kong:"cmd"`
}
p := mustNew(t, &cli)
ctx, err := kong.Trace(p, []string{"one", "bad"})
require.NoError(t, err)
require.Error(t, ctx.Error)
require.Equal(t, "one", ctx.Command())
}
func TestHooks(t *testing.T) {
var cli struct {
One struct {
Two string `kong:"arg,optional"`
Three string
} `kong:"cmd"`
}
type values struct {
one bool
two string
three string
}
hooked := values{}
var tests = []struct {
name string
input string
values values
}{
{"Command", "one", values{true, "", ""}},
{"Arg", "one two", values{true, "two", ""}},
{"Flag", "one --three=three", values{true, "", "three"}},
{"ArgAndFlag", "one two --three=three", values{true, "two", "three"}},
}
setOne := func(ctx *kong.Context, path *kong.Path) error { hooked.one = true; return nil }
setTwo := func(ctx *kong.Context, path *kong.Path) error { hooked.two = ctx.Value(path).String(); return nil }
setThree := func(ctx *kong.Context, path *kong.Path) error { hooked.three = ctx.Value(path).String(); return nil }
p := mustNew(t, &cli,
kong.Hook(&cli.One, setOne),
kong.Hook(&cli.One.Two, setTwo),
kong.Hook(&cli.One.Three, setThree))
for _, test := range tests {
hooked = values{}
t.Run(test.name, func(t *testing.T) {
_, err := p.Parse(strings.Split(test.input, " "))
require.NoError(t, err)
require.Equal(t, test.values, hooked)
})
}
}
func TestShort(t *testing.T) {
var cli struct {
Bool bool `short:"b"`
String string `short:"s"`
}
app := mustNew(t, &cli)
_, err := app.Parse([]string{"-b", "-shello"})
require.NoError(t, err)
require.True(t, cli.Bool)
require.Equal(t, "hello", cli.String)
}
func TestDuplicateFlagChoosesLast(t *testing.T) {
var cli struct {
Flag int
}
_, err := mustNew(t, &cli).Parse([]string{"--flag=1", "--flag=2"})
require.NoError(t, err)
require.Equal(t, 2, cli.Flag)
}
func TestDuplicateSliceAccumulates(t *testing.T) {
var cli struct {
Flag []int
}
args := []string{"--flag=1,2", "--flag=3,4"}
_, err := mustNew(t, &cli).Parse(args)
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3, 4}, cli.Flag)
}
func TestMapFlag(t *testing.T) {
var cli struct {
Set map[string]int
}
_, err := mustNew(t, &cli).Parse([]string{"--set", "a=10", "--set", "b=20"})
require.NoError(t, err)
require.Equal(t, map[string]int{"a": 10, "b": 20}, cli.Set)
}
func TestMapFlagWithSliceValue(t *testing.T) {
var cli struct {
Set map[string][]int
}
_, err := mustNew(t, &cli).Parse([]string{"--set", "a=1,2", "--set", "b=3"})
require.NoError(t, err)
require.Equal(t, map[string][]int{"a": {1, 2}, "b": {3}}, cli.Set)
}
type embeddedFlags struct {
Embedded string
}
func TestEmbeddedStruct(t *testing.T) {
var cli struct {
embeddedFlags
NotEmbedded string
}
_, err := mustNew(t, &cli).Parse([]string{"--embedded=moo", "--not-embedded=foo"})
require.NoError(t, err)
require.Equal(t, "moo", cli.Embedded)
require.Equal(t, "foo", cli.NotEmbedded)
}
func TestSliceWithDisabledSeparator(t *testing.T) {
var cli struct {
Flag []string `sep:"none"`
}
_, err := mustNew(t, &cli).Parse([]string{"--flag=a,b", "--flag=b,c"})
require.NoError(t, err)
require.Equal(t, []string{"a,b", "b,c"}, cli.Flag)
}
func TestMultilineMessage(t *testing.T) {
w := &bytes.Buffer{}
var cli struct{}
p := mustNew(t, &cli, kong.Writers(w, w))
p.Printf("hello\nworld")
require.Equal(t, "test: hello\n world\n", w.String())
}
type cmdWithRun struct {
Arg string `arg:""`
}
func (c *cmdWithRun) Run(key string) error {
c.Arg += key
if key == "ERROR" {
return fmt.Errorf("ERROR")
}
return nil
}
type grammarWithRun struct {
One cmdWithRun `cmd:""`
Two cmdWithRun `cmd:""`
}
func TestRun(t *testing.T) {
cli := &grammarWithRun{}
p := mustNew(t, cli)
ctx, err := p.Parse([]string{"one", "two"})
require.NoError(t, err)
err = ctx.Run("hello")
require.NoError(t, err)
require.Equal(t, "twohello", cli.One.Arg)
ctx, err = p.Parse([]string{"two", "three"})
require.NoError(t, err)
err = ctx.Run("ERROR")
require.Error(t, err)
}