PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_deviceWrappers.cpp
Go to the documentation of this file.
1
11
12#include <gmock/gmock.h>
13#include <gtest/gtest.h>
14
15#include <cstdint>
16#include <memory>
17#include <optional>
18#include <ranges>
19#include <string>
20#include <utility>
21#include <vector>
22
24#include "mocks/mockRPCCon.h"
25#include "profinet/device.h"
26#include "profinet/exceptions.h"
27#include "profinet/indices.h"
28
29using ::testing::_;
30using ::testing::DoAll;
31using ::testing::Eq;
32using ::testing::Return;
33using ::testing::Throw;
34
35namespace profinet::device
36{
37
38namespace
39{
40
42 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
43
44dcp::DCPDeviceDescription MakeInfo(
45 std::string name = "dev",
46 std::string ip = "192.168.0.10",
47 std::string mac = "02:00:00:00:00:01")
48{
49 dcp::DCPDeviceDescription info(
50 String2Mac(mac),
51 {});
52
53 info.name = std::move(name);
54 info.ip = std::move(ip);
55 info.mac = std::move(mac);
56 info.deviceType = "test";
57 info.netmask = "255.255.255.0";
58 info.gateway = "0.0.0.0";
59
60 return info;
61}
62
63} // namespace
64
65// ============================================================================
66// ParseMacTests
67// ============================================================================
68
69class ParseMacTests : public ::testing::Test
70{
71};
72
74TEST_F(ParseMacTests, ParsesColonSeparatedMac)
75{
76 const auto result = ParseMac("02:00:00:00:00:01");
77
78 ASSERT_TRUE(result.has_value());
79 EXPECT_EQ(*result, (MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
80}
81
83TEST_F(ParseMacTests, ParsesDashSeparatedMac)
84{
85 const auto result = ParseMac("02-00-00-00-00-01");
86
87 ASSERT_TRUE(result.has_value());
88 EXPECT_EQ(*result, (MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
89}
90
92TEST_F(ParseMacTests, ParsesDotSeparatedMac)
93{
94 const auto result = ParseMac("0200.0000.0001");
95
96 ASSERT_TRUE(result.has_value());
97 EXPECT_EQ(*result, (MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
98}
99
101TEST_F(ParseMacTests, RejectsInvalidMac)
102{
103 EXPECT_FALSE(ParseMac("not-a-mac").has_value());
104}
105
107TEST_F(ParseMacTests, IsMacAddressAcceptsValidMac)
108{
109 EXPECT_TRUE(IsMacAddress("02:00:00:00:00:01"));
110}
111
113TEST_F(ParseMacTests, IsMacAddressRejectsInvalidIdentifier)
114{
115 EXPECT_FALSE(IsMacAddress("device-name"));
116}
117
118// ============================================================================
119// DeviceInfoTests
120// ============================================================================
121
122class DeviceInfoTests : public ::testing::Test
123{
124};
125
127TEST_F(DeviceInfoTests, SerialNumberReturnsEmptyWhenIm0Missing)
128{
129 DeviceInfo info;
130
131 EXPECT_TRUE(info.SerialNumber().empty());
132}
133
135TEST_F(DeviceInfoTests, OrderIdReturnsEmptyWhenIm0Missing)
136{
137 DeviceInfo info;
138
139 EXPECT_TRUE(info.OrderId().empty());
140}
141
143TEST_F(DeviceInfoTests, HardwareRevisionReturnsZeroWhenIm0Missing)
144{
145 DeviceInfo info;
146
147 EXPECT_EQ(info.HardwareRevision(), 0);
148}
149
151TEST_F(DeviceInfoTests, SoftwareRevisionReturnsEmptyWhenIm0Missing)
152{
153 DeviceInfo info;
154
155 EXPECT_TRUE(info.SoftwareRevision().empty());
156}
157
158// ============================================================================
159// ProfinetDeviceReadWriteTests
160// ============================================================================
161//
162// These tests assume an injectable RPC interface. Replace MockRPCCon and the
163// injection mechanism with the concrete mock abstraction used by the project.
164// ============================================================================
165
166class ProfinetDeviceReadWriteTests : public ::testing::Test
167{
168 protected:
169 void SetUp() override
170 {
171 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
172 rpc = mock.get();
173 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mock));
174 }
175
176 std::unique_ptr<ProfinetDevice> device;
178};
179
182{
183 PNIODHeader readResult{};
184 readResult.payload = Bytes{0x01, 0x02};
185
186 EXPECT_CALL(*rpc, Read(0, 1, 2, 0xAFF0))
187 .WillOnce(Return(readResult));
188
189 // Expected:
190 EXPECT_EQ(device->Read(1, 2, 0xAFF0),
191 (Bytes{0x01, 0x02}));
192}
193
195TEST_F(ProfinetDeviceReadWriteTests, WriteDelegatesArguments)
196{
197 const Bytes data{0xAB};
198
199 EXPECT_CALL(*rpc, Write(3, 1, 2, 0xAFF1, data));
200
201 device->Write(1, 2, 0xAFF1, data, 3);
202}
203
205TEST_F(ProfinetDeviceReadWriteTests, WriteMultipleMapsItems)
206{
207 const std::vector<WriteItem> writes{
208 WriteItem{
209 .slot = 1,
210 .subslot = 2,
211 .index = 0xAFF1,
212 .data = Bytes{0x01},
213 .api = 0,
214 }};
215
216 EXPECT_CALL(*rpc, Connect(_, _, _))
217 .WillOnce(Return(std::nullopt));
218
219 EXPECT_CALL(*rpc, WriteMultiple(_))
220 .WillOnce([](const std::vector<rpc::WriteItem>& items)
221 {
222 EXPECT_EQ(items.size(), 1U);
223 if (items.size() != 1U)
224 {
225 return std::vector<blocks::WriteMultipleResult>{};
226 }
227 EXPECT_EQ(items[0].slot, 1);
228 EXPECT_EQ(items[0].subslot, 2);
229 EXPECT_EQ(items[0].index, 0xAFF1);
230 EXPECT_EQ(items[0].data, (Bytes{0x01}));
231 EXPECT_EQ(items[0].api, 0);
232
233 return std::vector<blocks::WriteMultipleResult>{};
234 });
235
236 device->WriteMultiple(writes);
237}
238
239// ============================================================================
240// ProfinetDeviceImTests
241// ============================================================================
242
243class ProfinetDeviceImTests : public ::testing::Test
244{
245 protected:
246 void SetUp() override
247 {
248 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
249 rpc = mock.get();
250
251 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mock));
252 }
253
254 std::unique_ptr<ProfinetDevice> device;
256};
257
260{
261 PNInM0 expected;
262 EXPECT_CALL(*rpc, ReadIm0(1, 2))
263 .WillOnce(Return(expected));
264
265 EXPECT_EQ(device->ReadIm0(1, 2), expected);
266}
267
270{
271 PNInM1 expected;
272 EXPECT_CALL(*rpc, ReadIm1(1, 2))
273 .WillOnce(Return(expected));
274
275 EXPECT_EQ(device->ReadIm1(1, 2), expected);
276}
277
280{
281 PNInM2 expected;
282
283 EXPECT_CALL(*rpc, ReadIm2(1, 2))
284 .WillOnce(Return(expected));
285
286 EXPECT_EQ(device->ReadIm2(1, 2), expected);
287}
288
291{
292 PNInM3 expected;
293 EXPECT_CALL(*rpc, ReadIm3(1, 2))
294 .WillOnce(Return(expected));
295
296 EXPECT_EQ(device->ReadIm3(1, 2), expected);
297}
298
301{
302 PNInM4 expected;
303 EXPECT_CALL(*rpc, ReadIm4(1, 2))
304 .WillOnce(Return(expected));
305
306 EXPECT_EQ(device->ReadIm4(1, 2), expected);
307}
308
311{
312 PNInM5 expected;
313 EXPECT_CALL(*rpc, ReadIm5(1, 2))
314 .WillOnce(Return(expected));
315
316 EXPECT_EQ(device->ReadIm5(1, 2), expected);
317}
318
320TEST_F(ProfinetDeviceImTests, ReadAllImDelegates)
321{
322 rpc::AllIM expected;
323 EXPECT_CALL(*rpc, ReadAllIm(1, 2))
324 .WillOnce(Return(expected));
325
326 EXPECT_EQ(device->ReadAllIm(1, 2), expected);
327}
328
330TEST_F(ProfinetDeviceImTests, WriteIm1BuildsPaddedBlock)
331{
332 EXPECT_CALL(*rpc, Write(0, 0, 1, IM1, _))
333 .WillOnce([](std::uint32_t,
334 std::uint16_t,
335 std::uint16_t,
336 std::uint16_t,
337 const Bytes& data)
338 {
339 EXPECT_EQ(data.size(), 62U);
340 if (data.size() != 62U)
341 {
342 return;
343 }
344 wire::Reader r(data);
345 EXPECT_EQ(r.U16(), static_cast<std::uint16_t>(0x0021));
346 EXPECT_EQ(r.U16(), static_cast<std::uint16_t>(58));
347
348 Bytes expected(32, static_cast<std::uint8_t>(' '));
349 std::ranges::copy(std::string_view{"func"}, expected.begin());
350
351 EXPECT_EQ(Bytes(data.begin() + 8, data.begin() + 40), expected);
352
353 return;
354 });
355
356 device->WriteIm1("func", "loc");
357}
358
360TEST_F(ProfinetDeviceImTests, WriteIm1RejectsFunctionLongerThan32Characters)
361{
362 EXPECT_THROW(
363 device->WriteIm1(std::string(33, 'x'), "loc"),
364 std::invalid_argument);
365}
366
368TEST_F(ProfinetDeviceImTests, WriteIm1RejectsLocationLongerThan22Characters)
369{
370 EXPECT_THROW(
371 device->WriteIm1("func", std::string(23, 'x')),
372 std::invalid_argument);
373}
374
376TEST_F(ProfinetDeviceImTests, WriteIm2BuildsPaddedBlock)
377{
378 EXPECT_CALL(*rpc, Write(0, 0, 1, IM2, _))
379 .WillOnce([](std::uint32_t,
380 std::uint16_t,
381 std::uint16_t,
382 std::uint16_t,
383 const Bytes& data)
384 {
385 EXPECT_EQ(data.size(), 24U);
386 if (data.size() != 24U)
387 {
388 return;
389 }
390 wire::Reader r(data);
391
392 EXPECT_EQ(r.U16(), 0x0022);
393 EXPECT_EQ(r.U16(), 20);
394
395 const Bytes expected{
396 '2', '0', '2', '6', '-', '0', '7', '-', '2', '1',
397 ' ', '1', '8', ':', '0', '0'};
398
399 EXPECT_EQ(Bytes(data.begin() + 8, data.begin() + 24),
400 expected);
401 });
402
403 device->WriteIm2("2026-07-21 18:00");
404}
405
407TEST_F(ProfinetDeviceImTests, WriteIm2RejectsDateLongerThan16Characters)
408{
409 EXPECT_THROW(
410 device->WriteIm2(std::string(17, 'x')),
411 std::invalid_argument);
412}
413
415TEST_F(ProfinetDeviceImTests, WriteIm3BuildsPaddedBlock)
416{
417 EXPECT_CALL(*rpc, Write(0, 0, 1, IM3, _))
418 .WillOnce([](std::uint32_t,
419 std::uint16_t,
420 std::uint16_t,
421 std::uint16_t,
422 const Bytes& data)
423 {
424 ASSERT_EQ(data.size(), 62U);
425 if (data.size() != 62U)
426 {
427 return;
428 }
429 wire::Reader r(data);
430
431 EXPECT_EQ(r.U16(), 0x0023);
432 EXPECT_EQ(r.U16(), 58);
433 Bytes expected(54, static_cast<std::uint8_t>(' '));
434 std::ranges::copy("descriptor" | std::views::take(10), expected.begin());
435
436 EXPECT_EQ(Bytes(data.begin() + 8, data.begin() + 62), expected);
437 });
438
439 device->WriteIm3("descriptor");
440}
441
443TEST_F(ProfinetDeviceImTests, WriteIm3RejectsDescriptorLongerThan54Characters)
444{
445 EXPECT_THROW(
446 device->WriteIm3(std::string(55, 'x')),
447 std::invalid_argument);
448}
449
450// ============================================================================
451// ProfinetDeviceDiagnosisTests
452// ============================================================================
453
454class ProfinetDeviceDiagnosisTests : public ::testing::Test
455{
456 protected:
457 void SetUp() override
458 {
459 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
460 rpc = mock.get();
461
462 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mock));
463 }
464 Bytes MakeBlockHeader(std::uint16_t blockType,
465 std::uint16_t blockLength,
466 std::uint8_t verHi = 1,
467 std::uint8_t verLo = 0)
468 {
469 Bytes out;
470 wire::PutU16(out, blockType);
471 wire::PutU16(out, blockLength);
472 wire::PutU8(out, verHi);
473 wire::PutU8(out, verLo);
474 return out;
475 }
476
477 Bytes MakeModuleDiffBlock(std::uint16_t slot,
478 std::uint16_t subslot,
479 std::uint16_t moduleState,
480 std::uint16_t submoduleState)
481 {
482 Bytes body;
483
484 wire::PutU16(body, 1); // NumberOfAPIs
485 wire::PutU32(body, 0); // API
486 wire::PutU16(body, 1); // NumberOfModules
487 wire::PutU16(body, slot); // SlotNumber
488 wire::PutU32(body, 0x0001); // ModuleIdentNumber
489 wire::PutU16(body, moduleState);
490 wire::PutU16(body, 1); // NumberOfSubmodules
491 wire::PutU16(body, subslot);
492 wire::PutU32(body, 0x0001); // SubmoduleIdentNumber
493 wire::PutU16(body, submoduleState);
494
495 const Bytes hdr = MakeBlockHeader(
496 static_cast<std::uint16_t>(BlockType::ModuleDifference),
497 static_cast<std::uint16_t>(body.size() + 2));
498
499 Bytes out = hdr;
500 out.insert(out.end(), body.begin(), body.end());
501
502 return out;
503 }
504
505 std::unique_ptr<ProfinetDevice> device;
507};
508
510TEST_F(ProfinetDeviceDiagnosisTests, ReadModuleDiffDelegates)
511{
512 const Bytes data = MakeModuleDiffBlock(
513 1,
514 1,
517
518 auto expected = blocks::ParseModuleDiffBlock(data);
519
520 EXPECT_CALL(*rpc, ReadModuleDiff()).WillOnce(Return(expected));
521 EXPECT_EQ(device->ReadModuleDiff(), expected);
522}
523
525TEST_F(ProfinetDeviceDiagnosisTests, ReadDiagnosisDelegatesArguments)
526{
528 EXPECT_CALL(*rpc, ReadDiagnosis(1, 2, 0xF00C))
529 .WillOnce(Return(expected));
530
531 EXPECT_EQ(device->ReadDiagnosis(1, 2, 0xF00C), expected);
532}
533
535TEST_F(ProfinetDeviceDiagnosisTests, ReadAllDiagnosisDelegates)
536{
537 std::map<std::uint16_t, diagnosis::DiagnosisData> expected;
538 EXPECT_CALL(*rpc, ReadAllDiagnosis()).WillOnce(Return(expected));
539 EXPECT_EQ(device->ReadAllDiagnosis(), expected);
540}
541
543TEST_F(ProfinetDeviceDiagnosisTests, DiscoverSlotsDelegates)
544{
545 std::vector<blocks::SlotInfo> expected;
546 EXPECT_CALL(*rpc, DiscoverSlots()).WillOnce(Return(expected));
547 EXPECT_EQ(device->DiscoverSlots(), expected);
548}
549
552{
553 blocks::PDRealData expected;
554 EXPECT_CALL(*rpc, ReadPdRealData()).WillOnce(Return(expected));
555 EXPECT_EQ(device->ReadTopology(), expected);
556}
557
558// ============================================================================
559// ProfinetDeviceAlarmTests
560// ============================================================================
561
562class ProfinetDeviceAlarmTests : public ::testing::Test
563{
564 protected:
565 void SetUp() override
566 {
567 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
568 rpc = mock.get();
569
570 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mock));
571 }
572
573 std::unique_ptr<ProfinetDevice> device;
575};
576
578TEST_F(ProfinetDeviceAlarmTests, ReturnsNoneOnRpcError)
579{
580 EXPECT_CALL(*rpc, Read(0, 0, 0, 0x800C))
581 .WillOnce(Throw(RPCError("nope")));
582
583 EXPECT_FALSE(device->ReadAlarm().has_value());
584}
585
587TEST_F(ProfinetDeviceAlarmTests, ReturnsNoneOnShortPayload)
588{
589 PNIODHeader readResult{};
590 readResult.payload = Bytes{0x04, 0x00};
591
592 EXPECT_CALL(*rpc, Read(_, _, _, _))
593 .WillOnce(Return(readResult));
594
595 EXPECT_FALSE(device->ReadAlarm().has_value());
596}
597
600{
601 const Bytes block{
602 // Block header
603 0x00, 0x02, // BlockType = 0x0002
604 0x00, 0x16, // BlockLength = 20 + 2
605 0x01, // BlockVersionHigh
606 0x00, // BlockVersionLow
607
608 // body
609 0x00, 0x01, // H = 0x0001
610 0x00, 0x00, 0x00, 0x00, // I = 0
611 0x00, 0x01, // H = 1
612 0x00, 0x01, // H = 1
613 0x00, 0x00, 0x00, 0x01, // I = 0x01
614 0x00, 0x00, 0x00, 0x01, // I = 0x01
615 0x04, 0x05, // H = 0x0400 | 5
616
617 // padding
618 0x00, 0x00};
619 PNIODHeader readResult{};
620 readResult.payload = block;
621
622 EXPECT_CALL(*rpc, Read(_, _, _, _))
623 .WillOnce(Return(readResult));
624
625 const auto alarm = device->ReadAlarm(1, 0);
626
627 ASSERT_TRUE(alarm.has_value());
628 EXPECT_EQ(alarm->slotNumber, 1);
629}
630
631// ============================================================================
632// ProfinetDeviceConnectionLifecycleTests
633// ============================================================================
634
635class ProfinetDeviceConnectionLifecycleTests : public ::testing::Test
636{
637 protected:
638 void SetUp() override
639 {
640 auto mockRPC = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
641 rpc = mockRPC.get();
642
643 auto mockAL = std::make_unique<testing::NiceMock<MockAlarmListener>>();
644 alarmListener = mockAL.get();
645 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mockRPC), std::move(mockAL));
646 }
647
648 std::unique_ptr<ProfinetDevice> device;
651};
652
655{
656 EXPECT_CALL(*rpc, Connect);
657
658 device->Connect();
659
660 EXPECT_TRUE(device->IsConnected());
661}
662
665{
666 EXPECT_CALL(*rpc, Connect)
667 .WillOnce(Throw(RPCError("refused")));
668
669 EXPECT_THROW(device->Connect(), RPCConnectionError);
670 EXPECT_FALSE(device->IsConnected());
671}
672
674TEST_F(ProfinetDeviceConnectionLifecycleTests, ConnectIsNoOpWhenAlreadyConnected)
675{
676 // Establish a connected device first.
677
678 device->Connect();
679
680 EXPECT_CALL(*rpc, Connect).Times(0);
681
682 device->Connect();
683
684 EXPECT_TRUE(device->IsConnected());
685}
686
688TEST_F(ProfinetDeviceConnectionLifecycleTests, DisconnectClearsConnectionState)
689{
690 EXPECT_CALL(*rpc, Disconnect());
691
692 device->Disconnect();
693
694 EXPECT_FALSE(device->IsConnected());
695}
696
699{
700 EXPECT_CALL(*rpc, Close());
701
702 device->Close();
703
704 EXPECT_FALSE(device->IsConnected());
705}
706
708TEST_F(ProfinetDeviceConnectionLifecycleTests, DISABLED_CloseStopsAlarmListener)
709{
710 // Configure alarm listener mock.
711
712 device->Close();
713
714 EXPECT_CALL(*alarmListener, Stop());
715}
716
718TEST_F(ProfinetDeviceConnectionLifecycleTests, ReleaseArClearsOnlyCurrentRpc)
719{
720 // This test requires access to the RPC object/reference used by the
721 // ProfinetDevice and a second independent RPCCon instance.
722
723 // device->ReleaseAr(otherRpc);
724 // EXPECT_TRUE(device->IsConnected());
725
726 // device->ReleaseAr(currentRpc);
727 // EXPECT_FALSE(device->IsConnected());
728}
729
731TEST_F(ProfinetDeviceConnectionLifecycleTests, ReleaseArSuppressesDisconnectErrors)
732{
733 // EXPECT_CALL(*rpc, Disconnect())
734 // .WillOnce(Throw(std::runtime_error("gone")));
735 //
736 // EXPECT_NO_THROW(device->ReleaseAr(*rpc));
737 // EXPECT_FALSE(device->IsConnected());
738}
739
740// ============================================================================
741// ProfinetDeviceAlarmListenerTests
742// ============================================================================
743
744class ProfinetDeviceAlarmListenerTests : public ::testing::Test
745{
746 protected:
747 void SetUp() override
748 {
749 auto mockRPC = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
750 rpc = mockRPC.get();
751
752 auto mockAL = std::make_unique<testing::NiceMock<MockAlarmListener>>();
753 alarmListener = mockAL.get();
754 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mockRPC), std::move(mockAL));
755 }
756
757 std::unique_ptr<ProfinetDevice> device;
760};
761
764{
765 EXPECT_THROW(device->StartAlarmListener(), std::runtime_error);
766}
767
770{
771 // Configure a connected RPC mock with HasAlarmCr() == false.
772
773 EXPECT_THROW(device->StartAlarmListener(), std::runtime_error);
774}
775
778{
779 EXPECT_CALL(*rpc, HasAlarmCr())
780 .WillOnce(Return(true));
781 EXPECT_CALL(*rpc, LocalAlarmRef())
782 .WillOnce(Return(1));
783 EXPECT_CALL(*rpc, DeviceAlarmRef())
784 .WillOnce(Return(42));
785
786 EXPECT_CALL(*alarmListener, Start(_, _)).Times(1);
787 EXPECT_CALL(*alarmListener, IsRunning())
788 .WillOnce(Return(false))
789 .WillOnce(Return(false))
790 .WillOnce(Return(true));
791 EXPECT_CALL(*rpc, Connect);
792 device->Connect();
793 device->OnAlarm([](const AlarmNotification&) {});
794 device->StartAlarmListener();
795
796 EXPECT_TRUE(device->AlarmListenerRunning());
797}
798
800TEST_F(ProfinetDeviceAlarmListenerTests, OnAlarmForwardsToRunningListener)
801{
802 // Configure an existing alarm listener mock.
803
804 const auto callback = [](const AlarmNotification&) {};
805
806 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_));
807 EXPECT_CALL(*alarmListener, IsRunning())
808 .WillOnce(Return(true));
809 device->OnAlarm(callback);
810}
811
814{
815 device->StopAlarmListener();
816
817 EXPECT_FALSE(device->AlarmListenerRunning());
818}
820TEST_F(ProfinetDeviceAlarmListenerTests, RepeatedStartDoesNotDuplicateCallbackRegistration)
821{
822 EXPECT_CALL(*rpc, HasAlarmCr())
823 .WillRepeatedly(Return(true));
824
825 EXPECT_CALL(*rpc, LocalAlarmRef())
826 .WillRepeatedly(Return(1));
827
828 EXPECT_CALL(*rpc, DeviceAlarmRef())
829 .WillRepeatedly(Return(42));
830
831 EXPECT_CALL(*alarmListener, IsRunning())
832 .WillOnce(Return(false))
833 .WillOnce(Return(false))
834 .WillRepeatedly(Return(true));
835
836 const auto callback = [](const AlarmNotification&) {};
837
838 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
839 .Times(1);
840
841 EXPECT_CALL(*alarmListener, Start(_, _))
842 .Times(1);
843
844 EXPECT_CALL(*rpc, Connect);
845
846 device->Connect();
847 device->OnAlarm(callback);
848
849 device->StartAlarmListener();
850 device->StartAlarmListener();
851}
852
854TEST_F(ProfinetDeviceAlarmListenerTests, CallbackAddedBeforeStartIsRegisteredOnce)
855{
856 EXPECT_CALL(*rpc, HasAlarmCr())
857 .WillOnce(Return(true));
858
859 EXPECT_CALL(*rpc, LocalAlarmRef())
860 .WillOnce(Return(1));
861
862 EXPECT_CALL(*rpc, DeviceAlarmRef())
863 .WillOnce(Return(42));
864
865 EXPECT_CALL(*alarmListener, IsRunning())
866 .WillOnce(Return(false))
867 .WillOnce(Return(false));
868
869 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
870 .Times(1);
871
872 EXPECT_CALL(*alarmListener, Start(_, _))
873 .Times(1);
874
875 EXPECT_CALL(*rpc, Connect);
876
877 device->Connect();
878
879 device->OnAlarm([](const AlarmNotification&) {});
880 device->StartAlarmListener();
881}
883TEST_F(ProfinetDeviceAlarmListenerTests, CallbackAddedWhileRunningIsForwardedOnce)
884{
885 const auto callback = [](const AlarmNotification&) {};
886
887 EXPECT_CALL(*alarmListener, IsRunning())
888 .WillOnce(Return(true));
889
890 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
891 .Times(1);
892
893 device->OnAlarm(callback);
894}
895// ============================================================================
896// ProfinetDeviceInfoTests
897// ============================================================================
898
899class ProfinetDeviceInfoTests : public ::testing::Test
900{
901 protected:
902 void SetUp() override
903 {
904 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
905 rpc = mock.get();
906
907 device = std::make_unique<ProfinetDevice>(MakeInfo(), "eth0", kControllerMac, 5.0, std::move(mock));
908 }
909
910 std::unique_ptr<ProfinetDevice> device;
912};
913
916{
917 PNInM0 expectedIm0;
918 EXPECT_CALL(*rpc, ReadIm0(0, 1))
919 .WillOnce(Return(expectedIm0));
920
921 const DeviceInfo info = device->GetInfo();
922
923 ASSERT_TRUE(info.im0.has_value());
924}
925
927TEST_F(ProfinetDeviceInfoTests, Im0FailureIsTolerated)
928{
929 EXPECT_CALL(*rpc, ReadIm0(0, 1))
930 .WillOnce(Throw(RPCError("unsupported")));
931
932 const DeviceInfo info = device->GetInfo();
933
934 EXPECT_FALSE(info.im0.has_value());
935}
936
938TEST_F(ProfinetDeviceInfoTests, IncludesTopologyWhenRequested)
939{
940 blocks::PDRealData expectedTopology;
941 // EXPECT_CALL(*rpc, ReadIm0(0, 1))
942 // .WillOnce(Return(std::nullopt));
943
944 EXPECT_CALL(*rpc, ReadPdRealData())
945 .WillOnce(Return(expectedTopology));
946
947 const DeviceInfo info = device->GetInfo(true);
948
949 ASSERT_TRUE(info.topology.has_value());
950}
951
953TEST_F(ProfinetDeviceInfoTests, DoesNotReadTopologyByDefault)
954{
955 EXPECT_CALL(*rpc, ReadPdRealData()).Times(0);
956
957 const DeviceInfo info = device->GetInfo();
958
959 EXPECT_FALSE(info.topology.has_value());
960}
961
962// ============================================================================
963// ProfinetDeviceStringTests
964// ============================================================================
965
966class ProfinetDeviceStringTests : public ::testing::Test
967{
968};
969
971TEST_F(ProfinetDeviceStringTests, ToStringShowsDisconnectedState)
972{
973 ProfinetDevice device(
974 MakeInfo(),
975 "eth0",
976 kControllerMac);
977
978 EXPECT_EQ(
979 device.ToString(),
980 "ProfinetDevice('dev', 192.168.0.10, disconnected)");
981}
982
983} // namespace profinet::device
Google Mock implementation of IAlarmListener.
Failed to establish RPC connection.
Definition exceptions.h:615
DCE/RPC protocol errors.
Definition exceptions.h:575
Bytes MakeModuleDiffBlock(std::uint16_t slot, std::uint16_t subslot, std::uint16_t moduleState, std::uint16_t submoduleState)
Bytes MakeBlockHeader(std::uint16_t blockType, std::uint16_t blockLength, std::uint8_t verHi=1, std::uint8_t verLo=0)
std::unique_ptr< ProfinetDevice > device
std::unique_ptr< ProfinetDevice > device
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
Google Mock implementation of the IRPCCon RPC connection interface.
Definition mockRPCCon.h:35
Lightweight cursor for parsing a big-endian byte buffer without copies.
Definition wire.h:192
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Definition wire.h:258
High-level PROFINET device interface and device lifecycle management.
Declares exceptions and error types used by the PROFINET IO controller stack.
PROFINET record-data indices, block types, and protocol enumerations.
Google Mock implementation of the PROFINET alarm listener interface.
Google Mock implementation of the IRPCCon PROFINET RPC connection interface.
ModuleDiffBlock ParseModuleDiffBlock(const Bytes &data)
Parse a ModuleDiffBlock (0x8104) from raw bytes.
Definition blocks.cpp:609
bool IsMacAddress(const std::string &identifier)
Whether a string parses as a MAC address.
Definition device.cpp:173
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
constexpr MacAddress kControllerMac
void PutU8(std::vector< std::uint8_t > &out, std::uint8_t value)
Append a single byte to a buffer.
Definition wire.h:89
void PutU16(std::vector< std::uint8_t > &out, std::uint16_t value)
Append a 16-bit value to a buffer in big-endian order.
Definition wire.h:97
void PutU32(std::vector< std::uint8_t > &out, std::uint32_t value)
Append a 32-bit value to a buffer in big-endian order.
Definition wire.h:105
@ ModuleDifference
Module difference block.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
constexpr std::uint16_t IM2
I&M2.
Definition indices.h:705
constexpr std::uint16_t IM1
I&M1.
Definition indices.h:703
constexpr std::uint16_t MODULE_STATE_PROPER_MODULE
Module State Proper Module.
Definition indices.h:645
constexpr std::uint16_t SUBMODULE_STATE_OK
Submodule State Ok.
Definition indices.h:658
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
constexpr std::uint16_t IM3
I&M3.
Definition indices.h:707
Complete parsed AlarmNotification PDU: header fields + parsed items.
Definition alarms.h:356
IOD read/write request/response header (RPC body for READ/WRITE/IMPLICIT_READ).
Definition protocol.h:1190
Bytes payload
Record data payload.
Definition protocol.h:1229
I&M0: mandatory identification data (vendor, order ID, serial number, revisions).
Definition protocol.h:1517
I&M1: user-assigned tag function and location strings.
Definition protocol.h:1624
I&M2: installation date.
Definition protocol.h:1668
I&M3: free-text descriptor.
Definition protocol.h:1709
I&M4: PROFIsafe signature (binary, not text).
Definition protocol.h:1749
I&M5: free-text annotation.
Definition protocol.h:1789
Parsed PDRealData (0xF841) structure.
Definition blocks.h:221
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
std::optional< blocks::PDRealData > topology
Physical topology, if it was read successfully.
Definition device.h:130
A single write operation for ProfinetDevice::WriteMultiple().
Definition device.h:74
std::uint16_t slot
Slot number.
Definition device.h:76
Complete diagnosis data for one slot/subslot.
Definition diagnosis.h:275
All I&M records a device supports, read in one call.
Definition rpcTypes.h:451