casefs_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. // Copyright (C) 2020 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. "fmt"
  10. "os"
  11. "path/filepath"
  12. "runtime"
  13. "strings"
  14. "testing"
  15. "time"
  16. )
  17. func TestRealCase(t *testing.T) {
  18. // Verify realCase lookups on various underlying filesystems.
  19. t.Run("fake-sensitive", func(t *testing.T) {
  20. testRealCase(t, newFakeFilesystem(t.Name()))
  21. })
  22. t.Run("fake-insensitive", func(t *testing.T) {
  23. testRealCase(t, newFakeFilesystem(t.Name()+"?insens=true"))
  24. })
  25. t.Run("actual", func(t *testing.T) {
  26. fsys, tmpDir := setup(t)
  27. defer os.RemoveAll(tmpDir)
  28. testRealCase(t, fsys)
  29. })
  30. }
  31. func testRealCase(t *testing.T, fsys Filesystem) {
  32. testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
  33. comps := []string{"Foo", "bar", "BAZ", "bAs"}
  34. path := filepath.Join(comps...)
  35. testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0777)
  36. fd, err := testFs.Create(path)
  37. if err != nil {
  38. t.Fatal(err)
  39. }
  40. fd.Close()
  41. for i, tc := range []struct {
  42. in string
  43. len int
  44. }{
  45. {path, 4},
  46. {strings.ToLower(path), 4},
  47. {strings.ToUpper(path), 4},
  48. {"foo", 1},
  49. {"FOO", 1},
  50. {"foO", 1},
  51. {filepath.Join("Foo", "bar"), 2},
  52. {filepath.Join("Foo", "bAr"), 2},
  53. {filepath.Join("FoO", "bar"), 2},
  54. {filepath.Join("foo", "bar", "BAZ"), 3},
  55. {filepath.Join("Foo", "bar", "bAz"), 3},
  56. {filepath.Join("foo", "bar", "BAZ"), 3}, // Repeat on purpose
  57. } {
  58. out, err := testFs.realCase(tc.in)
  59. if err != nil {
  60. t.Error(err)
  61. } else if exp := filepath.Join(comps[:tc.len]...); out != exp {
  62. t.Errorf("tc %v: Expected %v, got %v", i, exp, out)
  63. }
  64. }
  65. }
  66. func TestRealCaseSensitive(t *testing.T) {
  67. // Verify that realCase returns the best on-disk case for case sensitive
  68. // systems. Test is skipped if the underlying fs is insensitive.
  69. t.Run("fake-sensitive", func(t *testing.T) {
  70. testRealCaseSensitive(t, newFakeFilesystem(t.Name()))
  71. })
  72. t.Run("actual", func(t *testing.T) {
  73. fsys, tmpDir := setup(t)
  74. defer os.RemoveAll(tmpDir)
  75. testRealCaseSensitive(t, fsys)
  76. })
  77. }
  78. func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
  79. testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
  80. names := make([]string, 2)
  81. names[0] = "foo"
  82. names[1] = strings.ToUpper(names[0])
  83. for _, n := range names {
  84. if err := testFs.MkdirAll(n, 0777); err != nil {
  85. if IsErrCaseConflict(err) {
  86. t.Skip("Filesystem is case-insensitive")
  87. }
  88. t.Fatal(err)
  89. }
  90. }
  91. for _, n := range names {
  92. if rn, err := testFs.realCase(n); err != nil {
  93. t.Error(err)
  94. } else if rn != n {
  95. t.Errorf("Got %v, expected %v", rn, n)
  96. }
  97. }
  98. }
  99. func TestCaseFSStat(t *testing.T) {
  100. // Verify that a Stat() lookup behaves in a case sensitive manner
  101. // regardless of the underlying fs.
  102. t.Run("fake-sensitive", func(t *testing.T) {
  103. testCaseFSStat(t, newFakeFilesystem(t.Name()))
  104. })
  105. t.Run("fake-insensitive", func(t *testing.T) {
  106. testCaseFSStat(t, newFakeFilesystem(t.Name()+"?insens=true"))
  107. })
  108. t.Run("actual", func(t *testing.T) {
  109. fsys, tmpDir := setup(t)
  110. defer os.RemoveAll(tmpDir)
  111. testCaseFSStat(t, fsys)
  112. })
  113. }
  114. func testCaseFSStat(t *testing.T, fsys Filesystem) {
  115. fd, err := fsys.Create("foo")
  116. if err != nil {
  117. t.Fatal(err)
  118. }
  119. fd.Close()
  120. // Check if the underlying fs is sensitive or not
  121. sensitive := true
  122. if _, err = fsys.Stat("FOO"); err == nil {
  123. sensitive = false
  124. }
  125. testFs := NewCaseFilesystem(fsys)
  126. _, err = testFs.Stat("FOO")
  127. if sensitive {
  128. if IsNotExist(err) {
  129. t.Log("pass: case sensitive underlying fs")
  130. } else {
  131. t.Error("expected NotExist, not", err, "for sensitive fs")
  132. }
  133. } else if IsErrCaseConflict(err) {
  134. t.Log("pass: case insensitive underlying fs")
  135. } else {
  136. t.Error("expected ErrCaseConflict, not", err, "for insensitive fs")
  137. }
  138. }
  139. func BenchmarkWalkCaseFakeFS10k(b *testing.B) {
  140. fsys, paths, err := fakefsForBenchmark(10_000, 0)
  141. if err != nil {
  142. b.Fatal(err)
  143. }
  144. slowsys, paths, err := fakefsForBenchmark(10_000, 100*time.Microsecond)
  145. if err != nil {
  146. b.Fatal(err)
  147. }
  148. b.Run("raw-fastfs", func(b *testing.B) {
  149. benchmarkWalkFakeFS(b, fsys, paths)
  150. b.ReportAllocs()
  151. })
  152. b.Run("case-fastfs", func(b *testing.B) {
  153. benchmarkWalkFakeFS(b, NewCaseFilesystem(fsys), paths)
  154. b.ReportAllocs()
  155. })
  156. b.Run("raw-slowfs", func(b *testing.B) {
  157. benchmarkWalkFakeFS(b, slowsys, paths)
  158. b.ReportAllocs()
  159. })
  160. b.Run("case-slowfs", func(b *testing.B) {
  161. benchmarkWalkFakeFS(b, NewCaseFilesystem(slowsys), paths)
  162. b.ReportAllocs()
  163. })
  164. }
  165. func benchmarkWalkFakeFS(b *testing.B, fsys Filesystem, paths []string) {
  166. // Simulate a scanner pass over the filesystem. First walk it to
  167. // discover all names, then stat each name individually to check if it's
  168. // been deleted or not (pretending that they all existed in the
  169. // database).
  170. var ms0 runtime.MemStats
  171. runtime.ReadMemStats(&ms0)
  172. t0 := time.Now()
  173. for i := 0; i < b.N; i++ {
  174. if err := doubleWalkFS(fsys, paths); err != nil {
  175. b.Fatal(err)
  176. }
  177. }
  178. t1 := time.Now()
  179. var ms1 runtime.MemStats
  180. runtime.ReadMemStats(&ms1)
  181. // We add metrics per path entry
  182. b.ReportMetric(float64(t1.Sub(t0))/float64(b.N)/float64(len(paths)), "ns/entry")
  183. b.ReportMetric(float64(ms1.Mallocs-ms0.Mallocs)/float64(b.N)/float64(len(paths)), "allocs/entry")
  184. b.ReportMetric(float64(ms1.TotalAlloc-ms0.TotalAlloc)/float64(b.N)/float64(len(paths)), "B/entry")
  185. }
  186. func TestStressCaseFS(t *testing.T) {
  187. // Exercise a bunch of paralell operations for stressing out race
  188. // conditions in the realnamer cache etc.
  189. const limit = 10 * time.Second
  190. if testing.Short() {
  191. t.Skip("long test")
  192. }
  193. fsys, paths, err := fakefsForBenchmark(10_000, 0)
  194. if err != nil {
  195. t.Fatal(err)
  196. }
  197. for i := 0; i < runtime.NumCPU()/2+1; i++ {
  198. t.Run(fmt.Sprintf("walker-%d", i), func(t *testing.T) {
  199. // Walk the filesystem and stat everything
  200. t.Parallel()
  201. t0 := time.Now()
  202. for time.Since(t0) < limit {
  203. if err := doubleWalkFS(fsys, paths); err != nil {
  204. t.Fatal(err)
  205. }
  206. }
  207. })
  208. t.Run(fmt.Sprintf("toucher-%d", i), func(t *testing.T) {
  209. // Touch all the things
  210. t.Parallel()
  211. t0 := time.Now()
  212. for time.Since(t0) < limit {
  213. for _, p := range paths {
  214. now := time.Now()
  215. if err := fsys.Chtimes(p, now, now); err != nil {
  216. t.Fatal(err)
  217. }
  218. }
  219. }
  220. })
  221. }
  222. }
  223. func doubleWalkFS(fsys Filesystem, paths []string) error {
  224. if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
  225. return err
  226. }); err != nil {
  227. return err
  228. }
  229. for _, p := range paths {
  230. if _, err := fsys.Lstat(p); err != nil {
  231. return err
  232. }
  233. }
  234. return nil
  235. }
  236. func fakefsForBenchmark(nfiles int, latency time.Duration) (Filesystem, []string, error) {
  237. fsys := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("fakefsForBenchmark?files=%d&insens=true&latency=%s", nfiles, latency))
  238. var paths []string
  239. if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
  240. paths = append(paths, path)
  241. return err
  242. }); err != nil {
  243. return nil, nil, err
  244. }
  245. if len(paths) < nfiles {
  246. return nil, nil, errors.New("didn't find enough stuff")
  247. }
  248. return fsys, paths, nil
  249. }