279 lines
9.5 KiB
Go
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)
|
|
}
|
|
}
|