modbus_rtu_lib: implement library APIs
Implement modbus-rtu library APIs which will be used by
phosphor-modbus-rtu service based on [1].
[1]: https://gerrit.openbmc.org/c/openbmc/docs/+/77318
Tested:
Added a Mock Modbus RTU server using socat which intercepts and replies
to modbus messages for testing.
```
> meson test -C builddir
ninja: Entering directory `/host/repos/Modbus/phosphor-modbus/builddir'
ninja: no work to do.
1/2 test_modbus_commands OK 0.01s
2/2 test_modbus OK 6.02s
Ok: 2
Fail: 0
```
Change-Id: I66cdc8fd930dd6f7ad6888116d1419ad8f8b8ed8
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/tests/test_modbus.cpp b/tests/test_modbus.cpp
new file mode 100644
index 0000000..c272f96
--- /dev/null
+++ b/tests/test_modbus.cpp
@@ -0,0 +1,138 @@
+#include "modbus/modbus.hpp"
+#include "modbus_server_tester.hpp"
+
+#include <fcntl.h>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+
+namespace RTUIntf = phosphor::modbus::rtu;
+using ModbusIntf = RTUIntf::Modbus;
+namespace TestIntf = phosphor::modbus::test;
+
+class ModbusTest : public ::testing::Test
+{
+ public:
+ static constexpr const char* clientDevicePath = "/tmp/ttyV0";
+ static constexpr const char* serverDevicePath = "/tmp/ttyV1";
+ static constexpr const auto defaultBaudeRate = "b115200";
+ int socat_pid = -1;
+ sdbusplus::async::context ctx;
+ std::unique_ptr<ModbusIntf> modbus;
+ int fdClient = -1;
+ std::unique_ptr<TestIntf::ServerTester> serverTester;
+ int fdServer = -1;
+
+ ModbusTest()
+ {
+ std::string socatCmd = std::format(
+ "socat -x -v -d -d pty,link={},rawer,echo=0,parenb,{} pty,link={},rawer,echo=0,parenb,{} & echo $!",
+ serverDevicePath, defaultBaudeRate, clientDevicePath,
+ defaultBaudeRate);
+
+ // Start socat in the background and capture its PID
+ FILE* fp = popen(socatCmd.c_str(), "r");
+ EXPECT_NE(fp, nullptr) << "Failed to start socat: " << strerror(errno);
+ EXPECT_GT(fscanf(fp, "%d", &socat_pid), 0);
+ pclose(fp);
+
+ // Wait for socat to start up
+ sleep(1);
+
+ fdClient = open(clientDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ EXPECT_NE(fdClient, -1)
+ << "Failed to open serial port " << clientDevicePath
+ << " with error: " << strerror(errno);
+
+ modbus = std::make_unique<ModbusIntf>(ctx, fdClient, 115200, 0);
+
+ fdServer = open(serverDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ EXPECT_NE(fdServer, -1)
+ << "Failed to open serial port " << serverDevicePath
+ << " with error: " << strerror(errno);
+
+ serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer);
+ }
+
+ ~ModbusTest() noexcept override
+ {
+ if (fdClient != -1)
+ {
+ close(fdClient);
+ fdClient = -1;
+ }
+ if (fdServer != -1)
+ {
+ close(fdServer);
+ fdServer = -1;
+ }
+ kill(socat_pid, SIGTERM);
+ }
+
+ void SetUp() override
+ {
+ ctx.spawn(serverTester->processRequests());
+ }
+
+ auto TestHoldingRegisters(uint16_t registerOffset, bool res)
+ -> sdbusplus::async::task<void>
+ {
+ std::cout << "TestHoldingRegisters() start" << std::endl;
+
+ std::vector<uint16_t> registers(
+ TestIntf::testSuccessReadHoldingRegisterCount);
+
+ auto ret = co_await modbus->readHoldingRegisters(
+ TestIntf::testDeviceAddress, registerOffset, registers);
+
+ EXPECT_EQ(ret, res) << "Failed to read holding registers";
+
+ if (!res)
+ {
+ co_return;
+ }
+
+ for (auto i = 0; i < TestIntf::testSuccessReadHoldingRegisterCount; i++)
+ {
+ EXPECT_EQ(registers[i],
+ TestIntf::testSuccessReadHoldingRegisterResponse[i]);
+ }
+
+ co_return;
+ }
+};
+
+TEST_F(ModbusTest, TestReadHoldingRegisterSuccess)
+{
+ ctx.spawn(TestHoldingRegisters(
+ TestIntf::testSuccessReadHoldingRegisterOffset, true));
+
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+ ctx.run();
+}
+
+TEST_F(ModbusTest, TestReadHoldingRegisterSegmentedSuccess)
+{
+ ctx.spawn(TestHoldingRegisters(
+ TestIntf::testSuccessReadHoldingRegisterSegmentedOffset, true));
+
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+ ctx.run();
+}
+
+TEST_F(ModbusTest, TestReadHoldingRegisterFailure)
+{
+ ctx.spawn(
+ TestHoldingRegisters(TestIntf::testFailureReadHoldingRegister, false));
+
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+ ctx.run();
+}