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}