From f7d4fe00bac55862df2d4ab22d429e158e74191e Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Sat, 20 Sep 2025 08:43:35 -0500 Subject: [PATCH] [unit test] Complete FUSE unit tests #22 --- .cspell/words.txt | 1 + .../include/fixtures/drive_fixture.hpp | 50 ++++ .../src/fuse_drive_rename_test.cpp | 86 ++----- .../src/fuse_drive_test_legacy.cpp | 25 -- .../src/fuse_drive_truncate_test.cpp | 214 ++++++++++++++++++ 5 files changed, 290 insertions(+), 86 deletions(-) create mode 100644 repertory/repertory_test/src/fuse_drive_truncate_test.cpp diff --git a/.cspell/words.txt b/.cspell/words.txt index 691b7d2b..68b1a0e2 100644 --- a/.cspell/words.txt +++ b/.cspell/words.txt @@ -5,6 +5,7 @@ _sh_denyrd _sh_denyrw _spawnv aarch64 +abcdefgh advapi32 armv8 autogen diff --git a/repertory/repertory_test/include/fixtures/drive_fixture.hpp b/repertory/repertory_test/include/fixtures/drive_fixture.hpp index 4b48063b..5b1e08f7 100644 --- a/repertory/repertory_test/include/fixtures/drive_fixture.hpp +++ b/repertory/repertory_test/include/fixtures/drive_fixture.hpp @@ -496,6 +496,56 @@ public: unlink_file_and_test(file_path); } + + static void overwrite_text(const std::string &path, const std::string &data) { + int desc = ::open(path.c_str(), O_WRONLY | O_TRUNC); + ASSERT_NE(desc, -1); + write_all(desc, data); + ::close(desc); + } + + static void write_all(int desc, const std::string &data) { + std::size_t off{0U}; + while (off < data.size()) { + auto written = ::write(desc, &data.at(off), data.length() - off); + ASSERT_NE(written, -1); + off += static_cast(written); + } + } + + static auto slurp(const std::string &path) -> std::string { + int desc = ::open(path.c_str(), O_RDONLY); + if (desc == -1) { + return {}; + } + std::string out; + std::array buf{}; + for (;;) { + auto bytes_read = ::read(desc, buf.data(), buf.size()); + if (bytes_read == 0) { + break; + } + if (bytes_read == -1) { + if (errno == EINTR) { + continue; + } + break; + } + + out.append(buf.begin(), std::next(buf.begin(), bytes_read)); + } + + ::close(desc); + return out; + } + + [[nodiscard]] static auto stat_size(const std::string &path) -> off_t { + struct stat st_unix{}; + int res = ::stat(path.c_str(), &st_unix); + EXPECT_EQ(0, res) << "stat(" << path + << ") failed: " << std::strerror(errno); + return st_unix.st_size; + } #endif // !defined(_WIN32) }; diff --git a/repertory/repertory_test/src/fuse_drive_rename_test.cpp b/repertory/repertory_test/src/fuse_drive_rename_test.cpp index 14d4d80d..9be3db5d 100644 --- a/repertory/repertory_test/src/fuse_drive_rename_test.cpp +++ b/repertory/repertory_test/src/fuse_drive_rename_test.cpp @@ -23,60 +23,13 @@ #include "fixtures/drive_fixture.hpp" -namespace { -void overwrite_text(const std::string &path, const std::string &data); -void write_all(int desc, const std::string &data); -[[nodiscard]] auto slurp(const std::string &path) -> std::string; - -void overwrite_text(const std::string &path, const std::string &data) { - int desc = ::open(path.c_str(), O_WRONLY | O_TRUNC); - ASSERT_NE(desc, -1); - write_all(desc, data); - ::close(desc); -} - -void write_all(int desc, const std::string &data) { - std::size_t off{0U}; - while (off < data.size()) { - auto written = ::write(desc, &data.at(off), data.length() - off); - ASSERT_NE(written, -1); - off += static_cast(written); - } -} - -auto slurp(const std::string &path) -> std::string { - int desc = ::open(path.c_str(), O_RDONLY); - if (desc == -1) { - return {}; - } - std::string out; - std::array buf{}; - for (;;) { - auto bytes_read = ::read(desc, buf.data(), buf.size()); - if (bytes_read == 0) { - break; - } - if (bytes_read == -1) { - if (errno == EINTR) { - continue; - } - break; - } - - out.append(buf.begin(), std::next(buf.begin(), bytes_read)); - } - - ::close(desc); - return out; -} -} // namespace - namespace repertory { TYPED_TEST_SUITE(fuse_test, platform_provider_types); TYPED_TEST(fuse_test, rename_can_rename_a_file) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -103,6 +56,7 @@ TYPED_TEST(fuse_test, rename_can_rename_a_file) { TYPED_TEST(fuse_test, rename_can_rename_a_directory) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -129,6 +83,7 @@ TYPED_TEST(fuse_test, rename_can_rename_a_directory) { TYPED_TEST(fuse_test, rename_can_overwrite_existing_file) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -138,8 +93,8 @@ TYPED_TEST(fuse_test, rename_can_overwrite_existing_file) { std::string dst_name{"rename2.txt"}; auto dst = this->create_file_and_test(dst_name); - overwrite_text(src, "SRC"); - overwrite_text(dst, "DST"); + this->overwrite_text(src, "SRC"); + this->overwrite_text(dst, "DST"); errno = 0; ASSERT_EQ(0, ::rename(src.c_str(), dst.c_str())); @@ -148,7 +103,7 @@ TYPED_TEST(fuse_test, rename_can_overwrite_existing_file) { EXPECT_EQ(-1, ::access(src.c_str(), F_OK)); EXPECT_EQ(ENOENT, errno); - EXPECT_EQ("SRC", slurp(dst)); + EXPECT_EQ("SRC", this->slurp(dst)); this->unlink_file_and_test(dst); } @@ -156,6 +111,7 @@ TYPED_TEST(fuse_test, rename_can_overwrite_existing_file) { TYPED_TEST(fuse_test, rename_can_rename_file_into_different_directory) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -169,7 +125,7 @@ TYPED_TEST(fuse_test, rename_can_rename_file_into_different_directory) { auto src = this->create_file_and_test(file_name); std::string dst = utils::path::combine(dir2, {"moved.txt"}); - overwrite_text(src, "CMDC"); + this->overwrite_text(src, "CMDC"); errno = 0; ASSERT_EQ(0, ::rename(src.c_str(), dst.c_str())); @@ -177,7 +133,7 @@ TYPED_TEST(fuse_test, rename_can_rename_file_into_different_directory) { errno = 0; EXPECT_EQ(-1, ::access(src.c_str(), F_OK)); EXPECT_EQ(ENOENT, errno); - EXPECT_EQ("CMDC", slurp(dst)); + EXPECT_EQ("CMDC", this->slurp(dst)); this->unlink_file_and_test(dst); this->rmdir_and_test(dir1); @@ -187,16 +143,17 @@ TYPED_TEST(fuse_test, rename_can_rename_file_into_different_directory) { TYPED_TEST(fuse_test, rename_can_rename_file_to_same_path) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } std::string file_name{"rename"}; auto src = this->create_file_and_test(file_name); - overwrite_text(src, "CMDC"); + this->overwrite_text(src, "CMDC"); errno = 0; EXPECT_EQ(0, ::rename(src.c_str(), src.c_str())); - EXPECT_EQ("CMDC", slurp(src)); + EXPECT_EQ("CMDC", this->slurp(src)); this->unlink_file_and_test(src); } @@ -204,6 +161,7 @@ TYPED_TEST(fuse_test, rename_can_rename_file_to_same_path) { TYPED_TEST(fuse_test, rename_file_fails_if_source_file_does_not_exist) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -225,6 +183,7 @@ TYPED_TEST(fuse_test, rename_file_fails_if_destination_directory_does_not_exist) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -246,6 +205,7 @@ TYPED_TEST(fuse_test, TYPED_TEST(fuse_test, rename_file_fails_if_destination_is_directory) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -268,6 +228,7 @@ TYPED_TEST(fuse_test, rename_file_fails_if_destination_is_directory) { TYPED_TEST(fuse_test, rename_file_fails_if_source_directory_is_read_only) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -299,6 +260,7 @@ TYPED_TEST(fuse_test, rename_file_fails_if_source_directory_is_read_only) { TYPED_TEST(fuse_test, rename_file_fails_if_destination_directory_is_read_only) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -330,6 +292,7 @@ TYPED_TEST(fuse_test, rename_file_fails_if_destination_directory_is_read_only) { TYPED_TEST(fuse_test, rename_file_succeeds_if_destination_file_is_read_only) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -339,8 +302,8 @@ TYPED_TEST(fuse_test, rename_file_succeeds_if_destination_file_is_read_only) { std::string dest_file_name{"rename_test_2"}; auto dst = this->create_file_and_test(dest_file_name); - overwrite_text(src, "NEW"); - overwrite_text(dst, "OLD"); + this->overwrite_text(src, "NEW"); + this->overwrite_text(dst, "OLD"); ASSERT_EQ(0, ::chmod(dst.c_str(), 0444)); @@ -354,7 +317,7 @@ TYPED_TEST(fuse_test, rename_file_succeeds_if_destination_file_is_read_only) { } ASSERT_EQ(0, res); - EXPECT_EQ("NEW", slurp(dst)); + EXPECT_EQ("NEW", this->slurp(dst)); ASSERT_EQ(0, ::chmod(dst.c_str(), 0644)); this->unlink_file_and_test(dst); @@ -363,6 +326,7 @@ TYPED_TEST(fuse_test, rename_file_succeeds_if_destination_file_is_read_only) { TYPED_TEST(fuse_test, rename_file_retains_open_file_descriptor) { if (this->current_provider != provider_type::sia) { // TODO finish test + GTEST_SKIP(); return; } @@ -372,7 +336,7 @@ TYPED_TEST(fuse_test, rename_file_retains_open_file_descriptor) { std::string dest_file_name{"rename_test_2"}; auto dst = this->create_file_and_test(dest_file_name); - overwrite_text(src, "HELLO"); + this->overwrite_text(src, "HELLO"); int desc = ::open(src.c_str(), O_RDWR); ASSERT_NE(desc, -1); @@ -385,10 +349,10 @@ TYPED_TEST(fuse_test, rename_file_retains_open_file_descriptor) { EXPECT_EQ(ENOENT, errno); ASSERT_NE(-1, ::lseek(desc, 0, SEEK_END)); - write_all(desc, " WORLD"); + this->write_all(desc, " WORLD"); ::close(desc); - EXPECT_EQ("HELLO WORLD", slurp(dst)); + EXPECT_EQ("HELLO WORLD", this->slurp(dst)); this->unlink_file_and_test(dst); } diff --git a/repertory/repertory_test/src/fuse_drive_test_legacy.cpp b/repertory/repertory_test/src/fuse_drive_test_legacy.cpp index f4041442..4e9c46f9 100644 --- a/repertory/repertory_test/src/fuse_drive_test_legacy.cpp +++ b/repertory/repertory_test/src/fuse_drive_test_legacy.cpp @@ -1,28 +1,3 @@ -// static void test_truncate(const std::string &file_path) { -// std::cout << __FUNCTION__ << std::endl; -// EXPECT_EQ(0, truncate(file_path.c_str(), 10u)); -// -// std::uint64_t file_size{}; -// EXPECT_TRUE(utils::file::get_file_size(file_path, file_size)); -// -// EXPECT_EQ(std::uint64_t(10u), file_size); -// } -// -// static void test_ftruncate(const std::string &file_path) { -// std::cout << __FUNCTION__ << std::endl; -// auto fd = open(file_path.c_str(), O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP); -// EXPECT_LE(1, fd); -// -// EXPECT_EQ(0, ftruncate(fd, 10u)); -// -// std::uint64_t file_size{}; -// EXPECT_TRUE(utils::file::get_file_size(file_path, file_size)); -// -// EXPECT_EQ(std::uint64_t(10u), file_size); -// -// close(fd); -// } -// // #if !defined(__APPLE__) // static void test_fallocate(const std::string & /* api_path */, // const std::string &file_path) { diff --git a/repertory/repertory_test/src/fuse_drive_truncate_test.cpp b/repertory/repertory_test/src/fuse_drive_truncate_test.cpp new file mode 100644 index 00000000..126fbb2d --- /dev/null +++ b/repertory/repertory_test/src/fuse_drive_truncate_test.cpp @@ -0,0 +1,214 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#if !defined(_WIN32) + +#include "fixtures/drive_fixture.hpp" + +namespace repertory { +TYPED_TEST_SUITE(fuse_test, platform_provider_types); + +TYPED_TEST(fuse_test, truncate_can_shrink_file) { + std::string file_name{"truncate"}; + auto src = this->create_file_and_test(file_name); + this->overwrite_text(src, "ABCDEFGH"); + + errno = 0; + ASSERT_EQ(0, ::truncate(src.c_str(), 3)); + EXPECT_EQ(3, this->stat_size(src)); + EXPECT_EQ("ABC", this->slurp(src)); + + this->unlink_file_and_test(src); +} + +TYPED_TEST(fuse_test, truncate_expand_file_is_zero_filled) { + std::string name{"truncate"}; + auto src = this->create_file_and_test(name); + this->overwrite_text(src, "XYZ"); + + errno = 0; + ASSERT_EQ(0, ::truncate(src.c_str(), 10)); + EXPECT_EQ(10, this->stat_size(src)); + + auto data = this->slurp(src); + ASSERT_EQ(10U, data.size()); + EXPECT_EQ('X', data.at(0U)); + EXPECT_EQ('Y', data.at(1U)); + EXPECT_EQ('Z', data.at(2U)); + for (std::size_t idx = 3; idx < data.size(); ++idx) { + EXPECT_EQ('\0', data[idx]); + } + + this->unlink_file_and_test(src); +} + +TYPED_TEST(fuse_test, truncate_fails_if_source_does_not_exist) { + std::string name{"truncate"}; + auto src = this->create_file_path(name); + + errno = 0; + EXPECT_EQ(-1, ::truncate(src.c_str(), 1)); + EXPECT_EQ(ENOENT, errno); + + EXPECT_FALSE(utils::file::file{src}.exists()); +} + +TYPED_TEST(fuse_test, truncate_fails_if_path_is_directory) { + std::string name{"truncate"}; + auto src = this->create_directory_and_test(name); + + errno = 0; + auto res = ::truncate(src.c_str(), 0); + EXPECT_EQ(-1, res); + EXPECT_TRUE(errno == EISDIR || errno == EPERM || errno == EACCES || + errno == EINVAL); + + this->rmdir_and_test(src); +} + +/* +TYPED_TEST(fuse_test, truncate_readonly_file_eacces_or_eperm_or_erofs_skip) { + if (this->current_provider != provider_type::sia) + return; + + std::string name{"trunc_ro"}; + std::string p = this->create_file_and_test(name); + this->overwrite_text(p, "DATA"); + + ASSERT_EQ(0, ::chmod(p.c_str(), 0444)) << std::strerror(errno); + + errno = 0; + int res = ::truncate(p.c_str(), 2); + if (res == -1 && errno == EROFS) { + ASSERT_EQ(0, ::chmod(p.c_str(), 0644)); + this->unlink_file_and_test(p); + GTEST_SKIP() << "read-only mount; truncate returns EROFS"; + } + EXPECT_EQ(-1, res); + EXPECT_TRUE(errno == EACCES || errno == EPERM) + << "errno=" << errno << " " << std::strerror(errno); + + ASSERT_EQ(0, ::chmod(p.c_str(), 0644)); + this->unlink_file_and_test(p); +} + +// ========== ftruncate(2) ========== + +TYPED_TEST(fuse_test, ftruncate_shrink_open_fd) { + if (this->current_provider != provider_type::sia) + return; + + std::string name{"ftrunc_shrink"}; + std::string p = this->create_file_and_test(name); + this->overwrite_text(p, "HELLOWORLD"); // 10 bytes + + int fd = ::open(p.c_str(), O_RDWR); + ASSERT_NE(fd, -1) << "open failed: " << std::strerror(errno); + + errno = 0; + ASSERT_EQ(0, ::ftruncate(fd, 4)) << std::strerror(errno); + ::close(fd); + + EXPECT_EQ(4, this->stat_size(p)); + EXPECT_EQ("HELL", this->slurp(p)); + + this->unlink_file_and_test(p); +} + +TYPED_TEST(fuse_test, ftruncate_expand_open_fd_zero_fills) { + if (this->current_provider != provider_type::sia) + return; + + std::string name{"ftrunc_expand"}; + std::string p = this->create_file_and_test(name); + this->overwrite_text(p, "AA"); // 2 bytes + + int fd = ::open(p.c_str(), O_RDWR); + ASSERT_NE(fd, -1); + + errno = 0; + ASSERT_EQ(0, ::ftruncate(fd, 6)) << std::strerror(errno); + ::close(fd); + + EXPECT_EQ(6, this->stat_size(p)); + + auto s = this->slurp(p); + ASSERT_EQ(6u, s.size()); + EXPECT_EQ('A', s[0]); + EXPECT_EQ('A', s[1]); + for (size_t i = 2; i < s.size(); ++i) { + EXPECT_EQ('\0', s[i]) << "byte " << i << " not zero"; + } + + this->unlink_file_and_test(p); +} + +TYPED_TEST(fuse_test, ftruncate_readonly_fd_ebadf) { + if (this->current_provider != provider_type::sia) + return; + + std::string name{"ftrunc_rofd"}; + std::string p = this->create_file_and_test(name); + this->overwrite_text(p, "RW"); + + int fd = ::open(p.c_str(), O_RDONLY); + ASSERT_NE(fd, -1); + + errno = 0; + EXPECT_EQ(-1, ::ftruncate(fd, 1)); + EXPECT_EQ(EBADF, errno); + + ::close(fd); + this->unlink_file_and_test(p); +} + +TYPED_TEST(fuse_test, ftruncate_on_ro_mount_erofs_or_skip) { + if (this->current_provider != provider_type::sia) + return; + + std::string name{"ftrunc_ro_mount"}; + std::string p = this->create_file_and_test(name); + this->overwrite_text(p, "X"); + + int fd = ::open(p.c_str(), O_RDWR); + if (fd == -1) { + this->unlink_file_and_test(p); + GTEST_SKIP() << "cannot open O_RDWR; probable RO mount"; + } + + errno = 0; + int res = ::ftruncate(fd, 0); + if (res == -1 && errno == EROFS) { + ::close(fd); + this->unlink_file_and_test(p); + GTEST_SKIP() << "read-only mount; ftruncate returns EROFS"; + } + + ASSERT_EQ(0, res) << std::strerror(errno); + ::close(fd); + + this->unlink_file_and_test(p); +} +*/ + +} // namespace repertory + +#endif // !defined(_WIN32)