You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/cmd/plugin_test.go

428 lines
12 KiB

/*
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 cmd
import (
"bytes"
"fmt"
"os"
"runtime"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestManuallyProcessArgs(t *testing.T) {
input := []string{
"--debug",
"--foo", "bar",
"--kubeconfig=/home/foo",
"--kubeconfig", "/home/foo",
"--kube-context=test1",
"--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2",
"-n", "test2",
"--namespace=test2",
"--namespace", "test2",
"--home=/tmp",
"command",
}
expectKnown := []string{
"--debug",
"--kubeconfig=/home/foo",
"--kubeconfig", "/home/foo",
"--kube-context=test1",
"--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2",
"-n", "test2",
"--namespace=test2",
"--namespace", "test2",
}
expectUnknown := []string{
"--foo", "bar", "--home=/tmp", "command",
}
known, unknown := manuallyProcessArgs(input)
for i, k := range known {
if k != expectKnown[i] {
t.Errorf("expected known flag %d to be %q, got %q", i, expectKnown[i], k)
}
}
for i, k := range unknown {
if k != expectUnknown[i] {
t.Errorf("expected unknown flag %d to be %q, got %q", i, expectUnknown[i], k)
}
}
}
func TestLoadCLIPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repositories.yaml"
settings.RepositoryCache = "testdata/helmhome/helm/repository"
var (
out bytes.Buffer
cmd cobra.Command
)
loadCLIPlugins(&cmd, &out)
fullEnvOutput := strings.Join([]string{
"HELM_PLUGIN_NAME=fullenv",
"HELM_PLUGIN_DIR=testdata/helmhome/helm/plugins/fullenv",
"HELM_PLUGINS=testdata/helmhome/helm/plugins",
"HELM_REPOSITORY_CONFIG=testdata/helmhome/helm/repositories.yaml",
"HELM_REPOSITORY_CACHE=testdata/helmhome/helm/repository",
fmt.Sprintf("HELM_BIN=%s", os.Args[0]),
}, "\n") + "\n"
// Test that the YAML file was correctly converted to a command.
tests := []struct {
use string
short string
long string
expect string
args []string
code int
}{
{"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}, 0},
{"echo", "echo stuff", "This echos stuff", "hello\n", []string{}, 0},
{"env", "env stuff", "show the env", "HELM_PLUGIN_NAME=env\n", []string{}, 0},
{"exitwith", "exitwith code", "This exits with the specified exit code", "", []string{"2"}, 2},
{"fullenv", "show env vars", "show all env vars", fullEnvOutput, []string{}, 0},
}
pluginCmds := cmd.Commands()
require.Len(t, pluginCmds, len(tests), "Expected %d plugins, got %d", len(tests), len(pluginCmds))
for i := range pluginCmds {
out.Reset()
tt := tests[i]
pluginCmd := pluginCmds[i]
t.Run(fmt.Sprintf("%s-%d", pluginCmd.Name(), i), func(t *testing.T) {
out.Reset()
if pluginCmd.Use != tt.use {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pluginCmd.Use)
}
if pluginCmd.Short != tt.short {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pluginCmd.Short)
}
if pluginCmd.Long != tt.long {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pluginCmd.Long)
}
// Currently, plugins assume a Linux subsystem. Skip the execution
// tests until this is fixed
if runtime.GOOS != "windows" {
if err := pluginCmd.RunE(pluginCmd, tt.args); err != nil {
if tt.code > 0 {
cerr, ok := err.(CommandError)
if !ok {
t.Errorf("Expected %s to return pluginError: got %v(%T)", tt.use, err, err)
}
if cerr.ExitCode != tt.code {
t.Errorf("Expected %s to return %d: got %d", tt.use, tt.code, cerr.ExitCode)
}
} else {
t.Errorf("Error running %s: %+v", tt.use, err)
}
}
assert.Equal(t, tt.expect, out.String(), "expected output for %q", tt.use)
}
})
}
}
func TestLoadPluginsWithSpace(t *testing.T) {
settings.PluginsDirectory = "testdata/helm home with space/helm/plugins"
settings.RepositoryConfig = "testdata/helm home with space/helm/repositories.yaml"
settings.RepositoryCache = "testdata/helm home with space/helm/repository"
var (
out bytes.Buffer
cmd cobra.Command
)
loadCLIPlugins(&cmd, &out)
envs := strings.Join([]string{
"fullenv",
"testdata/helm home with space/helm/plugins/fullenv",
"testdata/helm home with space/helm/plugins",
"testdata/helm home with space/helm/repositories.yaml",
"testdata/helm home with space/helm/repository",
os.Args[0],
}, "\n")
// Test that the YAML file was correctly converted to a command.
tests := []struct {
use string
short string
long string
expect string
args []string
code int
}{
{"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}, 0},
}
plugins := cmd.Commands()
if len(plugins) != len(tests) {
t.Fatalf("Expected %d plugins, got %d", len(tests), len(plugins))
}
for i := 0; i < len(plugins); i++ {
out.Reset()
tt := tests[i]
pp := plugins[i]
if pp.Use != tt.use {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pp.Use)
}
if pp.Short != tt.short {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pp.Short)
}
if pp.Long != tt.long {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pp.Long)
}
// Currently, plugins assume a Linux subsystem. Skip the execution
// tests until this is fixed
if runtime.GOOS != "windows" {
if err := pp.RunE(pp, tt.args); err != nil {
if tt.code > 0 {
cerr, ok := err.(CommandError)
if !ok {
t.Errorf("Expected %s to return pluginError: got %v(%T)", tt.use, err, err)
}
if cerr.ExitCode != tt.code {
t.Errorf("Expected %s to return %d: got %d", tt.use, tt.code, cerr.ExitCode)
}
} else {
t.Errorf("Error running %s: %+v", tt.use, err)
}
}
assert.Equal(t, tt.expect, out.String(), "expected output for %s", tt.use)
}
}
}
type staticCompletionDetails struct {
use string
validArgs []string
flags []string
next []staticCompletionDetails
}
func TestLoadCLIPluginsForCompletion(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
var out bytes.Buffer
cmd := &cobra.Command{
Use: "completion",
}
loadCLIPlugins(cmd, &out)
tests := []staticCompletionDetails{
{"args", []string{}, []string{}, []staticCompletionDetails{}},
{"echo", []string{}, []string{}, []staticCompletionDetails{}},
{"env", []string{}, []string{"global"}, []staticCompletionDetails{
{"list", []string{}, []string{"a", "all", "log"}, []staticCompletionDetails{}},
{"remove", []string{"all", "one"}, []string{}, []staticCompletionDetails{}},
}},
{"exitwith", []string{}, []string{}, []staticCompletionDetails{
{"code", []string{}, []string{"a", "b"}, []staticCompletionDetails{}},
}},
{"fullenv", []string{}, []string{"q", "z"}, []staticCompletionDetails{
{"empty", []string{}, []string{}, []staticCompletionDetails{}},
{"full", []string{}, []string{}, []staticCompletionDetails{
{"less", []string{}, []string{"a", "all"}, []staticCompletionDetails{}},
{"more", []string{"one", "two"}, []string{"b", "ball"}, []staticCompletionDetails{}},
}},
}},
}
checkCommand(t, cmd.Commands(), tests)
}
func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) {
t.Helper()
require.Len(t, plugins, len(tests), "Expected commands %v, got %v", tests, plugins)
is := assert.New(t)
for i := range plugins {
pp := plugins[i]
tt := tests[i]
is.Equal(pp.Use, tt.use, "Expected Use=%q, got %q", tt.use, pp.Use)
targs := tt.validArgs
pargs := pp.ValidArgs
is.ElementsMatch(targs, pargs)
tflags := tt.flags
var pflags []string
pp.LocalFlags().VisitAll(func(flag *pflag.Flag) {
pflags = append(pflags, flag.Name)
if len(flag.Shorthand) > 0 && flag.Shorthand != flag.Name {
pflags = append(pflags, flag.Shorthand)
}
})
is.ElementsMatch(tflags, pflags)
// Check the next level
checkCommand(t, pp.Commands(), tt.next)
}
}
func TestPluginDynamicCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin",
cmd: "__complete args ''",
golden: "output/plugin_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with flag",
cmd: "__complete args --myflag ''",
golden: "output/plugin_args_flag_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with global flag",
cmd: "__complete args --namespace mynamespace ''",
golden: "output/plugin_args_ns_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with multiple args",
cmd: "__complete args --myflag --namespace mynamespace start",
golden: "output/plugin_args_many_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin no directive",
cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{},
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestLoadCLIPlugins_HelmNoPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repository"
t.Setenv("HELM_NO_PLUGINS", "1")
out := bytes.NewBuffer(nil)
cmd := &cobra.Command{}
loadCLIPlugins(cmd, out)
plugins := cmd.Commands()
if len(plugins) != 0 {
t.Fatalf("Expected 0 plugins, got %d", len(plugins))
}
}
func TestPluginCmdsCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin update",
cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update, no filter",
cmd: "__complete plugin update full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall",
cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall, no filter",
cmd: "__complete plugin uninstall full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin list",
cmd: "__complete plugin list ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install no args",
cmd: "__complete plugin install ''",
golden: "output/empty_default_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install one arg",
cmd: "__complete plugin list /tmp ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestPluginFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin", false)
}
func TestPluginInstallFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin install", true)
checkFileCompletion(t, "plugin install mypath", false)
}
func TestPluginListFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin list", false)
}
func TestPluginUninstallFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin uninstall", false)
checkFileCompletion(t, "plugin uninstall myplugin", false)
}
func TestPluginUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin update", false)
checkFileCompletion(t, "plugin update myplugin", false)
}