PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_asioEthernetSocket.cpp
Go to the documentation of this file.
1
10
11#include <gtest/gtest.h>
12
13#include <asio.hpp>
14#include <chrono>
15#include <cstdint>
16#include <cstdlib>
17#include <memory>
18#include <optional>
19#include <thread>
20#include <vector>
21
23#include "profinet/exceptions.h"
24#include "profinet/protocol.h"
25#include "profinet/util.h"
26
27using namespace profinet;
28using namespace std::chrono_literals;
29
30namespace
31{
32
38class TestIoContextRunner
39{
40 public:
44 explicit TestIoContextRunner(asio::io_context& ioContext)
46 workGuard(asio::make_work_guard(ioContext)),
47 thread([&]
48 {
49 ioContext.run();
50 })
51 {
52 }
53
55 ~TestIoContextRunner()
56 {
57 workGuard.reset();
58 ioContext.stop();
59
60 if (thread.joinable())
61 {
62 thread.join();
63 }
64 }
65
66 TestIoContextRunner(const TestIoContextRunner&) = delete;
67 TestIoContextRunner& operator=(const TestIoContextRunner&) = delete;
68
69 private:
71 asio::io_context& ioContext;
72
74 asio::executor_work_guard<asio::io_context::executor_type> workGuard;
75
77 std::thread thread;
78};
79
87std::string GetTestInterface()
88{
89 const char* value = std::getenv("PROFINET_TEST_INTERFACE");
90
91 if (value == nullptr)
92 {
93 return {};
94 }
95
96 return value;
97}
98
100class AsioEthernetSocketTests : public ::testing::Test
101{
102 protected:
104 void SetUp() override
105 {
106 runner = std::make_unique<TestIoContextRunner>(ioContext);
107 }
108
110 void TearDown() override
111 {
112 runner.reset();
113 }
114
116 asio::io_context ioContext;
117
119 std::unique_ptr<TestIoContextRunner> runner;
120};
121
122} // namespace
123
125TEST_F(AsioEthernetSocketTests, ConstructorRejectsUnknownInterface)
126{
127 EXPECT_THROW(AsioEthernetSocket(ioContext, "this-interface-does-not-exist"), SocketError);
128}
129
131TEST_F(AsioEthernetSocketTests, ConstructorAcceptsEthernetInterface)
132{
133 const std::string interface = GetTestInterface();
134
135 if (interface.empty())
136 {
137 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
138 }
139
140 EXPECT_NO_THROW(AsioEthernetSocket(ioContext, interface));
141}
142
144TEST_F(AsioEthernetSocketTests, ConstructorAcceptsSpecificEtherType)
145{
146 const std::string interface = GetTestInterface();
147
148 if (interface.empty())
149 {
150 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
151 }
152
153 EXPECT_NO_THROW(AsioEthernetSocket(ioContext, interface, PROFINET_ETHERTYPE));
154}
155
158TEST_F(AsioEthernetSocketTests, SetTimeoutAcceptsFiniteDuration)
159{
160 const std::string interface = GetTestInterface();
161
162 if (interface.empty())
163 {
164 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
165 }
166
167 AsioEthernetSocket socket(ioContext, interface);
168
169 EXPECT_NO_THROW(socket.SetTimeout(20ms));
170}
171
174TEST_F(AsioEthernetSocketTests, RecvReturnsEmptyFrameOnTimeout)
175{
176 const std::string interface = GetTestInterface();
177
178 if (interface.empty())
179 {
180 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
181 }
182
183 AsioEthernetSocket socket(ioContext, interface);
184
185 socket.SetTimeout(20ms);
186
187 const std::vector<std::uint8_t> received = socket.Recv();
188
189 EXPECT_TRUE(received.empty());
190}
191
194TEST_F(AsioEthernetSocketTests, RecvUsesExternallyDrivenIoContext)
195{
196 const std::string interface = GetTestInterface();
197
198 if (interface.empty())
199 {
200 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
201 }
202
203 AsioEthernetSocket socket(ioContext, interface);
204
205 socket.SetTimeout(20ms);
206
207 // If the Asio execution context were not driven by the fixture's
208 // external runner, this call would never complete. The test therefore
209 // verifies the execution model used by the production transport.
210
211 const std::vector<std::uint8_t> received = socket.Recv();
212
213 EXPECT_TRUE(received.empty());
214}
216TEST_F(AsioEthernetSocketTests, InjectedEthernetFrameCanBeCapturedByPeer)
217{
218 const std::string interface = GetTestInterface();
219
220 if (interface.empty())
221 {
222 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
223 }
224
225 AsioEthernetSocket sender(ioContext, interface);
226 AsioEthernetSocket receiver(ioContext, interface);
227
228 const Bytes frame{
229 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
230 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
231 0x88, 0x92,
232 0x01, 0x02, 0x03, 0x04};
233
234 receiver.SetTimeout(1000ms);
235
236 sender.Send(frame);
237
238 const Bytes received = receiver.Recv();
239
240 ASSERT_FALSE(received.empty());
241 EXPECT_EQ(received, frame);
242}
243
245TEST_F(AsioEthernetSocketTests, EtherTypeFilterAcceptsVlanTaggedFrame)
246{
247 const std::string interface = GetTestInterface();
248
249 if (interface.empty())
250 {
251 GTEST_SKIP()
252 << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
253 }
254
255 AsioEthernetSocket sender(ioContext, interface);
256
257 AsioEthernetSocket receiver(
258 ioContext,
259 interface,
261
262 const Bytes frame{
263 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
264 0x02, 0x00, 0x00, 0x00, 0x00, 0x02,
265
266 0x81, 0x00,
267 0xC0, 0x00,
268
269 0x88, 0x92,
270 0x01, 0x02, 0x03, 0x04};
271
272 receiver.SetTimeout(1000ms);
273
274 sender.Send(frame);
275
276 const Bytes received = receiver.Recv();
277
278 ASSERT_FALSE(received.empty());
279 EXPECT_EQ(received, frame);
280}
281
285TEST_F(AsioEthernetSocketTests, UnicastFrameCanBeReceivedByPeer)
286{
287 const std::string interface = GetTestInterface();
288
289 if (interface.empty())
290 {
291 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
292 }
293
294 AsioEthernetSocket sender(ioContext, interface);
295 AsioEthernetSocket receiver(ioContext, interface);
296
297 const Bytes frame{
298 0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13,
299 0x08, 0x00, 0x27, 0x31, 0x39, 0xDC,
300 0x88, 0x92,
301 0x01, 0x02, 0x03, 0x04};
302
303 receiver.SetTimeout(1000ms);
304
305 sender.Send(frame);
306
307 const Bytes received = receiver.Recv();
308
309 ASSERT_FALSE(received.empty());
310 EXPECT_EQ(received, frame);
311}
312
315TEST_F(AsioEthernetSocketTests, ConcurrentUnicastFrameCanBeReceivedByPeer)
316{
317 const std::string interface = GetTestInterface();
318
319 if (interface.empty())
320 {
321 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
322 }
323
324 AsioEthernetSocket sender(ioContext, interface);
325 AsioEthernetSocket receiver(ioContext, interface);
326
327 const Bytes frame{
328 0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13,
329 0x08, 0x00, 0x27, 0x31, 0x39, 0xDC,
330 0x88, 0x92,
331 0x01, 0x02, 0x03, 0x04};
332
333 Bytes received;
334 std::jthread receiverThread([&]
335 {
336 receiver.SetTimeout(1000ms);
337 received = receiver.Recv();
338 });
339
340 sender.Send(frame);
341
342 receiverThread.join();
343
344 ASSERT_FALSE(received.empty());
345 EXPECT_EQ(received, frame);
346}
347
350TEST_F(AsioEthernetSocketTests, PeerCanReceiveAndRespondToController)
351{
352 const std::string interface = GetTestInterface();
353
354 if (interface.empty())
355 {
356 GTEST_SKIP() << "Set PROFINET_TEST_INTERFACE to run live Ethernet tests";
357 }
358 constexpr std::uint16_t testEtherType = 0x88B5;
359 AsioEthernetSocket controller(ioContext, interface, testEtherType);
360 AsioEthernetSocket peer(ioContext, interface, testEtherType);
361
362 const Bytes request{
363 0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13,
364 0x08, 0x00, 0x27, 0x31, 0x39, 0xDC,
365 0x88, 0xB5,
366 0x01, 0x02, 0x03, 0x04};
367
368 const Bytes response{
369 0x08, 0x00, 0x27, 0x31, 0x39, 0xDC,
370 0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13,
371 0x88, 0xB5,
372 0x05, 0x06, 0x07, 0x08};
373
374 Bytes receivedRequest;
375 std::exception_ptr peerException;
376
377 std::jthread peerThread([&]
378 {
379 try
380 {
381 peer.SetTimeout(1000ms);
382
383 receivedRequest = peer.Recv();
384
385 if (receivedRequest.empty())
386 {
387 throw std::runtime_error("Peer did not receive request");
388 }
389
390 if (receivedRequest != request)
391 {
392 throw std::runtime_error("Peer received unexpected request");
393 }
394
395 peer.Send(response);
396 }
397 catch (...)
398 {
399 peerException = std::current_exception();
400 }
401 });
402
403 controller.SetTimeout(2000ms);
404 controller.Send(request);
405
406 // the controller send request, is received by a peer socket
407 // the peer socket send reponse, is received by controller socket
408
409 peerThread.join();
410
411 if (peerException)
412 {
413 std::rethrow_exception(peerException);
414 }
415
416 ASSERT_EQ(receivedRequest, request);
417
418 const Bytes receivedResponse = controller.Recv();
419
420 ASSERT_FALSE(receivedResponse.empty());
421 EXPECT_EQ(receivedResponse, response);
422}
Asio-backed platform-independent raw Ethernet transport.
Platform-independent raw Ethernet transport using Asio.
void Send(const std::vector< std::uint8_t > &frame) override
Send one complete Ethernet frame.
std::vector< std::uint8_t > Recv() override
Receive one complete Ethernet frame.
void SetTimeout(std::chrono::milliseconds timeout) override
Set the receive timeout.
Socket operation error.
Definition exceptions.h:663
Declares exceptions and error types used by the PROFINET IO controller stack.
TEST_F(AlarmListenerTests, InitializesWithEndpoint)
constexpr std::uint16_t PROFINET_ETHERTYPE
EtherType value identifying PROFINET frames (0x8892).
Definition util.h:30
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
PROFINET protocol wire-format structures and serialization helpers.
std::unique_ptr< cyclic::CyclicController > controller
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Declares Linux Ethernet, addressing, timing, and utility helpers used by the PROFINET stack.