PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_rpcTransport.cpp
Go to the documentation of this file.
1
9
10#include <gtest/gtest.h>
11
12#include <array>
13#include <asio.hpp>
14#include <chrono>
15#include <cstdint>
16#include <memory>
17#include <string>
18#include <system_error>
19#include <thread>
20#include <vector>
21
23#include "profinet/exceptions.h"
25
26using profinet::Bytes;
28using namespace std::chrono_literals;
29
30namespace
31{
32
36std::uint16_t FindFreeUdpPort()
37{
38 asio::io_context ioContext;
39 asio::ip::udp::socket socket(ioContext);
40
41 socket.open(asio::ip::udp::v4());
42 socket.bind(asio::ip::udp::endpoint(asio::ip::udp::v4(), 0));
43
44 return socket.local_endpoint().port();
45}
46
52std::unique_ptr<asio::ip::udp::socket> CreateBoundUdpSocket(asio::io_context& ioContext, std::uint16_t port = 0)
53{
54 auto socket = std::make_unique<asio::ip::udp::socket>(ioContext);
55
56 socket->open(asio::ip::udp::v4());
57 socket->bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), port));
58
59 return socket;
60}
61
66void SendUdpDatagram(std::uint16_t port, const Bytes& data)
67{
68 asio::io_context ioContext;
69 asio::ip::udp::socket socket(ioContext);
70
71 socket.open(asio::ip::udp::v4());
72
73 const asio::ip::udp::endpoint endpoint(asio::ip::address_v4::loopback(), port);
74
75 asio::error_code ec;
76 socket.send_to(asio::buffer(data), endpoint, 0, ec);
77
78 ASSERT_FALSE(ec) << "UDP send failed: " << ec.message();
79}
80
86Bytes ReceiveUdpDatagram(asio::ip::udp::socket& socket, asio::ip::udp::endpoint& remoteEndpoint)
87{
88 std::array<std::uint8_t, profinet::RECEIVE_BUFFER_LENGTH> buffer{};
89 asio::error_code ec;
90
91 const std::size_t receivedBytes = socket.receive_from(asio::buffer(buffer), remoteEndpoint, 0, ec);
92
93 if (ec)
94 {
95 throw std::system_error(ec, "Failed to receive test UDP datagram");
96 }
97
98 return Bytes(buffer.begin(), buffer.begin() + receivedBytes);
99}
100
102class UdpEchoServer
103{
104 public:
106 UdpEchoServer()
107 : socket(ioContext)
108 {
109 socket.open(asio::ip::udp::v4());
110 socket.bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), 0));
111 port = socket.local_endpoint().port();
112 }
113
115 ~UdpEchoServer()
116 {
117 Stop();
118 }
119
120 UdpEchoServer(const UdpEchoServer&) = delete;
121 UdpEchoServer& operator=(const UdpEchoServer&) = delete;
122
126 [[nodiscard]] std::uint16_t Port() const
127 {
128 return port;
129 }
130
132 void Start()
133 {
134 Receive();
135
136 thread = std::thread(
137 [this]
138 {
139 ioContext.run();
140 });
141 }
142
144 void Stop()
145 {
146 asio::error_code ec;
147
148 socket.cancel(ec);
149 socket.close(ec);
150
151 ioContext.stop();
152
153 if (thread.joinable())
154 {
155 thread.join();
156 }
157 }
158
159 private:
161 void Receive()
162 {
163 socket.async_receive_from(asio::buffer(receiveBuffer),
164 remoteEndpoint,
165 [&](const asio::error_code& ec, std::size_t bytesReceived)
166 {
167 if (ec)
168 {
169 return;
170 }
171
172 socket.async_send_to(asio::buffer(receiveBuffer.data(), bytesReceived),
173 remoteEndpoint,
174 [&](const asio::error_code& sendEc, std::size_t)
175 {
176 if (sendEc)
177 {
178 return;
179 }
180
181 Receive();
182 });
183 });
184 }
185
186 asio::io_context ioContext;
187 asio::ip::udp::socket socket;
188 asio::ip::udp::endpoint remoteEndpoint;
189 std::array<std::uint8_t, profinet::RECEIVE_BUFFER_LENGTH> receiveBuffer{};
190 std::uint16_t port{0};
191 std::thread thread;
192};
193
194} // namespace
195
196class RpcTransportTests : public ::testing::Test
197{
198 protected:
200 void SetUp() override
201 {
202 ioContext = std::make_unique<asio::io_context>();
203 runner = std::make_unique<profinet::AsioIoContextRunner>(*ioContext);
204
205 // workGuard = std::make_unique<asio::executor_work_guard<asio::io_context::executor_type>>(asio::make_work_guard(*ioContext));
206
207 // worker = std::jthread([&]
208 // {
209 // ioContext->run();
210 // });
211 }
212
214 void TearDown() override
215 {
216 // workGuard.reset();
217 // ioContext->stop();
218
219 // if (worker.joinable())
220 //{
221 // worker.join();
222 // }
223 runner.reset();
224 ioContext.reset();
225 }
226
228 std::unique_ptr<asio::io_context> ioContext;
229
230 // std::unique_ptr<asio::executor_work_guard<asio::io_context::executor_type>> workGuard;
231 // std::jthread worker;
232
233 std::unique_ptr<profinet::AsioIoContextRunner> runner;
234};
235
237TEST_F(RpcTransportTests, ConstructorCreatesClosedTransport)
238{
239 RpcTransport transport(*ioContext);
240
241 EXPECT_FALSE(transport.IsOpen());
242}
243
245TEST_F(RpcTransportTests, OpenCreatesUdpSocket)
246{
247 RpcTransport transport(*ioContext);
248
249 ASSERT_FALSE(transport.IsOpen());
250
251 transport.Open("127.0.0.1", 34964);
252
253 EXPECT_TRUE(transport.IsOpen());
254}
255
257TEST_F(RpcTransportTests, OpenListenerBindsRequestedPort)
258{
259 const std::uint16_t port = FindFreeUdpPort();
260
261 RpcTransport transport(*ioContext);
262
263 transport.Open("127.0.0.1", port, true);
264
265 EXPECT_TRUE(transport.IsOpen());
266}
267
269TEST_F(RpcTransportTests, OpenRejectsInvalidIpv4Address)
270{
271 RpcTransport transport(*ioContext);
272
273 EXPECT_THROW(transport.Open("not-an-ip-address", 34964), std::system_error);
274
275 EXPECT_FALSE(transport.IsOpen());
276}
277
279TEST_F(RpcTransportTests, OpenListenerRejectsOccupiedPort)
280{
281 asio::io_context occupiedIoContext;
282 asio::ip::udp::socket occupiedSocket(occupiedIoContext);
283
284 occupiedSocket.open(asio::ip::udp::v4());
285 occupiedSocket.bind(asio::ip::udp::endpoint(asio::ip::address_v4::any(), 0));
286
287 const std::uint16_t port = occupiedSocket.local_endpoint().port();
288
289 RpcTransport transport(*ioContext);
290
291 EXPECT_THROW(transport.Open("127.0.0.1", port, true), std::system_error);
292
293 EXPECT_FALSE(transport.IsOpen());
294}
295
297TEST_F(RpcTransportTests, OpenUpdatesEndpointWhenTransportIsAlreadyOpen)
298{
299 auto receiver = CreateBoundUdpSocket(*ioContext);
300 const std::uint16_t receiverPort = receiver->local_endpoint().port();
301
302 const std::uint16_t unusedPort = FindFreeUdpPort();
303
304 RpcTransport transport(*ioContext);
305
306 transport.Open("127.0.0.1", unusedPort);
307
308 ASSERT_TRUE(transport.IsOpen());
309
310 transport.Open("127.0.0.1", receiverPort);
311
312 const Bytes request{0x10, 0x20, 0x30, 0x40};
313
314 ASSERT_NO_THROW(transport.Send(request));
315
316 asio::ip::udp::endpoint senderEndpoint;
317 const Bytes received = ReceiveUdpDatagram(*receiver, senderEndpoint);
318
319 EXPECT_EQ(received, request);
320}
321
323TEST_F(RpcTransportTests, ReceiveReturnsDatagramAndSenderEndpoint)
324{
325 const std::uint16_t port = FindFreeUdpPort();
326
327 RpcTransport transport(*ioContext);
328 transport.Open("127.0.0.1", port, true);
329
330 const Bytes expected{0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB};
331
332 SendUdpDatagram(port, expected);
333
334 asio::ip::udp::endpoint remoteEndpoint;
335 const Bytes received = transport.Receive(1000ms, remoteEndpoint);
336
337 EXPECT_EQ(received, expected);
338 EXPECT_EQ(remoteEndpoint.address(), asio::ip::address_v4::loopback());
339 EXPECT_NE(remoteEndpoint.port(), 0);
340}
341
343TEST_F(RpcTransportTests, ReceiveFailsWhenTransportIsClosed)
344{
345 RpcTransport transport(*ioContext);
346
347 asio::ip::udp::endpoint remoteEndpoint;
348
349 EXPECT_THROW(transport.Receive(100ms, remoteEndpoint), std::system_error);
350}
351
353TEST_F(RpcTransportTests, ReceiveRejectsZeroTimeout)
354{
355 const std::uint16_t port = FindFreeUdpPort();
356
357 RpcTransport transport(*ioContext);
358 transport.Open("127.0.0.1", port, true);
359
360 asio::ip::udp::endpoint remoteEndpoint;
361
362 EXPECT_THROW(transport.Receive(0ms, remoteEndpoint), std::system_error);
363}
364
366TEST_F(RpcTransportTests, ReceiveRejectsNegativeTimeout)
367{
368 const std::uint16_t port = FindFreeUdpPort();
369
370 RpcTransport transport(*ioContext);
371 transport.Open("127.0.0.1", port, true);
372
373 asio::ip::udp::endpoint remoteEndpoint;
374
375 EXPECT_THROW(transport.Receive(-1ms, remoteEndpoint), std::system_error);
376}
377
379TEST_F(RpcTransportTests, ReceiveThrowsOnTimeout)
380{
381 const std::uint16_t port = FindFreeUdpPort();
382
383 RpcTransport transport(*ioContext);
384 transport.Open("127.0.0.1", port, true);
385
386 asio::ip::udp::endpoint remoteEndpoint;
387
388 EXPECT_THROW(transport.Receive(20ms, remoteEndpoint), profinet::RPCTimeoutError);
389}
390
392TEST_F(RpcTransportTests, ReceiveThrowsSystemErrorWhenSocketIsCancelled)
393{
394 const std::uint16_t port = FindFreeUdpPort();
395
396 RpcTransport transport(*ioContext);
397 transport.Open("127.0.0.1", port, true);
398
399 asio::ip::udp::endpoint remoteEndpoint;
400
401 asio::steady_timer closeTimer(*ioContext);
402 closeTimer.expires_after(1ms);
403 closeTimer.async_wait([&transport](const asio::error_code& ec)
404 {
405 if (!ec)
406 {
407 transport.Close();
408 }
409 });
410
411 EXPECT_THROW(transport.Receive(500ms, remoteEndpoint), std::system_error);
412}
413
415TEST_F(RpcTransportTests, SendToTransmitsDatagramToSuppliedEndpoint)
416{
417 auto receiver = CreateBoundUdpSocket(*ioContext);
418 const std::uint16_t receiverPort = receiver->local_endpoint().port();
419
420 RpcTransport transport(*ioContext);
421 transport.Open("127.0.0.1", FindFreeUdpPort());
422
423 const Bytes request{0x10, 0x20, 0x30, 0x40};
424 const asio::ip::udp::endpoint target(asio::ip::address_v4::loopback(), receiverPort);
425
426 ASSERT_NO_THROW(transport.SendTo(request, target));
427
428 asio::ip::udp::endpoint senderEndpoint;
429 const Bytes received = ReceiveUdpDatagram(*receiver, senderEndpoint);
430
431 EXPECT_EQ(received, request);
432 EXPECT_EQ(senderEndpoint.address(), asio::ip::address_v4::loopback());
433 EXPECT_NE(senderEndpoint.port(), 0);
434}
435
437TEST_F(RpcTransportTests, SendToFailsWhenTransportIsClosed)
438{
439 RpcTransport transport(*ioContext);
440
441 const Bytes request{0x01, 0x02, 0x03};
442 const asio::ip::udp::endpoint target(asio::ip::address_v4::loopback(), 34964);
443
444 EXPECT_THROW(transport.SendTo(request, target), std::system_error);
445}
446
448TEST_F(RpcTransportTests, SendToReportsUnderlyingSocketError)
449{
450 RpcTransport transport(*ioContext);
451 transport.Open("127.0.0.1", FindFreeUdpPort());
452
453 const Bytes request{0x01, 0x02, 0x03};
454 const asio::ip::udp::endpoint target(asio::ip::address_v6::loopback(), 34964);
455
456 EXPECT_THROW(transport.SendTo(request, target), std::system_error);
457}
458
460TEST_F(RpcTransportTests, SendReceiveReturnsEchoResponse)
461{
462 UdpEchoServer server;
463 server.Start();
464
465 RpcTransport transport(*ioContext);
466 transport.Open("127.0.0.1", server.Port());
467
468 const Bytes request{0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB};
469
470 const Bytes response = transport.SendReceive(request, 1000ms);
471
472 EXPECT_EQ(response, request);
473
474 server.Stop();
475}
484TEST_F(RpcTransportTests, SendReceiveUsesExternallyDrivenIoContext)
485{
486 UdpEchoServer server;
487 server.Start();
488
489 asio::io_context ioContext;
491
492 RpcTransport transport(ioContext);
493 transport.Open("127.0.0.1", server.Port());
494
495 const Bytes request{
496 0x10,
497 0x20,
498 0x30,
499 0x40,
500 0xAA,
501 0xBB};
502
503 const Bytes response = transport.SendReceive(request, 1000ms);
504
505 EXPECT_EQ(response, request);
506
507 server.Stop();
508}
509
511TEST_F(RpcTransportTests, SendReceiveThrowsOnTimeout)
512{
513 auto silentServer = CreateBoundUdpSocket(*ioContext);
514 const std::uint16_t port = silentServer->local_endpoint().port();
515
516 RpcTransport transport(*ioContext);
517 transport.Open("127.0.0.1", port);
518
519 const Bytes request{0x01, 0x02, 0x03};
520
521 EXPECT_THROW(transport.SendReceive(request, 20ms), profinet::RPCTimeoutError);
522}
523
525TEST_F(RpcTransportTests, SendReceiveFailsWhenTransportIsClosed)
526{
527 RpcTransport transport(*ioContext);
528
529 const Bytes request{0x01, 0x02, 0x03};
530
531 EXPECT_THROW(transport.SendReceive(request, 100ms), std::system_error);
532}
533
535TEST_F(RpcTransportTests, SendReceiveRejectsZeroTimeout)
536{
537 UdpEchoServer server;
538 server.Start();
539
540 RpcTransport transport(*ioContext);
541 transport.Open("127.0.0.1", server.Port());
542
543 const Bytes request{0x01, 0x02, 0x03};
544
545 EXPECT_THROW(transport.SendReceive(request, 0ms), std::system_error);
546
547 server.Stop();
548}
549
551TEST_F(RpcTransportTests, SendReceiveRejectsNegativeTimeout)
552{
553 UdpEchoServer server;
554 server.Start();
555
556 RpcTransport transport(*ioContext);
557 transport.Open("127.0.0.1", server.Port());
558
559 const Bytes request{0x01, 0x02, 0x03};
560
561 EXPECT_THROW(transport.SendReceive(request, -1ms), std::system_error);
562
563 server.Stop();
564}
565
567TEST_F(RpcTransportTests, SendReceiveThrowsSystemErrorWhenSocketIsCancelled)
568{
569 auto silentServer = CreateBoundUdpSocket(*ioContext);
570 const std::uint16_t port = silentServer->local_endpoint().port();
571
572 RpcTransport transport(*ioContext);
573 transport.Open("127.0.0.1", port);
574
575 const Bytes request{0x01, 0x02, 0x03};
576
577 asio::steady_timer closeTimer(*ioContext);
578 closeTimer.expires_after(1ms);
579 closeTimer.async_wait(
580 [&transport](const asio::error_code& ec)
581 {
582 if (!ec)
583 {
584 transport.Close();
585 }
586 });
587
588 EXPECT_THROW(transport.SendReceive(request, 500ms), std::system_error);
589}
590
592TEST_F(RpcTransportTests, SendTransmitsDatagramWithoutWaiting)
593{
594 auto receiver = CreateBoundUdpSocket(*ioContext);
595 const std::uint16_t receiverPort = receiver->local_endpoint().port();
596
597 RpcTransport transport(*ioContext);
598 transport.Open("127.0.0.1", receiverPort);
599
600 const Bytes request{0x10, 0x20, 0x30, 0x40};
601
602 ASSERT_NO_THROW(transport.Send(request));
603
604 asio::ip::udp::endpoint senderEndpoint;
605 const Bytes received = ReceiveUdpDatagram(*receiver, senderEndpoint);
606
607 EXPECT_EQ(received, request);
608}
609
611TEST_F(RpcTransportTests, SendFailsWhenTransportIsClosed)
612{
613 RpcTransport transport(*ioContext);
614
615 const Bytes request{0x01, 0x02, 0x03};
616
617 EXPECT_THROW(transport.Send(request), std::system_error);
618}
619
621TEST_F(RpcTransportTests, CloseClosesSocket)
622{
623 RpcTransport transport(*ioContext);
624
625 transport.Open("127.0.0.1", FindFreeUdpPort());
626
627 ASSERT_TRUE(transport.IsOpen());
628
629 transport.Close();
630
631 EXPECT_FALSE(transport.IsOpen());
632}
633
635TEST_F(RpcTransportTests, CloseIsIdempotent)
636{
637 RpcTransport transport(*ioContext);
638
639 transport.Open("127.0.0.1", FindFreeUdpPort());
640
641 EXPECT_NO_THROW(transport.Close());
642 EXPECT_NO_THROW(transport.Close());
643
644 EXPECT_FALSE(transport.IsOpen());
645}
646
648TEST_F(RpcTransportTests, CloseOnClosedTransportDoesNothing)
649{
650 RpcTransport transport(*ioContext);
651
652 EXPECT_FALSE(transport.IsOpen());
653
654 EXPECT_NO_THROW(transport.Close());
655
656 EXPECT_FALSE(transport.IsOpen());
657}
658
660TEST_F(RpcTransportTests, IsOpenReturnsTrueForOpenTransport)
661{
662 RpcTransport transport(*ioContext);
663
664 EXPECT_FALSE(transport.IsOpen());
665
666 transport.Open("127.0.0.1", FindFreeUdpPort());
667
668 EXPECT_TRUE(transport.IsOpen());
669}
670
672TEST_F(RpcTransportTests, IsOpenReturnsFalseForClosedTransport)
673{
674 RpcTransport transport(*ioContext);
675
676 transport.Open("127.0.0.1", FindFreeUdpPort());
677
678 ASSERT_TRUE(transport.IsOpen());
679
680 transport.Close();
681
682 EXPECT_FALSE(transport.IsOpen());
683}
684
696TEST_F(RpcTransportTests, ReceiveUsesExternallyDrivenIoContext)
697{
698 const std::uint16_t port = FindFreeUdpPort();
699 RpcTransport transport(*ioContext);
700
701 transport.Open("127.0.0.1", port, true);
702
703 const Bytes expected{
704 0x01,
705 0x02,
706 0x03,
707 0x04,
708 0xAA,
709 0xBB};
710
711 // Send the datagram before Receive() starts. UDP retains the datagram in
712 // the kernel receive queue, so the asynchronous receive operation can
713 // consume it once it is registered with the socket.
714 SendUdpDatagram(port, expected);
715
716 asio::ip::udp::endpoint remoteEndpoint;
717
718 const Bytes received = transport.Receive(1000ms, remoteEndpoint);
719
720 EXPECT_EQ(received, expected);
721 EXPECT_EQ(remoteEndpoint.address(), asio::ip::address_v4::loopback());
722 EXPECT_NE(remoteEndpoint.port(), 0);
723}
Runs an Asio I/O context on a dedicated worker thread.
void SetUp() override
Create a fresh Asio execution context for the test.
std::unique_ptr< profinet::AsioIoContextRunner > runner
void TearDown() override
Destroy the Asio execution context after the test.
std::unique_ptr< asio::io_context > ioContext
Asio execution context used by the transport under test.
Keeps an Asio I/O context running on a dedicated worker thread.
RPC operation timed out.
Definition exceptions.h:587
Synchronous UDP transport used by PROFINET DCE/RPC.
void SendTo(const Bytes &data, const asio::ip::udp::endpoint &targetEndpoint) override
Send a UDP datagram to a specific remote endpoint.
bool IsOpen() const noexcept
Check whether the socket is open.
void Open(const std::string &address, std::uint16_t port, bool isListener=false) override
Open the UDP socket and configure the remote endpoint.
Bytes SendReceive(const Bytes &data, std::chrono::milliseconds timeout) override
Send a UDP datagram and wait for one response.
void Close() noexcept override
Close the UDP socket.
void Send(const Bytes &data) override
Send a UDP datagram without waiting for a response.
Bytes Receive(std::chrono::milliseconds timeout, asio::ip::udp::endpoint &remoteEndpoint) override
Wait for an incoming UDP datagram and capture the sender's endpoint.
Declares exceptions and error types used by the PROFINET IO controller stack.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
Bytes buffer
asio::ip::udp::endpoint remoteEndpoint
std::size_t receivedBytes
UDP transport abstraction for PROFINET DCE/RPC.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
TEST_F(RpcTransportTests, ConstructorCreatesClosedTransport)