PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_device.cpp
Go to the documentation of this file.
1
14
15#include <gtest/gtest.h>
16
18#include "mocks/mockRPCCon.h"
19#include "profinet/device.h"
20
21using namespace profinet;
22using namespace profinet::device;
23namespace profinet::device
24{
25
26class DeviceTest : public ::testing::Test
27{
28};
29
31TEST_F(DeviceTest, ParseMacAcceptsColonDashAndDotSeparators)
32{
33 auto a = ParseMac("aa:bb:cc:dd:ee:ff");
34 auto b = ParseMac("AA-BB-CC-DD-EE-FF");
35 auto c = ParseMac("aa.bb.cc.dd.ee.ff");
36
37 ASSERT_TRUE(a.has_value());
38 ASSERT_TRUE(b.has_value());
39 ASSERT_TRUE(c.has_value());
40
41 EXPECT_EQ(*a, *b);
42 EXPECT_EQ(*a, *c);
43 EXPECT_EQ((*a)[0], static_cast<std::uint8_t>(0xAA));
44 EXPECT_EQ((*a)[5], static_cast<std::uint8_t>(0xFF));
45}
46
48TEST_F(DeviceTest, ParseMacRejectsNonMacStrings)
49{
50 EXPECT_FALSE(ParseMac("my-device-name").has_value());
51 EXPECT_FALSE(ParseMac("").has_value());
52 EXPECT_FALSE(ParseMac("aa:bb:cc:dd:ee").has_value());
53 EXPECT_FALSE(ParseMac("aa:bb:cc:dd:ee:ff:11").has_value());
54 EXPECT_FALSE(ParseMac("gg:bb:cc:dd:ee:ff").has_value());
55}
56
58TEST_F(DeviceTest, IsMacAddressMatchesParseMac)
59{
60 EXPECT_TRUE(IsMacAddress("aa:bb:cc:dd:ee:ff"));
61 EXPECT_FALSE(IsMacAddress("plc-station-1"));
62}
63
65TEST_F(DeviceTest, DeviceInfoHelpersReturnEmptyOrZeroWhenIm0Unset)
66{
67 const DeviceInfo info;
68
69 EXPECT_EQ(info.SerialNumber(), "");
70 EXPECT_EQ(info.OrderId(), "");
71 EXPECT_EQ(info.HardwareRevision(), static_cast<std::uint16_t>(0));
72 EXPECT_EQ(info.SoftwareRevision(), "");
73}
74
76TEST_F(DeviceTest, DeviceInfoHelpersExtractAndTrimIm0Fields)
77{
78 DeviceInfo info;
79 PNInM0 im0;
80
81 im0.orderId = "MyOrder123 ";
82 im0.imSerialNumber = "SN00042 ";
83 im0.imHardwareRevision = 3;
84 im0.swRevisionPrefix = 'V';
86 im0.imSwRevisionBugFix = 2;
88
89 info.im0 = im0;
90
91 EXPECT_EQ(info.SerialNumber(), "SN00042");
92 EXPECT_EQ(info.OrderId(), "MyOrder123");
93 EXPECT_EQ(info.HardwareRevision(), static_cast<std::uint16_t>(3));
94 EXPECT_EQ(info.SoftwareRevision(), "V1.2.3");
95}
96
98TEST_F(DeviceTest, FromDcpInfoConstructsWithoutDiscovery)
99{
101 String2Mac("aa:bb:cc:dd:ee:ff"),
102 {});
103
104 info.name = "test-device";
105 info.ip = "192.168.1.50";
106
107 // "lo" is used here purely so get_mac() succeeds in this sandboxed
108 // test environment; no packets are sent.
109 const ProfinetDevice dev = ProfinetDevice::FromDcpInfo(info, "lo");
110
111 EXPECT_EQ(dev.Name(), "test-device");
112 EXPECT_EQ(dev.Ip(), "192.168.1.50");
113 EXPECT_FALSE(dev.IsConnected());
114 EXPECT_NE(
115 dev.ToString().find("disconnected"),
116 std::string::npos);
117}
118
120TEST_F(DeviceTest, DiscoverWithUnknownMacThrowsDcpDeviceNotFoundError)
121{
122 // No device will ever claim this MAC on "lo", and the discovery
123 // timeout is kept short so the test doesn't hang.
124 EXPECT_THROW(
126 "aa:bb:cc:dd:ee:ff",
127 "lo",
128 /*timeoutSec=*/1.0),
130}
131
134TEST(DeviceMoveTests, MoveConstructionDisconnectsSource)
135{
136 constexpr MacAddress kControllerMac{
137 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
138
140 String2Mac("aa:bb:cc:dd:ee:ff"),
141 {});
142
143 info.name = "test-device";
144 info.ip = "192.168.1.50";
145
146 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
147 auto* rpc = mock.get();
148 ProfinetDevice source(info, "eth0", kControllerMac, 5.0, std::move(mock));
149
150 source.Connect();
151
152 ProfinetDevice destination(std::move(source));
153
154 EXPECT_EQ(destination.Mac(), "aa:bb:cc:dd:ee:ff");
155 EXPECT_EQ(destination.Name(), "test-device");
156
157 EXPECT_TRUE(destination.IsConnected());
158 EXPECT_FALSE(source.IsConnected());
159}
160
164TEST(DeviceMoveTests, MoveAssignmentReleasesDestinationFirst)
165{
166 constexpr MacAddress kControllerMac{
167 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
168
170 String2Mac("aa:bb:cc:dd:ee:ff"),
171 {});
172
173 info1.name = "test-device";
174 info1.ip = "192.168.1.50";
175
176 auto mock1 = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
177
178 ProfinetDevice source(info1, "eth0", kControllerMac, 5.0, std::move(mock1));
179 source.Connect();
180
182 String2Mac("00:11:22:33:44:55"),
183 {});
184 info2.name = "test-device2";
185 info2.ip = "192.168.1.80";
186
187 auto mockRpcDestination = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
188 auto* rpcDestination = mockRpcDestination.get();
189 auto mockAlarmListenerDestination = std::make_unique<testing::NiceMock<MockAlarmListener>>();
190
191 auto* alarmListenerDestination = mockAlarmListenerDestination.get();
192 {
193 testing::InSequence sequence;
194 EXPECT_CALL(*alarmListenerDestination, Stop)
195 .Times(1);
196 EXPECT_CALL(*rpcDestination, Close)
197 .Times(1);
198 }
199 ProfinetDevice destination(info2, "eth0", kControllerMac, 5.0, std::move(mockRpcDestination), std::move(mockAlarmListenerDestination));
200 destination.Connect();
201 EXPECT_TRUE(destination.IsConnected());
202
203 destination = std::move(source);
204
205 EXPECT_TRUE(destination.IsConnected());
206 EXPECT_FALSE(source.IsConnected());
207}
208
211TEST(DeviceMoveTests, MovedFromDeviceCanBeDestroyedSafely)
212{
213 constexpr MacAddress kControllerMac{
214 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
215
217 String2Mac("aa:bb:cc:dd:ee:ff"),
218 {});
219
220 info.name = "test-device";
221 info.ip = "192.168.1.50";
222
223 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
224 ProfinetDevice source(info, "eth0", kControllerMac, 5.0, std::move(mock));
225
226 source.Connect();
227 {
228 auto destination = std::move(source);
229 }
230 SUCCEED();
231}
232
235class MockDcpScanner final : public IDcpScanner
236{
237 public:
242 MOCK_METHOD(dcp::ResponseMap, Scan, (const std::string& interface, double timeoutSec), (override));
243};
246{
247 public:
249 MOCK_METHOD(MacAddress, GetMac, (const std::string& interface), (override));
250};
251
253class ProfinetScannerTest : public ::testing::Test
254{
255 protected:
257 void SetUp() override
258 {
259 scanner = std::make_unique<MockDcpScanner>();
260 mockDcpScanner = scanner.get();
261
262 macAddressProvider = std::make_unique<MockMacAddressProvider>();
264 }
265
269 void ConfigureScanResults(const dcp::ResponseMap& responses, double timeoutSec)
270 {
271 EXPECT_CALL(*mockDcpScanner, Scan("eth0", timeoutSec))
272 .WillOnce(::testing::Return(responses));
273 }
276 {
278 std::string mac;
279
281 std::string name;
282
284 std::string ip;
285 };
286
296 dcp::ResponseMap CreateScanResponses(std::initializer_list<TestScanDevice> devices)
297 {
298 dcp::ResponseMap responses;
299
300 for (const auto& device : devices)
301 {
302 const MacAddress mac = String2Mac(device.mac);
303
304 auto& blocks = responses[mac];
305
306 blocks[dcp::MakeBlockKey(DcpOption::Device, DcpDeviceSuboption::Name)] = Bytes(device.name.begin(), device.name.end());
307
308 const auto ip = Ip2String(device.ip);
309 blocks[dcp::MakeBlockKey(DcpOption::Ip, DcpIpSuboption::IpParameter)] = Bytes(ip.begin(), ip.end());
310 }
311
312 return responses;
313 }
314
315 [[nodiscard]] static double GetDeviceTimeout(const ProfinetDevice& device)
316 {
317 return device.timeout;
318 }
319
321 std::unique_ptr<MockDcpScanner> scanner;
322
324 std::unique_ptr<MockMacAddressProvider> macAddressProvider;
325 static constexpr MacAddress kMacA{0x02, 0x00, 0x00, 0x00, 0x00, 0x0A};
326};
327
329TEST_F(ProfinetScannerTest, ScanPreservesZeroTimeout)
330{
331 const auto responses = CreateScanResponses({
332 {
333 .mac = "AA:BB:CC:DD:EE:01",
334 .name = "valve",
335 .ip = "192.168.1.10",
336 },
337 {
338 .mac = "AA:BB:CC:DD:EE:02",
339 .name = "valve",
340 .ip = "192.168.1.11",
341 },
342 });
343
344 EXPECT_CALL(*mockDcpScanner, Scan("eth0", 0.0))
345 .WillOnce(::testing::Return(responses));
346
347 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
348 .WillOnce(::testing::Return(kMacA));
349
350 const auto devices = Scan("eth0", 0.0, std::move(scanner), std::move(macAddressProvider));
351
352 ASSERT_EQ(devices.size(), responses.size());
353
354 for (const auto& device : devices)
355 {
356 EXPECT_DOUBLE_EQ(GetDeviceTimeout(device), 0.0);
357 }
358}
359
361TEST_F(ProfinetScannerTest, ScanPreservesShortTimeout)
362{
363 constexpr double timeoutSec = 0.1;
364 const auto responses = CreateScanResponses({{
365 .mac = "AA:BB:CC:DD:EE:01",
366 .name = "valve",
367 .ip = "192.168.1.10",
368 },
369 {
370 .mac = "AA:BB:CC:DD:EE:02",
371 .name = "valve",
372 .ip = "192.168.1.11",
373 },
374 {
375 .mac = "AA:BB:CC:DD:EE:03",
376 .name = "valve",
377 .ip = "192.168.1.12",
378 }});
379
380 EXPECT_CALL(*mockDcpScanner, Scan("eth0", timeoutSec))
381 .WillOnce(::testing::Return(responses));
382
383 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
384 .WillOnce(::testing::Return(kMacA));
385
386 const auto devices = Scan("eth0", timeoutSec, std::move(scanner), std::move(macAddressProvider));
387
388 ASSERT_EQ(devices.size(), responses.size());
389
390 for (const auto& device : devices)
391 {
392 EXPECT_DOUBLE_EQ(GetDeviceTimeout(device), timeoutSec);
393 }
394}
395
397TEST_F(ProfinetScannerTest, ScanPreservesLongTimeout)
398{
399 constexpr double timeoutSec = 30.0;
400 const auto responses = CreateScanResponses({{
401 .mac = "AA:BB:CC:DD:EE:01",
402 .name = "valve",
403 .ip = "192.168.1.10",
404 },
405 {
406 .mac = "AA:BB:CC:DD:EE:02",
407 .name = "valve",
408 .ip = "192.168.1.11",
409 },
410 {
411 .mac = "AA:BB:CC:DD:EE:03",
412 .name = "valve",
413 .ip = "192.168.1.12",
414 }});
415
416 EXPECT_CALL(*mockDcpScanner, Scan("eth0", timeoutSec))
417 .WillOnce(::testing::Return(responses));
418
419 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
420 .WillOnce(::testing::Return(kMacA));
421
422 const auto devices = Scan("eth0", timeoutSec, std::move(scanner), std::move(macAddressProvider));
423
424 ASSERT_EQ(devices.size(), responses.size());
425
426 for (const auto& device : devices)
427 {
428 EXPECT_DOUBLE_EQ(GetDeviceTimeout(device), timeoutSec);
429 }
430}
431
433TEST_F(ProfinetScannerTest, ScanCreatesDeviceForEveryResponse)
434{
435 constexpr double timeoutSec = 5.0;
436 const auto responses = CreateScanResponses({{
437 .mac = "AA:BB:CC:DD:EE:01",
438 .name = "valve",
439 .ip = "192.168.1.10",
440 },
441 {
442 .mac = "AA:BB:CC:DD:EE:02",
443 .name = "valve",
444 .ip = "192.168.1.11",
445 }});
446 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
447 .WillOnce(::testing::Return(kMacA));
448
449 ConfigureScanResults(responses, timeoutSec);
450
451 const auto devices = Scan("eth0", timeoutSec, std::move(scanner), std::move(macAddressProvider));
452
453 EXPECT_EQ(devices.size(), 2U);
454}
455
457TEST_F(ProfinetScannerTest, ScanDictKeepsDuplicateStationNames)
458{
459 constexpr double timeoutSec = 5.0;
460
461 const auto responses = CreateScanResponses(
462 {
463 {"AA:BB:CC:DD:EE:01", "valve", "192.168.1.10"},
464 {"AA:BB:CC:DD:EE:02", "valve", "192.168.1.11"},
465 });
466 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
467 .WillOnce(::testing::Return(kMacA));
468
469 ConfigureScanResults(responses, timeoutSec);
470
471 const auto result = ScanDict("eth0", timeoutSec, std::move(scanner), std::move(macAddressProvider));
472
473 ASSERT_EQ(result.size(), 2U);
474
475 ASSERT_NE(result.find("aa:bb:cc:dd:ee:01"), result.end());
476
477 ASSERT_NE(result.find("aa:bb:cc:dd:ee:02"), result.end());
478
479 EXPECT_EQ(result.at("aa:bb:cc:dd:ee:01").name, "valve");
480
481 EXPECT_EQ(result.at("aa:bb:cc:dd:ee:02").name, "valve");
482}
483
485TEST_F(ProfinetScannerTest, ScanDictKeepsEmptyStationNames)
486{
487 constexpr double timeoutSec = 5.0;
488
489 const auto responses = CreateScanResponses(
490 {
491 {"AA:BB:CC:DD:EE:01", "", "192.168.1.10"},
492 {"AA:BB:CC:DD:EE:02", "", "192.168.1.11"},
493 });
494
495 EXPECT_CALL(*mockMacAddressProvider, GetMac("eth0"))
496 .WillOnce(::testing::Return(kMacA));
497
498 ConfigureScanResults(responses, timeoutSec);
499
500 const auto result = ScanDict("eth0", timeoutSec, std::move(scanner), std::move(macAddressProvider));
501
502 ASSERT_EQ(result.size(), 2U);
503
504 ASSERT_NE(result.find("aa:bb:cc:dd:ee:01"), result.end());
505
506 ASSERT_NE(result.find("aa:bb:cc:dd:ee:02"), result.end());
507
508 EXPECT_TRUE(result.at("aa:bb:cc:dd:ee:01").name.empty());
509
510 EXPECT_TRUE(result.at("aa:bb:cc:dd:ee:02").name.empty());
511}
512} // namespace profinet::device
Device not found via DCP.
Definition exceptions.h:563
Provides the MAC address of a network interface.
Definition util.h:155
virtual MacAddress GetMac(const std::string &interface)=0
Get the MAC address of a network interface.
Parsed PROFINET device information from a DCP response.
Definition dcp.h:268
std::string name
Station name.
Definition dcp.h:282
Interface for PROFINET DCP device discovery.
Definition device.h:577
virtual dcp::ResponseMap Scan(const std::string &interface, double timeoutSec)=0
Discover PROFINET devices using DCP.
Mock implementation of IDcpScanner used to test ProfinetScanner without performing real DCP network d...
MOCK_METHOD(dcp::ResponseMap, Scan,(const std::string &interface, double timeoutSec),(override))
Discover PROFINET devices using the configured mock response.
Mock implementation of IMacAddressProvider for unit tests.
MOCK_METHOD(MacAddress, GetMac,(const std::string &interface),(override))
Mock GetMac implementation.
High-level PROFINET device interface.
Definition device.h:167
std::string ToString() const
Human-readable summary of this device, for logging/debugging.
Definition device.cpp:777
std::string Mac() const
The device's MAC address.
Definition device.h:275
static ProfinetDevice Discover(const std::string &identifier, const std::string &interface, double timeoutSec=10.0)
Discover a device by station name or MAC address.
Definition device.cpp:260
void Connect()
Establish the AR.
Definition device.cpp:340
const std::string & Ip() const
The device's IPv4 address.
Definition device.h:268
static ProfinetDevice FromDcpInfo(dcp::DCPDeviceDescription info, const std::string &interface, double timeoutSec=5.0)
Wrap an already-discovered DCPDeviceDescription.
Definition device.cpp:332
bool IsConnected() const
Whether the AR is currently established.
Definition device.h:250
double timeout
Default RPC response timeout in seconds.
Definition device.h:548
const std::string & Name() const
The device's station name.
Definition device.h:261
Test fixture for ProfinetScanner.
dcp::ResponseMap CreateScanResponses(std::initializer_list< TestScanDevice > devices)
Create synthetic DCP discovery responses for scanner tests.
std::unique_ptr< MockDcpScanner > scanner
void ConfigureScanResults(const dcp::ResponseMap &responses, double timeoutSec)
Configure the mock DCP scanner to return the supplied responses.
MockMacAddressProvider * mockMacAddressProvider
static double GetDeviceTimeout(const ProfinetDevice &device)
static constexpr MacAddress kMacA
std::unique_ptr< MockMacAddressProvider > macAddressProvider
void SetUp() override
Creates the scanner with a mock DCP scanner.
High-level PROFINET device interface and device lifecycle management.
Google Mock implementation of the PROFINET alarm listener interface.
Google Mock implementation of the IRPCCon PROFINET RPC connection interface.
constexpr DcpBlockKey MakeBlockKey(std::uint16_t option, std::uint8_t suboption)
Definition dcp.h:231
std::map< MacAddress, std::map< BlockKey, std::vector< std::uint8_t > > > ResponseMap
Result of ReadResponse(): MAC address -> parsed (option,suboption) -> payload blocks.
Definition dcp.h:483
bool IsMacAddress(const std::string &identifier)
Whether a string parses as a MAC address.
Definition device.cpp:173
std::vector< ProfinetDevice > Scan(const std::string &interface="eth0", double timeoutSec=3.0, std::unique_ptr< IDcpScanner > dcpScanner=nullptr, std::unique_ptr< IMacAddressProvider > macAddressProvider=nullptr)
Scan the network for PROFINET devices via DCP Identify.
Definition device.cpp:799
TEST_F(DeviceTest, ParseMacAcceptsColonDashAndDotSeparators)
std::optional< MacAddress > ParseMac(const std::string &macStr)
Parse a MAC address string in any common separator style.
Definition device.cpp:57
std::map< std::string, DeviceInfo > ScanDict(const std::string &interface="eth0", double timeoutSec=3.0, std::unique_ptr< IDcpScanner > dcpScanner=nullptr, std::unique_ptr< IMacAddressProvider > macAddressProvider=nullptr)
Scan the network and return a name -> DeviceInfo map.
Definition device.cpp:837
TEST(DeviceMoveTests, MoveConstructionDisconnectsSource)
@ IpParameter
IP address, subnet mask and gateway.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
std::array< std::uint8_t, 4 > Ip2String(const std::string &ipStr)
Parse a dotted-decimal IPv4 address string.
Definition util.cpp:222
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
@ Device
Device identification.
@ Ip
IP configuration.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
I&M0: mandatory identification data (vendor, order ID, serial number, revisions).
Definition protocol.h:1517
std::uint16_t imHardwareRevision
Hardware revision number.
Definition protocol.h:1542
std::string imSerialNumber
Device serial number (16 chars).
Definition protocol.h:1539
std::uint8_t imSwRevisionBugFix
Bug fix revision digit.
Definition protocol.h:1551
std::uint8_t swRevisionPrefix
Software revision prefix character (e.g. 'V', 'R', 'P', 'U', 'T').
Definition protocol.h:1545
std::uint8_t imSwRevisionFunctionalEnhancement
Functional enhancement revision digit.
Definition protocol.h:1548
std::uint8_t imSwRevisionInternalChange
Internal change revision digit.
Definition protocol.h:1554
std::string orderId
Manufacturer-specific order/part number (20 chars).
Definition protocol.h:1536
Complete device information summary.
Definition device.h:95
std::string SerialNumber() const
Serial number from I&M0, trimmed.
Definition device.cpp:182
std::optional< PNInM0 > im0
I&M0 identification data, if it was read successfully.
Definition device.h:127
std::uint16_t HardwareRevision() const
Hardware revision from I&M0.
Definition device.cpp:190
std::string OrderId() const
Order ID from I&M0, trimmed.
Definition device.cpp:186
std::string SoftwareRevision() const
Software revision string from I&M0.
Definition device.cpp:195
Test description used to construct a synthetic DCP discovery response.