From 4bdd70e9d9f55aed7a4cf3eb75c86ce4e529c865 Mon Sep 17 00:00:00 2001 From: Angela Chen Date: Mon, 21 Aug 2023 09:28:07 -0400 Subject: [PATCH] Add --config-json-path to accept flags specified in a JSON file. (#265) Add flag --config-json-path to accept commandline flags that are specified in a JSON file. This flag can be used multiple times. The priority of any conflicting flags is as follows: later flag on commandline > earlier flag on commandline > flag in later JSON file > flag in earlier JSON file For flags that accept multiple values, the same priority is used and only flags specified in the same JSON file or flags all on the commandline will be combined. Detailed changes: * Changed map `mAllOptions` to hold `strings` instead of `string_views` since there are now temporary strings originating from the JSON structure, changed some input parameters for `CliOptions` member functions to match. * Created some `CommandLineParser` methods to aid in parsing options (these are public for unit testing purposes) * `ParseJson` * `ParseOption` * Created more methods for `CliOptions` * `AddOption` overload that accepts one key and a vector of values to reduce the time needed to insert arrays. This function is only used by the JSON parsing flow. On the command line the flag must still be specified multiple times (`--flag-name 1,2,3` is still invalid) * `OverwriteOptions` * Updated `CommandLineParser::Parse` * Added a second initial pass of argv during commandline parsing to identify any provided JSON config files * Implemented overwriting * Added `mJsonConfigFlagName` to `CommandLineParser` so that the string `config-json-path` isn't specified in multiple places --- include/ppx/application.h | 1 + include/ppx/command_line_parser.h | 44 ++++-- src/ppx/application.cpp | 13 +- src/ppx/command_line_parser.cpp | 140 +++++++++++++++--- src/test/command_line_parser_test.cpp | 199 +++++++++++++++++++++++--- 5 files changed, 346 insertions(+), 51 deletions(-) diff --git a/include/ppx/application.h b/include/ppx/application.h index 164a6d42e..71a2d8ed3 100644 --- a/include/ppx/application.h +++ b/include/ppx/application.h @@ -252,6 +252,7 @@ struct StandardOptions #endif std::shared_ptr>> pAssetsPaths; + std::shared_ptr>> pConfigJsonPaths; }; // ------------------------------------------------------------------------------------------------- diff --git a/include/ppx/command_line_parser.h b/include/ppx/command_line_parser.h index 653a6b890..8a54bd6c4 100644 --- a/include/ppx/command_line_parser.h +++ b/include/ppx/command_line_parser.h @@ -15,6 +15,8 @@ #ifndef ppx_command_line_parser_h #define ppx_command_line_parser_h +#include "nlohmann/json.hpp" + #include #include #include @@ -44,7 +46,7 @@ class CliOptions public: CliOptions() = default; - bool HasExtraOption(std::string_view option) const { return mAllOptions.contains(option); } + bool HasExtraOption(const std::string& option) const { return mAllOptions.contains(option); } // Returns the number of unique options and flags that were specified on the commandline, // not counting multiple appearances of the same flag such as: --assets-path a --assets-path b @@ -55,7 +57,7 @@ class CliOptions // Warning: If this is called instead of the vector overload for multiple-value flags, // only the last value will be returned. template - T GetOptionValueOrDefault(std::string_view optionName, const T& defaultValue) const + T GetOptionValueOrDefault(const std::string& optionName, const T& defaultValue) const { auto it = mAllOptions.find(optionName); if (it == mAllOptions.cend()) { @@ -68,7 +70,7 @@ class CliOptions // Same as above, but intended for list flags that are specified on the command line // with multiple instances of the same flag template - std::vector GetOptionValueOrDefault(std::string_view optionName, const std::vector& defaultValues) const + std::vector GetOptionValueOrDefault(const std::string& optionName, const std::vector& defaultValues) const { auto it = mAllOptions.find(optionName); if (it == mAllOptions.cend()) { @@ -84,14 +86,14 @@ class CliOptions // Same as above, but intended for resolution flags that are specified on command line // with x - std::pair GetOptionValueOrDefault(std::string_view optionName, const std::pair& defaultValue) const; + std::pair GetOptionValueOrDefault(const std::string& optionName, const std::pair& defaultValue) const; // (WILL BE DEPRECATED, USE KNOBS INSTEAD) // Get the parameter value after converting it into the desired integral, // floating-point, or boolean type. If the value fails to be converted, // return the specified default value. template - T GetExtraOptionValueOrDefault(std::string_view optionName, const T& defaultValue) const + T GetExtraOptionValueOrDefault(const std::string& optionName, const T& defaultValue) const { static_assert(std::is_integral_v || std::is_floating_point_v || std::is_same_v, "GetExtraOptionValueOrDefault must be called with an integral, floating-point, boolean, or std::string type"); @@ -101,8 +103,13 @@ class CliOptions private: // Adds new option if the option does not already exist // Otherwise, the new value is appended to the end of the vector of stored parameters for this option - void - AddOption(std::string_view optionName, std::string_view value); + void AddOption(std::string_view optionName, std::string_view value); + + // Same as above, but appends an array of values at the same key + void AddOption(std::string_view optionName, const std::vector& valueArray); + + // For all options existing in newOptions, current entries in mAllOptions will be replaced by them + void OverwriteOptions(const CliOptions& newOptions); template T GetParsedOrDefault(std::string_view valueStr, const T& defaultValue) const @@ -120,9 +127,9 @@ class CliOptions T Parse(std::string_view valueStr, const T defaultValue) const { if constexpr (std::is_same_v) { - return static_cast(valueStr); + return std::string(valueStr); } - std::stringstream ss{static_cast(valueStr)}; + std::stringstream ss((std::string(valueStr))); T valueAsNum; ss >> valueAsNum; if (ss.fail()) { @@ -133,7 +140,7 @@ class CliOptions private: // All flag names (string) and parameters (vector of strings) specified on the command line - std::unordered_map> mAllOptions; + std::unordered_map> mAllOptions; friend class CommandLineParser; }; @@ -156,14 +163,23 @@ class CommandLineParser // and write the error to `out_error`. std::optional Parse(int argc, const char* argv[]); + // Parses all options specified within jsonConfig and adds them to cliOptions. + std::optional ParseJson(CliOptions& cliOptions, const nlohmann::json& jsonConfig); + + // Parses an option, handles the special --no-flag-name case, then adds the option to cliOptions + // Expects option names without the "--" prefix. + std::optional ParseOption(CliOptions& cliOptions, std::string_view optionName, std::string_view valueStr); + + std::string GetJsonConfigFlagName() const { return mJsonConfigFlagName; } const CliOptions& GetOptions() const { return mOpts; } std::string GetUsageMsg() const { return mUsageMsg; } - void AppendUsageMsg(const std::string& additionalMsg) { mUsageMsg += additionalMsg; } -private: - CliOptions mOpts; + void AppendUsageMsg(const std::string& additionalMsg) { mUsageMsg += additionalMsg; } - std::string mUsageMsg = R"( +private: + CliOptions mOpts; + std::string mJsonConfigFlagName = "config-json-path"; + std::string mUsageMsg = R"( USAGE ============================== Boolean options can be turned on with: diff --git a/src/ppx/application.cpp b/src/ppx/application.cpp index 94a672847..5045c9c00 100644 --- a/src/ppx/application.cpp +++ b/src/ppx/application.cpp @@ -806,13 +806,20 @@ void Application::SaveMetricsReportToDisk() void Application::InitStandardKnobs() { // Flag names in alphabetical order - std::vector defaultAssetsPaths = {}; + std::vector defaultEmptyList = {}; mStandardOpts.pAssetsPaths = - mKnobManager.CreateKnob>>("assets-path", defaultAssetsPaths); + mKnobManager.CreateKnob>>("assets-path", defaultEmptyList); mStandardOpts.pAssetsPaths->SetFlagDescription( - "Add a path in front of the assets search path list."); + "Add a path before the default assets folder in the search list."); mStandardOpts.pAssetsPaths->SetFlagParameters(""); + mStandardOpts.pConfigJsonPaths = mKnobManager.CreateKnob>>(mCommandLineParser.GetJsonConfigFlagName(), defaultEmptyList); + mStandardOpts.pConfigJsonPaths->SetFlagDescription( + "Additional commandline flags specified in a JSON file. Values specified in JSON files are " + "always overwritten by those specified on the command line. Between different files, the " + "later ones take priority."); + mStandardOpts.pConfigJsonPaths->SetFlagParameters(""); + mStandardOpts.pDeterministic = mKnobManager.CreateKnob>("deterministic", false); mStandardOpts.pDeterministic->SetFlagDescription( diff --git a/src/ppx/command_line_parser.cpp b/src/ppx/command_line_parser.cpp index 19d31dbc0..9987a110c 100644 --- a/src/ppx/command_line_parser.cpp +++ b/src/ppx/command_line_parser.cpp @@ -13,6 +13,7 @@ // limitations under the License. #include +#include #include #include #include @@ -37,7 +38,14 @@ bool StartsWithDoubleDash(std::string_view s) namespace ppx { -std::pair CliOptions::GetOptionValueOrDefault(std::string_view optionName, const std::pair& defaultValue) const +void CliOptions::OverwriteOptions(const CliOptions& newOptions) +{ + for (auto& it : newOptions.mAllOptions) { + mAllOptions[it.first] = it.second; + } +} + +std::pair CliOptions::GetOptionValueOrDefault(const std::string& optionName, const std::pair& defaultValue) const { auto it = mAllOptions.find(optionName); if (it == mAllOptions.cend()) { @@ -54,17 +62,31 @@ std::pair CliOptions::GetOptionValueOrDefault(std::string_view optionN return std::make_pair(N, M); } -void CliOptions::AddOption(std::string_view optionName, std::string_view valueStr) +void CliOptions::AddOption(std::string_view optionName, std::string_view value) { - auto it = mAllOptions.find(optionName); + std::string optionNameStr(optionName); + std::string valueStr(value); + auto it = mAllOptions.find(optionNameStr); if (it == mAllOptions.cend()) { - std::vector v{valueStr}; - mAllOptions.emplace(optionName, v); + std::vector v{std::move(valueStr)}; + mAllOptions.emplace(std::move(optionNameStr), std::move(v)); return; } it->second.push_back(valueStr); } +void CliOptions::AddOption(std::string_view optionName, const std::vector& valueArray) +{ + std::string optionNameStr(optionName); + auto it = mAllOptions.find(optionNameStr); + if (it == mAllOptions.cend()) { + mAllOptions.emplace(std::move(optionNameStr), valueArray); + return; + } + auto storedValueArray = it->second; + storedValueArray.insert(storedValueArray.end(), valueArray.cbegin(), valueArray.cend()); +} + bool CliOptions::Parse(std::string_view valueStr, bool defaultValue) const { if (valueStr == "") { @@ -91,7 +113,7 @@ std::optional CommandLineParser::Parse(int argc return std::nullopt; } - // Split flag and parameters connected with '=' + // Initial pass to trim the name of executable and to split any flag and parameters that are connected with '=' std::vector args; for (size_t i = 1; i < argc; ++i) { std::string_view argString(argv[i]); @@ -103,14 +125,60 @@ std::optional CommandLineParser::Parse(int argc if (res->first.empty() || res->second.empty()) { return "Malformed flag with '=': \"" + std::string(argString) + "\""; } - else if (res->second.find('=') != std::string_view::npos) { + if (res->second.find('=') != std::string_view::npos) { return "Unexpected number of '=' symbols in the following string: \"" + std::string(argString) + "\""; } args.emplace_back(res->first); args.emplace_back(res->second); } - // Process arguments into either standalone flags or options with parameters. + // Another pass to identify JSON config files, add that option, and remove it from the argument list + std::vector argsFiltered; + for (size_t i = 0; i < args.size(); ++i) { + bool nextArgumentIsParameter = (i + 1 < args.size()) && !StartsWithDoubleDash(args[i + 1]); + if ((args[i] == "--" + mJsonConfigFlagName) && nextArgumentIsParameter) { + mOpts.AddOption(mJsonConfigFlagName, ppx::string_util::TrimBothEnds(args[i + 1])); + ++i; + continue; + } + argsFiltered.emplace_back(args[i]); + } + args = argsFiltered; + argsFiltered.clear(); + + // Flags inside JSON files are processed first + // These are always lower priority than flags on the command-line + std::vector configJsonPaths; + configJsonPaths = mOpts.GetOptionValueOrDefault(mJsonConfigFlagName, configJsonPaths); + for (const auto& jsonPath : configJsonPaths) { + std::ifstream f(jsonPath); + if (f.fail()) { + return "Cannot locate file --" + mJsonConfigFlagName + ": " + jsonPath; + } + + PPX_LOG_INFO("Parsing JSON config file: " << jsonPath); + nlohmann::json data; + try { + data = nlohmann::json::parse(f); + } + catch (nlohmann::json::parse_error& e) { + PPX_LOG_ERROR("nlohmann::json::parse error: " << e.what() << '\n' + << "exception id: " << e.id << '\n' + << "byte position of error: " << e.byte); + } + if (!(data.is_object())) { + return "The following config file could not be parsed as a JSON object: " + jsonPath; + } + + CliOptions jsonOptions; + if (auto error = ParseJson(jsonOptions, data)) { + return error; + } + mOpts.OverwriteOptions(jsonOptions); + } + + CliOptions commandlineOptions; + // Main pass, process arguments into either standalone flags or options with parameters. for (size_t i = 0; i < args.size(); ++i) { std::string_view name = ppx::string_util::TrimBothEnds(args[i]); if (!StartsWithDoubleDash(name)) { @@ -118,25 +186,61 @@ std::optional CommandLineParser::Parse(int argc } name = name.substr(2); + std::string_view value = ""; if (i + 1 < args.size()) { - std::string_view nextElem = ppx::string_util::TrimBothEnds(args[i + 1]); + auto nextElem = ppx::string_util::TrimBothEnds(args[i + 1]); if (!StartsWithDoubleDash(nextElem)) { - // We found an option with a parameter. - mOpts.AddOption(name, nextElem); + // The next element is a parameter for the current option + value = nextElem; ++i; - continue; } } - // There is no parameter so it's likely a flag - if (name.substr(0, 3) == "no-") { - mOpts.AddOption(name.substr(3), "0"); + + if (auto error = ParseOption(commandlineOptions, name, value)) { + return error; + } + } + mOpts.OverwriteOptions(commandlineOptions); + + return std::nullopt; +} + +std::optional CommandLineParser::ParseJson(CliOptions& cliOptions, const nlohmann::json& jsonConfig) +{ + std::stringstream ss; + for (auto it = jsonConfig.cbegin(); it != jsonConfig.cend(); ++it) { + if ((it.value()).is_array()) { + // Special case, arrays specified in JSON are added directly to cliOptions to avoid inserting element by element + std::vector jsonStringArray; + for (const auto& elem : it.value()) { + ss << elem; + jsonStringArray.push_back(std::string(ppx::string_util::TrimBothEnds(ss.str(), " \t\""))); + ss.str(""); + } + cliOptions.AddOption(it.key(), jsonStringArray); + continue; } - else { - // Do not assign "1" in case it's an option lacking a parameter - mOpts.AddOption(name, ""); + + ss << it.value(); + std::string value = ss.str(); + ss.str(""); + if (auto error = ParseOption(cliOptions, it.key(), ppx::string_util::TrimBothEnds(value, " \t\""))) { + return error; } } + return std::nullopt; +} +std::optional CommandLineParser::ParseOption(CliOptions& cliOptions, std::string_view optionName, std::string_view valueStr) +{ + if (optionName.length() > 2 && optionName.substr(0, 3) == "no-") { + if (valueStr.length() > 0) { + return "invalid prefix no- for option \"" + std::string(optionName) + "\" and value \"" + std::string(valueStr) + "\""; + } + optionName = optionName.substr(3); + valueStr = "0"; + } + cliOptions.AddOption(optionName, valueStr); return std::nullopt; } diff --git a/src/test/command_line_parser_test.cpp b/src/test/command_line_parser_test.cpp index 93d79b2f2..f38d49fd4 100644 --- a/src/test/command_line_parser_test.cpp +++ b/src/test/command_line_parser_test.cpp @@ -22,7 +22,7 @@ namespace { using ::testing::HasSubstr; -TEST(CommandLineParserTest, ZeroArguments) +TEST(CommandLineParserTest, Parse_ZeroArguments) { CommandLineParser parser; if (auto error = parser.Parse(0, nullptr)) { @@ -31,7 +31,7 @@ TEST(CommandLineParserTest, ZeroArguments) EXPECT_EQ(parser.GetOptions().GetNumUniqueOptions(), 0); } -TEST(CommandLineParserTest, FirstArgumentIgnored) +TEST(CommandLineParserTest, Parse_FirstArgumentIgnored) { CommandLineParser parser; const char* args[] = {"/path/to/executable"}; @@ -41,7 +41,7 @@ TEST(CommandLineParserTest, FirstArgumentIgnored) EXPECT_EQ(parser.GetOptions().GetNumUniqueOptions(), 0); } -TEST(CommandLineParserTest, BooleansSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_Booleans) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b", "1", "--c", "true", "--no-d", "--e", "0", "--f", "false"}; @@ -58,7 +58,7 @@ TEST(CommandLineParserTest, BooleansSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("f", true), false); } -TEST(CommandLineParserTest, StringsSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_Strings) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "filename with spaces", "--b", "filenameWithoutSpaces", "--c", "filename,with/.punctuation,", "--d", "", "--e"}; @@ -74,7 +74,7 @@ TEST(CommandLineParserTest, StringsSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("e", "foo"), ""); } -TEST(CommandLineParserTest, IntegersSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_Integers) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "0", "--b", "-5", "--c", "300", "--d", "0", "--e", "1000"}; @@ -90,7 +90,7 @@ TEST(CommandLineParserTest, IntegersSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("e", -1), 1000); } -TEST(CommandLineParserTest, FloatsSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_Floats) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "1.0", "--b", "-6.5", "--c", "300"}; @@ -104,7 +104,7 @@ TEST(CommandLineParserTest, FloatsSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("c", 0.0f), 300.0f); } -TEST(CommandLineParserTest, StringListSuccesfullyParsed) +TEST(CommandLineParserTest, Parse_StringList) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "some-path", "--a", "some-other-path", "--a", "last-path"}; @@ -122,7 +122,7 @@ TEST(CommandLineParserTest, StringListSuccesfullyParsed) } } -TEST(CommandLineParserTest, ResolutionSuccesfullyParsed) +TEST(CommandLineParserTest, Parse_Resolution) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "1000x2000"}; @@ -136,7 +136,7 @@ TEST(CommandLineParserTest, ResolutionSuccesfullyParsed) EXPECT_EQ(res.second, 2000); } -TEST(CommandLineParserTest, ResolutionSuccessfullyParsedButDefaulted) +TEST(CommandLineParserTest, Parse_ResolutionDefaulted) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "1000X2000"}; @@ -150,7 +150,7 @@ TEST(CommandLineParserTest, ResolutionSuccessfullyParsedButDefaulted) EXPECT_EQ(res.second, 0); } -TEST(CommandLineParserTest, EqualSignsSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_EqualSigns) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b=5", "--c", "--d", "11"}; @@ -165,7 +165,7 @@ TEST(CommandLineParserTest, EqualSignsSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("d", 0), 11); } -TEST(CommandLineParserTest, EqualSignsMultipleFailedParsed) +TEST(CommandLineParserTest, Parse_EqualSignsMultipleFail) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b=5=8", "--c", "--d", "11"}; @@ -174,7 +174,7 @@ TEST(CommandLineParserTest, EqualSignsMultipleFailedParsed) EXPECT_THAT(error->errorMsg, HasSubstr("Unexpected number of '=' symbols in the following string")); } -TEST(CommandLineParserTest, EqualSignsMalformedFailedParsed) +TEST(CommandLineParserTest, Parse_EqualSignsMalformedFail) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b=", "--c", "--d", "11"}; @@ -183,7 +183,7 @@ TEST(CommandLineParserTest, EqualSignsMalformedFailedParsed) EXPECT_THAT(error->errorMsg, HasSubstr("Malformed flag with '='")); } -TEST(CommandLineParserTest, LeadingParameterFailedParsed) +TEST(CommandLineParserTest, Parse_LeadingParameterFail) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "10", "--a", "--b", "5", "--c", "--d", "11"}; @@ -192,7 +192,7 @@ TEST(CommandLineParserTest, LeadingParameterFailedParsed) EXPECT_THAT(error->errorMsg, HasSubstr("Invalid command-line option")); } -TEST(CommandLineParserTest, AdjacentParameterFailedParsed) +TEST(CommandLineParserTest, Parse_AdjacentParameterFail) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b", "5", "8", "--c", "--d", "11"}; @@ -201,7 +201,7 @@ TEST(CommandLineParserTest, AdjacentParameterFailedParsed) EXPECT_THAT(error->errorMsg, HasSubstr("Invalid command-line option")); } -TEST(CommandLineParserTest, LastValueIsTaken) +TEST(CommandLineParserTest, Parse_LastValueIsTaken) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "1", "--b", "1", "--a", "2", "--a", "3"}; @@ -214,7 +214,7 @@ TEST(CommandLineParserTest, LastValueIsTaken) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("b", 0), 1); } -TEST(CommandLineParserTest, ExtraOptionsSuccessfullyParsed) +TEST(CommandLineParserTest, Parse_ExtraOptions) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--extra-option-bool", "true", "--extra-option-int", "123", "--extra-option-no-param", "--extra-option-str", "option string value"}; @@ -230,5 +230,172 @@ TEST(CommandLineParserTest, ExtraOptionsSuccessfullyParsed) EXPECT_TRUE(opts.HasExtraOption("extra-option-no-param")); } +TEST(CommandLineParserTest, ParseJson_Empty) +{ + CommandLineParser parser; + CliOptions opts; + nlohmann::json jsonConfig; + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 0); +} + +TEST(CommandLineParserTest, ParseJson_Simple) +{ + CommandLineParser parser; + CliOptions opts; + std::string jsonText = R"( + { + "a": true, + "b": false, + "c": 1.234, + "d": 5, + "e": "helloworld", + "f": "hello world", + "g": "200x300" + } +)"; + nlohmann::json jsonConfig = nlohmann::json::parse(jsonText); + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 7); + EXPECT_EQ(opts.GetOptionValueOrDefault("a", false), true); + EXPECT_EQ(opts.GetOptionValueOrDefault("b", true), false); + EXPECT_FLOAT_EQ(opts.GetOptionValueOrDefault("c", 6.0f), 1.234f); + EXPECT_EQ(opts.GetOptionValueOrDefault("d", 0), 5); + EXPECT_EQ(opts.GetOptionValueOrDefault("e", "foo"), "helloworld"); + EXPECT_EQ(opts.GetOptionValueOrDefault("f", "foo"), "hello world"); + std::pair gFlag = opts.GetOptionValueOrDefault("g", std::make_pair(1, 1)); + EXPECT_EQ(gFlag.first, 200); + EXPECT_EQ(gFlag.second, 300); +} + +TEST(CommandLineParserTest, ParseJson_NestedStructure) +{ + CommandLineParser parser; + CliOptions opts; + std::string jsonText = R"( + { + "a": true, + "b": { + "c" : 1, + "d" : 2 + } + } +)"; + nlohmann::json jsonConfig = nlohmann::json::parse(jsonText); + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 2); + EXPECT_EQ(opts.GetOptionValueOrDefault("a", false), true); + EXPECT_TRUE(opts.HasExtraOption("b")); + EXPECT_EQ(opts.GetOptionValueOrDefault("b", "default"), "{\"c\":1,\"d\":2}"); + EXPECT_FALSE(opts.HasExtraOption("c")); + EXPECT_FALSE(opts.HasExtraOption("d")); +} + +TEST(CommandLineParserTest, ParseJson_IntArray) +{ + CommandLineParser parser; + CliOptions opts; + std::string jsonText = R"( + { + "a": true, + "b": [1, 2, 3] + } +)"; + nlohmann::json jsonConfig = nlohmann::json::parse(jsonText); + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 2); + EXPECT_EQ(opts.GetOptionValueOrDefault("a", false), true); + EXPECT_TRUE(opts.HasExtraOption("b")); + std::vector defaultB = {100}; + std::vector gotB = opts.GetOptionValueOrDefault("b", defaultB); + EXPECT_EQ(gotB.size(), 3); + EXPECT_EQ(gotB.at(0), 1); + EXPECT_EQ(gotB.at(1), 2); + EXPECT_EQ(gotB.at(2), 3); +} + +TEST(CommandLineParserTest, ParseJson_StrArray) +{ + CommandLineParser parser; + CliOptions opts; + std::string jsonText = R"( + { + "a": true, + "b": ["first", "second", "third"] + } +)"; + nlohmann::json jsonConfig = nlohmann::json::parse(jsonText); + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 2); + EXPECT_EQ(opts.GetOptionValueOrDefault("a", false), true); + EXPECT_TRUE(opts.HasExtraOption("b")); + std::vector defaultB = {}; + std::vector gotB = opts.GetOptionValueOrDefault("b", defaultB); + EXPECT_EQ(gotB.size(), 3); + EXPECT_EQ(gotB.at(0), "first"); + EXPECT_EQ(gotB.at(1), "second"); + EXPECT_EQ(gotB.at(2), "third"); +} + +TEST(CommandLineParserTest, ParseJson_HeterogeneousArray) +{ + CommandLineParser parser; + CliOptions opts; + std::string jsonText = R"( + { + "a": true, + "b": [1, "2", {"c" : 3}, 4.0] + } +)"; + nlohmann::json jsonConfig = nlohmann::json::parse(jsonText); + if (auto error = parser.ParseJson(opts, jsonConfig)) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 2); + EXPECT_EQ(opts.GetOptionValueOrDefault("a", false), true); + EXPECT_TRUE(opts.HasExtraOption("b")); + std::vector defaultB; + std::vector gotB = opts.GetOptionValueOrDefault("b", defaultB); + EXPECT_EQ(gotB.size(), 4); + EXPECT_EQ(gotB.at(0), "1"); + EXPECT_EQ(gotB.at(1), "2"); + EXPECT_EQ(gotB.at(2), "{\"c\":3}"); + EXPECT_EQ(gotB.at(3), "4.0"); +} + +TEST(CommandLineParserTest, ParseOption_Simple) +{ + CommandLineParser parser; + CliOptions opts; + if (auto error = parser.ParseOption(opts, "flag-name", "true")) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 1); + EXPECT_TRUE(opts.HasExtraOption("flag-name")); + EXPECT_EQ(opts.GetOptionValueOrDefault("flag-name", false), true); +} + +TEST(CommandLineParserTest, ParseOption_NoPrefix) +{ + CommandLineParser parser; + CliOptions opts; + if (auto error = parser.ParseOption(opts, "no-flag-name", "")) { + FAIL() << error->errorMsg; + } + EXPECT_EQ(opts.GetNumUniqueOptions(), 1); + EXPECT_TRUE(opts.HasExtraOption("flag-name")); + EXPECT_EQ(opts.GetOptionValueOrDefault("flag-name", true), false); +} + } // namespace } // namespace ppx