Files
kopia/tests/robustness/engine/engine_test.go
Nick eaf14a5fa5 Path protection between robustness engine FileWriter and Snapshotter (#865)
Protect filesystem subtrees from concurrent manipulation during critical sections
if engine actions are called asynchronously. This change provides coordination
between the `Snapshotter` and the `FileWriter`. For example, the `FileWriter`
should be blocked from perturbing the same directory tree if a
Gather-Snapshot is taking place along that tree simultaneously.
This will ensure the fingerprint data accumulated during the `Gather` phase
will correspond unambiguously to the data included in the snapshot.

Extend build flags to kopia snapshotter
  This package now imports fswalker which can only be built for
  darwin,amd64 or linux,amd64
2021-03-16 15:15:15 -07:00

918 lines
24 KiB
Go

// +build darwin,amd64 linux,amd64
// Package engine provides the framework for a snapshot repository testing engine
package engine
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"math"
"os"
"path/filepath"
"strconv"
"syscall"
"testing"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/tests/robustness"
"github.com/kopia/kopia/tests/robustness/fiofilewriter"
"github.com/kopia/kopia/tests/robustness/snapmeta"
"github.com/kopia/kopia/tests/testenv"
"github.com/kopia/kopia/tests/tools/fio"
"github.com/kopia/kopia/tests/tools/fswalker"
"github.com/kopia/kopia/tests/tools/kopiarunner"
)
var (
repoBaseDirName = "engine"
fsBasePath = "/tmp"
s3BasePath = ""
dataRepoPath = "unit-tests/data-repo"
metadataRepoPath = "unit-tests/metadata-repo"
fsRepoBaseDirPath = filepath.Join(fsBasePath, repoBaseDirName)
s3RepoBaseDirPath = filepath.Join(s3BasePath, repoBaseDirName)
fsMetadataRepoPath = filepath.Join(fsRepoBaseDirPath, metadataRepoPath)
s3MetadataRepoPath = filepath.Join(s3RepoBaseDirPath, metadataRepoPath)
fsDataRepoPath = filepath.Join(fsRepoBaseDirPath, dataRepoPath)
s3DataRepoPath = filepath.Join(s3RepoBaseDirPath, dataRepoPath)
)
func TestEngineWritefilesBasicFS(t *testing.T) {
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, "")
th, eng, err := newTestHarness(t, fsDataRepoPath, fsMetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
os.RemoveAll(fsRepoBaseDirPath)
}()
opts := map[string]string{}
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
fileSize := int64(256 * 1024)
numFiles := 10
fioOpts := fio.Options{}.WithFileSize(fileSize).WithNumFiles(numFiles)
fioRunner := th.FioRunner()
err = fioRunner.WriteFiles("", fioOpts)
testenv.AssertNoError(t, err)
snapIDs := eng.Checker.GetSnapIDs()
snapID, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
err = eng.Checker.RestoreSnapshot(ctx, snapID, os.Stdout, opts)
testenv.AssertNoError(t, err)
for _, sID := range snapIDs {
err = eng.Checker.RestoreSnapshot(ctx, sID, os.Stdout, opts)
testenv.AssertNoError(t, err)
}
}
func randomString(n int) string {
b := make([]byte, n)
io.ReadFull(rand.Reader, b)
return hex.EncodeToString(b)
}
func makeTempS3Bucket(t *testing.T) (bucketName string, cleanupCB func()) {
t.Helper()
endpoint := "s3.amazonaws.com"
accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
sessionToken := os.Getenv("AWS_SESSION_TOKEN")
if accessKeyID == "" || secretAccessKey == "" || sessionToken == "" {
t.Skip("Skipping S3 tests if no creds provided")
}
ctx := context.Background()
cli, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, sessionToken),
Secure: true,
Region: "",
})
testenv.AssertNoError(t, err)
bucketName = fmt.Sprintf("engine-unit-tests-%s", randomString(4))
err = cli.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
testenv.AssertNoError(t, err)
return bucketName, func() {
objChan := make(chan minio.ObjectInfo)
errCh := cli.RemoveObjects(ctx, bucketName, objChan, minio.RemoveObjectsOptions{})
go func() {
for removeErr := range errCh {
t.Errorf("error removing key %s from bucket: %s", removeErr.ObjectName, removeErr.Err)
}
}()
for obj := range cli.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Prefix: "",
Recursive: true,
}) {
objChan <- obj
}
close(objChan)
retries := 10
retryPeriod := 1 * time.Second
var err error
for retry := 0; retry < retries; retry++ {
time.Sleep(retryPeriod)
err = cli.RemoveBucket(ctx, bucketName)
if err == nil {
break
}
}
testenv.AssertNoError(t, err)
}
}
func TestWriteFilesBasicS3(t *testing.T) {
bucketName, cleanupCB := makeTempS3Bucket(t)
defer cleanupCB()
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, bucketName)
th, eng, err := newTestHarness(t, s3DataRepoPath, s3MetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
}()
opts := map[string]string{}
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
fileSize := int64(256 * 1024)
numFiles := 10
fioOpts := fio.Options{}.WithFileSize(fileSize).WithNumFiles(numFiles)
fioRunner := th.FioRunner()
err = fioRunner.WriteFiles("", fioOpts)
testenv.AssertNoError(t, err)
snapIDs := eng.Checker.GetLiveSnapIDs()
snapID, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
err = eng.Checker.RestoreSnapshot(ctx, snapID, os.Stdout, opts)
testenv.AssertNoError(t, err)
for _, sID := range snapIDs {
err = eng.Checker.RestoreSnapshot(ctx, sID, os.Stdout, opts)
testenv.AssertNoError(t, err)
}
}
func TestDeleteSnapshotS3(t *testing.T) {
bucketName, cleanupCB := makeTempS3Bucket(t)
defer cleanupCB()
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, bucketName)
th, eng, err := newTestHarness(t, s3DataRepoPath, s3MetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
}()
opts := map[string]string{}
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
fileSize := int64(256 * 1024)
numFiles := 10
fioOpts := fio.Options{}.WithFileSize(fileSize).WithNumFiles(numFiles)
fioRunner := th.FioRunner()
err = fioRunner.WriteFiles("", fioOpts)
testenv.AssertNoError(t, err)
snapID, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
err = eng.Checker.RestoreSnapshot(ctx, snapID, os.Stdout, opts)
testenv.AssertNoError(t, err)
err = eng.Checker.DeleteSnapshot(ctx, snapID, opts)
testenv.AssertNoError(t, err)
err = eng.Checker.RestoreSnapshot(ctx, snapID, os.Stdout, opts)
if err == nil {
t.Fatalf("Expected an error when trying to restore a deleted snapshot")
}
}
func TestSnapshotVerificationFail(t *testing.T) {
bucketName, cleanupCB := makeTempS3Bucket(t)
defer cleanupCB()
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, bucketName)
th, eng, err := newTestHarness(t, s3DataRepoPath, s3MetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
}()
opts := map[string]string{}
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
// Perform writes
fileSize := int64(256 * 1024)
numFiles := 10
fioOpt := fio.Options{}.WithFileSize(fileSize).WithNumFiles(numFiles)
fioRunner := th.FioRunner()
err = fioRunner.WriteFiles("", fioOpt)
testenv.AssertNoError(t, err)
// Take a first snapshot
snapID1, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
// Get the metadata collected on that snapshot
ssMeta1, err := eng.Checker.GetSnapshotMetadata(snapID1)
testenv.AssertNoError(t, err)
// Do additional writes, writing 1 extra byte than before
err = fioRunner.WriteFiles("", fioOpt.WithFileSize(fileSize+1))
testenv.AssertNoError(t, err)
// Take a second snapshot
snapID2, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
// Get the second snapshot's metadata
ssMeta2, err := eng.Checker.GetSnapshotMetadata(snapID2)
testenv.AssertNoError(t, err)
// Swap second snapshot's validation data into the first's metadata
ssMeta1.ValidationData = ssMeta2.ValidationData
restoreDir, err := ioutil.TempDir(eng.Checker.RestoreDir, fmt.Sprintf("restore-snap-%v", snapID1))
testenv.AssertNoError(t, err)
defer os.RemoveAll(restoreDir)
// Restore snapshot ID 1 with snapshot 2's validation data in metadata, expect error
err = eng.Checker.RestoreVerifySnapshot(ctx, snapID1, restoreDir, ssMeta1, os.Stdout, opts)
if err == nil {
t.Fatalf("Expected an integrity error when trying to restore a snapshot with incorrect metadata")
}
}
func TestDataPersistency(t *testing.T) {
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, "")
tempDir := testutil.TempDirectory(t)
dataRepoPath := filepath.Join(tempDir, "data-repo-")
metadataRepoPath := filepath.Join(tempDir, "metadata-repo-")
th, eng, err := newTestHarness(t, dataRepoPath, metadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
}()
opts := map[string]string{}
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
// Perform writes
fileSize := int64(256 * 1024)
numFiles := 10
fioOpt := fio.Options{}.WithFileSize(fileSize).WithNumFiles(numFiles)
fioRunner := th.FioRunner()
err = fioRunner.WriteFiles("", fioOpt)
testenv.AssertNoError(t, err)
// Take a snapshot
snapID, err := eng.Checker.TakeSnapshot(ctx, fioRunner.LocalDataDir, opts)
testenv.AssertNoError(t, err)
// Get the walk data associated with the snapshot that was taken
dataDirWalk, err := eng.Checker.GetSnapshotMetadata(snapID)
testenv.AssertNoError(t, err)
// Save the snapshot ID index
err = eng.saveSnapIDIndex()
testenv.AssertNoError(t, err)
// Flush the snapshot metadata to persistent storage
err = eng.MetaStore.FlushMetadata()
testenv.AssertNoError(t, err)
// Create a new engine
th2, eng2, err := newTestHarness(t, dataRepoPath, metadataRepoPath)
testenv.AssertNoError(t, err)
defer func() {
th2.eng.cleanComponents()
th2.eng = nil
th2.Cleanup()
}()
// Connect this engine to the same data and metadata repositories -
// expect that the snapshot taken above will be found in metadata,
// and the data will be chosen to be restored to this engine's DataDir
// as a starting point.
err = eng2.Init(ctx)
testenv.AssertNoError(t, err)
fioRunner2 := th2.FioRunner()
err = eng2.Checker.RestoreSnapshotToPath(ctx, snapID, fioRunner2.LocalDataDir, os.Stdout, opts)
testenv.AssertNoError(t, err)
// Compare the data directory of the second engine with the fingerprint
// of the snapshot taken earlier. They should match.
err = fswalker.NewWalkCompare().Compare(ctx, fioRunner2.LocalDataDir, dataDirWalk.ValidationData, os.Stdout, opts)
testenv.AssertNoError(t, err)
}
func TestPickActionWeighted(t *testing.T) {
for _, tc := range []struct {
name string
inputCtrlWeights map[string]float64
inputActionList map[ActionKey]Action
}{
{
name: "basic uniform",
inputCtrlWeights: map[string]float64{
"A": 1,
"B": 1,
"C": 1,
},
inputActionList: map[ActionKey]Action{
"A": {},
"B": {},
"C": {},
},
},
{
name: "basic weighted",
inputCtrlWeights: map[string]float64{
"A": 1,
"B": 10,
"C": 100,
},
inputActionList: map[ActionKey]Action{
"A": {},
"B": {},
"C": {},
},
},
{
name: "include a zero weight",
inputCtrlWeights: map[string]float64{
"A": 1,
"B": 0,
"C": 1,
},
inputActionList: map[ActionKey]Action{
"A": {},
"B": {},
"C": {},
},
},
{
name: "include an ActionKey that is not in the action list",
inputCtrlWeights: map[string]float64{
"A": 1,
"B": 1,
"C": 1,
"D": 100,
},
inputActionList: map[ActionKey]Action{
"A": {},
"B": {},
"C": {},
},
},
} {
t.Log(tc.name)
weightsSum := 0.0
inputCtrlOpts := make(map[string]string)
for k, v := range tc.inputCtrlWeights {
// Do not weight actions that are not expected in the results
if _, ok := tc.inputActionList[ActionKey(k)]; !ok {
continue
}
inputCtrlOpts[k] = strconv.Itoa(int(v))
weightsSum += v
}
numTestLoops := 100000
results := make(map[ActionKey]int, len(tc.inputCtrlWeights))
for loop := 0; loop < numTestLoops; loop++ {
results[pickActionWeighted(inputCtrlOpts, tc.inputActionList)]++
}
for actionKey, count := range results {
p := tc.inputCtrlWeights[string(actionKey)] / weightsSum
exp := p * float64(numTestLoops)
errPcnt := math.Abs(exp-float64(count)) / exp
if errPcnt > 0.1 {
t.Errorf("Error in actual counts was above 10%% for %v (exp %v, actual %v)", actionKey, exp, count)
}
}
}
}
func TestActionsFilesystem(t *testing.T) {
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, "")
th, eng, err := newTestHarness(t, fsDataRepoPath, fsMetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
os.RemoveAll(fsRepoBaseDirPath)
}()
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
actionOpts := ActionOpts{
WriteRandomFilesActionKey: map[string]string{
fiofilewriter.MaxDirDepthField: "20",
fiofilewriter.MaxFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MinFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MaxNumFilesPerWriteField: "10",
fiofilewriter.MinNumFilesPerWriteField: "10",
fiofilewriter.MaxDedupePercentField: "100",
fiofilewriter.MinDedupePercentField: "100",
fiofilewriter.DedupePercentStepField: "1",
fiofilewriter.IOLimitPerWriteAction: "0",
},
}
numActions := 10
for loop := 0; loop < numActions; loop++ {
err := eng.RandomAction(actionOpts)
if !(err == nil || errors.Is(err, robustness.ErrNoOp)) {
t.Error("Hit error", err)
}
}
}
func TestActionsS3(t *testing.T) {
bucketName, cleanupCB := makeTempS3Bucket(t)
defer cleanupCB()
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, bucketName)
th, eng, err := newTestHarness(t, s3DataRepoPath, s3MetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
}()
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
actionOpts := ActionOpts{
WriteRandomFilesActionKey: map[string]string{
fiofilewriter.MaxDirDepthField: "20",
fiofilewriter.MaxFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MinFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MaxNumFilesPerWriteField: "10",
fiofilewriter.MinNumFilesPerWriteField: "10",
fiofilewriter.MaxDedupePercentField: "100",
fiofilewriter.MinDedupePercentField: "100",
fiofilewriter.DedupePercentStepField: "1",
fiofilewriter.IOLimitPerWriteAction: "0",
},
}
numActions := 10
for loop := 0; loop < numActions; loop++ {
err := eng.RandomAction(actionOpts)
if !(err == nil || errors.Is(err, robustness.ErrNoOp)) {
t.Error("Hit error", err)
}
}
}
func TestIOLimitPerWriteAction(t *testing.T) {
// Instruct a write action to write a large amount of data, but add
// an I/O limit parameter. Expect that the FIO action should limit
// the amount of data it writes.
os.Setenv(snapmeta.EngineModeEnvKey, snapmeta.EngineModeBasic)
os.Setenv(snapmeta.S3BucketNameEnvKey, "")
th, eng, err := newTestHarness(t, fsDataRepoPath, fsMetadataRepoPath)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) || errors.Is(err, fio.ErrEnvNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
defer func() {
cleanupErr := th.Cleanup()
testenv.AssertNoError(t, cleanupErr)
os.RemoveAll(fsRepoBaseDirPath)
}()
ctx := context.TODO()
err = eng.Init(ctx)
testenv.AssertNoError(t, err)
ioLimitBytes := 1 * 1024 * 1024
actionOpts := ActionOpts{
ActionControlActionKey: map[string]string{
string(SnapshotDirActionKey): strconv.Itoa(0),
string(RestoreSnapshotActionKey): strconv.Itoa(0),
string(DeleteRandomSnapshotActionKey): strconv.Itoa(0),
string(WriteRandomFilesActionKey): strconv.Itoa(1),
string(DeleteRandomSubdirectoryActionKey): strconv.Itoa(0),
},
WriteRandomFilesActionKey: map[string]string{
fiofilewriter.MaxDirDepthField: "2",
fiofilewriter.MaxFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MinFileSizeField: strconv.Itoa(10 * 1024 * 1024),
fiofilewriter.MaxNumFilesPerWriteField: "100",
fiofilewriter.MinNumFilesPerWriteField: "100",
fiofilewriter.IOLimitPerWriteAction: strconv.Itoa(ioLimitBytes),
},
}
err = eng.RandomAction(actionOpts)
testenv.AssertNoError(t, err)
size := 0
// The FIO write operation is expected to create multiple files.
// The behavior is that I/O will begin on a file, writing randomly
// to that file until the I/O limit is hit. Thus we expect to see
// one file with non-zero size (should be approx. between min-max file size
// parameters, i.e. 10 MiB) and 1 MiB or less of non-zero data
// written to it, due to the I/O limit.
walkFunc := func(path string, info fs.FileInfo, err error) error {
if !info.IsDir() && info.Size() > 0 {
fileContentB, err := ioutil.ReadFile(path)
testenv.AssertNoError(t, err)
nonZeroByteCount := 0
for _, byteVal := range fileContentB {
if byteVal > 0 {
nonZeroByteCount++
}
}
size += nonZeroByteCount
}
return nil
}
fioPath := eng.FileWriter.DataDirectory()
// Walk the FIO data directory tree, counting the non-zero data written.
err = filepath.Walk(fioPath, walkFunc)
testenv.AssertNoError(t, err)
if got, want := size, ioLimitBytes; got > want {
t.Fatalf("IO write limit exceeded for write action. Wrote %v B with io limit %v", got, want)
}
// We might expect that a '0' gets written as part of the FIO data. This
// means the count of non-zero bytes above might be a bit less than the exact
// i/o limit parameter. We shouldn't expect a large percent of '0' though, so
// this check will ensure fio didn't write significantly less than the limit.
// A fraction of at least 95% non-zero should be very conservative.
const thresholdNonZeroFraction = 0.95
if got, want := float64(size), float64(ioLimitBytes)*thresholdNonZeroFraction; got <= want {
t.Fatalf("IO write limit exceeded for write action. Wrote %v B with io limit %v", got, want)
}
}
func TestStatsPersist(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "stats-persist-test")
testenv.AssertNoError(t, err)
defer os.RemoveAll(tmpDir)
snapStore, err := snapmeta.NewPersister(tmpDir)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
err = snapStore.ConnectOrCreateFilesystem(tmpDir)
testenv.AssertNoError(t, err)
actionstats := &ActionStats{
Count: 120,
TotalRuntime: 25 * time.Hour,
MinRuntime: 5 * time.Minute,
MaxRuntime: 35 * time.Minute,
}
creationTime := clock.Now().Add(-time.Hour)
eng := &Engine{
MetaStore: snapStore,
CumulativeStats: Stats{
ActionCounter: 11235,
CreationTime: creationTime,
PerActionStats: map[ActionKey]*ActionStats{
ActionKey("some-action"): actionstats,
},
DataRestoreCount: 99,
},
}
err = eng.saveStats()
testenv.AssertNoError(t, err)
err = eng.MetaStore.FlushMetadata()
testenv.AssertNoError(t, err)
snapStoreNew, err := snapmeta.NewPersister(tmpDir)
testenv.AssertNoError(t, err)
// Connect to the same metadata store
err = snapStoreNew.ConnectOrCreateFilesystem(tmpDir)
testenv.AssertNoError(t, err)
err = snapStoreNew.LoadMetadata()
testenv.AssertNoError(t, err)
engNew := &Engine{
MetaStore: snapStoreNew,
}
err = engNew.loadStats()
testenv.AssertNoError(t, err)
if got, want := engNew.Stats(), eng.Stats(); got != want {
t.Errorf("Stats do not match\n%v\n%v", got, want)
}
fmt.Println(eng.Stats())
fmt.Println(engNew.Stats())
}
func TestLogsPersist(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "logs-persist-test")
testenv.AssertNoError(t, err)
defer os.RemoveAll(tmpDir)
snapStore, err := snapmeta.NewPersister(tmpDir)
if errors.Is(err, kopiarunner.ErrExeVariableNotSet) {
t.Skip(err)
}
testenv.AssertNoError(t, err)
err = snapStore.ConnectOrCreateFilesystem(tmpDir)
testenv.AssertNoError(t, err)
logData := Log{
Log: []*LogEntry{
{
StartTime: clock.Now().Add(-time.Hour),
EndTime: clock.Now(),
Action: ActionKey("some action"),
Error: "some error",
Idx: 11235,
ActionOpts: map[string]string{
"opt1": "opt1 value",
},
CmdOpts: map[string]string{
"cmdOpt": "cmdOptVal",
},
},
},
}
eng := &Engine{
MetaStore: snapStore,
EngineLog: logData,
}
err = eng.saveLog()
testenv.AssertNoError(t, err)
err = eng.MetaStore.FlushMetadata()
testenv.AssertNoError(t, err)
snapStoreNew, err := snapmeta.NewPersister(tmpDir)
testenv.AssertNoError(t, err)
// Connect to the same metadata store
err = snapStoreNew.ConnectOrCreateFilesystem(tmpDir)
testenv.AssertNoError(t, err)
err = snapStoreNew.LoadMetadata()
testenv.AssertNoError(t, err)
engNew := &Engine{
MetaStore: snapStoreNew,
}
err = engNew.loadLog()
testenv.AssertNoError(t, err)
if got, want := engNew.EngineLog.String(), eng.EngineLog.String(); got != want {
t.Errorf("Logs do not match\n%v\n%v", got, want)
}
}
type testHarness struct {
fw *fiofilewriter.FileWriter
ks *snapmeta.KopiaSnapshotter
kp *snapmeta.KopiaPersister
baseDir string
eng *Engine
}
func newTestHarness(t *testing.T, dataRepoPath, metaRepoPath string) (*testHarness, *Engine, error) {
t.Helper()
var (
th = &testHarness{}
err error
)
if th.baseDir, err = ioutil.TempDir("", "engine-data-"); err != nil {
return nil, nil, err
}
if th.fw, err = fiofilewriter.New(); err != nil {
th.Cleanup()
return nil, nil, err
}
if th.ks, err = snapmeta.NewSnapshotter(th.baseDir); err != nil {
th.Cleanup()
return nil, nil, err
}
if err = th.ks.ConnectOrCreateRepo(dataRepoPath); err != nil {
th.Cleanup()
return nil, nil, err
}
if th.kp, err = snapmeta.NewPersister(th.baseDir); err != nil {
th.Cleanup()
return nil, nil, err
}
if err = th.kp.ConnectOrCreateRepo(metaRepoPath); err != nil {
th.Cleanup()
return nil, nil, err
}
if th.eng, err = New(th.args()); err != nil {
th.Cleanup()
return nil, nil, err
}
return th, th.eng, err
}
func (th *testHarness) args() *Args {
return &Args{
MetaStore: th.kp,
TestRepo: th.ks,
FileWriter: th.fw,
WorkingDir: th.baseDir,
}
}
func (th *testHarness) FioRunner() *fio.Runner {
return th.fw.Runner
}
func (th *testHarness) Cleanup() error {
var err error
if th.eng != nil {
err = th.eng.Shutdown()
}
if th.fw != nil {
th.fw.Cleanup()
}
if th.ks != nil {
if sc := th.ks.ServerCmd(); sc != nil {
if err = sc.Process.Signal(syscall.SIGTERM); err != nil {
log.Println("Failed to send termination signal to kopia server process:", err)
}
}
th.ks.Cleanup()
}
if th.kp != nil {
th.kp.Cleanup()
}
os.RemoveAll(th.baseDir)
return err
}