requester: PLDM handler for async request/response

PLDM request handler provides APIs to register PLDM request message,
handle retries and instance ID expiration. Sending the PLDM request
and handling response is handled in an async manner. On receiving
the response the corresponding response handler registered for the
request is invoked.

Tested: Ran unit tests

Signed-off-by: Tom Joseph <rushtotom@gmail.com>
Change-Id: I9f0a9dfcf0fbc9a84eefad375b92d40dd8b48d3d
diff --git a/requester/test/handler_test.cpp b/requester/test/handler_test.cpp
new file mode 100644
index 0000000..7230b08
--- /dev/null
+++ b/requester/test/handler_test.cpp
@@ -0,0 +1,149 @@
+#include "libpldm/base.h"
+
+#include "common/types.hpp"
+#include "common/utils.hpp"
+#include "mock_request.hpp"
+#include "pldmd/dbus_impl_requester.hpp"
+#include "requester/handler.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace pldm::requester;
+using namespace std::chrono;
+
+using ::testing::AtLeast;
+using ::testing::Between;
+using ::testing::Exactly;
+using ::testing::NiceMock;
+using ::testing::Return;
+
+class HandlerTest : public testing::Test
+{
+  protected:
+    HandlerTest() :
+        event(sdeventplus::Event::get_default()),
+        dbusImplReq(pldm::utils::DBusHandler::getBus(),
+                    "/xyz/openbmc_project/pldm")
+    {}
+
+    int fd = 0;
+    mctp_eid_t eid = 0;
+    sdeventplus::Event event;
+    pldm::dbus_api::Requester dbusImplReq;
+
+    /** @brief This function runs the sd_event_run in a loop till all the events
+     *         in the testcase are dispatched and exits when there are no events
+     *         for the timeout time.
+     *
+     *  @param[in] timeout - maximum time to wait for an event
+     */
+    void waitEventExpiry(milliseconds timeout)
+    {
+        while (1)
+        {
+            auto sleepTime = duration_cast<microseconds>(timeout);
+            // Returns 0 on timeout
+            if (!sd_event_run(event.get(), sleepTime.count()))
+            {
+                break;
+            }
+        }
+    }
+
+  public:
+    bool nullResponse = false;
+    bool validResponse = false;
+    int callbackCount = 0;
+    bool response2 = false;
+
+    void pldmResponseCallBack(mctp_eid_t /*eid*/, const pldm_msg* response,
+                              size_t respMsgLen)
+    {
+        if (response == nullptr && respMsgLen == 0)
+        {
+            nullResponse = true;
+        }
+        else
+        {
+            validResponse = true;
+        }
+        callbackCount++;
+    }
+};
+
+TEST_F(HandlerTest, singleRequestResponseScenario)
+{
+    Handler<NiceMock<MockRequest>> reqHandler(fd, event, dbusImplReq,
+                                              seconds(1), 2, milliseconds(100));
+    pldm::Request request{};
+    auto instanceId = dbusImplReq.getInstanceId(eid);
+    reqHandler.registerRequest(
+        eid, instanceId, 0, 0, std::move(request),
+        std::move(std::bind_front(&HandlerTest::pldmResponseCallBack, this)));
+
+    pldm::Response response(sizeof(pldm_msg_hdr) + sizeof(uint8_t));
+    auto responsePtr = reinterpret_cast<const pldm_msg*>(response.data());
+    reqHandler.handleResponse(eid, instanceId, 0, 0, responsePtr,
+                              sizeof(response));
+
+    // handleResponse() will free the instance ID after calling the response
+    // handler, so the same instance ID is granted next as well
+    ASSERT_EQ(validResponse, true);
+    ASSERT_EQ(instanceId, dbusImplReq.getInstanceId(eid));
+}
+
+TEST_F(HandlerTest, singleRequestInstanceIdTimerExpired)
+{
+    Handler<NiceMock<MockRequest>> reqHandler(fd, event, dbusImplReq,
+                                              seconds(1), 2, milliseconds(100));
+    pldm::Request request{};
+    auto instanceId = dbusImplReq.getInstanceId(eid);
+    reqHandler.registerRequest(
+        eid, instanceId, 0, 0, std::move(request),
+        std::move(std::bind_front(&HandlerTest::pldmResponseCallBack, this)));
+
+    // Waiting for 500ms so that the instance ID expiry callback is invoked
+    waitEventExpiry(milliseconds(500));
+
+    // cleanup() will free the instance ID after calling the response
+    // handler will no response, so the same instance ID is granted next
+    ASSERT_EQ(instanceId, dbusImplReq.getInstanceId(eid));
+    ASSERT_EQ(nullResponse, true);
+}
+
+TEST_F(HandlerTest, multipleRequestResponseScenario)
+{
+    Handler<NiceMock<MockRequest>> reqHandler(fd, event, dbusImplReq,
+                                              seconds(2), 2, milliseconds(100));
+    pldm::Request request{};
+    auto instanceId = dbusImplReq.getInstanceId(eid);
+    reqHandler.registerRequest(
+        eid, instanceId, 0, 0, std::move(request),
+        std::move(std::bind_front(&HandlerTest::pldmResponseCallBack, this)));
+
+    pldm::Request requestNxt{};
+    auto instanceIdNxt = dbusImplReq.getInstanceId(eid);
+    reqHandler.registerRequest(
+        eid, instanceIdNxt, 0, 0, std::move(requestNxt),
+        std::move(std::bind_front(&HandlerTest::pldmResponseCallBack, this)));
+
+    pldm::Response response(sizeof(pldm_msg_hdr) + sizeof(uint8_t));
+    auto responsePtr = reinterpret_cast<const pldm_msg*>(response.data());
+    reqHandler.handleResponse(eid, instanceIdNxt, 0, 0, responsePtr,
+                              sizeof(response));
+    ASSERT_EQ(validResponse, true);
+    ASSERT_EQ(callbackCount, 1);
+    validResponse = false;
+
+    // Waiting for 500ms and handle the response for the first request, to
+    // simulate a delayed response for the first request
+    waitEventExpiry(milliseconds(500));
+
+    reqHandler.handleResponse(eid, instanceId, 0, 0, responsePtr,
+                              sizeof(response));
+
+    ASSERT_EQ(validResponse, true);
+    ASSERT_EQ(callbackCount, 2);
+    ASSERT_EQ(instanceId, dbusImplReq.getInstanceId(eid));
+}
\ No newline at end of file
diff --git a/requester/test/meson.build b/requester/test/meson.build
new file mode 100644
index 0000000..1f5fe17
--- /dev/null
+++ b/requester/test/meson.build
@@ -0,0 +1,25 @@
+test_src = declare_dependency(
+          sources: [
+            '../../pldmd/dbus_impl_requester.cpp',
+            '../../pldmd/instance_id.cpp'])
+
+tests = [
+  'handler_test',
+  'request_test',
+]
+
+foreach t : tests
+  test(t, executable(t.underscorify(), t + '.cpp',
+                     implicit_include_directories: false,
+                     link_args: dynamic_linker,
+                     build_rpath: get_option('oe-sdk').enabled() ? rpath : '',
+                     dependencies: [
+                         gtest,
+                         gmock,
+                         libpldm_dep,
+                         phosphor_dbus_interfaces,
+                         sdbusplus,
+                         sdeventplus,
+                         test_src]),
+       workdir: meson.current_source_dir())
+endforeach
diff --git a/requester/test/mock_request.hpp b/requester/test/mock_request.hpp
new file mode 100644
index 0000000..b8e9efb
--- /dev/null
+++ b/requester/test/mock_request.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "requester/request.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace pldm
+{
+
+namespace requester
+{
+
+using namespace std::chrono;
+
+class MockRequest : public RequestRetryTimer
+{
+  public:
+    MockRequest(int /*fd*/, mctp_eid_t /*eid*/, sdeventplus::Event& event,
+                pldm::Request&& /*requestMsg*/, uint8_t numRetries,
+                milliseconds responseTimeOut) :
+        RequestRetryTimer(event, numRetries, responseTimeOut)
+    {}
+
+    MOCK_METHOD(int, send, (), (const, override));
+};
+
+} // namespace requester
+
+} // namespace pldm
\ No newline at end of file
diff --git a/requester/test/request_test.cpp b/requester/test/request_test.cpp
new file mode 100644
index 0000000..6471d9e
--- /dev/null
+++ b/requester/test/request_test.cpp
@@ -0,0 +1,101 @@
+#include "libpldm/base.h"
+
+#include "mock_request.hpp"
+
+#include <sdbusplus/timer.hpp>
+#include <sdeventplus/event.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace pldm::requester;
+using namespace std::chrono;
+using ::testing::AtLeast;
+using ::testing::Between;
+using ::testing::Exactly;
+using ::testing::Return;
+
+class RequestIntfTest : public testing::Test
+{
+  protected:
+    RequestIntfTest() : event(sdeventplus::Event::get_default())
+    {}
+
+    /** @brief This function runs the sd_event_run in a loop till all the events
+     *         in the testcase are dispatched and exits when there are no events
+     *         for the timeout time.
+     *
+     *  @param[in] timeout - maximum time to wait for an event
+     */
+    void waitEventExpiry(milliseconds timeout)
+    {
+        while (1)
+        {
+            auto sleepTime = duration_cast<microseconds>(timeout);
+            // Returns 0 on timeout
+            if (!sd_event_run(event.get(), sleepTime.count()))
+            {
+                break;
+            }
+        }
+    }
+
+    int fd = 0;
+    mctp_eid_t eid = 0;
+    sdeventplus::Event event;
+    std::vector<uint8_t> requestMsg;
+};
+
+TEST_F(RequestIntfTest, 0Retries100msTimeout)
+{
+    MockRequest request(fd, eid, event, std::move(requestMsg), 0,
+                        milliseconds(100));
+    EXPECT_CALL(request, send())
+        .Times(Exactly(1))
+        .WillOnce(Return(PLDM_SUCCESS));
+    auto rc = request.start();
+    ASSERT_EQ(rc, PLDM_SUCCESS);
+}
+
+TEST_F(RequestIntfTest, 2Retries100msTimeout)
+{
+    MockRequest request(fd, eid, event, std::move(requestMsg), 2,
+                        milliseconds(100));
+    // send() is called a total of 3 times, the original plus two retries
+    EXPECT_CALL(request, send()).Times(3).WillRepeatedly(Return(PLDM_SUCCESS));
+    auto rc = request.start();
+    ASSERT_EQ(rc, PLDM_SUCCESS);
+    waitEventExpiry(milliseconds(500));
+}
+
+TEST_F(RequestIntfTest, 9Retries100msTimeoutRequestStoppedAfter1sec)
+{
+    MockRequest request(fd, eid, event, std::move(requestMsg), 9,
+                        milliseconds(100));
+    // send() will be called a total of 10 times, the original plus 9 retries.
+    // In a ideal scenario send() would have been called 10 times in 1 sec (when
+    // the timer is stopped) with a timeout of 100ms. Because there are delays
+    // in dispatch, the range is kept between 5 and 10. This recreates the
+    // situation where the Instance ID expires before the all the retries have
+    // been completed and the timer is stopped.
+    EXPECT_CALL(request, send())
+        .Times(Between(5, 10))
+        .WillRepeatedly(Return(PLDM_SUCCESS));
+    auto rc = request.start();
+    ASSERT_EQ(rc, PLDM_SUCCESS);
+
+    auto requestStopCallback = [&](void) { request.stop(); };
+    phosphor::Timer timer(event.get(), requestStopCallback);
+    timer.start(duration_cast<microseconds>(seconds(1)));
+
+    waitEventExpiry(milliseconds(500));
+}
+
+TEST_F(RequestIntfTest, 2Retries100msTimeoutsendReturnsError)
+{
+    MockRequest request(fd, eid, event, std::move(requestMsg), 2,
+                        milliseconds(100));
+    EXPECT_CALL(request, send()).Times(Exactly(1)).WillOnce(Return(PLDM_ERROR));
+    auto rc = request.start();
+    ASSERT_EQ(rc, PLDM_ERROR);
+}