casefs_test.go 8.9 KB

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