Files
backrest/pkg/restic/restic_test.go
2024-05-27 10:26:18 -07:00

561 lines
15 KiB
Go

package restic
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"testing"
"time"
"github.com/garethgeorge/backrest/test/helpers"
)
func TestResticInit(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
}
func TestResticBackup(t *testing.T) {
t.Parallel()
repo := t.TempDir()
// create a new repo with cache disabled for testing
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
testData2 := helpers.CreateTestData(t)
testDataUnreadable := t.TempDir()
helpers.CreateUnreadable(t, testDataUnreadable+"/unreadable")
var tests = []struct {
name string
opts []GenericOption
paths []string
files int64 // expected files at the end of the backup
wantErr bool
}{
{
name: "no options",
paths: []string{testData},
opts: []GenericOption{},
files: 100,
},
{
name: "with two paths",
paths: []string{testData, testData2},
opts: []GenericOption{},
files: 200,
},
{
name: "with exclude",
paths: []string{testData},
opts: []GenericOption{WithFlags("--exclude", "file1*")},
files: 90,
},
{
name: "with exclude pattern",
paths: []string{testData},
opts: []GenericOption{WithFlags("--iexclude=file*")},
files: 0,
},
{
name: "with nothing to backup",
paths: []string{},
opts: []GenericOption{},
wantErr: true,
},
{
name: "with unreadable file",
paths: []string{testData, testDataUnreadable},
opts: []GenericOption{},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gotEvent := false
summary, err := r.Backup(context.Background(), tc.paths, func(event *BackupProgressEntry) {
t.Logf("backup event: %v", event)
gotEvent = true
}, tc.opts...)
if (err != nil) != tc.wantErr {
t.Fatalf("wanted error: %v, got: %v", tc.wantErr, err)
}
if tc.wantErr {
return
}
if summary == nil {
t.Fatalf("wanted summary, got: nil")
}
if summary.TotalFilesProcessed != tc.files {
t.Errorf("wanted %d files, got: %d", tc.files, summary.TotalFilesProcessed)
}
if !gotEvent {
t.Errorf("wanted backup event, got: false")
}
})
}
}
func TestResticPartialBackup(t *testing.T) {
t.Parallel()
repo := t.TempDir()
// create a new repo with cache disabled for testing
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testDataUnreadable := t.TempDir()
unreadablePath := filepath.Join(testDataUnreadable, "unreadable")
helpers.CreateUnreadable(t, unreadablePath)
var entries []BackupProgressEntry
summary, err := r.Backup(context.Background(), []string{testDataUnreadable}, func(entry *BackupProgressEntry) {
entries = append(entries, *entry)
})
if !errors.Is(err, ErrPartialBackup) {
t.Fatalf("wanted error to be partial backup, got: %v", err)
}
if summary == nil {
t.Fatalf("wanted summary, got: nil")
}
if summary.TotalFilesProcessed != 0 {
t.Errorf("wanted 0 files, got: %d", summary.TotalFilesProcessed)
}
if !slices.ContainsFunc(entries, func(e BackupProgressEntry) bool {
return e.MessageType == "error" && e.Item == unreadablePath
}) {
t.Errorf("wanted entries to contain an error event for the unreadable file (%s), but did not find it", unreadablePath)
t.Logf("entries:\n")
for _, entry := range entries {
t.Logf("%+v\n", entry)
}
}
}
func TestResticBackupLots(t *testing.T) {
t.Parallel()
t.Skip("this test takes a long time to run")
repo := t.TempDir()
// create a new repo with cache disabled for testing
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
// backup 25 times
for i := 0; i < 25; i++ {
_, err := r.Backup(context.Background(), []string{testData}, func(e *BackupProgressEntry) {
t.Logf("backup event: %+v", e)
})
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
}
}
func TestSnapshot(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
for i := 0; i < 10; i++ {
_, err := r.Backup(context.Background(), []string{testData}, nil, WithFlags("--tag", fmt.Sprintf("tag%d", i)))
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
}
var tests = []struct {
name string
opts []GenericOption
count int
}{
{
name: "no options",
opts: []GenericOption{},
count: 10,
},
{
name: "with tag",
opts: []GenericOption{WithTags("tag1")},
count: 1,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
snapshots, err := r.Snapshots(context.Background(), tc.opts...)
if err != nil {
t.Fatalf("failed to list snapshots: %v", err)
}
if len(snapshots) != tc.count {
t.Errorf("wanted %d snapshots, got: %d", tc.count, len(snapshots))
}
// Ensure that snapshot timestamps are set, this is critical for correct ordering in the orchestrator.
for _, snapshot := range snapshots {
if snapshot.UnixTimeMs() == 0 {
t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs())
}
}
})
}
}
func TestLs(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
snapshot, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
_, entries, err := r.ListDirectory(context.Background(), snapshot.SnapshotId, toRepoPath(testData))
if err != nil {
t.Fatalf("failed to list directory: %v", err)
}
if len(entries) != 101 {
t.Errorf("wanted 101 entries, got: %d", len(entries))
}
}
func TestResticForget(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
ids := make([]string, 0)
for i := 0; i < 10; i++ {
output, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
ids = append(ids, output.SnapshotId)
}
// forget snapshots
res, err := r.Forget(context.Background(), &RetentionPolicy{KeepLastN: 3})
if err != nil {
t.Fatalf("failed to forget snapshots: %v", err)
}
if len(res.Keep) != 3 {
t.Errorf("wanted 3 snapshots to be kept, got: %d", len(res.Keep))
}
if len(res.Remove) != 7 {
t.Errorf("wanted 7 snapshots to be removed, got: %d", len(res.Remove))
}
removedIds := make([]string, 0)
for _, snapshot := range res.Remove {
removedIds = append(removedIds, snapshot.Id)
}
slices.Reverse(removedIds)
keptIds := make([]string, 0)
for _, snapshot := range res.Keep {
keptIds = append(keptIds, snapshot.Id)
}
slices.Reverse(keptIds)
if !reflect.DeepEqual(removedIds, ids[:7]) {
t.Errorf("wanted removed ids to be %v, got: %v", ids[:7], removedIds)
}
if !reflect.DeepEqual(keptIds, ids[7:]) {
t.Errorf("wanted kept ids to be %v, got: %v", ids[7:], keptIds)
}
}
func TestForgetSnapshotId(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
ids := make([]string, 0)
for i := 0; i < 5; i++ {
output, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
ids = append(ids, output.SnapshotId)
}
// forget snapshot by ID
err := r.ForgetSnapshot(context.Background(), ids[0])
if err != nil {
t.Fatalf("failed to forget snapshots: %v", err)
}
snapshots, err := r.Snapshots(context.Background())
if err != nil {
t.Fatalf("failed to list snapshots: %v", err)
}
if len(snapshots) != 4 {
t.Errorf("wanted 4 snapshots, got: %d", len(snapshots))
}
}
func TestResticPrune(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
for i := 0; i < 3; i++ {
_, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup: %v", err)
}
}
// forget recent snapshots
_, err := r.Forget(context.Background(), &RetentionPolicy{KeepLastN: 1})
if err != nil {
t.Fatalf("failed to forget snapshots: %v", err)
}
// prune all snapshots
output := bytes.NewBuffer(nil)
if err := r.Prune(context.Background(), output); err != nil {
t.Fatalf("failed to prune snapshots: %v", err)
}
wantStr := "collecting packs for deletion and repacking"
if !bytes.Contains(output.Bytes(), []byte(wantStr)) {
t.Errorf("wanted output to contain 'keep 1 snapshots', got: %s", output.String())
}
}
func TestResticRestore(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
restorePath := t.TempDir()
testData := helpers.CreateTestData(t)
dirCount := strings.Count(testData, string(filepath.Separator))
if runtime.GOOS == "windows" {
// On Windows, the volume name is also included as a dir in the path.
dirCount += 1
}
snapshot, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
// restore all files
summary, err := r.Restore(context.Background(), snapshot.SnapshotId, func(event *RestoreProgressEntry) {
t.Logf("restore event: %v", event)
}, WithFlags("--target", restorePath))
if err != nil {
t.Fatalf("failed to restore snapshot: %v", err)
}
// should be 100 files + parent directories.
fileCount := 100 + dirCount
if summary.TotalFiles != int64(fileCount) {
t.Errorf("wanted %d files to be restored, got: %d", fileCount, summary.TotalFiles)
}
}
func TestResticStats(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
_, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
// restore all files
stats, err := r.Stats(context.Background())
if err != nil {
t.Fatalf("failed to get stats: %v", err)
}
if stats.SnapshotsCount != 1 {
t.Errorf("wanted 1 snapshot, got: %d", stats.SnapshotsCount)
}
if stats.TotalSize == 0 {
t.Errorf("wanted non-zero total size, got: %d", stats.TotalSize)
}
if stats.TotalUncompressedSize == 0 {
t.Errorf("wanted non-zero total uncompressed size, got: %d", stats.TotalUncompressedSize)
}
if stats.TotalBlobCount == 0 {
t.Errorf("wanted non-zero total blob count, got: %d", stats.TotalBlobCount)
}
}
func TestResticCheck(t *testing.T) {
t.Parallel()
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
testData := helpers.CreateTestData(t)
_, err := r.Backup(context.Background(), []string{testData}, nil)
if err != nil {
t.Fatalf("failed to backup and create new snapshot: %v", err)
}
// check repo
output := bytes.NewBuffer(nil)
if err := r.Check(context.Background(), output, WithFlags("--read-data")); err != nil {
t.Fatalf("failed to check repo: %v", err)
}
wantStr := "no errors were found"
if !bytes.Contains(output.Bytes(), []byte(wantStr)) {
t.Errorf("wanted output to contain 'no errors were found', got: %s", output.String())
}
}
func toRepoPath(path string) string {
if runtime.GOOS != "windows" {
return path
}
// On Windows, the temp directory path needs to be converted to a repo path
// for restic to interpret it correctly in restore/snapshot operations.
sepIdx := strings.Index(path, string(filepath.Separator))
if sepIdx != 2 || path[1] != ':' {
return path
}
return filepath.ToSlash(filepath.Join(
string(filepath.Separator), // leading slash
string(path[0]), // drive volume
path[3:], // path
))
}
func BenchmarkBackup(t *testing.B) {
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
workdir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := r.Backup(context.Background(), []string{workdir}, func(e *BackupProgressEntry) {})
if err != nil {
t.Fatalf("failed to backup: %v", err)
}
}
}
func BenchmarkBackupWithSimulatedCallback(t *testing.B) {
repo := t.TempDir()
r := NewRepo(helpers.ResticBinary(t), repo, WithFlags("--no-cache"), WithEnv("RESTIC_PASSWORD=test"))
if err := r.Init(context.Background()); err != nil {
t.Fatalf("failed to init repo: %v", err)
}
workdir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := r.Backup(context.Background(), []string{workdir}, func(e *BackupProgressEntry) {
time.Sleep(50 * time.Millisecond) // simulate work being done in the callback
})
if err != nil {
t.Fatalf("failed to backup: %v", err)
}
}
}