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}