// Copyright (C) 2014 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. //go:build integration // +build integration package integration import ( cr "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "log" "math/rand" "os" "path/filepath" "runtime" "sort" "strings" "testing" "time" "unicode" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/rc" ) func init() { rand.Seed(42) } const ( id1 = "I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" id2 = "MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" id3 = "373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" apiKey = "abc123" ) func generateFiles(dir string, files, maxexp int, srcname string) error { return generateFilesWithTime(dir, files, maxexp, srcname, time.Now()) } func generateFilesWithTime(dir string, files, maxexp int, srcname string, t0 time.Time) error { fd, err := os.Open(srcname) if err != nil { return err } for i := 0; i < files; i++ { n := randomName() if rand.Float64() < 0.05 { // Some files and directories are dotfiles n = "." + n } p0 := filepath.Join(dir, string(n[0]), n[0:2]) err = os.MkdirAll(p0, 0o755) if err != nil { log.Fatal(err) } p1 := filepath.Join(p0, n) s := int64(1 << uint(rand.Intn(maxexp))) a := int64(128 * 1024) if a > s { a = s } s += rand.Int63n(a) if err := generateOneFile(fd, p1, s, t0); err != nil { return err } } return nil } func generateOneFile(fd io.ReadSeeker, p1 string, s int64, t0 time.Time) error { src := io.LimitReader(&infiniteReader{fd}, int64(s)) dst, err := os.Create(p1) if err != nil { return err } _, err = io.Copy(dst, src) if err != nil { return err } err = dst.Close() if err != nil { return err } os.Chmod(p1, os.FileMode(rand.Intn(0o777)|0o400)) t := t0.Add(-time.Duration(rand.Intn(30*86400)) * time.Second) err = os.Chtimes(p1, t, t) if err != nil { return err } return nil } func alterFiles(dir string) error { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if os.IsNotExist(err) { // Something we deleted. Never mind. return nil } info, err = os.Stat(path) if err != nil { // Something we deleted while walking. Ignore. return nil } if strings.HasPrefix(filepath.Base(path), "test-") { return nil } switch filepath.Base(path) { case ".stfolder": return nil case ".stversions": return nil } // File structure is base/x/xy/xyz12345... // comps == 1: base (don't touch) // comps == 2: base/x (must be dir) // comps == 3: base/x/xy (must be dir) // comps > 3: base/x/xy/xyz12345... (can be dir or file) comps := len(strings.Split(path, string(os.PathSeparator))) r := rand.Intn(10) switch { case r == 0 && comps > 2: // Delete every tenth file or directory, except top levels return removeAll(path) case r == 1 && info.Mode().IsRegular(): if info.Mode()&0o200 != 0o200 { // Not owner writable. Fix. if err = os.Chmod(path, 0o644); err != nil { return err } } // Overwrite a random kilobyte of every tenth file fd, err := os.OpenFile(path, os.O_RDWR, 0o644) if err != nil { return err } if info.Size() > 1024 { _, err = fd.Seek(rand.Int63n(info.Size()), os.SEEK_SET) if err != nil { return err } } _, err = io.Copy(fd, io.LimitReader(cr.Reader, 1024)) if err != nil { return err } return fd.Close() // Change capitalization case r == 2 && comps > 3 && rand.Float64() < 0.2: if build.IsDarwin || build.IsWindows { // Syncthing is currently broken for case-only renames on case- // insensitive platforms. // https://github.com/syncthing/syncthing/issues/1787 return nil } base := []rune(filepath.Base(path)) for i, r := range base { if rand.Float64() < 0.5 { base[i] = unicode.ToLower(r) } else { base[i] = unicode.ToUpper(r) } } newPath := filepath.Join(filepath.Dir(path), string(base)) if newPath != path { return os.Rename(path, newPath) } /* This doesn't in fact work. Sometimes it appears to. We need to get this sorted... // Switch between files and directories case r == 3 && comps > 3 && rand.Float64() < 0.2: if !info.Mode().IsRegular() { err = removeAll(path) if err != nil { return err } d1 := []byte("I used to be a dir: " + path) err := os.WriteFile(path, d1, 0644) if err != nil { return err } } else { err := os.Remove(path) if err != nil { return err } err = os.MkdirAll(path, 0755) if err != nil { return err } generateFiles(path, 10, 20, "../LICENSE") } return err */ /* This fails. Bug? // Rename the file, while potentially moving it up in the directory hierarchy case r == 4 && comps > 2 && (info.Mode().IsRegular() || rand.Float64() < 0.2): rpath := filepath.Dir(path) if rand.Float64() < 0.2 { for move := rand.Intn(comps - 1); move > 0; move-- { rpath = filepath.Join(rpath, "..") } } return osutil.TryRename(path, filepath.Join(rpath, randomName())) */ } return nil }) if err != nil { return err } return generateFiles(dir, 25, 20, "../LICENSE") } func ReadRand(bs []byte) (int, error) { var r uint32 for i := range bs { if i%4 == 0 { r = uint32(rand.Int63()) } bs[i] = byte(r >> uint((i%4)*8)) } return len(bs), nil } func randomName() string { var b [16]byte ReadRand(b[:]) return fmt.Sprintf("%x", b[:]) } type infiniteReader struct { rd io.ReadSeeker } func (i *infiniteReader) Read(bs []byte) (int, error) { n, err := i.rd.Read(bs) if err == io.EOF { err = nil i.rd.Seek(0, 0) } return n, err } // rm -rf func removeAll(dirs ...string) error { for _, dir := range dirs { files, err := filepath.Glob(dir) if err != nil { return err } for _, file := range files { // Set any non-writeable files and dirs to writeable. This is necessary for os.RemoveAll to work on Windows. filepath.Walk(file, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.Mode()&0o700 != 0o700 { os.Chmod(path, 0o777) } return nil }) os.RemoveAll(file) } } return nil } // Compare a number of directories. Returns nil if the contents are identical, // otherwise an error describing the first found difference. func compareDirectories(dirs ...string) error { chans := make([]chan fileInfo, len(dirs)) for i := range chans { chans[i] = make(chan fileInfo) } errcs := make([]chan error, len(dirs)) abort := make(chan struct{}) for i := range dirs { errcs[i] = startWalker(dirs[i], chans[i], abort) } res := make([]fileInfo, len(dirs)) for { numDone := 0 for i := range chans { fi, ok := <-chans[i] if !ok { err, hasError := <-errcs[i] if hasError { close(abort) return err } numDone++ } res[i] = fi } for i := 1; i < len(res); i++ { if res[i] != res[0] { close(abort) return fmt.Errorf("mismatch; %#v (%s) != %#v (%s)", res[i], dirs[i], res[0], dirs[0]) } } if numDone == len(dirs) { return nil } } } func directoryContents(dir string) ([]fileInfo, error) { res := make(chan fileInfo) errc := startWalker(dir, res, nil) var files []fileInfo for f := range res { files = append(files, f) } return files, <-errc } func mergeDirectoryContents(c ...[]fileInfo) []fileInfo { m := make(map[string]fileInfo) for _, l := range c { for _, f := range l { if cur, ok := m[f.name]; !ok || cur.mod < f.mod { m[f.name] = f } } } res := make([]fileInfo, len(m)) i := 0 for _, f := range m { res[i] = f i++ } sort.Sort(fileInfoList(res)) return res } func compareDirectoryContents(actual, expected []fileInfo) error { if len(actual) != len(expected) { return fmt.Errorf("len(actual) = %d; len(expected) = %d", len(actual), len(expected)) } for i := range actual { if actual[i] != expected[i] { return fmt.Errorf("mismatch; actual %#v != expected %#v", actual[i], expected[i]) } } return nil } type fileInfo struct { name string mode os.FileMode mod int64 hash [sha256.Size]byte size int64 } func (f fileInfo) String() string { return fmt.Sprintf("%s %04o %d %x", f.name, f.mode, f.mod, f.hash) } type fileInfoList []fileInfo func (l fileInfoList) Len() int { return len(l) } func (l fileInfoList) Less(a, b int) bool { return l[a].name < l[b].name } func (l fileInfoList) Swap(a, b int) { l[a], l[b] = l[b], l[a] } func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan error { walker := func(path string, info os.FileInfo, err error) error { if err != nil { return err } rn, _ := filepath.Rel(dir, path) if rn == "." || rn == ".stfolder" { return nil } if rn == ".stversions" { return filepath.SkipDir } var f fileInfo if info.Mode()&os.ModeSymlink != 0 { f = fileInfo{ name: rn, mode: os.ModeSymlink, } tgt, err := os.Readlink(path) if err != nil { return err } f.hash = sha256.Sum256([]byte(tgt)) } else if info.IsDir() { f = fileInfo{ name: rn, mode: info.Mode(), // hash and modtime zero for directories } } else { f = fileInfo{ name: rn, mode: info.Mode(), // comparing timestamps with better precision than a second // is problematic as there is rounding and truncatign going // on at every level mod: info.ModTime().Unix(), size: info.Size(), } sum, err := sha256file(path) if err != nil { return err } f.hash = sum } select { case res <- f: return nil case <-abort: return errors.New("abort") } } errc := make(chan error) go func() { err := filepath.Walk(dir, walker) close(res) if err != nil { errc <- err } close(errc) }() return errc } func sha256file(fname string) (hash [sha256.Size]byte, err error) { f, err := os.Open(fname) if err != nil { return } defer f.Close() h := sha256.New() io.Copy(h, f) hb := h.Sum(nil) copy(hash[:], hb) return } func isTimeout(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), "use of closed network connection") || strings.Contains(err.Error(), "request canceled while waiting") || strings.Contains(err.Error(), "operation timed out") } func getTestName() string { callers := make([]uintptr, 100) runtime.Callers(1, callers) for i, caller := range callers { f := runtime.FuncForPC(caller) if f != nil { if f.Name() == "testing.tRunner" { testf := runtime.FuncForPC(callers[i-1]) if testf != nil { path := strings.Split(testf.Name(), ".") return path[len(path)-1] } } } } return time.Now().String() } func checkedStop(t *testing.T, p *rc.Process) { if _, err := p.Stop(); err != nil { t.Fatal(err) } } func startInstance(t *testing.T, i int) *rc.Process { log.Printf("Starting instance %d...", i) addr := fmt.Sprintf("127.0.0.1:%d", 8080+i) log := fmt.Sprintf("logs/%s-%d-%d.out", getTestName(), i, time.Now().Unix()%86400) p := rc.NewProcess(addr) p.LogTo(log) if err := p.Start("../bin/syncthing", "--home", fmt.Sprintf("h%d", i), "--no-browser"); err != nil { t.Fatal(err) } p.AwaitStartup() p.PauseAll() return p } func symlinksSupported() bool { tmp, err := os.MkdirTemp("", "symlink-test") if err != nil { return false } defer os.RemoveAll(tmp) err = os.Symlink("tmp", filepath.Join(tmp, "link")) return err == nil } // checkRemoteInSync checks if the devices associated twith the given processes // are in sync according to the remote status on both sides. func checkRemoteInSync(folder string, p1, p2 *rc.Process) error { if inSync, err := p1.RemoteInSync(folder, p2.ID()); err != nil { return err } else if !inSync { return fmt.Errorf(`from device %v folder "%v" is not in sync on device %v`, p1.ID(), folder, p2.ID()) } if inSync, err := p2.RemoteInSync(folder, p1.ID()); err != nil { return err } else if !inSync { return fmt.Errorf(`from device %v folder "%v" is not in sync on device %v`, p2.ID(), folder, p1.ID()) } return nil }