sdk/glob/fs_test.go
Louis Seubert 24ad00274d
All checks were successful
default / ensure tests work (push) Successful in 35s
feat: rewrote glob to have a full doublestar impl
This implements a full double star glob implementation with it's own filesystem implementation.
2026-01-25 17:22:16 +01:00

505 lines
13 KiB
Go

package glob
import (
"archive/zip"
"bytes"
"io"
"io/fs"
"reflect"
"sort"
"testing"
"testing/fstest"
)
func setupFS() fs.ReadDirFS {
// Create an in-memory FS with a mix of files and directories
return fstest.MapFS{
"main.go": &fstest.MapFile{Data: []byte("package main")},
"main_test.go": &fstest.MapFile{Data: []byte("package main_test")},
"README.md": &fstest.MapFile{Data: []byte("# readme")},
"LICENSE": &fstest.MapFile{Data: []byte("MIT")},
"docs/guide.md": &fstest.MapFile{Data: []byte("Docs")},
"docs/other.txt": &fstest.MapFile{Data: []byte("Other")},
"docs/hidden/.keep": &fstest.MapFile{Data: []byte("")},
"assets/img.png": &fstest.MapFile{Data: []byte("PNG")},
"assets/style.css": &fstest.MapFile{Data: []byte("CSS")},
".gitignore": &fstest.MapFile{Data: []byte("*.log")},
".hiddenfile": &fstest.MapFile{Data: []byte("")},
"emptydir": &fstest.MapFile{Mode: fs.ModeDir | 0o755},
}
}
// helper to get base names for easier comparison
func basenames(entries []fs.DirEntry) []string {
names := []string{}
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
return names
}
func TestGlobFS_MultiplePatterns(t *testing.T) {
memfs := setupFS()
gfs, err := NewGlobFS(memfs, "*.go", "*.md", "assets/*", "docs/guide.md", ".gitignore")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
tests := []struct {
path string
want []string
wantErr bool
}{
{path: ".", want: []string{"README.md", "assets", "docs", "main.go", "main_test.go", ".gitignore"}},
{path: "assets", want: []string{"img.png", "style.css"}},
{path: "docs", want: []string{"guide.md"}},
{path: "docs/hidden", want: []string{}, wantErr: true},
{path: "emptydir", want: []string{}, wantErr: true},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run(escape(tc.path), func(t *testing.T) {
entries, err := fs.ReadDir(gfs, tc.path)
if tc.wantErr && err == nil {
t.Errorf("expected error, got nil")
return
}
if !tc.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
return
}
got := basenames(entries)
sort.Strings(tc.want)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got %v; want %v", got, tc.want)
}
})
}
}
func TestGlobFS_Open(t *testing.T) {
memfs := setupFS()
gfs, err := NewGlobFS(memfs, "*.go", "*.md", "assets/*", "docs/guide.md", ".gitignore")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
type test struct {
path string
wantErr bool
}
tests := []test{
{path: "main.go"},
{path: "README.md"},
{path: "LICENSE", wantErr: true},
{path: "assets/img.png"},
{path: "assets/style.css"},
{path: "assets/nonexistent.png", wantErr: true},
{path: "docs/guide.md"},
{path: "docs/other.txt", wantErr: true},
{path: ".gitignore"},
{path: ".hiddenfile", wantErr: true},
{path: "docs/hidden/.keep", wantErr: true},
{path: "emptydir", wantErr: true},
{path: "docs"}, // allowed because it contains matching file(s)
{path: "assets"}, // allowed because it contains matching file(s)
}
for _, tc := range tests {
tc := tc
t.Run(escape(tc.path), func(t *testing.T) {
f, err := gfs.Open(tc.path)
if tc.wantErr && err == nil {
t.Errorf("expected error, got file")
if f != nil {
f.Close()
}
} else if !tc.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
} else if !tc.wantErr && err == nil {
info, _ := f.Stat()
if info.IsDir() {
_, derr := fs.ReadDir(gfs, tc.path)
if derr != nil && !tc.wantErr {
t.Errorf("unexpected error: %v", derr)
}
}
f.Close()
}
})
}
}
func TestGlobFS_ReadFile(t *testing.T) {
memfs := setupFS()
gfs, err := NewGlobFS(memfs, "*.go", "*.md", "assets/*", ".gitignore")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
tests := []struct {
name string
want []byte
wantErr bool
}{
{name: "main.go", want: []byte("package main")},
{name: "main_test.go", want: []byte("package main_test")},
{name: "README.md", want: []byte("# readme")},
{name: "assets/img.png", want: []byte("PNG")},
{name: "assets/style.css", want: []byte("CSS")},
{name: ".gitignore", want: []byte("*.log")},
{name: "LICENSE", wantErr: true}, // not allowed by filter
{name: "docs/guide.md", wantErr: true}, // not allowed by filter
{name: "docs/hidden/.keep", wantErr: true}, // not allowed by filter
{name: "doesnotexist.txt", wantErr: true}, // does not exist
}
for _, tc := range tests {
tc := tc
t.Run(escape(tc.name), func(t *testing.T) {
got, err := fs.ReadFile(gfs, tc.name)
if tc.wantErr {
if err == nil {
t.Errorf("expected error, got nil (got=%q)", got)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if string(got) != string(tc.want) {
t.Errorf("got %q; want %q", got, tc.want)
}
}
})
}
}
func TestGlobFS_RelativePaths(t *testing.T) {
memfs := setupFS()
gfs, err := NewGlobFS(memfs, "docs/*.md")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
entries, err := fs.ReadDir(gfs, "docs")
if err != nil {
t.Fatal(err)
}
got := basenames(entries)
want := []string{"guide.md"}
if !reflect.DeepEqual(got, want) {
t.Errorf("docs/*.md: got %v, want %v", got, want)
}
}
func TestGlobFS_NoMatchesOpen(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "*.xyz")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = gfs.Open("main.go")
if err == nil {
t.Fatal("expected error when opening file with no matches")
}
}
func TestGlobFS_NoMatchesStat(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "*.xyz")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.Stat(gfs, "main.go")
if err == nil {
t.Fatal("expected error with no matches: stat")
}
}
func TestGlobFS_NoMatchesReadDir(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "*.xyz")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.ReadDir(gfs, "main.go")
if err == nil {
t.Fatal("expected error with no matches: readdir")
}
}
func TestGlobFS_NoMatchesReadFile(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "*.xyz")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.ReadFile(gfs, "main.go")
if err == nil {
t.Fatal("expected error with no matches: readfile")
}
}
func TestGlobFS_MatchEmptyDirExact(t *testing.T) {
// the trailing slash indicates that the directory should be included
gfs, err := NewGlobFS(setupFS(), "emptydir/")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.ReadDir(gfs, "emptydir")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestGlobFS_MatchEmptyDirExact2(t *testing.T) {
// the trailing slash indicates that the directory should be included
gfs, err := NewGlobFS(setupFS(), "emptydir/*")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.ReadDir(gfs, "emptydir")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestGlobFS_NoMatchEmptyDirExact(t *testing.T) {
// no traling slash indicates that the directory must be a file to be included
gfs, err := NewGlobFS(setupFS(), "emptydir")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
_, err = fs.ReadDir(gfs, "emptydir")
if err == nil {
t.Fatal("expected error with no matches: readfile")
}
}
func TestGlobFS_IntegrationWithStdlibWalkDir(t *testing.T) {
memfs := setupFS()
gfs, err := NewGlobFS(memfs, "*.go", "docs/guide.md")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
// Use fs.WalkDir with our filtered FS
var walked []string
err = fs.WalkDir(gfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Fatalf("the %q caused: %v", path, err)
return err
}
walked = append(walked, path)
return nil
})
if err != nil {
t.Fatal(err)
}
// Only files and dirs matching or containing matches should appear
for _, p := range walked {
if p == "." || p == "main.go" || p == "main_test.go" || p == "docs" || p == "docs/guide.md" {
continue
}
t.Errorf("WalkDir: unexpected path %q", p)
}
}
func TestGlobFS_InvalidPattern(t *testing.T) {
_, err := NewGlobFS(setupFS(), "[invalid")
if err == nil {
t.Fatal("expected error for invalid pattern, got nil")
}
}
func TestGlobFS_WildcardInDirSegment(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "docs/*/*.md")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, "docs/hidden")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected no entries, got %v", basenames(entries))
}
}
func TestGlobFS_DeeplyNestedMatch(t *testing.T) {
memfs := fstest.MapFS{
"a/b/c/d.txt": &fstest.MapFile{Data: []byte("deep")},
}
gfs, err := NewGlobFS(memfs, "a/b/c/*.txt")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := fs.ReadFile(gfs, "a/b/c/d.txt")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if string(data) != "deep" {
t.Errorf("got %q, want %q", data, "deep")
}
}
func TestGlobFS_HiddenFilesOnly(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), ".*")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, ".")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := basenames(entries)
want := []string{".gitignore", ".hiddenfile"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
// Test directory pattern matching with various directory globs
func TestGlobFS_DirectoryPatterns(t *testing.T) {
memfs := fstest.MapFS{
"foo/bar/baz.txt": &fstest.MapFile{Data: []byte("baz")},
"foo/bar/qux.txt": &fstest.MapFile{Data: []byte("qux")},
"foo/readme.md": &fstest.MapFile{Data: []byte("readme")},
"foo/empty/.keep": &fstest.MapFile{Data: []byte("")}, // represent empty dir by a file inside
"top.txt": &fstest.MapFile{Data: []byte("top")},
}
t.Run("single dir segment wildcard", func(t *testing.T) {
gfs, err := NewGlobFS(memfs, "foo/bar/*")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, "foo/bar")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := basenames(entries)
want := []string{"baz.txt", "qux.txt"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
t.Run("recursive dir wildcard", func(t *testing.T) {
gfs, err := NewGlobFS(memfs, "foo/bar/*")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, "foo/bar")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := basenames(entries)
want := []string{"baz.txt", "qux.txt"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
entries, err = fs.ReadDir(gfs, "foo")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got = basenames(entries)
want = []string{"bar"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
t.Run("match empty directory", func(t *testing.T) {
gfs, err := NewGlobFS(memfs, "foo/empty/")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, "foo/empty")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected empty, got %v", basenames(entries))
}
})
t.Run("top-level dir wildcard", func(t *testing.T) {
gfs, err := NewGlobFS(memfs, "*/bar/*")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, err := fs.ReadDir(gfs, "foo/bar")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := basenames(entries)
want := []string{"baz.txt", "qux.txt"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
}
func TestGlobFS_IntegrationWithStdlibZipWriter(t *testing.T) {
gfs, err := NewGlobFS(setupFS(), "*")
if err != nil {
t.Errorf("unexpected error while creating glob fs: %v", err)
}
want := map[string]string{
"main.go": "package main",
"main_test.go": "package main_test",
"README.md": "# readme",
"LICENSE": "MIT",
".gitignore": "*.log",
".hiddenfile": "",
}
buf := new(bytes.Buffer)
wr := zip.NewWriter(buf)
err = wr.AddFS(gfs)
if err != nil {
t.Fatalf("adding fs to zip writer: %v", err)
}
err = wr.Close()
if err != nil {
t.Fatalf("close zip writer: %v", err)
}
rd, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(len(buf.Bytes())))
if err != nil {
t.Fatalf("invalid zip archive: %v", err)
}
got := make(map[string]string)
for _, f := range rd.File {
rc, err := f.Open()
if err != nil {
t.Fatalf("cannot open file %s: %v", f.Name, err)
}
content, err := io.ReadAll(rc)
defer rc.Close()
if err != nil {
t.Fatalf("cannot read file %s: %v", f.Name, err)
}
got[f.Name] = string(content)
}
// Compare expected vs actual.
for name, exp := range want {
act, ok := got[name]
if !ok {
t.Errorf("expected file %q not found in zip", name)
continue
}
if act != exp {
t.Errorf("content mismatch for %q:\nexpected: %q\nactual: %q", name, exp, act)
}
}
// Check for unexpected extra files.
for name := range got {
if _, ok := want[name]; !ok {
t.Errorf("unexpected file %q found in zip", name)
}
}
}