Add integration test for restic methods

This commit is contained in:
IamTheFij 2022-03-24 14:59:40 -07:00
parent 870d19fd27
commit a746859386
5 changed files with 205 additions and 17 deletions

2
job.go
View File

@ -484,7 +484,7 @@ func (j Job) RunRestore() error {
restic := j.NewRestic()
jobDir := j.JobDir()
if err := restic.RunRestic("snapshots", NoOpts{}); err != nil {
if _, err := restic.RunRestic("snapshots", NoOpts{}); errors.Is(err, ErrRepoNotFound) {
return fmt.Errorf("no repository or snapshots for job %s: %w", j.Name, err)
}

View File

@ -1,6 +1,8 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
@ -9,6 +11,19 @@ import (
"time"
)
var ErrRestic = errors.New("restic error")
var ErrRepoNotFound = errors.New("repository not found or uninitialized")
func lineIn(needle string, haystack []string) bool {
for _, line := range haystack {
if line == needle {
return true
}
}
return false
}
func maybeAddArgString(args []string, name, value string) []string {
if value != "" {
return append(args, name, value)
@ -45,6 +60,12 @@ type CommandOptions interface {
ToArgs() []string
}
type GenericOpts []string
func (o GenericOpts) ToArgs() []string {
return o
}
type NoOpts struct{}
func (NoOpts) ToArgs() []string {
@ -220,7 +241,7 @@ func (rcmd Restic) BuildEnv() []string {
return envList
}
func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs ...string) error {
func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs ...string) ([]string, error) {
args := []string{}
if rcmd.GlobalOpts != nil {
args = rcmd.GlobalOpts.ToArgs()
@ -232,37 +253,87 @@ func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs
cmd := exec.Command("restic", args...)
cmd.Stdout = NewLogWriter(rcmd.Logger)
cmd.Stderr = cmd.Stdout
output := NewCapturedLogWriter(rcmd.Logger)
cmd.Stdout = output
cmd.Stderr = output
cmd.Env = rcmd.BuildEnv()
cmd.Dir = rcmd.Cwd
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running restic %s: %w", command, err)
responseErr := ErrRestic
if lineIn("Is there a repository at the following location?", output.Lines) {
responseErr = ErrRepoNotFound
}
return output.Lines, fmt.Errorf("error running restic %s: %w", command, responseErr)
}
return nil
return output.Lines, nil
}
func (rcmd Restic) Backup(files []string, opts BackupOpts) error {
return rcmd.RunRestic("backup", opts, files...)
_, err := rcmd.RunRestic("backup", opts, files...)
return err
}
func (rcmd Restic) Restore(snapshot string, opts RestoreOpts) error {
return rcmd.RunRestic("restore", opts, snapshot)
_, err := rcmd.RunRestic("restore", opts, snapshot)
return err
}
func (rcmd Restic) Forget(forgetOpts ForgetOpts) error {
return rcmd.RunRestic("forget", forgetOpts)
_, err := rcmd.RunRestic("forget", forgetOpts)
return err
}
func (rcmd Restic) Check() error {
return rcmd.RunRestic("check", NoOpts{})
_, err := rcmd.RunRestic("check", NoOpts{})
return err
}
type Snapshot struct {
UID int `json:"uid"`
GID int `json:"gid"`
Time string `json:"time"`
Tree string `json:"tree"`
Hostname string `json:"hostname"`
Username string `json:"username"`
ID string `json:"id"`
ShortID string `json:"short_id"` // nolint:tagliatelle
Paths []string `json:"paths"`
Tags []string `json:"tags,omitempty"`
}
func (rcmd Restic) ReadSnapshots() ([]Snapshot, error) {
lines, err := rcmd.RunRestic("snapshots", GenericOpts{"--json"})
if err != nil {
return nil, err
}
snapshots := new([]Snapshot)
if err = json.Unmarshal([]byte(lines[0]), snapshots); err != nil {
return nil, fmt.Errorf("failed parsing snapshot results: %w", err)
}
return *snapshots, nil
}
func (rcmd Restic) Snapshots() error {
_, err := rcmd.RunRestic("snapshots", NoOpts{})
return err
}
func (rcmd Restic) EnsureInit() error {
if err := rcmd.RunRestic("snapshots", NoOpts{}); err != nil {
return rcmd.RunRestic("init", NoOpts{})
if err := rcmd.Snapshots(); errors.Is(err, ErrRepoNotFound) {
_, err := rcmd.RunRestic("init", NoOpts{})
return err
}
return nil

View File

@ -1,7 +1,10 @@
package main_test
import (
"errors"
"log"
"os"
"path/filepath"
"testing"
"time"
@ -182,3 +185,105 @@ func TestBuildEnv(t *testing.T) {
})
}
}
func TestResticInterface(t *testing.T) {
t.Parallel()
dataDir := t.TempDir()
repoDir := t.TempDir()
cacheDir := t.TempDir()
restoreTarget := t.TempDir()
dataFile := filepath.Join(dataDir, "test.txt")
restoredDataFile := filepath.Join(restoreTarget, dataFile)
restic := main.Restic{
Logger: log.New(os.Stderr, t.Name()+":", log.Lmsgprefix),
Repo: repoDir,
Env: map[string]string{},
Passphrase: "Correct.Horse.Battery.Staple",
// nolint:exhaustivestruct
GlobalOpts: &main.ResticGlobalOpts{
CacheDir: cacheDir,
},
Cwd: dataDir,
}
// Write test file to the data dir
err := os.WriteFile(dataFile, []byte("testing"), 0644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Make sure no existing repo is found
_, err = restic.ReadSnapshots()
if err == nil || !errors.Is(err, main.ErrRepoNotFound) {
AssertEqualFail(t, "didn't get expected error for backup", main.ErrRepoNotFound, err)
}
// Try to backup when repo is not initialized
err = restic.Backup([]string{dataDir}, main.BackupOpts{}) // nolint:exhaustivestruct
if !errors.Is(err, main.ErrRepoNotFound) {
AssertEqualFail(t, "unexpected error creating making backup", nil, err)
}
// Init repo
err = restic.EnsureInit()
AssertEqualFail(t, "unexpected error initializing repo", nil, err)
// Verify it can be reinitialized with no issues
err = restic.EnsureInit()
AssertEqualFail(t, "unexpected error reinitializing repo", nil, err)
// Backup for real this time
err = restic.Backup([]string{dataDir}, main.BackupOpts{Tags: []string{"test"}}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error creating making backup", nil, err)
// Check snapshots
expectedHostname, _ := os.Hostname()
snapshots, err := restic.ReadSnapshots()
AssertEqualFail(t, "unexpected error reading snapshots", nil, err)
AssertEqual(t, "unexpected number of snapshots", 1, len(snapshots))
AssertEqual(t, "unexpected snapshot value: hostname", expectedHostname, snapshots[0].Hostname)
AssertEqual(t, "unexpected snapshot value: paths", []string{dataDir}, snapshots[0].Paths)
AssertEqual(t, "unexpected snapshot value: tags", []string{"test"}, snapshots[0].Tags)
// Backup again
err = restic.Backup([]string{dataDir}, main.BackupOpts{}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error creating making second backup", nil, err)
// Check for second backup
snapshots, err = restic.ReadSnapshots()
AssertEqualFail(t, "unexpected error reading second snapshots", nil, err)
AssertEqual(t, "unexpected number of snapshots", 2, len(snapshots))
// Forget one backup
err = restic.Forget(main.ForgetOpts{KeepLast: 1, Prune: true}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error forgetting snapshot", nil, err)
// Check forgotten snapshot
snapshots, err = restic.ReadSnapshots()
AssertEqualFail(t, "unexpected error reading post forget snapshots", nil, err)
AssertEqual(t, "unexpected number of snapshots", 1, len(snapshots))
// Check restic repo
err = restic.Check()
AssertEqualFail(t, "unexpected error checking repo", nil, err)
// Change the data file
err = os.WriteFile(dataFile, []byte("unexpected"), 0644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Check that data wrote
value, err := os.ReadFile(dataFile)
AssertEqualFail(t, "unexpected error reading from test file", nil, err)
AssertEqualFail(t, "incorrect value in test file (we expect the unexpected!)", "unexpected", string(value))
// Restore files
err = restic.Restore("latest", main.RestoreOpts{Target: restoreTarget}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error restoring latest snapshot", nil, err)
// Check restored values
value, err = os.ReadFile(restoredDataFile)
AssertEqualFail(t, "unexpected error reading from test file", nil, err)
AssertEqualFail(t, "incorrect value in test file", "testing", string(value))
}

View File

@ -30,17 +30,19 @@ func GetChildLogger(parent *log.Logger, name string) *log.Logger {
return GetLogger(childName)
}
type LogWriter struct {
type CapturedLogWriter struct {
Lines []string
logger *log.Logger
}
func NewLogWriter(logger *log.Logger) *LogWriter {
return &LogWriter{logger}
func NewCapturedLogWriter(logger *log.Logger) *CapturedLogWriter {
return &CapturedLogWriter{Lines: []string{}, logger: logger}
}
func (w LogWriter) Write(content []byte) (n int, err error) {
func (w *CapturedLogWriter) Write(content []byte) (n int, err error) {
message := string(content)
for _, line := range strings.Split(message, "\n") {
w.Lines = append(w.Lines, line)
w.logger.Printf(" %s", line)
}
@ -51,7 +53,7 @@ func RunShell(script string, cwd string, env map[string]string, logger *log.Logg
cmd := exec.Command("sh", "-c", strings.TrimSpace(script)) // nolint:gosec
// Make both stderr and stdout go to logger
cmd.Stdout = NewLogWriter(logger)
cmd.Stdout = NewCapturedLogWriter(logger)
cmd.Stderr = cmd.Stdout
// Set working directory

View File

@ -12,11 +12,21 @@ func AssertEqual(t *testing.T, message string, expected, actual interface{}) boo
if diff := deep.Equal(expected, actual); diff != nil {
t.Errorf("%s: %v", message, diff)
return false
}
return true
}
func AssertEqualFail(t *testing.T, message string, expected, actual interface{}) {
t.Helper()
if !AssertEqual(t, message, expected, actual) {
t.FailNow()
}
}
func TestMergeEnvMap(t *testing.T) {
t.Parallel()