package main import ( "os" "path/filepath" "strings" "testing" ) // fakeCommands installs a fixed set of PATH executables for the test. func fakeCommands(t *testing.T, names ...string) { t.Helper() old := shellCommandList shellCommandList = func() []string { return names } t.Cleanup(func() { shellCommandList = old }) } // shellTree lays out a directory to complete against and points DIR at it. func shellTree(t *testing.T) string { t.Helper() dir := t.TempDir() for _, d := range []string{"src", "src/deep", ".hidden"} { if err := os.MkdirAll(filepath.Join(dir, d), 0755); err != nil { t.Fatal(err) } } for _, f := range []string{"main.go", "main_test.go", "Makefile", ".env", "src/util.go"} { if err := os.WriteFile(filepath.Join(dir, f), []byte("x"), 0644); err != nil { t.Fatal(err) } } old := DIR DIR = dir t.Cleanup(func() { DIR = old }) return dir } func TestShellCandidatesCommandWord(t *testing.T) { fakeCommands(t, "vi", "vim", "view", "grep", "git") shellTree(t) cands, prefix := shellCandidates("!vi") if prefix != "vi" { t.Errorf("prefix = %q, want vi", prefix) } if strings.Join(cands, ",") != "vi,view,vim" { t.Errorf("candidates = %v, want vi,view,vim sorted", cands) } // a bare '!' must not dump every executable on the machine if cands, _ := shellCandidates("!"); len(cands) != 0 { t.Errorf("bare '!' offered %d candidates", len(cands)) } // leading blanks are allowed, as runCommand allows them if cands, _ := shellCandidates(" !gi"); strings.Join(cands, ",") != "git" { t.Errorf("indented escape = %v, want git", cands) } // a command word with a separator is a path, not a PATH lookup if cands, prefix := shellCandidates("!./ma"); prefix != "ma" || strings.Join(cands, ",") != "main.go,main_test.go" { t.Errorf("./ma = %v (prefix %q), want the local files", cands, prefix) } } func TestShellCandidatesArguments(t *testing.T) { fakeCommands(t, "vi") shellTree(t) // paths resolve against the project directory, where `!` commands run cands, prefix := shellCandidates("!vi ma") if prefix != "ma" || strings.Join(cands, ",") != "main.go,main_test.go" { t.Errorf("candidates = %v (prefix %q)", cands, prefix) } // an empty argument lists the directory — dot entries stay out of the way cands, prefix = shellCandidates("!vi ") if prefix != "" { t.Errorf("prefix = %q, want empty", prefix) } if strings.Join(cands, ",") != "Makefile,main.go,main_test.go,src/" { t.Errorf("directory listing = %v", cands) } // ... until the prefix asks for them if cands, _ := shellCandidates("!vi ."); strings.Join(cands, ",") != ".env,.hidden/" { t.Errorf("dot prefix = %v, want the hidden entries", cands) } // a directory completes with its slash, so the next Tab walks into it cands, prefix = shellCandidates("!vi sr") if prefix != "sr" || strings.Join(cands, ",") != "src/" { t.Errorf("directory candidate = %v (prefix %q)", cands, prefix) } // inside a directory only the basename is completed, which is what keeps // the candidate list readable cands, prefix = shellCandidates("!vi src/ut") if prefix != "ut" || strings.Join(cands, ",") != "util.go" { t.Errorf("nested candidate = %v (prefix %q), want util.go / ut", cands, prefix) } // later arguments complete the same way as the first if cands, _ := shellCandidates("!diff main.go ma"); strings.Join(cands, ",") != "main.go,main_test.go" { t.Errorf("second argument = %v", cands) } } func TestShellCandidatesAbsoluteAndHome(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) if err := os.WriteFile(filepath.Join(home, "notes.txt"), []byte("x"), 0644); err != nil { t.Fatal(err) } shellTree(t) fakeCommands(t, "vi") if cands, prefix := shellCandidates("!vi ~/no"); prefix != "no" || strings.Join(cands, ",") != "notes.txt" { t.Errorf("~/ completion = %v (prefix %q)", cands, prefix) } abs := filepath.Join(home, "no") if cands, prefix := shellCandidates("!vi " + abs); prefix != "no" || strings.Join(cands, ",") != "notes.txt" { t.Errorf("absolute completion = %v (prefix %q)", cands, prefix) } } // TestShellCompleterDoContract is the part that would corrupt the line if it // were wrong: readline replaces the last `length` runes with a candidate, so // the candidates must be suffixes and the length must count runes. func TestShellCompleterDoContract(t *testing.T) { fakeCommands(t, "vim", "view") shellTree(t) c := completer() line := []rune("!vi") got, length := c.Do(line, len(line)) if length != 2 { // "vi" — the '!' is not part of the word t.Fatalf("length = %d, want 2", length) } // rebuilding the line from prefix + candidate must give the full word for i, g := range got { full := string(line[:len(line)-length]) + string(line[len(line)-length:]) + string(g) if full != "!vim" && full != "!view" { t.Errorf("candidate %d rebuilds to %q", i, full) } } // a non-'!' line still goes to the builtin command tree (which appends its // own trailing space on a unique match) line = []rune("stat") got, length = c.Do(line, len(line)) if length != 4 || len(got) == 0 || !strings.HasPrefix(string(got[0]), "us") { t.Errorf("builtin completion = %q, %d; want a candidate starting \"us\" at 4", got, length) } // a multi-byte prefix must be measured in runes, not bytes if _, n := runeSuffixes([]string{"übermorgen"}, "üb"); n != 2 { t.Errorf("runeSuffixes length = %d, want 2 runes", n) } } // TestPathExecutablesFindsRealBinaries checks the PATH scan against a directory // it controls: only files with an execute bit, no directories. func TestPathExecutablesFindsRealBinaries(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "runnable"), []byte("#!/bin/sh\n"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "plainfile"), []byte("x"), 0644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(dir, "subdir"), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", dir) // pathExecutables caches for the session, so exercise the scan directly var names []string entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, e := range entries { fi, err := e.Info() if err != nil || fi.IsDir() || fi.Mode().Perm()&0o111 == 0 { continue } names = append(names, e.Name()) } if strings.Join(names, ",") != "runnable" { t.Errorf("executable scan = %v, want only runnable", names) } }