diff --git a/src/test/neorados/CMakeLists.txt b/src/test/neorados/CMakeLists.txt index c625133d50a58..bfb201506aae4 100644 --- a/src/test/neorados/CMakeLists.txt +++ b/src/test/neorados/CMakeLists.txt @@ -41,3 +41,51 @@ target_link_libraries(ceph_test_neorados_handler_error install(TARGETS ceph_test_neorados_handler_error DESTINATION ${CMAKE_INSTALL_BINDIR}) + +add_executable(ceph_test_neorados_cls + cls.cc + ) +target_link_libraries(ceph_test_neorados_cls + libneorados + ${BLKID_LIBRARIES} + ${CMAKE_DL_LIBS} + ${CRYPTO_LIBS} + ${EXTRALIBS} + neoradostest-support + ${UNITTEST_LIBS} + ) +install(TARGETS + ceph_test_neorados_cls + DESTINATION ${CMAKE_INSTALL_BINDIR}) + +add_executable(ceph_test_neorados_cmd + cmd.cc + ) +target_link_libraries(ceph_test_neorados_cmd + libneorados + ${BLKID_LIBRARIES} + ${CMAKE_DL_LIBS} + ${CRYPTO_LIBS} + ${EXTRALIBS} + neoradostest-support + ${UNITTEST_LIBS} + ) +install(TARGETS + ceph_test_neorados_cmd + DESTINATION ${CMAKE_INSTALL_BINDIR}) + +add_executable(ceph_test_neorados_read_operations + read_operations.cc + ) +target_link_libraries(ceph_test_neorados_read_operations + libneorados + ${BLKID_LIBRARIES} + ${CMAKE_DL_LIBS} + ${CRYPTO_LIBS} + ${EXTRALIBS} + neoradostest-support + ${UNITTEST_LIBS} + ) +install(TARGETS + ceph_test_neorados_read_operations + DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/test/neorados/cls.cc b/src/test/neorados/cls.cc new file mode 100644 index 0000000000000..c3578da78064d --- /dev/null +++ b/src/test/neorados/cls.cc @@ -0,0 +1,97 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2023 IBM + * + * See file COPYING for license information. + * + */ + +#include +#include +#include +#include +#include + +#include + +#include + +#include "include/neorados/RADOS.hpp" + +#include "common/ceph_json.h" + +#include "test/neorados/common_tests.h" + +#include "gtest/gtest.h" + +namespace asio = boost::asio; +namespace sys = boost::system; + +using namespace std::literals; + +using neorados::ReadOp; +using neorados::WriteOp; + +CORO_TEST_F(NeoRadosCls, DNE, NeoRadosTest) +{ + std::string_view oid = "obj"; + co_await execute(oid, WriteOp{}.create(true)); + // Call a bogus class + co_await expect_error_code( + execute(oid, ReadOp{}.exec("doesnotexistasdfasdf", "method", {})), + sys::errc::operation_not_supported); + + // Call a bogus method on an existent class + co_await expect_error_code( + execute(oid, ReadOp{}.exec("lock", "doesnotexistasdfasdfasdf", {})), + sys::errc::operation_not_supported); + co_return; +} + +CORO_TEST_F(NeoRadosCls, RemoteReads, NeoRadosTest) +{ + static constexpr std::size_t object_size = 4096; + static constexpr std::array oids{"src_object.1"sv, "src_object.2"sv, + "src_object.3"sv}; + + std::array buf; + buf.fill(1); + + for (const auto& oid : oids) { + buffer::list in; + in.append(buf.data(), buf.size()); + co_await execute(oid, WriteOp{}.write_full(std::move(in))); + } + + // Construct JSON request passed to "test_gather" method, and in + // turn, to "test_read" method + buffer::list in; + { + auto formatter = std::make_unique(true); + formatter->open_object_section("foo"); + encode_json("src_objects", oids, formatter.get()); + encode_json("cls", "test_remote_reads", formatter.get()); + encode_json("method", "test_read", formatter.get()); + encode_json("pool", pool_name(), formatter.get()); + formatter->close_section(); + formatter->flush(in); + } + + static const auto target = "tgt_object"s; + + // Create target object by combining data gathered from source + // objects using "test_read" method + co_await execute(target, + WriteOp{}.exec("test_remote_reads", "test_gather", in)); + + + // Read target object and check its size. + buffer::list out; + co_await execute(target, ReadOp{}.read(0, 0, &out)); + EXPECT_EQ(3 * object_size, out.length()); + + co_return; +} diff --git a/src/test/neorados/cmd.cc b/src/test/neorados/cmd.cc new file mode 100644 index 0000000000000..f27df529eacbb --- /dev/null +++ b/src/test/neorados/cmd.cc @@ -0,0 +1,116 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2023 IBM + * + * See file COPYING for license information. + * + */ + +#include +#include + +#include + +#include +#include + +#include + +#include "include/neorados/RADOS.hpp" + +#include "include/stringify.h" + +#include "test/neorados/common_tests.h" + +#include "gtest/gtest.h" + +namespace asio = boost::asio; +namespace sys = boost::system; +namespace buffer = ceph::buffer; + +using neorados::ReadOp; + +using namespace std::literals; + +CORO_TEST_F(NeoRadosCmd, MonDescribe, NeoRadosTest) { + std::string outs; + buffer::list outbl; + std::vector arg({R"({"prefix": "get_command_descriptions"})"s}); + co_await rados().mon_command(std::move(arg), {}, &outs, &outbl, + asio::use_awaitable); + EXPECT_LT(0u, outbl.length()); + EXPECT_LE(0u, outs.length()); + co_return; +} + +CORO_TEST_F(NeoRadosCmd, OSDCmd, NeoRadosTest) { + { + std::vector arg({R"(asdfasdf)"s}); + co_await expect_error_code( + rados().osd_command(0, std::move(arg), + {}, asio::use_awaitable), + sys::errc::invalid_argument, sys::errc::no_such_device_or_address); + } + + { + std::vector arg({R"(version)"s}); + co_await expect_error_code( + rados().osd_command(0, std::move(arg), + {}, asio::use_awaitable), + sys::errc::invalid_argument, sys::errc::no_such_device_or_address); + } + + { + std::vector arg({R"({"prefix":"version"})"s}); + auto [ec, outs, outbl] = co_await + rados().osd_command(0, std::move(arg), {}, + asio::as_tuple(asio::use_awaitable)); + + EXPECT_TRUE((!ec && outbl.length() > 0) || + (ec == sys::errc::no_such_device_or_address && + outbl.length() == 0)); + + } + co_return; +} + +CORO_TEST_F(NeoRadosCmd, PGCmd, NeoRadosTest) { + const neorados::PG pgid{uint64_t(pool().get_pool()), 0}; + + { + std::vector arg({R"(asdfasdf)"s}); + // note: tolerate NXIO here in case the cluster is thrashing out underneath us. + co_await expect_error_code( + rados().pg_command(pgid, std::move(arg), + {}, asio::use_awaitable), + sys::errc::invalid_argument, sys::errc::no_such_device_or_address); + } + + // make sure the pg exists on the osd before we query it + for (auto i = 0; i < 100; ++i) { + co_await expect_error_code( + rados().execute(fmt::format("obj{}", i), pool(), + ReadOp{}.assert_exists(), nullptr, + asio::use_awaitable), + sys::errc::no_such_file_or_directory); + } + + { + std::vector arg({fmt::format(R"({{"prefix":"pg", "cmd":"query", "pgid":"{}.{}"}})", + pgid.pool, pgid.seed)}); + // Working around a bug in GCC. + auto coro = rados().pg_command(pgid, std::move(arg), + {}, asio::as_tuple(asio::use_awaitable)); + auto [ec, outs, outbl] = co_await std::move(coro); + + EXPECT_TRUE(!ec || ec == sys::errc::no_such_file_or_directory || + ec == sys::errc::no_such_device_or_address); + + EXPECT_LT(0u, outbl.length()); + } + + co_return; +} diff --git a/src/test/neorados/read_operations.cc b/src/test/neorados/read_operations.cc new file mode 100644 index 0000000000000..adf5f34ae5e59 --- /dev/null +++ b/src/test/neorados/read_operations.cc @@ -0,0 +1,748 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2023 IBM + * + * See file COPYING for license information. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include + +#include "include/neorados/RADOS.hpp" + +#include "osd/error_code.h" + +#include "test/neorados/common_tests.h" + +#include "gtest/gtest.h" + +namespace asio = boost::asio; +namespace ctnr = boost::container; +namespace hash_alg = neorados::hash_alg; +namespace sys = boost::system; + +using namespace std::literals; + +using neorados::cmp_op; +using neorados::ReadOp; +using neorados::WriteOp; + +class ReadOpTest : public NeoRadosTest { +protected: + static constexpr auto oid = "testobj"sv; + static constexpr auto data = "testdata"sv; + static constexpr std::size_t datalen = 16; + + auto write_object(std::string_view data, uint64_t* objver = nullptr) { + return execute(oid, WriteOp{}.write_full(to_buffer_list(data)), objver); + } + + auto remove_object() { + return execute(oid, WriteOp{}.remove()); + } + + asio::awaitable CoSetUp() override { + co_await NeoRadosTest::CoSetUp(); + co_await write_object(data); + } + + asio::awaitable CoTearDown() override { + co_await remove_object(); + co_await NeoRadosTest::CoTearDown(); + } + + auto assert_version(uint64_t v) { + return execute(oid, ReadOp{}.assert_version(v)); + } + + auto setxattr(std::string_view xattr, buffer::list bl) { + return execute(oid, WriteOp{}.setxattr(xattr, std::move(bl))); + } + + auto cmpxattr(std::string_view xattr, cmp_op op, buffer::list bl) { + return execute(oid, ReadOp{}.cmpxattr(xattr, op, std::move(bl))); + } +}; + +CORO_TEST_F(NeoRadosReadOps, SetOpFlags, ReadOpTest) { + sys::error_code ec; + co_await execute(oid, ReadOp{} + .exec("rbd"sv, "get_id"sv, {}, nullptr, &ec) + .set_failok()); + EXPECT_EQ(sys::errc::io_error, ec); + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, AssertExists, ReadOpTest) { + co_await expect_error_code(execute("nonexistent"sv, ReadOp{}.assert_exists()), + sys::errc::no_such_file_or_directory); + co_await execute(oid, ReadOp{}.assert_exists()); + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, AssertVersion, ReadOpTest) { + std::uint64_t v = 0; + // Write to the object a second time to guarantee that its + // version number is greater than 0 + co_await write_object(data, &v); + + co_await expect_error_code(assert_version(v + 1), + sys::errc::value_too_large); + co_await assert_version(v); + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, CmpXattr, ReadOpTest) { + using enum cmp_op; + using sys::errc::operation_canceled; + + static constexpr auto xattr = "test"sv; + + co_await setxattr(xattr, filled_buffer_list(0xcc, datalen)); + + // Equal value + co_await cmpxattr(xattr, eq, filled_buffer_list(0xcc, datalen)); + co_await expect_error_code(cmpxattr(xattr, ne, + filled_buffer_list(0xcc, datalen)), + operation_canceled); + co_await expect_error_code(cmpxattr(xattr, gt, + filled_buffer_list(0xcc, datalen)), + operation_canceled); + co_await cmpxattr(xattr, gte, filled_buffer_list(0xcc, datalen)); + co_await expect_error_code(cmpxattr(xattr, lt, + filled_buffer_list(0xcc, datalen)), + operation_canceled); + co_await cmpxattr(xattr, lte, filled_buffer_list(0xcc, datalen)); + + // < value + co_await expect_error_code(cmpxattr(xattr, eq, + filled_buffer_list(0xcb, datalen)), + operation_canceled); + co_await cmpxattr(xattr, ne, filled_buffer_list(0xcb, datalen)); + co_await expect_error_code(cmpxattr(xattr, gt, + filled_buffer_list(0xcb, datalen)), + operation_canceled); + co_await expect_error_code(cmpxattr(xattr, gte, + filled_buffer_list(0xcb, datalen)), + operation_canceled); + co_await cmpxattr(xattr, lt, filled_buffer_list(0xcb, datalen)); + co_await cmpxattr(xattr, lte, filled_buffer_list(0xcb, datalen)); + + // > value + co_await expect_error_code(cmpxattr(xattr, eq, + filled_buffer_list(0xcd, datalen)), + operation_canceled); + co_await cmpxattr(xattr, ne, filled_buffer_list(0xcd, datalen)); + co_await cmpxattr(xattr, gt, filled_buffer_list(0xcd, datalen)); + co_await cmpxattr(xattr, gte, filled_buffer_list(0xcd, datalen)); + co_await expect_error_code(cmpxattr(xattr, lt, + filled_buffer_list(0xcd, datalen)), + operation_canceled); + co_await expect_error_code(cmpxattr(xattr, lte, + filled_buffer_list(0xcd, datalen)), + operation_canceled); + + // check that null bytes are compared correctly + + co_await setxattr(xattr, to_buffer_list("\x00\x00"sv)); + co_await expect_error_code(cmpxattr(xattr, eq, + to_buffer_list("\x00\xcc"sv)), + operation_canceled); + co_await cmpxattr(xattr, ne, to_buffer_list("\x00\xcc"sv)); + co_await cmpxattr(xattr, gt, to_buffer_list("\x00\xcc"sv)); + co_await cmpxattr(xattr, gte, to_buffer_list("\x00\xcc"sv)); + co_await expect_error_code(cmpxattr(xattr, lt, + to_buffer_list("\x00\xcc"sv)), + operation_canceled); + co_await expect_error_code(cmpxattr(xattr, lte, + to_buffer_list("\x00\xcc"sv)), + operation_canceled); + + co_await cmpxattr(xattr, eq, to_buffer_list("\x00\x00"sv)); + co_await expect_error_code(cmpxattr(xattr, ne, + to_buffer_list("\x00\x00"sv)), + operation_canceled); + co_await expect_error_code(cmpxattr(xattr, gt, + to_buffer_list("\x00\x00"sv)), + operation_canceled); + co_await cmpxattr(xattr, gte, to_buffer_list("\x00\x00"sv)); + co_await expect_error_code(cmpxattr(xattr, lt, + to_buffer_list("\x00\x00"sv)), + operation_canceled); + co_await cmpxattr(xattr, lte, to_buffer_list("\x00\x00"sv)); + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, Read, ReadOpTest) { + // Check that using read_ops returns the same data with + // or without ec out params + { + buffer::list bl; + co_await execute(oid, ReadOp{}.read(0, 0, &bl)); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + } + { + buffer::list bl; + sys::error_code ec; + co_await execute(oid, ReadOp{}.read(0, 0, &bl, &ec)); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + EXPECT_FALSE(ec); + } + + { + buffer::list bl; + sys::error_code ec; + co_await execute(oid, ReadOp{} + .read(0, 0, &bl, &ec) + .set_fadvise_dontneed()); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + EXPECT_FALSE(ec); + } + co_return; +} + +inline std::uint32_t crc32c(uint32_t seed, std::string_view v) { + return ceph_crc32c( + seed, reinterpret_cast(v.data()), + uint32_t(v.size())); +} + +CORO_TEST_F(NeoRadosReadOps, Checksum, ReadOpTest) { + { + static constexpr uint64_t seed = -1; + std::vector hash; + + co_await execute(oid, ReadOp{} + .checksum(hash_alg::xxhash64, seed, 0, 0, 0, &hash)); + EXPECT_EQ(1u, hash.size()); + EXPECT_EQ(XXH64(data.data(), data.size(), seed), hash[0]); + } + { + static constexpr uint32_t seed = -1; + std::vector crc; + co_await execute(oid, ReadOp{} + .checksum(hash_alg::crc32c, seed, 0, 0, 0, &crc)); + EXPECT_EQ(crc32c(seed, data), crc[0]); + } + { + static constexpr uint32_t seed = -1; + std::vector hash; + co_await execute(oid, ReadOp{} + .checksum(hash_alg::xxhash32, seed, 0, 0, 0, &hash)); + EXPECT_EQ(XXH32(data.data(), data.size(), seed), hash[0]); + } + + { + static constexpr uint32_t seed = -1; + std::vector crc; + co_await execute(oid, ReadOp{}.checksum(hash_alg::crc32c, seed, 0, + data.length(), 4, &crc)); + EXPECT_EQ(crc32c(seed, data.substr(0, 4)), crc[0]); + EXPECT_EQ(crc32c(seed, data.substr(4, 4)), crc[1]); + } + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, RWOrderedRead, ReadOpTest) { + buffer::list bl; + sys::error_code ec; + ReadOp op; + op.read(0, 0, &bl, &ec); + op.set_fadvise_dontneed(); + op.order_reads_writes(); + co_await execute(oid, std::move(op)); + + EXPECT_FALSE(ec); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, ShortRead, ReadOpTest) { + constexpr auto read_len = data.size() * 2; + buffer::list bl; + // check that using read_ops returns the same data with + // or without ec out params + co_await execute(oid, ReadOp{}.read(0, read_len, &bl)); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + + sys::error_code ec; + bl.clear(); + co_await execute(oid, ReadOp{}.read(0, read_len, &bl, &ec)); + EXPECT_FALSE(ec); + EXPECT_TRUE((data.length() == bl.length()) && + (0 == std::memcmp(data.data(), bl.c_str(), data.length()))); + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, Exec, ReadOpTest) { + buffer::list bl; + sys::error_code ec; + co_await execute(oid, + ReadOp{}.exec("rbd"sv, "get_all_features"sv, {}, &bl, &ec)); + EXPECT_FALSE(ec); + std::uint64_t features; + EXPECT_EQ(sizeof(features), bl.length()); + auto it = bl.cbegin(); + ceph::decode(features, it); + EXPECT_EQ(RBD_FEATURES_ALL, features); + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, Stat, ReadOpTest) { + std::uint64_t size = 1; + sys::error_code ec; + co_await expect_error_code(execute("nonexistent"sv, + ReadOp{}.stat(&size, nullptr, &ec)), + sys::errc::no_such_file_or_directory); + EXPECT_EQ(sys::errc::io_error, ec); + EXPECT_EQ(1u, size); + + const ceph::real_time ts{1'457'129'052 * 1s}; + auto bl = to_buffer_list(data); + co_await execute(oid, WriteOp{}.write(0, std::move(bl)).set_mtime(ts)); + + ceph::real_time ts2; + ec.clear(); + co_await execute(oid, ReadOp{}.stat(&size, &ts2, &ec)); + EXPECT_FALSE(ec); + EXPECT_EQ(data.size(), size); + EXPECT_EQ(ts, ts2); + + co_await execute(oid, ReadOp{}.stat(nullptr, nullptr)); + + co_await expect_error_code(execute("nonexistent"sv, + ReadOp{}.stat(nullptr, nullptr)), + sys::errc::no_such_file_or_directory); + + co_return; +} + + +CORO_TEST_F(NeoRadosReadOps, Omap, ReadOpTest) { + const ctnr::flat_map omap{ + {"bar"s, {}}, + {"foo"s, to_buffer_list("\0"sv)}, + {"test1"s, to_buffer_list("abc"sv)}, + {"test2"s, to_buffer_list("va\0lue"sv)} + }; + + co_await expect_error_code( + execute("nonexistent"sv, + ReadOp{}.get_omap_vals({}, {}, 10, nullptr, nullptr)), + sys::errc::no_such_file_or_directory); + + { + ctnr::flat_map omap2; + bool truncated; + sys::error_code ec; + co_await execute(oid, ReadOp{}.get_omap_vals({}, {}, 10, &omap2, + &truncated, &ec)); + EXPECT_FALSE(ec); + EXPECT_TRUE(omap2.empty()); + EXPECT_FALSE(truncated); + } + + co_await execute(oid, WriteOp{}.set_omap(omap)); + + // Check for readability + { + ctnr::flat_map omap2; + ctnr::flat_set keys; + bool truncated, truncated2; + sys::error_code ec, ec2; + + co_await execute(oid, ReadOp{} + .get_omap_vals({}, {}, 10, &omap2, &truncated, &ec) + .get_omap_keys({}, 10, &keys, &truncated2, &ec2)); + EXPECT_FALSE(ec); + EXPECT_FALSE(ec2); + EXPECT_FALSE(truncated2); + EXPECT_EQ(omap, omap2); + EXPECT_FALSE(truncated); + EXPECT_EQ(omap.size(), keys.size()); + EXPECT_TRUE(std::all_of(keys.begin(), keys.end(), + [&](const auto& s) { + return omap.contains(s); + })); + EXPECT_TRUE(std::all_of(omap.begin(), omap.end(), + [&](const auto& kv) { + return keys.contains(kv.first); + })); + } + + // Check iteration and truncation + { + std::unordered_set keys; + for (const auto& [key, value] : omap) { + keys.insert(key); + } + bool truncated = true; + std::optional lastkey; + while (truncated) { + ctnr::flat_set keys2; + ctnr::flat_map omap2; + bool truncated2; + ReadOp op; + op.get_omap_vals(lastkey, {}, 1, &omap2, &truncated); + op.get_omap_keys(lastkey, 1, &keys2, &truncated2); + co_await execute(oid, std::move(op)); + EXPECT_EQ(1, std::ssize(keys2)); + EXPECT_EQ(1, std::ssize(omap2)); + EXPECT_EQ(truncated, truncated2); + + const auto& key = *keys2.begin(); + EXPECT_EQ(omap2.begin()->first, key); + EXPECT_TRUE(keys.contains(key)); + EXPECT_EQ(omap.at(key), omap2[key]); + keys.erase(key); + lastkey = key; + } + EXPECT_TRUE(keys.empty()); + } + + // check omap_cmp finds all expected values + { + ReadOp op; + for (const auto& [key, value] : omap) { + op.cmp_omap({{key, cmp_op::eq, value}}); + } + co_await execute(oid, std::move(op)); + } + { + std::vector cmps; + for (const auto& [key, value] : omap) { + cmps.push_back({key, cmp_op::eq, value}); + } + co_await execute(oid, ReadOp{}.cmp_omap(cmps)); + } + + // try to remove keys with a guard that should fail + { + WriteOp op; + auto key = (omap.begin() + 2)->first; + op.cmp_omap({{key, cmp_op::lt,omap.at(key)}}); + op.rm_omap_keys({omap.begin()->first, (omap.begin() + 1)->first}); + co_await expect_error_code(execute(oid, std::move(op)), + sys::errc::operation_canceled); + } + // Verify the keys are still there, and then remove them + { + WriteOp op; + op.cmp_omap({{omap.begin()->first, cmp_op::eq, omap.begin()->second}}); + op.cmp_omap({{(omap.begin() + 1)->first, cmp_op::eq, + {(omap.begin() + 1)->second}}}); + op.rm_omap_keys({omap.begin()->first, (omap.begin() + 1)->first}); + co_await execute(oid, std::move(op)); + + ctnr::flat_map omap2; + const ctnr::flat_map omapcmp{omap.begin() + 2, omap.end()}; + bool trunc; + co_await execute(oid, ReadOp{}.get_omap_vals({}, {}, 10, &omap2, &trunc)); + EXPECT_FALSE(trunc); + EXPECT_EQ(omapcmp, omap2); + } + + // clear the rest and check there are none left + { + co_await execute(oid, WriteOp{}.clear_omap()); + ctnr::flat_map omap2; + bool trunc; + co_await execute(oid, ReadOp{}.get_omap_vals({}, {}, 10, &omap2, &trunc)); + EXPECT_FALSE(trunc); + EXPECT_TRUE(omap2.empty()); + } + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, OmapNuls, ReadOpTest) { + const ctnr::flat_map omap{ + {"1\0bar"s, to_buffer_list("_\0var"sv)}, + {"2baar\0"s, to_buffer_list("_vaar\0"sv)}, + {"3baa\0rr"s, to_buffer_list("__vaa\0rr"sv)} + }; + + co_await expect_error_code( + execute("nonexistent"sv, ReadOp{}.get_omap_vals({}, {}, 10, nullptr, nullptr)), + sys::errc::no_such_file_or_directory); + { + ctnr::flat_map omap2; + bool truncated; + sys::error_code ec; + co_await execute(oid, ReadOp{} + .get_omap_vals({}, {}, 10, &omap2, &truncated, &ec)); + EXPECT_FALSE(ec); + EXPECT_TRUE(omap2.empty()); + EXPECT_FALSE(truncated); + } + + co_await execute(oid, WriteOp{}.set_omap(omap)); + + // Check for readability + { + ctnr::flat_map omap2; + ctnr::flat_set keys; + bool truncated, truncated2; + sys::error_code ec, ec2; + ReadOp op; + op.get_omap_vals({}, {}, 10, &omap2, &truncated, &ec); + op.get_omap_keys({}, 10, &keys, &truncated2, &ec2); + co_await execute(oid, std::move(op)); + EXPECT_FALSE(ec); + EXPECT_FALSE(ec2); + EXPECT_FALSE(truncated2); + EXPECT_EQ(omap, omap2); + EXPECT_FALSE(truncated); + EXPECT_EQ(omap.size(), keys.size()); + EXPECT_TRUE(std::all_of(keys.begin(), keys.end(), + [&](const auto& s) { + return omap.contains(s); + })); + EXPECT_TRUE(std::all_of(omap.begin(), omap.end(), + [&](const auto& kv) { + return keys.contains(kv.first); + })); + } + + // Check iteration and truncation + { + std::unordered_set keys; + for (const auto& [key, value] : omap) { + keys.insert(key); + } + bool truncated = true; + std::optional lastkey; + while (truncated) { + ctnr::flat_set keys2; + ctnr::flat_map omap2; + bool truncated2; + ReadOp op; + op.get_omap_vals(lastkey, {}, 1, &omap2, &truncated); + op.get_omap_keys(lastkey, 1, &keys2, &truncated2); + co_await execute(oid, std::move(op)); + EXPECT_EQ(1, std::ssize(keys2)); + EXPECT_EQ(1, std::ssize(omap2)); + EXPECT_EQ(truncated, truncated2); + + const auto& key = *keys2.begin(); + EXPECT_EQ(omap2.begin()->first, key); + EXPECT_TRUE(keys.contains(key)); + EXPECT_EQ(omap.at(key), omap2[key]); + keys.erase(key); + lastkey = key; + } + EXPECT_TRUE(keys.empty()); + } + + // check omap_cmp finds all expected values + { + ReadOp op; + for (const auto& [key, value] : omap) { + op.cmp_omap({{key, cmp_op::eq, value}}); + } + co_await execute(oid, std::move(op)); + } + { + std::vector cmps; + for (const auto& [key, value] : omap) { + cmps.push_back({key, cmp_op::eq, value}); + } + co_await execute(oid, ReadOp{}.cmp_omap(cmps)); + } + + // try to remove keys with a guard that should fail + { + WriteOp op; + auto key = (omap.begin() + 2)->first; + op.cmp_omap({{key, cmp_op::lt, omap.at(key)}}); + op.rm_omap_keys({omap.begin()->first, (omap.begin() + 1)->first}); + co_await expect_error_code(execute(oid, std::move(op)), + sys::errc::operation_canceled); + } + // Verify the keys are still there, and then remove them + { + WriteOp op; + op.cmp_omap({{omap.begin()->first, cmp_op::eq, omap.begin()->second}}); + op.cmp_omap({{(omap.begin() + 1)->first, cmp_op::eq, + (omap.begin() + 1)->second}}); + op.rm_omap_keys({omap.begin()->first, (omap.begin() + 1)->first}); + co_await execute(oid, std::move(op)); + + ctnr::flat_map omap2; + const ctnr::flat_map omapcmp{omap.begin() + 2, omap.end()}; + bool trunc; + co_await execute(oid, ReadOp{}.get_omap_vals({}, {}, 10, &omap2, &trunc)); + EXPECT_FALSE(trunc); + EXPECT_EQ(omapcmp, omap2); + } + + // clear the rest and check there are none left + { + co_await execute(oid, WriteOp{}.clear_omap()); + ctnr::flat_map omap2; + bool trunc; + co_await execute(oid, ReadOp{}.get_omap_vals({}, {}, 10, &omap2, &trunc)); + EXPECT_FALSE(trunc); + EXPECT_TRUE(omap2.empty()); + } + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, GetXattrs, ReadOpTest) { + const ctnr::flat_map xattrs{ + {"bar"s, {}}, + {"foo"s, to_buffer_list("\0"sv)}, + {"test1"s, to_buffer_list("abc"sv)}, + {"test2"s, to_buffer_list("va\0lue"sv)} + }; + + { + ctnr::flat_map xattrs2; + sys::error_code ec; + co_await execute(oid, ReadOp{}.get_xattrs(&xattrs2, &ec)); + EXPECT_FALSE(ec); + EXPECT_TRUE(xattrs2.empty()); + } + + { + WriteOp op; + for (const auto& [key, value] : xattrs) { + op.setxattr(key, buffer::list{value}); + } + co_await execute(oid, std::move(op)); + } + + { + ctnr::flat_map xattrs2; + sys::error_code ec; + co_await execute(oid, ReadOp{}.get_xattrs(&xattrs2, &ec)); + EXPECT_FALSE(ec); + EXPECT_EQ(xattrs, xattrs2); + } + + { + ReadOp op; + std::vector bls; + std::vector ecs; + bls.reserve(xattrs.size()); + ecs.reserve(xattrs.size()); + for (const auto& [key, value] : xattrs) { + bls.push_back({}); + ecs.push_back({}); + op.get_xattr(key, &bls.back(), &ecs.back()); + } + + co_await execute(oid, std::move(op)); + + EXPECT_EQ(xattrs.size(), ecs.size()); + EXPECT_EQ(xattrs.size(), bls.size()); + for (auto i = 0; i < std::ssize(xattrs); ++i) { + const auto& key = (xattrs.begin() + i)->first; + EXPECT_FALSE(ecs[i]); + EXPECT_EQ(xattrs.at(key), bls[i]); + } + } + + { + ReadOp op; + for (const auto& [key, value] : xattrs) { + op.cmpxattr(key, cmp_op::eq, value); + } + co_await execute(oid, std::move(op)); + } + + co_return; +} + +CORO_TEST_F(NeoRadosReadOps, CmpExt, ReadOpTest) { + co_await execute(oid, WriteOp{}.write_full(to_buffer_list("\x01\x02\x03"sv))); + uint64_t unmatch = 0; + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x01\x02\x03"sv), &unmatch); + op.read(0, 0, &bl); + co_await execute(oid, std::move(op)); + EXPECT_EQ(-1 , unmatch); + EXPECT_EQ(to_buffer_list("\x01\x02\x03"sv), bl); + } + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x00\x02\x03"sv), &unmatch); + op.read(0, 0, &bl); + co_await expect_error_code(execute(oid, std::move(op)), + osd_errc::cmpext_mismatch); + EXPECT_EQ(0 , unmatch); + EXPECT_EQ(0, bl.length()); + } + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x01\x00\x03"sv), &unmatch); + op.read(0, 0, &bl); + co_await expect_error_code(execute(oid, std::move(op)), + osd_errc::cmpext_mismatch); + EXPECT_EQ(1, unmatch); + EXPECT_EQ(0, bl.length()); + } + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x01\x02\x00"sv), &unmatch); + op.read(0, 0, &bl); + co_await expect_error_code(execute(oid, std::move(op)), + osd_errc::cmpext_mismatch); + EXPECT_EQ(2, unmatch); + EXPECT_EQ(0, bl.length()); + } + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x01\x02\x03\x04"sv), &unmatch); + op.read(0, 0, &bl); + co_await expect_error_code(execute(oid, std::move(op)), + osd_errc::cmpext_mismatch); + EXPECT_EQ(3, unmatch); + EXPECT_EQ(0, bl.length()); + } + // Make sure other error codes work properly + { + buffer::list bl; + ReadOp op; + op.cmpext(0, to_buffer_list("\x01\x02\x03"sv), &unmatch); + op.read(0, 0, &bl); + co_await expect_error_code(execute("nonexistent"sv, std::move(op)), + sys::errc::no_such_file_or_directory); + EXPECT_EQ(-1, unmatch); + EXPECT_EQ(0, bl.length()); + } + co_return; +}