diff --git a/cmd/helm/flags_test.go b/cmd/helm/flags_test.go new file mode 100644 index 000000000..d5576fe9f --- /dev/null +++ b/cmd/helm/flags_test.go @@ -0,0 +1,88 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "testing" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" +) + +func outputFlagCompletionTest(t *testing.T, cmdName string) { + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "athos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "porthos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "aramis", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "completion for output flag long and before arg", + cmd: fmt.Sprintf("__complete %s --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag long and after arg", + cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag short and before arg", + cmd: fmt.Sprintf("__complete %s -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag short and after arg", + cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }} + runTestCmd(t, tests) +} diff --git a/cmd/helm/get_all_test.go b/cmd/helm/get_all_test.go index 0b026fca4..a213ac583 100644 --- a/cmd/helm/get_all_test.go +++ b/cmd/helm/get_all_test.go @@ -41,3 +41,7 @@ func TestGetCmd(t *testing.T) { }} runTestCmd(t, tests) } + +func TestGetAllRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get all") +} diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go index f843f7d59..7b9142d12 100644 --- a/cmd/helm/get_hooks_test.go +++ b/cmd/helm/get_hooks_test.go @@ -36,3 +36,7 @@ func TestGetHooks(t *testing.T) { }} runTestCmd(t, tests) } + +func TestGetHooksRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get hooks") +} diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go index be54d4a5b..bd0ffc5d6 100644 --- a/cmd/helm/get_manifest_test.go +++ b/cmd/helm/get_manifest_test.go @@ -36,3 +36,7 @@ func TestGetManifest(t *testing.T) { }} runTestCmd(t, tests) } + +func TestGetManifestRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get manifest") +} diff --git a/cmd/helm/get_notes_test.go b/cmd/helm/get_notes_test.go index a3ddea9a7..a59120b77 100644 --- a/cmd/helm/get_notes_test.go +++ b/cmd/helm/get_notes_test.go @@ -36,3 +36,7 @@ func TestGetNotesCmd(t *testing.T) { }} runTestCmd(t, tests) } + +func TestGetNotesRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get notes") +} diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index da3cc9e39..ecd92d354 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -52,3 +52,11 @@ func TestGetValuesCmd(t *testing.T) { }} runTestCmd(t, tests) } + +func TestGetValuesRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get values") +} + +func TestGetValuesOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "get values") +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index b7156daf3..5f9d80a3a 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "fmt" "io/ioutil" "os" "os/exec" @@ -96,11 +97,39 @@ func storageFixture() *storage.Storage { return storage.Init(driver.NewMemory()) } +// go-shellwords does not handle empty arguments properly +// https://github.com/mattn/go-shellwords/issues/5#issuecomment-573431458 +// +// This method checks if the last argument was an empty one, +// and if go-shellwords missed it, we add it ourselves. +// +// This is important for completion tests as completion often +// uses an empty last parameter. +func checkLastEmpty(in string, out []string) []string { + lastIndex := len(in) - 1 + + if lastIndex >= 1 && (in[lastIndex] == '"' && in[lastIndex-1] == '"' || + in[lastIndex] == '\'' && in[lastIndex-1] == '\'') { + // The last parameter of 'in' was empty ("" or ''), let's make sure it was detected. + if len(out) > 0 && out[len(out)-1] != "" { + // Bug from go-shellwords: + // 'out' does not have the empty parameter. We add it ourselves as a workaround. + out = append(out, "") + } else { + fmt.Println("WARNING: go-shellwords seems to have been fixed. This workaround can be removed.") + } + } + return out +} + func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err } + // Workaround the bug in shellwords + args = checkLastEmpty(cmd, args) + buf := new(bytes.Buffer) actionConfig := &action.Configuration{ diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index 3e750cefc..c9bfb648e 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "fmt" "testing" "helm.sh/helm/v3/pkg/release" @@ -68,3 +69,42 @@ func TestHistoryCmd(t *testing.T) { }} runTestCmd(t, tests) } + +func TestHistoryOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "history") +} + +func revisionFlagCompletionTest(t *testing.T, cmdName string) { + mk := func(name string, vers int, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, + }) + } + + releases := []*release.Release{ + mk("musketeers", 11, release.StatusDeployed), + mk("musketeers", 10, release.StatusSuperseded), + mk("musketeers", 9, release.StatusSuperseded), + mk("musketeers", 8, release.StatusSuperseded), + } + + tests := []cmdTestCase{{ + name: "completion for revision flag", + cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-comp.txt", + }, { + name: "completion for revision flag with too few args", + cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }, { + name: "completion for revision flag with too many args", + cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }} + runTestCmd(t, tests) +} diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index e8b573dfc..57972024f 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -193,3 +193,7 @@ func TestInstall(t *testing.T) { runTestActionCmd(t, tests) } + +func TestInstallOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "install") +} diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index b5833fd72..127a8a980 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -206,3 +206,7 @@ func TestListCmd(t *testing.T) { }} runTestCmd(t, tests) } + +func TestListOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "list") +} diff --git a/cmd/helm/repo_list_test.go b/cmd/helm/repo_list_test.go new file mode 100644 index 000000000..f371452f2 --- /dev/null +++ b/cmd/helm/repo_list_test.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestRepoListOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "repo list") +} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index bf98e72fe..c6c055993 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -232,7 +232,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newDocsCmd(out), // Setup the special hidden __complete command to allow for dynamic auto-completion - completion.NewCompleteCmd(settings), + completion.NewCompleteCmd(settings, out), ) // Add annotation to flags for which we can generate completion choices diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go index dfe0cacc2..7b0f3a389 100644 --- a/cmd/helm/search_hub_test.go +++ b/cmd/helm/search_hub_test.go @@ -49,5 +49,8 @@ func TestSearchHubCmd(t *testing.T) { t.Log(out) t.Log(expected) } +} +func TestSearchHubOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "search hub") } diff --git a/cmd/helm/search_repo_test.go b/cmd/helm/search_repo_test.go index 6ece55505..402ef2970 100644 --- a/cmd/helm/search_repo_test.go +++ b/cmd/helm/search_repo_test.go @@ -83,3 +83,7 @@ func TestSearchRepositoriesCmd(t *testing.T) { } runTestCmd(t, tests) } + +func TestSearchRepoOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "search repo") +} diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 91a008e5a..0d2500e65 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -108,3 +108,66 @@ func mustParseTime(t string) helmtime.Time { res, _ := helmtime.Parse(time.RFC3339, t) return res } + +func TestStatusCompletion(t *testing.T) { + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "athos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "porthos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "aramis", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "completion for status", + cmd: "__complete status a", + golden: "output/status-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for status with too many arguments", + cmd: "__complete status dartagnan ''", + golden: "output/status-wrong-args-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for status with too many arguments", + cmd: "__complete status --debug a", + golden: "output/status-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }} + runTestCmd(t, tests) +} + +func TestStatusRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "status") +} + +func TestStatusOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "status") +} diff --git a/cmd/helm/testdata/output/output-comp.txt b/cmd/helm/testdata/output/output-comp.txt new file mode 100644 index 000000000..de5f16f1d --- /dev/null +++ b/cmd/helm/testdata/output/output-comp.txt @@ -0,0 +1,4 @@ +table +json +yaml +:0 diff --git a/cmd/helm/testdata/output/revision-comp.txt b/cmd/helm/testdata/output/revision-comp.txt new file mode 100644 index 000000000..b4799f059 --- /dev/null +++ b/cmd/helm/testdata/output/revision-comp.txt @@ -0,0 +1,5 @@ +8 +9 +10 +11 +:0 diff --git a/cmd/helm/testdata/output/revision-wrong-args-comp.txt b/cmd/helm/testdata/output/revision-wrong-args-comp.txt new file mode 100644 index 000000000..b6f867176 --- /dev/null +++ b/cmd/helm/testdata/output/revision-wrong-args-comp.txt @@ -0,0 +1 @@ +:4 diff --git a/cmd/helm/testdata/output/status-comp.txt b/cmd/helm/testdata/output/status-comp.txt new file mode 100644 index 000000000..c97882964 --- /dev/null +++ b/cmd/helm/testdata/output/status-comp.txt @@ -0,0 +1,3 @@ +aramis +athos +:4 diff --git a/cmd/helm/testdata/output/status-wrong-args-comp.txt b/cmd/helm/testdata/output/status-wrong-args-comp.txt new file mode 100644 index 000000000..b6f867176 --- /dev/null +++ b/cmd/helm/testdata/output/status-wrong-args-comp.txt @@ -0,0 +1 @@ +:4 diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index bd1ccec35..3cecbe6d3 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -258,3 +258,7 @@ func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, return relMock, ch, chartPath } + +func TestUpgradeOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "upgrade") +} diff --git a/internal/completion/complete.go b/internal/completion/complete.go index d13c7f5bd..ccc868a59 100644 --- a/internal/completion/complete.go +++ b/internal/completion/complete.go @@ -18,6 +18,7 @@ package completion import ( "errors" "fmt" + "io" "log" "os" "strings" @@ -111,7 +112,7 @@ func (d BashCompDirective) string() string { } // NewCompleteCmd add a special hidden command that an be used to request completions -func NewCompleteCmd(settings *cli.EnvSettings) *cobra.Command { +func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { debug = settings.Debug return &cobra.Command{ Use: fmt.Sprintf("%s [command-line]", CompRequestCmd), @@ -169,14 +170,14 @@ func NewCompleteCmd(settings *cli.EnvSettings) *cobra.Command { completions, directive := completionFn(finalCmd, argsWoFlags, toComplete) for _, comp := range completions { // Print each possible completion to stdout for the completion script to consume. - fmt.Println(comp) + fmt.Fprintln(out, comp) } // As the last printout, print the completion directive for the // completion script to parse. // The directive integer must be that last character following a single : // The completion script expects :directive - fmt.Printf("\n:%d\n", directive) + fmt.Fprintln(out, fmt.Sprintf(":%d", directive)) // Print some helpful info to stderr for the user to understand. // Output from stderr should be ignored from the completion script.