| // Copyright 2016 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 <errno.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| #include <zircon/syscalls.h> |
| |
| #include <algorithm> |
| #include <iostream> |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/unique_fd.h> |
| |
| #include "fs_test_fixture.h" |
| |
| namespace fs_test { |
| namespace { |
| |
| using ParamType = std::tuple<TestFilesystemOptions, /*remount=*/bool>; |
| |
| constexpr int kMb = 1 << 20; |
| constexpr int kPrintSize = 100 * kMb; |
| |
| class MaxFileTest : public BaseFilesystemTest, public testing::WithParamInterface<ParamType> { |
| public: |
| MaxFileTest() : BaseFilesystemTest(std::get<0>(GetParam())) {} |
| |
| bool ShouldRemount() const { return std::get<1>(GetParam()); } |
| }; |
| |
| // Test writing as much as we can to a file until we run out of space. |
| TEST_P(MaxFileTest, ReadAfterWriteMaxFileSucceeds) { |
| // TODO(fxbug.dev/31604): We avoid making files that consume more than half |
| // of physical memory. When we can page out files, this restriction |
| // should be removed. |
| const size_t physmem = zx_system_get_physmem(); |
| const size_t max_cap = physmem / 2; |
| |
| fbl::unique_fd fd(open(GetPath("bigfile").c_str(), O_CREAT | O_RDWR, 0644)); |
| ASSERT_TRUE(fd); |
| char data_a[8192]; |
| char data_b[8192]; |
| char data_c[8192]; |
| memset(data_a, 0xaa, sizeof(data_a)); |
| memset(data_b, 0xbb, sizeof(data_b)); |
| memset(data_c, 0xcc, sizeof(data_c)); |
| size_t sz = 0; |
| ssize_t r; |
| |
| auto rotate = [&](const char* data) { |
| if (data == data_a) { |
| return data_b; |
| } else if (data == data_b) { |
| return data_c; |
| } else { |
| return data_a; |
| } |
| }; |
| |
| const char* data = data_a; |
| for (;;) { |
| if (sz >= max_cap) { |
| std::cout << "Approaching physical memory capacity: " << sz << " bytes" << std::endl; |
| r = 0; |
| break; |
| } |
| |
| const int offset = sz % sizeof(data_a); |
| const int len = sizeof(data_a) - offset; |
| if ((r = write(fd.get(), data + offset, len)) < 0) { |
| std::cout << "bigfile received error: " << strerror(errno) << std::endl; |
| if ((errno == EFBIG) || (errno == ENOSPC)) { |
| // Either the file should be too big (EFBIG) or the file should |
| // consume the whole volume (ENOSPC). |
| std::cout << "(This was an expected error)" << std::endl; |
| r = 0; |
| } |
| break; |
| } |
| if ((sz + r) % kPrintSize < (sz % kPrintSize)) { |
| std::cout << "wrote " << (sz + r) / kMb << " MB" << std::endl; |
| } |
| sz += r; |
| if (r == len) { |
| // Rotate which data buffer we use |
| data = rotate(data); |
| } else { |
| ASSERT_LT(r, len); |
| } |
| } |
| ASSERT_EQ(r, 0) << "Saw an unexpected error from write"; |
| std::cout << "wrote " << sz << " bytes" << std::endl; |
| |
| struct stat buf; |
| ASSERT_EQ(fstat(fd.get(), &buf), 0); |
| ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz)); |
| |
| // Try closing, re-opening, and verifying the file |
| ASSERT_EQ(close(fd.release()), 0); |
| if (ShouldRemount()) { |
| EXPECT_EQ(fs().Unmount().status_value(), ZX_OK); |
| EXPECT_EQ(fs().Mount().status_value(), ZX_OK); |
| } |
| fd.reset(open(GetPath("bigfile").c_str(), O_RDWR, 0644)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(fstat(fd.get(), &buf), 0); |
| ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz)); |
| char readbuf[8192]; |
| size_t bytes_read = 0; |
| data = data_a; |
| while (bytes_read < sz) { |
| r = read(fd.get(), readbuf, sizeof(readbuf)); |
| ASSERT_EQ(r, static_cast<ssize_t>(std::min(sz - bytes_read, sizeof(readbuf)))); |
| ASSERT_EQ(memcmp(readbuf, data, r), 0); |
| data = rotate(data); |
| bytes_read += r; |
| } |
| |
| ASSERT_EQ(bytes_read, sz); |
| |
| ASSERT_EQ(unlink(GetPath("bigfile").c_str()), 0); |
| ASSERT_EQ(close(fd.release()), 0); |
| } |
| |
| using MaxFileSizeTest = MaxFileTest; |
| |
| TEST_P(MaxFileSizeTest, WritingToMaxSupportedOffset) { |
| fbl::unique_fd fd(open(GetPath("foo").c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(pwrite(fd.get(), "h", 1, fs().GetTraits().max_file_size - 1), 1); |
| } |
| |
| TEST_P(MaxFileTest, WritingBeyondMaxSupportedOffset) { |
| // Fat does not support sparse files. Maybe test is not worth the time spent writing a huge file. |
| if (fs().GetTraits().is_fat) { |
| return; |
| } |
| fbl::unique_fd fd(open(GetPath("foo").c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(pwrite(fd.get(), "h", 1, fs().GetTraits().max_file_size), -1); |
| } |
| |
| TEST_P(MaxFileTest, TruncatingToMaxSupportedOffset) { |
| // Fat does not support sparse files. Maybe test is not worth the time spent writing a huge file. |
| if (fs().GetTraits().is_fat) { |
| return; |
| } |
| fbl::unique_fd fd(open(GetPath("foo").c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(ftruncate(fd.get(), fs().GetTraits().max_file_size), 0); |
| } |
| |
| TEST_P(MaxFileTest, TruncatingBeyondMaxSupportedOffset) { |
| // Fat does not support sparse files. Maybe test is not worth the time spent writing a huge file. |
| if (fs().GetTraits().is_fat) { |
| return; |
| } |
| fbl::unique_fd fd(open(GetPath("foo").c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd); |
| ASSERT_EQ(ftruncate(fd.get(), fs().GetTraits().max_file_size + 1), -1); |
| } |
| |
| // Test writing to two files, in alternation, until we run out of space. For trivial (sequential) |
| // block allocation policies, this will create two large files with non-contiguous block |
| // allocations. |
| TEST_P(MaxFileTest, ReadAfterNonContiguousWritesSuceeds) { |
| // TODO(fxbug.dev/31604): We avoid making files that consume more than half |
| // of physical memory. When we can page out files, this restriction |
| // should be removed. |
| const size_t physmem = zx_system_get_physmem(); |
| const size_t max_cap = physmem / 4; |
| |
| fbl::unique_fd fda(open(GetPath("bigfile-A").c_str(), O_CREAT | O_RDWR, 0644)); |
| fbl::unique_fd fdb(open(GetPath("bigfile-B").c_str(), O_CREAT | O_RDWR, 0644)); |
| ASSERT_TRUE(fda); |
| ASSERT_TRUE(fdb); |
| char data_a[8192]; |
| char data_b[8192]; |
| memset(data_a, 0xaa, sizeof(data_a)); |
| memset(data_b, 0xbb, sizeof(data_b)); |
| size_t sz_a = 0; |
| size_t sz_b = 0; |
| ssize_t r; |
| |
| size_t* sz = &sz_a; |
| int fd = fda.get(); |
| const char* data = data_a; |
| for (;;) { |
| if (*sz >= max_cap) { |
| std::cout << "Approaching physical memory capacity: " << *sz << " bytes" << std::endl; |
| r = 0; |
| break; |
| } |
| |
| const int offset = *sz % sizeof(data_a); |
| const int len = sizeof(data_a) - offset; |
| if ((r = write(fd, data + offset, len)) <= 0) { |
| std::cout << "bigfile received error: " << strerror(errno); |
| // Either the file should be too big (EFBIG) or the file should |
| // consume the whole volume (ENOSPC). |
| ASSERT_TRUE(errno == EFBIG || errno == ENOSPC); |
| std::cout << "(This was an expected error)"; |
| break; |
| } |
| if ((*sz + r) % kPrintSize < (*sz % kPrintSize)) { |
| std::cout << "wrote " << (*sz + r) / kMb << " MB"; |
| } |
| *sz += r; |
| if (r == len) { |
| fd = (fd == fda.get()) ? fdb.get() : fda.get(); |
| data = (data == data_a) ? data_b : data_a; |
| sz = (sz == &sz_a) ? &sz_b : &sz_a; |
| } else { |
| ASSERT_LT(r, len); |
| } |
| } |
| std::cout << "wrote " << sz_a << " bytes (to A)"; |
| std::cout << "wrote " << sz_b << " bytes (to B)"; |
| |
| struct stat buf; |
| ASSERT_EQ(fstat(fda.get(), &buf), 0); |
| ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz_a)); |
| ASSERT_EQ(fstat(fdb.get(), &buf), 0); |
| ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz_b)); |
| |
| // Try closing, re-opening, and verifying the file |
| ASSERT_EQ(close(fda.release()), 0); |
| ASSERT_EQ(close(fdb.release()), 0); |
| if (ShouldRemount()) { |
| EXPECT_EQ(fs().Unmount().status_value(), ZX_OK); |
| EXPECT_EQ(fs().Mount().status_value(), ZX_OK); |
| } |
| fda.reset(open(GetPath("bigfile-A").c_str(), O_RDWR, 0644)); |
| fdb.reset(open(GetPath("bigfile-B").c_str(), O_RDWR, 0644)); |
| ASSERT_TRUE(fda); |
| ASSERT_TRUE(fdb); |
| |
| char readbuf[8192]; |
| size_t bytes_read_a = 0; |
| size_t bytes_read_b = 0; |
| |
| fd = fda.get(); |
| data = data_a; |
| sz = &sz_a; |
| size_t* bytes_read = &bytes_read_a; |
| while (*bytes_read < *sz) { |
| r = read(fd, readbuf, sizeof(readbuf)); |
| ASSERT_EQ(r, static_cast<ssize_t>(std::min(*sz - *bytes_read, sizeof(readbuf)))); |
| ASSERT_EQ(memcmp(readbuf, data, r), 0); |
| *bytes_read += r; |
| |
| fd = (fd == fda.get()) ? fdb.get() : fda.get(); |
| data = (data == data_a) ? data_b : data_a; |
| sz = (sz == &sz_a) ? &sz_b : &sz_a; |
| bytes_read = (bytes_read == &bytes_read_a) ? &bytes_read_b : &bytes_read_a; |
| } |
| |
| ASSERT_EQ(bytes_read_a, sz_a); |
| ASSERT_EQ(bytes_read_b, sz_b); |
| |
| ASSERT_EQ(unlink(GetPath("bigfile-A").c_str()), 0); |
| ASSERT_EQ(unlink(GetPath("bigfile-B").c_str()), 0); |
| ASSERT_EQ(close(fda.release()), 0); |
| ASSERT_EQ(close(fdb.release()), 0); |
| } |
| |
| std::string GetParamDescription(const testing::TestParamInfo<ParamType>& param) { |
| std::stringstream s; |
| s << std::get<0>(param.param) << (std::get<1>(param.param) ? "WithRemount" : "WithoutRemount"); |
| return s.str(); |
| } |
| |
| std::vector<ParamType> GetTestCombinations() { |
| std::vector<ParamType> test_combinations; |
| for (TestFilesystemOptions options : AllTestFilesystems()) { |
| // Fatfs is slow and there's no real benefit from having a larger ram-disk. |
| if (options.filesystem->GetTraits().name != "fatfs") { |
| // Use a larger ram-disk than the default so that the maximum transaction limit is exceeded |
| // for during delayed data allocation on non-FVM-backed Minfs partitions. |
| options.device_block_size = 512; |
| options.device_block_count = 1'048'576; |
| options.fvm_slice_size = 8'388'608; |
| } |
| test_combinations.push_back(ParamType{options, false}); |
| if (options.filesystem->GetTraits().can_unmount) { |
| test_combinations.push_back(ParamType{options, true}); |
| } |
| } |
| return test_combinations; |
| } |
| |
| std::vector<ParamType> GetCombinationWithSparseFileSupport() { |
| std::vector<ParamType> test_combinations; |
| for (TestFilesystemOptions options : AllTestFilesystems()) { |
| // For those filesystems that don't support sparse files, it is not worth the time spent |
| // writing a huge file. |
| if (!options.filesystem->GetTraits().supports_sparse_files) { |
| continue; |
| } |
| // Use a larger ram-disk than the default so that the maximum transaction limit is exceeded |
| // for during delayed data allocation on non-FVM-backed Minfs partitions. |
| options.device_block_size = 512; |
| options.device_block_count = 1'048'576; |
| options.fvm_slice_size = 8'388'608; |
| test_combinations.emplace_back(options, false); |
| if (options.filesystem->GetTraits().can_unmount) { |
| test_combinations.emplace_back(options, true); |
| } |
| } |
| return test_combinations; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/, MaxFileTest, testing::ValuesIn(GetTestCombinations()), |
| GetParamDescription); |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/, MaxFileSizeTest, |
| testing::ValuesIn(GetCombinationWithSparseFileSupport()), |
| GetParamDescription); |
| |
| } // namespace |
| } // namespace fs_test |