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::Return;
48 using ::testing::Throw;
49 using ::testing::InSequence;
50 using ::testing::InvokeWithoutArgs;
51 using ::testing::Invoke;
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  : client_(client)
83 {
84 }
85 
87 {
89 
90  ros::Time start_waiting = ros::Time::now();
91  while (!client_->isRunning())
92  {
94  if (ros::Time::now() > start_waiting + ros::Duration(WAIT_FOR_START_TIMEOUT_S))
95  {
96  break;
97  }
98  }
99 }
100 
102 {
103  client_->terminate();
104  ros::Time start_waiting = ros::Time::now();
105  while (client_->isRunning())
106  {
108  if (ros::Time::now() > start_waiting + ros::Duration(WAIT_FOR_STOP_TIMEOUT_S))
109  {
110  break;
111  }
112  }
113  client_thread_.join();
114 }
115 
122 class PilzModbusClientTests : public testing::Test, public testing::AsyncTest
123 {
124 public:
125  virtual void SetUp();
126 
127 protected:
129  ros::AsyncSpinner spinner_{2};
131 
132  MOCK_METHOD1(modbus_read_cb, void(ModbusMsgInStamped msg));
133 
134 };
135 
137 {
138  subscriber_ = nh_.subscribe<ModbusMsgInStamped>(prbt_hardware_support::TOPIC_MODBUS_READ, 1, &PilzModbusClientTests::modbus_read_cb, this);
139  spinner_.start();
140 }
141 
142 MATCHER_P(IsSuccessfullRead, vec, "") { return arg.holding_registers.data == vec; }
143 MATCHER(IsDisconnect, "") { return arg.disconnect.data; }
144 
145 
149 TEST_F(PilzModbusClientTests, testInitialization)
150 {
151  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
152 
153  EXPECT_CALL(*mock, init(_,_))
154  .Times(1)
155  .WillOnce(Return(true));
156 
157  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
158  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
159 
160  PilzModbusClient client(nh_, registers, std::move(mock),
163 
164  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
165 }
166 
170 TEST_F(PilzModbusClientTests, testInitializationWithRetry)
171 {
172  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
173 
174  EXPECT_CALL(*mock, init(_,_))
175  .Times(2)
176  .WillOnce(Return(false))
177  .WillOnce(Return(true));
178 
179  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
180  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
181 
182  PilzModbusClient client(nh_,registers,std::move(mock),
185 
186  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST, REGISTER_SIZE_TEST, ros::Duration(0.1)));
187 }
188 
192 TEST_F(PilzModbusClientTests, doubleInitialization)
193 {
194  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
195 
196  EXPECT_CALL(*mock, init(_,_))
197  .Times(1)
198  .WillOnce(Return(true));
199 
200  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
201  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
202 
203  PilzModbusClient client(nh_,registers,std::move(mock),
206 
207  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
208  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
209 }
210 
214 TEST_F(PilzModbusClientTests, failingInitialization)
215 {
216  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
217 
218  EXPECT_CALL(*mock, init(_,_))
219  .Times(1)
220  .WillOnce(Return(false));
221 
222  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
223  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
224 
225  PilzModbusClient client(nh_,registers,std::move(mock),
228 
229  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
230 }
231 
235 TEST_F(PilzModbusClientTests, failingInitializationWithRetry)
236 {
237  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
238 
239  EXPECT_CALL(*mock, init(_,_))
240  .Times(2)
241  .WillOnce(Return(false))
242  .WillOnce(Return(false));
243 
244  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
245  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
246 
247  PilzModbusClient client(nh_,registers,std::move(mock),
250 
251  EXPECT_FALSE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST, REGISTER_SIZE_TEST, ros::Duration(0.1)));
252 }
253 
257 TEST_F(PilzModbusClientTests, properReadingAndDisconnect)
258 {
259  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
260 
261  {
262  InSequence s;
263  EXPECT_CALL(*mock, init(_,_))
264  .Times(1)
265  .WillOnce(Return(true));
266  EXPECT_CALL(*mock, readHoldingRegister(_,_))
267  .WillOnce(Return(std::vector<uint16_t>{1, 2}))
268  .WillOnce(Return(std::vector<uint16_t>{1, 2}))
269  .WillOnce(Return(std::vector<uint16_t>{3, 4}))
270  .WillOnce(Throw(ModbusExceptionDisconnect("disconnect_message")));
271  }
272  {
273  InSequence s;
274  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{1, 2})))
275  .Times(2);
276  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{3, 4})))
277  .Times(1);
278  EXPECT_CALL(*this, modbus_read_cb(IsDisconnect()))
279  .Times(1)
280  .WillOnce(ACTION_OPEN_BARRIER_VOID("disconnected"));
281  }
282 
283  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
284  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
285 
286  PilzModbusClient client(nh_,registers,std::move(mock),
289 
290  EXPECT_TRUE(client.init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
291  EXPECT_NO_THROW(client.run());
292  BARRIER("disconnected");
293 }
294 
298 TEST_F(PilzModbusClientTests, runningWithoutInit)
299 {
300  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
301 
302  EXPECT_CALL(*mock, init(_,_)).Times(0);
303  EXPECT_CALL(*mock, readHoldingRegister(_,_)).Times(0);
304  EXPECT_CALL(*this, modbus_read_cb(_)).Times(0);
305 
306  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
307  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
308 
309  PilzModbusClient client(nh_,registers,std::move(mock),
312 
313  EXPECT_THROW(client.run(), PilzModbusClientException);
314 }
315 
319 TEST_F(PilzModbusClientTests, terminateRunningClient)
320 {
321  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
322 
323  EXPECT_CALL(*mock, init(_,_))
324  .Times(1)
325  .WillOnce(Return(true));
326  ON_CALL(*mock, readHoldingRegister(_,_)).WillByDefault(Return(std::vector<uint16_t>{3, 4}));
327  ON_CALL(*this, modbus_read_cb(IsSuccessfullRead(std::vector<uint16_t>{3,4})))
328  .WillByDefault(Return());
329 
330  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
331  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
332 
333  auto client = std::make_shared< PilzModbusClient >(nh_,registers,std::move(mock),
336 
337  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
338 
339  PilzModbusClientExecutor executor(client.get());
340  executor.start();
341  EXPECT_TRUE(client->isRunning());
342  executor.stop();
343  EXPECT_FALSE(client->isRunning());
344 }
345 
346 void callbackDummy(const ModbusMsgInStampedConstPtr& /*msg*/) {}
347 
351 TEST_F(PilzModbusClientTests, testTopicNameChange)
352 {
353  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
354 
355  const std::string topic_name {"test_topic_name"};
356 
357  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
358  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
359 
360  auto client = std::make_shared< PilzModbusClient >(nh_,registers,std::move(mock),
362  // Wait for a moment to ensure the topic is "up and running"
364  ros::Subscriber sub = nh_.subscribe<ModbusMsgInStamped>(topic_name, 1, &callbackDummy);
365  ASSERT_EQ(1u, sub.getNumPublishers());
366 }
367 
371 TEST_F(PilzModbusClientTests, testSettingOfTimeOut)
372 {
373  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
374 
375  const unsigned int response_timeout {RESPONSE_TIMEOUT + 4};
376 
377  EXPECT_CALL(*mock, init(_,_))
378  .Times(1)
379  .WillOnce(Return(true));
380 
381  EXPECT_CALL(*mock, setResponseTimeoutInMs(response_timeout)).Times(1);
382 
383  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
384  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
385 
386  auto client = std::make_shared< PilzModbusClient >(nh_,registers,std::move(mock),
389 
390  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
391 }
392 
398 {
399 public:
400  HoldingRegisterIncreaser(unsigned int register_index = 0u)
401  : register_index_(register_index)
402  {}
403 
404 public:
405  RegCont readHoldingRegister(int /*addr*/, int nb)
406  {
407  RegCont vec( static_cast<RegCont::size_type>(nb) , 0u);
408  vec.at(register_index_) = curr_count_;
409  ++curr_count_;
410  return vec;
411  }
412 
413  void reset()
414  {
415  curr_count_ = 0;
416  }
417 
418 private:
419  const unsigned int register_index_;
420  uint16_t curr_count_ {0};
421 };
422 
424 {
425 public:
426  void add(ModbusMsgInStamped msg)
427  {
428  std::unique_lock<std::mutex> lk(m_);
429  buffer_ = msg.holding_registers.data.at(0);
430  }
431 
432  uint16_t get()
433  {
434  std::unique_lock<std::mutex> lk(m_);
435  return buffer_;
436  }
437 
438 private:
439  std::mutex m_;
440  uint16_t buffer_;
441 };
442 
458 TEST_F(PilzModbusClientTests, testSettingReadFrequency)
459 {
460  std::vector<double> checked_frequencies {20.0, 40.0, 60.0};
461 
462  for(const auto& expected_read_frequency : checked_frequencies)
463  {
464  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
465 
466  EXPECT_CALL(*mock, init(_,_))
467  .Times(1)
468  .WillOnce(Return(true));
469 
470  HoldingRegisterIncreaser fake_holding_register;
471  ON_CALL(*mock, readHoldingRegister(_, _))
472  .WillByDefault(Invoke(&fake_holding_register, &HoldingRegisterIncreaser::readHoldingRegister));
473 
474 
475  RegisterBuffer buffer;
476  ON_CALL(*this, modbus_read_cb(_))
477  .WillByDefault(Invoke(&buffer, &RegisterBuffer::add));
478 
479  std::vector<unsigned short> registers(REGISTER_SIZE_TEST);
480  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
481 
482  auto client = std::make_shared< PilzModbusClient >(nh_,registers,std::move(mock),
485  expected_read_frequency);
486 
487  EXPECT_TRUE(client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST));
488 
489  PilzModbusClientExecutor executor(client.get());
490  auto time_start = ros::Time::now();
491  executor.start();
492 
493  ASSERT_TRUE(client->isRunning());
494 
495  // Time for measuring
496  ros::Duration(1).sleep();
497 
498  auto final_value = buffer.get();
499  auto time_stop = ros::Time::now();
500  executor.stop();
501 
502  // Checks
503  auto actual_frequency = (final_value - 1) / (time_stop - time_start).toSec();
504  EXPECT_NEAR(expected_read_frequency, actual_frequency, 0.1 * expected_read_frequency);
505 
506  EXPECT_FALSE(client->isRunning());
507  }
508 
509 
510 }
511 
516 TEST_F(PilzModbusClientTests, testWritingOfHoldingRegister)
517 {
518  std::unique_ptr<PilzModbusClientMock> mock(new PilzModbusClientMock());
519 
520  EXPECT_CALL(*mock, init(_,_))
521  .Times(1)
522  .WillOnce(Return(true));
523 
524  ON_CALL(*mock, readHoldingRegister(_, _))
525  .WillByDefault(Return(std::vector<uint16_t>{1, 7}));
526 
527  // Data to be expected
528  RegCont expected_write_reg {1, 5, 4};
529  WriteModbusRegister reg_write_srv;
530  reg_write_srv.request.holding_register_block.start_idx = 3;
531  reg_write_srv.request.holding_register_block.values = expected_write_reg;
532 
533  RegCont expected_read_reg {14, 17};
534 
535  EXPECT_CALL( *mock, writeReadHoldingRegister(static_cast<int>(reg_write_srv.request.holding_register_block.start_idx),
536  expected_write_reg,
538  static_cast<int>(expected_read_reg.size())) )
539  .Times(1)
540  .WillOnce(DoAll(ACTION_OPEN_BARRIER_VOID("writeHoldingRegister"), Return(expected_read_reg)));
541 
542  EXPECT_CALL(*this, modbus_read_cb(_))
543  .Times(AnyNumber());
544 
545  EXPECT_CALL(*this, modbus_read_cb(IsSuccessfullRead(expected_read_reg)))
546  .Times(AtLeast(1));
547 
548  std::vector<unsigned short> registers(expected_read_reg.size());
549  std::iota(registers.begin(), registers.end(), REGISTER_FIRST_IDX_TEST);
550 
551  auto modbus_client = std::make_shared< PilzModbusClient >(nh_, registers, std::move(mock),
554 
555  EXPECT_TRUE(modbus_client->init(LOCALHOST, DEFAULT_MODBUS_PORT_TEST)) << "Initialization of modbus client failed";
556 
557  ros::ServiceClient writer_client = nh_.serviceClient<WriteModbusRegister>(prbt_hardware_support::SERVICE_MODBUS_WRITE);
558  ASSERT_TRUE(writer_client.waitForExistence(ros::Duration(WAIT_FOR_SERVICE_TIMEOUT_S))) << "Modbus write service was not advertised in time";
559 
560  PilzModbusClientExecutor executor(modbus_client.get());
561  executor.start();
562  EXPECT_TRUE(modbus_client->isRunning()) << "Modbus client not running";
563 
564  EXPECT_TRUE(writer_client.call(reg_write_srv)) << "Modbus write service failed";
565  EXPECT_TRUE(reg_write_srv.response.success) << "Modbus write service could not write modbus register";
566 
567  BARRIER("writeHoldingRegister");
568 
569  executor.stop();
570  EXPECT_FALSE(modbus_client->isRunning());
571 }
572 
573 
577 TEST_F(PilzModbusClientTests, testSplitIntoBlocksFcn){
578  // lists with same elements will throw exception
579  std::vector<unsigned short> in_throw = {1, 2, 1};
580  ASSERT_THROW(PilzModbusClient::split_into_blocks(in_throw),
582 
583  // properly splitting into blocks
584  std::vector<unsigned short> in_split = {1, 2, 4, 5};
585  std::vector<std::vector<unsigned short>> out_split = PilzModbusClient::split_into_blocks(in_split);
586  std::vector<std::vector<unsigned short>> expected_out_split = {{1, 2}, {4, 5}};
587  EXPECT_EQ(out_split, expected_out_split);
588 
589  // not splitting already consecutive blocks
590  std::vector<unsigned short> in_nosplit = {1, 2, 3};
591  std::vector<std::vector<unsigned short>> out_nosplit = PilzModbusClient::split_into_blocks(in_nosplit);
592  std::vector<std::vector<unsigned short>> expected_out_nosplit = {{1, 2, 3}};
593  EXPECT_EQ(out_nosplit, expected_out_nosplit);
594 }
595 } // namespace pilz_modbus_client_test
596 
597 int main(int argc, char *argv[])
598 {
599  ros::init(argc, argv, "pilz_modbus_client_test");
600  ros::NodeHandle nh;
601 
602  testing::InitGoogleTest(&argc, argv);
603  return RUN_ALL_TESTS();
604 }
void run()
Publishes the register values as messages.
std::vector< uint16_t > RegCont
Convenience data type defining the data type for a collection of registers.
int main(int argc, char *argv[])
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 callbackDummy(const ModbusMsgInStampedConstPtr &)
XmlRpcServer s
bool sleep() const
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
MATCHER_P(IsSuccessfullRead, vec,"")
static const std::string SERVICE_MODBUS_WRITE
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.
uint32_t getNumPublishers() const
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.
static constexpr unsigned int DEFAULT_MODBUS_PORT_TEST
static std::vector< std::vector< unsigned short > > split_into_blocks(std::vector< unsigned short > &in)
Splits a vector of integers into a vector of vectors with consecutive groups.
static Time now()
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 Tue Feb 2 2021 03:50:17