filesystem_copy_range_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. // Copyright (C) 2019 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. "bytes"
  9. "errors"
  10. "io"
  11. "math/rand"
  12. "os"
  13. "path/filepath"
  14. "syscall"
  15. "testing"
  16. )
  17. var (
  18. generationSize int64 = 4 << 20
  19. defaultCopySize int64 = 1 << 20
  20. testCases = []struct {
  21. name string
  22. // Starting size of files
  23. srcSize int64
  24. dstSize int64
  25. // Offset from which to read
  26. srcOffset int64
  27. dstOffset int64
  28. // Cursor position before the copy
  29. srcStartingPos int64
  30. dstStartingPos int64
  31. // Expected destination size
  32. expectedDstSizeAfterCopy int64
  33. // Custom copy size
  34. copySize int64
  35. // Expected failure
  36. expectedErrors map[CopyRangeMethod]error
  37. }{
  38. {
  39. name: "append to end",
  40. srcSize: generationSize,
  41. dstSize: generationSize,
  42. srcOffset: 0,
  43. dstOffset: generationSize,
  44. srcStartingPos: generationSize,
  45. dstStartingPos: generationSize,
  46. expectedDstSizeAfterCopy: generationSize + defaultCopySize,
  47. copySize: defaultCopySize,
  48. expectedErrors: nil,
  49. },
  50. {
  51. name: "append to end offsets at start",
  52. srcSize: generationSize,
  53. dstSize: generationSize,
  54. srcOffset: 0,
  55. dstOffset: generationSize,
  56. srcStartingPos: 0, // We seek back to start, and expect src not to move after copy
  57. dstStartingPos: 0, // Seek back, but expect dst pos to not change
  58. expectedDstSizeAfterCopy: generationSize + defaultCopySize,
  59. copySize: defaultCopySize,
  60. expectedErrors: nil,
  61. },
  62. {
  63. name: "overwrite part of destination region",
  64. srcSize: generationSize,
  65. dstSize: generationSize,
  66. srcOffset: defaultCopySize,
  67. dstOffset: generationSize,
  68. srcStartingPos: generationSize,
  69. dstStartingPos: generationSize,
  70. expectedDstSizeAfterCopy: generationSize + defaultCopySize,
  71. copySize: defaultCopySize,
  72. expectedErrors: nil,
  73. },
  74. {
  75. name: "overwrite all of destination",
  76. srcSize: generationSize,
  77. dstSize: generationSize,
  78. srcOffset: 0,
  79. dstOffset: 0,
  80. srcStartingPos: generationSize,
  81. dstStartingPos: generationSize,
  82. expectedDstSizeAfterCopy: generationSize,
  83. copySize: defaultCopySize,
  84. expectedErrors: nil,
  85. },
  86. {
  87. name: "overwrite part of destination",
  88. srcSize: generationSize,
  89. dstSize: generationSize,
  90. srcOffset: defaultCopySize,
  91. dstOffset: 0,
  92. srcStartingPos: generationSize,
  93. dstStartingPos: generationSize,
  94. expectedDstSizeAfterCopy: generationSize,
  95. copySize: defaultCopySize,
  96. expectedErrors: nil,
  97. },
  98. // Write way past the end of the file
  99. {
  100. name: "destination gets expanded as it is being written to",
  101. srcSize: generationSize,
  102. dstSize: generationSize,
  103. srcOffset: 0,
  104. dstOffset: generationSize * 2,
  105. srcStartingPos: generationSize,
  106. dstStartingPos: generationSize,
  107. expectedDstSizeAfterCopy: generationSize*2 + defaultCopySize,
  108. copySize: defaultCopySize,
  109. expectedErrors: nil,
  110. },
  111. // Source file does not have enough bytes to copy in that range, should result in an unexpected eof.
  112. {
  113. name: "source file too small",
  114. srcSize: generationSize,
  115. dstSize: generationSize,
  116. srcOffset: 0,
  117. dstOffset: 0,
  118. srcStartingPos: 0,
  119. dstStartingPos: 0,
  120. expectedDstSizeAfterCopy: -11, // Does not matter, should fail.
  121. copySize: defaultCopySize * 10,
  122. // ioctl returns syscall.EINVAL, rest are wrapped
  123. expectedErrors: map[CopyRangeMethod]error{
  124. CopyRangeMethodIoctl: io.ErrUnexpectedEOF,
  125. CopyRangeMethodStandard: io.ErrUnexpectedEOF,
  126. CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
  127. CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
  128. CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
  129. CopyRangeMethodDuplicateExtents: io.ErrUnexpectedEOF,
  130. },
  131. },
  132. {
  133. name: "unaligned source offset unaligned size",
  134. srcSize: generationSize,
  135. dstSize: 0,
  136. srcOffset: 1,
  137. dstOffset: 0,
  138. srcStartingPos: 0,
  139. dstStartingPos: 0,
  140. expectedDstSizeAfterCopy: defaultCopySize + 1,
  141. copySize: defaultCopySize + 1,
  142. expectedErrors: map[CopyRangeMethod]error{
  143. CopyRangeMethodIoctl: syscall.EINVAL,
  144. CopyRangeMethodDuplicateExtents: syscall.EINVAL,
  145. },
  146. },
  147. {
  148. name: "unaligned source offset aligned size",
  149. srcSize: generationSize,
  150. dstSize: 0,
  151. srcOffset: 1,
  152. dstOffset: 0,
  153. srcStartingPos: 0,
  154. dstStartingPos: 0,
  155. expectedDstSizeAfterCopy: defaultCopySize,
  156. copySize: defaultCopySize,
  157. expectedErrors: map[CopyRangeMethod]error{
  158. CopyRangeMethodIoctl: syscall.EINVAL,
  159. CopyRangeMethodDuplicateExtents: syscall.EINVAL,
  160. },
  161. },
  162. {
  163. name: "unaligned destination offset unaligned size",
  164. srcSize: generationSize,
  165. dstSize: generationSize,
  166. srcOffset: 0,
  167. dstOffset: 1,
  168. srcStartingPos: 0,
  169. dstStartingPos: 0,
  170. expectedDstSizeAfterCopy: generationSize,
  171. copySize: defaultCopySize + 1,
  172. expectedErrors: map[CopyRangeMethod]error{
  173. CopyRangeMethodIoctl: syscall.EINVAL,
  174. CopyRangeMethodDuplicateExtents: syscall.EINVAL,
  175. },
  176. },
  177. {
  178. name: "unaligned destination offset aligned size",
  179. srcSize: generationSize,
  180. dstSize: generationSize,
  181. srcOffset: 0,
  182. dstOffset: 1,
  183. srcStartingPos: 0,
  184. dstStartingPos: 0,
  185. expectedDstSizeAfterCopy: generationSize,
  186. copySize: defaultCopySize,
  187. expectedErrors: map[CopyRangeMethod]error{
  188. CopyRangeMethodIoctl: syscall.EINVAL,
  189. CopyRangeMethodDuplicateExtents: syscall.EINVAL,
  190. },
  191. },
  192. {
  193. name: "aligned offsets unaligned size",
  194. srcSize: generationSize,
  195. dstSize: generationSize,
  196. srcOffset: 0,
  197. dstOffset: 0,
  198. srcStartingPos: 0,
  199. dstStartingPos: 0,
  200. expectedDstSizeAfterCopy: generationSize,
  201. copySize: defaultCopySize + 1,
  202. expectedErrors: map[CopyRangeMethod]error{
  203. CopyRangeMethodIoctl: syscall.EINVAL,
  204. CopyRangeMethodDuplicateExtents: syscall.EINVAL,
  205. },
  206. },
  207. // Last block that starts on a nice boundary
  208. {
  209. name: "last block",
  210. srcSize: generationSize + 2,
  211. dstSize: 0,
  212. srcOffset: generationSize,
  213. dstOffset: 0,
  214. srcStartingPos: 0,
  215. dstStartingPos: 0,
  216. expectedDstSizeAfterCopy: 2,
  217. copySize: 2,
  218. // Succeeds on all, as long as the offset is file-system block aligned.
  219. expectedErrors: nil,
  220. },
  221. // Copy whole file
  222. {
  223. name: "whole file copy block aligned",
  224. srcSize: generationSize,
  225. dstSize: 0,
  226. srcOffset: 0,
  227. dstOffset: 0,
  228. srcStartingPos: 0,
  229. dstStartingPos: 0,
  230. expectedDstSizeAfterCopy: generationSize,
  231. copySize: generationSize,
  232. expectedErrors: nil,
  233. },
  234. {
  235. name: "whole file copy not block aligned",
  236. srcSize: generationSize + 1,
  237. dstSize: 0,
  238. srcOffset: 0,
  239. dstOffset: 0,
  240. srcStartingPos: 0,
  241. dstStartingPos: 0,
  242. expectedDstSizeAfterCopy: generationSize + 1,
  243. copySize: generationSize + 1,
  244. expectedErrors: nil,
  245. },
  246. }
  247. )
  248. func TestCopyRange(tttt *testing.T) {
  249. randSrc := rand.New(rand.NewSource(rand.Int63()))
  250. paths := filepath.SplitList(os.Getenv("STFSTESTPATH"))
  251. if len(paths) == 0 {
  252. paths = []string{""}
  253. }
  254. for _, path := range paths {
  255. testPath, err := os.MkdirTemp(path, "")
  256. if err != nil {
  257. tttt.Fatal(err)
  258. }
  259. defer os.RemoveAll(testPath)
  260. name := path
  261. if name == "" {
  262. name = "tmp"
  263. }
  264. tttt.Run(name, func(ttt *testing.T) {
  265. for copyMethod, impl := range copyRangeMethods {
  266. ttt.Run(copyMethod.String(), func(tt *testing.T) {
  267. for _, testCase := range testCases {
  268. tt.Run(testCase.name, func(t *testing.T) {
  269. srcBuf := make([]byte, testCase.srcSize)
  270. dstBuf := make([]byte, testCase.dstSize)
  271. td, err := os.MkdirTemp(testPath, "")
  272. if err != nil {
  273. t.Fatal(err)
  274. }
  275. defer os.RemoveAll(td)
  276. fs := NewFilesystem(FilesystemTypeBasic, td)
  277. if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
  278. t.Fatal(err)
  279. }
  280. if _, err := io.ReadFull(randSrc, dstBuf); err != nil {
  281. t.Fatal(err)
  282. }
  283. src, err := fs.Create("src")
  284. if err != nil {
  285. t.Fatal(err)
  286. }
  287. defer func() { _ = src.Close() }()
  288. dst, err := fs.Create("dst")
  289. if err != nil {
  290. t.Fatal(err)
  291. }
  292. defer func() { _ = dst.Close() }()
  293. // Write some data
  294. if _, err := src.Write(srcBuf); err != nil {
  295. t.Fatal(err)
  296. }
  297. if _, err := dst.Write(dstBuf); err != nil {
  298. t.Fatal(err)
  299. }
  300. // Set the offsets
  301. if n, err := src.Seek(testCase.srcStartingPos, io.SeekStart); err != nil || n != testCase.srcStartingPos {
  302. t.Fatal(err)
  303. }
  304. if n, err := dst.Seek(testCase.dstStartingPos, io.SeekStart); err != nil || n != testCase.dstStartingPos {
  305. t.Fatal(err)
  306. }
  307. srcBasic, ok := unwrap(src).(basicFile)
  308. if !ok {
  309. t.Fatal("src file is not a basic file")
  310. }
  311. dstBasic, ok := unwrap(dst).(basicFile)
  312. if !ok {
  313. t.Fatal("dst file is not a basic file")
  314. }
  315. if err := impl(srcBasic, dstBasic, testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
  316. if errors.Is(err, errors.ErrUnsupported) {
  317. // Test runner can adjust directory in which to run the tests, that allow broader tests.
  318. t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
  319. }
  320. if testCase.expectedErrors[copyMethod] == err {
  321. return
  322. }
  323. t.Fatal(err)
  324. } else if testCase.expectedErrors[copyMethod] != nil {
  325. t.Fatal("did not get expected error")
  326. }
  327. // Check offsets where we expect them
  328. if srcCurPos, err := src.Seek(0, io.SeekCurrent); err != nil {
  329. t.Fatal(err)
  330. } else if srcCurPos != testCase.srcStartingPos {
  331. t.Errorf("src pos expected %d got %d", testCase.srcStartingPos, srcCurPos)
  332. }
  333. if dstCurPos, err := dst.Seek(0, io.SeekCurrent); err != nil {
  334. t.Fatal(err)
  335. } else if dstCurPos != testCase.dstStartingPos {
  336. t.Errorf("dst pos expected %d got %d", testCase.dstStartingPos, dstCurPos)
  337. }
  338. // Check dst size
  339. if fi, err := dst.Stat(); err != nil {
  340. t.Fatal(err)
  341. } else if fi.Size() != testCase.expectedDstSizeAfterCopy {
  342. t.Errorf("expected %d size, got %d", testCase.expectedDstSizeAfterCopy, fi.Size())
  343. }
  344. // Check the data is as expected
  345. if _, err := dst.Seek(0, io.SeekStart); err != nil {
  346. t.Fatal(err)
  347. }
  348. resultBuf := make([]byte, testCase.expectedDstSizeAfterCopy)
  349. if _, err := io.ReadFull(dst, resultBuf); err != nil {
  350. t.Fatal(err)
  351. }
  352. if !bytes.Equal(srcBuf[testCase.srcOffset:testCase.srcOffset+testCase.copySize], resultBuf[testCase.dstOffset:testCase.dstOffset+testCase.copySize]) {
  353. t.Errorf("Not equal")
  354. }
  355. // Check not copied content does not get corrupted
  356. if testCase.dstOffset > testCase.dstSize {
  357. if !bytes.Equal(dstBuf[:testCase.dstSize], resultBuf[:testCase.dstSize]) {
  358. t.Error("region before copy region not equals")
  359. }
  360. if !bytes.Equal(resultBuf[testCase.dstSize:testCase.dstOffset], make([]byte, testCase.dstOffset-testCase.dstSize)) {
  361. t.Error("found non zeroes in expected zero region")
  362. }
  363. } else {
  364. if !bytes.Equal(dstBuf[:testCase.dstOffset], resultBuf[:testCase.dstOffset]) {
  365. t.Error("region before copy region not equals")
  366. }
  367. afterCopyStart := testCase.dstOffset + testCase.copySize
  368. if afterCopyStart < testCase.dstSize {
  369. if !bytes.Equal(dstBuf[afterCopyStart:], resultBuf[afterCopyStart:len(dstBuf)]) {
  370. t.Error("region after copy region not equals")
  371. }
  372. }
  373. }
  374. })
  375. }
  376. })
  377. }
  378. })
  379. }
  380. }