1package tools
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"regexp"
  7	"testing"
  8
  9	"github.com/stretchr/testify/require"
 10)
 11
 12func TestRegexCache(t *testing.T) {
 13	cache := newRegexCache()
 14
 15	// Test basic caching
 16	pattern := "test.*pattern"
 17	regex1, err := cache.get(pattern)
 18	if err != nil {
 19		t.Fatalf("Failed to compile regex: %v", err)
 20	}
 21
 22	regex2, err := cache.get(pattern)
 23	if err != nil {
 24		t.Fatalf("Failed to get cached regex: %v", err)
 25	}
 26
 27	// Should be the same instance (cached)
 28	if regex1 != regex2 {
 29		t.Error("Expected cached regex to be the same instance")
 30	}
 31
 32	// Test that it actually works
 33	if !regex1.MatchString("test123pattern") {
 34		t.Error("Regex should match test string")
 35	}
 36}
 37
 38func TestGlobToRegexCaching(t *testing.T) {
 39	// Test that globToRegex uses pre-compiled regex
 40	pattern1 := globToRegex("*.{js,ts}")
 41
 42	// Should not panic and should work correctly
 43	regex1, err := regexp.Compile(pattern1)
 44	if err != nil {
 45		t.Fatalf("Failed to compile glob regex: %v", err)
 46	}
 47
 48	if !regex1.MatchString("test.js") {
 49		t.Error("Glob regex should match .js files")
 50	}
 51	if !regex1.MatchString("test.ts") {
 52		t.Error("Glob regex should match .ts files")
 53	}
 54	if regex1.MatchString("test.go") {
 55		t.Error("Glob regex should not match .go files")
 56	}
 57}
 58
 59func TestGrepWithIgnoreFiles(t *testing.T) {
 60	t.Parallel()
 61	tempDir := t.TempDir()
 62
 63	// Create test files
 64	testFiles := map[string]string{
 65		"file1.txt":           "hello world",
 66		"file2.txt":           "hello world",
 67		"ignored/file3.txt":   "hello world",
 68		"node_modules/lib.js": "hello world",
 69		"secret.key":          "hello world",
 70	}
 71
 72	for path, content := range testFiles {
 73		fullPath := filepath.Join(tempDir, path)
 74		require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
 75		require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
 76	}
 77
 78	// Create .gitignore file
 79	gitignoreContent := "ignored/\n*.key\n"
 80	require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte(gitignoreContent), 0o644))
 81
 82	// Create .crushignore file
 83	crushignoreContent := "node_modules/\n"
 84	require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte(crushignoreContent), 0o644))
 85
 86	// Test both implementations
 87	for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
 88		"regex": searchFilesWithRegex,
 89		"rg": func(pattern, path, include string) ([]grepMatch, error) {
 90			return searchWithRipgrep(t.Context(), pattern, path, include)
 91		},
 92	} {
 93		t.Run(name, func(t *testing.T) {
 94			t.Parallel()
 95
 96			if name == "rg" && getRg() == "" {
 97				t.Skip("rg is not in $PATH")
 98			}
 99
100			matches, err := fn("hello world", tempDir, "")
101			require.NoError(t, err)
102
103			// Convert matches to a set of file paths for easier testing
104			foundFiles := make(map[string]bool)
105			for _, match := range matches {
106				foundFiles[filepath.Base(match.path)] = true
107			}
108
109			// Should find file1.txt and file2.txt
110			require.True(t, foundFiles["file1.txt"], "Should find file1.txt")
111			require.True(t, foundFiles["file2.txt"], "Should find file2.txt")
112
113			// Should NOT find ignored files
114			require.False(t, foundFiles["file3.txt"], "Should not find file3.txt (ignored by .gitignore)")
115			require.False(t, foundFiles["lib.js"], "Should not find lib.js (ignored by .crushignore)")
116			require.False(t, foundFiles["secret.key"], "Should not find secret.key (ignored by .gitignore)")
117
118			// Should find exactly 2 matches
119			require.Equal(t, 2, len(matches), "Should find exactly 2 matches")
120		})
121	}
122}
123
124func TestSearchImplementations(t *testing.T) {
125	t.Parallel()
126	tempDir := t.TempDir()
127
128	for path, content := range map[string]string{
129		"file1.go":         "package main\nfunc main() {\n\tfmt.Println(\"hello world\")\n}",
130		"file2.js":         "console.log('hello world');",
131		"file3.txt":        "hello world from text file",
132		"binary.exe":       "\x00\x01\x02\x03",
133		"empty.txt":        "",
134		"subdir/nested.go": "package nested\n// hello world comment",
135		".hidden.txt":      "hello world in hidden file",
136		"file4.txt":        "hello world from a banana",
137		"file5.txt":        "hello world from a grape",
138	} {
139		fullPath := filepath.Join(tempDir, path)
140		require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
141		require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
142	}
143
144	require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte("file4.txt\n"), 0o644))
145	require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte("file5.txt\n"), 0o644))
146
147	for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
148		"regex": searchFilesWithRegex,
149		"rg": func(pattern, path, include string) ([]grepMatch, error) {
150			return searchWithRipgrep(t.Context(), pattern, path, include)
151		},
152	} {
153		t.Run(name, func(t *testing.T) {
154			t.Parallel()
155
156			if name == "rg" && getRg() == "" {
157				t.Skip("rg is not in $PATH")
158			}
159
160			matches, err := fn("hello world", tempDir, "")
161			require.NoError(t, err)
162
163			require.Equal(t, len(matches), 4)
164			for _, match := range matches {
165				require.NotEmpty(t, match.path)
166				require.NotZero(t, match.lineNum)
167				require.NotEmpty(t, match.lineText)
168				require.NotZero(t, match.modTime)
169				require.NotContains(t, match.path, ".hidden.txt")
170				require.NotContains(t, match.path, "file4.txt")
171				require.NotContains(t, match.path, "file5.txt")
172				require.NotContains(t, match.path, "binary.exe")
173			}
174		})
175	}
176}
177
178// Benchmark to show performance improvement
179func BenchmarkRegexCacheVsCompile(b *testing.B) {
180	cache := newRegexCache()
181	pattern := "test.*pattern.*[0-9]+"
182
183	b.Run("WithCache", func(b *testing.B) {
184		for b.Loop() {
185			_, err := cache.get(pattern)
186			if err != nil {
187				b.Fatal(err)
188			}
189		}
190	})
191
192	b.Run("WithoutCache", func(b *testing.B) {
193		for b.Loop() {
194			_, err := regexp.Compile(pattern)
195			if err != nil {
196				b.Fatal(err)
197			}
198		}
199	})
200}