]> ruderich.org/simon Gitweb - safcm/safcm.git/blobdiff - cmd/safcm-remote/sync/files_test.go
First working version
[safcm/safcm.git] / cmd / safcm-remote / sync / files_test.go
diff --git a/cmd/safcm-remote/sync/files_test.go b/cmd/safcm-remote/sync/files_test.go
new file mode 100644 (file)
index 0000000..22daa63
--- /dev/null
@@ -0,0 +1,2572 @@
+// Copyright (C) 2021  Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package sync
+
+import (
+       "fmt"
+       "io/fs"
+       "math/rand"
+       "os"
+       "os/user"
+       "path/filepath"
+       "reflect"
+       "regexp"
+       "strconv"
+       "syscall"
+       "testing"
+
+       "github.com/google/go-cmp/cmp"
+
+       "ruderich.org/simon/safcm"
+)
+
+type File struct {
+       Path string
+       Mode fs.FileMode
+       Data []byte
+}
+
+func walkDir(basePath string) ([]File, error) {
+       var res []File
+       err := filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
+               if err != nil {
+                       return err
+               }
+               info, err := d.Info()
+               if err != nil {
+                       return err
+               }
+               rel, err := filepath.Rel(basePath, path)
+               if err != nil {
+                       return err
+               }
+
+               f := File{
+                       Path: rel,
+                       Mode: info.Mode(),
+               }
+               if f.Mode.Type() == 0 {
+                       x, err := os.ReadFile(path)
+                       if err != nil {
+                               return err
+                       }
+                       f.Data = x
+               } else if f.Mode.Type() == fs.ModeSymlink {
+                       x, err := os.Readlink(path)
+                       if err != nil {
+                               return err
+                       }
+                       f.Data = []byte(x)
+               }
+               res = append(res, f)
+               return nil
+       })
+       if err != nil {
+               return nil, err
+       }
+       return res, nil
+}
+
+var randFilesRegexp = regexp.MustCompile(`\d+"$`)
+
+func TestSyncFiles(t *testing.T) {
+       cwd, err := os.Getwd()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chdir(cwd)
+
+       err = os.RemoveAll("testdata")
+       if err != nil {
+               t.Fatal(err)
+       }
+       err = os.Mkdir("testdata", 0700)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       root := File{
+               Path: ".",
+               Mode: fs.ModeDir | 0700,
+       }
+       user, uid, group, gid := currentUserAndGroup()
+
+       tmpTestFilePath := "/tmp/safcm-sync-files-test-file"
+
+       tests := []struct {
+               name     string
+               req      safcm.MsgSyncReq
+               prepare  func()
+               triggers []string
+               expFiles []File
+               expResp  safcm.MsgSyncResp
+               expDbg   []string
+               expErr   error
+       }{
+
+               // NOTE: Also update MsgSyncResp in safcm test cases when
+               // changing anything here!
+
+               // See TestSyncFile() for most file related tests. This
+               // function only tests the overall results and triggers.
+
+               {
+                       "basic: create",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                       },
+                               },
+                       },
+                       nil,
+                       nil,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "dir",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0755,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                                       {
+                                               Path:    "dir/file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): will create`,
+                               `3: sync remote: files: "dir" (group): creating`,
+                               `4: sync remote: files: "dir" (group): creating directory`,
+                               `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+                               fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
+                               `4: sync remote: files: "dir/file" (group): will create`,
+                               `3: sync remote: files: "dir/file" (group): creating`,
+                               `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+                               `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "basic: no change",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                       },
+                               },
+                       },
+                       func() {
+                               createDirectory("dir", 0755)
+                               createFile("dir/file", "content\n", 0644)
+                       },
+                       nil,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): unchanged`,
+                               `4: sync remote: files: "dir/file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "invalid File: user",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               User:      "user",
+                                               Uid:       1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                               },
+                       },
+                       nil,
+                       nil,
+                       []File{
+                               root,
+                       },
+                       safcm.MsgSyncResp{},
+                       nil,
+                       fmt.Errorf("\".\": cannot set both User (\"user\") and Uid (1)"),
+               },
+               {
+                       "invalid File: group",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Group:     "group",
+                                               Gid:       1,
+                                               OrigGroup: "group",
+                                       },
+                               },
+                       },
+                       nil,
+                       nil,
+                       []File{
+                               root,
+                       },
+                       safcm.MsgSyncResp{},
+                       nil,
+                       fmt.Errorf("\".\": cannot set both Group (\"group\") and Gid (1)"),
+               },
+
+               {
+                       // We use relative paths for most tests because we
+                       // don't want to modify the running system. Use this
+                       // test (and the one below for triggers) as a basic
+                       // check that absolute paths work.
+                       "absolute paths: no change",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       "/": {
+                                               Path:      "/",
+                                               Mode:      fs.ModeDir | 0755,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "/etc": {
+                                               Path:      "/etc",
+                                               Mode:      fs.ModeDir | 0755,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "/tmp": {
+                                               Path:      "/tmp",
+                                               Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                                       "/var/tmp": {
+                                               Path:      "/var/tmp",
+                                               Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                       },
+                               },
+                       },
+                       nil,
+                       nil,
+                       []File{
+                               root,
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "/" (group): unchanged`,
+                               `4: sync remote: files: "/etc" (group): unchanged`,
+                               `4: sync remote: files: "/tmp" (group): unchanged`,
+                               `4: sync remote: files: "/var/tmp" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: no change",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger .",
+                                               },
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir",
+                                               },
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir/file",
+                                               },
+                                       },
+                               },
+                       },
+                       func() {
+                               createDirectory("dir", 0755)
+                               createFile("dir/file", "content\n", 0644)
+                       },
+                       nil,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): unchanged`,
+                               `4: sync remote: files: "dir/file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: change root",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger .",
+                                               },
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir",
+                                               },
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir/file",
+                                               },
+                                       },
+                               },
+                       },
+                       func() {
+                               err = os.Chmod(".", 0750)
+                               if err != nil {
+                                       panic(err)
+                               }
+                               createDirectory("dir", 0755)
+                               createFile("dir/file", "content\n", 0644)
+                       },
+                       []string{
+                               ".",
+                       },
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: ".",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0750,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0700,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "." (group): permission differs drwxr-x--- -> drwx------`,
+                               `3: sync remote: files: "." (group): updating`,
+                               `4: sync remote: files: "." (group): chmodding drwx------`,
+                               `3: sync remote: files: ".": queuing trigger on "."`,
+                               `4: sync remote: files: "dir" (group): unchanged`,
+                               `4: sync remote: files: "dir/file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: change middle",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger .",
+                                               },
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir",
+                                               },
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir/file",
+                                               },
+                                       },
+                               },
+                       },
+                       func() {
+                               createDirectory("dir", 0750)
+                               createFile("dir/file", "content\n", 0644)
+                       },
+                       []string{
+                               ".",
+                               "dir",
+                       },
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "dir",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0750,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0755,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): permission differs drwxr-x--- -> drwxr-xr-x`,
+                               `3: sync remote: files: "dir" (group): updating`,
+                               `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+                               `3: sync remote: files: "dir": queuing trigger on "."`,
+                               `3: sync remote: files: "dir": queuing trigger on "dir"`,
+                               `4: sync remote: files: "dir/file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: change leaf",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger .",
+                                               },
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir",
+                                               },
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir/file",
+                                               },
+                                       },
+                               },
+                       },
+                       func() {
+                               createDirectory("dir", 0755)
+                       },
+                       []string{
+                               ".",
+                               "dir",
+                               "dir/file",
+                       },
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "dir/file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): unchanged`,
+                               `4: sync remote: files: "dir/file" (group): will create`,
+                               `3: sync remote: files: "dir/file" (group): creating`,
+                               `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+                               `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+                               `3: sync remote: files: "dir/file": queuing trigger on "."`,
+                               `3: sync remote: files: "dir/file": queuing trigger on "dir"`,
+                               `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: multiple changes",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       ".": {
+                                               Path:      ".",
+                                               Mode:      fs.ModeDir | 0700,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger .",
+                                               },
+                                       },
+                                       "dir": {
+                                               Path:      "dir",
+                                               Mode:      fs.ModeDir | 0755,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir",
+                                               },
+                                       },
+                                       "dir/file": {
+                                               Path:      "dir/file",
+                                               Mode:      0644,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               Data:      []byte("content\n"),
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger dir/file",
+                                               },
+                                       },
+                               },
+                       },
+                       nil,
+                       []string{
+                               ".",
+                               "dir",
+                               "dir/file",
+                       },
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                               {
+                                       Path: "dir/file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "dir",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0755,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                                       {
+                                               Path:    "dir/file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "." (group): unchanged`,
+                               `4: sync remote: files: "dir" (group): will create`,
+                               `3: sync remote: files: "dir" (group): creating`,
+                               `4: sync remote: files: "dir" (group): creating directory`,
+                               `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+                               fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
+                               `3: sync remote: files: "dir": queuing trigger on "."`,
+                               `3: sync remote: files: "dir": queuing trigger on "dir"`,
+                               `4: sync remote: files: "dir/file" (group): will create`,
+                               `3: sync remote: files: "dir/file" (group): creating`,
+                               `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+                               `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+                               `4: sync remote: files: "dir/file": skipping trigger on ".", already active`,
+                               `4: sync remote: files: "dir/file": skipping trigger on "dir", already active`,
+                               `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "triggers: absolute paths",
+                       safcm.MsgSyncReq{
+                               Files: map[string]*safcm.File{
+                                       "/": {
+                                               Path:      "/",
+                                               Mode:      fs.ModeDir | 0755,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger /",
+                                               },
+                                       },
+                                       "/tmp": {
+                                               Path:      "/tmp",
+                                               Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
+                                               User:      "root",
+                                               Uid:       -1,
+                                               Group:     "root",
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger /tmp",
+                                               },
+                                       },
+                                       tmpTestFilePath: {
+                                               Path:      tmpTestFilePath,
+                                               Mode:      0600,
+                                               Uid:       -1,
+                                               Gid:       -1,
+                                               OrigGroup: "group",
+                                               TriggerCommands: []string{
+                                                       "echo trigger /tmp/file",
+                                               },
+                                       },
+                               },
+                       },
+                       func() {
+                               // This is slightly racy but the file name
+                               // should be rare enough that this isn't an
+                               // issue
+                               _, err := os.Stat(tmpTestFilePath)
+                               if err == nil {
+                                       t.Fatalf("%q exists, aborting",
+                                               tmpTestFilePath)
+                               }
+                       },
+                       []string{
+                               "/",
+                               "/tmp",
+                               // Don't use variable for more robust test
+                               "/tmp/safcm-sync-files-test-file",
+                       },
+                       []File{
+                               root,
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "/tmp/safcm-sync-files-test-file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0600,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "/" (group): unchanged`,
+                               `4: sync remote: files: "/tmp" (group): unchanged`,
+                               `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): will create`,
+                               `3: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating`,
+                               `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating temporary file "/tmp/.safcm-sync-files-test-file*"`,
+                               `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): renaming "/tmp/.safcm-sync-files-test-fileRND"`,
+                               `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/"`,
+                               `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp"`,
+                               `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp/safcm-sync-files-test-file"`,
+                       },
+                       nil,
+               },
+       }
+
+       for _, tc := range tests {
+               // Create separate test directory for each test case
+               path := filepath.Join(cwd, "testdata", "files-"+tc.name)
+               err = os.Mkdir(path, 0700)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               err = os.Chdir(path)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               if tc.prepare != nil {
+                       tc.prepare()
+               }
+
+               s, res := prepareSync(tc.req, &testRunner{
+                       t:    t,
+                       name: tc.name,
+               })
+               s.setDefaults()
+
+               err := s.syncFiles()
+               // Ugly but the simplest way to compare errors (including nil)
+               if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
+                       t.Errorf("%s: err = %#v, want %#v",
+                               tc.name, err, tc.expErr)
+               }
+               dbg := res.Wait()
+               // Remove random file names from result
+               for i, x := range dbg {
+                       dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
+               }
+               if !reflect.DeepEqual(tc.expDbg, dbg) {
+                       t.Errorf("%s: dbg: %s", tc.name,
+                               cmp.Diff(tc.expDbg, dbg))
+               }
+
+               files, err := walkDir(path)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if !reflect.DeepEqual(tc.expFiles, files) {
+                       t.Errorf("%s: files: %s", tc.name,
+                               cmp.Diff(tc.expFiles, files))
+               }
+
+               if !reflect.DeepEqual(tc.expResp, s.resp) {
+                       t.Errorf("%s: resp: %s", tc.name,
+                               cmp.Diff(tc.expResp, s.resp))
+               }
+               if !reflect.DeepEqual(tc.triggers, s.triggers) {
+                       t.Errorf("%s: triggers: %s", tc.name,
+                               cmp.Diff(tc.triggers, s.triggers))
+               }
+       }
+
+       os.Remove(tmpTestFilePath)
+       if !t.Failed() {
+               err = os.RemoveAll(filepath.Join(cwd, "testdata"))
+               if err != nil {
+                       t.Fatal(err)
+               }
+       }
+}
+
+func TestSyncFile(t *testing.T) {
+       cwd, err := os.Getwd()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chdir(cwd)
+
+       err = os.RemoveAll("testdata")
+       if err != nil {
+               t.Fatal(err)
+       }
+       err = os.Mkdir("testdata", 0700)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       root := File{
+               Path: ".",
+               Mode: fs.ModeDir | 0700,
+       }
+       user, uid, group, gid := currentUserAndGroup()
+
+       tests := []struct {
+               name       string
+               req        safcm.MsgSyncReq
+               file       *safcm.File
+               prepare    func()
+               expChanged bool
+               expFiles   []File
+               expResp    safcm.MsgSyncResp
+               expDbg     []string
+               expErr     error
+       }{
+
+               // NOTE: Also update MsgSyncResp in safcm test cases when
+               // changing anything here!
+
+               // TODO: Add tests for chown and run them only as root
+
+               // Regular file
+
+               {
+                       "file: create",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): will create`,
+                               `3: sync remote: files: "file" (group): creating`,
+                               `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+                               `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
+                       },
+                       nil,
+               },
+               {
+                       "file: create (dry-run)",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{root},
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "file",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): will create`,
+                               `3: sync remote: files: "file" (group): creating`,
+                               `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "file: unchanged",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "content\n", 0644)
+                       },
+                       false,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "file: unchanged (non-default user-group)",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               User:      user,
+                               Uid:       -1,
+                               Group:     group,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "content\n", 0644)
+                       },
+                       false,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "file" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "file: permission",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0755 | fs.ModeSetuid,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "content\n", 0755)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0755 | fs.ModeSetuid,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0755,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0755 | fs.ModeSetuid,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): permission differs -rwxr-xr-x -> urwxr-xr-x`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+                               `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "file: content",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "old content\n", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,2 +1,2 @@
+-old content
++content
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): content differs`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+                               `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
+                       },
+                       nil,
+               },
+
+               // Symbolic link
+
+               {
+                       "symlink: create",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "link",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("target"),
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "link",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "link",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "link" (group): will create`,
+                               `3: sync remote: files: "link" (group): creating`,
+                               `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+                               `4: sync remote: files: "link" (group): renaming ".linkRND"`,
+                       },
+                       nil,
+               },
+               {
+                       "symlink: create (conflict)",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "link",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("target"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile(".link8717895732742165505", "", 0600)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: ".link8717895732742165505",
+                                       Mode: 0600,
+                                       Data: []byte(""),
+                               },
+                               {
+                                       Path: "link",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "link",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "link" (group): will create`,
+                               `3: sync remote: files: "link" (group): creating`,
+                               `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+                               `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+                               `4: sync remote: files: "link" (group): renaming ".linkRND"`,
+                       },
+                       nil,
+               },
+               {
+                       "symlink: create (dry-run)",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "link",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("target"),
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{root},
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "link",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "link" (group): will create`,
+                               `3: sync remote: files: "link" (group): creating`,
+                               `4: sync remote: files: "link" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "symlink: unchanged",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "link",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("target"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createSymlink("link", "target")
+                       },
+                       false,
+                       []File{
+                               root,
+                               {
+                                       Path: "link",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "link" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "symlink: content",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "link",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("target"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createSymlink("link", "old-target")
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "link",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "link",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1 +1 @@
+-old-target
++target
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "link" (group): content differs`,
+                               `3: sync remote: files: "link" (group): updating`,
+                               `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+                               `4: sync remote: files: "link" (group): renaming ".linkRND"`,
+                       },
+                       nil,
+               },
+
+               // Directory
+
+               {
+                       "directory: create",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "dir",
+                               Mode:      fs.ModeDir | 0705,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0705,
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "dir",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0705,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "dir" (group): will create`,
+                               `3: sync remote: files: "dir" (group): creating`,
+                               `4: sync remote: files: "dir" (group): creating directory`,
+                               `4: sync remote: files: "dir" (group): chmodding drwx---r-x`,
+                               fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
+                       },
+                       nil,
+               },
+               {
+                       "directory: create (dry-run)",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "dir",
+                               Mode:      fs.ModeDir | 0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       nil,
+                       true,
+                       []File{root},
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path:    "dir",
+                                               Created: true,
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "dir" (group): will create`,
+                               `3: sync remote: files: "dir" (group): creating`,
+                               `4: sync remote: files: "dir" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "directory: unchanged",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "dir",
+                               Mode:      fs.ModeDir | 0755,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createDirectory("dir", 0755)
+                       },
+                       false,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755,
+                               },
+                       },
+                       safcm.MsgSyncResp{},
+                       []string{
+                               `4: sync remote: files: "dir" (group): unchanged`,
+                       },
+                       nil,
+               },
+
+               {
+                       "directory: permission",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "dir",
+                               Mode:      fs.ModeDir | 0755 | fs.ModeSetgid,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createDirectory("dir", 0500|fs.ModeSticky)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "dir",
+                                       Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "dir",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0500 | fs.ModeSticky,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0755 | fs.ModeSetgid,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "dir" (group): permission differs dtr-x------ -> dgrwxr-xr-x`,
+                               `3: sync remote: files: "dir" (group): updating`,
+                               `4: sync remote: files: "dir" (group): chmodding dgrwxr-xr-x`,
+                       },
+                       nil,
+               },
+
+               // Type changes
+
+               {
+                       "change: file to directory",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeDir | 0751,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("path", "content\n", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeDir | 0751,
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0751,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,2 +1 @@
+-content
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs ---------- -> d---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): removing (due to type change)`,
+                               `4: sync remote: files: "path" (group): creating directory`,
+                               `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+                               fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
+                       },
+                       nil,
+               },
+
+               {
+                       "change: file to symlink",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("target"),
+                       },
+                       func() {
+                               createFile("path", "content\n", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,2 +1 @@
+-content
+-
++target
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs ---------- -> L---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+                               `4: sync remote: files: "path" (group): renaming ".pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: symlink to file",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      0640,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("content\n"),
+                       },
+                       func() {
+                               createSymlink("path", "target")
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: 0640,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0640,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1 +1,2 @@
+-target
++content
++
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs L--------- -> ----------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+                               `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: symlink to directory",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeDir | 0751,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createSymlink("path", "target")
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeDir | 0751,
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0751,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1 +1 @@
+-target
++
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs L--------- -> d---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): removing (due to type change)`,
+                               `4: sync remote: files: "path" (group): creating directory`,
+                               `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+                               fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
+                       },
+                       nil,
+               },
+
+               {
+                       "change: directory to file",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      0666,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("content\n"),
+                       },
+                       func() {
+                               createDirectory("path", 0777)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: 0666,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0666,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs d--------- -> ----------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): removing (due to type change)`,
+                               `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+                               `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: directory to symlink",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("target"),
+                       },
+                       func() {
+                               createDirectory("path", 0777)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs d--------- -> L---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): removing (due to type change)`,
+                               `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+                               `4: sync remote: files: "path" (group): renaming ".pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: other to file",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      0640,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("content\n"),
+                       },
+                       func() {
+                               createFifo("path", 0666)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: 0640,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeNamedPipe | 0666,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0640,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs p--------- -> ----------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+                               `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: other to symlink",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("target"),
+                       },
+                       func() {
+                               createFifo("path", 0666)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeNamedPipe | 0666,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs p--------- -> L---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+                               `4: sync remote: files: "path" (group): renaming ".pathRND"`,
+                       },
+                       nil,
+               },
+
+               {
+                       "change: other to directory",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeDir | 0751,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFifo("path", 0666)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeDir | 0751,
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeNamedPipe | 0666,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeDir | 0751,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs p--------- -> d---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): removing (due to type change)`,
+                               `4: sync remote: files: "path" (group): creating directory`,
+                               `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+                               fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
+                       },
+                       nil,
+               },
+
+               {
+                       "change: file to symlink (same content)",
+                       safcm.MsgSyncReq{},
+                       &safcm.File{
+                               Path:      "path",
+                               Mode:      fs.ModeSymlink | 0777,
+                               Uid:       -1,
+                               Gid:       -1,
+                               OrigGroup: "group",
+                               Data:      []byte("target"),
+                       },
+                       func() {
+                               createFile("path", "target", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "path",
+                                       Mode: fs.ModeSymlink | 0777,
+                                       Data: []byte("target"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "path",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  fs.ModeSymlink | 0777,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "path" (group): type differs ---------- -> L---------`,
+                               `3: sync remote: files: "path" (group): updating`,
+                               `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+                               `4: sync remote: files: "path" (group): renaming ".pathRND"`,
+                       },
+                       nil,
+               },
+
+               // Diffs
+
+               {
+                       "diff: textual",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path: "file",
+                               Mode: 0644,
+                               Uid:  -1,
+                               Gid:  -1,
+                               Data: []byte(`
+this
+is
+a
+simple
+file
+`),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", `this
+is
+file
+!
+`, 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte(`this
+is
+file
+!
+`),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,5 +1,7 @@
++
+ this
+ is
++a
++simple
+ file
+-!
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): content differs`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "diff: binary both",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("\x00\x01\x02\x03"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "\x00\x01\x02", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("\x00\x01\x02"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: "Binary files differ, cannot show diff",
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): content differs`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "diff: binary old",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("content\n"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "\x00\x01\x02", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("\x00\x01\x02"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,2 +1,2 @@
+-<binary content>
++content
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): content differs`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+
+               {
+                       "diff: binary new",
+                       safcm.MsgSyncReq{
+                               DryRun: true,
+                       },
+                       &safcm.File{
+                               Path:      "file",
+                               Mode:      0644,
+                               Uid:       -1,
+                               Gid:       -1,
+                               Data:      []byte("\x00\x01\x02\x03"),
+                               OrigGroup: "group",
+                       },
+                       func() {
+                               createFile("file", "content\n", 0644)
+                       },
+                       true,
+                       []File{
+                               root,
+                               {
+                                       Path: "file",
+                                       Mode: 0644,
+                                       Data: []byte("content\n"),
+                               },
+                       },
+                       safcm.MsgSyncResp{
+                               FileChanges: []safcm.FileChange{
+                                       {
+                                               Path: "file",
+                                               Old: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               New: safcm.FileChangeInfo{
+                                                       Mode:  0644,
+                                                       User:  user,
+                                                       Uid:   uid,
+                                                       Group: group,
+                                                       Gid:   gid,
+                                               },
+                                               DataDiff: `@@ -1,2 +1,2 @@
+-content
++<binary content>
+`,
+                                       },
+                               },
+                       },
+                       []string{
+                               `4: sync remote: files: "file" (group): content differs`,
+                               `3: sync remote: files: "file" (group): updating`,
+                               `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+                       },
+                       nil,
+               },
+       }
+
+       for _, tc := range tests {
+               // Create separate test directory for each test case
+               path := filepath.Join(cwd, "testdata", "file-"+tc.name)
+               err = os.Mkdir(path, 0700)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               err = os.Chdir(path)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               if tc.prepare != nil {
+                       tc.prepare()
+               }
+
+               s, res := prepareSync(tc.req, &testRunner{
+                       t:    t,
+                       name: tc.name,
+               })
+               s.setDefaults()
+
+               // Deterministic temporary symlink names
+               rand.Seed(0)
+
+               var changed bool
+               err := s.syncFile(tc.file, &changed)
+               // Ugly but the simplest way to compare errors (including nil)
+               if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
+                       t.Errorf("%s: err = %#v, want %#v",
+                               tc.name, err, tc.expErr)
+               }
+               dbg := res.Wait()
+               // Remove random file names from result
+               for i, x := range dbg {
+                       dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
+               }
+               if !reflect.DeepEqual(tc.expDbg, dbg) {
+                       t.Errorf("%s: dbg: %s", tc.name,
+                               cmp.Diff(tc.expDbg, dbg))
+               }
+
+               files, err := walkDir(path)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if !reflect.DeepEqual(tc.expFiles, files) {
+                       t.Errorf("%s: files: %s", tc.name,
+                               cmp.Diff(tc.expFiles, files))
+               }
+
+               if tc.expChanged != changed {
+                       t.Errorf("%s: changed = %#v, want %#v",
+                               tc.name, changed, tc.expChanged)
+               }
+               if !reflect.DeepEqual(tc.expResp, s.resp) {
+                       t.Errorf("%s: resp: %s", tc.name,
+                               cmp.Diff(tc.expResp, s.resp))
+               }
+       }
+
+       if !t.Failed() {
+               err = os.RemoveAll(filepath.Join(cwd, "testdata"))
+               if err != nil {
+                       t.Fatal(err)
+               }
+       }
+}
+
+// Helper functions
+
+func createFile(path string, data string, mode fs.FileMode) {
+       err := os.WriteFile(path, []byte(data), 0644)
+       if err != nil {
+               panic(err)
+       }
+       err = os.Chmod(path, mode)
+       if err != nil {
+               panic(err)
+       }
+}
+func createSymlink(path string, data string) {
+       err := os.Symlink(data, path)
+       if err != nil {
+               panic(err)
+       }
+}
+func createDirectory(path string, mode fs.FileMode) {
+       err := os.Mkdir(path, 0700)
+       if err != nil {
+               panic(err)
+       }
+       err = os.Chmod(path, mode)
+       if err != nil {
+               panic(err)
+       }
+}
+func createFifo(path string, mode fs.FileMode) {
+       err := syscall.Mkfifo(path, 0600)
+       if err != nil {
+               panic(err)
+       }
+       err = os.Chmod(path, mode)
+       if err != nil {
+               panic(err)
+       }
+}
+
+func currentUserAndGroup() (string, int, string, int) {
+       u, err := user.Current()
+       if err != nil {
+               panic(err)
+       }
+       g, err := user.LookupGroupId(u.Gid)
+       if err != nil {
+               panic(err)
+       }
+       uid, err := strconv.Atoi(u.Uid)
+       if err != nil {
+               panic(err)
+       }
+       gid, err := strconv.Atoi(g.Gid)
+       if err != nil {
+               panic(err)
+       }
+       return u.Username, uid, g.Name, gid
+}