From 837734b74507902d64a6578563dbba8d8916da8a Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Fri, 9 Jun 2023 16:32:24 -0400 Subject: [PATCH] test(bsr/fstest): Add new FS for testing out of space errors (#3305) This provides a new storage.FS implementation that expands the on fstest.MemFS by allowing tests to mark the filesystem as out of space. When this happens any write/create operation will start returning an error indicating that the filesystem is out of space. --- internal/bsr/internal/fstest/fs_test.go | 79 ++++++++++++ internal/bsr/internal/fstest/space.go | 153 ++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 internal/bsr/internal/fstest/space.go diff --git a/internal/bsr/internal/fstest/fs_test.go b/internal/bsr/internal/fstest/fs_test.go index fa276ec78..9713f2f18 100644 --- a/internal/bsr/internal/fstest/fs_test.go +++ b/internal/bsr/internal/fstest/fs_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/bsr/internal/fstest" "github.com/hashicorp/boundary/internal/storage" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -144,6 +145,10 @@ func TestFSOpen(t *testing.T) { return fstest.NewLocalFS(ctx, d) }, }, + { + "LimitedSpaceFS", + func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() }, + }, } cases := []struct { name string @@ -210,6 +215,10 @@ func TestContainerOpenFile(t *testing.T) { return fstest.NewLocalFS(ctx, d) }, }, + { + "LimitedSpaceFS", + func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() }, + }, } cases := []struct { @@ -320,6 +329,10 @@ func TestContainerCreate(t *testing.T) { return fstest.NewLocalFS(ctx, d) }, }, + { + "LimitedSpaceFS", + func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() }, + }, } cases := []struct { @@ -392,6 +405,10 @@ func TestMemContainerSubContainer(t *testing.T) { return fstest.NewLocalFS(ctx, d) }, }, + { + "LimitedSpaceFS", + func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() }, + }, } cases := []struct { name string @@ -478,3 +495,65 @@ func TestMemContainerSubContainer(t *testing.T) { }) } } + +func TestOutOfSpace(t *testing.T) { + ctx := context.Background() + + line := []byte("foo") + + fs := fstest.NewLimitedSpaceFS() + + // Create some containers, subcontains and files. + r, err := fs.New(ctx, "root") + require.NoError(t, err) + + sub, err := r.SubContainer(ctx, "sub", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)) + require.NoError(t, err) + + f1, err := r.Create(ctx, "f1") + require.NoError(t, err) + c, err := f1.Write(line) + require.NoError(t, err) + assert.Equal(t, c, len(line)) + + f2, err := sub.Create(ctx, "f2") + require.NoError(t, err) + c, err = f2.Write(line) + require.NoError(t, err) + assert.Equal(t, c, len(line)) + + // Now mark out of space + fs.SetOutOfSpace(true) + + // eixsing files can no longer write + c, err = f1.Write(line) + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Equal(t, 0, c) + + c, err = f2.Write(line) + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Equal(t, 0, c) + + // existing containers cannot create new files + f3, err := r.Create(ctx, "f3") + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Nil(t, f3) + + f4, err := sub.Create(ctx, "f4") + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Nil(t, f4) + + // existing containers cannot create sub containers + sub2, err := r.SubContainer(ctx, "sub2", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)) + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Nil(t, sub2) + + sub3, err := sub.SubContainer(ctx, "sub3", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)) + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Nil(t, sub3) + + // fs cannot make a new container + r2, err := fs.New(ctx, "root2") + require.ErrorIs(t, err, fstest.ErrOutOfSpace) + assert.Nil(t, r2) +} diff --git a/internal/bsr/internal/fstest/space.go b/internal/bsr/internal/fstest/space.go new file mode 100644 index 000000000..b58cb054c --- /dev/null +++ b/internal/bsr/internal/fstest/space.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package fstest provides test implementations of the fs interfaces. +package fstest + +import ( + "context" + "errors" + "fmt" + sfs "io/fs" + "sync" + + "github.com/hashicorp/boundary/internal/storage" +) + +// ErrOutOfSpace is used when the FS is out of space. +var ErrOutOfSpace = errors.New("no space left on device") + +// LimitedSpaceFS is a test FS that can simulate running out of disk space. +type LimitedSpaceFS struct { + *MemFS + + outOfSpace bool + sync.RWMutex +} + +// SetOutOfSpace is a helper to mark the file system as being out of space. +func (l *LimitedSpaceFS) SetOutOfSpace(b bool) { + l.Lock() + defer l.Unlock() + l.outOfSpace = b +} + +// OutOfSpace is used to check if the filesystem is out of space in a concurrent safe way. +func (l *LimitedSpaceFS) OutOfSpace() bool { + l.RLock() + defer l.RUnlock() + return l.outOfSpace +} + +// NewLimitedSpaceFS creates a LimitedSpaceFS. It supports WithNewFunc, WithReadOnly. +func NewLimitedSpaceFS(options ...Option) *LimitedSpaceFS { + return &LimitedSpaceFS{ + MemFS: NewMemFS(options...), + } +} + +// New creates a storage.Container in the LimitedSpaceFS. +func (l *LimitedSpaceFS) New(ctx context.Context, n string) (storage.Container, error) { + if l.OutOfSpace() { + return nil, ErrOutOfSpace + } + c, err := l.MemFS.New(ctx, n) + if err != nil { + return nil, err + } + + cc := c.(*MemContainer) + return &LimitedSpaceContainer{ + MemContainer: cc, + fs: l, + }, nil +} + +// Open opens an existing a storage.Container from the LimitedSpaceFS. +func (l *LimitedSpaceFS) Open(ctx context.Context, n string) (storage.Container, error) { + return l.MemFS.Open(ctx, n) +} + +// LimitedSpaceContainer is a storage.Container that resides in memory. +type LimitedSpaceContainer struct { + *MemContainer + + fs *LimitedSpaceFS +} + +// Close closes the container. +func (l *LimitedSpaceContainer) Close() error { + return l.MemContainer.Close() +} + +// Create makes a new storage.File in the container. +func (l *LimitedSpaceContainer) Create(ctx context.Context, n string) (storage.File, error) { + if l.fs.OutOfSpace() { + return nil, ErrOutOfSpace + } + f, err := l.MemContainer.Create(ctx, n) + if err != nil { + return nil, err + } + ff := f.(*MemFile) + return &LimitedSpaceFile{ + MemFile: ff, + fs: l.fs, + }, nil +} + +// OpenFile creates a storage.File in the container using the provided options +// It supports WithCloseSyncMode. +func (l *LimitedSpaceContainer) OpenFile(ctx context.Context, n string, option ...storage.Option) (storage.File, error) { + return l.MemContainer.OpenFile(ctx, n, option...) +} + +// SubContainer creates a new storage.Container in the container. +func (l *LimitedSpaceContainer) SubContainer(ctx context.Context, n string, option ...storage.Option) (storage.Container, error) { + if l.fs.OutOfSpace() { + return nil, ErrOutOfSpace + } + c, err := l.MemContainer.SubContainer(ctx, n, option...) + if err != nil { + return nil, err + } + + cc := c.(*MemContainer) + return &LimitedSpaceContainer{ + MemContainer: cc, + fs: l.fs, + }, nil +} + +// LimitedSpaceFile is a storage.File that resides in memory. +type LimitedSpaceFile struct { + *MemFile + + fs *LimitedSpaceFS +} + +// Stat returns the FileInfo for the file. +func (l *LimitedSpaceFile) Stat() (sfs.FileInfo, error) { + return l.MemFile.Stat() +} + +func (l *LimitedSpaceFile) Read(p []byte) (int, error) { + return l.MemFile.Read(p) +} + +// Close closes the file. +func (l *LimitedSpaceFile) Close() error { + return l.MemFile.Close() +} + +// WriteString implements io.StringWriter. +func (l *LimitedSpaceFile) WriteString(s string) (n int, err error) { + return l.Write([]byte(s)) +} + +func (l *LimitedSpaceFile) Write(p []byte) (n int, err error) { + if l.fs.OutOfSpace() { + return 0, fmt.Errorf("%s %w", l.MemFile.name, ErrOutOfSpace) + } + return l.MemFile.Write(p) +}