1package fsext
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/require"
11)
12
13func TestGlobWithDoubleStar(t *testing.T) {
14 t.Run("finds files matching pattern", func(t *testing.T) {
15 testDir := t.TempDir()
16
17 mainGo := filepath.Join(testDir, "src", "main.go")
18 utilsGo := filepath.Join(testDir, "src", "utils.go")
19 helperGo := filepath.Join(testDir, "pkg", "helper.go")
20 readmeMd := filepath.Join(testDir, "README.md")
21
22 for _, file := range []string{mainGo, utilsGo, helperGo, readmeMd} {
23 require.NoError(t, os.MkdirAll(filepath.Dir(file), 0o755))
24 require.NoError(t, os.WriteFile(file, []byte("test content"), 0o644))
25 }
26
27 matches, truncated, err := GlobWithDoubleStar("**/main.go", testDir, 0)
28 require.NoError(t, err)
29 require.False(t, truncated)
30
31 require.Equal(t, matches, []string{mainGo})
32 })
33
34 t.Run("finds directories matching pattern", func(t *testing.T) {
35 testDir := t.TempDir()
36
37 srcDir := filepath.Join(testDir, "src")
38 pkgDir := filepath.Join(testDir, "pkg")
39 internalDir := filepath.Join(testDir, "internal")
40 cmdDir := filepath.Join(testDir, "cmd")
41 pkgFile := filepath.Join(testDir, "pkg.txt")
42
43 for _, dir := range []string{srcDir, pkgDir, internalDir, cmdDir} {
44 require.NoError(t, os.MkdirAll(dir, 0o755))
45 }
46
47 require.NoError(t, os.WriteFile(filepath.Join(srcDir, "main.go"), []byte("package main"), 0o644))
48 require.NoError(t, os.WriteFile(pkgFile, []byte("test"), 0o644))
49
50 matches, truncated, err := GlobWithDoubleStar("pkg", testDir, 0)
51 require.NoError(t, err)
52 require.False(t, truncated)
53
54 require.Equal(t, matches, []string{pkgDir})
55 })
56
57 t.Run("finds nested directories with wildcard patterns", func(t *testing.T) {
58 testDir := t.TempDir()
59
60 srcPkgDir := filepath.Join(testDir, "src", "pkg")
61 libPkgDir := filepath.Join(testDir, "lib", "pkg")
62 mainPkgDir := filepath.Join(testDir, "pkg")
63 otherDir := filepath.Join(testDir, "other")
64
65 for _, dir := range []string{srcPkgDir, libPkgDir, mainPkgDir, otherDir} {
66 require.NoError(t, os.MkdirAll(dir, 0o755))
67 }
68
69 matches, truncated, err := GlobWithDoubleStar("**/pkg", testDir, 0)
70 require.NoError(t, err)
71 require.False(t, truncated)
72
73 var relativeMatches []string
74 for _, match := range matches {
75 rel, err := filepath.Rel(testDir, match)
76 require.NoError(t, err)
77 relativeMatches = append(relativeMatches, filepath.ToSlash(rel))
78 }
79
80 require.ElementsMatch(t, relativeMatches, []string{"pkg", "src/pkg", "lib/pkg"})
81 })
82
83 t.Run("finds directory contents with recursive patterns", func(t *testing.T) {
84 testDir := t.TempDir()
85
86 pkgDir := filepath.Join(testDir, "pkg")
87 pkgFile1 := filepath.Join(pkgDir, "main.go")
88 pkgFile2 := filepath.Join(pkgDir, "utils.go")
89 pkgSubdir := filepath.Join(pkgDir, "internal")
90 pkgSubfile := filepath.Join(pkgSubdir, "helper.go")
91
92 require.NoError(t, os.MkdirAll(pkgSubdir, 0o755))
93
94 for _, file := range []string{pkgFile1, pkgFile2, pkgSubfile} {
95 require.NoError(t, os.WriteFile(file, []byte("package main"), 0o644))
96 }
97
98 matches, truncated, err := GlobWithDoubleStar("pkg/**", testDir, 0)
99 require.NoError(t, err)
100 require.False(t, truncated)
101
102 var relativeMatches []string
103 for _, match := range matches {
104 rel, err := filepath.Rel(testDir, match)
105 require.NoError(t, err)
106 relativeMatches = append(relativeMatches, filepath.ToSlash(rel))
107 }
108
109 require.ElementsMatch(t, relativeMatches, []string{
110 "pkg",
111 "pkg/main.go",
112 "pkg/utils.go",
113 "pkg/internal",
114 "pkg/internal/helper.go",
115 })
116 })
117
118 t.Run("respects limit parameter", func(t *testing.T) {
119 testDir := t.TempDir()
120
121 for i := range 10 {
122 file := filepath.Join(testDir, "file", fmt.Sprintf("test%d.txt", i))
123 require.NoError(t, os.MkdirAll(filepath.Dir(file), 0o755))
124 require.NoError(t, os.WriteFile(file, []byte("test"), 0o644))
125 }
126
127 matches, truncated, err := GlobWithDoubleStar("**/*.txt", testDir, 5)
128 require.NoError(t, err)
129 require.True(t, truncated, "Expected truncation with limit")
130 require.Len(t, matches, 5, "Expected exactly 5 matches with limit")
131 })
132
133 t.Run("handles nested directory patterns", func(t *testing.T) {
134 testDir := t.TempDir()
135
136 file1 := filepath.Join(testDir, "a", "b", "c", "file1.txt")
137 file2 := filepath.Join(testDir, "a", "b", "file2.txt")
138 file3 := filepath.Join(testDir, "a", "file3.txt")
139 file4 := filepath.Join(testDir, "file4.txt")
140
141 for _, file := range []string{file1, file2, file3, file4} {
142 require.NoError(t, os.MkdirAll(filepath.Dir(file), 0o755))
143 require.NoError(t, os.WriteFile(file, []byte("test"), 0o644))
144 }
145
146 matches, truncated, err := GlobWithDoubleStar("a/b/c/file1.txt", testDir, 0)
147 require.NoError(t, err)
148 require.False(t, truncated)
149
150 require.Equal(t, []string{file1}, matches)
151 })
152
153 t.Run("returns results sorted by modification time (newest first)", func(t *testing.T) {
154 testDir := t.TempDir()
155
156 file1 := filepath.Join(testDir, "file1.txt")
157 require.NoError(t, os.WriteFile(file1, []byte("first"), 0o644))
158
159 file2 := filepath.Join(testDir, "file2.txt")
160 require.NoError(t, os.WriteFile(file2, []byte("second"), 0o644))
161
162 file3 := filepath.Join(testDir, "file3.txt")
163 require.NoError(t, os.WriteFile(file3, []byte("third"), 0o644))
164
165 base := time.Now()
166 m1 := base
167 m2 := base.Add(10 * time.Hour)
168 m3 := base.Add(20 * time.Hour)
169
170 require.NoError(t, os.Chtimes(file1, m1, m1))
171 require.NoError(t, os.Chtimes(file2, m2, m2))
172 require.NoError(t, os.Chtimes(file3, m3, m3))
173
174 matches, truncated, err := GlobWithDoubleStar("*.txt", testDir, 0)
175 require.NoError(t, err)
176 require.False(t, truncated)
177
178 require.Equal(t, []string{file3, file2, file1}, matches)
179 })
180
181 t.Run("handles empty directory", func(t *testing.T) {
182 testDir := t.TempDir()
183
184 matches, truncated, err := GlobWithDoubleStar("**", testDir, 0)
185 require.NoError(t, err)
186 require.False(t, truncated)
187 // Even empty directories should return the directory itself
188 require.Equal(t, []string{testDir}, matches)
189 })
190
191 t.Run("handles non-existent search path", func(t *testing.T) {
192 nonExistentDir := filepath.Join(t.TempDir(), "does", "not", "exist")
193
194 matches, truncated, err := GlobWithDoubleStar("**", nonExistentDir, 0)
195 require.Error(t, err, "Should return error for non-existent search path")
196 require.False(t, truncated)
197 require.Empty(t, matches)
198 })
199
200 t.Run("respects basic ignore patterns", func(t *testing.T) {
201 testDir := t.TempDir()
202
203 rootIgnore := filepath.Join(testDir, ".crushignore")
204
205 require.NoError(t, os.WriteFile(rootIgnore, []byte("*.tmp\nbackup/\n"), 0o644))
206
207 goodFile := filepath.Join(testDir, "good.txt")
208 require.NoError(t, os.WriteFile(goodFile, []byte("content"), 0o644))
209
210 badFile := filepath.Join(testDir, "bad.tmp")
211 require.NoError(t, os.WriteFile(badFile, []byte("temp content"), 0o644))
212
213 goodDir := filepath.Join(testDir, "src")
214 require.NoError(t, os.MkdirAll(goodDir, 0o755))
215
216 ignoredDir := filepath.Join(testDir, "backup")
217 require.NoError(t, os.MkdirAll(ignoredDir, 0o755))
218
219 ignoredFileInDir := filepath.Join(testDir, "backup", "old.txt")
220 require.NoError(t, os.WriteFile(ignoredFileInDir, []byte("old content"), 0o644))
221
222 matches, truncated, err := GlobWithDoubleStar("*.tmp", testDir, 0)
223 require.NoError(t, err)
224 require.False(t, truncated)
225 require.Empty(t, matches, "Expected no matches for '*.tmp' pattern (should be ignored)")
226
227 matches, truncated, err = GlobWithDoubleStar("backup", testDir, 0)
228 require.NoError(t, err)
229 require.False(t, truncated)
230 require.Empty(t, matches, "Expected no matches for 'backup' pattern (should be ignored)")
231
232 matches, truncated, err = GlobWithDoubleStar("*.txt", testDir, 0)
233 require.NoError(t, err)
234 require.False(t, truncated)
235 require.Equal(t, []string{goodFile}, matches)
236 })
237
238 t.Run("handles mixed file and directory matching with sorting", func(t *testing.T) {
239 testDir := t.TempDir()
240
241 oldestFile := filepath.Join(testDir, "old.rs")
242 require.NoError(t, os.WriteFile(oldestFile, []byte("old"), 0o644))
243
244 middleDir := filepath.Join(testDir, "mid.rs")
245 require.NoError(t, os.MkdirAll(middleDir, 0o755))
246
247 newestFile := filepath.Join(testDir, "new.rs")
248 require.NoError(t, os.WriteFile(newestFile, []byte("new"), 0o644))
249
250 base := time.Now()
251 tOldest := base
252 tMiddle := base.Add(10 * time.Hour)
253 tNewest := base.Add(20 * time.Hour)
254
255 // Reverse the expected order
256 require.NoError(t, os.Chtimes(newestFile, tOldest, tOldest))
257 require.NoError(t, os.Chtimes(middleDir, tMiddle, tMiddle))
258 require.NoError(t, os.Chtimes(oldestFile, tNewest, tNewest))
259
260 matches, truncated, err := GlobWithDoubleStar("*.rs", testDir, 0)
261 require.NoError(t, err)
262 require.False(t, truncated)
263 require.Len(t, matches, 3)
264
265 // Results should be sorted by mod time, but we set the oldestFile
266 // to have the most recent mod time
267 require.Equal(t, []string{oldestFile, middleDir, newestFile}, matches)
268 })
269}