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}