feat(bsr/fstest): Add localfs implementation
This adds another test storage.FS implemenation. This can be used to write/read containers and files from a local filesystem via calls to the os pacakge.
This commit is contained in:
3 files changed
+649
-151
No files matched your search
@@ -7,6 +7,7 @@ package fstest
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -17,6 +18,14 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
)
|
||||
|
||||
// Common erorrs
|
||||
var (
|
||||
ErrClosed = errors.New("closed")
|
||||
ErrAlreadyExists = errors.New("already exists")
|
||||
ErrDoesNotExist = errors.New("does not exist")
|
||||
ErrReadOnly = errors.New("read-only")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultContainerPerm = 0o775
|
||||
defaultFilePerm = 0o644
|
||||
@@ -48,14 +57,14 @@ func (m *MemFS) New(ctx context.Context, n string) (storage.Container, error) {
|
||||
}
|
||||
|
||||
if m.readOnly {
|
||||
return nil, fmt.Errorf("cannot create new container from read-only fs")
|
||||
return nil, fmt.Errorf("cannot create new container from read-only fs: %w", ErrReadOnly)
|
||||
}
|
||||
|
||||
if m.Containers == nil {
|
||||
m.Containers = make(map[string]*MemContainer)
|
||||
}
|
||||
if _, exists := m.Containers[n]; exists {
|
||||
return nil, fmt.Errorf("container %s already exists", n)
|
||||
return nil, fmt.Errorf("container %s already exists: %w", n, ErrAlreadyExists)
|
||||
}
|
||||
|
||||
c := &MemContainer{
|
||||
@@ -72,12 +81,12 @@ func (m *MemFS) New(ctx context.Context, n string) (storage.Container, error) {
|
||||
// Open opens an existing a storage.Container from the MemFS.
|
||||
func (m *MemFS) Open(_ context.Context, n string) (storage.Container, error) {
|
||||
if m.Containers == nil {
|
||||
return nil, fmt.Errorf("container %s not found", n)
|
||||
return nil, fmt.Errorf("container %s not found: %w", n, ErrDoesNotExist)
|
||||
}
|
||||
|
||||
c, ok := m.Containers[n]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("container %s not found", n)
|
||||
return nil, fmt.Errorf("container %s not found: %w", n, ErrDoesNotExist)
|
||||
}
|
||||
c.Closed = false
|
||||
return c, nil
|
||||
@@ -109,7 +118,7 @@ func (m *MemContainer) Close() error {
|
||||
// Create makes a new storage.File in the container.
|
||||
func (m *MemContainer) Create(ctx context.Context, n string) (storage.File, error) {
|
||||
if m.Closed {
|
||||
return nil, fmt.Errorf("create on closed container")
|
||||
return nil, fmt.Errorf("create on closed container: %w", ErrClosed)
|
||||
}
|
||||
return m.OpenFile(ctx, n, storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
}
|
||||
@@ -121,16 +130,16 @@ func (m *MemContainer) OpenFile(_ context.Context, n string, option ...storage.O
|
||||
defer m.Unlock()
|
||||
|
||||
if m.Closed {
|
||||
return nil, fmt.Errorf("create on closed container")
|
||||
return nil, fmt.Errorf("create on closed container: %w", ErrClosed)
|
||||
}
|
||||
opts := storage.GetOpts(option...)
|
||||
|
||||
if m.accessMode == storage.ReadOnly && opts.WithFileAccessMode != storage.ReadOnly {
|
||||
return nil, fmt.Errorf("cannot create writeable file in readonly container")
|
||||
return nil, fmt.Errorf("cannot create writeable file in readonly container: %w", ErrReadOnly)
|
||||
}
|
||||
|
||||
if opts.WithCreateFile && opts.WithFileAccessMode == storage.ReadOnly {
|
||||
return nil, fmt.Errorf("cannot create file in read-only mode")
|
||||
return nil, fmt.Errorf("cannot create file in read-only mode: %w", ErrReadOnly)
|
||||
}
|
||||
|
||||
var f *MemFile
|
||||
@@ -144,7 +153,7 @@ func (m *MemContainer) OpenFile(_ context.Context, n string, option ...storage.O
|
||||
var ok bool
|
||||
f, ok = m.Files[n]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %s does not exist", n)
|
||||
return nil, fmt.Errorf("file %s does not exist: %w", n, ErrDoesNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,19 +171,19 @@ func (m *MemContainer) SubContainer(_ context.Context, n string, option ...stora
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.Closed {
|
||||
return nil, fmt.Errorf("subcontainer on closed container")
|
||||
return nil, fmt.Errorf("subcontainer on closed container: %w", ErrClosed)
|
||||
}
|
||||
opts := storage.GetOpts(option...)
|
||||
|
||||
c, exists := m.Sub[n]
|
||||
|
||||
if opts.WithCreateFile && opts.WithFileAccessMode == storage.ReadOnly {
|
||||
return nil, fmt.Errorf("cannot create container in read-only mode")
|
||||
return nil, fmt.Errorf("cannot create container in read-only mode: %w", ErrReadOnly)
|
||||
}
|
||||
|
||||
if opts.WithCreateFile {
|
||||
if exists {
|
||||
return nil, fmt.Errorf("container %s already exists", n)
|
||||
return nil, fmt.Errorf("container %s already exists: %w", n, ErrAlreadyExists)
|
||||
}
|
||||
c = &MemContainer{
|
||||
Name: n,
|
||||
@@ -183,7 +192,7 @@ func (m *MemContainer) SubContainer(_ context.Context, n string, option ...stora
|
||||
}
|
||||
} else {
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("container %s does not exist", n)
|
||||
return nil, fmt.Errorf("container %s does not exist: %w", n, ErrDoesNotExist)
|
||||
}
|
||||
}
|
||||
c.accessMode = opts.WithFileAccessMode
|
||||
|
||||
@@ -6,7 +6,7 @@ package fstest_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
|
||||
@@ -14,45 +14,31 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test options that only MemFS supports
|
||||
func TestMemFSNew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
errCustom := errors.New("custom error from new")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
m *fstest.MemFS
|
||||
n string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"default",
|
||||
fstest.NewMemFS(),
|
||||
"test",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"read-only",
|
||||
fstest.NewMemFS(fstest.WithReadOnly(true)),
|
||||
"test",
|
||||
errors.New("cannot create new container from read-only fs"),
|
||||
fstest.ErrReadOnly,
|
||||
},
|
||||
{
|
||||
"custom-new-func",
|
||||
fstest.NewMemFS(fstest.WithNewFunc(func(_ context.Context, _ string) (storage.Container, error) {
|
||||
return nil, fmt.Errorf("custom error from new")
|
||||
return nil, errCustom
|
||||
})),
|
||||
"test",
|
||||
errors.New("custom error from new"),
|
||||
},
|
||||
{
|
||||
"already-exists",
|
||||
func() *fstest.MemFS {
|
||||
f := fstest.NewMemFS()
|
||||
_, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}(),
|
||||
"test",
|
||||
errors.New("container test already exists"),
|
||||
errCustom,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,7 +46,7 @@ func TestMemFSNew(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, err := tc.m.New(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
@@ -69,184 +55,317 @@ func TestMemFSNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemFSOpen(t *testing.T) {
|
||||
func TestFSNew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fsCases := []struct {
|
||||
name string
|
||||
newFs func(t *testing.T) storage.FS
|
||||
}{
|
||||
{
|
||||
"MemFS",
|
||||
func(t *testing.T) storage.FS { return fstest.NewMemFS() },
|
||||
},
|
||||
{
|
||||
"LocalFS",
|
||||
func(t *testing.T) storage.FS {
|
||||
d, err := os.MkdirTemp("", "localfs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return fstest.NewLocalFS(ctx, d)
|
||||
},
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
m *fstest.MemFS
|
||||
n string
|
||||
setupFn func(t *testing.T, fs storage.FS)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"default",
|
||||
"test",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"already-exists",
|
||||
"test",
|
||||
func(t *testing.T, fs storage.FS) {
|
||||
_, err := fs.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
},
|
||||
fstest.ErrAlreadyExists,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tfs := range fsCases {
|
||||
t.Run(tfs.name, func(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := tfs.newFs(t)
|
||||
if tc.setupFn != nil {
|
||||
tc.setupFn(t, f)
|
||||
}
|
||||
c, err := f.New(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSOpen(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fsCases := []struct {
|
||||
name string
|
||||
newFs func(t *testing.T) storage.FS
|
||||
}{
|
||||
{
|
||||
"MemFS",
|
||||
func(t *testing.T) storage.FS { return fstest.NewMemFS() },
|
||||
},
|
||||
{
|
||||
"LocalFS",
|
||||
func(t *testing.T) storage.FS {
|
||||
d, err := os.MkdirTemp("", "localfs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return fstest.NewLocalFS(ctx, d)
|
||||
},
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
setupFn func(t *testing.T, fs storage.FS)
|
||||
n string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"exists",
|
||||
func() *fstest.MemFS {
|
||||
f := fstest.NewMemFS()
|
||||
_, err := f.New(ctx, "test")
|
||||
func(t *testing.T, fs storage.FS) {
|
||||
_, err := fs.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}(),
|
||||
},
|
||||
"test",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"does-not-exist",
|
||||
fstest.NewMemFS(),
|
||||
nil,
|
||||
"test",
|
||||
errors.New("container test not found"),
|
||||
fstest.ErrDoesNotExist,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, err := tc.m.Open(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
for _, tfs := range fsCases {
|
||||
t.Run(tfs.name, func(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := tfs.newFs(t)
|
||||
if tc.setupFn != nil {
|
||||
tc.setupFn(t, f)
|
||||
}
|
||||
c, err := f.Open(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemContainerOpenFile(t *testing.T) {
|
||||
func TestContainerOpenFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fsCases := []struct {
|
||||
name string
|
||||
newFs func(t *testing.T) storage.FS
|
||||
}{
|
||||
{
|
||||
"MemFS",
|
||||
func(t *testing.T) storage.FS { return fstest.NewMemFS() },
|
||||
},
|
||||
{
|
||||
"LocalFS",
|
||||
func(t *testing.T) storage.FS {
|
||||
d, err := os.MkdirTemp("", "localfs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return fstest.NewLocalFS(ctx, d)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
c *fstest.MemContainer
|
||||
setupFn func(t *testing.T, f storage.FS) storage.Container
|
||||
n string
|
||||
opts []storage.Option
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"create",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"create-already-exists",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
_, err = cc.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
_, err = c.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
require.NoError(t, err)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"create-read-only",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
errors.New("cannot create file in read-only mode"),
|
||||
fstest.ErrReadOnly,
|
||||
},
|
||||
{
|
||||
"read-only-does-not-exist",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
errors.New("file test does not exist"),
|
||||
fstest.ErrDoesNotExist,
|
||||
},
|
||||
{
|
||||
"read-only-exist",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
_, err = cc.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
_, err = c.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
require.NoError(t, err)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, err := tc.c.OpenFile(ctx, tc.n, tc.opts...)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
for _, tfs := range fsCases {
|
||||
t.Run(tfs.name, func(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fs := tfs.newFs(t)
|
||||
c := tc.setupFn(t, fs)
|
||||
f, err := c.OpenFile(ctx, tc.n, tc.opts...)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, f)
|
||||
})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemContainerCreate(t *testing.T) {
|
||||
func TestContainerCreate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fsCases := []struct {
|
||||
name string
|
||||
newFs func(t *testing.T) storage.FS
|
||||
}{
|
||||
{
|
||||
"MemFS",
|
||||
func(t *testing.T) storage.FS { return fstest.NewMemFS() },
|
||||
},
|
||||
{
|
||||
"LocalFS",
|
||||
func(t *testing.T) storage.FS {
|
||||
d, err := os.MkdirTemp("", "localfs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return fstest.NewLocalFS(ctx, d)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
c *fstest.MemContainer
|
||||
setupFn func(t *testing.T, f storage.FS) storage.Container
|
||||
n string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"create",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"create-already-exists",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
_, err = cc.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
_, err = c.OpenFile(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
require.NoError(t, err)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, err := tc.c.Create(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
for _, tfs := range fsCases {
|
||||
t.Run(tfs.name, func(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fs := tfs.newFs(t)
|
||||
c := tc.setupFn(t, fs)
|
||||
f, err := c.Create(ctx, tc.n)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, f)
|
||||
})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -254,92 +373,108 @@ func TestMemContainerCreate(t *testing.T) {
|
||||
func TestMemContainerSubContainer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fsCases := []struct {
|
||||
name string
|
||||
newFs func(t *testing.T) storage.FS
|
||||
}{
|
||||
{
|
||||
"MemFS",
|
||||
func(t *testing.T) storage.FS { return fstest.NewMemFS() },
|
||||
},
|
||||
{
|
||||
"LocalFS",
|
||||
func(t *testing.T) storage.FS {
|
||||
d, err := os.MkdirTemp("", "localfs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return fstest.NewLocalFS(ctx, d)
|
||||
},
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
c *fstest.MemContainer
|
||||
setupFn func(t *testing.T, f storage.FS) storage.Container
|
||||
n string
|
||||
opts []storage.Option
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"create",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"create-already-exists",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
_, err = cc.SubContainer(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
_, err = c.SubContainer(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
require.NoError(t, err)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)},
|
||||
errors.New("container test already exists"),
|
||||
fstest.ErrAlreadyExists,
|
||||
},
|
||||
{
|
||||
"create-read-only",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
errors.New("cannot create container in read-only mode"),
|
||||
fstest.ErrReadOnly,
|
||||
},
|
||||
{
|
||||
"read-only-does-not-exist",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
errors.New("container test does not exist"),
|
||||
fstest.ErrDoesNotExist,
|
||||
},
|
||||
{
|
||||
"read-only-exist",
|
||||
func() *fstest.MemContainer {
|
||||
f := fstest.NewMemFS()
|
||||
func(t *testing.T, f storage.FS) storage.Container {
|
||||
c, err := f.New(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
cc := c.(*fstest.MemContainer)
|
||||
_, err = cc.SubContainer(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
_, err = c.SubContainer(ctx, "test", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
require.NoError(t, err)
|
||||
return cc
|
||||
}(),
|
||||
return c
|
||||
},
|
||||
"test",
|
||||
[]storage.Option{storage.WithFileAccessMode(storage.ReadOnly)},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, err := tc.c.SubContainer(ctx, tc.n, tc.opts...)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
for _, tfs := range fsCases {
|
||||
t.Run(tfs.name, func(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fs := tfs.newFs(t)
|
||||
c := tc.setupFn(t, fs)
|
||||
sc, err := c.SubContainer(ctx, tc.n, tc.opts...)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sc)
|
||||
})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package fstest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
)
|
||||
|
||||
// LocalFS is a test filesystem that creates files in the provided directory.
|
||||
type LocalFS struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// NewLocalFS creates a LocalFS.
|
||||
func NewLocalFS(_ context.Context, p string) *LocalFS {
|
||||
return &LocalFS{
|
||||
Path: p,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates the named root container within the local FS.
|
||||
// Calls to New make use of the os.Mkdir locking and error handling
|
||||
// to ensure that the directory being made is a valid directory and does not
|
||||
// exist.
|
||||
func (fs *LocalFS) New(_ context.Context, name string) (storage.Container, error) {
|
||||
const op = "fstest.(LocalFS).New"
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%s: missing name", op)
|
||||
}
|
||||
path, err := joinPath(fs.Path, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path, defaultContainerPerm); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrAlreadyExists)
|
||||
}
|
||||
|
||||
return &LocalContainer{
|
||||
Path: path,
|
||||
IsRoot: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Open opens an existing root container.
|
||||
func (fs *LocalFS) Open(_ context.Context, name string) (storage.Container, error) {
|
||||
const op = "fstest.(LocalFS).Open"
|
||||
path, err := joinPath(fs.Path, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrDoesNotExist)
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrDoesNotExist)
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return nil, fmt.Errorf("%s not a dir: %w", name, ErrDoesNotExist)
|
||||
}
|
||||
|
||||
return &LocalContainer{
|
||||
Path: path,
|
||||
IsRoot: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// joinPath joins a parent path with the single name child container or file.
|
||||
// The name cannot contain any path seperators.
|
||||
// The name cannot contain `..`
|
||||
// The subpath returned will be a child of the parent.
|
||||
func joinPath(parentPath, name string) (string, error) {
|
||||
// Check if name has any OS path separator and fail if it does
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
||||
return "", fmt.Errorf("name contains path separator")
|
||||
}
|
||||
|
||||
subPath := filepath.Join(parentPath, name)
|
||||
if len(parentPath) >= len(subPath) {
|
||||
return "", fmt.Errorf("new path must be longer than parent")
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(parentPath, subPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
return "", fmt.Errorf("subpath must be within current container")
|
||||
}
|
||||
|
||||
return subPath, nil
|
||||
}
|
||||
|
||||
// LocalContainer is a storage.Container backed by a os.File that is a directory.
|
||||
type LocalContainer struct {
|
||||
Path string
|
||||
closed bool
|
||||
IsRoot bool
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Sub returns a map of subcontainers in this container where the key is the name
|
||||
// of the container.
|
||||
func (c *LocalContainer) Sub(ctx context.Context) (map[string]*LocalContainer, error) {
|
||||
sub := make(map[string]*LocalContainer, 0)
|
||||
|
||||
err := filepath.WalkDir(c.Path, func(path string, d fs.DirEntry, _ error) error {
|
||||
if !d.IsDir() {
|
||||
// continue to next file if not a directory
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the root, continue the walk
|
||||
if path == c.Path {
|
||||
return nil
|
||||
}
|
||||
bn := filepath.Base(path)
|
||||
cc, err := c.SubContainer(ctx, bn, storage.WithFileAccessMode(storage.ReadWrite))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sub[bn] = cc.(*LocalContainer)
|
||||
// Only do one level, don't walk down into each directory.
|
||||
return filepath.SkipDir
|
||||
})
|
||||
return sub, err
|
||||
}
|
||||
|
||||
// Close closes the container.
|
||||
func (c *LocalContainer) Close() error {
|
||||
const op = "fstest.(LocalContainer).Close"
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.closed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create creates a new storage.File in the container.
|
||||
func (c *LocalContainer) Create(ctx context.Context, name string) (storage.File, error) {
|
||||
const op = "fstest.(LocalContainer).Create"
|
||||
file, err := c.OpenFile(ctx, name, storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// OpenFile opens a file in the container.
|
||||
// Supports the following options:
|
||||
// - WithCloseSyncMode: This is ignored by stored on the LocalContainer for test assertions.
|
||||
// - WithFileAccessMode: Sets the access mode for the file. If opened with Write, it will also be append.
|
||||
// - WithCreateFile: Creates the file if it does not exist. If the file does exists, it is truncated.
|
||||
func (c *LocalContainer) OpenFile(ctx context.Context, name string, options ...storage.Option) (storage.File, error) {
|
||||
const op = "fstest.(LocalContainer).OpenFile"
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.closed {
|
||||
return nil, fmt.Errorf("%s: container closed: %w", op, ErrClosed)
|
||||
}
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%s: missing name", op)
|
||||
}
|
||||
path, err := joinPath(c.Path, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
opts := storage.GetOpts(options...)
|
||||
|
||||
if opts.WithCreateFile && opts.WithFileAccessMode == storage.ReadOnly {
|
||||
return nil, fmt.Errorf("cannot create file in read-only mode: %w", ErrReadOnly)
|
||||
}
|
||||
|
||||
var fileFlag int
|
||||
switch opts.WithFileAccessMode {
|
||||
case storage.WriteOnly:
|
||||
fileFlag = os.O_WRONLY
|
||||
case storage.ReadWrite:
|
||||
fileFlag = os.O_RDWR
|
||||
default:
|
||||
fileFlag = os.O_RDONLY
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.WithCreateFile:
|
||||
fileFlag |= os.O_CREATE | os.O_TRUNC
|
||||
default:
|
||||
fileFlag |= os.O_APPEND
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, fileFlag, defaultFilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrDoesNotExist)
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil, fmt.Errorf("%s: file is a directory", op)
|
||||
}
|
||||
|
||||
return &LocalFile{
|
||||
ctx: ctx,
|
||||
File: file,
|
||||
Path: path,
|
||||
SyncMode: opts.WithCloseSyncMode,
|
||||
AccessMode: opts.WithFileAccessMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SubContainer creates or opens a container in this container.
|
||||
func (c *LocalContainer) SubContainer(_ context.Context, name string, options ...storage.Option) (storage.Container, error) {
|
||||
const op = "fstest.(LocalContainer).SubContainer"
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.closed {
|
||||
return nil, fmt.Errorf("%s: container closed: %w", op, ErrClosed)
|
||||
}
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%s: missing name", op)
|
||||
}
|
||||
opts := storage.GetOpts(options...)
|
||||
if opts.WithCreateFile && opts.WithFileAccessMode == storage.ReadOnly {
|
||||
return nil, fmt.Errorf("%s: cannot create container in read-only mode: %w", op, ErrReadOnly)
|
||||
}
|
||||
|
||||
path, err := joinPath(c.Path, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
if opts.WithCreateFile {
|
||||
if err := os.Mkdir(path, defaultContainerPerm); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrAlreadyExists)
|
||||
}
|
||||
} else {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrDoesNotExist)
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return nil, fmt.Errorf("%s: %w: %w", op, err, ErrDoesNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
return &LocalContainer{
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LocalFile is a storage.File backed by a os.File.
|
||||
type LocalFile struct {
|
||||
ctx context.Context
|
||||
|
||||
Path string
|
||||
File *os.File
|
||||
SyncMode storage.SyncMode
|
||||
AccessMode storage.AccessMode
|
||||
closed bool
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Stat returns the FileInfo structure describing file.
|
||||
func (f *LocalFile) Stat() (fs.FileInfo, error) {
|
||||
const op = "fstest.(LocalFile).Stat"
|
||||
f.RLock()
|
||||
defer f.RUnlock()
|
||||
if f.closed {
|
||||
return nil, fmt.Errorf("%s: file is closed", op)
|
||||
}
|
||||
|
||||
fi, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (f *LocalFile) Read(b []byte) (int, error) {
|
||||
const op = "fstest.(localFile).Read"
|
||||
f.RLock()
|
||||
defer f.RUnlock()
|
||||
if f.closed {
|
||||
return 0, fmt.Errorf("%s: file is closed", op)
|
||||
}
|
||||
if f.AccessMode == storage.WriteOnly {
|
||||
return 0, fmt.Errorf("%s: file is write-only", op)
|
||||
}
|
||||
|
||||
n, err := f.File.Read(b)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return n, err
|
||||
}
|
||||
return n, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Close closes the file preventing reads or writes.
|
||||
func (f *LocalFile) Close() error {
|
||||
const op = "fstest.(LocalFile).Stat"
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if f.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := f.File.Close(); err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
f.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *LocalFile) Write(b []byte) (int, error) {
|
||||
const op = "fstest.(localFile).Write"
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if f.closed {
|
||||
return 0, fmt.Errorf("%s: file is closed", op)
|
||||
}
|
||||
if f.AccessMode == storage.ReadOnly {
|
||||
return 0, fmt.Errorf("%s: file is read-only", op)
|
||||
}
|
||||
|
||||
n, err := f.File.Write(b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// WriteString writes a string to the file.
|
||||
func (f *LocalFile) WriteString(s string) (int, error) {
|
||||
const op = "storage.(localFile).WriteString"
|
||||
return f.Write([]byte(s))
|
||||
}
|
||||
Reference in new issue
Block a user