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