diff --git a/embedfs/chroot_test.go b/embedfs/chroot_test.go new file mode 100644 index 0000000..c545a41 --- /dev/null +++ b/embedfs/chroot_test.go @@ -0,0 +1,294 @@ +package embedfs + +import ( + "io" + "testing" + + "github.com/go-git/go-billy/v6" + "github.com/go-git/go-billy/v6/embedfs/internal/testdata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChroot_Basic(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test chroot to existing directory + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + require.NotNil(t, chrootFS) + + // Test that we can access files in the chrooted filesystem + f, err := chrootFS.Open("file1.txt") + require.NoError(t, err) + defer f.Close() + + content, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, "Hello from embedfs!", string(content)) +} + +func TestChroot_NestedDirectory(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test chroot to nested directory + chrootFS, err := fs.Chroot("testdata/subdir") + require.NoError(t, err) + require.NotNil(t, chrootFS) + + // Test that we can access nested files from the chrooted root + f, err := chrootFS.Open("nested.txt") + require.NoError(t, err) + defer f.Close() + + content, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, "Nested file content", string(content)) +} + +func TestChroot_StatInChroot(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test stat on files that exist in chrooted directory + fi, err := chrootFS.Stat("file1.txt") + require.NoError(t, err) + assert.Equal(t, "file1.txt", fi.Name()) + assert.False(t, fi.IsDir()) + + // Test stat on directories that exist in chrooted directory + fi, err = chrootFS.Stat("subdir") + require.NoError(t, err) + assert.Equal(t, "subdir", fi.Name()) + assert.True(t, fi.IsDir()) + + // Test stat with absolute path in chrooted filesystem + fi, err = chrootFS.Stat("/file2.txt") + require.NoError(t, err) + assert.Equal(t, "file2.txt", fi.Name()) + assert.False(t, fi.IsDir()) +} + +func TestChroot_ReadDirInChroot(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test reading directory contents from chrooted root + entries, err := chrootFS.ReadDir("/") + require.NoError(t, err) + + expectedFiles := []string{"empty.txt", "file1.txt", "file2.txt", "subdir"} + assert.Len(t, entries, len(expectedFiles)) + + foundFiles := make(map[string]bool) + for _, entry := range entries { + foundFiles[entry.Name()] = true + } + + for _, expected := range expectedFiles { + assert.True(t, foundFiles[expected], "Expected file %s not found", expected) + } + + // Test reading subdirectory from chrooted filesystem + entries, err = chrootFS.ReadDir("subdir") + require.NoError(t, err) + assert.Len(t, entries, 1) + assert.Equal(t, "nested.txt", entries[0].Name()) +} + +func TestChroot_PathNormalization(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test chroot with different path formats + tests := []struct { + name string + chrootPath string + openPath string + expectFile string + }{ + { + name: "absolute chroot path", + chrootPath: "/testdata", + openPath: "file1.txt", + expectFile: "file1.txt", + }, + { + name: "relative chroot path", + chrootPath: "testdata", + openPath: "file1.txt", + expectFile: "file1.txt", + }, + { + name: "absolute open path in chroot", + chrootPath: "testdata", + openPath: "/file1.txt", + expectFile: "file1.txt", + }, + { + name: "nested chroot", + chrootPath: "testdata/subdir", + openPath: "nested.txt", + expectFile: "nested.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chrootFS, err := fs.Chroot(tt.chrootPath) + require.NoError(t, err) + + f, err := chrootFS.Open(tt.openPath) + require.NoError(t, err) + defer f.Close() + + assert.Equal(t, tt.expectFile, f.Name()) + }) + } +} + +func TestChroot_NonExistentPath(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test chroot to non-existent directory - billy's chroot helper allows this + chrootFS, err := fs.Chroot("nonexistent") + require.NoError(t, err) + require.NotNil(t, chrootFS) + + // But accessing files within the non-existent chroot should fail + _, err = chrootFS.Open("anyfile.txt") + assert.Error(t, err) +} + +func TestChroot_Join(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test Join operation in chrooted filesystem + joined := chrootFS.Join("subdir", "nested.txt") + assert.Equal(t, "subdir/nested.txt", joined) + + // Test that joined path can be used to open file + f, err := chrootFS.Open(joined) + require.NoError(t, err) + defer f.Close() + + content, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, "Nested file content", string(content)) +} + +func TestChroot_UnsupportedOperations(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test that write operations still fail in chrooted embedfs + _, err = chrootFS.Create("newfile.txt") + require.ErrorIs(t, err, billy.ErrReadOnly) + + err = chrootFS.Remove("file1.txt") + require.ErrorIs(t, err, billy.ErrReadOnly) + + err = chrootFS.Rename("file1.txt", "renamed.txt") + require.ErrorIs(t, err, billy.ErrReadOnly) + + err = chrootFS.MkdirAll("newdir", 0755) + require.ErrorIs(t, err, billy.ErrReadOnly) +} + +func TestChroot_NestedChroot(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test creating nested chrootfs + firstChroot, err := fs.Chroot("testdata") + require.NoError(t, err) + + secondChroot, err := firstChroot.Chroot("subdir") + require.NoError(t, err) + + // Test that nested chroot works correctly + f, err := secondChroot.Open("nested.txt") + require.NoError(t, err) + defer f.Close() + + content, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, "Nested file content", string(content)) + + // Test that we can't access parent directory from nested chroot + entries, err := secondChroot.ReadDir("/") + require.NoError(t, err) + assert.Len(t, entries, 1) + assert.Equal(t, "nested.txt", entries[0].Name()) +} + +func TestChroot_FileOperations(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test file operations in chrooted filesystem + f, err := chrootFS.Open("file2.txt") + require.NoError(t, err) + defer f.Close() + + // Test Read + buf := make([]byte, 10) + n, err := f.Read(buf) + require.NoError(t, err) + assert.Equal(t, "Another te", string(buf[:n])) + + // Test Seek + _, err = f.Seek(0, io.SeekStart) + require.NoError(t, err) + + // Test ReadAt + buf2 := make([]byte, 7) + n, err = f.ReadAt(buf2, 8) + require.NoError(t, err) + assert.Equal(t, "test fi", string(buf2[:n])) + + // Test that file position wasn't affected by ReadAt + n, err = f.Read(buf) + require.NoError(t, err) + assert.Equal(t, "Another te", string(buf[:n])) +} + +func TestChroot_Lstat(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + chrootFS, err := fs.Chroot("testdata") + require.NoError(t, err) + + // Test Lstat in chrooted filesystem (should behave same as Stat for embedfs) + fi, err := chrootFS.Lstat("file1.txt") + require.NoError(t, err) + assert.Equal(t, "file1.txt", fi.Name()) + assert.False(t, fi.IsDir()) +} diff --git a/embedfs/embed.go b/embedfs/embed.go index d0b7c8c..80c896f 100644 --- a/embedfs/embed.go +++ b/embedfs/embed.go @@ -13,7 +13,7 @@ import ( "sync" "github.com/go-git/go-billy/v6" - "github.com/go-git/go-billy/v6/memfs" + "github.com/go-git/go-billy/v6/helper/chroot" ) type Embed struct { @@ -29,7 +29,20 @@ func New(efs *embed.FS) billy.Filesystem { fs.underlying = &embed.FS{} } - return fs + return chroot.New(fs, "/") +} + +// normalizePath converts billy's absolute paths to embed.FS relative paths +func (fs *Embed) normalizePath(path string) string { + // embed.FS uses "." for root directory, but billy uses "/" + if path == "/" { + return "." + } + // Remove leading slash for embed.FS + if strings.HasPrefix(path, "/") { + return path[1:] + } + return path } func (fs *Embed) Root() string { @@ -37,6 +50,8 @@ func (fs *Embed) Root() string { } func (fs *Embed) Stat(filename string) (os.FileInfo, error) { + filename = fs.normalizePath(filename) + f, err := fs.underlying.Open(filename) if err != nil { return nil, err @@ -53,6 +68,7 @@ func (fs *Embed) OpenFile(filename string, flag int, _ os.FileMode) (billy.File, return nil, billy.ErrReadOnly } + filename = fs.normalizePath(filename) f, err := fs.underlying.Open(filename) if err != nil { return nil, err @@ -90,7 +106,15 @@ func (fs *Embed) Join(elem ...string) string { return "" } +type ByName []os.FileInfo + +func (a ByName) Len() int { return len(a) } +func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() } +func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + func (fs *Embed) ReadDir(path string) ([]os.FileInfo, error) { + path = fs.normalizePath(path) + e, err := fs.underlying.ReadDir(path) if err != nil { return nil, err @@ -102,23 +126,14 @@ func (fs *Embed) ReadDir(path string) ([]os.FileInfo, error) { entries = append(entries, fi) } - sort.Sort(memfs.ByName(entries)) + sort.Sort(ByName(entries)) return entries, nil } -// Chroot is not supported. -// -// Calls will always return billy.ErrNotSupported. -func (fs *Embed) Chroot(_ string) (billy.Filesystem, error) { - return nil, billy.ErrNotSupported -} - -// Lstat is not supported. -// -// Calls will always return billy.ErrNotSupported. -func (fs *Embed) Lstat(_ string) (os.FileInfo, error) { - return nil, billy.ErrNotSupported +// Lstat behaves the same as Stat for embedded filesystems since embed.FS does not support symlinks. +func (fs *Embed) Lstat(filename string) (os.FileInfo, error) { + return fs.Stat(filename) } // Readlink is not supported. diff --git a/embedfs/embed_test.go b/embedfs/embed_test.go index 2501afe..3b65465 100644 --- a/embedfs/embed_test.go +++ b/embedfs/embed_test.go @@ -8,18 +8,11 @@ import ( "testing" "github.com/go-git/go-billy/v6" + "github.com/go-git/go-billy/v6/embedfs/internal/testdata" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -//go:embed testdata/empty.txt -var singleFile embed.FS - -//go:embed testdata -var testdataDir embed.FS - -var empty embed.FS - func TestOpen(t *testing.T) { t.Parallel() @@ -29,12 +22,12 @@ func TestOpen(t *testing.T) { wantErr bool }{ { - name: "testdata/empty.txt", - want: []byte(""), + name: "testdata/file1.txt", + want: []byte("Hello from embedfs!"), }, { - name: "testdata/empty2.txt", - want: []byte("test"), + name: "testdata/file2.txt", + want: []byte("Another test file"), }, { name: "non-existent", @@ -44,7 +37,7 @@ func TestOpen(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - fs := New(&testdataDir) + fs := New(testdata.GetTestData()) var got []byte f, err := fs.Open(tc.name) @@ -74,48 +67,48 @@ func TestOpenFileFlags(t *testing.T) { }{ { name: "O_CREATE", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_CREATE, wantErr: "read-only filesystem", }, { name: "O_WRONLY", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_WRONLY, wantErr: "read-only filesystem", }, { name: "O_TRUNC", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_TRUNC, wantErr: "read-only filesystem", }, { name: "O_RDWR", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_RDWR, wantErr: "read-only filesystem", }, { name: "O_EXCL", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_EXCL, wantErr: "read-only filesystem", }, { name: "O_RDONLY", - file: "testdata/empty.txt", + file: "testdata/file1.txt", flag: os.O_RDONLY, }, { name: "no flags", - file: "testdata/empty.txt", + file: "testdata/file1.txt", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - fs := New(&testdataDir) + fs := New(testdata.GetTestData()) _, err := fs.OpenFile(tc.file, tc.flag, 0o700) if tc.wantErr != "" { @@ -137,12 +130,12 @@ func TestStat(t *testing.T) { wantErr bool }{ { - name: "testdata/empty.txt", - want: "empty.txt", + name: "testdata/file1.txt", + want: "file1.txt", }, { - name: "testdata/empty2.txt", - want: "empty2.txt", + name: "testdata/file2.txt", + want: "file2.txt", }, { name: "non-existent", @@ -157,7 +150,7 @@ func TestStat(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - fs := New(&testdataDir) + fs := New(testdata.GetTestData()) fi, err := fs.Stat(tc.name) if tc.wantErr { @@ -184,30 +177,29 @@ func TestReadDir(t *testing.T) { wantErr bool }{ { - name: "singleFile", + name: "testdata", path: "testdata", - fs: &singleFile, - want: []string{"empty.txt"}, + fs: testdata.GetTestData(), + want: []string{"empty.txt", "file1.txt", "file2.txt", "subdir"}, }, { - name: "empty", + name: "empty path", path: "", - fs: &empty, - want: []string{}, - wantErr: true, + fs: testdata.GetTestData(), + want: []string{"testdata"}, + wantErr: false, }, { - name: "testdataDir w/ path", - path: "testdata", - fs: &testdataDir, - want: []string{"empty.txt", "empty2.txt"}, + name: "root path", + path: "/", + fs: testdata.GetTestData(), + want: []string{"testdata"}, }, { - name: "testdataDir return no dir names", - path: "", - fs: &testdataDir, - want: []string{}, - wantErr: true, + name: "nested directory", + path: "testdata/subdir", + fs: testdata.GetTestData(), + want: []string{"nested.txt"}, }, } @@ -225,9 +217,14 @@ func TestReadDir(t *testing.T) { assert.Len(t, fis, len(tc.want)) matched := 0 - for _, n := range fis { - for _, w := range tc.want { - if n.Name() == w { + for _, fi := range fis { + // Verify all entries have proper FileInfo + assert.NotEmpty(t, fi.Name()) + assert.NotNil(t, fi.ModTime()) + assert.Greater(t, fi.Size(), int64(-1)) // Size can be 0 but not negative + + for _, expected := range tc.want { + if fi.Name() == expected { matched++ } } @@ -241,7 +238,7 @@ func TestReadDir(t *testing.T) { func TestUnsupported(t *testing.T) { t.Parallel() - fs := New(&testdataDir) + fs := New(testdata.GetTestData()) _, err := fs.Create("test") require.ErrorIs(t, err, billy.ErrReadOnly) @@ -259,9 +256,9 @@ func TestUnsupported(t *testing.T) { func TestFileUnsupported(t *testing.T) { t.Parallel() - fs := New(&testdataDir) + fs := New(testdata.GetTestData()) - f, err := fs.Open("testdata/empty.txt") + f, err := fs.Open("testdata/file1.txt") require.NoError(t, err) assert.NotNil(t, f) @@ -273,9 +270,11 @@ func TestFileUnsupported(t *testing.T) { } func TestFileSeek(t *testing.T) { - fs := New(&testdataDir) + t.Parallel() - f, err := fs.Open("testdata/empty2.txt") + fs := New(testdata.GetTestData()) + + f, err := fs.Open("testdata/file2.txt") require.NoError(t, err) assert.NotNil(t, f) @@ -284,14 +283,14 @@ func TestFileSeek(t *testing.T) { seekWhence int want string }{ - {seekOff: 3, seekWhence: io.SeekStart, want: ""}, - {seekOff: 3, seekWhence: io.SeekStart, want: "t"}, - {seekOff: 2, seekWhence: io.SeekStart, want: "st"}, - {seekOff: 1, seekWhence: io.SeekStart, want: "est"}, - {seekOff: 0, seekWhence: io.SeekStart, want: "test"}, - {seekOff: 0, seekWhence: io.SeekStart, want: "t"}, - {seekOff: 1, seekWhence: io.SeekCurrent, want: "s"}, - {seekOff: -2, seekWhence: io.SeekEnd, want: "st"}, + {seekOff: 8, seekWhence: io.SeekStart, want: "test file"}, // pos now at 17 + {seekOff: 8, seekWhence: io.SeekStart, want: "t"}, // pos now at 9 + {seekOff: 9, seekWhence: io.SeekStart, want: "est"}, // pos now at 12 + {seekOff: 1, seekWhence: io.SeekStart, want: "nother test file"}, // pos now at 17 + {seekOff: 0, seekWhence: io.SeekStart, want: "Another test file"}, // pos now at 17 + {seekOff: 0, seekWhence: io.SeekStart, want: "A"}, // pos now at 1 + {seekOff: 0, seekWhence: io.SeekCurrent, want: "n"}, // pos now at 2 + {seekOff: -4, seekWhence: io.SeekEnd, want: "file"}, // pos now at 17 } for i, tc := range tests { @@ -338,10 +337,235 @@ func TestJoin(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - fs := New(&empty) + fs := New(testdata.GetTestData()) got := fs.Join(tc.path...) assert.Equal(t, tc.want, got) }) } } + +func TestComprehensiveOpen(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test opening existing embedded file with content + f, err := fs.Open("/testdata/file1.txt") + require.NoError(t, err) + assert.Equal(t, "testdata/file1.txt", f.Name()) + require.NoError(t, f.Close()) +} + +func TestComprehensiveRead(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + f, err := fs.Open("/testdata/file1.txt") + require.NoError(t, err) + defer f.Close() + + // Read the actual content + buf := make([]byte, 100) + n, err := f.Read(buf) + require.NoError(t, err) + assert.Equal(t, "Hello from embedfs!", string(buf[:n])) +} + +func TestNestedFileOperations(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test nested file read + f, err := fs.Open("/testdata/subdir/nested.txt") + require.NoError(t, err) + defer f.Close() + + buf := make([]byte, 100) + n, err := f.Read(buf) + require.NoError(t, err) + assert.Equal(t, "Nested file content", string(buf[:n])) +} + +func TestPathNormalization(t *testing.T) { + t.Parallel() + + fs := &Embed{underlying: testdata.GetTestData()} + + // Test that our path normalization works correctly + tests := []struct { + name string + input string + expected string + }{ + {"root", "/", "."}, + {"top-level", "/testdata", "testdata"}, + {"nested", "/testdata/subdir", "testdata/subdir"}, + {"deep file", "/testdata/subdir/nested.txt", "testdata/subdir/nested.txt"}, + {"relative path", "testdata", "testdata"}, + {"empty path", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := fs.normalizePath(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFile_ReadAt(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + f, err := fs.Open("/testdata/file1.txt") + require.NoError(t, err) + defer f.Close() + + // Test ReadAt without affecting file position + tests := []struct { + name string + offset int64 + length int + want string + }{ + {"beginning", 0, 5, "Hello"}, + {"middle", 6, 4, "from"}, + {"end", 15, 4, "dfs!"}, + {"full content", 0, 19, "Hello from embedfs!"}, + {"beyond end", 100, 10, ""}, // Should return EOF + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.length) + n, err := f.ReadAt(buf, tt.offset) + + if tt.offset >= 19 { // Beyond file size + require.Error(t, err) + assert.Equal(t, 0, n) + } else { + if tt.offset+int64(tt.length) > 19 { + // Partial read at end of file + require.Error(t, err) // Should be EOF + assert.Greater(t, n, 0) + assert.Equal(t, tt.want, string(buf[:n])) + } else { + require.NoError(t, err) + assert.Equal(t, tt.length, n) + assert.Equal(t, tt.want, string(buf[:n])) + } + } + }) + } + + // Verify ReadAt doesn't change file position + pos, err := f.Seek(0, 1) // Get current position + require.NoError(t, err) + assert.Equal(t, int64(0), pos, "ReadAt should not change file position") +} + +func TestFile_Close(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + f, err := fs.Open("/testdata/file1.txt") + require.NoError(t, err) + + // Test first close + err = f.Close() + require.NoError(t, err) + + // Test multiple closes (should be safe) + err = f.Close() + require.NoError(t, err) + + err = f.Close() + require.NoError(t, err) + + // Note: embedfs doesn't necessarily fail operations after close + // since embed.FS files remain readable. This tests that Close() works + // without error, but doesn't enforce post-close failure behavior. +} + +func TestFile_LockUnlock(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + f, err := fs.Open("/testdata/file1.txt") + require.NoError(t, err) + defer f.Close() + + // Lock/Unlock should be no-ops that don't error + err = f.Lock() + require.NoError(t, err) + + err = f.Unlock() + require.NoError(t, err) + + // Multiple lock/unlock sequences should work + err = f.Lock() + require.NoError(t, err) + err = f.Lock() + require.NoError(t, err) + err = f.Unlock() + require.NoError(t, err) + err = f.Unlock() + require.NoError(t, err) +} + +func TestReadDirDirectories(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + entries, err := fs.ReadDir("/testdata") + require.NoError(t, err) + + // Find the subdirectory entry + var subdirEntry os.FileInfo + for _, entry := range entries { + if entry.Name() == "subdir" { + subdirEntry = entry + break + } + } + + require.NotNil(t, subdirEntry, "subdir should be found") + assert.True(t, subdirEntry.IsDir(), "subdir should be a directory") + assert.Equal(t, "subdir", subdirEntry.Name()) +} + +func TestEmptyFileHandling(t *testing.T) { + t.Parallel() + + fs := New(testdata.GetTestData()) + + // Test empty file stat + fi, err := fs.Stat("/testdata/empty.txt") + require.NoError(t, err) + assert.Equal(t, "empty.txt", fi.Name()) + assert.False(t, fi.IsDir()) + assert.Equal(t, int64(0), fi.Size()) + + // Test opening empty file + f, err := fs.Open("/testdata/empty.txt") + require.NoError(t, err) + defer f.Close() + + // Test reading from empty file + buf := make([]byte, 10) + n, err := f.Read(buf) + require.Error(t, err) // Should be EOF + assert.Equal(t, 0, n) + + // Test ReadAt on empty file + n, err = f.ReadAt(buf, 0) + require.Error(t, err) // Should be EOF + assert.Equal(t, 0, n) +} diff --git a/embedfs/internal/testdata/provider.go b/embedfs/internal/testdata/provider.go new file mode 100644 index 0000000..5209e44 --- /dev/null +++ b/embedfs/internal/testdata/provider.go @@ -0,0 +1,16 @@ +// Package testdata provides embedded test data for billy embedfs testing. +// This package is only imported by test code and won't be included in production builds. +package testdata + +import ( + "embed" +) + +//go:embed testdata +var TestData embed.FS + +// GetTestData returns the raw embed.FS for tests to wrap with their own embedfs.New(). +// This avoids import cycles while providing embedded test data. +func GetTestData() *embed.FS { + return &TestData +} diff --git a/embedfs/testdata/empty.txt b/embedfs/internal/testdata/testdata/empty.txt similarity index 100% rename from embedfs/testdata/empty.txt rename to embedfs/internal/testdata/testdata/empty.txt diff --git a/embedfs/internal/testdata/testdata/file1.txt b/embedfs/internal/testdata/testdata/file1.txt new file mode 100644 index 0000000..7f71942 --- /dev/null +++ b/embedfs/internal/testdata/testdata/file1.txt @@ -0,0 +1 @@ +Hello from embedfs! \ No newline at end of file diff --git a/embedfs/internal/testdata/testdata/file2.txt b/embedfs/internal/testdata/testdata/file2.txt new file mode 100644 index 0000000..b2679d1 --- /dev/null +++ b/embedfs/internal/testdata/testdata/file2.txt @@ -0,0 +1 @@ +Another test file \ No newline at end of file diff --git a/embedfs/internal/testdata/testdata/subdir/nested.txt b/embedfs/internal/testdata/testdata/subdir/nested.txt new file mode 100644 index 0000000..276dd4b --- /dev/null +++ b/embedfs/internal/testdata/testdata/subdir/nested.txt @@ -0,0 +1 @@ +Nested file content \ No newline at end of file diff --git a/embedfs/testdata/empty2.txt b/embedfs/testdata/empty2.txt deleted file mode 100644 index 30d74d2..0000000 --- a/embedfs/testdata/empty2.txt +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file diff --git a/go.mod b/go.mod index 9acc289..9944d4d 100644 --- a/go.mod +++ b/go.mod @@ -14,3 +14,4 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + diff --git a/go.sum b/go.sum index 871e9f4..1c7d51e 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22r github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-git/go-billy/v6 v6.0.0-20250711053805-c1f149aaab07/go.mod h1:Pa0/zeE0tC0GiZLFFtOYXOky9SgpNF+zkrj7aEJhBVg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=