[#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:
Alejandro Lopez 2023-03-21 13:38:44 +03:00 committed by Gitea
parent e843e7f090
commit 341fe1688f
20 changed files with 617 additions and 208 deletions

View file

@ -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")

View file

@ -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),

View file

@ -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)
})
}

View file

@ -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)

View 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 }
}

View 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))
}
}