forked from TrueCloudLab/restic
ff7ef5007e
The ioutil functions are deprecated since Go 1.17 and only wrap another library function. Thus directly call the underlying function. This commit only mechanically replaces the function calls.
272 lines
6 KiB
Go
272 lines
6 KiB
Go
package cache
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/restic/restic/internal/errors"
|
|
"github.com/restic/restic/internal/restic"
|
|
"github.com/restic/restic/internal/test"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func generateRandomFiles(t testing.TB, tpe restic.FileType, c *Cache) restic.IDSet {
|
|
ids := restic.NewIDSet()
|
|
for i := 0; i < rand.Intn(15)+10; i++ {
|
|
buf := test.Random(rand.Int(), 1<<19)
|
|
id := restic.Hash(buf)
|
|
h := restic.Handle{Type: tpe, Name: id.String()}
|
|
|
|
if c.Has(h) {
|
|
t.Errorf("index %v present before save", id)
|
|
}
|
|
|
|
err := c.Save(h, bytes.NewReader(buf))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids.Insert(id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// randomID returns a random ID from s.
|
|
func randomID(s restic.IDSet) restic.ID {
|
|
for id := range s {
|
|
return id
|
|
}
|
|
panic("set is empty")
|
|
}
|
|
|
|
func load(t testing.TB, c *Cache, h restic.Handle) []byte {
|
|
rd, err := c.load(h, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if rd == nil {
|
|
t.Fatalf("load() returned nil reader")
|
|
}
|
|
|
|
buf, err := io.ReadAll(rd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err = rd.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
func listFiles(t testing.TB, c *Cache, tpe restic.FileType) restic.IDSet {
|
|
list, err := c.list(tpe)
|
|
if err != nil {
|
|
t.Errorf("listing failed: %v", err)
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
func clearFiles(t testing.TB, c *Cache, tpe restic.FileType, valid restic.IDSet) {
|
|
if err := c.Clear(tpe, valid); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestFiles(t *testing.T) {
|
|
seed := time.Now().Unix()
|
|
t.Logf("seed is %v", seed)
|
|
rand.Seed(seed)
|
|
|
|
c, cleanup := TestNewCache(t)
|
|
defer cleanup()
|
|
|
|
var tests = []restic.FileType{
|
|
restic.SnapshotFile,
|
|
restic.PackFile,
|
|
restic.IndexFile,
|
|
}
|
|
|
|
for _, tpe := range tests {
|
|
t.Run(tpe.String(), func(t *testing.T) {
|
|
ids := generateRandomFiles(t, tpe, c)
|
|
id := randomID(ids)
|
|
|
|
h := restic.Handle{Type: tpe, Name: id.String()}
|
|
id2 := restic.Hash(load(t, c, h))
|
|
|
|
if !id.Equal(id2) {
|
|
t.Errorf("wrong data returned, want %v, got %v", id.Str(), id2.Str())
|
|
}
|
|
|
|
if !c.Has(h) {
|
|
t.Errorf("cache thinks index %v isn't present", id.Str())
|
|
}
|
|
|
|
list := listFiles(t, c, tpe)
|
|
if !ids.Equals(list) {
|
|
t.Errorf("wrong list of index IDs returned, want:\n %v\ngot:\n %v", ids, list)
|
|
}
|
|
|
|
clearFiles(t, c, tpe, restic.NewIDSet(id))
|
|
list2 := listFiles(t, c, tpe)
|
|
ids.Delete(id)
|
|
want := restic.NewIDSet(id)
|
|
if !list2.Equals(want) {
|
|
t.Errorf("ClearIndexes removed indexes, want:\n %v\ngot:\n %v", list2, want)
|
|
}
|
|
|
|
clearFiles(t, c, tpe, restic.NewIDSet())
|
|
want = restic.NewIDSet()
|
|
list3 := listFiles(t, c, tpe)
|
|
if !list3.Equals(want) {
|
|
t.Errorf("ClearIndexes returned a wrong list, want:\n %v\ngot:\n %v", want, list3)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileLoad(t *testing.T) {
|
|
seed := time.Now().Unix()
|
|
t.Logf("seed is %v", seed)
|
|
rand.Seed(seed)
|
|
|
|
c, cleanup := TestNewCache(t)
|
|
defer cleanup()
|
|
|
|
// save about 5 MiB of data in the cache
|
|
data := test.Random(rand.Int(), 5234142)
|
|
id := restic.ID{}
|
|
copy(id[:], data)
|
|
h := restic.Handle{
|
|
Type: restic.PackFile,
|
|
Name: id.String(),
|
|
}
|
|
if err := c.Save(h, bytes.NewReader(data)); err != nil {
|
|
t.Fatalf("Save() returned error: %v", err)
|
|
}
|
|
|
|
var tests = []struct {
|
|
offset int64
|
|
length int
|
|
}{
|
|
{0, 0},
|
|
{5, 0},
|
|
{32*1024 + 5, 0},
|
|
{0, 123},
|
|
{0, 64*1024 + 234},
|
|
{100, 5234142 - 100},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("%v/%v", test.length, test.offset), func(t *testing.T) {
|
|
rd, err := c.load(h, test.length, test.offset)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
buf, err := io.ReadAll(rd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err = rd.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
o := int(test.offset)
|
|
l := test.length
|
|
if test.length == 0 {
|
|
l = len(data) - o
|
|
}
|
|
|
|
if l > len(data)-o {
|
|
l = len(data) - o
|
|
}
|
|
|
|
if len(buf) != l {
|
|
t.Fatalf("wrong number of bytes returned: want %d, got %d", l, len(buf))
|
|
}
|
|
|
|
if !bytes.Equal(buf, data[o:o+l]) {
|
|
t.Fatalf("wrong data returned, want:\n %02x\ngot:\n %02x", data[o:o+16], buf[:16])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Simulate multiple processes writing to a cache, using goroutines.
|
|
//
|
|
// The possibility of sharing a cache between multiple concurrent restic
|
|
// processes isn't guaranteed in the docs and doesn't always work on Windows, hence the
|
|
// check on GOOS. Cache sharing is considered a "nice to have" on POSIX, for now.
|
|
//
|
|
// The cache first creates a temporary file and then renames it to its final name.
|
|
// On Windows renaming internally creates a file handle with a shareMode which
|
|
// includes FILE_SHARE_DELETE. The Go runtime opens files without FILE_SHARE_DELETE,
|
|
// thus Open(fn) will fail until the file handle used for renaming was closed.
|
|
// See https://devblogs.microsoft.com/oldnewthing/20211022-00/?p=105822
|
|
// for hints on how to fix this properly.
|
|
func TestFileSaveConcurrent(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("may not work due to FILE_SHARE_DELETE issue")
|
|
}
|
|
|
|
const nproc = 40
|
|
|
|
c, cleanup := TestNewCache(t)
|
|
defer cleanup()
|
|
|
|
var (
|
|
data = test.Random(1, 10000)
|
|
g errgroup.Group
|
|
id restic.ID
|
|
)
|
|
rand.Read(id[:])
|
|
|
|
h := restic.Handle{
|
|
Type: restic.PackFile,
|
|
Name: id.String(),
|
|
}
|
|
|
|
for i := 0; i < nproc/2; i++ {
|
|
g.Go(func() error { return c.Save(h, bytes.NewReader(data)) })
|
|
|
|
// Can't use load because only the main goroutine may call t.Fatal.
|
|
g.Go(func() error {
|
|
// The timing is hard to get right, but the main thing we want to
|
|
// ensure is ENOENT or nil error.
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
f, err := c.load(h, 0, 0)
|
|
t.Logf("Load error: %v", err)
|
|
switch {
|
|
case err == nil:
|
|
case errors.Is(err, os.ErrNotExist):
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
read, err := io.ReadAll(f)
|
|
if err == nil && !bytes.Equal(read, data) {
|
|
err = errors.New("mismatch between Save and Load")
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
test.OK(t, g.Wait())
|
|
saved := load(t, c, h)
|
|
test.Equals(t, data, saved)
|
|
}
|