123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- // Copyright (C) 2019 The Syncthing Authors.
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this file,
- // You can obtain one at https://mozilla.org/MPL/2.0/.
- package fs
- import (
- "bytes"
- "errors"
- "io"
- "math/rand"
- "os"
- "path/filepath"
- "syscall"
- "testing"
- )
- var (
- generationSize int64 = 4 << 20
- defaultCopySize int64 = 1 << 20
- testCases = []struct {
- name string
- // Starting size of files
- srcSize int64
- dstSize int64
- // Offset from which to read
- srcOffset int64
- dstOffset int64
- // Cursor position before the copy
- srcStartingPos int64
- dstStartingPos int64
- // Expected destination size
- expectedDstSizeAfterCopy int64
- // Custom copy size
- copySize int64
- // Expected failure
- expectedErrors map[CopyRangeMethod]error
- }{
- {
- name: "append to end",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: generationSize,
- srcStartingPos: generationSize,
- dstStartingPos: generationSize,
- expectedDstSizeAfterCopy: generationSize + defaultCopySize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- {
- name: "append to end offsets at start",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: generationSize,
- srcStartingPos: 0, // We seek back to start, and expect src not to move after copy
- dstStartingPos: 0, // Seek back, but expect dst pos to not change
- expectedDstSizeAfterCopy: generationSize + defaultCopySize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- {
- name: "overwrite part of destination region",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: defaultCopySize,
- dstOffset: generationSize,
- srcStartingPos: generationSize,
- dstStartingPos: generationSize,
- expectedDstSizeAfterCopy: generationSize + defaultCopySize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- {
- name: "overwrite all of destination",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: 0,
- srcStartingPos: generationSize,
- dstStartingPos: generationSize,
- expectedDstSizeAfterCopy: generationSize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- {
- name: "overwrite part of destination",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: defaultCopySize,
- dstOffset: 0,
- srcStartingPos: generationSize,
- dstStartingPos: generationSize,
- expectedDstSizeAfterCopy: generationSize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- // Write way past the end of the file
- {
- name: "destination gets expanded as it is being written to",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: generationSize * 2,
- srcStartingPos: generationSize,
- dstStartingPos: generationSize,
- expectedDstSizeAfterCopy: generationSize*2 + defaultCopySize,
- copySize: defaultCopySize,
- expectedErrors: nil,
- },
- // Source file does not have enough bytes to copy in that range, should result in an unexpected eof.
- {
- name: "source file too small",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: -11, // Does not matter, should fail.
- copySize: defaultCopySize * 10,
- // ioctl returns syscall.EINVAL, rest are wrapped
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: io.ErrUnexpectedEOF,
- CopyRangeMethodStandard: io.ErrUnexpectedEOF,
- CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
- CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
- CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
- CopyRangeMethodDuplicateExtents: io.ErrUnexpectedEOF,
- },
- },
- {
- name: "unaligned source offset unaligned size",
- srcSize: generationSize,
- dstSize: 0,
- srcOffset: 1,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: defaultCopySize + 1,
- copySize: defaultCopySize + 1,
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: syscall.EINVAL,
- CopyRangeMethodDuplicateExtents: syscall.EINVAL,
- },
- },
- {
- name: "unaligned source offset aligned size",
- srcSize: generationSize,
- dstSize: 0,
- srcOffset: 1,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: defaultCopySize,
- copySize: defaultCopySize,
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: syscall.EINVAL,
- CopyRangeMethodDuplicateExtents: syscall.EINVAL,
- },
- },
- {
- name: "unaligned destination offset unaligned size",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: 1,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: generationSize,
- copySize: defaultCopySize + 1,
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: syscall.EINVAL,
- CopyRangeMethodDuplicateExtents: syscall.EINVAL,
- },
- },
- {
- name: "unaligned destination offset aligned size",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: 1,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: generationSize,
- copySize: defaultCopySize,
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: syscall.EINVAL,
- CopyRangeMethodDuplicateExtents: syscall.EINVAL,
- },
- },
- {
- name: "aligned offsets unaligned size",
- srcSize: generationSize,
- dstSize: generationSize,
- srcOffset: 0,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: generationSize,
- copySize: defaultCopySize + 1,
- expectedErrors: map[CopyRangeMethod]error{
- CopyRangeMethodIoctl: syscall.EINVAL,
- CopyRangeMethodDuplicateExtents: syscall.EINVAL,
- },
- },
- // Last block that starts on a nice boundary
- {
- name: "last block",
- srcSize: generationSize + 2,
- dstSize: 0,
- srcOffset: generationSize,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: 2,
- copySize: 2,
- // Succeeds on all, as long as the offset is file-system block aligned.
- expectedErrors: nil,
- },
- // Copy whole file
- {
- name: "whole file copy block aligned",
- srcSize: generationSize,
- dstSize: 0,
- srcOffset: 0,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: generationSize,
- copySize: generationSize,
- expectedErrors: nil,
- },
- {
- name: "whole file copy not block aligned",
- srcSize: generationSize + 1,
- dstSize: 0,
- srcOffset: 0,
- dstOffset: 0,
- srcStartingPos: 0,
- dstStartingPos: 0,
- expectedDstSizeAfterCopy: generationSize + 1,
- copySize: generationSize + 1,
- expectedErrors: nil,
- },
- }
- )
- func TestCopyRange(tttt *testing.T) {
- randSrc := rand.New(rand.NewSource(rand.Int63()))
- paths := filepath.SplitList(os.Getenv("STFSTESTPATH"))
- if len(paths) == 0 {
- paths = []string{""}
- }
- for _, path := range paths {
- testPath, err := os.MkdirTemp(path, "")
- if err != nil {
- tttt.Fatal(err)
- }
- defer os.RemoveAll(testPath)
- name := path
- if name == "" {
- name = "tmp"
- }
- tttt.Run(name, func(ttt *testing.T) {
- for copyMethod, impl := range copyRangeMethods {
- ttt.Run(copyMethod.String(), func(tt *testing.T) {
- for _, testCase := range testCases {
- tt.Run(testCase.name, func(t *testing.T) {
- srcBuf := make([]byte, testCase.srcSize)
- dstBuf := make([]byte, testCase.dstSize)
- td, err := os.MkdirTemp(testPath, "")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(td)
- fs := NewFilesystem(FilesystemTypeBasic, td)
- if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
- t.Fatal(err)
- }
- if _, err := io.ReadFull(randSrc, dstBuf); err != nil {
- t.Fatal(err)
- }
- src, err := fs.Create("src")
- if err != nil {
- t.Fatal(err)
- }
- defer func() { _ = src.Close() }()
- dst, err := fs.Create("dst")
- if err != nil {
- t.Fatal(err)
- }
- defer func() { _ = dst.Close() }()
- // Write some data
- if _, err := src.Write(srcBuf); err != nil {
- t.Fatal(err)
- }
- if _, err := dst.Write(dstBuf); err != nil {
- t.Fatal(err)
- }
- // Set the offsets
- if n, err := src.Seek(testCase.srcStartingPos, io.SeekStart); err != nil || n != testCase.srcStartingPos {
- t.Fatal(err)
- }
- if n, err := dst.Seek(testCase.dstStartingPos, io.SeekStart); err != nil || n != testCase.dstStartingPos {
- t.Fatal(err)
- }
- srcBasic, ok := unwrap(src).(basicFile)
- if !ok {
- t.Fatal("src file is not a basic file")
- }
- dstBasic, ok := unwrap(dst).(basicFile)
- if !ok {
- t.Fatal("dst file is not a basic file")
- }
- if err := impl(srcBasic, dstBasic, testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
- if errors.Is(err, errors.ErrUnsupported) {
- // Test runner can adjust directory in which to run the tests, that allow broader tests.
- t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
- }
- if testCase.expectedErrors[copyMethod] == err {
- return
- }
- t.Fatal(err)
- } else if testCase.expectedErrors[copyMethod] != nil {
- t.Fatal("did not get expected error")
- }
- // Check offsets where we expect them
- if srcCurPos, err := src.Seek(0, io.SeekCurrent); err != nil {
- t.Fatal(err)
- } else if srcCurPos != testCase.srcStartingPos {
- t.Errorf("src pos expected %d got %d", testCase.srcStartingPos, srcCurPos)
- }
- if dstCurPos, err := dst.Seek(0, io.SeekCurrent); err != nil {
- t.Fatal(err)
- } else if dstCurPos != testCase.dstStartingPos {
- t.Errorf("dst pos expected %d got %d", testCase.dstStartingPos, dstCurPos)
- }
- // Check dst size
- if fi, err := dst.Stat(); err != nil {
- t.Fatal(err)
- } else if fi.Size() != testCase.expectedDstSizeAfterCopy {
- t.Errorf("expected %d size, got %d", testCase.expectedDstSizeAfterCopy, fi.Size())
- }
- // Check the data is as expected
- if _, err := dst.Seek(0, io.SeekStart); err != nil {
- t.Fatal(err)
- }
- resultBuf := make([]byte, testCase.expectedDstSizeAfterCopy)
- if _, err := io.ReadFull(dst, resultBuf); err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(srcBuf[testCase.srcOffset:testCase.srcOffset+testCase.copySize], resultBuf[testCase.dstOffset:testCase.dstOffset+testCase.copySize]) {
- t.Errorf("Not equal")
- }
- // Check not copied content does not get corrupted
- if testCase.dstOffset > testCase.dstSize {
- if !bytes.Equal(dstBuf[:testCase.dstSize], resultBuf[:testCase.dstSize]) {
- t.Error("region before copy region not equals")
- }
- if !bytes.Equal(resultBuf[testCase.dstSize:testCase.dstOffset], make([]byte, testCase.dstOffset-testCase.dstSize)) {
- t.Error("found non zeroes in expected zero region")
- }
- } else {
- if !bytes.Equal(dstBuf[:testCase.dstOffset], resultBuf[:testCase.dstOffset]) {
- t.Error("region before copy region not equals")
- }
- afterCopyStart := testCase.dstOffset + testCase.copySize
- if afterCopyStart < testCase.dstSize {
- if !bytes.Equal(dstBuf[afterCopyStart:], resultBuf[afterCopyStart:len(dstBuf)]) {
- t.Error("region after copy region not equals")
- }
- }
- }
- })
- }
- })
- }
- })
- }
- }
|