diff --git a/pkg/local_object_storage/internal/storagetest/sanity.go b/pkg/local_object_storage/internal/storagetest/sanity.go new file mode 100644 index 000000000..c8c1a3c62 --- /dev/null +++ b/pkg/local_object_storage/internal/storagetest/sanity.go @@ -0,0 +1,182 @@ +package storagetest + +import ( + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/testutil" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" + objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +// ObjectStore is the general interface of implementations of object storage. +// +// It can be used to wrap other storages and execute generic sanity tests on them. +type ObjectStore interface { + Put(*objectSDK.Object) error + Get(oid.Address) (*objectSDK.Object, error) + Delete(oid.Address) error +} + +func TestSanityBasicLifecycle(t *testing.T, st ObjectStore) { + obj := testutil.GenerateObject() + addr := testutil.AddressFromObject(t, obj) + data, err := obj.Marshal() + require.NoError(t, err) + + // Get nonexistent object + { + _, gotErr := st.Get(addr) + require.True(t, client.IsErrObjectNotFound(gotErr)) + } + + // Put an object + require.NoError(t, st.Put(obj)) + + // Get the object previously put + { + gotObj, err := st.Get(addr) + require.NoError(t, err) + gotData, err := gotObj.Marshal() + require.NoError(t, err) + require.Equal(t, data, gotData) + } + + // Delete the object previously put + { + require.NoError(t, st.Delete(addr)) + require.True(t, client.IsErrObjectNotFound(st.Delete(addr))) + } + + // Get the object previously deleted + { + _, gotErr := st.Get(addr) + require.True(t, client.IsErrObjectNotFound(gotErr)) + } +} + +func TestSanityPutGetSequence(t *testing.T, st ObjectStore, iterations int) { + var objs []*objectSDK.Object + + for i := 0; i < iterations; i++ { + obj := testutil.GenerateObject() + require.NoError(t, st.Put(obj)) + objs = append(objs, obj) + } + + for _, wantObj := range objs { + addr := testutil.AddressFromObject(t, wantObj) + gotObj, err := st.Get(addr) + require.NoError(t, err) + + wantData, err := wantObj.Marshal() + require.NoError(t, err) + gotData, err := gotObj.Marshal() + require.NoError(t, err) + + require.Equal(t, wantData, gotData) + } +} + +func TestSanityPutDeleteSequence(t *testing.T, st ObjectStore, iterations int) { + for i := 0; i < iterations; i++ { + obj := testutil.GenerateObject() + addr := testutil.AddressFromObject(t, obj) + require.NoError(t, st.Put(obj)) + require.NoError(t, st.Delete(addr)) + require.True(t, client.IsErrObjectNotFound(st.Delete(addr))) + } +} + +func TestSanityStress(t *testing.T, st ObjectStore, iterations, maxResidentObjects int) { + type entry struct { + obj *objectSDK.Object + index int + } + + objs := map[oid.Address]*entry{} + var addrs []oid.Address + + putOne := func() { + obj := testutil.GenerateObject() + addr := testutil.AddressFromObject(t, obj) + require.NoError(t, st.Put(obj)) + objs[addr] = &entry{obj, len(addrs)} + addrs = append(addrs, addr) + } + + putOverwrite := func() { + index := rand.Intn(len(addrs)) + addr := addrs[index] + require.NoError(t, st.Put(objs[addr].obj)) + } + + getOne := func() { + index := rand.Intn(len(addrs)) + addr := addrs[index] + + wantObj := objs[addr].obj + wantData, err := wantObj.Marshal() + require.NoError(t, err) + + gotObj, err := st.Get(addr) + require.NoError(t, err) + gotData, err := gotObj.Marshal() + require.NoError(t, err) + + require.Equal(t, wantData, gotData) + } + + getNonexistent := func() { + addr := randNonexistingAddr(objs) + _, err := st.Get(addr) + require.True(t, client.IsErrObjectNotFound(err)) + } + + deleteOne := func() { + index := rand.Intn(len(addrs)) + addr := addrs[index] + + require.NoError(t, st.Delete(addr)) + + objs[addrs[len(addrs)-1]].index = index + addrs[index] = addrs[len(addrs)-1] + addrs = addrs[:len(addrs)-1] + delete(objs, addr) + } + + deleteNonexistent := func() { + addr := randNonexistingAddr(objs) + require.True(t, client.IsErrObjectNotFound(st.Delete(addr))) + } + + for i := 0; i < iterations; i++ { + // Make a slice of the operations that are currently possible + validOps := []func(){getNonexistent, deleteNonexistent} + + if len(addrs) < maxResidentObjects { + validOps = append(validOps, putOne) + } + if len(addrs) > 0 { + validOps = append(validOps, getOne, putOverwrite, deleteOne) + } + + // Run a random operation + validOps[rand.Intn(len(validOps))]() + } +} + +func randNonexistingAddr[V any](objs map[oid.Address]V) oid.Address { + addr := oidtest.Address() + // Make sure the key doesn't exist + for { + if _, exists := objs[addr]; !exists { + break + } + addr = oidtest.Address() + } + return addr +}