unittest_pilz_modbus_client.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2018 Pilz GmbH & Co. KG
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU Lesser General Public License for more details.
13 
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include <gtest/gtest.h>
19 
20 #include <chrono>
21 #include <memory>
22 #include <mutex>
23 #include <numeric>
24 #include <string>
25 #include <thread>
26 #include <vector>
27 
28 #include <ros/ros.h>
29 #include <modbus/modbus.h>
30 
32 #include <prbt_hardware_support/ModbusMsgInStamped.h>
37 
39 
40 #include <pilz_testutils/async_test.h>
41 
43 {
44 using ::testing::_;
45 using ::testing::AnyNumber;
46 using ::testing::AtLeast;
47 using ::testing::InSequence;
48 using ::testing::Invoke;
49 using ::testing::InvokeWithoutArgs;
50 using ::testing::Return;
51 using ::testing::Throw;
52 using namespace prbt_hardware_support;
53 
54 static constexpr unsigned int DEFAULT_MODBUS_PORT_TEST{ 502 };
55 static constexpr unsigned int REGISTER_FIRST_IDX_TEST{ 512 };
56 static constexpr unsigned int REGISTER_SIZE_TEST{ 2 };
57 static constexpr unsigned int RESPONSE_TIMEOUT{ 10 };
58 
59 static constexpr double WAIT_FOR_START_TIMEOUT_S{ 3.0 };
60 static constexpr double WAIT_SLEEPTIME_S{ 0.1 };
61 static constexpr double WAIT_FOR_STOP_TIMEOUT_S{ 3.0 };
62 static constexpr double WAIT_FOR_SERVICE_TIMEOUT_S{ 3.0 };
63 
68 {
69 public:
71 
72 public:
73  void start();
74  void stop();
75 
76 private:
78  std::thread client_thread_;
79 };
80 
82 {
83 }
84 
86 {
88 
89  ros::Time start_waiting = ros::Time::now();
90  while (!client_->isRunning())
91  {
93  if (ros::Time::now() > start_waiting + ros::Duration(WAIT_FOR_START_TIMEOUT_S))
94  {
95  break;
96  }
97  }
98 }
99 
101 {
102  client_->terminate();
103  ros::Time start_waiting = ros::Time::now();
104  while (client_->isRunning())
105  {
107  if (ros::Time::now() > start_waiting + ros::Duration(WAIT_FOR_STOP_TIMEOUT_S))
108  {
109  break;
110  }
111  }
112  client_thread_.join();
113 }
114 
121 class PilzModbusClientTests : public testing::Test, public testing::AsyncTest
122 {
123 public:
124  void SetUp() override;
125 
126 protected:
128  ros::AsyncSpinner spinner_{ 2 };
130 
131  MOCK_METHOD1(modbus_read_cb, void(const ModbusMsgInStampedConstPtr& msg));
132 };
133 
135 {
136  subscriber_ = nh_.subscribe<ModbusMsgInStamped>(prbt_hardware_support::TOPIC_MODBUS_READ, 1,
137  &PilzModbusClientTests::modbus_read_cb, this);
138  spinner_.start();
139 }
140 
141 MATCHER_P(IsSuccessfullRead, vec, "")
142 {
143  return arg->holding_registers.data == vec;
144 }
145 MATCHER(IsDisconnect, "")
146 {
147  return arg->disconnect.data;
148 }
149 
153 TEST_F(PilzModbusClientTests, testInitialization)
154 {
155  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
156 
157  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
158 
159  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
160  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
161 
162  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
164 
165  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
166 }
167 
171 TEST_F(PilzModbusClientTests, testInitializationWithRetry)
172 {
173  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
174 
175  EXPECT_CALL(*mock, init(_, _)).Times(2).WillOnce(Return(false)).WillOnce(Return(true));
176 
177  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
178  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
179 
180  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
182 
183  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST, REGISTER_SIZE_TEST, ros::Duration(0.1)));
184 }
185 
189 TEST_F(PilzModbusClientTests, doubleInitialization)
190 {
191  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
192 
193  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
194 
195  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
196  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
197 
198  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
200 
201  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
202  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
203 }
204 
208 TEST_F(PilzModbusClientTests, failingInitialization)
209 {
210  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
211 
212  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(false));
213 
214  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
215  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
216 
217  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
219 
220  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
221 }
222 
226 TEST_F(PilzModbusClientTests, failingInitializationWithRetry)
227 {
228  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
229 
230  EXPECT_CALL(*mock, init(_, _)).Times(2).WillOnce(Return(false)).WillOnce(Return(false));
231 
232  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
233  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
234 
235  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
237 
238  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST, REGISTER_SIZE_TEST, ros::Duration(0.1)));
239 }
240 
244 TEST_F(PilzModbusClientTests, properReadingAndDisconnect)
245 {
246  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
247 
248  {
249  InSequence s;
250  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
251  EXPECT_CALL(*mock, readHoldingRegister(_, _))
252  .WillOnce(Return(std::vector<uint16_t>{ 1, 2 }))
253  .WillOnce(Return(std::vector<uint16_t>{ 1, 2 }))
254  .WillOnce(Return(std::vector<uint16_t>{ 3, 4 }))
255  .WillOnce(Throw(ModbusExceptionDisconnect("disconnect_message")));
256  }
257  {
258  InSequence s;
259  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{ 1, 2 }))).Times(2);
260  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{ 3, 4 }))).Times(1);
261  EXPECT_CALL(*this, modbus_read_cb(IsDisconnect())).Times(1).WillOnce(ACTION_OPEN_BARRIER_VOID("disconnected"));
262  }
263 
264  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
265  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
266 
267  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
269 
270  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
271  EXPECT_NO_THROW(client.run());
272  BARRIER("disconnected");
273 }
274 
278 TEST_F(PilzModbusClientTests, runningWithoutInit)
279 {
280  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
281 
282  EXPECT_CALL(*mock, init(_, _)).Times(0);
283  EXPECT_CALL(*mock, readHoldingRegister(_, _)).Times(0);
284  EXPECT_CALL(*this, modbus_read_cb(_)).Times(0);
285 
286  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
287  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
288 
289  PilzModbusClient client(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
291 
292  EXPECT_THROW(client.run(), PilzModbusClientException);
293 }
294 
298 TEST_F(PilzModbusClientTests, terminateRunningClient)
299 {
300  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
301 
302  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
303  ON_CALL(*mock, readHoldingRegister(_, _)).WillByDefault(Return(std::vector<uint16_t>{ 3, 4 }));
304  ON_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{ 3, 4 }))).WillByDefault(Return());
305 
306  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
307  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
308 
309  auto client = std::make_shared<PilzModbusClient>(nh_, registers, std::move(mock), RESPONSE_TIMEOUT,
312 
313  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
314 
315  PilzModbusClientExecutor executor(client.get());
316  executor.start();
317  EXPECT_TRUE(client->isRunning());
318  executor.stop();
319  EXPECT_FALSE(client->isRunning());
320 }
321 
322 void callbackDummy(const ModbusMsgInStampedConstPtr& /*msg*/)
323 {
324 }
325 
329 TEST_F(PilzModbusClientTests, testTopicNameChange)
330 {
331  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
332 
333  const std::string topic_name{ "test_topic_name" };
334 
335  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
336  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
337 
338  auto client = std::make_shared<PilzModbusClient>(nh_, registers, std::move(mock), RESPONSE_TIMEOUT, topic_name,
340  // Wait for a moment to ensure the topic is "up and running"
342  ros::Subscriber sub = nh_.subscribe<ModbusMsgInStamped>(topic_name, 1, &callbackDummy);
343  ASSERT_EQ(1u, sub.getNumPublishers());
344 }
345 
349 TEST_F(PilzModbusClientTests, testSettingOfTimeOut)
350 {
351  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
352 
353  const unsigned int response_timeout{ RESPONSE_TIMEOUT + 4 };
354 
355  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
356 
357  EXPECT_CALL(*mock, setResponseTimeoutInMs(response_timeout)).Times(1);
358 
359  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
360  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
361 
362  auto client = std::make_shared<PilzModbusClient>(nh_, registers, std::move(mock), response_timeout,
365 
366  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
367 }
368 
374 {
375 public:
376  HoldingRegisterIncreaser(unsigned int register_index = 0u) : register_index_(register_index)
377  {
378  }
379 
380 public:
381  RegCont readHoldingRegister(int /*addr*/, int nb)
382  {
383  RegCont vec(static_cast<RegCont::size_type>(nb), 0u);
384  vec.at(register_index_) = curr_count_;
385  ++curr_count_;
386  return vec;
387  }
388 
389  void reset()
390  {
391  curr_count_ = 0;
392  }
393 
394 private:
395  const unsigned int register_index_;
396  uint16_t curr_count_{ 0 };
397 };
398 
400 {
401 public:
402  void add(const ModbusMsgInStampedConstPtr& msg)
403  {
404  std::unique_lock<std::mutex> lk(m_);
405  buffer_ = msg->holding_registers.data.at(0);
406  }
407 
408  uint16_t get()
409  {
410  std::unique_lock<std::mutex> lk(m_);
411  return buffer_;
412  }
413 
414 private:
415  std::mutex m_;
416  uint16_t buffer_;
417 };
418 
434 TEST_F(PilzModbusClientTests, testSettingReadFrequency)
435 {
436  std::vector<double> checked_frequencies{ 20.0, 40.0, 60.0 };
437 
438  for (const auto& expected_read_frequency : checked_frequencies)
439  {
440  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
441 
442  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
443 
444  HoldingRegisterIncreaser fake_holding_register;
445  ON_CALL(*mock, readHoldingRegister(_, _))
446  .WillByDefault(Invoke(&fake_holding_register, &HoldingRegisterIncreaser::readHoldingRegister));
447 
448  RegisterBuffer buffer;
449  ON_CALL(*this, modbus_read_cb(_)).WillByDefault(Invoke(&buffer, &RegisterBuffer::add));
450 
451  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
452  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
453 
454  auto client = std::make_shared<PilzModbusClient>(
455  nh_, registers, std::move(mock), RESPONSE_TIMEOUT, prbt_hardware_support::TOPIC_MODBUS_READ,
456  prbt_hardware_support::SERVICE_MODBUS_WRITE, expected_read_frequency);
457 
458  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
459 
460  PilzModbusClientExecutor executor(client.get());
461  auto time_start = ros::Time::now();
462  executor.start();
463 
464  ASSERT_TRUE(client->isRunning());
465 
466  // Time for measuring
467  ros::Duration(1).sleep();
468 
469  auto final_value = buffer.get();
470  auto time_stop = ros::Time::now();
471  executor.stop();
472 
473  // Checks
474  auto actual_frequency = (final_value - 1) / (time_stop - time_start).toSec();
475  EXPECT_NEAR(expected_read_frequency, actual_frequency, 0.1 * expected_read_frequency);
476 
477  EXPECT_FALSE(client->isRunning());
478  }
479 }
480 
485 TEST_F(PilzModbusClientTests, testWritingOfHoldingRegister)
486 {
487  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
488 
489  EXPECT_CALL(*mock, init(_, _)).Times(1).WillOnce(Return(true));
490 
491  ON_CALL(*mock, readHoldingRegister(_, _)).WillByDefault(Return(std::vector<uint16_t>{ 1, 7 }));
492 
493  // Data to be expected
494  RegCont expected_write_reg{ 1, 5, 4 };
495  WriteModbusRegister reg_write_srv;
496  reg_write_srv.request.holding_register_block.start_idx = 3;
497  reg_write_srv.request.holding_register_block.values = expected_write_reg;
498 
499  RegCont expected_read_reg{ 14, 17 };
500 
501  EXPECT_CALL(*mock, writeReadHoldingRegister(static_cast<int>(reg_write_srv.request.holding_register_block.start_idx),
502  expected_write_reg, REGISTER_FIRST_IDX_TEST,
503  static_cast<int>(expected_read_reg.size())))
504  .Times(1)
505  .WillOnce(DoAll(ACTION_OPEN_BARRIER_VOID("writeHoldingRegister"), Return(expected_read_reg)));
506 
507  EXPECT_CALL(*this, modbus_read_cb(_)).Times(AnyNumber());
508 
509  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(expected_read_reg))).Times(AtLeast(1));
510 
511  std::vector<unsigned short> registers(expected_read_reg.size());
512  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
513 
514  auto modbus_client = std::make_shared<PilzModbusClient>(nh_, registers, std::move(mock), RESPONSE_TIMEOUT,
517 
518  EXPECT_TRUE(modbus_client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST)) << "Initialization of modbus client failed";
519 
520  ros::ServiceClient writer_client =
521  nh_.serviceClient<WriteModbusRegister>(prbt_hardware_support::SERVICE_MODBUS_WRITE);
522  ASSERT_TRUE(writer_client.waitForExistence(ros::Duration(WAIT_FOR_SERVICE_TIMEOUT_S))) << "Modbus write service was "
523  "not advertised in time";
524 
525  PilzModbusClientExecutor executor(modbus_client.get());
526  executor.start();
527  EXPECT_TRUE(modbus_client->isRunning()) << "Modbus client not running";
528 
529  EXPECT_TRUE(writer_client.call(reg_write_srv)) << "Modbus write service failed";
530  EXPECT_TRUE(reg_write_srv.response.success) << "Modbus write service could not write modbus register";
531 
532  BARRIER("writeHoldingRegister");
533 
534  executor.stop();
535  EXPECT_FALSE(modbus_client->isRunning());
536 }
537 
541 TEST_F(PilzModbusClientTests, testSplitIntoBlocksFcn)
542 {
543  // lists with same elements will throw exception
544  std::vector<unsigned short> in_throw = { 1, 2, 1 };
546 
547  // properly splitting into blocks
548  std::vector<unsigned short> in_split = { 1, 2, 4, 5 };
549  std::vector<std::vector<unsigned short>> out_split = PilzModbusClient::splitIntoBlocks(in_split);
550  std::vector<std::vector<unsigned short>> expected_out_split = { { 1, 2 }, { 4, 5 } };
551  EXPECT_EQ(out_split, expected_out_split);
552 
553  // not splitting already consecutive blocks
554  std::vector<unsigned short> in_nosplit = { 1, 2, 3 };
555  std::vector<std::vector<unsigned short>> out_nosplit = PilzModbusClient::splitIntoBlocks(in_nosplit);
556  std::vector<std::vector<unsigned short>> expected_out_nosplit = { { 1, 2, 3 } };
557  EXPECT_EQ(out_nosplit, expected_out_nosplit);
558 }
559 } // namespace pilz_modbus_client_test
560 
561 int main(int argc, char* argv[])
562 {
563  ros::init(argc, argv, "pilz_modbus_client_test");
564  ros::NodeHandle nh;
565 
566  testing::InitGoogleTest(&argc, argv);
567  return RUN_ALL_TESTS();
568 }
void run()
Publishes the register values as messages.
ROSCPP_DECL void start()
std::vector< uint16_t > RegCont
Convenience data type defining the data type for a collection of registers.
int main(int argc, char *argv[])
void add(const ModbusMsgInStampedConstPtr &msg)
Expection thrown by prbt_hardware_support::PilzModbusClient.
Subscriber subscribe(const std::string &topic, uint32_t queue_size, void(T::*fp)(M), T *obj, const TransportHints &transport_hints=TransportHints())
static constexpr unsigned int REGISTER_FIRST_IDX_TEST
void init(const M_string &remappings)
void callbackDummy(const ModbusMsgInStampedConstPtr &)
XmlRpcServer s
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
uint32_t getNumPublishers() const
static const std::string SERVICE_MODBUS_WRITE
static std::vector< std::vector< unsigned short > > splitIntoBlocks(std::vector< unsigned short > &in)
Splits a vector of integers into a vector of vectors with consecutive groups.
static constexpr double WAIT_FOR_START_TIMEOUT_S
static constexpr unsigned int REGISTER_SIZE_TEST
bool isRunning()
True if &#39;run()&#39; method is active, false if &#39;run()&#39; method is not active or, currently, finishing.
static constexpr double WAIT_SLEEPTIME_S
Test if PilzModbusClient correctly publishes ROS-Modbus messages.
static const std::string TOPIC_MODBUS_READ
static constexpr double WAIT_FOR_STOP_TIMEOUT_S
static constexpr double WAIT_FOR_SERVICE_TIMEOUT_S
Mock used in the unittest of the PilzModbusClient.
TEST_F(LibModbusClientTest, testModbusClientDtor)
Test increases function coverage by ensuring that all Dtor variants are called.
Connects to a modbus server and publishes the received data into ROS.
Strictly increases the holding register each time the "readHoldingRegister" method is called...
Helper class to simplify the threading of the PilzModbusClient.
MATCHER_P(IsSuccessfullRead, vec, "")
static constexpr unsigned int DEFAULT_MODBUS_PORT_TEST
static Time now()
bool sleep() const
constexpr const char *const LOCALHOST
Expection thrown by prbt_hardware_support::LibModbusClient::readHoldingRegister if a disconnect from ...
void terminate()
Ends the infinite loop started in method &#39;run()&#39;.
static constexpr unsigned int RESPONSE_TIMEOUT


prbt_hardware_support
Author(s):
autogenerated on Mon Feb 28 2022 23:14:34