added backup wrapper exec.Command tests

This commit is contained in:
Wouter Groeneveld 2023-03-07 13:50:04 +01:00
parent d3f16fe958
commit 8f5b6c0727
6 changed files with 224 additions and 8 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.idea/
on_exit_*.txt
restictray
config.json
restictray.app/
.DS_Store

View File

@ -13,4 +13,6 @@
- [ ] `restic backup` can result in `Warning: at least one source file could not be read\n: exit status 3`. This returns an error, but the backup itself seems to be all right.
- [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?
- [ ] 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.

View File

@ -22,29 +22,30 @@ const (
var home, _ = os.UserHomeDir()
var executable, _ = os.Executable()
var isDev = os.Getenv("RESTICTRAY_DEV")
var configDir = home + "/.resitc/"
func IsDev() bool {
return isDev != ""
}
func PasswordFile() string {
return home + "/.restic/password.txt"
return configDir + "password.txt"
}
func LogFile() string {
return home + "/.restic/log.txt"
return configDir + "log.txt"
}
func ExcludeFile() string {
return home + "/.restic/excludes.txt"
return configDir + "excludes.txt"
}
func ConfigFile() string {
return home + "/.restic/config.json"
return configDir + "config.json"
}
func (cnf *Config) MountDir() string {
return home + "/.restic/mnt"
return configDir + "mnt"
}
func (cnf *Config) CreateMountDirIfDoesntExist() error {

46
restic/config_test.go Normal file
View File

@ -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)
}

View File

@ -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
// Installed: could be /Applications/restictray/restictray.app/Contents/MacOS/restictray
// 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
}
var resticCmd = resticCmdFn
var cmdTimeout = 10 * time.Second
/*
UpdateLatestSnapshots updates LatestSnapshots or returns an error.
Expected restic snapshot JSON format:
@ -165,7 +168,7 @@ func (w *Wrapper) Backup(c *Config) error {
select {
case <-busy:
busy = nil // It's alive! continue with cmd.Wait()
case <-time.After(10 * time.Second):
case <-time.After(cmdTimeout):
killErr := cmd.Process.Kill()
return fmt.Errorf("backup output timeout, repository server down?: %w", killErr)
}

163
restic/wrapper_test.go Normal file
View File

@ -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")
}