added backup wrapper exec.Command tests
This commit is contained in:
parent
d3f16fe958
commit
8f5b6c0727
|
@ -1,5 +1,6 @@
|
||||||
.idea/
|
.idea/
|
||||||
on_exit_*.txt
|
on_exit_*.txt
|
||||||
restictray
|
restictray
|
||||||
|
config.json
|
||||||
restictray.app/
|
restictray.app/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
2
TODO.md
2
TODO.md
|
@ -14,3 +14,5 @@
|
||||||
- [X] exit code 3: https://restic.readthedocs.io/en/stable/040_backup.html likely to be a permission problem, perhaps ignoring instead of panicking as of now?
|
- [X] exit code 3: https://restic.readthedocs.io/en/stable/040_backup.html likely to be a permission problem, perhaps ignoring instead of panicking as of now?
|
||||||
- [ ] `restic list locks` showed a dangling one; PID of already gone `restic` process. Why? Unable to reproduce?
|
- [ ] `restic list locks` showed a dangling one; PID of already gone `restic` process. Why? Unable to reproduce?
|
||||||
- [ ] Verify backups with `restic check`? If not okay, remove (snapshot/folder?) and rebackup? What's the plan then?
|
- [ ] Verify backups with `restic check`? If not okay, remove (snapshot/folder?) and rebackup? What's the plan then?
|
||||||
|
- [ ] Implement `restic init`, reducing the need for an existing backup
|
||||||
|
- [ ] Cross-platform compatibility with Linux? Shouldn't be hard.
|
|
@ -22,29 +22,30 @@ const (
|
||||||
var home, _ = os.UserHomeDir()
|
var home, _ = os.UserHomeDir()
|
||||||
var executable, _ = os.Executable()
|
var executable, _ = os.Executable()
|
||||||
var isDev = os.Getenv("RESTICTRAY_DEV")
|
var isDev = os.Getenv("RESTICTRAY_DEV")
|
||||||
|
var configDir = home + "/.resitc/"
|
||||||
|
|
||||||
func IsDev() bool {
|
func IsDev() bool {
|
||||||
return isDev != ""
|
return isDev != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func PasswordFile() string {
|
func PasswordFile() string {
|
||||||
return home + "/.restic/password.txt"
|
return configDir + "password.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogFile() string {
|
func LogFile() string {
|
||||||
return home + "/.restic/log.txt"
|
return configDir + "log.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExcludeFile() string {
|
func ExcludeFile() string {
|
||||||
return home + "/.restic/excludes.txt"
|
return configDir + "excludes.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigFile() string {
|
func ConfigFile() string {
|
||||||
return home + "/.restic/config.json"
|
return configDir + "config.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cnf *Config) MountDir() string {
|
func (cnf *Config) MountDir() string {
|
||||||
return home + "/.restic/mnt"
|
return configDir + "mnt"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cnf *Config) CreateMountDirIfDoesntExist() error {
|
func (cnf *Config) CreateMountDirIfDoesntExist() error {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configDir = "./"
|
||||||
|
os.RemoveAll("config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfig_NoFile_Error(t *testing.T) {
|
||||||
|
config, err := ReadConfig()
|
||||||
|
assert.Nilf(t, config, "expected config to be nil")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfig_MalformedJSON_Error(t *testing.T) {
|
||||||
|
os.WriteFile("config.json", []byte("{ whoops"), fs.ModePerm)
|
||||||
|
config, err := ReadConfig()
|
||||||
|
assert.Nilf(t, config, "expected config to be nil")
|
||||||
|
assert.ErrorContainsf(t, err, "malformed", "expected err to contain malformed msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfig_ValidJSONFile_Ok(t *testing.T) {
|
||||||
|
os.WriteFile("config.json", []byte("{\"backupTimeInHours\": 5, \"repository\": \"repodir\", \"backup\": \"backupfiles\"}"), fs.ModePerm)
|
||||||
|
config, err := ReadConfig()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 5, config.BackupTimeInHours)
|
||||||
|
assert.Equal(t, "repodir", config.Repository)
|
||||||
|
assert.Equal(t, "backupfiles", config.Backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfig_WithoutBackupTime_DefaultsTo24hrs(t *testing.T) {
|
||||||
|
os.WriteFile("config.json", []byte("{\"repository\": \"repodir\", \"backup\": \"backupfiles\"}"), fs.ModePerm)
|
||||||
|
config, err := ReadConfig()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 24, config.BackupTimeInHours)
|
||||||
|
assert.Equal(t, "repodir", config.Repository)
|
||||||
|
assert.Equal(t, "backupfiles", config.Backup)
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ func (w *Wrapper) Cleanup() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resticCmd(args ...string) *exec.Cmd {
|
func resticCmdFn(args ...string) *exec.Cmd {
|
||||||
// Running from GoLand: could be /private/var/folders/5s/csgpcjlx1wg9659_485vqz880000gn/T/GoLand/restictray
|
// Running from GoLand: could be /private/var/folders/5s/csgpcjlx1wg9659_485vqz880000gn/T/GoLand/restictray
|
||||||
// Installed: could be /Applications/restictray/restictray.app/Contents/MacOS/restictray
|
// Installed: could be /Applications/restictray/restictray.app/Contents/MacOS/restictray
|
||||||
// This isn't ideal but I don't want to fiddle with build flags
|
// This isn't ideal but I don't want to fiddle with build flags
|
||||||
|
@ -67,6 +67,9 @@ func resticCmd(args ...string) *exec.Cmd {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resticCmd = resticCmdFn
|
||||||
|
var cmdTimeout = 10 * time.Second
|
||||||
|
|
||||||
/*
|
/*
|
||||||
UpdateLatestSnapshots updates LatestSnapshots or returns an error.
|
UpdateLatestSnapshots updates LatestSnapshots or returns an error.
|
||||||
Expected restic snapshot JSON format:
|
Expected restic snapshot JSON format:
|
||||||
|
@ -165,7 +168,7 @@ func (w *Wrapper) Backup(c *Config) error {
|
||||||
select {
|
select {
|
||||||
case <-busy:
|
case <-busy:
|
||||||
busy = nil // It's alive! continue with cmd.Wait()
|
busy = nil // It's alive! continue with cmd.Wait()
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(cmdTimeout):
|
||||||
killErr := cmd.Process.Kill()
|
killErr := cmd.Process.Kill()
|
||||||
return fmt.Errorf("backup output timeout, repository server down?: %w", killErr)
|
return fmt.Errorf("backup output timeout, repository server down?: %w", killErr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapper_LastSnapshot(t *testing.T) {
|
||||||
|
shot1 := ResticSnapshot{
|
||||||
|
Id: "shot1",
|
||||||
|
Time: time.Date(2020, 10, 10, 10, 10, 10, 10, time.UTC),
|
||||||
|
}
|
||||||
|
shot2 := ResticSnapshot{
|
||||||
|
Id: "shot2",
|
||||||
|
Time: time.Date(2022, 11, 11, 11, 11, 11, 11, time.UTC),
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
snapshots []ResticSnapshot
|
||||||
|
expected ResticSnapshot
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"None yet",
|
||||||
|
[]ResticSnapshot{},
|
||||||
|
ResticSnapshot{
|
||||||
|
Id: "(no snapshots yet)",
|
||||||
|
Time: time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"take the last from the array",
|
||||||
|
[]ResticSnapshot{shot1, shot2},
|
||||||
|
shot2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.label, func(t *testing.T) {
|
||||||
|
wrapper := Wrapper{
|
||||||
|
LatestSnapshots: tc.snapshots,
|
||||||
|
}
|
||||||
|
last := wrapper.LastSnapshot()
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected.Id, last.Id)
|
||||||
|
assert.Equal(t, tc.expected.Time.Hour(), last.Time.Hour())
|
||||||
|
assert.Equal(t, tc.expected.Time.Minute(), last.Time.Minute())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a very clever way to test exec.Command in go: https://jamiethompson.me/posts/Unit-Testing-Exec-Command-In-Golang/
|
||||||
|
func fakeExecCommand(whichTest string) func(args ...string) *exec.Cmd {
|
||||||
|
return func(args ...string) *exec.Cmd {
|
||||||
|
cs := []string{"-test.run=" + whichTest, "--", "restic"}
|
||||||
|
cs = append(cs, args...)
|
||||||
|
cmd := exec.Command(os.Args[0], cs...)
|
||||||
|
cmd.Env = []string{"GO_TEST_PROCESS=1"}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdSuccessWithinTime(t *testing.T) {
|
||||||
|
if os.Getenv("GO_TEST_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "some output\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
func TestCmdExitCode1WithinTime(t *testing.T) {
|
||||||
|
if os.Getenv("GO_TEST_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "some output\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
func TestCmdExitCode3WithinTime(t *testing.T) {
|
||||||
|
if os.Getenv("GO_TEST_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "some output\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdTakesLongerThan2sButOutputsFaster(t *testing.T) {
|
||||||
|
if os.Getenv("GO_TEST_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// don't forget \n as the scanner splits on ScanLines
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
fmt.Fprintf(os.Stdout, "some output after 1s\n")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
fmt.Fprintf(os.Stdout, "some output after 3s\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdTakesLongerThan2sToOutputAnything(t *testing.T) {
|
||||||
|
if os.Getenv("GO_TEST_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
fmt.Fprintf(os.Stdout, "some output, too late!\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackup_ExitCode1_ReturnsError(t *testing.T) {
|
||||||
|
resticCmd = fakeExecCommand("TestCmdExitCode1WithinTime")
|
||||||
|
|
||||||
|
wrapper := Wrapper{}
|
||||||
|
err := wrapper.Backup(&Config{})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackup_ExitCode3_IssuesWarning(t *testing.T) {
|
||||||
|
resticCmd = fakeExecCommand("TestCmdExitCode3WithinTime")
|
||||||
|
var logBuffer bytes.Buffer
|
||||||
|
log.Logger = log.Output(&logBuffer)
|
||||||
|
|
||||||
|
wrapper := Wrapper{}
|
||||||
|
err := wrapper.Backup(&Config{})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, logBuffer.String(), "permission problem?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackup_OutputsAndFinishesAsExpected(t *testing.T) {
|
||||||
|
resticCmd = fakeExecCommand("TestCmdSuccessWithinTime")
|
||||||
|
var logBuffer bytes.Buffer
|
||||||
|
log.Logger = log.Output(&logBuffer)
|
||||||
|
|
||||||
|
wrapper := Wrapper{}
|
||||||
|
err := wrapper.Backup(&Config{})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, logBuffer.String(), "some output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackup_OutputsBefore2sButStillTakesLonger(t *testing.T) {
|
||||||
|
resticCmd = fakeExecCommand("TestCmdTakesLongerThan2sButOutputsFaster")
|
||||||
|
cmdTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
wrapper := Wrapper{}
|
||||||
|
err := wrapper.Backup(&Config{})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackup_TakesLongerThanNeededToOutput_TimesOut(t *testing.T) {
|
||||||
|
resticCmd = fakeExecCommand("TestCmdTakesLongerThan2sToOutputAnything")
|
||||||
|
cmdTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
wrapper := Wrapper{}
|
||||||
|
err := wrapper.Backup(&Config{})
|
||||||
|
|
||||||
|
assert.ErrorContainsf(t, err, "backup output timeout", "timeout expected")
|
||||||
|
}
|
Loading…
Reference in New Issue