mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
application_test.cpp
Go to the documentation of this file.
1/** \file application_test.cpp
2 * \brief Tests for the application framework lifecycle.
3 */
4#include "../../catch2/catch.hpp"
5
6#include <filesystem>
7#include <fstream>
8#include <cstdlib>
9#include <sstream>
10
12
13/** \defgroup application_unit_tests application Unit Tests
14 * \ingroup app_unit_tests
15 */
16
17/// \cond
18namespace unitTest::app_application_test
19{
20
21class temporaryConfigFile
22{
23 public:
24 explicit temporaryConfigFile( const std::string &contents )
25 : m_path( std::filesystem::temp_directory_path() / "mxlib_application_test.conf" )
26 {
27 std::ofstream file{ m_path };
28 file << contents;
29 }
30
31 ~temporaryConfigFile()
32 {
33 std::filesystem::remove( m_path );
34 }
35
36 std::string string() const
37 {
38 return m_path.string();
39 }
40
41 private:
42 std::filesystem::path m_path;
43};
44
45class scopedEnvironment
46{
47 public:
48 scopedEnvironment( const std::string &name, const std::string &value ) : m_name( name )
49 {
50 if( const char *oldValue = std::getenv( m_name.c_str() ) )
51 {
52 m_oldValue = oldValue;
53 m_hadOldValue = true;
54 }
55
56 setenv( m_name.c_str(), value.c_str(), 1 );
57 }
58
59 ~scopedEnvironment()
60 {
61 if( !m_hadOldValue )
62 {
63 unsetenv( m_name.c_str() );
64 }
65 else
66 {
67 setenv( m_name.c_str(), m_oldValue.c_str(), 1 );
68 }
69 }
70
71 private:
72 std::string m_name;
73 std::string m_oldValue;
74 bool m_hadOldValue{ false };
75};
76
77class scopedCerr
78{
79 public:
80 explicit scopedCerr( std::ostream &stream ) : m_oldBuffer( std::cerr.rdbuf( stream.rdbuf() ) )
81 {
82 }
83
84 ~scopedCerr()
85 {
86 std::cerr.rdbuf( m_oldBuffer );
87 }
88
89 private:
90 std::streambuf *m_oldBuffer;
91};
92
93class lifecycleApplication : public mx::app::application
94{
95 public:
96 lifecycleApplication()
97 {
98 m_requireConfigPathGlobal = false;
99 m_requireConfigPathUser = false;
100 m_requireConfigPathLocal = false;
101 }
102
103 int m_setupCalls{ 0 };
104 int m_loadCalls{ 0 };
105 int m_executeCalls{ 0 };
106 int m_helpCalls{ 0 };
107 int m_value{ 0 };
108 bool m_configAvailableInExecute{ false };
109
110 void preserveConfig( bool preserve )
111 {
112 m_preserveConfig = preserve;
113 }
114
115 void configOnly( bool configOnly )
116 {
117 m_configOnly = configOnly;
118 }
119
120 void requireGlobalConfig( bool require )
121 {
122 m_requireConfigPathGlobal = require;
123 }
124
125 void useDefaultHelp( bool useDefault )
126 {
127 m_useDefaultHelp = useDefault;
128 }
129
130 void setPathEnvironmentNames( const std::string &global,
131 const std::string &user,
132 const std::string &local,
133 const std::string &configBase )
134 {
135 m_configPathGlobal_env = global;
136 m_configPathUser_env = user;
137 m_configPathLocal_env = local;
138 m_configPathCLBase_env = configBase;
139 }
140
141 void applyDefaults()
142 {
143 setDefaults( 0, nullptr );
144 }
145
146 int reRead()
147 {
148 return reReadConfig();
149 }
150
151 int configuredValueCount()
152 {
153 return config.count( "value" );
154 }
155
156 const std::string &globalPath() const
157 {
158 return m_configPathGlobal;
159 }
160
161 const std::string &userPath() const
162 {
163 return m_configPathUser;
164 }
165
166 const std::string &localPath() const
167 {
168 return m_configPathLocal;
169 }
170
171 const std::string &configBasePath() const
172 {
173 return m_configPathCLBase;
174 }
175
176 protected:
177 void setupConfig() override
178 {
179 ++m_setupCalls;
180 config.add( "value", "v", "value", mx::app::argType::Required, "test", "value", false, "int", "" );
181 }
182
183 void loadConfig() override
184 {
185 ++m_loadCalls;
186 config( m_value, "value" );
187 }
188
189 int execute() override
190 {
191 ++m_executeCalls;
192 m_configAvailableInExecute = config.isSet( "value" );
193 return m_value;
194 }
195
196 void help() override
197 {
198 ++m_helpCalls;
199 if( m_useDefaultHelp )
200 {
201 application::help();
202 }
203 }
204
205 private:
206 bool m_useDefaultHelp{ false };
207};
208
209} // namespace unitTest::app_application_test
210/// \endcond
211
212namespace unitTest::app_application_test
213{
214
215/** \brief Verifies that mx::app::application runs setup, loading, and execution in order.
216 *
217 * \ingroup application_unit_tests
218 */
219TEST_CASE( "application runs the configured lifecycle", "[app::application]" )
220{
221 lifecycleApplication app;
222 char invokedName[] = "lifecycleApplication";
223 char valueOption[] = "--value=17";
224 char *argv[] = { invokedName, valueOption };
225
226 REQUIRE( app.main( 2, argv ) == 17 );
227 REQUIRE( app.m_setupCalls == 1 );
228 REQUIRE( app.m_loadCalls == 1 );
229 REQUIRE( app.m_executeCalls == 1 );
230 REQUIRE( app.m_value == 17 );
231 REQUIRE_FALSE( app.m_configAvailableInExecute );
232}
233
234/** \brief Verifies that mx::app::application preserves configuration only when requested.
235 *
236 * \ingroup application_unit_tests
237 */
238TEST_CASE( "application optionally preserves configuration for execution", "[app::application]" )
239{
240 lifecycleApplication app;
241 app.preserveConfig( true );
242 char invokedName[] = "lifecycleApplication";
243 char valueOption[] = "--value=23";
244 char *argv[] = { invokedName, valueOption };
245
246 REQUIRE( app.main( 2, argv ) == 23 );
247 REQUIRE( app.m_value == 23 );
248 REQUIRE( app.m_configAvailableInExecute );
249}
250
251/** \brief Verifies that mx::app::application short-circuits execution for help and config-only requests.
252 *
253 * \ingroup application_unit_tests
254 */
255TEST_CASE( "application short-circuits help and config-only requests", "[app::application]" )
256{
257 SECTION( "help" )
258 {
259 lifecycleApplication app;
260 char invokedName[] = "lifecycleApplication";
261 char helpOption[] = "--help";
262 char *argv[] = { invokedName, helpOption };
263
264 REQUIRE( app.main( 2, argv ) == 1 );
265 REQUIRE( app.m_setupCalls == 1 );
266 REQUIRE( app.m_loadCalls == 0 );
267 REQUIRE( app.m_helpCalls == 1 );
268 REQUIRE( app.m_executeCalls == 0 );
269 }
270
271 SECTION( "config-only" )
272 {
273 lifecycleApplication app;
274 app.configOnly( true );
275 char invokedName[] = "lifecycleApplication";
276 char valueOption[] = "--value=41";
277 char *argv[] = { invokedName, valueOption };
278
279 REQUIRE( app.main( 2, argv ) == 1 );
280 REQUIRE( app.m_setupCalls == 1 );
281 REQUIRE( app.m_loadCalls == 1 );
282 REQUIRE( app.m_executeCalls == 0 );
283 }
284}
285
286/** \brief Verifies configuration-file loading, command-line precedence, and required-file failure behavior.
287 *
288 * \ingroup application_unit_tests
289 */
290TEST_CASE( "application loads configuration files", "[app::application]" )
291{
292 SECTION( "command-line config and value override" )
293 {
294 temporaryConfigFile configFile{ "[test]\nvalue=31\n" };
295 lifecycleApplication app;
296 char invokedName[] = "lifecycleApplication";
297 std::string configOption = "--config=" + configFile.string();
298 char valueOption[] = "--value=37";
299 char *argv[] = { invokedName, configOption.data(), valueOption };
300
301 REQUIRE( app.main( 3, argv ) == 37 );
302 REQUIRE( app.m_loadCalls == 1 );
303 REQUIRE( app.m_executeCalls == 1 );
304 REQUIRE( app.m_value == 37 );
305 }
306
307 SECTION( "required global config failure" )
308 {
309 lifecycleApplication app;
310 app.requireGlobalConfig( true );
311 app.setConfigPathGlobal( "/tmp/mxlib_application_test_missing.conf" );
312 char invokedName[] = "lifecycleApplication";
313 char *argv[] = { invokedName };
314
315 REQUIRE( app.main( 1, argv ) == 1 );
316 REQUIRE( app.m_loadCalls == 0 );
317 REQUIRE( app.m_helpCalls == 1 );
318 REQUIRE( app.m_executeCalls == 0 );
319 }
320}
321
322/** \brief Verifies that mx::app::application resolves environment-selected configuration search paths.
323 *
324 * \ingroup application_unit_tests
325 */
326TEST_CASE( "application resolves configuration paths from the environment", "[app::application]" )
327{
328 scopedEnvironment global{ "MXLIB_APPLICATION_TEST_GLOBAL", "/tmp/global.conf" };
329 scopedEnvironment user{ "MXLIB_APPLICATION_TEST_USER", "/tmp/user.conf" };
330 scopedEnvironment local{ "MXLIB_APPLICATION_TEST_LOCAL", "/tmp/local-config" };
331 scopedEnvironment configBase{ "MXLIB_APPLICATION_TEST_CONFIG_BASE", "/tmp/config-base" };
332
333 lifecycleApplication app;
334 app.setConfigPathGlobal( "ignored-global" );
335 app.setConfigPathUser( "ignored-user" );
336 app.setConfigPathLocal( "ignored-local" );
337 app.setPathEnvironmentNames( "MXLIB_APPLICATION_TEST_GLOBAL",
338 "MXLIB_APPLICATION_TEST_USER",
339 "MXLIB_APPLICATION_TEST_LOCAL",
340 "MXLIB_APPLICATION_TEST_CONFIG_BASE" );
341 app.applyDefaults();
342
343 REQUIRE( app.globalPath() == "/tmp/global.conf" );
344 REQUIRE( app.userPath() == "/tmp/user.conf" );
345 REQUIRE( app.localPath() == "/tmp/local-config/" );
346 REQUIRE( app.configBasePath() == "/tmp/config-base/" );
347}
348
349/** \brief Verifies that mx::app::application emits its default help for registered options.
350 *
351 * \ingroup application_unit_tests
352 */
353TEST_CASE( "application renders default help", "[app::application]" )
354{
355 lifecycleApplication app;
356 app.useDefaultHelp( true );
357 char invokedName[] = "lifecycleApplication";
358 char helpOption[] = "--help";
359 char *argv[] = { invokedName, helpOption };
360 std::ostringstream help;
361 scopedCerr capture{ help };
362
363 REQUIRE( app.main( 2, argv ) == 1 );
364 REQUIRE( help.str().find( "usage: lifecycleApplication" ) != std::string::npos );
365 REQUIRE( help.str().find( "--value" ) != std::string::npos );
366}
367
368/** \brief Verifies that mx::app::application re-reads registered configuration sources without rebuilding targets.
369 *
370 * \ingroup application_unit_tests
371 */
372TEST_CASE( "application re-reads its configuration stack", "[app::application]" )
373{
374 temporaryConfigFile configFile{ "[test]\nvalue=29\n" };
375 lifecycleApplication app;
376 app.preserveConfig( true );
377 char invokedName[] = "lifecycleApplication";
378 std::string configOption = "--config=" + configFile.string();
379 char *argv[] = { invokedName, configOption.data() };
380
381 REQUIRE( app.main( 2, argv ) == 29 );
382 REQUIRE( app.configuredValueCount() == 1 );
383 REQUIRE( app.reRead() == 0 );
384 REQUIRE( app.configuredValueCount() == 2 );
385 REQUIRE( app.m_setupCalls == 1 );
386 REQUIRE( app.m_loadCalls == 1 );
387}
388
389} // namespace unitTest::app_application_test
A class for managing application configuration and execution.
TEST_CASE("application runs the configured lifecycle", "[app::application]")
Verifies that mx::app::application runs setup, loading, and execution in order.
@ filesystem
A general filesystem error occurred.
Definition error_t.hpp:39