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