FeatureStore: Add unit tests Adds unit tests for FeatureStore and updates existing tests to support FeatureStore mocks. Design-Doc: go/bmc-redfish-nvme-get-set-features Google-Bug-Id: 405609473 Change-Id: I8edea2aba5a665764609a400fd097b65d24b66cb Signed-off-by: Guangzong Chen <guangzong@google.com>
diff --git a/src/NVMeFeatureStore.cpp b/src/NVMeFeatureStore.cpp index b08ce56..0a2a0ed 100644 --- a/src/NVMeFeatureStore.cpp +++ b/src/NVMeFeatureStore.cpp
@@ -191,9 +191,9 @@ void FeatureStore::init(std::function<void(const std::error_code&)>&& cb) { - boost::asio::spawn(io, - [self{shared_from_this()}, cb{std::move(cb)}]( - const boost::asio::yield_context& yield) mutable { + (void)boost::asio::spawn( + io, [self{shared_from_this()}, cb{std::move(cb)}]( + const boost::asio::yield_context& yield) mutable { self->fetchIdentifyData(yield); self->registerDbusInterface(); cb({});
diff --git a/src/NVMeFeatureStore.hpp b/src/NVMeFeatureStore.hpp index 37f59a7..a513a73 100644 --- a/src/NVMeFeatureStore.hpp +++ b/src/NVMeFeatureStore.hpp
@@ -20,6 +20,8 @@ class FeatureStore : public std::enable_shared_from_this<FeatureStore> { + friend class FeatureStoreTest; + public: FeatureStore(boost::asio::io_context& io, sdbusplus::asio::object_server& objServer,
diff --git a/tests/meson.build b/tests/meson.build index 8e89d85..8221e3a 100644 --- a/tests/meson.build +++ b/tests/meson.build
@@ -89,6 +89,7 @@ '../src/NVMeVolume.cpp', '../src/NVMePlugin.cpp', '../src/NVMeMetricStore.cpp', + '../src/NVMeFeatureStore.cpp', cpp_args: ['-UBOOST_ASIO_NO_DEPRECATED', '-UBOOST_ASIO_DISABLE_THREADS', '-UBOOST_ASIO_HAS_IO_URING','-DBUILDDIR='+ meson.current_build_dir(), '-DNVME_UNIT_TEST=1'], link_args: cxx.get_id() == 'clang' ? ['-rdynamic'] : ['-export-dynamic'], dependencies: [ut_deps_list, nvme_deps, nlohmann_json, plugin_dep], @@ -107,6 +108,19 @@ include_directories: '../src', ) ) + test( + 'test_nvme_feature_store', + executable( + 'test_nvme_feature_store', + 'test_nvme_feature_store.cpp', + '../src/NVMeFeatureStore.cpp', + '../src/NVMeError.cpp', + cpp_args: ['-UBOOST_ASIO_NO_DEPRECATED', '-UBOOST_ASIO_DISABLE_THREADS', '-UBOOST_ASIO_HAS_IO_URING','-DBUILDDIR='+ meson.current_build_dir(), '-DNVME_UNIT_TEST=1'], + link_args: cxx.get_id() == 'clang' ? ['-rdynamic'] : ['-export-dynamic'], + dependencies: [ut_deps_list, nvme_deps, nlohmann_json, plugin_dep], + include_directories: '../src', + ) + ) endif
diff --git a/tests/test_nvme_feature_store.cpp b/tests/test_nvme_feature_store.cpp new file mode 100644 index 0000000..7c101bf --- /dev/null +++ b/tests/test_nvme_feature_store.cpp
@@ -0,0 +1,590 @@ +#include "nvme_features.pb.h" + +#include "NVMeError.hpp" +#include "NVMeFeatureStore.hpp" +#include "NVMeMiFake.hpp" + +#include <boost/asio/spawn.hpp> +#include <sdbusplus/asio/connection.hpp> +#include <sdbusplus/asio/object_server.hpp> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +class NVMeMiMock : + public NVMeMiIntf, + public std::enable_shared_from_this<NVMeMiMock> +{ + public: + ~NVMeMiMock() override = default; + MOCK_METHOD(void, start, (const std::shared_ptr<MctpEndpoint>&), + (override)); + MOCK_METHOD(void, stop, (), (override)); + MOCK_METHOD(void, recover, (), (override)); + MOCK_METHOD(void, miSubsystemHealthStatusPoll, + (std::function<void(const std::error_code&, + nvme_mi_nvm_ss_health_status*)>&&), + (override)); + MOCK_METHOD(void, miScanCtrl, + (std::function<void(const std::error_code&, + const std::vector<nvme_mi_ctrl_t>&)>), + (override)); + MOCK_METHOD(bool, flushOperations, (std::function<void()>&&), (override)); + MOCK_METHOD(void, adminIdentify, + (nvme_mi_ctrl_t ctrl, nvme_identify_cns cns, uint32_t nsid, + uint16_t cntid, + std::function<void(nvme_ex_ptr, std::span<uint8_t>)>&& cb), + (override)); + MOCK_METHOD(void, adminGetFeatures, + (nvme_mi_ctrl_t ctrl, const NVMeMiIntf::GetFeaturesRequest& req, + std::function<void(nvme_ex_ptr, uint32_t response, + std::span<uint8_t>)>&& cb), + (override)); + MOCK_METHOD(void, adminSetFeatures, + (nvme_mi_ctrl_t ctrl, const NVMeMiIntf::SetFeaturesRequest& req, + std::function<void(nvme_ex_ptr, uint32_t response)>&& cb), + (override)); + MOCK_METHOD( + void, adminGetLogPage, + (nvme_mi_ctrl_t ctrl, nvme_cmd_get_log_lid lid, uint32_t nsid, + uint8_t lsp, uint16_t lsi, + std::function<void(const std::error_code&, std::span<uint8_t>)>&& cb), + (override)); + MOCK_METHOD( + void, adminFwCommit, + (nvme_mi_ctrl_t ctrl, nvme_fw_commit_ca action, uint8_t slot, bool bpid, + std::function<void(const std::error_code&, nvme_status_field)>&& cb), + (override)); + MOCK_METHOD( + void, adminFwDownload, + (nvme_mi_ctrl_t ctrl, std::string firmwarefile, + std::function<void(const std::error_code&, nvme_status_field)>&& cb), + (override)); + MOCK_METHOD(void, adminXfer, + (nvme_mi_ctrl_t ctrl, const nvme_mi_admin_req_hdr& adminReq, + std::span<uint8_t> data, unsigned int timeoutMs, + std::function<void(const std::error_code& ec, + const nvme_mi_admin_resp_hdr& adminResp, + std::span<uint8_t> respData)>&& cb), + (override)); + MOCK_METHOD(void, adminSecuritySend, + (nvme_mi_ctrl_t ctrl, uint8_t proto, uint16_t specific, + std::span<uint8_t> data, + std::function<void(const std::error_code&, int)>&& cb), + (override)); + MOCK_METHOD(void, adminSecurityReceive, + (nvme_mi_ctrl_t ctrl, uint8_t proto, uint16_t specific, + uint32_t al, + std::function<void(const std::error_code&, int, + const std::span<uint8_t>)>&& cb), + (override)); + MOCK_METHOD( + void, adminNonDataCmd, + (nvme_mi_ctrl_t ctrl, uint8_t opcode, uint32_t cdw1, uint32_t cdw2, + uint32_t cdw3, uint32_t cdw10, uint32_t cdw11, uint32_t cdw12, + uint32_t cdw13, uint32_t cdw14, uint32_t cdw15, + std::function<void(const std::error_code&, int, uint32_t)>&& cb), + (override)); + MOCK_METHOD(void, createNamespace, + (nvme_mi_ctrl_t ctrl, uint64_t size, size_t lbaFormat, + bool metadataAtEnd, + std::function<void(nvme_ex_ptr)>&& submittedCb, + std::function<void(nvme_ex_ptr, NVMeNSIdentify)>&& finishedCb), + (override)); + MOCK_METHOD(void, adminDeleteNamespace, + (nvme_mi_ctrl_t ctrl, uint32_t nsid, + std::function<void(const std::error_code&, int)>&& cb), + (override)); + MOCK_METHOD(void, adminListNamespaces, + (nvme_mi_ctrl_t ctrl, + std::function<void(nvme_ex_ptr, std::vector<uint32_t>)>&& cb), + (override)); + MOCK_METHOD(void, adminAttachDetachNamespace, + (nvme_mi_ctrl_t ctrl, uint16_t ctrlid, uint32_t nsid, + bool attach, + std::function<void(const std::error_code&, int)>&& cb), + (override)); + MOCK_METHOD(void, adminSanitize, + (nvme_mi_ctrl_t ctrl, enum nvme_sanitize_sanact sanact, + uint8_t passes, uint32_t pattern, bool invertPattern, + std::function<void(nvme_ex_ptr)>&& cb), + (override)); + MOCK_METHOD( + void, adminLockdownInband, + (nvme_mi_ctrl_t ctrl, uint8_t prohibit, + const std::vector<uint8_t>& adminCmds, + const std::vector<uint8_t>& features, + const std::vector<uint8_t>& logPages, + std::function< + void(const std::error_code&, int nvmeStatus, uint32_t comptionDw0, + const std::string& failureScope, uint32_t failureId)>&& cb), + (override)); +}; + +class FeatureStoreTest : public ::testing::Test +{ + public: + static std::vector<uint8_t> + callGetFeature(const std::shared_ptr<FeatureStore>& fs, + boost::asio::yield_context yield, + const std::string& featureName) + { + return fs->getFeature(std::move(yield), featureName); + } + + static void callSetFeature(const std::shared_ptr<FeatureStore>& fs, + boost::asio::yield_context yield, + const std::string& featureName, + const std::vector<uint8_t>& data) + { + fs->setFeature(std::move(yield), featureName, data); + } + + static std::map<uint8_t, uint32_t>& + getFeatureCaps(const std::shared_ptr<FeatureStore>& fs) + { + return fs->featureCaps; + } +}; + +TEST_F(FeatureStoreTest, GetFeatureArbitration) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + EXPECT_CALL(*mockNvme, + adminGetFeatures(::testing::_, ::testing::_, ::testing::_)) + .WillOnce( + [](nvme_mi_ctrl_t, const NVMeMiIntf::GetFeaturesRequest& req, + std::function<void(nvme_ex_ptr, uint32_t, std::span<uint8_t>)>&& + cb) { + EXPECT_EQ(req.fid, 0x01); // Arbitration + cb(nullptr, 0x01020304, {}); // burst=4, low=3, med=2, high=1 + }); + + std::vector<uint8_t> result; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + result = FeatureStoreTest::callGetFeature( + featureStore, std::move(yield), "Arbitration_Current"); + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(done); + + google::gbmc::nvme::base::Arbitration msg; + ASSERT_TRUE( + msg.ParseFromArray(result.data(), static_cast<int>(result.size()))); + EXPECT_EQ(msg.arbitration_burst(), 0x04); + EXPECT_EQ(msg.low_priority_weight(), 0x03); + EXPECT_EQ(msg.medium_priority_weight(), 0x02); + EXPECT_EQ(msg.high_priority_weight(), 0x01); +} + +TEST_F(FeatureStoreTest, SetFeatureArbitration) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + google::gbmc::nvme::base::Arbitration msg; + msg.set_arbitration_burst(0x08); + msg.set_low_priority_weight(0x07); + msg.set_medium_priority_weight(0x06); + msg.set_high_priority_weight(0x05); + + std::vector<uint8_t> data(msg.ByteSizeLong()); + ASSERT_TRUE( + msg.SerializeToArray(data.data(), static_cast<int>(data.size()))); + + EXPECT_CALL(*mockNvme, + adminSetFeatures(::testing::_, ::testing::_, ::testing::_)) + .WillOnce([](nvme_mi_ctrl_t, const NVMeMiIntf::SetFeaturesRequest& req, + std::function<void(nvme_ex_ptr, uint32_t)>&& cb) { + EXPECT_EQ(req.fid, 0x01); // Arbitration + EXPECT_EQ(req.cdw11, 0x05060708); + cb(nullptr, 0); + }); + + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_Current", data); + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(done); +} + +TEST_F(FeatureStoreTest, DiscoverFeaturesONCS) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + // Mock Identify Controller to return ONCS with bit 4 set + EXPECT_CALL(*mockNvme, + adminIdentify(::testing::_, NVME_IDENTIFY_CNS_CTRL, + ::testing::_, ::testing::_, ::testing::_)) + .WillOnce( + [](nvme_mi_ctrl_t, nvme_identify_cns, uint32_t, uint16_t, + std::function<void(nvme_ex_ptr, std::span<uint8_t>)>&& cb) { + nvme_id_ctrl id{}; + id.vwc = 1; // VWC supported + id.oncs = htole16(1 << 4); // Save and Select supported + std::vector<uint8_t> data(sizeof(id)); + memcpy(data.data(), &id, sizeof(id)); + cb(nullptr, data); + }); + + // Mock Get Features for capabilities of Arbitration (FID 0x01) + // Return Saveable (bit 2) and Changeable (bit 0) + EXPECT_CALL( + *mockNvme, + adminGetFeatures(::testing::_, + ::testing::Field(&NVMeMiIntf::GetFeaturesRequest::sel, + NVME_GET_FEATURES_SEL_SUPPORTED), + ::testing::_)) + .WillRepeatedly( + [](nvme_mi_ctrl_t, const NVMeMiIntf::GetFeaturesRequest& req, + std::function<void(nvme_ex_ptr, uint32_t, std::span<uint8_t>)>&& + cb) { + if (req.fid == 0x01) + { + cb(nullptr, 0x5, {}); // Changeable | Saveable + } + else + { + cb(nullptr, 0x4, {}); // Just Changeable + } + }); + + bool done = false; + featureStore->init([&done](const std::error_code& ec) { + EXPECT_FALSE(ec); + done = true; + }); + + while (!done) + { + io.run_one(); + } + + auto caps = FeatureStoreTest::getFeatureCaps(featureStore); + + // Check for Arbitration states (FID 0x01) + auto it = caps.find(0x01); + ASSERT_TRUE(it != caps.end()); + uint32_t cap = it->second; + + EXPECT_TRUE(cap & 0x1); // Saveable + EXPECT_TRUE(cap & 0x4); // Changeable +} + +TEST_F(FeatureStoreTest, GetFeatureInvalidName) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + bool threw = false; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callGetFeature(featureStore, std::move(yield), + "InvalidFeature_Current"); + } + catch (const sdbusplus::xyz::openbmc_project::Common::Error:: + InvalidArgument&) + { + threw = true; + } + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(threw); +} + +TEST_F(FeatureStoreTest, GetFeatureDbusError) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + EXPECT_CALL(*mockNvme, + adminGetFeatures(::testing::_, ::testing::_, ::testing::_)) + .WillOnce( + [](nvme_mi_ctrl_t, const NVMeMiIntf::GetFeaturesRequest&, + std::function<void(nvme_ex_ptr, uint32_t, std::span<uint8_t>)>&& + cb) { + cb(makeLibNVMeError(std::make_error_code(std::errc::connection_aborted), + -1, "mock"), + 0, {}); + }); + + bool threw = false; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callGetFeature(featureStore, std::move(yield), + "Arbitration_Current"); + } + catch ( + const sdbusplus::xyz::openbmc_project::Common::Error::Unavailable&) + { + threw = true; + } + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(threw); +} + +TEST_F(FeatureStoreTest, SetFeatureInvalidName) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + bool threw = false; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_InvalidSelect", {}); + } + catch (const sdbusplus::xyz::openbmc_project::Common::Error:: + InvalidArgument&) + { + threw = true; + } + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(threw); +} + +TEST_F(FeatureStoreTest, SetFeatureProtoDeserializationFailure) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + std::vector<uint8_t> malformedData = {0xFF, 0xFF, 0xFF}; + + bool threw = false; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_Current", + malformedData); + } + catch (const sdbusplus::xyz::openbmc_project::Common::Error:: + InvalidArgument&) + { + threw = true; + } + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(threw); +} + +TEST_F(FeatureStoreTest, SetFeatureInvalidArgumentBounds) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + google::gbmc::nvme::base::Arbitration msg; + msg.set_arbitration_burst(256); // Exceeds 8-bit bound (0-255) + + std::vector<uint8_t> data(msg.ByteSizeLong()); + ASSERT_TRUE( + msg.SerializeToArray(data.data(), static_cast<int>(data.size()))); + + bool threw = false; + bool done = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_Current", data); + } + catch (const sdbusplus::xyz::openbmc_project::Common::Error:: + InvalidArgument&) + { + threw = true; + } + done = true; + }); + while (!done) + { + io.run_one(); + } + EXPECT_TRUE(threw); +} + +TEST_F(FeatureStoreTest, SetFeatureNotChangeableOrSaveable) +{ + boost::asio::io_context io; + auto conn = std::make_shared<sdbusplus::asio::connection>(io); + sdbusplus::asio::object_server objServer(conn); + std::string objectPath = "/xyz/openbmc_project/nvme/ctrl0"; + + auto mockNvme = std::make_shared<NVMeMiMock>(); + nvme_mi_ctrl_t ctrl = nullptr; + + auto featureStore = std::make_shared<FeatureStore>( + io, objServer, conn, objectPath, mockNvme, ctrl, 0, + FeatureScope::Controller); + + // Seed featureCaps for Arbitration (FID 0x01) with 0 (neither changeable + // nor saveable) + FeatureStoreTest::getFeatureCaps(featureStore)[0x01] = 0; + + google::gbmc::nvme::base::Arbitration msg; + msg.set_arbitration_burst(4); + std::vector<uint8_t> data(msg.ByteSizeLong()); + ASSERT_TRUE( + msg.SerializeToArray(data.data(), static_cast<int>(data.size()))); + + // 1. Try to set Current (changeable=false) -> should throw NotAllowed + bool threwNotAllowedCurrent = false; + bool doneCurrent = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_Current", data); + } + catch ( + const sdbusplus::xyz::openbmc_project::Common::Error::NotAllowed&) + { + threwNotAllowedCurrent = true; + } + doneCurrent = true; + }); + while (!doneCurrent) + { + io.run_one(); + } + EXPECT_TRUE(threwNotAllowedCurrent); + + // 2. Try to set Saved (saveable=false) -> should throw NotAllowed + io.restart(); + bool threwNotAllowedSaved = false; + bool doneSaved = false; + boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + try + { + FeatureStoreTest::callSetFeature(featureStore, std::move(yield), + "Arbitration_Saved", data); + } + catch ( + const sdbusplus::xyz::openbmc_project::Common::Error::NotAllowed&) + { + threwNotAllowedSaved = true; + } + doneSaved = true; + }); + while (!doneSaved) + { + io.run_one(); + } + EXPECT_TRUE(threwNotAllowedSaved); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}