Compare commits

...

10 Commits

Author SHA1 Message Date
489f3078fe bump version 2022-08-24 12:03:11 +02:00
Romain de Laage
e07dd0d991 Don't check if path is a directory (#220)
We only run a stat on the path to check there is no error
2022-08-22 17:28:38 +02:00
kenc
2b9dc9f17c Add test for locking behaviour (#211) 2022-06-27 09:03:34 +02:00
kenc
465bc037c2 Add lock tests (#209)
* Add lock tests

* Refactor setLock to accept key value pairs

This allows SetCron and Lock to use the same function setLockValue. It
also removes the need to call getLock explicitly in tests by returning
the lock object.
2022-06-06 12:59:47 +02:00
kenc
37a043afff Add location unit tests (#208) 2022-06-02 17:05:44 +02:00
kenc
e91b632181 Add backend unit tests (#207) 2022-06-01 14:58:38 +02:00
kenc
2b30998b9a Add options parsing unit tests (#205)
* Add options parsing unit tests

* Refactor into subtests
2022-05-30 12:56:25 +02:00
Varac
49b37a0a9a Add varacs ansible role (#201)
Co-authored-by: Nicco <hi@nicco.io>
2022-05-24 13:34:10 +02:00
3bc091f826 lean flag 2022-04-27 00:55:31 +02:00
5bcf5c9217 1.7.0 (#188)
* stream the output (#186)

* dont duplicate global flags (#187)

* docs for tagging

* fix self update path (#190)

* version bump & changelog
2022-04-27 00:48:52 +02:00
16 changed files with 700 additions and 78 deletions

View File

@@ -5,11 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.7.1] - 2022-04-27
### Fixed
- #178 Lean flag not working properly.
## [1.7.0] - 2022-04-27
### Changed
- #147 Stream output instead of buffering.
### Fixed
- #184 duplicate global options.
- #154 add docs for migration.
- #182 fix bug with upgrading custom restic with custom path.
## [1.6.2] - 2022-04-14
### Fixed
- Version bump in code
- Version bump in code.
## [1.6.1] - 2022-04-14

View File

@@ -13,7 +13,6 @@ var cronCmd = &cobra.Command{
Long: `Intended to be mainly triggered by an automated system like systemd or crontab. For each location checks if a cron backup is due and runs it.`,
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
flags.CRON_LEAN, _ = cmd.Flags().GetBool("lean")
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
@@ -25,5 +24,5 @@ var cronCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(cronCmd)
cronCmd.Flags().Bool("lean", false, "only output information about actual backups")
cronCmd.Flags().BoolVar(&flags.CRON_LEAN, "lean", false, "only output information about actual backups")
}

View File

@@ -40,7 +40,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
rootCmd.PersistentFlags().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&internal.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
cobra.OnInitialize(initConfig)
}

View File

@@ -2,10 +2,11 @@
A list of community driven projects. (No official affiliation)
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units
- Docker image: https://github.com/pascaliske/docker-autorestic
- Ansible Role: https://github.com/adsanz/ansible-restic-role
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
- Docker image: <https://github.com/pascaliske/docker-autorestic>
- Ansible Role: <https://github.com/adsanz/ansible-restic-role>
- Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
> :ToCPrevNext

View File

@@ -57,4 +57,12 @@ locations:
type: volume
```
## Tagging
Autorestic changed the way backups are referenced. Before we took the paths as the identifying information. Now autorestic uses native restic tags to reference them. This means that old backups are not referenced. You can the old snapshots manually. An example can be shown below.
```bash
autorestic exec -va -- tag --add ar:location:LOCATION_NAME # Only if you have only one location
```
> :ToCPrevNext

View File

@@ -9,7 +9,6 @@ import (
"strings"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
)
type BackendRest struct {
@@ -120,18 +119,15 @@ func (b Backend) validate() error {
if err != nil {
return err
}
options := ExecuteOptions{Envs: env}
options := ExecuteOptions{Envs: env, Silent: true}
// Check if already initialized
_, _, err = ExecuteResticCommand(options, "snapshots")
_, _, err = ExecuteResticCommand(options, "check")
if err == nil {
return nil
} else {
// If not initialize
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
_, out, err := ExecuteResticCommand(options, "init")
if flags.VERBOSE {
colors.Faint.Println(out)
}
_, _, err := ExecuteResticCommand(options, "init")
return err
}
}
@@ -147,9 +143,6 @@ func (b Backend) Exec(args []string) error {
colors.Error.Println(out)
return err
}
if flags.VERBOSE {
colors.Faint.Println(out)
}
return nil
}

225
internal/backend_test.go Normal file
View File

@@ -0,0 +1,225 @@
package internal
import (
"fmt"
"os"
"testing"
"github.com/spf13/viper"
)
func TestGenerateRepo(t *testing.T) {
t.Run("empty backend", func(t *testing.T) {
b := Backend{
name: "empty backend",
Type: "",
}
_, err := b.generateRepo()
if err == nil {
t.Errorf("Error expected for empty backend type")
}
})
t.Run("local backend", func(t *testing.T) {
b := Backend{
name: "local backend",
Type: "local",
Path: "/foo/bar",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "/foo/bar")
})
t.Run("local backend with homedir prefix", func(t *testing.T) {
b := Backend{
name: "local backend",
Type: "local",
Path: "~/foo/bar",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, fmt.Sprintf("%s/foo/bar", os.Getenv("HOME")))
})
t.Run("local backend with config file", func(t *testing.T) {
// config file path should always be present from initConfig
viper.SetConfigFile("/tmp/.autorestic.yml")
defer viper.Reset()
b := Backend{
name: "local backend",
Type: "local",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "/tmp")
})
t.Run("rest backend with valid path", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://localhost:8000/foo")
})
t.Run("rest backend with user", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
Rest: BackendRest{
User: "user",
Password: "",
},
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://user@localhost:8000/foo")
})
t.Run("rest backend with user and password", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
Rest: BackendRest{
User: "user",
Password: "pass",
},
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://user:pass@localhost:8000/foo")
})
backendTests := []struct {
name string
backend Backend
want string
}{
{name: "b2 backend", backend: Backend{name: "b2", Type: "b2", Path: "foo"}, want: "b2:foo"},
{name: "azure backend", backend: Backend{name: "azure", Type: "azure", Path: "foo"}, want: "azure:foo"},
{name: "gs backend", backend: Backend{name: "gs", Type: "gs", Path: "foo"}, want: "gs:foo"},
{name: "s3 backend", backend: Backend{name: "s3", Type: "s3", Path: "foo"}, want: "s3:foo"},
{name: "sftp backend", backend: Backend{name: "sftp", Type: "sftp", Path: "foo"}, want: "sftp:foo"},
{name: "rclone backend", backend: Backend{name: "rclone", Type: "rclone", Path: "foo"}, want: "rclone:foo"},
}
for _, tt := range backendTests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.backend.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, got, tt.want)
})
}
}
func TestGetEnv(t *testing.T) {
t.Run("env in key field", func(t *testing.T) {
b := Backend{
name: "",
Type: "local",
Path: "/foo/bar",
Key: "secret123",
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
})
t.Run("env in config file", func(t *testing.T) {
b := Backend{
name: "",
Type: "local",
Path: "/foo/bar",
Env: map[string]string{
"B2_ACCOUNT_ID": "foo123",
"B2_ACCOUNT_KEY": "foo456",
},
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "")
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
})
t.Run("env in Envfile or env vars", func(t *testing.T) {
// generate env variables
// TODO better way to teardown
defer os.Unsetenv("AUTORESTIC_FOO_RESTIC_PASSWORD")
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_ID")
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY")
os.Setenv("AUTORESTIC_FOO_RESTIC_PASSWORD", "secret123")
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_ID", "foo123")
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY", "foo456")
b := Backend{
name: "foo",
Type: "local",
Path: "/foo/bar",
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
})
}
func TestValidate(t *testing.T) {
t.Run("no type given", func(t *testing.T) {
b := Backend{
name: "foo",
Type: "",
Path: "/foo/bar",
}
err := b.validate()
if err == nil {
t.Error("expected to get error")
}
assertEqual(t, err.Error(), "Backend \"foo\" has no \"type\"")
})
t.Run("no path given", func(t *testing.T) {
b := Backend{
name: "foo",
Type: "local",
Path: "",
}
err := b.validate()
if err == nil {
t.Error("expected to get error")
}
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
})
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/blang/semver/v4"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
)
const INSTALL_PATH = "/usr/local/bin"
@@ -128,10 +129,9 @@ func InstallRestic() error {
}
func upgradeRestic() error {
_, out, err := internal.ExecuteCommand(internal.ExecuteOptions{
Command: "restic",
_, _, err := internal.ExecuteCommand(internal.ExecuteOptions{
Command: flags.RESTIC_BIN,
}, "self-update")
colors.Faint.Println(out)
return err
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/viper"
)
const VERSION = "1.6.2"
const VERSION = "1.7.2"
type OptionMap map[string][]interface{}
type Options map[string]OptionMap
@@ -185,7 +185,7 @@ func CheckConfig() error {
return fmt.Errorf("config could not be loaded/found")
}
if !CheckIfResticIsCallable() {
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, RESTIC_BIN)
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN)
}
for name, backend := range c.Backends {
backend.name = name
@@ -295,12 +295,8 @@ func appendOptionsToSlice(str *[]string, options OptionMap) {
}
}
func getOptions(options Options, key string) []string {
func getOptions(options Options, keys []string) []string {
var selected []string
var keys = []string{"all"}
if key != "" {
keys = append(keys, key)
}
for _, key := range keys {
appendOptionsToSlice(&selected, options[key])
}
@@ -310,9 +306,9 @@ func getOptions(options Options, key string) []string {
func combineOptions(key string, l Location, b Backend) []string {
// Priority: location > backend > global
var options []string
gFlags := getOptions(GetConfig().Global, key)
bFlags := getOptions(b.Options, key)
lFlags := getOptions(l.Options, key)
gFlags := getOptions(GetConfig().Global, []string{key})
bFlags := getOptions(b.Options, []string{"all", key})
lFlags := getOptions(l.Options, []string{"all", key})
options = append(options, gFlags...)
options = append(options, bFlags...)
options = append(options, lFlags...)

164
internal/config_test.go Normal file
View File

@@ -0,0 +1,164 @@
package internal
import (
"reflect"
"strconv"
"strings"
"testing"
)
func TestOptionToString(t *testing.T) {
t.Run("no prefix", func(t *testing.T) {
opt := "test"
result := optionToString(opt)
assertEqual(t, result, "--test")
})
t.Run("single prefix", func(t *testing.T) {
opt := "-test"
result := optionToString(opt)
assertEqual(t, result, "-test")
})
t.Run("double prefix", func(t *testing.T) {
opt := "--test"
result := optionToString(opt)
assertEqual(t, result, "--test")
})
}
func TestAppendOneOptionToSlice(t *testing.T) {
t.Run("string flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"string-flag": []interface{}{"/root"}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
}
assertSliceEqual(t, result, expected)
})
t.Run("bool flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"boolean-flag": []interface{}{true}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--boolean-flag",
}
assertSliceEqual(t, result, expected)
})
t.Run("int flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"int-flag": []interface{}{123}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--int-flag", "123",
}
assertSliceEqual(t, result, expected)
})
}
func TestAppendMultipleOptionsToSlice(t *testing.T) {
result := []string{}
optionMap := OptionMap{
"string-flag": []interface{}{"/root"},
"int-flag": []interface{}{123},
}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
"--int-flag", "123",
}
if len(result) != len(expected) {
t.Errorf("got length %d, want length %d", len(result), len(expected))
}
// checks that expected option comes after flag, regardless of key order in map
for i, v := range expected {
v = strings.TrimPrefix(v, "--")
if value, ok := optionMap[v]; ok {
if val, ok := value[0].(int); ok {
if expected[i+1] != strconv.Itoa(val) {
t.Errorf("Flags and options order are mismatched. got %v, want %v", result, expected)
}
}
}
}
}
func TestAppendOptionWithMultipleValuesToSlice(t *testing.T) {
result := []string{}
optionMap := OptionMap{
"string-flag": []interface{}{"/root", "/bin"},
}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
"--string-flag", "/bin",
}
assertSliceEqual(t, result, expected)
}
func TestGetOptionsOneKey(t *testing.T) {
optionMap := OptionMap{
"string-flag": []interface{}{"/root"},
}
options := Options{"backend": optionMap}
keys := []string{"backend"}
result := getOptions(options, keys)
expected := []string{
"--string-flag", "/root",
}
assertSliceEqual(t, result, expected)
}
func TestGetOptionsMultipleKeys(t *testing.T) {
firstOptionMap := OptionMap{
"string-flag": []interface{}{"/root"},
}
secondOptionMap := OptionMap{
"boolean-flag": []interface{}{true},
"int-flag": []interface{}{123},
}
options := Options{
"all": firstOptionMap,
"forget": secondOptionMap,
}
keys := []string{"all", "forget"}
result := getOptions(options, keys)
expected := []string{
"--string-flag", "/root",
"--boolean-flag",
"--int-flag", "123",
}
reflect.DeepEqual(result, expected)
}
func assertEqual[T comparable](t testing.TB, result, expected T) {
t.Helper()
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}
func assertSliceEqual(t testing.TB, result, expected []string) {
t.Helper()
if len(result) != len(expected) {
t.Errorf("got length %d, want length %d", len(result), len(expected))
}
for i := range result {
assertEqual(t, result[i], expected[i])
}
}

View File

@@ -1,5 +1,8 @@
package flags
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false
var (
CI bool = false
VERBOSE bool = false
CRON_LEAN bool = false
RESTIC_BIN string
)

View File

@@ -74,12 +74,8 @@ func (l Location) validate() error {
if from, err := GetPathRelativeToConfig(path); err != nil {
return err
} else {
if stat, err := os.Stat(from); err != nil {
if _, err := os.Stat(from); err != nil {
return err
} else {
if !stat.IsDir() {
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
}
}
}
}
@@ -146,9 +142,6 @@ func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error
colors.Error.Println(out)
return err
}
if flags.VERBOSE {
colors.Faint.Println(out)
}
}
colors.Body.Println("")
return nil
@@ -284,24 +277,16 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
for k, v := range env2 {
env[k+"2"] = v
}
_, out, err := ExecuteResticCommand(ExecuteOptions{
_, _, err := ExecuteResticCommand(ExecuteOptions{
Envs: env,
}, "copy", md.SnapshotID)
if flags.VERBOSE {
colors.Faint.Println(out)
}
if err != nil {
errors = append(errors, err)
}
}
}
}
if flags.VERBOSE {
colors.Faint.Println(out)
}
}
// After hooks
@@ -353,10 +338,7 @@ func (l Location) Forget(prune bool, dry bool) error {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, combineOptions("forget", l, backend)...)
_, out, err := ExecuteResticCommand(options, cmd...)
if flags.VERBOSE {
colors.Faint.Println(out)
}
_, _, err = ExecuteResticCommand(options, cmd...)
if err != nil {
return err
}

93
internal/location_test.go Normal file
View File

@@ -0,0 +1,93 @@
package internal
import "testing"
func TestGetType(t *testing.T) {
t.Run("TypeLocal", func(t *testing.T) {
l := Location{
Type: "local",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeLocal)
})
t.Run("TypeVolume", func(t *testing.T) {
l := Location{
Type: "volume",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeVolume)
})
t.Run("Empty type", func(t *testing.T) {
l := Location{
Type: "",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeLocal)
})
t.Run("Invalid type", func(t *testing.T) {
l := Location{
Type: "foo",
}
_, err := l.getType()
if err == nil {
t.Error("expected error")
}
})
}
func TestBuildTag(t *testing.T) {
result := buildTag("foo", "bar")
expected := "ar:foo:bar"
assertEqual(t, result, expected)
}
func TestGetLocationTags(t *testing.T) {
l := Location{
name: "foo",
}
result := l.getLocationTags()
expected := "ar:location:foo"
assertEqual(t, result, expected)
}
func TestHasBackend(t *testing.T) {
t.Run("backend present", func(t *testing.T) {
l := Location{
name: "foo",
To: []string{"foo", "bar"},
}
result := l.hasBackend("foo")
assertEqual(t, result, true)
})
t.Run("backend absent", func(t *testing.T) {
l := Location{
name: "foo",
To: []string{"bar", "baz"},
}
result := l.hasBackend("foo")
assertEqual(t, result, false)
})
}
func TestBuildRestoreCommand(t *testing.T) {
l := Location{
name: "foo",
}
result := buildRestoreCommand(l, "to", "snapshot", []string{"options"})
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
assertSliceEqual(t, result, expected)
}

View File

@@ -14,6 +14,10 @@ var lock *viper.Viper
var file string
var once sync.Once
const (
RUNNING = "running"
)
func getLock() *viper.Viper {
if lock == nil {
@@ -37,36 +41,38 @@ func getLock() *viper.Viper {
return lock
}
func setLock(locked bool) error {
func setLockValue(key string, value interface{}) (*viper.Viper, error) {
lock := getLock()
if locked {
running := lock.GetBool("running")
if running {
if key == RUNNING {
value := value.(bool)
if value && lock.GetBool(key) {
colors.Error.Println("an instance is already running. exiting")
os.Exit(1)
}
}
lock.Set("running", locked)
lock.Set(key, value)
if err := lock.WriteConfigAs(file); err != nil {
return err
return nil, err
}
return nil
return lock, nil
}
func GetCron(location string) int64 {
lock := getLock()
return lock.GetInt64("cron." + location)
return getLock().GetInt64("cron." + location)
}
func SetCron(location string, value int64) {
lock.Set("cron."+location, value)
lock.WriteConfigAs(file)
setLockValue("cron."+location, value)
}
func Lock() error {
return setLock(true)
_, err := setLockValue(RUNNING, true)
return err
}
func Unlock() error {
return setLock(false)
_, err := setLockValue(RUNNING, false)
return err
}

114
internal/lock/lock_test.go Normal file
View File

@@ -0,0 +1,114 @@
package lock
import (
"log"
"os"
"os/exec"
"strconv"
"testing"
"github.com/spf13/viper"
)
var testDirectory = "autorestic_test_tmp"
// All tests must share the same lock file as it is only initialized once
func setup(t *testing.T) {
d, err := os.MkdirTemp("", testDirectory)
if err != nil {
log.Fatalf("error creating temp dir: %v", err)
return
}
// set config file location
viper.SetConfigFile(d + "/.autorestic.yml")
t.Cleanup(func() {
os.RemoveAll(d)
viper.Reset()
})
}
func TestLock(t *testing.T) {
setup(t)
t.Run("getLock", func(t *testing.T) {
result := getLock().GetBool(RUNNING)
if result {
t.Errorf("got %v, want %v", result, false)
}
})
t.Run("lock", func(t *testing.T) {
lock, err := setLockValue(RUNNING, true)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result := lock.GetBool(RUNNING)
if !result {
t.Errorf("got %v, want %v", result, true)
}
})
t.Run("unlock", func(t *testing.T) {
lock, err := setLockValue(RUNNING, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result := lock.GetBool(RUNNING)
if result {
t.Errorf("got %v, want %v", result, false)
}
})
// locking a locked instance exits the instance
// this trick to capture os.Exit(1) is discussed here:
// https://talks.golang.org/2014/testing.slide#23
t.Run("lock twice", func(t *testing.T) {
if os.Getenv("CRASH") == "1" {
err := Lock()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// should fail
Lock()
}
cmd := exec.Command(os.Args[0], "-test.run=TestLock/lock_twice")
cmd.Env = append(os.Environ(), "CRASH=1")
err := cmd.Run()
err, ok := err.(*exec.ExitError)
if !ok {
t.Error("unexpected error")
}
expected := "exit status 1"
if err.Error() != expected {
t.Errorf("got %q, want %q", err.Error(), expected)
}
})
t.Run("set cron", func(t *testing.T) {
expected := int64(5)
SetCron("foo", expected)
result, err := strconv.ParseInt(getLock().GetString("cron.foo"), 10, 64)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != expected {
t.Errorf("got %d, want %d", result, expected)
}
})
t.Run("get cron", func(t *testing.T) {
expected := int64(5)
result := GetCron("foo")
if result != expected {
t.Errorf("got %d, want %d", result, expected)
}
})
}

View File

@@ -9,23 +9,34 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/fatih/color"
)
var RESTIC_BIN string
func CheckIfCommandIsCallable(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func CheckIfResticIsCallable() bool {
return CheckIfCommandIsCallable(RESTIC_BIN)
return CheckIfCommandIsCallable(flags.RESTIC_BIN)
}
type ExecuteOptions struct {
Command string
Envs map[string]string
Dir string
Silent bool
}
type ColoredWriter struct {
target io.Writer
color *color.Color
}
func (w ColoredWriter) Write(p []byte) (n int, err error) {
colored := []byte(w.color.Sprint(string(p)))
w.target.Write(colored)
return len(p), nil
}
func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) {
@@ -43,23 +54,32 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error)
var out bytes.Buffer
var error bytes.Buffer
cmd.Stdout = &out
if flags.VERBOSE && !options.Silent {
var colored ColoredWriter = ColoredWriter{
target: os.Stdout,
color: colors.Faint,
}
mw := io.MultiWriter(colored, &out)
cmd.Stdout = mw
} else {
cmd.Stdout = &out
}
cmd.Stderr = &error
err := cmd.Run()
if err != nil {
code := -1
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode(), error.String(), err
} else {
return -1, error.String(), err
code = exitError.ExitCode()
}
return code, error.String(), err
}
return 0, out.String(), nil
}
func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) {
options.Command = RESTIC_BIN
options.Command = flags.RESTIC_BIN
var c = GetConfig()
var optionsAsString = getOptions(c.Global, "")
var optionsAsString = getOptions(c.Global, []string{"all"})
args = append(optionsAsString, args...)
return ExecuteCommand(options, args...)
}