ChimeraTK-ApplicationCore 04.06.00
Loading...
Searching...
No Matches
testDeviceInitialisationHandler.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#define BOOST_TEST_MODULE testDeviceInitialisationHandler
5
6#include "Application.h"
7#include "check_timeout.h"
8#include "DeviceModule.h"
9#include "TestFacility.h"
10#include "Utilities.h"
11
12#include <ChimeraTK/Device.h>
13#include <ChimeraTK/ExceptionDummyBackend.h>
14
15#include <boost/test/included/unit_test.hpp>
16
17#include <cstdlib>
18
19using namespace boost::unit_test_framework;
20namespace ctk = ChimeraTK;
21
23
24 static const std::string deviceCDD{"(ExceptionDummy?map=test.map)"};
25 static const std::string exceptionMessage{"DEBUG: runtime error intentionally cased in device initialisation"};
26
27 static std::atomic<bool> throwInInitialisation{false};
28 static std::atomic<int32_t> var1{0};
29 static std::atomic<int32_t> var2{0};
30 static std::atomic<int32_t> var3{0};
31
33 var1 = 42;
34 if(throwInInitialisation) {
35 throw ctk::runtime_error(exceptionMessage);
36 }
37 }
38
40 // the initialisation of reg 2 must happen after the initialisation of reg1
41 var2 = var1 + 5;
42 }
43
45 // the initialisation of reg 3 must happen after the initialisation of reg2
46 var3 = var2 + 5;
47 }
48
49 /* dummy application */
51 TestApplication() : Application("testSuite") {}
52 ~TestApplication() override { shutdown(); }
53
54 ctk::DeviceModule dev{this, deviceCDD, "", &initialiseReg1};
55 };
56
57 /********************************************************************************************************************/
58
59 BOOST_AUTO_TEST_CASE(TestBasicInitialisation) {
60 std::cout << "testBasicInitialisation" << std::endl;
62
63 var1 = 0;
64 var2 = 0;
65 var3 = 0;
66
67 ctk::TestFacility test{app};
68 test.runApplication();
69 ctk::Device dummy;
70 dummy.open(deviceCDD);
71
72 // ********************************************************
73 // REQUIRED TEST 1: After opening the device is initialised
74 // ********************************************************
75 BOOST_CHECK_EQUAL(var1, 42);
76
77 var1 = 0;
78
79 // check that accessing an exception triggers a reconnection with re-initialisation
80 auto dummyBackend =
81 boost::dynamic_pointer_cast<ctk::ExceptionDummy>(ctk::BackendFactory::getInstance().createBackend(deviceCDD));
82 dummyBackend->throwExceptionWrite = true;
83
84 auto reg2_cs = test.getScalar<int32_t>("/REG2");
85 reg2_cs = 19;
86 reg2_cs.write();
87 test.stepApplication(false);
88
89 BOOST_CHECK_EQUAL(var2, 0);
90 BOOST_CHECK_EQUAL(var1, 0);
91 dummyBackend->throwExceptionWrite = false; // now the device should work again and be re-initialised
92
93 reg2_cs = 20;
94 reg2_cs.write();
95 test.stepApplication();
96
97 BOOST_CHECK_EQUAL(dummy.read<int32_t>("/REG2"), 20);
98
99 // ****************************************************************
100 // REQUIRED TEST 2: After an exception the device is re-initialised
101 // ****************************************************************
102 BOOST_CHECK_EQUAL(var1, 42);
103 }
104
105 /********************************************************************************************************************/
106
107 BOOST_AUTO_TEST_CASE(TestMultipleInitialisationHandlers) {
108 std::cout << "testMultipleInitialisationHandlers" << std::endl;
109 TestApplication app;
110
111 var1 = 0;
112 var2 = 0;
113 var3 = 0;
114
117 ctk::TestFacility test{app};
118 test.runApplication();
119
120 auto deviceStatus = test.getScalar<int32_t>(
121 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(deviceCDD, false) / "status");
122
123 // *********************************************************
124 // REQUIRED TEST 4: Handlers are executed in the right order
125 // *********************************************************
126 BOOST_CHECK_EQUAL(var1, 42);
127 BOOST_CHECK_EQUAL(var2, 47); // the initialiser used reg1+5, so order matters
128 BOOST_CHECK_EQUAL(var3, 52); // the initialiser used reg2+5, so order matters
129
130 // check that after an exception the re-initialisation is OK
131 var1 = 0;
132 var2 = 0;
133 var3 = 0;
134
135 // cause an exception
136 auto dummyBackend =
137 boost::dynamic_pointer_cast<ctk::ExceptionDummy>(ctk::BackendFactory::getInstance().createBackend(deviceCDD));
138 dummyBackend->throwExceptionWrite = true;
139
140 auto reg4_cs = test.getScalar<int32_t>("/REG4");
141 reg4_cs = 19;
142 reg4_cs.write();
143 test.stepApplication(false);
144
145 // recover
146 dummyBackend->throwExceptionWrite = false;
147
148 reg4_cs = 20;
149 reg4_cs.write();
150 test.stepApplication();
151
152 BOOST_CHECK_EQUAL(var1, 42);
153 BOOST_CHECK_EQUAL(var2, 47); // the initialiser used reg1+5, so order matters
154 BOOST_CHECK_EQUAL(var3, 52); // the initialiser used reg2+5, so order matters
155 }
156
157 /********************************************************************************************************************/
158
159 BOOST_AUTO_TEST_CASE(TestInitialisationException) {
160 std::cout << "testInitialisationException" << std::endl;
161
162 var1 = 0;
163 var2 = 0;
164 var3 = 0;
165
166 throwInInitialisation = true;
167 TestApplication app;
168
171 // app.dev.connectTo(app.cs);
172 ctk::TestFacility test(app, false); // test facility without testable mode
173
174 auto status = test.getScalar<int32_t>(
175 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(std::string(deviceCDD), false) / "status");
176 auto status_message = test.getScalar<std::string>(
177 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(std::string(deviceCDD), false) / "status_message");
178
179 ctk::Device dummy;
180 dummy.open(deviceCDD);
181
182 // We cannot use runApplication because the DeviceModule leaves the testable mode without variables in the queue,
183 // but has not finished error handling yet. In this special case we cannot make the programme continue, because
184 // stepApplication only works if the queues are not empty. We have to work with timeouts here (until someone comes
185 // up with a better idea)
186 app.run();
187 // app.dumpConnections();
188
189 // initial values of device status
190 BOOST_TEST(status.readAndGet() == 1);
191 auto initialMessage = status_message.readAndGet();
192
193 // wait for exception, message shall be changed
194 status_message.read();
195 BOOST_TEST(std::string(status_message) != initialMessage);
196
197 // Check that the execution of init handlers was stopped after the exception:
198 // initialiseReg2 and initialiseReg3 were not executed. As we already checked with timeout that the
199 // initialisation error has been reported, we know that the data was written to the device and don't need the
200 // timeout here.
201
202 usleep(10000); // allow bugs to manifest (e.g. if manager would continue with other handlers after exception)
203
204 BOOST_CHECK_EQUAL(var1, 42);
205 BOOST_CHECK_EQUAL(var2, 0);
206 BOOST_CHECK_EQUAL(var3, 0);
207
208 // recover the error
209 throwInInitialisation = false;
210
211 // wait until the device is reported to be OK again (check with timeout),
212 // then check the initialisation (again, no extra timeout needed because of the logic:
213 // success is only reported after successful init).
214 CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(
215 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(deviceCDD, false) / "status"),
216 0, 30000);
217 // We use the macro here for convenience, it's a test, speed should not matter
218 // NOLINTNEXTLINE(readability-container-size-empty)
219 CHECK_EQUAL_TIMEOUT(test.readScalar<std::string>(ctk::RegisterPath("/Devices") /
220 ctk::Utilities::escapeName(deviceCDD, false) / "status_message"),
221 "", 30000);
222
223 // initialisation should be correct now
224 BOOST_CHECK_EQUAL(var1, 42);
225 BOOST_CHECK_EQUAL(var2, 47);
226 BOOST_CHECK_EQUAL(var3, 52);
227
228 // now check that the initialisation error is also reported when recovering
229 // Prepare registers to be initialised
230 var1 = 12;
231 var2 = 13;
232 var3 = 14;
233
234 // Make initialisation fail when executed, and then cause an error condition
235 throwInInitialisation = true;
236 auto dummyBackend =
237 boost::dynamic_pointer_cast<ctk::ExceptionDummy>(ctk::BackendFactory::getInstance().createBackend(deviceCDD));
238 dummyBackend->throwExceptionWrite = true;
239
240 auto reg4_cs = test.getScalar<int32_t>("/REG4");
241 reg4_cs = 20;
242 reg4_cs.write();
243
244 CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(
245 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(deviceCDD, false) / "status"),
246 1, 30000);
247 // First we see the message from the failing write
248 CHECK_TIMEOUT(!test
249 .readScalar<std::string>(ctk::RegisterPath("/Devices") /
250 ctk::Utilities::escapeName(deviceCDD, false) / "status_message")
251 .empty(),
252 30000);
253 dummyBackend->throwExceptionWrite = false;
254 // Afterwards we see a message from the failing initialisation (which we can now distinguish from the original write
255 // exception because write does not throw any more)
256 CHECK_EQUAL_TIMEOUT(test.readScalar<std::string>(ctk::RegisterPath("/Devices") /
257 ctk::Utilities::escapeName(deviceCDD, false) / "status_message"),
258 exceptionMessage, 30000);
259
260 // Now fix the initialisation error and check that the device comes up.
261 throwInInitialisation = false;
262 // Wait until the device is OK again
263 CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(
264 ctk::RegisterPath("/Devices") / ctk::Utilities::escapeName(deviceCDD, false) / "status"),
265 0, 30000);
266
267 // We use the macro here for convenience, it's a test, speed should not matter
268 // NOLINTNEXTLINE(readability-container-size-empty)
269 CHECK_EQUAL_TIMEOUT(test.readScalar<std::string>(ctk::RegisterPath("/Devices") /
270 ctk::Utilities::escapeName(deviceCDD, false) / "status_message"),
271 "", 30000);
272 // Finally check that the 20 arrives on the device
273 CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/REG4"), 20, 30000);
274 }
275
276 /********************************************************************************************************************/
282 BOOST_AUTO_TEST_CASE(TestDeviceClosedInInitHandler) {
283 // Check that the device has been closed when the init handler is called.
284 std::cout << "TestDeviceClosedInInitHandler" << std::endl;
285
286 TestApplication app;
287 // Cache the opened state in the init handler in a variable. BOOST_CHECK is not threat safe and
288 // cannot directly be used in the handler.
289 bool isOpenedInInitHandler{
290 true}; // We expect false, so we set the starting value to true to know the test is sensitive.
291 app.dev.addInitialisationHandler([&](ctk::Device& d) { isOpenedInInitHandler = d.isOpened(); });
292
293 ctk::TestFacility testFacility(app);
294 testFacility.runApplication();
295 // The testFacility in testable mode guarantees that the device has been opened at this point. So we know the init
296 // handler with the test has been run at this point.
297 BOOST_CHECK(!isOpenedInInitHandler);
298 }
299
300} // namespace Tests::testDeviceInitialisationHandler
#define CHECK_EQUAL_TIMEOUT(left, right, maxMilliseconds)
void run() override
Execute the module.
void shutdown() override
This will remove the global pointer to the instance and allows creating another instance afterwards.
void addInitialisationHandler(std::function< void(ChimeraTK::Device &)> initialisationHandler)
friend class Application
Definition ModuleGroup.h:47
Helper class to facilitate tests of applications based on ApplicationCore.
TYPE readScalar(const std::string &name)
Convenience function to read the latest value of a scalar process variable in a single call.
ChimeraTK::ScalarRegisterAccessor< T > getScalar(const ChimeraTK::RegisterPath &name) const
Obtain a scalar process variable from the application, which is published to the control system.
void runApplication() const
Start the application in testable mode.
InvalidityTracer application module.
#define CHECK_TIMEOUT(condition, maxMilliseconds)