forked from TrueCloudLab/frostfs-node
[#139] test: Add test storage implementation
This aims to reduce the usage of chmod hackery to induce or simulate OS-related failures. Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
This commit is contained in:
parent
e843e7f090
commit
341fe1688f
20 changed files with 617 additions and 208 deletions
|
@ -50,7 +50,6 @@ func TestExistsInvalidStorageID(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("invalid storage id", func(t *testing.T) {
|
||||
// "0/X/Y" <-> "1/X/Y"
|
||||
storageID := slice.Copy(putRes.StorageID)
|
||||
storageID[0] = '9'
|
||||
badDir := filepath.Join(dir, "9")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package blobstor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
|
@ -9,32 +8,37 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/blobovniczatree"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/fstree"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/teststore"
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const blobovniczaDir = "blobovniczas"
|
||||
|
||||
func defaultStorages(p string, smallSizeLimit uint64) []SubStorage {
|
||||
func defaultTestStorages(p string, smallSizeLimit uint64) ([]SubStorage, *teststore.TestStore, *teststore.TestStore) {
|
||||
smallFileStorage := teststore.New(teststore.WithSubstorage(blobovniczatree.NewBlobovniczaTree(
|
||||
blobovniczatree.WithRootPath(filepath.Join(p, "blobovniczas")),
|
||||
blobovniczatree.WithBlobovniczaShallowWidth(1)), // default width is 16, slow init
|
||||
))
|
||||
largeFileStorage := teststore.New(teststore.WithSubstorage(fstree.New(fstree.WithPath(p))))
|
||||
return []SubStorage{
|
||||
{
|
||||
Storage: blobovniczatree.NewBlobovniczaTree(
|
||||
blobovniczatree.WithRootPath(filepath.Join(p, "blobovniczas")),
|
||||
blobovniczatree.WithBlobovniczaShallowWidth(1)), // default width is 16, slow init
|
||||
Storage: smallFileStorage,
|
||||
Policy: func(_ *objectSDK.Object, data []byte) bool {
|
||||
return uint64(len(data)) <= smallSizeLimit
|
||||
},
|
||||
},
|
||||
{
|
||||
Storage: fstree.New(fstree.WithPath(p)),
|
||||
Storage: largeFileStorage,
|
||||
},
|
||||
}
|
||||
}, smallFileStorage, largeFileStorage
|
||||
}
|
||||
|
||||
func defaultStorages(p string, smallSizeLimit uint64) []SubStorage {
|
||||
storages, _, _ := defaultTestStorages(p, smallSizeLimit)
|
||||
return storages
|
||||
}
|
||||
|
||||
func TestCompression(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "frostfs*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
dir := t.TempDir()
|
||||
|
||||
const (
|
||||
smallSizeLimit = 512
|
||||
|
@ -70,7 +74,7 @@ func TestCompression(t *testing.T) {
|
|||
testPut := func(t *testing.T, b *BlobStor, i int) {
|
||||
var prm common.PutPrm
|
||||
prm.Object = smallObj[i]
|
||||
_, err = b.Put(prm)
|
||||
_, err := b.Put(prm)
|
||||
require.NoError(t, err)
|
||||
|
||||
prm = common.PutPrm{}
|
||||
|
@ -102,9 +106,7 @@ func TestCompression(t *testing.T) {
|
|||
func TestBlobstor_needsCompression(t *testing.T) {
|
||||
const smallSizeLimit = 512
|
||||
newBlobStor := func(t *testing.T, compress bool, ct ...string) *BlobStor {
|
||||
dir, err := os.MkdirTemp("", "frostfs*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
dir := t.TempDir()
|
||||
|
||||
bs := New(
|
||||
WithCompressObjects(compress),
|
||||
|
|
|
@ -2,11 +2,11 @@ package blobstor
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
objectCore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/teststore"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
|
@ -20,8 +20,10 @@ func TestExists(t *testing.T) {
|
|||
|
||||
const smallSizeLimit = 512
|
||||
|
||||
b := New(
|
||||
WithStorages(defaultStorages(dir, smallSizeLimit)))
|
||||
storages, _, largeFileStorage := defaultTestStorages(dir, smallSizeLimit)
|
||||
|
||||
b := New(WithStorages(storages))
|
||||
|
||||
require.NoError(t, b.Open(false))
|
||||
require.NoError(t, b.Init())
|
||||
|
||||
|
@ -33,7 +35,7 @@ func TestExists(t *testing.T) {
|
|||
for i := range objects {
|
||||
var prm common.PutPrm
|
||||
prm.Object = objects[i]
|
||||
_, err = b.Put(prm)
|
||||
_, err := b.Put(prm)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -51,20 +53,10 @@ func TestExists(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.False(t, res.Exists)
|
||||
|
||||
t.Run("corrupt direcrory", func(t *testing.T) {
|
||||
var bigDir string
|
||||
de, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
for i := range de {
|
||||
if de[i].Name() != blobovniczaDir {
|
||||
bigDir = filepath.Join(dir, de[i].Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, bigDir)
|
||||
|
||||
require.NoError(t, os.Chmod(dir, 0))
|
||||
t.Cleanup(func() { require.NoError(t, os.Chmod(dir, 0777)) })
|
||||
t.Run("corrupt directory", func(t *testing.T) {
|
||||
largeFileStorage.SetOption(teststore.WithExists(func(common.ExistsPrm) (common.ExistsRes, error) {
|
||||
return common.ExistsRes{}, teststore.ErrDiskExploded
|
||||
}))
|
||||
|
||||
// Object exists, first error is logged.
|
||||
prm.Address = objectCore.AddressOf(objects[0])
|
||||
|
@ -76,6 +68,7 @@ func TestExists(t *testing.T) {
|
|||
prm.Address = objectCore.AddressOf(objects[1])
|
||||
_, err = b.Exists(prm)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, teststore.ErrDiskExploded)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
package blobstor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/storagetest"
|
||||
)
|
||||
|
||||
func TestGeneric(t *testing.T) {
|
||||
defer func() { _ = os.RemoveAll(t.Name()) }()
|
||||
|
||||
var n int
|
||||
newMetabase := func(t *testing.T) storagetest.Component {
|
||||
n++
|
||||
dir := filepath.Join(t.Name(), strconv.Itoa(n))
|
||||
return New(
|
||||
WithStorages(defaultStorages(dir, 128)))
|
||||
WithStorages(defaultStorages(t.TempDir(), 128)))
|
||||
}
|
||||
|
||||
storagetest.TestAll(t, newMetabase)
|
||||
|
|
74
pkg/local_object_storage/blobstor/teststore/option.go
Normal file
74
pkg/local_object_storage/blobstor/teststore/option.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package teststore
|
||||
|
||||
import (
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/compression"
|
||||
)
|
||||
|
||||
type cfg struct {
|
||||
st common.Storage
|
||||
overrides struct {
|
||||
Open func(readOnly bool) error
|
||||
Init func() error
|
||||
Close func() error
|
||||
|
||||
Type func() string
|
||||
Path func() string
|
||||
SetCompressor func(cc *compression.Config)
|
||||
SetReportErrorFunc func(f func(string, error))
|
||||
|
||||
Get func(common.GetPrm) (common.GetRes, error)
|
||||
GetRange func(common.GetRangePrm) (common.GetRangeRes, error)
|
||||
Exists func(common.ExistsPrm) (common.ExistsRes, error)
|
||||
Put func(common.PutPrm) (common.PutRes, error)
|
||||
Delete func(common.DeletePrm) (common.DeleteRes, error)
|
||||
Iterate func(common.IteratePrm) (common.IterateRes, error)
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(*cfg)
|
||||
|
||||
func WithSubstorage(st common.Storage) Option {
|
||||
return func(c *cfg) {
|
||||
c.st = st
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpen(f func(bool) error) Option { return func(c *cfg) { c.overrides.Open = f } }
|
||||
func WithInit(f func() error) Option { return func(c *cfg) { c.overrides.Init = f } }
|
||||
func WithClose(f func() error) Option { return func(c *cfg) { c.overrides.Close = f } }
|
||||
|
||||
func WithType(f func() string) Option { return func(c *cfg) { c.overrides.Type = f } }
|
||||
func WithPath(f func() string) Option { return func(c *cfg) { c.overrides.Path = f } }
|
||||
|
||||
func WithSetCompressor(f func(*compression.Config)) Option {
|
||||
return func(c *cfg) { c.overrides.SetCompressor = f }
|
||||
}
|
||||
|
||||
func WithReportErrorFunc(f func(func(string, error))) Option {
|
||||
return func(c *cfg) { c.overrides.SetReportErrorFunc = f }
|
||||
}
|
||||
|
||||
func WithGet(f func(common.GetPrm) (common.GetRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.Get = f }
|
||||
}
|
||||
|
||||
func WithGetRange(f func(common.GetRangePrm) (common.GetRangeRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.GetRange = f }
|
||||
}
|
||||
|
||||
func WithExists(f func(common.ExistsPrm) (common.ExistsRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.Exists = f }
|
||||
}
|
||||
|
||||
func WithPut(f func(common.PutPrm) (common.PutRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.Put = f }
|
||||
}
|
||||
|
||||
func WithDelete(f func(common.DeletePrm) (common.DeleteRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.Delete = f }
|
||||
}
|
||||
|
||||
func WithIterate(f func(common.IteratePrm) (common.IterateRes, error)) Option {
|
||||
return func(c *cfg) { c.overrides.Iterate = f }
|
||||
}
|
215
pkg/local_object_storage/blobstor/teststore/teststore.go
Normal file
215
pkg/local_object_storage/blobstor/teststore/teststore.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
// Package teststore provides a common.Storage implementation for testing/mocking purposes.
|
||||
//
|
||||
// A new teststore.TestStore can be obtained with teststore.New. Whenever one of the common.Storage
|
||||
// methods is called, the implementation selects what function to call in the following order:
|
||||
// 1. If an override for that method was provided at construction time (via teststore.WithXXX()) or
|
||||
// afterwards via SetOption, that override is used.
|
||||
// 2. If a substorage was provided at construction time (via teststore.WithSubstorage()) or afterwars
|
||||
// via SetOption, the corresponding method in the substorage is used.
|
||||
// 3. If none of the above apply, the call panics with an error describing the unexpected call.
|
||||
//
|
||||
// It's safe to call SetOption and the overrides from multiple goroutines, but it's the override's
|
||||
// responsibility to ensure safety of whatever operation it executes.
|
||||
package teststore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/compression"
|
||||
)
|
||||
|
||||
// TestStore is a common.Storage implementation for testing/mocking purposes.
|
||||
type TestStore struct {
|
||||
mu sync.RWMutex
|
||||
*cfg
|
||||
}
|
||||
|
||||
// ErrDiskExploded is a phony error which can be used for testing purposes to differentiate it from
|
||||
// more common errors.
|
||||
var ErrDiskExploded = errors.New("disk exploded")
|
||||
|
||||
// New returns a teststore.TestStore from the given options.
|
||||
func New(opts ...Option) *TestStore {
|
||||
c := &cfg{}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return &TestStore{cfg: c}
|
||||
}
|
||||
|
||||
// SetOption overrides an option of an existing teststore.TestStore.
|
||||
// This is useful for overriding methods during a test so that different
|
||||
// behaviors are simulated.
|
||||
func (s *TestStore) SetOption(opt Option) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
opt(s.cfg)
|
||||
}
|
||||
|
||||
func (s *TestStore) Open(readOnly bool) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Open != nil:
|
||||
return s.overrides.Open(readOnly)
|
||||
case s.st != nil:
|
||||
return s.st.Open(readOnly)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Open(%v)", readOnly))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Init() error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Init != nil:
|
||||
return s.overrides.Init()
|
||||
case s.st != nil:
|
||||
return s.st.Init()
|
||||
default:
|
||||
panic("unexpected storage call: Init()")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Close() error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Close != nil:
|
||||
return s.overrides.Close()
|
||||
case s.st != nil:
|
||||
return s.st.Close()
|
||||
default:
|
||||
panic("unexpected storage call: Close()")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Type() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Type != nil:
|
||||
return s.overrides.Type()
|
||||
case s.st != nil:
|
||||
return s.st.Type()
|
||||
default:
|
||||
panic("unexpected storage call: Type()")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Path() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Path != nil:
|
||||
return s.overrides.Path()
|
||||
case s.st != nil:
|
||||
return s.st.Path()
|
||||
default:
|
||||
panic("unexpected storage call: Path()")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) SetCompressor(cc *compression.Config) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.SetCompressor != nil:
|
||||
s.overrides.SetCompressor(cc)
|
||||
case s.st != nil:
|
||||
s.st.SetCompressor(cc)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: SetCompressor(%+v)", cc))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) SetReportErrorFunc(f func(string, error)) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.SetReportErrorFunc != nil:
|
||||
s.overrides.SetReportErrorFunc(f)
|
||||
case s.st != nil:
|
||||
s.st.SetReportErrorFunc(f)
|
||||
default:
|
||||
panic("unexpected storage call: SetReportErrorFunc(<func>)")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Get(req common.GetPrm) (common.GetRes, error) {
|
||||
switch {
|
||||
case s.overrides.Get != nil:
|
||||
return s.overrides.Get(req)
|
||||
case s.st != nil:
|
||||
return s.st.Get(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Get(%+v)", req))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) GetRange(req common.GetRangePrm) (common.GetRangeRes, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.GetRange != nil:
|
||||
return s.overrides.GetRange(req)
|
||||
case s.st != nil:
|
||||
return s.st.GetRange(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: GetRange(%+v)", req))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Exists(req common.ExistsPrm) (common.ExistsRes, error) {
|
||||
switch {
|
||||
case s.overrides.Exists != nil:
|
||||
return s.overrides.Exists(req)
|
||||
case s.st != nil:
|
||||
return s.st.Exists(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Exists(%+v)", req))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Put(req common.PutPrm) (common.PutRes, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Put != nil:
|
||||
return s.overrides.Put(req)
|
||||
case s.st != nil:
|
||||
return s.st.Put(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Put(%+v)", req))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Delete(req common.DeletePrm) (common.DeleteRes, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Delete != nil:
|
||||
return s.overrides.Delete(req)
|
||||
case s.st != nil:
|
||||
return s.st.Delete(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Delete(%+v)", req))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) Iterate(req common.IteratePrm) (common.IterateRes, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
switch {
|
||||
case s.overrides.Iterate != nil:
|
||||
return s.overrides.Iterate(req)
|
||||
case s.st != nil:
|
||||
return s.st.Iterate(req)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected storage call: Iterate(%+v)", req))
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue