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()
|
restic := j.NewRestic()
|
||||||
jobDir := j.JobDir()
|
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)
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -9,6 +11,19 @@ import (
|
|||||||
"time"
|
"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 {
|
func maybeAddArgString(args []string, name, value string) []string {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return append(args, name, value)
|
return append(args, name, value)
|
||||||
@ -45,6 +60,12 @@ type CommandOptions interface {
|
|||||||
ToArgs() []string
|
ToArgs() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GenericOpts []string
|
||||||
|
|
||||||
|
func (o GenericOpts) ToArgs() []string {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
type NoOpts struct{}
|
type NoOpts struct{}
|
||||||
|
|
||||||
func (NoOpts) ToArgs() []string {
|
func (NoOpts) ToArgs() []string {
|
||||||
@ -220,7 +241,7 @@ func (rcmd Restic) BuildEnv() []string {
|
|||||||
return envList
|
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{}
|
args := []string{}
|
||||||
if rcmd.GlobalOpts != nil {
|
if rcmd.GlobalOpts != nil {
|
||||||
args = rcmd.GlobalOpts.ToArgs()
|
args = rcmd.GlobalOpts.ToArgs()
|
||||||
@ -232,37 +253,87 @@ func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs
|
|||||||
|
|
||||||
cmd := exec.Command("restic", args...)
|
cmd := exec.Command("restic", args...)
|
||||||
|
|
||||||
cmd.Stdout = NewLogWriter(rcmd.Logger)
|
output := NewCapturedLogWriter(rcmd.Logger)
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stdout = output
|
||||||
|
cmd.Stderr = output
|
||||||
cmd.Env = rcmd.BuildEnv()
|
cmd.Env = rcmd.BuildEnv()
|
||||||
cmd.Dir = rcmd.Cwd
|
cmd.Dir = rcmd.Cwd
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
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 {
|
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 {
|
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 {
|
func (rcmd Restic) Forget(forgetOpts ForgetOpts) error {
|
||||||
return rcmd.RunRestic("forget", forgetOpts)
|
_, err := rcmd.RunRestic("forget", forgetOpts)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rcmd Restic) Check() error {
|
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 {
|
func (rcmd Restic) EnsureInit() error {
|
||||||
if err := rcmd.RunRestic("snapshots", NoOpts{}); err != nil {
|
if err := rcmd.Snapshots(); errors.Is(err, ErrRepoNotFound) {
|
||||||
return rcmd.RunRestic("init", NoOpts{})
|
_, err := rcmd.RunRestic("init", NoOpts{})
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
105
restic_test.go
105
restic_test.go
@ -1,7 +1,10 @@
|
|||||||
package main_test
|
package main_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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)
|
return GetLogger(childName)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogWriter struct {
|
type CapturedLogWriter struct {
|
||||||
|
Lines []string
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogWriter(logger *log.Logger) *LogWriter {
|
func NewCapturedLogWriter(logger *log.Logger) *CapturedLogWriter {
|
||||||
return &LogWriter{logger}
|
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)
|
message := string(content)
|
||||||
for _, line := range strings.Split(message, "\n") {
|
for _, line := range strings.Split(message, "\n") {
|
||||||
|
w.Lines = append(w.Lines, line)
|
||||||
w.logger.Printf(" %s", 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
|
cmd := exec.Command("sh", "-c", strings.TrimSpace(script)) // nolint:gosec
|
||||||
|
|
||||||
// Make both stderr and stdout go to logger
|
// Make both stderr and stdout go to logger
|
||||||
cmd.Stdout = NewLogWriter(logger)
|
cmd.Stdout = NewCapturedLogWriter(logger)
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stderr = cmd.Stdout
|
||||||
|
|
||||||
// Set working directory
|
// 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 {
|
if diff := deep.Equal(expected, actual); diff != nil {
|
||||||
t.Errorf("%s: %v", message, diff)
|
t.Errorf("%s: %v", message, diff)
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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) {
|
func TestMergeEnvMap(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user