casefs_test.go 8.9 KB

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