@@ -0,0 +1,273 @@
1+package fsext
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+ "testing/synctest"
9+ "time"
10+
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func 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+}