tests: Add NVMe-MI recovery test

Exercise the NVMeMi endpoint recovery implementation by exploiting the
link seam for libnvme{,-mi}.

To do this we first reuse the headers provided by libnvme with a
partial-dependency declaration in the test. From there we provide our
own (largely neutered) implementation of the APIs to link against.

With this in-place we can trivially recreate the error discussed in[1]
by returning erroneous values from the libnvme APIs. The higher-level
consequence is we drive the NVMeMi implementation through the endpoint
recovery process while trying to optimize the connection. At that point
we can use the usual GMock APIs to define the behaviors we want from
MctpDevice and MctpEndpoint during the recovery process.

[1]: https://github.com/CodeConstruct/dbus-sensors/issues/14

An interesting future direction is to allow mocks to be injected behind
the libnvme{,-mi} link seam. Adding a method to inject mocks enables
describing arbitrary NVMe scenarios in the usual gmock style, which has
the potential to drastically increase test code coverage for nvmesensor.

Returning to the change at hand, note that the test case causes valgrind
to report a leak, however this appears to be from the gmock error report
itself. Given we're working to resolve the gmock error, the leak will
disappear with it:

```
...
Mock MCTP Device] MCTP connection is not established
error reading ctemp from subsystem, reason:Transport endpoint is not connected
Sensor bar reading error!
../tests/test_nvme_mi_recovery.cpp:63: Failure
Mock function called more times than expected - returning directly.
    Function call: recover()
         Expected: to be called between 1 and 3 times
           Actual: called 5 times - over-saturated and active

[Mock MCTP Device] MCTP connection is not established
error reading ctemp from subsystem, reason:Transport endpoint is not connected
../tests/test_nvme_mi_recovery.cpp:63: Failure
Mock function called more times than expected - returning directly.
    Function call: recover()
         Expected: to be called between 1 and 3 times
           Actual: called 6 times - over-saturated and active

status else
poll loop has been canceled
../tests/test_nvme_mi_recovery.cpp:90: Failure
Value of: testing::Mock::VerifyAndClearExpectations(mctpEp.get())
  Actual: false
Expected: true

[  FAILED  ] NVMeRecovery.optimisationFailure (8348 ms)
[----------] 1 test from NVMeRecovery (8359 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (8415 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] NVMeRecovery.optimisationFailure

 1 FAILED TEST
==197574==
==197574== HEAP SUMMARY:
==197574==     in use at exit: 8 bytes in 1 blocks
==197574==   total heap usage: 7,500 allocs, 7,499 frees, 1,751,392 bytes allocated
==197574==
==197574== 8 bytes in 1 blocks are still reachable in loss record 1 of 1
==197574==    at 0x4840F83: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==197574==    by 0xD157EF: testing::internal::GetFailureReporter() (gmock-internal-utils.cc:124)
==197574==    by 0x9C16E3: testing::internal::Expect(bool, char const*, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (gmock-internal-utils.h:259)
==197574==    by 0x9C2303: testing::internal::UntypedFunctionMockerBase::FailureCleanupHandler::~FailureCleanupHandler() (gmock-spec-builders.h:1427)
==197574==    by 0x9D852C: testing::internal::FunctionMocker<void ()>::InvokeWith(std::tuple<>&&) (gmock-spec-builders.h:1898)
==197574==    by 0x9CB22B: testing::internal::FunctionMocker<void ()>::Invoke() (gmock-spec-builders.h:1548)
==197574==    by 0x9C2EF8: MockMctpEndpoint::recover() (test_nvme_mi_recovery.cpp:27)
==197574==    by 0xAD57EB: NVMeMi::recover() (NVMeMi.cpp:192)
==197574==    by 0xAD50B1: NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}::operator()(std::error_code const&) const (NVMeMi.cpp:167)
==197574==    by 0xAFF26F: void std::__invoke_impl<void, NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}&, std::error_code const&>(std::__invoke_other, NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}&, std::error_code const&) (invoke.h:61)
==197574==    by 0xAF3ED2: std::enable_if<is_invocable_r_v<void, NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}&, std::error_code const&>, void>::type std::__invoke_r<void, NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}&, std::error_code const&>(NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}&, std::error_code const&) (invoke.h:111)
==197574==    by 0xAEFE8F: std::_Function_handler<void (std::error_code const&), NVMeMi::epOptimize()::{lambda(boost::system::error_code)#1}::operator()(boost::system::error_code) const::{lambda(std::error_code const&)#1}>::_M_invoke(std::_Any_data const&, std::error_code const&) (std_function.h:290)
==197574==
==197574== LEAK SUMMARY:
==197574==    definitely lost: 0 bytes in 0 blocks
==197574==    indirectly lost: 0 bytes in 0 blocks
==197574==      possibly lost: 0 bytes in 0 blocks
==197574==    still reachable: 8 bytes in 1 blocks
==197574==         suppressed: 0 bytes in 0 blocks
==197574==
==197574== For lists of detected and suppressed errors, rerun with: -s
==197574== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```

Change-Id: Idf3fb6d167a9e11ea1d554c4ab2f2c50d1489421
Signed-off-by: Andrew Jeffery <andrew@codeconstruct.com.au>
diff --git a/tests/libnvme_mi_mock.cpp b/tests/libnvme_mi_mock.cpp
new file mode 100644
index 0000000..171efab
--- /dev/null
+++ b/tests/libnvme_mi_mock.cpp
@@ -0,0 +1,198 @@
+#include <libnvme-mi.h>
+
+#include <gmock/gmock.h>
+
+// libnvme-mi mock implementation
+
+#define DS_UNUSED __attribute__((unused))
+
+extern "C"
+{
+struct nvme_mi_ctrl
+{
+    int member;
+};
+
+struct nvme_mi_ep
+{
+    struct nvme_mi_ctrl ctrl;
+};
+
+struct nvme_root
+{
+    struct nvme_mi_ep ep;
+};
+}
+
+static struct nvme_root root;
+
+const char* nvme_mi_status_to_string(int status DS_UNUSED)
+{
+    return "Unimplemented";
+}
+
+int nvme_mi_ep_set_timeout(
+    nvme_mi_ep_t ep DS_UNUSED,
+    unsigned int timeout_ms DS_UNUSED) // NOLINT(readability-identifier-naming)
+{
+    return 0;
+}
+
+unsigned int nvme_mi_ep_get_timeout(nvme_mi_ep_t ep DS_UNUSED)
+{
+    return 0;
+}
+
+nvme_root_t nvme_mi_create_root(
+    FILE* fp DS_UNUSED,
+    int log_level DS_UNUSED) // NOLINT(readability-identifier-naming)
+{
+    return &root;
+}
+
+int nvme_mi_admin_sanitize_nvm(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                               struct nvme_sanitize_nvm_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_admin_ns_attach(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                            struct nvme_ns_attach_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+// clang-format off
+int nvme_mi_admin_admin_passthru(
+    nvme_mi_ctrl_t ctrl DS_UNUSED, __u8 opcode DS_UNUSED, __u8 flags DS_UNUSED,
+    __u16 rsvd DS_UNUSED, __u32 nsid DS_UNUSED, __u32 cdw2 DS_UNUSED,
+    __u32 cdw3 DS_UNUSED, __u32 cdw10 DS_UNUSED, __u32 cdw11 DS_UNUSED,
+    __u32 cdw12 DS_UNUSED, __u32 cdw13 DS_UNUSED, __u32 cdw14 DS_UNUSED,
+    __u32 cdw15 DS_UNUSED,
+    __u32 data_len DS_UNUSED, // NOLINT(readability-identifier-naming)
+    void* data DS_UNUSED,
+    __u32 metadata_len DS_UNUSED, // NOLINT(readability-identifier-naming)
+    void* metadata DS_UNUSED,
+    __u32 timeout_ms DS_UNUSED, // NOLINT(readability-identifier-naming)
+    __u32* result DS_UNUSED)
+{
+    return 0;
+}
+// clang-format on
+
+int nvme_mi_admin_security_send(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                                struct nvme_security_send_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_admin_security_recv(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                                struct nvme_security_receive_args* args
+                                    DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_admin_fw_download(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                              struct nvme_fw_download_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_admin_fw_commit(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                            struct nvme_fw_commit_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+// clang-format off
+int nvme_mi_admin_xfer(
+    nvme_mi_ctrl_t ctrl DS_UNUSED,
+    struct nvme_mi_admin_req_hdr* admin_req DS_UNUSED, // NOLINT(readability-identifier-naming)
+    size_t req_data_size DS_UNUSED, // NOLINT(readability-identifier-naming)
+    struct nvme_mi_admin_resp_hdr* admin_resp DS_UNUSED, // NOLINT(readability-identifier-naming)
+    off_t resp_data_offset DS_UNUSED, // NOLINT(readability-identifier-naming)
+    size_t* resp_data_size DS_UNUSED) // NOLINT(readability-identifier-naming)
+{
+    return 0;
+}
+// clang-format on
+
+int nvme_mi_admin_identify_partial(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                                   struct nvme_identify_args* args DS_UNUSED,
+                                   off_t offset DS_UNUSED,
+                                   size_t size DS_UNUSED)
+{
+    return 0;
+}
+
+nvme_mi_ctrl_t nvme_mi_next_ctrl(nvme_mi_ep_t ep DS_UNUSED,
+                                 nvme_mi_ctrl_t c DS_UNUSED)
+{
+    return nullptr;
+}
+
+nvme_mi_ctrl_t nvme_mi_first_ctrl(nvme_mi_ep_t ep DS_UNUSED)
+{
+    return nullptr;
+}
+
+// clang-format off
+int nvme_mi_scan_ep(nvme_mi_ep_t ep DS_UNUSED, bool force_rescan DS_UNUSED) // NOLINT(readability-identifier-naming)
+{
+    return 0;
+}
+// clang-format on
+
+int nvme_mi_mi_subsystem_health_status_poll(
+    nvme_mi_ep_t ep DS_UNUSED, bool clear DS_UNUSED,
+    struct nvme_mi_nvm_ss_health_status* nshds DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_mi_read_mi_data_port(nvme_mi_ep_t ep DS_UNUSED,
+                                 __u8 portid DS_UNUSED,
+                                 struct nvme_mi_read_port_info* p DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_mi_read_mi_data_subsys(nvme_mi_ep_t ep DS_UNUSED,
+                                   struct nvme_mi_read_nvm_ss_info* s DS_UNUSED)
+{
+    return -1;
+}
+
+void nvme_mi_close(nvme_mi_ep_t ep DS_UNUSED) {}
+
+nvme_mi_ep_t nvme_mi_open_mctp(nvme_root_t root DS_UNUSED,
+                               unsigned int netid DS_UNUSED,
+                               uint8_t eid DS_UNUSED)
+{
+    return &root->ep;
+}
+
+int nvme_mi_admin_ns_mgmt(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                          struct nvme_ns_mgmt_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_admin_get_log(nvme_mi_ctrl_t ctrl DS_UNUSED,
+                          struct nvme_get_log_args* args DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_mi_config_set(nvme_mi_ep_t ep DS_UNUSED, __u32 dw0 DS_UNUSED,
+                          __u32 dw1 DS_UNUSED)
+{
+    return 0;
+}
+
+int nvme_mi_mi_config_get(nvme_mi_ep_t ep DS_UNUSED, __u32 dw0 DS_UNUSED,
+                          __u32 dw1 DS_UNUSED, __u32* nmresp DS_UNUSED)
+{
+    return 0;
+}
diff --git a/tests/libnvme_mock.cpp b/tests/libnvme_mock.cpp
new file mode 100644
index 0000000..1f14859
--- /dev/null
+++ b/tests/libnvme_mock.cpp
@@ -0,0 +1,17 @@
+#include <libnvme-mi.h>
+
+#include <gmock/gmock.h>
+
+// libnvme mock implementation
+
+#define DS_UNUSED __attribute__((unused))
+
+const char* nvme_status_to_string(int status DS_UNUSED, bool fabrics DS_UNUSED)
+{
+    return "Unimplemented";
+}
+
+// clang-format off
+void nvme_init_ctrl_list(struct nvme_ctrl_list* cntlist DS_UNUSED, __u16 num_ctrls DS_UNUSED, __u16* ctrlist DS_UNUSED) // NOLINT(readability-identifier-naming)
+{}
+// clang-format on
diff --git a/tests/meson.build b/tests/meson.build
index 8221e3a..199f371 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -122,6 +122,53 @@
         )
     )
 
+
+    test_nvme_recovery_bin = executable('test_nvme_recovery',
+        'test_nvme_mi_recovery.cpp',
+        'libnvme_mock.cpp',
+        'libnvme_mi_mock.cpp',
+        '../src/NVMeController.cpp',
+        '../src/NVMeDevice.cpp',
+        '../src/NVMeDrive.cpp',
+        '../src/NVMeError.cpp',
+        '../src/NVMeMi.cpp',
+        '../src/NVMeMetricStore.cpp',
+        '../src/NVMePlugin.cpp',
+        '../src/NVMeProgress.cpp',
+        '../src/NVMeSensor.cpp',
+        '../src/NVMeStorage.cpp',
+        '../src/NVMeSubsys.cpp',
+        '../src/NVMeVolume.cpp',
+        cpp_args: [
+            '-DBOOST_ASIO_DISABLE_EPOLL',
+            '-DBOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT',
+            '-UBOOST_ASIO_NO_DEPRECATED',
+            '-UBOOST_ASIO_DISABLE_THREADS',
+            '-UBOOST_ASIO_HAS_IO_URING',
+            '-DBUILDDIR='+ meson.current_build_dir(), '-DNVME_UNIT_TEST=1'],
+        export_dynamic: true,
+        dependencies: [
+            default_deps,
+            i2c,
+            libnvme.partial_dependency(compile_args: true, includes: true).as_system(),
+            libnvme_mi.partial_dependency(compile_args: true, includes: true).as_system(),
+            nlohmann_json,
+            nvme_dbus_dep,
+            pdi_dep,
+            plugin_dep,
+            threads,
+            thresholds_dep,
+            ut_deps_list,
+            utils_dep,
+        ],
+        implicit_include_directories: false,
+        include_directories: '../src',
+    )
+    test('test_nvme_recovery',
+        test_nvme_recovery_bin,
+        # depends: [ mapper ],
+        env:'LD_LIBRARY_PATH=/usr/local/lib',
+    )
 endif
 
 test(
diff --git a/tests/test_nvme_mi_recovery.cpp b/tests/test_nvme_mi_recovery.cpp
new file mode 100644
index 0000000..00f4714
--- /dev/null
+++ b/tests/test_nvme_mi_recovery.cpp
@@ -0,0 +1,116 @@
+#include "NVMeDevice.hpp"
+#include "NVMeIntf.hpp"
+#include "NVMeMi.hpp"
+#include "NVMeSubsys.hpp"
+#include "Utils.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+class MockMctpEndpoint : public MctpEndpoint
+{
+  public:
+    MOCK_METHOD(int, network, (), (const, override));
+    MOCK_METHOD(uint8_t, eid, (), (const, override));
+    MOCK_METHOD(void, subscribe,
+                (MctpEndpoint::Event && degraded,
+                 MctpEndpoint::Event&& available,
+                 MctpEndpoint::Event&& removed),
+                (override));
+    MOCK_METHOD(void, setMtu,
+                (uint32_t mtu,
+                 std::function<void(const std::error_code& ec)>&& completed),
+                (override));
+    MOCK_METHOD(void, remove, (), (override));
+    MOCK_METHOD(void, recover, (), (override));
+    MOCK_METHOD(std::string, describe, (), (const, override));
+    MOCK_METHOD(bool, isI2cAccessible, (), (const, override));
+};
+
+class MockMctpDevice : public MctpDevice
+{
+  public:
+    MOCK_METHOD(void, setup,
+                (std::function<void(const std::error_code& ec,
+                                    const std::shared_ptr<MctpEndpoint>& ep)> &&
+                 action),
+                (override));
+    MOCK_METHOD(void, remove, (), (override));
+    MOCK_METHOD(std::string, describe, (), (const, override));
+};
+
+TEST(NVMeRecovery, optimisationFailure)
+{
+    boost::asio::io_context io;
+
+    auto mctpEp = std::make_shared<MockMctpEndpoint>();
+    EXPECT_CALL(*mctpEp, describe())
+        .WillRepeatedly(testing::Return("Mock MCTP Endpoint"));
+    EXPECT_CALL(*mctpEp, eid()).WillRepeatedly(testing::Return(9));
+    EXPECT_CALL(*mctpEp, network()).WillRepeatedly(testing::Return(1));
+    EXPECT_CALL(*mctpEp, isI2cAccessible())
+        .WillRepeatedly(testing::Return(true));
+
+    boost::asio::spawn(io, [&](boost::asio::yield_context yield) {
+        MctpEndpoint::Event degradedHandler;
+        MctpEndpoint::Event availableHandler;
+        MctpEndpoint::Event removedHandler;
+        EXPECT_CALL(*mctpEp, subscribe(testing::_, testing::_, testing::_))
+            .WillOnce(testing::DoAll(testing::SaveArg<0>(&degradedHandler),
+                                     testing::SaveArg<1>(&availableHandler),
+                                     testing::SaveArg<2>(&removedHandler),
+                                     testing::InvokeWithoutArgs([&]() {
+            io.post([&]() { availableHandler(mctpEp); });
+        })));
+        EXPECT_CALL(*mctpEp, recover())
+            .Times(testing::Between(1, 3))
+            .WillRepeatedly(testing::InvokeWithoutArgs([&]() {
+            io.post([&]() { degradedHandler(mctpEp); });
+            io.post([&]() { availableHandler(mctpEp); });
+        }));
+
+        boost::asio::steady_timer timer(io);
+        auto mctpDev = std::make_shared<MockMctpDevice>();
+        EXPECT_CALL(*mctpDev, describe())
+            .WillRepeatedly(testing::Return("Mock MCTP Device"));
+        EXPECT_CALL(*mctpDev, setup(testing::_))
+            .WillOnce(testing::InvokeArgument<0>(std::error_code(), mctpEp));
+
+        auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+        sdbusplus::asio::object_server objectServer(systemBus, true);
+        auto worker = NVMeMiWorker::create(io);
+        auto intf = NVMeIntf::create<NVMeMi>(io, systemBus, mctpDev, worker);
+        SensorData sensorData{};
+        auto subsys = NVMeSubsystem::create(io, objectServer, systemBus, "/foo",
+                                            "bar", sensorData, intf, false);
+        auto nvmeDev = NVMeDevice::create(io, mctpDev, std::move(intf), subsys,
+                                          std::chrono::seconds(2));
+        nvmeDev->start();
+        timer.expires_after(std::chrono::seconds(6));
+        timer.async_wait(yield);
+        // io.run_for(std::chrono::seconds(6));
+
+        nvmeDev->stop();
+        // io.run_for(std::chrono::seconds(1));
+        timer.expires_after(std::chrono::seconds(1));
+        timer.async_wait(yield);
+
+        // https://stackoverflow.com/a/10289205
+        EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(mctpEp.get()));
+        EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(mctpDev.get()));
+        io.stop();
+    });
+
+    io.run();
+}
+
+// Unused, but required to link successfully
+std::unordered_map<std::string, void*> pluginLibMap = {};
+
+int main(int argc, char** argv)
+{
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}