tailscale/cmd/tailscale/cli/ffcomplete/internal/complete_test.go

220 lines
5.6 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package internal_test
import (
_ "embed"
"flag"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/cmd/tailscale/cli/ffcomplete"
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal"
)
func newFlagSet(name string, errh flag.ErrorHandling, flags func(fs *flag.FlagSet)) *flag.FlagSet {
fs := flag.NewFlagSet(name, errh)
if flags != nil {
flags(fs)
}
return fs
}
func TestComplete(t *testing.T) {
t.Parallel()
// Build our test program in testdata.
root := &ffcli.Command{
Name: "prog",
FlagSet: newFlagSet("prog", flag.ContinueOnError, func(fs *flag.FlagSet) {
fs.Bool("v", false, "verbose")
fs.Bool("root-bool", false, "root `bool`")
fs.String("root-str", "", "some `text`")
}),
Subcommands: []*ffcli.Command{
{
Name: "debug",
ShortHelp: "Debug data",
FlagSet: newFlagSet("prog debug", flag.ExitOnError, func(fs *flag.FlagSet) {
fs.String("cpu-profile", "", "write cpu profile to `file`")
fs.Bool("debug-bool", false, "debug bool")
fs.Int("level", 0, "a number")
fs.String("enum", "", "a flag that takes several specific values")
ffcomplete.Flag(fs, "enum", ffcomplete.Fixed("alpha", "beta", "charlie"))
}),
},
func() *ffcli.Command {
cmd := &ffcli.Command{
Name: "ping",
FlagSet: newFlagSet("prog ping", flag.ContinueOnError, func(fs *flag.FlagSet) {
fs.String("until", "", "when pinging should end")
ffcomplete.Flag(fs, "until", ffcomplete.Fixed("forever", "direct"))
}),
}
ffcomplete.Args(cmd, ffcomplete.Fixed("jupiter", "neptune", "venus"))
return cmd
}(),
},
}
tests := []struct {
args []string
showFlags bool
showDescs bool
wantComp []string
wantDir ffcomplete.ShellCompDirective
}{
{
args: []string{"deb"},
wantComp: []string{"debug"},
},
{
args: []string{"deb"},
showDescs: true,
wantComp: []string{"debug\tDebug data"},
},
{
args: []string{"-"},
wantComp: []string{"--root-bool", "--root-str", "-v"},
},
{
args: []string{"--"},
wantComp: []string{"--root-bool", "--root-str", "--v"},
},
{
args: []string{"-r"},
wantComp: []string{"-root-bool", "-root-str"},
},
{
args: []string{"--r"},
wantComp: []string{"--root-bool", "--root-str"},
},
{
args: []string{"--root-str=s", "--r"},
wantComp: []string{"--root-bool"}, // omits --root-str which is already set
},
{
// '--' disables flag parsing, so we shouldn't suggest flags.
args: []string{"--", "--root"},
wantComp: nil,
},
{
// '--' is used as the value of '--root-str'.
args: []string{"--root-str", "--", "--r"},
wantComp: []string{"--root-bool"},
},
{
// '--' here is a flag value, so doesn't disable flag parsing.
args: []string{"--root-str", "--", "--root"},
wantComp: []string{"--root-bool"},
},
{
// Equivalent to '--root-str=-- -- --r' meaning '--r' is not
// a flag because it's preceded by a '--' argument:
// https://go.dev/play/p/UCtftQqVhOD.
args: []string{"--root-str", "--", "--", "--r"},
wantComp: nil,
},
{
args: []string{"--root-bool="},
wantComp: []string{"true", "false"},
},
{
args: []string{"--root-bool=t"},
wantComp: []string{"true"},
},
{
args: []string{"--root-bool=T"},
wantComp: []string{"TRUE"},
},
{
args: []string{"debug", "--de"},
wantComp: []string{"--debug-bool"},
},
{
args: []string{"debug", "--enum="},
wantComp: []string{"alpha", "beta", "charlie"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"debug", "--enum=al"},
wantComp: []string{"alpha"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"debug", "--level", ""},
wantComp: nil,
},
{
args: []string{"debug", "--enum", "b"},
wantComp: []string{"beta"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"debug", "--enum", "al"},
wantComp: []string{"alpha"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"ping", ""},
showFlags: true,
wantComp: []string{"--until", "jupiter", "neptune", "venus"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"ping", ""},
showFlags: true,
showDescs: true,
wantComp: []string{
"--until\twhen pinging should end",
"jupiter",
"neptune",
"venus",
},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"ping", ""},
wantComp: []string{"jupiter", "neptune", "venus"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
{
args: []string{"ping", "j"},
wantComp: []string{"jupiter"},
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
},
}
// Run the tests.
for _, test := range tests {
test := test
name := strings.Join(test.args, "␣")
if test.showFlags {
name += "+flags"
}
if test.showDescs {
name += "+descs"
}
t.Run(name, func(t *testing.T) {
// Capture the binary
complete, dir, err := internal.Complete(root, test.args, test.showFlags, test.showDescs)
if err != nil {
t.Fatalf("completion error: %s", err)
}
// Test the results match our expectation.
if test.wantComp != nil {
if diff := cmp.Diff(test.wantComp, complete); diff != "" {
t.Errorf("unexpected completion directives (-want +got):\n%s", diff)
}
}
if test.wantDir != dir {
t.Errorf("got shell completion directive %[1]d (%[1]s), want %[2]d (%[2]s)", dir, test.wantDir)
}
})
}
}