powerful but friendly backup program that runs in your tray, powered by restic devins.page/restray
go restic system-tray
2

Configure Feed

Select the types of activity you want to include in your feed.

tests

intergrav (Jul 13, 2026, 10:11 AM EDT) 7c19a7e4 59c69fc5

+174
+54
cmd/restray/config_test.go
··· 1 + package main 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "reflect" 7 + "testing" 8 + ) 9 + 10 + func TestParseEnvFile(t *testing.T) { 11 + path := filepath.Join(t.TempDir(), "profile.env") 12 + if err := os.WriteFile(path, []byte("\n# comment\nexport RESTIC_REPOSITORY = 's3:bucket'\nRESTIC_PASSWORD=\"secret value\"\nBARE=value\ninvalid line\n"), 0600); err != nil { 13 + t.Fatal(err) 14 + } 15 + 16 + got, err := parseEnvFile(path) 17 + if err != nil { 18 + t.Fatal(err) 19 + } 20 + want := map[string]string{"RESTIC_REPOSITORY": "s3:bucket", "RESTIC_PASSWORD": "secret value", "BARE": "value"} 21 + if !reflect.DeepEqual(got, want) { 22 + t.Fatalf("parseEnvFile() = %#v, want %#v", got, want) 23 + } 24 + } 25 + 26 + func TestLoadConfig(t *testing.T) { 27 + dir := t.TempDir() 28 + old := configDirOverride 29 + configDirOverride = dir 30 + t.Cleanup(func() { configDirOverride = old }) 31 + 32 + contents := "[[profiles]]\nname = 'work'\n[profiles.backup]\npaths = ['/home/me']\n" 33 + if err := os.WriteFile(configPath(), []byte(contents), 0600); err != nil { 34 + t.Fatal(err) 35 + } 36 + cfg := loadConfig() 37 + if cfg.loadErr != nil { 38 + t.Fatal(cfg.loadErr) 39 + } 40 + p := cfg.Profiles[0] 41 + if p.Backend != "restic" || p.EnvFile != filepath.Join(dir, "work.env") || p.RetryLock != "2m" { 42 + t.Fatalf("unexpected profile defaults: %#v", p) 43 + } 44 + if !reflect.DeepEqual(p.Backup.Args, []string{"--exclude-caches", "--exclude", "*.tmp"}) { 45 + t.Fatalf("backup defaults = %#v", p.Backup.Args) 46 + } 47 + 48 + if err := os.WriteFile(configPath(), []byte("[[profiles]]\nname = 'same'\n[[profiles]]\nname = 'same'\n"), 0600); err != nil { 49 + t.Fatal(err) 50 + } 51 + if cfg := loadConfig(); cfg.loadErr == nil { 52 + t.Fatal("expected duplicate profile names to be rejected") 53 + } 54 + }
+53
cmd/restray/operations_test.go
··· 1 + package main 2 + 3 + import ( 4 + "reflect" 5 + "testing" 6 + ) 7 + 8 + func TestBackupArgs(t *testing.T) { 9 + prof := Profile{ 10 + Backend: "restic", 11 + RetryLock: "30s", 12 + Backup: Backup{ 13 + Args: []string{"--exclude", "*.tmp"}, 14 + ArgsScheduled: []string{"--tag", "scheduled"}, 15 + Paths: []string{"/documents", "/photos"}, 16 + }, 17 + } 18 + want := []string{"backup", "--json", "--retry-lock", "30s", "--exclude", "*.tmp", "--tag", "scheduled", "/documents", "/photos"} 19 + if got := backupArgs(prof, true, "--json"); !reflect.DeepEqual(got, want) { 20 + t.Fatalf("backupArgs() = %#v, want %#v", got, want) 21 + } 22 + 23 + prof.Backend = "rustic" 24 + want = []string{"backup", "--json-progress", "--exclude", "*.tmp", "/documents", "/photos"} 25 + if got := backupArgs(prof, false, "--json-progress"); !reflect.DeepEqual(got, want) { 26 + t.Fatalf("rustic backupArgs() = %#v, want %#v", got, want) 27 + } 28 + } 29 + 30 + func TestRunScheduledOperations(t *testing.T) { 31 + prof := Profile{Schedule: Schedule{Prune: true, Check: true}, Prune: Prune{Args: []string{"--keep-last", "4"}}} 32 + var ran []string 33 + failed, msg := runScheduledOperations(prof, nil, 34 + func() (bool, string) { ran = append(ran, "backup"); return false, "backup failed" }, 35 + func() (bool, string) { ran = append(ran, "prune"); return true, "" }, 36 + func() (bool, string) { ran = append(ran, "check"); return true, "" }, 37 + ) 38 + if !failed || msg != "backup failed" || !reflect.DeepEqual(ran, []string{"backup"}) { 39 + t.Fatalf("backup failure: failed=%v msg=%q ran=%v", failed, msg, ran) 40 + } 41 + 42 + ran = nil 43 + disabled := false 44 + prof.Schedule.Backup = &disabled 45 + failed, msg = runScheduledOperations(prof, nil, 46 + func() (bool, string) { ran = append(ran, "backup"); return true, "" }, 47 + func() (bool, string) { ran = append(ran, "prune"); return false, "prune failed" }, 48 + func() (bool, string) { ran = append(ran, "check"); return false, "check failed" }, 49 + ) 50 + if !failed || msg != "prune failed" || !reflect.DeepEqual(ran, []string{"prune", "check"}) { 51 + t.Fatalf("maintenance failure: failed=%v msg=%q ran=%v", failed, msg, ran) 52 + } 53 + }
+26
cmd/restray/repository_test.go
··· 1 + package main 2 + 3 + import "testing" 4 + 5 + func TestRepositoryNeedsInit(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + profile Profile 9 + code int 10 + output string 11 + want bool 12 + }{ 13 + {"restic exit code", Profile{Backend: "restic"}, 10, "", true}, 14 + {"restic other code", Profile{Backend: "restic"}, 1, "repository does not exist", false}, 15 + {"rustic missing repository", Profile{Backend: "rustic"}, 1, "Repository is not initialized", true}, 16 + {"rustic config missing", Profile{Backend: "rustic"}, 1, "no such file or directory: config", true}, 17 + {"rustic auth error", Profile{Backend: "rustic"}, 1, "password failed", false}, 18 + } 19 + for _, tt := range tests { 20 + t.Run(tt.name, func(t *testing.T) { 21 + if got := repositoryNeedsInit(tt.profile, tt.code, tt.output); got != tt.want { 22 + t.Fatalf("repositoryNeedsInit() = %v, want %v", got, tt.want) 23 + } 24 + }) 25 + } 26 + }
+17
cmd/restray/update_test.go
··· 1 + package main 2 + 3 + import "testing" 4 + 5 + func TestParseAndCompareVersion(t *testing.T) { 6 + if _, ok := parseVersion("1.2.3"); !ok { 7 + t.Fatal("parseVersion() unexpectedly failed") 8 + } 9 + for _, v := range []string{"1.2", "v1.2.3", "1.2.x", "1.2.3.4"} { 10 + if _, ok := parseVersion(v); ok { 11 + t.Fatalf("parseVersion(%q) unexpectedly succeeded", v) 12 + } 13 + } 14 + if compareVersion("1.10.0", "1.9.9") <= 0 || compareVersion("1.0.0", "1.0.0") != 0 || compareVersion("1.0.0", "1.0.1") >= 0 { 15 + t.Fatal("compareVersion returned an unexpected ordering") 16 + } 17 + }
+14
cmd/restray/webeditor_test.go
··· 1 + package main 2 + 3 + import ( 4 + "reflect" 5 + "testing" 6 + ) 7 + 8 + func TestSplitArgs(t *testing.T) { 9 + input := "--exclude '*.tmp files'\n--tag scheduled\nunterminated 'quote\n\n" 10 + want := []string{"--exclude", "*.tmp files", "--tag", "scheduled", "unterminated 'quote"} 11 + if got := splitArgs(input); !reflect.DeepEqual(got, want) { 12 + t.Fatalf("splitArgs() = %#v, want %#v", got, want) 13 + } 14 + }
+10
justfile
··· 200 200 run: 201 201 just build && {{bin}}/{{app}}-{{version}}-$(go env GOOS)-$(go env GOARCH) 202 202 203 + check: 204 + just lint 205 + just test 206 + 203 207 lint: 204 208 goimports -w {{pkg}} 205 209 go vet ./{{pkg}} 206 210 staticcheck ./{{pkg}} 211 + 212 + test: 213 + go test ./... 214 + 215 + test-race: 216 + go test -race ./... 207 217 208 218 bump new: 209 219 #!/usr/bin/env bash