Files
2026-07-13 13:00:08 +08:00

198 lines
6.8 KiB
Go

package builtin
import (
"os"
"path/filepath"
"strings"
"testing"
fileencoding "reasonix/internal/fileutil/encoding"
)
func writeFileT(t *testing.T, path, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func mkRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
t.Fatal(err)
}
return dir
}
func TestGrepNestedGitignore(t *testing.T) {
dir := mkRepo(t)
writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "secret.txt\n")
writeFileT(t, filepath.Join(dir, "pkg", "keep.go"), "NEEDLE\n")
writeFileT(t, filepath.Join(dir, "pkg", "secret.txt"), "NEEDLE\n") // ignored by nested
writeFileT(t, filepath.Join(dir, "pkg", "app.log"), "NEEDLE\n") // ignored by root
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "keep.go") {
t.Fatalf("kept file must be found: %q", out)
}
if strings.Contains(out, "secret.txt") {
t.Fatalf("a nested .gitignore must apply: %q", out)
}
if strings.Contains(out, "app.log") {
t.Fatalf("an ancestor .gitignore must apply in subdirs: %q", out)
}
}
func TestGrepNestedNegationReincludes(t *testing.T) {
dir := mkRepo(t)
writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "!keep.log\n")
writeFileT(t, filepath.Join(dir, "pkg", "keep.log"), "NEEDLE\n") // re-included by nested negation
writeFileT(t, filepath.Join(dir, "pkg", "drop.log"), "NEEDLE\n") // still ignored by root
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "keep.log") {
t.Fatalf("a nested !negation must re-include: %q", out)
}
if strings.Contains(out, "drop.log") {
t.Fatalf("a non-negated .log must stay ignored: %q", out)
}
}
func TestGrepSkipsHidden(t *testing.T) {
dir := mkRepo(t)
writeFileT(t, filepath.Join(dir, "visible.txt"), "NEEDLE\n")
writeFileT(t, filepath.Join(dir, ".env"), "NEEDLE\n")
writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "visible.txt") {
t.Fatalf("a visible file must be found: %q", out)
}
if strings.Contains(out, ".env") || strings.Contains(out, "ci.yml") {
t.Fatalf("hidden files/dirs must be skipped by default: %q", out)
}
}
func TestGrepExplicitHiddenRootSearched(t *testing.T) {
dir := mkRepo(t)
writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
// Pointing grep straight at a hidden dir searches it in full.
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, ".github")})
if !strings.Contains(out, "ci.yml") {
t.Fatalf("an explicitly targeted hidden dir should be searched: %q", out)
}
}
// repo scaffolds a fake git repo: a .git marker, a .gitignore, and files both
// kept and ignored — enough for the native grep walk to exercise .gitignore.
func gitignoreRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
t.Fatal(err)
}
writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\nbuild/\n")
writeFileT(t, filepath.Join(dir, "keep.txt"), "NEEDLE in kept file\n")
writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE in ignored file\n")
writeFileT(t, filepath.Join(dir, "build", "out.txt"), "NEEDLE in ignored dir\n")
writeFileT(t, filepath.Join(dir, "src", "deep.txt"), "NEEDLE deep in tree\n")
return dir
}
func TestGrepSkipsGitignored(t *testing.T) {
dir := gitignoreRepo(t)
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "keep.txt") || !strings.Contains(out, "deep.txt") {
t.Fatalf("kept files must be searched: %q", out)
}
if strings.Contains(out, "ignored.txt") {
t.Fatalf("a .gitignore'd file must be skipped: %q", out)
}
if strings.Contains(out, "out.txt") {
t.Fatalf("files under a .gitignore'd dir must be skipped: %q", out)
}
}
func TestGrepDecodesGB18030Gitignore(t *testing.T) {
dir := mkRepo(t)
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), fileencoding.Encode("秘密.txt\n", fileencoding.GB18030), 0o644); err != nil {
t.Fatal(err)
}
writeFileT(t, filepath.Join(dir, "秘密.txt"), "NEEDLE ignored\n")
writeFileT(t, filepath.Join(dir, "公开.txt"), "NEEDLE kept\n")
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "公开.txt") {
t.Fatalf("kept Chinese file must be searched: %q", out)
}
if strings.Contains(out, "秘密.txt") {
t.Fatalf("GB18030 .gitignore pattern should skip Chinese file: %q", out)
}
}
func TestScanGitConfigExcludesDecodesGB18030Path(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, ".gitconfig")
want := filepath.Join(dir, "中文忽略规则.txt")
body := "[core]\n\texcludesFile = " + want + "\n"
if err := os.WriteFile(path, fileencoding.Encode(body, fileencoding.GB18030), 0o644); err != nil {
t.Fatal(err)
}
if got := scanGitConfigExcludes(path); got != want {
t.Fatalf("scanGitConfigExcludes = %q, want %q", got, want)
}
}
func TestGrepExplicitIgnoredRootStillSearched(t *testing.T) {
dir := gitignoreRepo(t)
// Pointing grep straight at a gitignored directory still searches it — the
// walk root is never pruned.
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, "build")})
if !strings.Contains(out, "out.txt") {
t.Fatalf("an explicitly targeted ignored dir should still be searched: %q", out)
}
}
func TestGrepNoRepoIgnoresNothing(t *testing.T) {
// Same layout but without a .git marker: there is no repository, so
// .gitignore is not consulted and every file is searched.
dir := t.TempDir()
writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\n")
writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE here\n")
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
if !strings.Contains(out, "ignored.txt") {
t.Fatalf("outside a git repo, .gitignore must not be applied: %q", out)
}
}
func TestFindRepoRoot(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
t.Fatal(err)
}
sub := filepath.Join(dir, "a", "b")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatal(err)
}
got, err := filepath.EvalSymlinks(findRepoRoot(sub))
if err != nil {
t.Fatal(err)
}
want, _ := filepath.EvalSymlinks(dir)
if got != want {
t.Fatalf("findRepoRoot(%q) = %q, want %q", sub, got, want)
}
if rr := findRepoRoot(t.TempDir()); rr != "" {
t.Fatalf("a dir with no .git ancestor should return \"\", got %q", rr)
}
}