sq/cli/complete_test.go
2024-01-31 17:39:04 -07:00

547 lines
15 KiB
Go

package cli_test
import (
"context"
"strconv"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/neilotoole/sq/cli"
"github.com/neilotoole/sq/cli/cobraz"
"github.com/neilotoole/sq/cli/flag"
"github.com/neilotoole/sq/cli/testrun"
"github.com/neilotoole/sq/libsq/core/lg"
"github.com/neilotoole/sq/libsq/core/lg/lgt"
"github.com/neilotoole/sq/libsq/core/options"
"github.com/neilotoole/sq/libsq/source"
"github.com/neilotoole/sq/testh"
"github.com/neilotoole/sq/testh/sakila"
"github.com/neilotoole/sq/testh/tu"
)
// testComplete is a helper for testing cobra completion.
func testComplete(tb testing.TB, from *testrun.TestRun, args ...string) completion {
tb.Helper()
var ctx context.Context
if from == nil {
ctx = lg.NewContext(context.Background(), lgt.New(tb))
} else {
ctx = from.Context
}
ctx = enableCompletionLog(ctx)
tr := testrun.New(ctx, tb, from)
args = append([]string{cobra.ShellCompRequestCmd}, args...)
err := tr.Exec(args...)
require.NoError(tb, err)
c := parseCompletion(tr)
return c
}
// parseCompletion parses the output of cobra "__complete".
// Example output:
//
// @active
// @sakila
// :4
// Completion ended with directive: ShellCompDirectiveNoFileComp
//
// The tr.T test will fail on any error.
func parseCompletion(tr *testrun.TestRun) completion {
c := completion{
stdout: tr.Out.String(),
stderr: tr.ErrOut.String(),
}
lines := strings.Split(strings.TrimSpace(c.stdout), "\n")
require.True(tr.T, len(lines) >= 1)
c.values = lines[:len(lines)-1]
result, err := strconv.Atoi(lines[len(lines)-1][1:])
require.NoError(tr.T, err)
c.result = cobra.ShellCompDirective(result)
c.directives = cobraz.ParseDirectivesLine(c.stderr)
return c
}
// completion models the result returned from the cobra "__complete" command.
type completion struct {
stdout string
stderr string
values []string
result cobra.ShellCompDirective
directives []cobra.ShellCompDirective
}
// TestCompleteFlagActiveSchema_query_cmds tests flag.ActiveSchema
// behavior for the query commands (slq, sql).
//
// See also: TestCompleteFlagActiveSchema_inspect.
func TestCompleteFlagActiveSchema_query_cmds(t *testing.T) { //nolint:tparallel
const wantDirective = cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder
testCases := []struct {
handles []string
arg string
withFlagActiveSrc string
wantContains []string
wantDirective cobra.ShellCompDirective
}{
{
handles: []string{sakila.Pg},
arg: "saki",
wantContains: []string{"sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "",
wantContains: []string{"public", "sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "sakila",
wantContains: []string{"sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "sakila.pub",
wantContains: []string{"sakila.public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg},
arg: "pub",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg},
arg: "public",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.My, sakila.Pg},
withFlagActiveSrc: sakila.Pg,
arg: "publ",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg, sakila.My},
withFlagActiveSrc: sakila.My,
arg: "",
wantContains: []string{"mysql", "sys", "information_schema", "sakila"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.My, sakila.Pg},
withFlagActiveSrc: sakila.MS,
arg: "publ",
// Should error because sakila.MS isn't a loaded source (via "handles").
wantDirective: cobra.ShellCompDirectiveError,
},
}
for _, cmdName := range []string{"slq", "sql"} {
cmdName := cmdName
t.Run(cmdName, func(t *testing.T) {
t.Parallel()
for i, tc := range testCases {
tc := tc
t.Run(tu.Name(i, tc.handles, tc.withFlagActiveSrc, tc.arg), func(t *testing.T) {
t.Parallel()
th := testh.New(t)
tr := testrun.New(th.Context, t, nil)
for _, handle := range tc.handles {
tr.Add(*th.Source(handle))
}
args := []string{cmdName}
if tc.withFlagActiveSrc != "" {
args = append(args, "--"+flag.ActiveSrc, tc.withFlagActiveSrc)
}
args = append(args, "--"+flag.ActiveSchema, tc.arg)
got := testComplete(t, tr, args...)
assert.Equal(t, tc.wantDirective, got.result,
"wanted: %v\ngot : %v",
cobraz.MarshalDirective(tc.wantDirective),
cobraz.MarshalDirective(got.result))
if tc.wantDirective == cobra.ShellCompDirectiveError {
require.Empty(t, got.values)
} else {
for j := range tc.wantContains {
assert.Contains(t, got.values, tc.wantContains[j])
}
}
})
}
})
}
}
// TestCompleteFlagActiveSchema_inspect tests flag.ActiveSchema
// behavior for the inspect command.
//
// See also: TestCompleteFlagActiveSchema_query_cmds.
func TestCompleteFlagActiveSchema_inspect(t *testing.T) {
tu.SkipIssueWindows(t, tu.GH372ShellCompletionWin)
t.Parallel()
const wantDirective = cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder
testCases := []struct {
handles []string
arg string
withArgActiveSrc string
wantContains []string
wantDirective cobra.ShellCompDirective
}{
{
handles: []string{sakila.Pg},
arg: "saki",
wantContains: []string{"sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "",
wantContains: []string{"public", "sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "sakila",
wantContains: []string{"sakila."},
wantDirective: wantDirective | cobra.ShellCompDirectiveNoSpace,
},
{
handles: []string{sakila.Pg},
arg: "sakila.pub",
wantContains: []string{"sakila.public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg},
arg: "pub",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg},
arg: "public",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.My, sakila.Pg},
withArgActiveSrc: sakila.Pg,
arg: "publ",
wantContains: []string{"public"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.Pg, sakila.My},
withArgActiveSrc: sakila.My,
arg: "",
wantContains: []string{"mysql", "sys", "information_schema", "sakila"},
wantDirective: wantDirective,
},
{
handles: []string{sakila.My, sakila.Pg},
withArgActiveSrc: sakila.MS,
arg: "publ",
// Should error because sakila.MS isn't a loaded source (via "handles").
wantDirective: cobra.ShellCompDirectiveError,
},
}
for i, tc := range testCases {
tc := tc
t.Run(tu.Name(i, tc.handles, tc.withArgActiveSrc, tc.arg), func(t *testing.T) {
t.Parallel()
th := testh.New(t)
tr := testrun.New(th.Context, t, nil)
for _, handle := range tc.handles {
tr.Add(*th.Source(handle))
}
args := []string{"inspect"}
if tc.withArgActiveSrc != "" {
args = append(args, tc.withArgActiveSrc)
}
args = append(args, "--"+flag.ActiveSchema, tc.arg)
got := testComplete(t, tr, args...)
assert.Equal(t, tc.wantDirective, got.result,
"wanted: %v\ngot : %v",
cobraz.MarshalDirective(tc.wantDirective),
cobraz.MarshalDirective(got.result))
if tc.wantDirective == cobra.ShellCompDirectiveError {
require.Empty(t, got.values)
} else {
for j := range tc.wantContains {
assert.Contains(t, got.values, tc.wantContains[j])
}
}
})
}
}
// TestCompleteFilterActiveGroup tests completion behavior
// wrt [cli.OptShellCompletionGroupFilter].
func TestCompleteFilterActiveGroup(t *testing.T) {
tu.SkipIssueWindows(t, tu.GH372ShellCompletionWin)
t.Parallel()
const (
prodInv = "@prod/inventory"
prodSales = "@prod/sales"
devInv = "@dev/inventory"
devSales = "@dev/sales"
)
allSrcs := []string{prodInv, prodSales, devInv, devSales}
prodSrcs := []string{prodInv, prodSales}
devSrcs := []string{devInv, devSales}
testCases := []struct {
name string
srcs []string
activeSrc string
activeGroup string
args []string
wantEquals []string
wantDirective cobra.ShellCompDirective
}{
{
name: "src_prod_@",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"src", "@"},
wantEquals: prodSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "src_prod_@dev",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"src", "@d"},
wantEquals: devSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "src_prod_@dev_slash",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"src", "@dev/"},
wantEquals: devSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "src_dev_@",
srcs: allSrcs,
activeSrc: "",
activeGroup: "dev",
args: []string{"src", "@"},
wantEquals: devSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "inspect_prod_@",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"inspect", "@"},
wantEquals: prodSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
{
name: "inspect_prod_@prod/sal",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"inspect", "@prod/sal"},
wantEquals: []string{prodSales, prodSales + ".data"},
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "inspect_prod_no_active.dat",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"inspect", ".dat"},
wantEquals: []string{},
wantDirective: cobra.ShellCompDirectiveError,
},
{
name: "inspect_prod_active.dat",
srcs: allSrcs,
activeSrc: devInv,
activeGroup: "prod",
args: []string{"inspect", ".dat"},
wantEquals: []string{".data"},
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "inspect_prod_root_@",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"@"},
wantEquals: prodSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
{
name: "inspect_prod_root_@",
srcs: allSrcs,
activeSrc: "",
activeGroup: "prod",
args: []string{"@d"},
wantEquals: devSrcs,
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tr := testrun.New(context.Background(), t, nil).Hush()
for _, handle := range tc.srcs {
src := testh.NewActorSource(t, handle, true)
tr.Add(*src)
}
_, err := tr.Run.Config.Collection.SetActive(tc.activeSrc, false)
require.NoError(t, err)
require.NoError(t, tr.Run.Config.Collection.SetActiveGroup(tc.activeGroup))
require.NoError(t, tr.Run.ConfigStore.Save(tr.Context, tr.Run.Config))
got := testComplete(t, tr, tc.args...)
assert.Equal(t, tc.wantDirective, got.result,
"wanted: %v\ngot : %v",
cobraz.MarshalDirective(tc.wantDirective),
cobraz.MarshalDirective(got.result))
if tc.wantDirective == cobra.ShellCompDirectiveError {
require.Empty(t, got.values)
} else {
require.Equal(t, tc.wantEquals, got.values)
}
})
}
}
// TestCompleteAllCobraRequestCmds verifies that completion
// works with both cobra.ShellCompRequestCmd and
// cobra.ShellCompNoDescRequestCmd.
func TestCompleteAllCobraRequestCmds(t *testing.T) {
tu.SkipIssueWindows(t, tu.GH372ShellCompletionWin)
t.Parallel()
testCases := []struct {
name string
args []string
wantContains []string
wantDirective cobra.ShellCompDirective
}{
{
name: "slq_empty",
args: []string{"@"},
wantContains: []string{source.ActiveHandle, sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
{
name: "slq_@",
args: []string{"@"},
wantContains: []string{source.ActiveHandle, sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
{
name: "src_empty",
args: []string{"src", ""},
wantContains: []string{sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "src_@",
args: []string{"src", "@"},
wantContains: []string{sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
name: "inspect_empty",
args: []string{"inspect", ""},
wantContains: []string{sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
{
name: "inspect_@",
args: []string{"inspect", "@"},
wantContains: []string{sakila.SL3},
wantDirective: cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace,
},
}
for _, cobraCmd := range []string{cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd} {
cobraCmd := cobraCmd
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run(cobraCmd, func(t *testing.T) {
t.Parallel()
th := testh.New(t)
th.Context = options.NewContext(th.Context, options.Options{cli.OptShellCompletionLog.Key(): true})
tr := testrun.New(th.Context, t, nil)
tr.Add(*th.Source(sakila.SL3))
args := append([]string{cobraCmd}, tc.args...)
err := tr.Exec(args...)
require.NoError(t, err)
got := parseCompletion(tr)
assert.Equal(t, cobraz.MarshalDirective(tc.wantDirective), cobraz.MarshalDirective(got.result))
for j := range tc.wantContains {
assert.Contains(t, got.values, tc.wantContains[j])
}
})
})
}
}
}
func enableCompletionLog(ctx context.Context) context.Context {
o := options.FromContext(ctx)
if o == nil {
return options.NewContext(ctx, options.Options{cli.OptShellCompletionLog.Key(): true})
}
o[cli.OptShellCompletionLog.Key()] = true
return ctx
}