package rsync import ( "context" "os" "path/filepath" "slices" "strings" "testing" "git.casaderoll.de/michael/urbm/internal/model" ) func TestArgumentsUseExplicitSafeOptions(t *testing.T) { job := model.Job{Sources: []model.Source{{Path: "/mnt/user/data"}}, Excludes: []string{"*.tmp"}, Rsync: model.RsyncOptions{Target: "/mnt/remotes/backup", Delete: true, PreservePermissions: true}} args := Arguments(job) for _, expected := range []string{"--recursive", "--delete-delay", "--perms", "--exclude", "*.tmp", "--", "/mnt/user/data", "/mnt/remotes/backup/"} { if !slices.Contains(args, expected) { t.Fatalf("missing %q in %#v", expected, args) } } } func TestParseProgress(t *testing.T) { progress, ok := parseProgress(" 1,234,567 42% 12.34MB/s 0:00:05") if !ok || progress.Bytes != 1234567 || progress.Percent != 42 { t.Fatalf("progress = %#v, %v", progress, ok) } } func TestArrayDiskMirrorKeepsDiskDirectory(t *testing.T) { job := model.Job{Sources: []model.Source{{Path: "/mnt/disk1"}}, Rsync: model.RsyncOptions{Target: "/mnt/disks/USB Backup", Overwrite: true, Delete: true}} args := Arguments(job) if !slices.Equal(args[len(args)-3:], []string{"--", "/mnt/disk1", "/mnt/disks/USB Backup/"}) || !slices.Contains(args, "--delete-delay") { t.Fatalf("mirror arguments = %v", args) } } func TestMachineReadableArgumentsAndSummary(t *testing.T) { args := Arguments(model.Job{}) if slices.Contains(args, "--human-readable") { t.Fatal("human readable sizes break byte parsing") } if !slices.Contains(args, "--out-format=CHANGE|%i|%n%L") { t.Fatal(args) } var summary Summary for _, line := range []string{"Number of regular files transferred: 1,376", "Number of deleted files: 1,214 (reg: 1,200, dir: 14)", "Total transferred file size: 1,234,567,890 bytes"} { summary.parseLine(line) } if summary.Files != 1376 || summary.Deleted != 1214 || summary.Bytes != 1234567890 { t.Fatalf("summary = %+v", summary) } } func TestParseChangePreservesNames(t *testing.T) { for _, code := range []string{">f+++++++++", ">f.st......", ".d..t......", "*deleting "} { got, ok := parseChange("CHANGE|" + code + "|disk1/a | b.txt") if !ok || got.Change != code || got.CurrentFile != "disk1/a | b.txt" { t.Fatalf("change = %+v, %v", got, ok) } } } func TestRunnerReadsFinalStats(t *testing.T) { binary := filepath.Join(t.TempDir(), "rsync") script := "#!/bin/sh\nprintf '%s\\n' 'CHANGE|>f+++++++++|disk1/new.txt' 'Number of regular files transferred: 1,376' 'Number of deleted files: 1,214 (reg: 1214)' 'Total transferred file size: 1,234,567,890 bytes'\n" if err := os.WriteFile(binary, []byte(script), 0700); err != nil { t.Fatal(err) } runner := Runner{Binary: binary} for _, estimate := range []bool{false, true} { var summary Summary var err error var changes []string if estimate { summary, err = runner.Estimate(context.Background(), "test", model.Job{}) } else { summary, err = runner.Run(context.Background(), "test", model.Job{}, func(p Progress) { if p.CurrentFile != "" { changes = append(changes, p.CurrentFile) } }) } if err != nil || summary.Files != 1376 || summary.Deleted != 1214 || summary.Bytes != 1234567890 { t.Fatalf("estimate=%v summary=%+v err=%v", estimate, summary, err) } if !estimate && strings.Join(changes, ",") != "disk1/new.txt" { t.Fatal(changes) } } } // Opt in with URBM_TEST_RSYNC pointing to rsync 3.x (Unraid's supported version). func TestRealRsyncDryRunAndCopy(t *testing.T) { binary := os.Getenv("URBM_TEST_RSYNC") if binary == "" { t.Skip("set URBM_TEST_RSYNC to test with rsync 3.x") } dir := t.TempDir() source, target := filepath.Join(dir, "disk1"), filepath.Join(dir, "USB") destination := filepath.Join(target, "disk1") for _, path := range []string{source, filepath.Join(destination, "old-dir")} { if err := os.MkdirAll(path, 0700); err != nil { t.Fatal(err) } } for path, data := range map[string]string{ filepath.Join(source, "new"): "new", filepath.Join(source, "changed"): "updated", filepath.Join(source, "same"): "same", filepath.Join(destination, "same"): "same", filepath.Join(destination, "changed"): "old", filepath.Join(destination, "obsolete"): "delete", filepath.Join(destination, "old-dir", "file"): "delete", } { if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatal(err) } } if err := os.Symlink("obsolete", filepath.Join(destination, "old-link")); err != nil { t.Fatal(err) } runner := Runner{Binary: binary} job := model.Job{Sources: []model.Source{{Path: source}}, Rsync: model.RsyncOptions{Target: target, DryRun: true, Overwrite: true, Delete: true, Checksum: true, PreserveLinks: true}} var changes []Progress summary, err := runner.Run(context.Background(), "test", job, func(p Progress) { if p.Change != "" { changes = append(changes, p) } }) if err != nil || summary.Files != 2 || summary.Bytes != 10 || summary.Deleted != 4 { t.Fatalf("dry run: %+v %v", summary, err) } if len(changes) < 6 { t.Fatalf("missing change details: %+v", changes) } if _, err := os.Stat(filepath.Join(destination, "new")); !os.IsNotExist(err) { t.Fatal("dry run wrote a file") } if _, err := os.Stat(filepath.Join(destination, "obsolete")); err != nil { t.Fatal("dry run deleted a file") } estimate, err := runner.Estimate(context.Background(), "test", job) if err != nil || estimate != summary { t.Fatalf("estimate differs: %+v %v", estimate, err) } job.Rsync.DryRun = false copied, err := runner.Run(context.Background(), "test", job, nil) if err != nil || copied != summary { t.Fatalf("copy differs: %+v %v", copied, err) } for _, name := range []string{"new", "changed", "same"} { got, err := os.ReadFile(filepath.Join(destination, name)) if err != nil { t.Fatal(err) } want, err := os.ReadFile(filepath.Join(source, name)) if err != nil { t.Fatal(err) } if string(got) != string(want) { t.Fatalf("different contents: %s", name) } } for _, name := range []string{"obsolete", "old-dir", "old-link"} { if _, err := os.Lstat(filepath.Join(destination, name)); !os.IsNotExist(err) { t.Fatalf("not deleted: %s", name) } } identical, err := runner.Estimate(context.Background(), "test", job) if err != nil || identical.Files != 0 || identical.Bytes != 0 || identical.Deleted != 0 { t.Fatalf("identical: %+v %v", identical, err) } }