From: Adam C. Emerson Date: Sat, 21 Jan 2023 02:35:41 +0000 (-0500) Subject: test/neorados: Harness and convenience for Neorados tests X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=219d1a76f398f3663730ac32d85a9894363683a8;p=ceph.git test/neorados: Harness and convenience for Neorados tests Google Test does not support C++ coroutines, so kludge together a test harness that supports coroutines reasonably well. Also add a couple utility functions. Signed-off-by: Adam C. Emerson --- diff --git a/src/test/neorados/CMakeLists.txt b/src/test/neorados/CMakeLists.txt index 31e79a6615c46..e3caaaf07d9e5 100644 --- a/src/test/neorados/CMakeLists.txt +++ b/src/test/neorados/CMakeLists.txt @@ -21,7 +21,7 @@ target_link_libraries(ceph_test_neorados_op_speed add_library(neoradostest-support STATIC common_tests.cc) target_link_libraries(neoradostest-support - libneorados fmt::fmt) + libneorados fmt::fmt GTest::GTest) add_executable(ceph_test_neorados_list_pool list_pool.cc) target_link_libraries(ceph_test_neorados_list_pool diff --git a/src/test/neorados/common_tests.cc b/src/test/neorados/common_tests.cc index 4e4b6c0af14c8..6f274551d95e6 100644 --- a/src/test/neorados/common_tests.cc +++ b/src/test/neorados/common_tests.cc @@ -12,7 +12,6 @@ * Foundation. See file COPYING. */ -#include #include #include @@ -23,12 +22,11 @@ #include "common_tests.h" #include "include/neorados/RADOS.hpp" -namespace ba = boost::asio; -namespace R = neorados; +namespace asio = boost::asio; std::string get_temp_pool_name(std::string_view prefix) { - static auto hostname = ba::ip::host_name(); + static auto hostname = asio::ip::host_name(); static auto num = 1ull; return fmt::format("{}{}-{}-{}", prefix, hostname, getpid(), num++); } diff --git a/src/test/neorados/common_tests.h b/src/test/neorados/common_tests.h index 7afdf55c1bfa9..1a33a8a57502e 100644 --- a/src/test/neorados/common_tests.h +++ b/src/test/neorados/common_tests.h @@ -12,30 +12,355 @@ * Foundation. See file COPYING. */ +#pragma once + +#include +#include +#include +#include +#include #include #include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include #include "include/neorados/RADOS.hpp" +#include "common/dout.h" + +#include "gtest/gtest.h" + +/// \file test/neorados/common_tests.h +/// +/// \brief Tools for testing neorados code +/// +/// This is a set of utilities for testing code using the neorados +/// library, as well as for tests using C++20 Coroutines more generally. + +/// \brief Get a random, unique pool name +/// +/// Return a uniquified pool name specific to the host on which we are running. +/// +/// \param prefix A prefix for the returned pool name +/// +/// \return A unique pool name std::string get_temp_pool_name(std::string_view prefix = {}); -template -auto create_pool(neorados::RADOS& r, std::string pname, +/// \brief Create a RADOS pool +/// +/// Create a RADOS pool, returning its ID on success. +/// +/// \param r RADOS handle +/// \param pname Pool name +/// \param token Boost.Asio completion token +/// +/// \return The ID of the newly created pool +template CompletionToken> +auto create_pool(neorados::RADOS& r, + std::string pname, CompletionToken&& token) { - boost::asio::async_completion init(token); - r.create_pool(pname, std::nullopt, - [&r, pname = std::string(pname), - h = std::move(init.completion_handler)] - (boost::system::error_code ec) mutable { - r.lookup_pool( - pname, - [h = std::move(h)] - (boost::system::error_code ec, std::int64_t pool) mutable { - std::move(h)(ec, pool); - }); - }); - return init.result.get(); + namespace asio = boost::asio; + using boost::system::error_code; + using boost::system::system_error; + + return asio::async_initiate + (asio::experimental::co_composed + ([](auto state, neorados::RADOS& r, std::string pname) -> void { + try { + co_await r.create_pool(pname, std::nullopt, asio::deferred); + auto pool = co_await r.lookup_pool(pname, asio::deferred); + co_return {error_code{}, pool}; + } catch (const system_error& e) { + co_return {e.code(), int64_t{}}; + } + }, r.get_executor()), + token, std::ref(r), std::move(pname)); +} + +/// \brief Create a new, empty RADOS object +/// +/// \param r RADOS handle +/// \param oid Object name +/// \param ioc Locator +/// \param token Boost.Asio completion token +template CompletionToken> +auto create_obj(neorados::RADOS& r, std::string_view oid, + const neorados::IOContext& ioc, + CompletionToken&& token) +{ + neorados::WriteOp op; + op.create(true); + return r.execute(oid, ioc, std::move(op), + std::forward(token)); } + +/// \brief Expect one of several errors from a coroutine +/// +/// \param coro Awaitable coroutine +/// \param ec Valid errors +boost::asio::awaitable +expect_error_code(auto&& coro, auto ...ecs) { + bool failed = false; + try { + co_await std::move(coro); + } catch (const boost::system::system_error& e) { + failed = true; + auto h = [c = e.code()](auto t) -> bool { return t == c; }; + EXPECT_TRUE((h(ecs) || ...)) + << "Got unexpected error code " << e.code().message() << "."; + } + EXPECT_TRUE(failed) << "Operation did not error at all."; + co_return; +} + +/// \brief Test harness for C++20 Coroutines +/// +/// C++20 coroutines are better than what we had before, but don't +/// play well with RAII. There's no good way to run a coroutine from a +/// destructor, especially in a single-threaded, non-blocking +/// program. +/// +/// To be fair to C++20, this is difficult and even rust doesn't have +/// async drop yet. +/// +/// GTest has explicit SetUp and TearDown methods, however they're +/// just regular functions. So we get Coroutine analogues of SetUp and +/// TearDown that we then call from our custom TestBody. The user +/// writes their tests in CoTestBody. +class CoroTest : public testing::Test { +private: + std::exception_ptr eptr; +protected: + boost::asio::io_context asio_context; ///< The context on which the + /// coroutine runs. +public: + /// Final override that does nothing. Actual setup code should go in CoSetUp + void SetUp() override final { }; + /// Final override that does nothing. Actual teardown code should go + /// in CotearDown. + void TearDown() override final { }; + + /// \brief SetUp coroutine + /// + /// Called before the test body. Indicate failure by throwing + /// an exception. If an exception is thrown, neither the test body + /// nor teardown code are run. + virtual boost::asio::awaitable CoSetUp() { + co_return; + } + + /// \brief TearDown coroutine + /// + /// Called after the test body exits. + /// + /// \note This function is not run if CoSetup fails + virtual boost::asio::awaitable CoTearDown() { + co_return; + } + + /// \brief TestBody coroutine + /// + /// Run after setup. + virtual boost::asio::awaitable CoTestBody() = 0; + + /// \brief Run our coroutines + /// + /// This is marked final, since the actual test body belongs in + /// CoTestBody. + /// + /// Run CoSetUp and, if CoSetUp succeeded, CoTestBody and + /// CoTearDown. + /// + /// Error reporting of failures in CoSetUp and CoTearDown leaves + /// something to be desired as GTest thinks everything is the test + /// proper. + void TestBody() override final { + boost::asio::co_spawn( + asio_context, + [](CoroTest* t) -> boost::asio::awaitable { + co_await t->CoSetUp(); + try { + co_await t->CoTestBody(); + } catch (...) { + t->eptr = std::current_exception(); + } + co_await t->CoTearDown(); + if (t->eptr) { + std::rethrow_exception(t->eptr); + } + co_return; + }(this), + [](std::exception_ptr e) { + if (e) std::rethrow_exception(e); + }); + asio_context.run(); + } +}; + +/// \brief C++20 coroutine test harness for NeoRados +/// +/// CoTestBody has access to `rados`, a `neorados::RADOS` handle, and +/// `pool`, a `neorados::IOContext` representing a pool that will be +/// destroyed when the test exits. +class NeoRadosTest : public CoroTest { +private: + const std::string prefix_{std::string{"test framework "} + + testing::UnitTest::GetInstance()-> + current_test_info()->name() + + std::string{": "}}; + + std::optional rados_; + const std::string pool_name_ = get_temp_pool_name( + testing::UnitTest::GetInstance()->current_test_info()->name()); + neorados::IOContext pool_; + std::unique_ptr dpp_; + +protected: + + /// \brief Return reference to RADOS + /// + /// \warning This function should only be called from test bodies + /// (i.e. after `CoSetUp()`) + neorados::RADOS& rados() noexcept { return *rados_; } + + /// \brief Return name of created pool + /// + /// \warning This function should only be called from test bodies + /// (i.e. after `CoSetUp()`) + const std::string& pool_name() const noexcept { return pool_name_; } + + /// \brief Return reference to pool + /// + /// \warning This function should only be called from test bodies + /// (i.e. after `CoSetUp()`) + const neorados::IOContext& pool() const noexcept { return pool_; } + + /// \brief Return prefix for this test run + std::string_view prefix() const noexcept { return prefix_; } + + /// \brief Return DoutPrefixProvider* + /// + /// \warning This function should only be called from test bodies + /// (i.e. after `CoSetUp()`) + const DoutPrefixProvider* dpp() const noexcept { return dpp_.get(); } + + auto execute(std::string_view oid, neorados::WriteOp&& op, + std::uint64_t* ver = nullptr) { + return rados().execute(oid, pool(), std::move(op), + boost::asio::use_awaitable, ver); + } + + auto execute(std::string_view oid, neorados::ReadOp&& op, + std::uint64_t* ver = nullptr) { + return rados().execute(oid, pool(), std::move(op), nullptr, + boost::asio::use_awaitable, ver); + } + + boost::asio::awaitable + read(std::string_view oid, std::uint64_t off = 0, std::uint64_t len = 0) { + ceph::buffer::list bl; + neorados::ReadOp op; + op.read(off, len, &bl); + co_await rados().execute(oid, pool(), std::move(op), + nullptr, boost::asio::use_awaitable); + co_return bl; + } + +public: + + /// \brief Create RADOS handle and pool for the test + boost::asio::awaitable CoSetUp() override { + rados_ = co_await neorados::RADOS::Builder{} + .build(asio_context, boost::asio::use_awaitable); + dpp_ = std::make_unique(rados().cct(), 0, prefix().data()); + pool_.set_pool(co_await create_pool(rados(), pool_name(), + boost::asio::use_awaitable)); + co_return; + } + + ~NeoRadosTest() override = default; + + /// \brief Delete pool used for testing + boost::asio::awaitable CoTearDown() override { + co_await rados().delete_pool(pool().get_pool(), + boost::asio::use_awaitable); + co_return; + } +}; + +/// \brief Helper macro for defining coroutine tests with a fixture +/// +/// Defines a test using a coroutine fixture for +/// SetUp/TearDown. Fixtures must be descendants of `CoroTest`. +/// +/// \note Uses more of GTest's internals that I would like. +/// +/// \warning Use `EXPECT_*` only, not `ASSERT_*`. `ASSERT_` macros +/// return from the calling function and will not work in a +/// coroutine. +/// +/// \param test_suite_name Name of the test suite +/// \param test_name Name of the test +/// \param fixture Fixture class to use (descendent of CoroTest) +#define CORO_TEST_F(test_suite_name, test_name, fixture) \ + static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1, \ + "test_suite_name must not be empty"); \ + static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1, \ + "test_name must not be empty"); \ + class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) : public fixture { \ + public: \ + GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default; \ + ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default; \ + GTEST_DISALLOW_COPY_AND_ASSIGN_(GTEST_TEST_CLASS_NAME_(test_suite_name, \ + test_name)); \ + GTEST_DISALLOW_MOVE_AND_ASSIGN_(GTEST_TEST_CLASS_NAME_(test_suite_name, \ + test_name)); \ + \ + private: \ + boost::asio::awaitable CoTestBody() override; \ + static ::testing::TestInfo *const test_info_ GTEST_ATTRIBUTE_UNUSED_; \ + }; \ + \ + ::testing::TestInfo *const GTEST_TEST_CLASS_NAME_(test_suite_name, \ + test_name)::test_info_ = \ + ::testing::internal::MakeAndRegisterTestInfo( \ + #test_suite_name, #test_name, nullptr, nullptr, \ + ::testing::internal::CodeLocation(__FILE__, __LINE__), \ + (::testing::internal::GetTypeId()), \ + ::testing::internal::SuiteApiResolver::GetSetUpCaseOrSuite( \ + __FILE__, __LINE__), \ + ::testing::internal::SuiteApiResolver< \ + fixture>::GetTearDownCaseOrSuite(__FILE__, __LINE__), \ + new ::testing::internal::TestFactoryImpl); \ + boost::asio::awaitable GTEST_TEST_CLASS_NAME_(test_suite_name, \ + test_name)::CoTestBody() + +/// \brief Helper macro for defining coroutine tests +/// +/// Tests created this way are direct descendants of `CoroTest`. +/// +/// The Boost.Asio IO Context is `io_context`. +/// +/// \warning Use `EXPECT_*` only, not `ASSERT_*`. `ASSERT_` macros +/// return from the calling function and will not work in a +/// coroutine. +/// +/// \param test_suite_name Name of the test suite +/// \param test_name Name of the test +#define CORO_TEST(test_suite_name, test_name) \ + CORO_TEST_F(test_suite_name, test_name, CoroTest)