mirror of
https://github.com/kopia/kopia.git
synced 2026-03-27 10:32:08 -04:00
Also simplified validation test suite, which will simply test whether the provider supports DoNotRecreate or properly rejects it without external configuration.
227 lines
6.0 KiB
Go
227 lines
6.0 KiB
Go
package blobtesting
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/kopia/kopia/internal/gather"
|
|
"github.com/kopia/kopia/internal/providervalidation"
|
|
"github.com/kopia/kopia/repo/blob"
|
|
)
|
|
|
|
// VerifyStorage verifies the behavior of the specified storage.
|
|
// nolint:gocyclo,thelper
|
|
func VerifyStorage(ctx context.Context, t *testing.T, r blob.Storage, opts blob.PutOptions) {
|
|
blocks := []struct {
|
|
blk blob.ID
|
|
contents []byte
|
|
}{
|
|
{blk: "abcdbbf4f0507d054ed5a80a5b65086f602b", contents: []byte{}},
|
|
{blk: "zxce0e35630770c54668a8cfb4e414c6bf8f", contents: []byte{1}},
|
|
{blk: "abff4585856ebf0748fd989e1dd623a8963d", contents: bytes.Repeat([]byte{1}, 1000)},
|
|
{blk: "abgc3dca496d510f492c858a2df1eb824e62", contents: bytes.Repeat([]byte{1}, 10000)},
|
|
{blk: "kopia.repository", contents: bytes.Repeat([]byte{2}, 100)},
|
|
}
|
|
|
|
// First verify that blocks don't exist.
|
|
t.Run("VerifyBlobsNotFound", func(t *testing.T) {
|
|
for _, b := range blocks {
|
|
b := b
|
|
|
|
t.Run(string(b.blk), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
AssertGetBlobNotFound(ctx, t, r, b.blk)
|
|
AssertGetMetadataNotFound(ctx, t, r, b.blk)
|
|
})
|
|
}
|
|
})
|
|
|
|
if err := r.DeleteBlob(ctx, "no-such-blob"); err != nil && !errors.Is(err, blob.ErrBlobNotFound) {
|
|
t.Errorf("invalid error when deleting non-existent blob: %v", err)
|
|
}
|
|
|
|
initialAddConcurrency := 2
|
|
if os.Getenv("CI") != "" {
|
|
initialAddConcurrency = 4
|
|
}
|
|
|
|
// Now add blocks.
|
|
t.Run("AddBlobs", func(t *testing.T) {
|
|
for _, b := range blocks {
|
|
for i := 0; i < initialAddConcurrency; i++ {
|
|
b := b
|
|
|
|
t.Run(fmt.Sprintf("%v-%v", b.blk, i), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if err := r.PutBlob(ctx, b.blk, gather.FromSlice(b.contents), opts); err != nil {
|
|
t.Fatalf("can't put blob: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("GetBlobs", func(t *testing.T) {
|
|
for _, b := range blocks {
|
|
b := b
|
|
|
|
t.Run(string(b.blk), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
AssertGetBlob(ctx, t, r, b.blk, b.contents)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("ListBlobs", func(t *testing.T) {
|
|
errExpected := errors.New("expected error")
|
|
|
|
t.Run("ListErrorNoPrefix", func(t *testing.T) {
|
|
t.Parallel()
|
|
require.ErrorIs(t, r.ListBlobs(ctx, "", func(bm blob.Metadata) error {
|
|
return errExpected
|
|
}), errExpected)
|
|
})
|
|
t.Run("ListErrorWithPrefix", func(t *testing.T) {
|
|
t.Parallel()
|
|
require.ErrorIs(t, r.ListBlobs(ctx, "ab", func(bm blob.Metadata) error {
|
|
return errExpected
|
|
}), errExpected)
|
|
})
|
|
t.Run("ListNoPrefix", func(t *testing.T) {
|
|
t.Parallel()
|
|
AssertListResults(ctx, t, r, "", blocks[0].blk, blocks[1].blk, blocks[2].blk, blocks[3].blk, blocks[4].blk)
|
|
})
|
|
t.Run("ListWithPrefix", func(t *testing.T) {
|
|
t.Parallel()
|
|
AssertListResults(ctx, t, r, "ab", blocks[0].blk, blocks[2].blk, blocks[3].blk)
|
|
})
|
|
})
|
|
|
|
t.Run("OverwriteBlobs", func(t *testing.T) {
|
|
newContents := []byte{99}
|
|
|
|
for _, b := range blocks {
|
|
b := b
|
|
|
|
t.Run(string(b.blk), func(t *testing.T) {
|
|
t.Parallel()
|
|
err := r.PutBlob(ctx, b.blk, gather.FromSlice(newContents), opts)
|
|
if opts.DoNotRecreate {
|
|
require.ErrorIsf(t, err, blob.ErrBlobAlreadyExists, "overwrote blob: %v", b)
|
|
AssertGetBlob(ctx, t, r, b.blk, b.contents)
|
|
} else {
|
|
require.NoErrorf(t, err, "can't put blob: %v", b)
|
|
AssertGetBlob(ctx, t, r, b.blk, newContents)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteBlobsAndList", func(t *testing.T) {
|
|
require.NoError(t, r.DeleteBlob(ctx, blocks[0].blk))
|
|
require.NoError(t, r.DeleteBlob(ctx, blocks[0].blk))
|
|
|
|
AssertListResults(ctx, t, r, "ab", blocks[2].blk, blocks[3].blk)
|
|
AssertListResults(ctx, t, r, "", blocks[1].blk, blocks[2].blk, blocks[3].blk, blocks[4].blk)
|
|
})
|
|
|
|
t.Run("PutBlobsWithSetTime", func(t *testing.T) {
|
|
for _, b := range blocks {
|
|
b := b
|
|
|
|
t.Run(string(b.blk), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
inTime := time.Date(2020, 1, 2, 12, 30, 40, 0, time.UTC)
|
|
|
|
err := r.PutBlob(ctx, b.blk, gather.FromSlice(b.contents), blob.PutOptions{
|
|
SetModTime: inTime,
|
|
})
|
|
|
|
if errors.Is(err, blob.ErrSetTimeUnsupported) {
|
|
t.Skip("setting time unsupported")
|
|
}
|
|
|
|
bm, err := r.GetMetadata(ctx, b.blk)
|
|
require.NoError(t, err)
|
|
|
|
AssertTimestampsCloseEnough(t, bm.BlobID, bm.Timestamp, inTime)
|
|
|
|
all, err := blob.ListAllBlobs(ctx, r, b.blk)
|
|
require.NoError(t, err)
|
|
require.Len(t, all, 1)
|
|
|
|
AssertTimestampsCloseEnough(t, all[0].BlobID, all[0].Timestamp, inTime)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("PutBlobsWithGetTime", func(t *testing.T) {
|
|
for _, b := range blocks {
|
|
b := b
|
|
|
|
t.Run(string(b.blk), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var outTime time.Time
|
|
|
|
require.NoError(t, r.PutBlob(ctx, b.blk, gather.FromSlice(b.contents), blob.PutOptions{
|
|
GetModTime: &outTime,
|
|
}))
|
|
|
|
require.False(t, outTime.IsZero(), "modification time was not returned")
|
|
|
|
bm, err := r.GetMetadata(ctx, b.blk)
|
|
require.NoError(t, err)
|
|
|
|
AssertTimestampsCloseEnough(t, bm.BlobID, bm.Timestamp, outTime)
|
|
|
|
all, err := blob.ListAllBlobs(ctx, r, b.blk)
|
|
require.NoError(t, err)
|
|
require.Len(t, all, 1)
|
|
|
|
AssertTimestampsCloseEnough(t, all[0].BlobID, all[0].Timestamp, outTime)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// AssertConnectionInfoRoundTrips verifies that the ConnectionInfo returned by a given storage can be used to create
|
|
// equivalent storage.
|
|
// nolint:thelper
|
|
func AssertConnectionInfoRoundTrips(ctx context.Context, t *testing.T, s blob.Storage) {
|
|
ci := s.ConnectionInfo()
|
|
|
|
s2, err := blob.NewStorage(ctx, ci, false)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
ci2 := s2.ConnectionInfo()
|
|
require.Equal(t, ci, ci2)
|
|
|
|
require.NoError(t, s2.Close(ctx))
|
|
}
|
|
|
|
// TestValidationOptions is the set of options used when running providing validation from tests.
|
|
// nolint:gomnd
|
|
var TestValidationOptions = providervalidation.Options{
|
|
MaxClockDrift: 3 * time.Minute,
|
|
ConcurrencyTestDuration: 15 * time.Second,
|
|
NumPutBlobWorkers: 3,
|
|
NumGetBlobWorkers: 3,
|
|
NumGetMetadataWorkers: 3,
|
|
NumListBlobsWorkers: 3,
|
|
MaxBlobLength: 10e6,
|
|
}
|