4#include "../../catch2/catch.hpp"
18namespace unitTest::app_application_test
21class temporaryConfigFile
24 explicit temporaryConfigFile(
const std::string &contents )
25 : m_path( std::
filesystem::temp_directory_path() /
"mxlib_application_test.conf" )
27 std::ofstream file{ m_path };
31 ~temporaryConfigFile()
33 std::filesystem::remove( m_path );
36 std::string string()
const
38 return m_path.string();
42 std::filesystem::path m_path;
45class scopedEnvironment
48 scopedEnvironment(
const std::string &name,
const std::string &value ) : m_name( name )
50 if(
const char *oldValue = std::getenv( m_name.c_str() ) )
52 m_oldValue = oldValue;
56 setenv( m_name.c_str(), value.c_str(), 1 );
63 unsetenv( m_name.c_str() );
67 setenv( m_name.c_str(), m_oldValue.c_str(), 1 );
73 std::string m_oldValue;
74 bool m_hadOldValue{
false };
80 explicit scopedCerr( std::ostream &stream ) : m_oldBuffer( std::cerr.rdbuf( stream.rdbuf() ) )
86 std::cerr.rdbuf( m_oldBuffer );
90 std::streambuf *m_oldBuffer;
93class lifecycleApplication :
public mx::app::application
96 lifecycleApplication()
98 m_requireConfigPathGlobal =
false;
99 m_requireConfigPathUser =
false;
100 m_requireConfigPathLocal =
false;
103 int m_setupCalls{ 0 };
104 int m_loadCalls{ 0 };
105 int m_executeCalls{ 0 };
106 int m_helpCalls{ 0 };
108 bool m_configAvailableInExecute{
false };
110 void preserveConfig(
bool preserve )
112 m_preserveConfig = preserve;
115 void configOnly(
bool configOnly )
117 m_configOnly = configOnly;
120 void requireGlobalConfig(
bool require )
122 m_requireConfigPathGlobal = require;
125 void useDefaultHelp(
bool useDefault )
127 m_useDefaultHelp = useDefault;
130 void setPathEnvironmentNames(
const std::string &global,
131 const std::string &user,
132 const std::string &local,
133 const std::string &configBase )
135 m_configPathGlobal_env = global;
136 m_configPathUser_env = user;
137 m_configPathLocal_env = local;
138 m_configPathCLBase_env = configBase;
143 setDefaults( 0,
nullptr );
148 return reReadConfig();
151 int configuredValueCount()
153 return config.count(
"value" );
156 const std::string &globalPath()
const
158 return m_configPathGlobal;
161 const std::string &userPath()
const
163 return m_configPathUser;
166 const std::string &localPath()
const
168 return m_configPathLocal;
171 const std::string &configBasePath()
const
173 return m_configPathCLBase;
177 void setupConfig()
override
180 config.add(
"value",
"v",
"value", mx::app::argType::Required,
"test",
"value",
false,
"int",
"" );
183 void loadConfig()
override
186 config( m_value,
"value" );
189 int execute()
override
192 m_configAvailableInExecute = config.isSet(
"value" );
199 if( m_useDefaultHelp )
206 bool m_useDefaultHelp{
false };
212namespace unitTest::app_application_test
219TEST_CASE(
"application runs the configured lifecycle",
"[app::application]" )
221 lifecycleApplication app;
222 char invokedName[] =
"lifecycleApplication";
223 char valueOption[] =
"--value=17";
224 char *argv[] = { invokedName, valueOption };
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 );
238TEST_CASE(
"application optionally preserves configuration for execution",
"[app::application]" )
240 lifecycleApplication app;
241 app.preserveConfig(
true );
242 char invokedName[] =
"lifecycleApplication";
243 char valueOption[] =
"--value=23";
244 char *argv[] = { invokedName, valueOption };
246 REQUIRE( app.main( 2, argv ) == 23 );
247 REQUIRE( app.m_value == 23 );
248 REQUIRE( app.m_configAvailableInExecute );
255TEST_CASE(
"application short-circuits help and config-only requests",
"[app::application]" )
259 lifecycleApplication app;
260 char invokedName[] =
"lifecycleApplication";
261 char helpOption[] =
"--help";
262 char *argv[] = { invokedName, helpOption };
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 );
271 SECTION(
"config-only" )
273 lifecycleApplication app;
274 app.configOnly(
true );
275 char invokedName[] =
"lifecycleApplication";
276 char valueOption[] =
"--value=41";
277 char *argv[] = { invokedName, valueOption };
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 );
290TEST_CASE(
"application loads configuration files",
"[app::application]" )
292 SECTION(
"command-line config and value override" )
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 };
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 );
307 SECTION(
"required global config failure" )
309 lifecycleApplication app;
310 app.requireGlobalConfig(
true );
311 app.setConfigPathGlobal(
"/tmp/mxlib_application_test_missing.conf" );
312 char invokedName[] =
"lifecycleApplication";
313 char *argv[] = { invokedName };
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 );
326TEST_CASE(
"application resolves configuration paths from the environment",
"[app::application]" )
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" };
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" );
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/" );
353TEST_CASE(
"application renders default help",
"[app::application]" )
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 };
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 );
372TEST_CASE(
"application re-reads its configuration stack",
"[app::application]" )
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() };
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 );
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.