added backup wrapper exec.Command tests
This commit is contained in:
parent
d3f16fe958
commit
8f5b6c0727
|
@ -1,5 +1,6 @@
|
|||
.idea/
|
||||
on_exit_*.txt
|
||||
restictray
|
||||
config.json
|
||||
restictray.app/
|
||||
.DS_Store
|
||||
|
|
4
TODO.md
4
TODO.md
|
@ -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.
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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