]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
test/neorados: Rounding out test coverage, part 1
authorAdam Emerson <aemerson@redhat.com>
Mon, 7 Aug 2023 17:21:10 +0000 (13:21 -0400)
committerAdam Emerson <aemerson@redhat.com>
Thu, 14 Sep 2023 21:48:00 +0000 (17:48 -0400)
This includes cls, cmd, and read_operations.

Signed-off-by: Adam Emerson <aemerson@redhat.com>
src/test/neorados/CMakeLists.txt
src/test/neorados/cls.cc [new file with mode: 0644]
src/test/neorados/cmd.cc [new file with mode: 0644]
src/test/neorados/read_operations.cc [new file with mode: 0644]

index fd76b09cf952c2c2be24925a80d091b541d1744d..e38c77c1d979f5ab173f1889c1de3abee8e54dcf 100644 (file)
@@ -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 (file)
index 0000000..92afaa6
--- /dev/null
@@ -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 <array>
+#include <coroutine>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <boost/asio/use_awaitable.hpp>
+
+#include <boost/system/errc.hpp>
+
+#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<char, object_size> 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<JSONFormatter>(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 constexpr 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 (file)
index 0000000..dc4d0bf
--- /dev/null
@@ -0,0 +1,99 @@
+// -*- 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 <array>
+#include <coroutine>
+
+#include <fmt/format.h>
+
+#include <boost/asio/use_awaitable.hpp>
+
+#include <boost/system/errc.hpp>
+
+#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;
+
+CORO_TEST_F(NeoRadosCmd, MonDescribe, NeoRadosTest) {
+  std::string outs;
+  buffer::list outbl;
+  co_await rados().mon_command({R"({"prefix": "get_command_descriptions"})"},
+                              {}, &outs, &outbl, asio::use_awaitable);
+  EXPECT_LT(0u, outbl.length());
+  EXPECT_LE(0u, outs.length());
+  co_return;
+}
+
+CORO_TEST_F(NeoRadosCmd, OSDCmd, NeoRadosTest) {
+  co_await expect_error_code(
+    rados().osd_command(0, {R"(asdfasdf)"},
+                       {}, asio::use_awaitable),
+    sys::errc::invalid_argument, sys::errc::no_such_device_or_address);
+
+  co_await expect_error_code(
+    rados().osd_command(0, {R"(version)"},
+                       {}, asio::use_awaitable),
+    sys::errc::invalid_argument, sys::errc::no_such_device_or_address);
+
+  auto [ec, outs, outbl] = co_await
+    rados().osd_command(0, {R"({"prefix":"version"})"}, {},
+                       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};
+
+  // note: tolerate NXIO here in case the cluster is thrashing out underneath us.
+  co_await expect_error_code(
+    rados().pg_command(pgid, {R"(asdfasdf)"},
+                      {}, 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);
+  }
+
+  // Working around a bug in GCC.
+  auto coro = rados().pg_command(
+    pgid,
+    {fmt::format(R"({{"prefix":"pg", "cmd":"query", "pgid":"{}.{}"}})",
+                pgid.pool, pgid.seed)},
+    {}, 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 (file)
index 0000000..9af3f2b
--- /dev/null
@@ -0,0 +1,747 @@
+// -*- 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 <coroutine>
+#include <cstring>
+#include <cstdint>
+#include <initializer_list>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <boost/asio/use_awaitable.hpp>
+
+#include <boost/container/flat_map.hpp>
+
+#include <boost/system/error_code.hpp>
+#include <boost/system/errc.hpp>
+
+#include <xxHash/xxhash.h>
+
+#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());
+  }
+
+  boost::asio::awaitable<void> CoSetUp() override {
+    co_await NeoRadosTest::CoSetUp();
+    co_await write_object(data);
+  }
+
+  boost::asio::awaitable<void> 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({0x00, 0x00}));
+  co_await expect_error_code(cmpxattr(xattr, eq,
+                                     to_buffer_list({0x00, 0xcc})),
+                            operation_canceled);
+  co_await cmpxattr(xattr, ne, to_buffer_list({0x00, 0xcc}));
+  co_await cmpxattr(xattr, gt, to_buffer_list({0x00, 0xcc}));
+  co_await cmpxattr(xattr, gte, to_buffer_list({0x00, 0xcc}));
+  co_await expect_error_code(cmpxattr(xattr, lt,
+                                     to_buffer_list({0x00, 0xcc})),
+                            operation_canceled);
+  co_await expect_error_code(cmpxattr(xattr, lte,
+                                     to_buffer_list({0x00, 0xcc})),
+                            operation_canceled);
+
+  co_await cmpxattr(xattr, eq, to_buffer_list({0x00, 0x00}));
+  co_await expect_error_code(cmpxattr(xattr, ne,
+                                     to_buffer_list({0x00, 0x00})),
+                            operation_canceled);
+  co_await expect_error_code(cmpxattr(xattr, gt,
+                                     to_buffer_list({0x00, 0x00})),
+                            operation_canceled);
+  co_await cmpxattr(xattr, gte, to_buffer_list({0x00, 0x00}));
+  co_await expect_error_code(cmpxattr(xattr, lt,
+                                     to_buffer_list({0x00, 0x00})),
+                            operation_canceled);
+  co_await cmpxattr(xattr, lte, to_buffer_list({0x00, 0x00}));
+
+  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<const uint8_t*>(v.data()),
+    uint32_t(v.size()));
+}
+
+CORO_TEST_F(NeoRadosReadOps, Checksum, ReadOpTest) {
+  {
+    static constexpr uint64_t seed = -1;
+    std::vector<uint64_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> omap2;
+    ctnr::flat_set<std::string> 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<std::string> keys;
+    for (const auto& [key, value] : omap) {
+      keys.insert(key);
+    }
+    bool truncated = true;
+    std::optional<std::string> lastkey;
+    while (truncated) {
+      ctnr::flat_set<std::string> keys2;
+      ctnr::flat_map<std::string, buffer::list> 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<neorados::cmp_assertion> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> omap2;
+    ctnr::flat_set<std::string> 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<std::string> keys;
+    for (const auto& [key, value] : omap) {
+      keys.insert(key);
+    }
+    bool truncated = true;
+    std::optional<std::string> lastkey;
+    while (truncated) {
+      ctnr::flat_set<std::string> keys2;
+      ctnr::flat_map<std::string, buffer::list> 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<neorados::cmp_assertion> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<std::string, buffer::list> 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<buffer::list> bls;
+    std::vector<sys::error_code> 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({1, 2, 3})));
+  int unmatch = 0;
+  {
+    buffer::list bl;
+    ReadOp op;
+    op.cmpext(0, to_buffer_list({1, 2, 3}), &unmatch);
+    op.read(0, 0, &bl);
+    co_await execute(oid, std::move(op));
+    EXPECT_EQ(-1 , unmatch);
+    EXPECT_EQ(to_buffer_list({1, 2, 3}), bl);
+  }
+  {
+    buffer::list bl;
+    ReadOp op;
+    op.cmpext(0, to_buffer_list({0, 2, 3}), &unmatch);
+    op.read(0, 0, &bl);
+    co_await expect_error_code(execute(oid, std::move(op)),
+                              osd_errc::cmpext_failed);
+    EXPECT_EQ(0 , unmatch);
+    EXPECT_EQ(0, bl.length());
+  }
+  {
+    buffer::list bl;
+    ReadOp op;
+    op.cmpext(0, to_buffer_list({1, 0, 3}), &unmatch);
+    op.read(0, 0, &bl);
+    co_await expect_error_code(execute(oid, std::move(op)),
+                              osd_errc::cmpext_failed);
+    EXPECT_EQ(1, unmatch);
+    EXPECT_EQ(0, bl.length());
+  }
+  {
+    buffer::list bl;
+    ReadOp op;
+    op.cmpext(0, to_buffer_list({1, 2, 0}), &unmatch);
+    op.read(0, 0, &bl);
+    co_await expect_error_code(execute(oid, std::move(op)),
+                              osd_errc::cmpext_failed);
+    EXPECT_EQ(2, unmatch);
+    EXPECT_EQ(0, bl.length());
+  }
+  {
+    buffer::list bl;
+    ReadOp op;
+    op.cmpext(0, to_buffer_list({1, 2, 3, 4}), &unmatch);
+    op.read(0, 0, &bl);
+    co_await expect_error_code(execute(oid, std::move(op)),
+                              osd_errc::cmpext_failed);
+    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({1, 2, 3}), &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(0, bl.length());
+  }
+  co_return;
+}