ChimeraTK-ApplicationCore 04.08.00
Loading...
Searching...
No Matches
testScriptedInitialisationHandler.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#define BOOST_TEST_MODULE testScriptedInitialisationHandler
4#include "Application.h"
5#include "check_timeout.h"
6#include "DeviceModule.h"
8#include "TestFacility.h"
9
10#include <boost/test/included/unit_test.hpp>
11
12#include <filesystem>
13#include <fstream>
14
16
17 using namespace ChimeraTK;
18
19 /********************************************************************************************************************/
20
21 struct TestApp : public Application {
23 ~TestApp() override { shutdown(); }
24
25 SetDMapFilePath dmap{"test.dmap"};
26
27 DeviceModule dev1{this, "Dummy0",
28 "/MyModule/actuator"}; // pick one of the writable variables to AC knows that data type for the trigger
29
30 // default name for the output variable (initScriptOutput)
31 ScriptedInitHandler initHandler1{this, "InitHander1", "description", "./deviceInitScript1.bash", dev1};
32 // change the name of the output variable in case a second script is needed. Shorten the error grace time to 1 second
33 ScriptedInitHandler initHandler2{this, "InitHander2", "description", "./deviceInitScript2.bash", dev1,
34 "secondInitScriptOutput", "secondInitScriptExitCode", 1};
35 };
36
37 /********************************************************************************************************************/
38
39 struct Fixture {
40 TestApp testApp{"ScriptedInitApp"};
43 // always clear the temporary files before each test
45 }
47 // always clear the temporary files after each test
49 }
50 static void clearTemporaryFiles() {
51 (void)std::filesystem::remove("device1Init.complete");
52 (void)std::filesystem::remove("blockDevice1Init");
53 (void)std::filesystem::remove("produceDevice2InitError");
54 }
55 };
56
57 /********************************************************************************************************************/
58 /********************************************************************************************************************/
59 // Test that the script is actually executed
61 testFacility.runApplication();
62
63 CHECK_TIMEOUT(testFacility.readScalar<int32_t>("/Devices/Dummy0/status") == 0, 30000);
64 // As soon as the device reports ready, the init script effect must be visible. This is just a smoke test that the
65 // ScriptedInitialisationHandler actually executes the specified script. It does not check the initialisation
66 // handler spec that the call is happening at the right time in the recovery process, which is tested separately.
67 BOOST_CHECK(std::filesystem::exists("device1Init.complete"));
68 }
69
70 /********************************************************************************************************************/
71 // Test that the message is published to the control system. Lines appear in the correct order.
73 testFacility.runApplication();
74
75 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/initScriptOutput");
76
77 // Only test texts snippets and their order. There might be other stray output in the stuff collected from the
78 // command line, so don't test for exact string equality.
79 constexpr auto firstSnippet = "starting device1 init";
80 constexpr auto secondSnippet = "device1 init complete";
81
82 // Wait for the second message snippet.
83 size_t secondSnippetPos;
84 CHECK_TIMEOUT((initMessage.readLatest(),
85 (secondSnippetPos = std::string(initMessage).find(secondSnippet)) != std::string::npos),
86 30000);
87 // The first snippet is also there, and before the second snippet.
88 BOOST_TEST(std::string(initMessage).find(firstSnippet) < secondSnippetPos);
89 std::cout << std::string(initMessage) << std::endl;
90
91 BOOST_CHECK(std::filesystem::exists("device1Init.complete"));
92 }
93
94 /********************************************************************************************************************/
95 // A success message is appended after the script output.
97 testFacility.runApplication();
98
99 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/initScriptOutput");
100
101 constexpr auto scriptOutputSnippet = "device1 init complete";
102 constexpr auto successSnippet = "SUCCESS"; // We intentionally do not test the exact line but only a snipped to be
103 // less sensitive to message refactoring.
104
105 size_t successSnippetPos;
106 CHECK_TIMEOUT((initMessage.readLatest(),
107 (successSnippetPos = std::string(initMessage).find(successSnippet)) != std::string::npos),
108 30000);
109 // Tests that the script snippet is there before the success message.
110 BOOST_TEST(std::string(initMessage).find(scriptOutputSnippet) < successSnippetPos);
111 }
112
113 /********************************************************************************************************************/
114 // The script output does not only occur at the end of the script. Lines printed by the script are already shown
115 // while the script is still running.
116 BOOST_FIXTURE_TEST_CASE(TestPartialOutput, Fixture) {
117 // Don't let the script finish, so we can do a check without race condition and know the test is sensitive.
118 std::ofstream blockFile;
119 blockFile.open("blockDevice1Init", std::ios::out);
120
121 testFacility.runApplication();
122
123 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/initScriptOutput");
124
125 constexpr auto firstSnippet = "starting device1 init";
126 constexpr auto secondSnippet = "device1 init complete";
127
128 // The first snippet is in the message, the second not yet. The script has not reached the complete step yet.
129 CHECK_TIMEOUT((initMessage.readLatest(), std::string(initMessage).find(firstSnippet) != std::string::npos), 30000);
130 BOOST_TEST(std::string(initMessage).find(secondSnippet) == std::string::npos);
131 BOOST_CHECK(!std::filesystem::exists("device1Init.complete"));
132
133 // let the script finish
134 (void)std::filesystem::remove("blockDevice1Init");
135
136 // Wait for the second message snippet.
137 size_t secondSnippetPos;
138 CHECK_TIMEOUT((initMessage.readLatest(),
139 (secondSnippetPos = std::string(initMessage).find(secondSnippet)) != std::string::npos),
140 30000);
141 // The first snippet is still in there, and before the second snippet.
142 BOOST_TEST(std::string(initMessage).find(firstSnippet) < secondSnippetPos);
143
144 BOOST_CHECK(std::filesystem::exists("device1Init.complete"));
145 }
146
147 /********************************************************************************************************************/
148 // Two differennt scripts on the same device have different outputs.
150 testFacility.runApplication();
151
152 constexpr auto script1Snippet = "device1 init complete";
153 constexpr auto script2Snippet = "just a second script";
154 constexpr auto successSnippet = "SUCCESS";
155
156 // wait for the success snippet in message1. It must contain the first, but not the second snippet.
157 auto initMessage1 = testFacility.getScalar<std::string>("/Devices/Dummy0/initScriptOutput");
159 (initMessage1.readLatest(), std::string(initMessage1).find(successSnippet) != std::string::npos), 30000);
160 BOOST_TEST(std::string(initMessage1).find(script1Snippet) != std::string::npos);
161 BOOST_TEST(std::string(initMessage1).find(script2Snippet) == std::string::npos);
162
163 auto initExitCode1 = testFacility.getScalar<int>("/Devices/Dummy0/initScriptExitCode");
164 CHECK_TIMEOUT(initExitCode1.readNonBlocking(), 20000);
165 BOOST_TEST(!initExitCode1.readNonBlocking());
166 BOOST_TEST(initExitCode1 == 0);
167
168 // same for snippets in message2.
169 auto initMessage2 = testFacility.getScalar<std::string>("/Devices/Dummy0/secondInitScriptOutput");
171 (initMessage2.readLatest(), std::string(initMessage2).find(successSnippet) != std::string::npos), 30000);
172 BOOST_TEST(std::string(initMessage2).find(script1Snippet) == std::string::npos);
173 BOOST_TEST(std::string(initMessage2).find(script2Snippet) != std::string::npos);
174
175 auto initExitCode2 = testFacility.getScalar<int>("/Devices/Dummy0/secondInitScriptExitCode");
176 CHECK_TIMEOUT(initExitCode2.readNonBlocking(), 20000);
177 BOOST_TEST(!initExitCode2.readNonBlocking());
178 BOOST_TEST(initExitCode2 == 0);
179 }
180
181 /********************************************************************************************************************/
182 // The error message is renewed with each run of the script, so changing error messages can be seen.
184 std::ofstream produceErrorFile; // If the file exists, the script produces an error
185 produceErrorFile.open("produceDevice2InitError", std::ios::out);
186 produceErrorFile << 0 << std::flush;
187
188 testFacility.runApplication();
189
190 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/secondInitScriptOutput");
191 auto initExitCode2 = testFacility.getScalar<int>("/Devices/Dummy0/secondInitScriptExitCode");
192
193 // let the script run three times, check that always the output of the last run is visible in the control system
194 for(int i = 0; i < 3; ++i) {
195 produceErrorFile.seekp(0);
196 produceErrorFile << i << std::flush;
197 std::string expectedSnippet = "Simulating error in second script: " + std::to_string(i);
199 (initMessage.readLatest(), std::string(initMessage).find(expectedSnippet) != std::string::npos), 20000);
200 // renewed means the old message is replaced
201 if(i == 1) {
202 BOOST_TEST(std::string(initMessage).find("script: 0") == std::string::npos);
203 }
204 if(i == 2) {
205 BOOST_TEST(std::string(initMessage).find("script: 1") == std::string::npos);
206 }
207
208 CHECK_TIMEOUT(initExitCode2.readNonBlocking(), 20000);
209 BOOST_TEST(!initExitCode2.readNonBlocking());
210 BOOST_TEST(initExitCode2 == 42);
211 }
212
213 // recovery
214 (void)std::filesystem::remove("produceDevice2InitError");
215 CHECK_TIMEOUT(testFacility.readScalar<int32_t>("/Devices/Dummy0/status") == 0, 30000);
216
217 CHECK_TIMEOUT(initExitCode2.readNonBlocking(), 20000);
218 BOOST_TEST(!initExitCode2.readNonBlocking());
219 BOOST_TEST(initExitCode2 == 0);
220 }
221
222 /********************************************************************************************************************/
223 // A failure message is appended if the script fails.
224 BOOST_FIXTURE_TEST_CASE(TestFailureMessage, Fixture) {
225 std::ofstream produceErrorFile; // If the file exists, the script produces an error
226 produceErrorFile.open("produceDevice2InitError", std::ios::out);
227
228 testFacility.runApplication();
229
230 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/secondInitScriptOutput");
231
232 std::string scriptSnippet = "Simulating error in second script:";
233 std::string failSnippet = "FAILED";
234
235 size_t failSnippedPos;
237 (initMessage.readLatest(), (failSnippedPos = std::string(initMessage).find(failSnippet)) != std::string::npos),
238 20000);
239 BOOST_TEST(std::string(initMessage).find(scriptSnippet) < failSnippedPos);
240
241 // recovery
242 (void)std::filesystem::remove("produceDevice2InitError");
243 CHECK_TIMEOUT(testFacility.readScalar<int32_t>("/Devices/Dummy0/status") == 0, 30000);
244 }
245
246 /********************************************************************************************************************/
247 // A successful message replaces the previous failure message.
248 BOOST_FIXTURE_TEST_CASE(TestSuccessAfterFailure, Fixture) {
249 std::ofstream produceErrorFile; // If the file exists, the script produces an error
250 produceErrorFile.open("produceDevice2InitError", std::ios::out);
251
252 testFacility.runApplication();
253
254 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/secondInitScriptOutput");
255 std::string failSnippet = "FAILED";
256 std::string successSnippet = "SUCCESS";
257
258 // wait for fail message to appear
259 CHECK_TIMEOUT((initMessage.readLatest(), std::string(initMessage).find(failSnippet) != std::string::npos), 20000);
260
261 // recovery
262 (void)std::filesystem::remove("produceDevice2InitError");
263
264 // Wait for success message to appear
266 (initMessage.readLatest(), std::string(initMessage).find(successSnippet) != std::string::npos), 20000);
267
268 // The actual test: The failure message is not part of the message any more
269 BOOST_TEST(std::string(initMessage).find(failSnippet) == std::string::npos);
270 }
271
272 /********************************************************************************************************************/
273 // There is an error grace period which limits the retry rate.
274 BOOST_FIXTURE_TEST_CASE(TestErrorGracePeriod, Fixture) {
275 std::ofstream produceErrorFile; // If the file exists, the script produces an error
276 produceErrorFile.open("produceDevice2InitError", std::ios::out);
277 produceErrorFile << 1 << std::flush;
278
279 testFacility.runApplication();
280
281 auto initMessage = testFacility.getScalar<std::string>("/Devices/Dummy0/secondInitScriptOutput");
282
283 auto startTime = std::chrono::steady_clock::now();
284 // Let the script run three times, and wait for according error message to show. This is not the
285 // actual test here, just a pre-conditon for measuring the time.
286 for(int i = 0; i < 3; ++i) {
287 produceErrorFile.seekp(0);
288 produceErrorFile << i << std::flush;
289 std::string expectedSnippet = "Simulating error in second script: " + std::to_string(i);
291 (initMessage.readLatest(), std::string(initMessage).find(expectedSnippet) != std::string::npos), 20000);
292 }
293
294 // recovery
295 (void)std::filesystem::remove("produceDevice2InitError");
296 CHECK_TIMEOUT(testFacility.readScalar<int32_t>("/Devices/Dummy0/status") == 0, 30000);
297
298 // The actual test: At least three failure grace periods have passed.
299 auto stopTime = std::chrono::steady_clock::now();
300 BOOST_CHECK(std::chrono::duration_cast<std::chrono::seconds>(stopTime - startTime).count() >= 3);
301 }
302 /********************************************************************************************************************/
303
304} // namespace Tests::testScriptedInitialisationHandler
Application(const std::string &name)
The constructor takes the application name as an argument.
void shutdown() override
This will remove the global pointer to the instance and allows creating another instance afterwards.
Helper class to set the DMAP file path.
Helper class to facilitate tests of applications based on ApplicationCore.
InvalidityTracer application module.
Initialisation handler which calls an external application (usually a script), captures its output (b...
#define CHECK_TIMEOUT(condition, maxMilliseconds)