mtimefs_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. // Copyright (C) 2016 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package fs
  7. import (
  8. "errors"
  9. "os"
  10. "path/filepath"
  11. "testing"
  12. "time"
  13. "github.com/syncthing/syncthing/lib/build"
  14. )
  15. func TestMtimeFS(t *testing.T) {
  16. td := t.TempDir()
  17. os.Mkdir(filepath.Join(td, "testdata"), 0o755)
  18. os.WriteFile(filepath.Join(td, "testdata", "exists0"), []byte("hello"), 0o644)
  19. os.WriteFile(filepath.Join(td, "testdata", "exists1"), []byte("hello"), 0o644)
  20. os.WriteFile(filepath.Join(td, "testdata", "exists2"), []byte("hello"), 0o644)
  21. // a random time with nanosecond precision
  22. testTime := time.Unix(1234567890, 123456789)
  23. mtimefs := newMtimeFS(td, make(mapStore))
  24. // Do one Chtimes call that will go through to the normal filesystem
  25. mtimefs.chtimes = os.Chtimes
  26. if err := mtimefs.Chtimes("testdata/exists0", testTime, testTime); err != nil {
  27. t.Error("Should not have failed:", err)
  28. }
  29. // Do one call that gets an error back from the underlying Chtimes
  30. mtimefs.chtimes = failChtimes
  31. if err := mtimefs.Chtimes("testdata/exists1", testTime, testTime); err != nil {
  32. t.Error("Should not have failed:", err)
  33. }
  34. // Do one call that gets struck by an exceptionally evil Chtimes
  35. mtimefs.chtimes = evilChtimes
  36. if err := mtimefs.Chtimes("testdata/exists2", testTime, testTime); err != nil {
  37. t.Error("Should not have failed:", err)
  38. }
  39. // All of the calls were successful, so an Lstat on them should return
  40. // the test timestamp.
  41. for _, file := range []string{"testdata/exists0", "testdata/exists1", "testdata/exists2"} {
  42. if info, err := mtimefs.Lstat(file); err != nil {
  43. t.Error("Lstat shouldn't fail:", err)
  44. } else if !info.ModTime().Equal(testTime) {
  45. t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
  46. }
  47. }
  48. // The two last files should certainly not have the correct timestamp
  49. // when looking directly on disk though.
  50. for _, file := range []string{"testdata/exists1", "testdata/exists2"} {
  51. if info, err := os.Lstat(filepath.Join(td, file)); err != nil {
  52. t.Error("Lstat shouldn't fail:", err)
  53. } else if info.ModTime().Equal(testTime) {
  54. t.Errorf("Unexpected time match; %v == %v", info.ModTime(), testTime)
  55. }
  56. }
  57. // Changing the timestamp on disk should be reflected in a new Lstat
  58. // call. Choose a time that is likely to be able to be on all reasonable
  59. // filesystems.
  60. testTime = time.Now().Add(5 * time.Hour).Truncate(time.Minute)
  61. os.Chtimes(filepath.Join(td, "testdata/exists0"), testTime, testTime)
  62. if info, err := mtimefs.Lstat("testdata/exists0"); err != nil {
  63. t.Error("Lstat shouldn't fail:", err)
  64. } else if !info.ModTime().Equal(testTime) {
  65. t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
  66. }
  67. }
  68. func TestMtimeFSWalk(t *testing.T) {
  69. dir := t.TempDir()
  70. mtimefs, walkFs := newMtimeFSWithWalk(dir, make(mapStore))
  71. underlying := mtimefs.Filesystem
  72. mtimefs.chtimes = failChtimes
  73. if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0o644); err != nil {
  74. t.Fatal(err)
  75. }
  76. oldStat, err := mtimefs.Lstat("file")
  77. if err != nil {
  78. t.Fatal(err)
  79. }
  80. newTime := time.Now().Add(-2 * time.Hour)
  81. if err := mtimefs.Chtimes("file", newTime, newTime); err != nil {
  82. t.Fatal(err)
  83. }
  84. if newStat, err := mtimefs.Lstat("file"); err != nil {
  85. t.Fatal(err)
  86. } else if !newStat.ModTime().Equal(newTime) {
  87. t.Errorf("expected time %v, lstat time %v", newTime, newStat.ModTime())
  88. }
  89. if underlyingStat, err := underlying.Lstat("file"); err != nil {
  90. t.Fatal(err)
  91. } else if !underlyingStat.ModTime().Equal(oldStat.ModTime()) {
  92. t.Errorf("expected time %v, lstat time %v", oldStat.ModTime(), underlyingStat.ModTime())
  93. }
  94. found := false
  95. _ = walkFs.Walk("", func(path string, info FileInfo, err error) error {
  96. if path == "file" {
  97. found = true
  98. if !info.ModTime().Equal(newTime) {
  99. t.Errorf("expected time %v, lstat time %v", newTime, info.ModTime())
  100. }
  101. }
  102. return nil
  103. })
  104. if !found {
  105. t.Error("did not find")
  106. }
  107. }
  108. func TestMtimeFSOpen(t *testing.T) {
  109. dir := t.TempDir()
  110. mtimefs := newMtimeFS(dir, make(mapStore))
  111. underlying := mtimefs.Filesystem
  112. mtimefs.chtimes = failChtimes
  113. if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0o644); err != nil {
  114. t.Fatal(err)
  115. }
  116. oldStat, err := mtimefs.Lstat("file")
  117. if err != nil {
  118. t.Fatal(err)
  119. }
  120. newTime := time.Now().Add(-2 * time.Hour)
  121. if err := mtimefs.Chtimes("file", newTime, newTime); err != nil {
  122. t.Fatal(err)
  123. }
  124. if newStat, err := mtimefs.Lstat("file"); err != nil {
  125. t.Fatal(err)
  126. } else if !newStat.ModTime().Equal(newTime) {
  127. t.Errorf("expected time %v, lstat time %v", newTime, newStat.ModTime())
  128. }
  129. if underlyingStat, err := underlying.Lstat("file"); err != nil {
  130. t.Fatal(err)
  131. } else if !underlyingStat.ModTime().Equal(oldStat.ModTime()) {
  132. t.Errorf("expected time %v, lstat time %v", oldStat.ModTime(), underlyingStat.ModTime())
  133. }
  134. fd, err := mtimefs.Open("file")
  135. if err != nil {
  136. t.Fatal(err)
  137. }
  138. defer fd.Close()
  139. info, err := fd.Stat()
  140. if err != nil {
  141. t.Fatal(err)
  142. }
  143. if !info.ModTime().Equal(newTime) {
  144. t.Errorf("expected time %v, lstat time %v", newTime, info.ModTime())
  145. }
  146. }
  147. func TestMtimeFSInsensitive(t *testing.T) {
  148. if build.IsDarwin || build.IsWindows {
  149. // blatantly assume file systems here are case insensitive. Might be
  150. // a spurious failure on oddly configured systems.
  151. } else {
  152. t.Skip("need case insensitive FS")
  153. }
  154. theTest := func(t *testing.T, fs *mtimeFS, shouldSucceed bool) {
  155. fs.RemoveAll("testdata")
  156. defer fs.RemoveAll("testdata")
  157. fs.Mkdir("testdata", 0o755)
  158. WriteFile(fs, "testdata/FiLe", []byte("hello"), 0o644)
  159. // a random time with nanosecond precision
  160. testTime := time.Unix(1234567890, 123456789)
  161. // Do one call that gets struck by an exceptionally evil Chtimes, with a
  162. // different case from what is on disk.
  163. fs.chtimes = evilChtimes
  164. if err := fs.Chtimes("testdata/fIlE", testTime, testTime); err != nil {
  165. t.Error("Should not have failed:", err)
  166. }
  167. // Check that we get back the mtime we set, if we were supposed to succeed.
  168. info, err := fs.Lstat("testdata/FILE")
  169. if err != nil {
  170. t.Error("Lstat shouldn't fail:", err)
  171. } else if info.ModTime().Equal(testTime) != shouldSucceed {
  172. t.Errorf("Time mismatch; got %v, comparison %v, expected equal=%v", info.ModTime(), testTime, shouldSucceed)
  173. }
  174. }
  175. // The test should fail with a case sensitive mtimefs
  176. t.Run("with case sensitive mtimefs", func(t *testing.T) {
  177. theTest(t, newMtimeFS(t.TempDir(), make(mapStore)), false)
  178. })
  179. // And succeed with a case insensitive one.
  180. t.Run("with case insensitive mtimefs", func(t *testing.T) {
  181. theTest(t, newMtimeFS(t.TempDir(), make(mapStore), WithCaseInsensitivity(true)), true)
  182. })
  183. }
  184. // The mapStore is a simple database
  185. type mapStore map[string][2]time.Time
  186. func (s mapStore) PutMtime(_, name string, real, virtual time.Time) error {
  187. s[name] = [2]time.Time{real, virtual}
  188. return nil
  189. }
  190. func (s mapStore) GetMtime(_, name string) (real, virtual time.Time) {
  191. v := s[name]
  192. return v[0], v[1]
  193. }
  194. func (s mapStore) DeleteMtime(_, name string) error {
  195. delete(s, name)
  196. return nil
  197. }
  198. // failChtimes does nothing, and fails
  199. func failChtimes(_ string, _, _ time.Time) error {
  200. return errors.New("no")
  201. }
  202. // evilChtimes will set an mtime that's 300 days in the future of what was
  203. // asked for, and truncate the time to the closest hour.
  204. func evilChtimes(name string, mtime, atime time.Time) error {
  205. return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
  206. }
  207. func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
  208. mtimefs, _ := newMtimeFSWithWalk(path, db, options...)
  209. return mtimefs
  210. }
  211. func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
  212. fs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, "", options...))
  213. wfs, _ := unwrapFilesystem[*walkFilesystem](fs)
  214. mfs, _ := unwrapFilesystem[*mtimeFS](fs)
  215. return mfs, wfs
  216. }