From a7468593861f31b2db223096f948723e25f3da61 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Thu, 24 Mar 2022 14:59:40 -0700 Subject: [PATCH] Add integration test for restic methods --- job.go | 2 +- restic.go | 93 +++++++++++++++++++++++++++++++++++++------ restic_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ shell.go | 12 +++--- utils_test.go | 10 +++++ 5 files changed, 205 insertions(+), 17 deletions(-) diff --git a/job.go b/job.go index 8fc5269..b211e30 100644 --- a/job.go +++ b/job.go @@ -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) } diff --git a/restic.go b/restic.go index d12f096..51040b2 100644 --- a/restic.go +++ b/restic.go @@ -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 diff --git a/restic_test.go b/restic_test.go index 5549677..ea8d4b5 100644 --- a/restic_test.go +++ b/restic_test.go @@ -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)) +} diff --git a/shell.go b/shell.go index 416a15d..7fc1c0b 100644 --- a/shell.go +++ b/shell.go @@ -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 diff --git a/utils_test.go b/utils_test.go index 5122dc3..624630b 100644 --- a/utils_test.go +++ b/utils_test.go @@ -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()