Skip to content

Commit 78800f3

Browse files
feat: add ReadFileAll FS utility function to read file fully. The functionality seems common enough to be extracted
1 parent e007f56 commit 78800f3

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

fsutil/fsutil.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Package fsutil provides utilities to work with filesystem.
2+
package fsutil
3+
4+
import (
5+
"fmt"
6+
"io"
7+
"io/fs"
8+
)
9+
10+
// ReadFileAll reads a file by given [path] from [fs]. Read file is buffered.
11+
func ReadFileAll(fs fs.FS, path string) ([]byte, error) {
12+
file, err := fs.Open(path)
13+
if err != nil {
14+
return []byte{}, fmt.Errorf("foundations/fsutil: failed opening file %q: %w", path, err)
15+
}
16+
17+
content, err := io.ReadAll(file)
18+
if err != nil {
19+
return []byte{}, fmt.Errorf("foundations/fsutil: failed to read file %q: %w", path, err)
20+
}
21+
22+
return content, nil
23+
}

fsutil/fsutil_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package fsutil
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"testing"
7+
"testing/fstest"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
var _ fs.File = (*errorReader)(nil)
13+
var _ fs.FS = (*errorFS)(nil)
14+
15+
// errorReader always returns an error on Read
16+
type errorReader struct {
17+
fs.File
18+
}
19+
20+
func (e *errorReader) Read([]byte) (int, error) {
21+
return 0, errors.New("failed read")
22+
}
23+
24+
// errorFS is a wrapper around that returns an errorReader for a specific file
25+
type errorFS struct {
26+
fs.FS
27+
errorFile string
28+
}
29+
30+
func (e *errorFS) Open(name string) (fs.File, error) {
31+
if name == e.errorFile {
32+
return &errorReader{}, nil
33+
}
34+
return e.FS.Open(name)
35+
}
36+
37+
func TestReadFileAll(t *testing.T) {
38+
t.Run("successfully reads existing file", func(t *testing.T) {
39+
fs := fstest.MapFS{
40+
"testfile.txt": &fstest.MapFile{Data: []byte("Hello, World!")},
41+
}
42+
43+
content, err := ReadFileAll(fs, "testfile.txt")
44+
45+
assert.NoError(t, err)
46+
assert.Equal(t, []byte("Hello, World!"), content)
47+
})
48+
49+
t.Run("returns error for non-existent file", func(t *testing.T) {
50+
fs := fstest.MapFS{}
51+
_, err := ReadFileAll(fs, "nonexistent.txt")
52+
53+
assert.Error(t, err)
54+
})
55+
56+
t.Run("returns error for directory", func(t *testing.T) {
57+
fs := fstest.MapFS{
58+
"testdir": &fstest.MapFile{Mode: fs.ModeDir},
59+
}
60+
_, err := ReadFileAll(fs, "testdir")
61+
62+
assert.Error(t, err)
63+
})
64+
65+
t.Run("handles empty file", func(t *testing.T) {
66+
fs := fstest.MapFS{
67+
"empty.txt": &fstest.MapFile{Data: []byte{}},
68+
}
69+
70+
content, err := ReadFileAll(fs, "empty.txt")
71+
72+
assert.NoError(t, err)
73+
assert.Empty(t, content)
74+
})
75+
76+
t.Run("handles large file", func(t *testing.T) {
77+
largeContent := make([]byte, 1024*1024) // 1MB
78+
for i := range largeContent {
79+
largeContent[i] = byte(i % 256)
80+
}
81+
82+
fs := fstest.MapFS{
83+
"large.bin": &fstest.MapFile{Data: largeContent},
84+
}
85+
86+
content, err := ReadFileAll(fs, "large.bin")
87+
88+
assert.NoError(t, err)
89+
assert.Equal(t, largeContent, content)
90+
})
91+
92+
t.Run("handles read errors", func(t *testing.T) {
93+
errorFS := &errorFS{
94+
FS: fstest.MapFS{
95+
"error.txt": &fstest.MapFile{Data: []byte("Some data")},
96+
},
97+
errorFile: "error.txt",
98+
}
99+
100+
_, err := ReadFileAll(errorFS, "error.txt")
101+
102+
assert.Error(t, err)
103+
})
104+
}

0 commit comments

Comments
 (0)