| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <fcntl.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <random> |
| #include <string_view> |
| |
| #include <fbl/unique_fd.h> |
| |
| #include "src/storage/fs_test/fs_test_fixture.h" |
| |
| namespace fs_test { |
| namespace { |
| |
| using RwTest = FilesystemTest; |
| |
| // Test that zero length read and write operations are valid. |
| TEST_P(RwTest, ZeroLengthOperations) { |
| const std::string filename = GetPath("zero_length_ops"); |
| fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT, 0644)); |
| ASSERT_TRUE(fd); |
| |
| // Zero-length write. |
| ASSERT_EQ(write(fd.get(), nullptr, 0), 0); |
| ASSERT_EQ(pwrite(fd.get(), nullptr, 0, 0), 0); |
| |
| // Zero-length read. |
| ASSERT_EQ(read(fd.get(), nullptr, 0), 0); |
| ASSERT_EQ(pread(fd.get(), nullptr, 0, 0), 0); |
| |
| // Seek pointer unchanged. |
| ASSERT_EQ(lseek(fd.get(), 0, SEEK_CUR), 0); |
| |
| ASSERT_EQ(close(fd.release()), 0); |
| ASSERT_EQ(unlink(filename.c_str()), 0); |
| } |
| |
| // Test that zero length write interleaved with non-zero length writes |
| // works as expected. |
| TEST_P(RwTest, ZeroLengthOperationWithNonZeroLengthWrites) { |
| const std::string filename = GetPath("zero_length_after_large_offset_ops"); |
| fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT, 0644)); |
| ASSERT_TRUE(fd); |
| |
| constexpr size_t kBufferSize = 65374; |
| std::unique_ptr<uint8_t[]> buffer(new uint8_t[kBufferSize]()); |
| |
| memset(buffer.get(), 0, kBufferSize); |
| uint64_t kLargeOffset = 50ull * 1024ull * 1024ull; |
| uint64_t kOffset = 11ull * 1024ull * 1024ull; |
| |
| ASSERT_EQ(pwrite(fd.get(), buffer.get(), kBufferSize, kLargeOffset), |
| static_cast<ssize_t>(kBufferSize)); |
| ASSERT_EQ(pwrite(fd.get(), nullptr, 0, kOffset), 0); |
| ASSERT_EQ(pwrite(fd.get(), buffer.get(), kBufferSize, kLargeOffset - 8192), |
| static_cast<ssize_t>(kBufferSize)); |
| |
| ASSERT_EQ(close(fd.release()), 0); |
| } |
| |
| // Test that non-zero length read_at and write_at operations are valid. |
| TEST_P(RwTest, OffsetOperations) { |
| srand(0xDEADBEEF); |
| |
| constexpr size_t kBufferSize = PAGE_SIZE; |
| uint8_t expected[kBufferSize]; |
| for (size_t i = 0; i < std::size(expected); i++) { |
| expected[i] = static_cast<uint8_t>(rand()); |
| } |
| |
| struct TestOption { |
| size_t write_start; |
| size_t read_start; |
| size_t expected_read_length; |
| }; |
| |
| TestOption options[] = { |
| {0, 0, kBufferSize}, |
| {0, 1, kBufferSize - 1}, |
| {1, 0, kBufferSize}, |
| {1, 1, kBufferSize}, |
| }; |
| |
| for (const auto& opt : options) { |
| const std::string filename = GetPath("offset_ops"); |
| fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT, 0644)); |
| ASSERT_TRUE(fd); |
| |
| uint8_t buf[kBufferSize]; |
| memset(buf, 0, sizeof(buf)); |
| |
| // 1) Write "kBufferSize" bytes at opt.write_start |
| ASSERT_EQ(pwrite(fd.get(), expected, sizeof(expected), opt.write_start), |
| static_cast<ssize_t>(sizeof(expected))); |
| |
| // 2) Read "kBufferSize" bytes at opt.read_start; |
| // actually read opt.expected_read_length bytes. |
| ASSERT_EQ(pread(fd.get(), buf, sizeof(expected), opt.read_start), |
| static_cast<ssize_t>(opt.expected_read_length)); |
| |
| // 3) Verify the contents of the read matched, the seek |
| // pointer is unchanged, and the file size is correct. |
| if (opt.write_start <= opt.read_start) { |
| size_t read_skip = opt.read_start - opt.write_start; |
| ASSERT_EQ(memcmp(buf, expected + read_skip, opt.expected_read_length), 0); |
| } else { |
| size_t write_skip = opt.write_start - opt.read_start; |
| uint8_t zeroes[write_skip]; |
| memset(zeroes, 0, sizeof(zeroes)); |
| ASSERT_EQ(memcmp(buf, zeroes, write_skip), 0); |
| } |
| ASSERT_EQ(lseek(fd.get(), 0, SEEK_CUR), 0); |
| struct stat st; |
| ASSERT_EQ(fstat(fd.get(), &st), 0); |
| ASSERT_EQ(st.st_size, static_cast<ssize_t>(opt.write_start + sizeof(expected))); |
| |
| ASSERT_EQ(close(fd.release()), 0); |
| ASSERT_EQ(unlink(filename.c_str()), 0); |
| } |
| } |
| |
| using RwFullDiskTest = FilesystemTest; |
| |
| TEST_P(RwFullDiskTest, PartialWriteSucceedsForFullDisk) { |
| fbl::unique_fd fd(open(GetPath("bigfile").c_str(), O_CREAT | O_RDWR, 0644)); |
| ASSERT_TRUE(fd); |
| constexpr int kBufSize = 131072; |
| std::vector<uint64_t> data(kBufSize / sizeof(uint64_t)); |
| std::random_device random_device; |
| std::default_random_engine random(random_device()); |
| std::uniform_int_distribution<uint64_t> distribution; |
| std::generate(data.begin(), data.end(), [&]() { return distribution(random); }); |
| off_t done = 0; |
| for (;;) { |
| const int offset = done % kBufSize; |
| int len = kBufSize - offset; |
| // We should always hit ENOSPC on a power of 2; make sure that we'll always have a short write |
| // at the end. |
| if (done + len % 2 == 0) { |
| --len; |
| } |
| ssize_t r = write(fd.get(), reinterpret_cast<uint8_t*>(data.data()) + offset, len); |
| if (r < 0) { |
| EXPECT_EQ(errno, ENOSPC); |
| break; |
| } |
| EXPECT_LE(r, len); |
| done += r; |
| } |
| struct stat stat_buf; |
| ASSERT_EQ(fstat(fd.get(), &stat_buf), 0) << errno; |
| EXPECT_EQ(stat_buf.st_size, done); |
| ASSERT_EQ(lseek(fd.get(), 0, SEEK_SET), 0) << errno; |
| std::vector<uint8_t> read_buf(kBufSize); |
| off_t verified = 0; |
| for (;;) { |
| const int offset = verified % kBufSize; |
| int len = kBufSize - offset; |
| ssize_t r = read(fd.get(), read_buf.data(), len); |
| ASSERT_GE(r, 0) << errno; |
| if (r == 0) { |
| EXPECT_EQ(verified, done); |
| break; |
| } |
| ASSERT_LE(r, len); |
| EXPECT_EQ(memcmp(read_buf.data(), reinterpret_cast<uint8_t*>(data.data()) + offset, r), 0); |
| verified += r; |
| } |
| } |
| |
| using RwSparseTest = FilesystemTest; |
| |
| TEST_P(RwSparseTest, MaxFileSize) { |
| constexpr std::string_view kTestData = "hello"; |
| off_t offset = fs().GetTraits().max_file_size - kTestData.size(); |
| const std::string foo = GetPath("foo"); |
| { |
| fbl::unique_fd fd(open(foo.c_str(), O_RDWR | O_CREAT, 0644)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(pwrite(fd.get(), kTestData.data(), kTestData.size(), offset), |
| static_cast<ssize_t>(kTestData.size())); |
| ASSERT_EQ(fsync(fd.get()), 0); // Deliberate sync so that close is likely to unload the vnode. |
| ASSERT_EQ(close(fd.release()), 0); |
| } |
| { |
| fbl::unique_fd fd(open(foo.c_str(), O_RDONLY)); |
| ASSERT_TRUE(fd); |
| uint8_t buf[kTestData.size()]; |
| ASSERT_EQ(pread(fd.get(), buf, kTestData.size(), offset), |
| static_cast<ssize_t>(kTestData.size())); |
| ASSERT_EQ(memcmp(buf, kTestData.data(), kTestData.size()), 0); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/, RwTest, testing::ValuesIn(AllTestFilesystems()), |
| testing::PrintToStringParamName()); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /*no prefix*/, RwFullDiskTest, |
| testing::ValuesIn(MapAndFilterAllTestFilesystems( |
| [](TestFilesystemOptions options) -> std::optional<TestFilesystemOptions> { |
| if (options.filesystem->GetTraits().in_memory) { |
| return std::nullopt; |
| } |
| // Run on a smaller ram-disk to keep run-time reasonable. |
| options.device_block_count = 8192; |
| options.fvm_slice_size = 32768; |
| return options; |
| })), |
| testing::PrintToStringParamName()); |
| |
| // These tests will only work on a file system that supports sparse files. |
| INSTANTIATE_TEST_SUITE_P( |
| /*no prefix*/, RwSparseTest, |
| testing::ValuesIn(MapAndFilterAllTestFilesystems( |
| [](const TestFilesystemOptions& options) -> std::optional<TestFilesystemOptions> { |
| if (options.filesystem->GetTraits().supports_sparse_files) { |
| return options; |
| } else { |
| return std::nullopt; |
| } |
| })), |
| testing::PrintToStringParamName()); |
| |
| } // namespace |
| } // namespace fs_test |