Files
gvm/complete_test.go
2026-09-08 15:50:23 +02:00

279 lines
9.5 KiB
Go

package main
import (
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/integrii/flaggy"
)
// cacheHome points the cache directory at a temporary one, so a test never
// reads or writes the cache of the person running it. os.UserCacheDir goes by
// HOME on macOS and by XDG_CACHE_HOME on Linux, so both are set.
func cacheHome(t *testing.T) string {
t.Helper()
dir := t.TempDir()
t.Setenv("HOME", dir)
t.Setenv("XDG_CACHE_HOME", filepath.Join(dir, ".cache"))
return dir
}
func cacheRows(vc string, names ...string) []vmRow {
var rows []vmRow
for _, n := range names {
rows = append(rows, vmRow{vc: VCenter{Name: vc}, name: n})
}
return rows
}
func TestInventoryCacheRoundTrip(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "web01", "db01"))
if got := cachedNames(""); !slices.Equal(got, []string{"db01", "web01"}) {
t.Errorf("the cache gave back %v", got)
}
if got := cachedVCenters(); !slices.Equal(got, []string{"v308"}) {
t.Errorf("the servers came back as %v", got)
}
if got := cachedNames("web"); !slices.Equal(got, []string{"web01"}) {
t.Errorf("the prefix web matched %v", got)
}
// A shell completes what has been typed so far, whichever case it is in.
if got := cachedNames("WEB"); !slices.Equal(got, []string{"web01"}) {
t.Errorf("the prefix WEB matched %v", got)
}
if got := cachedNames("nothing-like-this"); len(got) != 0 {
t.Errorf("a prefix that matches nothing gave %v", got)
}
}
// A sweep of one server must not lose the others' machines. The point of
// completing out of a cache is that it works when a vCenter is down — or when
// the last command was `gvm -v v308 vm -l`.
func TestASweepOfOneServerKeepsTheOthers(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308", "v108"},
append(cacheRows("v308", "web01"), cacheRows("v108", "old01")...))
saveInventory([]string{"v308"}, cacheRows("v308", "web01", "web02"))
if got := cachedNames(""); !slices.Equal(got, []string{"old01", "web01", "web02"}) {
t.Errorf("after a sweep of one server the cache holds %v", got)
}
if got := cachedVCenters(); !slices.Equal(got, []string{"v108", "v308"}) {
t.Errorf("the servers came back as %v", got)
}
}
// A machine that has gone is gone from the cache of the server it was on.
func TestTheCacheForgetsWhatTheServerHasForgotten(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "web01", "temp01"))
saveInventory([]string{"v308"}, cacheRows("v308", "web01"))
if got := cachedNames(""); !slices.Equal(got, []string{"web01"}) {
t.Errorf("the cache still holds %v", got)
}
}
// A cache that cannot be read is one Tab without an answer, never an error.
func TestABrokenCacheIsSilent(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "web01"))
path, err := inventoryPath()
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("nonsense\nv308\tnot-a-date\tweb01\n"), 0o600); err != nil {
t.Fatal(err)
}
if got := cachedNames(""); len(got) != 0 {
t.Errorf("a broken cache offered %v", got)
}
if got := inventoryAge(); got != "-" {
t.Errorf("a broken cache is described as %q", got)
}
}
// The file holds machine names read off a vCenter, and the cache directory is
// not private, so it is written the way the configuration is.
func TestTheCacheIsNotWorldReadable(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "web01"))
path, _ := inventoryPath()
st, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if st.Mode().Perm() != 0o600 {
t.Errorf("the cache is mode %v", st.Mode().Perm())
}
}
// `gvm config` says how old the cache is, because a completion offering a
// machine deleted last month should be explicable.
func TestConfigSaysHowOldTheCacheIs(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "web01", "db01"))
got := inventoryAge()
if !strings.Contains(got, "2 machines") || !strings.Contains(got, "v308") {
t.Errorf("inventoryAge = %q", got)
}
if !strings.Contains(got, "ago") {
t.Errorf("inventoryAge does not say when: %q", got)
}
}
// The scripts complete against the cache and never against a vCenter: a Tab key
// that logs in three times is a Tab key nobody presses twice.
func TestTheCompletionScriptsAskGvmAndNothingElse(t *testing.T) {
for _, shell := range []string{"zsh", "bash"} {
script, ok := completionScript(shell, "# flaggy's half\n_half() {\n}\ncompdef _half gvm\n")
if !ok {
t.Fatalf("no script for %s", shell)
}
if !strings.Contains(script, "--complete-vms") || !strings.Contains(script, "--complete-vcenters") {
t.Errorf("the %s script does not ask gvm for the names:\n%s", shell, script)
}
// flaggy's half is carried, not replaced: that is the half that knows
// every subcommand and every option.
if !strings.Contains(script, "# flaggy's half") {
t.Errorf("the %s script threw flaggy's own half away", shell)
}
if !strings.Contains(script, "_gvm_names") {
t.Errorf("the %s script does not install itself", shell)
}
if !strings.Contains(script, "_half") {
t.Errorf("the %s script does not fall back to what flaggy installed", shell)
}
if !strings.Contains(script, "_half") {
t.Errorf("the %s script does not fall back to what flaggy installed", shell)
}
}
// A shell neither half knows is left to flaggy, whose own message names the
// ones it can write.
if _, ok := completionScript("klingon", ""); ok {
t.Error("a script was written for a shell nobody has")
}
}
// The addendum falls back to the function flaggy's own script installs, whose
// name is read off the script rather than written down twice. If flaggy ever
// names it differently the wrapper follows it there.
func TestTheAddendumDelegatesToFlaggysOwnFunction(t *testing.T) {
for _, c := range []struct{ script, want string }{
{"_gvm() {\n}\ncompdef _gvm gvm\n", "_gvm"},
{"_gvm_complete() {\n}\ncomplete -F _gvm_complete gvm\n", "_gvm_complete"},
{"_thing() {\n}\ncompdef _some_other_name thing\n", "_some_other_name"},
{"nothing installs anything here\n", ""},
} {
if got := installedFunction(c.script); got != c.want {
t.Errorf("installedFunction found %q, want %q", got, c.want)
}
}
// And on the real thing: flaggy builds the name out of the parser's name,
// which is the only part of the parser this depends on.
flaggy.SetName("gvm")
for _, shell := range []string{"zsh", "bash"} {
generated := flaggyCompletion(shell)
delegate := installedFunction(generated)
if delegate == "" {
t.Fatalf("nothing could be found to delegate to in flaggy's %s script:\n%s", shell, generated)
}
if !strings.Contains(generated, delegate+"()") {
t.Errorf("flaggy's %s script installs %s without defining it", shell, delegate)
}
script, _ := completionScript(shell, generated)
if !strings.Contains(script, delegate) {
t.Errorf("the %s addendum does not fall back to %s", shell, delegate)
}
}
}
// A shell script that is not valid shell is worse than none: the shell says so
// on every Tab. Both are checked with the shell's own parser, where there is one.
func TestTheCompletionScriptsAreValidShell(t *testing.T) {
flaggy.SetName("gvm")
for _, c := range []struct{ shell, flag string }{{"zsh", "-n"}, {"bash", "-n"}} {
if _, err := exec.LookPath(c.shell); err != nil {
t.Logf("no %s here to check with", c.shell)
continue
}
script, ok := completionScript(c.shell, flaggyCompletion(c.shell))
if !ok {
t.Fatalf("no %s script", c.shell)
}
path := filepath.Join(t.TempDir(), "completion."+c.shell)
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
t.Fatal(err)
}
out, err := exec.Command(c.shell, c.flag, path).CombinedOutput()
if err != nil {
t.Errorf("the %s script does not parse: %v\n%s\n%s", c.shell, err, out, script)
}
}
}
// `gvm completion <shell>` is flaggy's own subcommand, answered a step earlier.
// It has to be recognised exactly as flaggy would recognise it, or the two
// disagree about what the command line said.
func TestCompletionRequest(t *testing.T) {
for _, c := range []struct {
args []string
shell string
ok bool
}{
{[]string{"completion", "zsh"}, "zsh", true},
{[]string{"completion", "BASH"}, "bash", true},
{[]string{"Completion", "zsh"}, "zsh", true},
{[]string{"completion"}, "", false}, // flaggy asks which shell
{[]string{"vm", "-l"}, "", false},
{nil, "", false},
} {
shell, ok := completionRequest(c.args)
if ok != c.ok || shell != c.shell {
t.Errorf("completionRequest(%v) = %q, %v; want %q, %v", c.args, shell, ok, c.shell, c.ok)
}
}
}
// Every option the names are offered after is an option gvm actually has. The
// two lists cannot be one — a shell script has to name them as strings — so
// this is what keeps them from drifting apart. The subcommands and the rest of
// the options need no such check: flaggy writes those out of the parser itself.
func TestCompletionOptionsAreReal(t *testing.T) {
src, err := os.ReadFile("gvm.go")
if err != nil {
t.Fatal(err)
}
text := string(src)
for _, f := range append(append([]string{}, vmFlags...), vcFlags...) {
if !strings.Contains(text, `"`+strings.TrimLeft(f, "-")+`"`) {
t.Errorf("the completion offers %s, which gvm.go does not declare", f)
}
}
}
// A prefix beginning with a dash is still a prefix: the scripts put "--" in
// front of it so it cannot be taken for an option of gvm's own.
func TestACompletionPrefixMayLookLikeAnOption(t *testing.T) {
cacheHome(t)
saveInventory([]string{"v308"}, cacheRows("v308", "-odd-name", "web01"))
if got := cachedNames("-odd"); !slices.Equal(got, []string{"-odd-name"}) {
t.Errorf("the prefix -odd matched %v", got)
}
}