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