Add integration test for restic methods
This commit is contained in:
parent
870d19fd27
commit
a746859386
2
job.go
2
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)
|
||||
}
|
||||
|
||||
|
93
restic.go
93
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
|
||||
|
105
restic_test.go
105
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))
|
||||
}
|
||||
|
12
shell.go
12
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
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user