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..9fc79a954 100644 --- a/include/ppx/command_line_parser.h +++ b/include/ppx/command_line_parser.h @@ -15,6 +15,10 @@ #ifndef ppx_command_line_parser_h #define ppx_command_line_parser_h +#include "nlohmann/json.hpp" +#include "ppx/log.h" +#include "ppx/string_util.h" + #include #include #include @@ -34,7 +38,7 @@ namespace ppx { // All commandline flags are stored as key-value pairs (string, list of strings) // Value syntax: -// - strings cannot contain "=" +// - strings cannot contain "=" or "," // - boolean values stored as "0" "false" "1" "true" // // GetOptionValueOrDefault() can be used to access value of specified type. @@ -44,7 +48,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,20 +59,24 @@ 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()) { return defaultValue; } auto valueStr = it->second.back(); - return GetParsedOrDefault(valueStr, defaultValue); + auto result = ppx::string_util::ParseOrDefault(valueStr, defaultValue); + if (result.second != std::nullopt) { + PPX_LOG_ERROR(result.second->errorMsg); + } + return result.first; } // Same as above, but intended for list flags that are specified on the command line - // with multiple instances of the same flag + // with multiple instances of the same flag, or with comma-separated values 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()) { @@ -77,21 +85,21 @@ class CliOptions std::vector parsedValues; T nullValue{}; for (size_t i = 0; i < it->second.size(); ++i) { - parsedValues.emplace_back(GetParsedOrDefault(it->second.at(i), nullValue)); + auto result = ppx::string_util::ParseOrDefault(it->second.at(i), nullValue); + if (result.second != std::nullopt) { + PPX_LOG_ERROR(result.second->errorMsg); + } + parsedValues.emplace_back(result.first); } return parsedValues; } - // 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; - // (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,39 +109,17 @@ 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); - template - T GetParsedOrDefault(std::string_view valueStr, const T& defaultValue) const - { - static_assert(std::is_integral_v || std::is_floating_point_v || std::is_same_v, "GetParsedOrDefault must be called with an integral, floating-point, boolean, or std::string type"); - return Parse(valueStr, defaultValue); - } + // Same as above, but appends an array of values at the same key + void AddOption(std::string_view optionName, const std::vector& valueArray); - // For boolean parameters - // interpreted as true: "true", 1, "" - // interpreted as false: "false", 0 - bool Parse(std::string_view valueStr, bool defaultValue) const; - - template - T Parse(std::string_view valueStr, const T defaultValue) const - { - if constexpr (std::is_same_v) { - return static_cast(valueStr); - } - std::stringstream ss{static_cast(valueStr)}; - T valueAsNum; - ss >> valueAsNum; - if (ss.fail()) { - return defaultValue; - } - return valueAsNum; - } + // For all options existing in newOptions, current entries in mAllOptions will be replaced by them + void OverwriteOptions(const CliOptions& newOptions); 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; }; @@ -144,26 +130,28 @@ class CliOptions class CommandLineParser { public: - struct ParsingError - { - ParsingError(const std::string& error) - : errorMsg(error) {} - std::string errorMsg; - }; - // Parse the given arguments into options. Return false if parsing // succeeded. Otherwise, return true if an error occurred, // and write the error to `out_error`. - std::optional Parse(int argc, const char* argv[]); + 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/include/ppx/string_util.h b/include/ppx/string_util.h index ba5a40be6..0038a8684 100644 --- a/include/ppx/string_util.h +++ b/include/ppx/string_util.h @@ -23,6 +23,10 @@ namespace ppx { namespace string_util { +// ------------------------------------------------------------------------------------------------- +// Misc +// ------------------------------------------------------------------------------------------------- + void TrimLeft(std::string& s); void TrimRight(std::string& s); @@ -31,10 +35,18 @@ std::string TrimCopy(const std::string& s); // Trims all characters specified in c from both the left and right sides of s std::string_view TrimBothEnds(std::string_view s, std::string_view c = " \t"); +// Splits s at every instance of delimeter and returns a vector of substrings +// Returns std::nullopt if s contains: leading/trailing/consecutive delimiters +std::optional> Split(std::string_view s, char delimiter); + // Splits s at the first instance of delimeter and returns two substrings -// Returns std::nullopt if s does not contain the delimeter +// Returns std::nullopt if s is not in expected format of string-delimeter-string std::optional> SplitInTwo(std::string_view s, char delimiter); +// ------------------------------------------------------------------------------------------------- +// Formatting Strings +// ------------------------------------------------------------------------------------------------- + // Formats string for printing with the specified width and left indent. // Words will be pushed to the subsequent line to avoid line breaks in the // middle of a word if possible. @@ -74,6 +86,82 @@ std::string ToString(std::pair values) return ss.str(); } +// ------------------------------------------------------------------------------------------------- +// Parsing Strings +// ------------------------------------------------------------------------------------------------- + +struct ParsingError +{ + ParsingError(const std::string& error) + : errorMsg(error) {} + std::string errorMsg; +}; + +// ParseOrDefault() attempts to parse valueStr into the same type as defaultValue +// If successful, returns the parsed value and std::nullopt +// If unsucessful, returns defaultValue and ParsingError + +// For strings +// e.g. "a string" -> "a string" +std::pair> ParseOrDefault(std::string_view valueStr, const std::string& defaultValue); +std::pair> ParseOrDefault(std::string_view valueStr, std::string_view defaultValue); + +// For bool +// e.g. "true", "1", "" -> true +// e.g. "false", "0" -> false +std::pair> ParseOrDefault(std::string_view valueStr, bool defaultValue); + +// For integers, chars and floats +// e.g. "1.0" -> 1.0f +// e.g. "-20" -> -20 +// e.g. "c" -> 'c' +template +std::pair> ParseOrDefault(std::string_view valueStr, T defaultValue) +{ + static_assert(std::is_integral_v || std::is_floating_point_v, "Attempted to parse invalid type for ParseOrDefault"); + + std::stringstream ss((std::string(valueStr))); + T valueAsNum; + ss >> valueAsNum; + if (ss.fail()) { + return std::make_pair(defaultValue, "could not be parsed as integral or float: " + std::string(valueStr)); + } + return std::make_pair(valueAsNum, std::nullopt); +} + +// For lists with comma-separated string representation +// e.g. "i1,i2,i3 with spaces,i4" -> {"i1", "i2", "i3 with spaces", "i4"} +template +std::pair, std::optional> ParseOrDefault(std::string_view valueStr, const std::vector& defaultValues) +{ + std::vector splitStrings; + auto res = Split(valueStr, ','); + if (res == std::nullopt) { + // String contains no commas + splitStrings.emplace_back(valueStr); + } + else { + for (const auto sv : res.value()) { + splitStrings.emplace_back(std::string(sv)); + } + } + + std::vector parsedValues; + T nullValue{}; + for (const auto& singleStr : splitStrings) { + auto res = ParseOrDefault(singleStr, nullValue); + if (res.second != std::nullopt) { + return std::make_pair(defaultValues, res.second); + } + parsedValues.emplace_back(res.first); + } + return std::make_pair(parsedValues, std::nullopt); +} + +// For resolution with x-separated string representation +// e.g. "600x800" -> (600, 800) +std::pair, std::optional> ParseOrDefault(std::string_view valueStr, const std::pair& defaultValue); + } // namespace string_util } // namespace ppx 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..a1a7399ff 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 @@ -20,8 +21,6 @@ #include #include "ppx/command_line_parser.h" -#include "ppx/log.h" -#include "ppx/string_util.h" namespace { @@ -37,80 +36,108 @@ 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) { - auto it = mAllOptions.find(optionName); - if (it == mAllOptions.cend()) { - return defaultValue; - } - auto valueStr = it->second.back(); - auto res = ppx::string_util::SplitInTwo(valueStr, 'x'); - if (res == std::nullopt) { - PPX_LOG_ERROR("resolution flag must be in format x: " << valueStr); - return defaultValue; + for (auto& it : newOptions.mAllOptions) { + mAllOptions[it.first] = it.second; } - int N = GetParsedOrDefault(res->first, defaultValue.first); - int M = GetParsedOrDefault(res->second, defaultValue.second); - 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); } -bool CliOptions::Parse(std::string_view valueStr, bool defaultValue) const +void CliOptions::AddOption(std::string_view optionName, const std::vector& valueArray) { - if (valueStr == "") { - return true; - } - std::stringstream ss{std::string(valueStr)}; - bool valueAsBool; - ss >> valueAsBool; - if (ss.fail()) { - ss.clear(); - ss >> std::boolalpha >> valueAsBool; - if (ss.fail()) { - PPX_LOG_ERROR("could not be parsed as bool: " << valueStr); - return defaultValue; - } + std::string optionNameStr(optionName); + auto it = mAllOptions.find(optionNameStr); + if (it == mAllOptions.cend()) { + mAllOptions.emplace(std::move(optionNameStr), valueArray); + return; } - return valueAsBool; + auto storedValueArray = it->second; + storedValueArray.insert(storedValueArray.end(), valueArray.cbegin(), valueArray.cend()); } -std::optional CommandLineParser::Parse(int argc, const char* argv[]) +std::optional CommandLineParser::Parse(int argc, const char* argv[]) { // argc should be >= 1 and argv[0] the name of the executable. if (argc < 2) { 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]); - auto res = ppx::string_util::SplitInTwo(argString, '='); - if (res == std::nullopt) { + if (argString.find('=') == std::string_view::npos) { args.emplace_back(argString); continue; } - if (res->first.empty() || res->second.empty()) { + auto res = ppx::string_util::SplitInTwo(argString, '='); + if (res == std::nullopt) { return "Malformed flag with '=': \"" + std::string(argString) + "\""; } - else 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 +145,71 @@ 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; } - else { - // Do not assign "1" in case it's an option lacking a parameter - mOpts.AddOption(name, ""); + } + 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; } + + ss << it.value(); + std::string value = ss.str(); + ss.str(""); + cliOptions.AddOption(it.key(), ppx::string_util::TrimBothEnds(value, " \t\"")); + } + 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"; + } + + if (valueStr.find(',') != std::string_view::npos) { + auto substringArray = ppx::string_util::Split(valueStr, ','); + if (substringArray == std::nullopt) { + return "invalid comma use for option \"" + std::string(optionName) + "\" and value \"" + std::string(valueStr) + "\""; + } + // Special case, comma-separated value lists specified on the commandline are added directly to cliOptions to avoid inserting element by element + std::vector substringStringArray(substringArray->cbegin(), substringArray->cend()); + cliOptions.AddOption(optionName, substringStringArray); + return std::nullopt; } + cliOptions.AddOption(optionName, valueStr); return std::nullopt; } diff --git a/src/ppx/string_util.cpp b/src/ppx/string_util.cpp index dede05675..03c42a1d8 100644 --- a/src/ppx/string_util.cpp +++ b/src/ppx/string_util.cpp @@ -21,6 +21,10 @@ namespace ppx { namespace string_util { +// ------------------------------------------------------------------------------------------------- +// Misc +// ------------------------------------------------------------------------------------------------- + void TrimLeft(std::string& s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { @@ -55,20 +59,51 @@ std::string_view TrimBothEnds(std::string_view s, std::string_view c) return s.substr(strBegin, strRange); } +std::optional> Split(std::string_view s, char delimiter) +{ + if (s.size() == 0) { + return std::nullopt; + } + + std::vector substrings; + std::string_view remainingString = s; + while (remainingString != "") { + size_t delimeterIndex = remainingString.find(delimiter); + if (delimeterIndex == std::string_view::npos) { + substrings.push_back(remainingString); + break; + } + + std::string_view element = remainingString.substr(0, delimeterIndex); + if (element.length() == 0) { + return std::nullopt; + } + substrings.push_back(element); + + if (delimeterIndex == remainingString.length() - 1) { + return std::nullopt; + } + remainingString = remainingString.substr(delimeterIndex + 1); + } + return substrings; +} + std::optional> SplitInTwo(std::string_view s, char delimiter) { if (s.size() == 0) { return std::nullopt; } - size_t delimeterIndex = s.find(delimiter); - if (delimeterIndex == std::string_view::npos) { + auto splitResult = Split(s, delimiter); + if (splitResult == std::nullopt || splitResult->size() != 2) { return std::nullopt; } - std::string_view firstSubstring = s.substr(0, delimeterIndex); - std::string_view secondSubstring = s.substr(delimeterIndex + 1); - return std::make_pair(firstSubstring, secondSubstring); + return std::make_pair(splitResult->at(0), splitResult->at(1)); } +// ------------------------------------------------------------------------------------------------- +// Formatting Strings +// ------------------------------------------------------------------------------------------------- + std::string WrapText(const std::string& s, size_t width, size_t indent) { if (indent >= width) { @@ -105,5 +140,54 @@ std::string WrapText(const std::string& s, size_t width, size_t indent) return wrappedText; } +// ------------------------------------------------------------------------------------------------- +// Parsing Strings +// ------------------------------------------------------------------------------------------------- + +std::pair, std::optional> ParseOrDefault(std::string_view valueStr, const std::pair& defaultValue) +{ + auto parseResolution = SplitInTwo(valueStr, 'x'); + if (parseResolution == std::nullopt) { + return std::make_pair(defaultValue, "resolution flag must be in format x: " + std::string(valueStr)); + } + auto parseNResult = ParseOrDefault(parseResolution->first, defaultValue.first); + if (parseNResult.second != std::nullopt) { + return std::make_pair(defaultValue, "width cannot be parsed: " + parseNResult.second->errorMsg); + } + auto parseMResult = ParseOrDefault(parseResolution->second, defaultValue.first); + if (parseMResult.second != std::nullopt) { + return std::make_pair(defaultValue, "height cannot be parsed: " + parseMResult.second->errorMsg); + } + return std::make_pair(std::make_pair(parseNResult.first, parseMResult.first), std::nullopt); +} + +std::pair> ParseOrDefault(std::string_view valueStr, const std::string& defaultValue) +{ + return std::make_pair(std::string(valueStr), std::nullopt); +} + +std::pair> ParseOrDefault(std::string_view valueStr, std::string_view defaultValue) +{ + return std::make_pair(std::string(valueStr), std::nullopt); +} + +std::pair> ParseOrDefault(std::string_view valueStr, bool defaultValue) +{ + if (valueStr == "") { + return std::make_pair(true, std::nullopt); + } + std::stringstream ss{std::string(valueStr)}; + bool valueAsBool; + ss >> valueAsBool; + if (ss.fail()) { + ss.clear(); + ss >> std::boolalpha >> valueAsBool; + if (ss.fail()) { + return std::make_pair(defaultValue, "could not be parsed as bool: " + std::string(valueStr)); + } + } + return std::make_pair(valueAsBool, std::nullopt); +} + } // namespace string_util } // namespace ppx \ No newline at end of file diff --git a/src/test/command_line_parser_test.cpp b/src/test/command_line_parser_test.cpp index 93d79b2f2..a7e8821f6 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,10 +58,10 @@ 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"}; + const char* args[] = {"/path/to/executable", "--a", "filename with spaces", "--b", "filenameWithoutSpaces", "--c", "filename\\with/.punctuation", "--d", "", "--e"}; if (auto error = parser.Parse(sizeof(args) / sizeof(args[0]), args)) { FAIL() << error->errorMsg; } @@ -69,12 +69,12 @@ TEST(CommandLineParserTest, StringsSuccessfullyParsed) EXPECT_EQ(parser.GetOptions().GetNumUniqueOptions(), 5); EXPECT_EQ(gotOptions.GetOptionValueOrDefault("a", ""), "filename with spaces"); EXPECT_EQ(gotOptions.GetOptionValueOrDefault("b", ""), "filenameWithoutSpaces"); - EXPECT_EQ(gotOptions.GetOptionValueOrDefault("c", ""), "filename,with/.punctuation,"); + EXPECT_EQ(gotOptions.GetOptionValueOrDefault("c", ""), "filename\\with/.punctuation"); EXPECT_EQ(gotOptions.GetOptionValueOrDefault("d", "foo"), ""); 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,25 @@ TEST(CommandLineParserTest, StringListSuccesfullyParsed) } } -TEST(CommandLineParserTest, ResolutionSuccesfullyParsed) +TEST(CommandLineParserTest, Parse_StringListCommaSeparated) +{ + CommandLineParser parser; + const char* args[] = {"/path/to/executable", "--a", "some-path,some-other-path,last-path"}; + if (auto error = parser.Parse(sizeof(args) / sizeof(args[0]), args)) { + FAIL() << error->errorMsg; + } + CliOptions gotOptions = parser.GetOptions(); + EXPECT_EQ(parser.GetOptions().GetNumUniqueOptions(), 1); + auto paths = gotOptions.GetOptionValueOrDefault("a", {"a-path"}); + EXPECT_EQ(paths.size(), 3); + if (paths.size() == 3) { + EXPECT_EQ(paths[0], "some-path"); + EXPECT_EQ(paths[1], "some-other-path"); + EXPECT_EQ(paths[2], "last-path"); + } +} + +TEST(CommandLineParserTest, Parse_Resolution) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "1000x2000"}; @@ -136,7 +154,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 +168,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,16 +183,16 @@ 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"}; auto error = parser.Parse(sizeof(args) / sizeof(args[0]), args); EXPECT_TRUE(error); - EXPECT_THAT(error->errorMsg, HasSubstr("Unexpected number of '=' symbols in the following string")); + EXPECT_THAT(error->errorMsg, HasSubstr("Malformed flag with '='")); } -TEST(CommandLineParserTest, EqualSignsMalformedFailedParsed) +TEST(CommandLineParserTest, Parse_EqualSignsMalformedFail) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "--b=", "--c", "--d", "11"}; @@ -183,7 +201,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 +210,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 +219,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 +232,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 +248,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 diff --git a/src/test/string_util_test.cpp b/src/test/string_util_test.cpp index 2671b7262..a53befc77 100644 --- a/src/test/string_util_test.cpp +++ b/src/test/string_util_test.cpp @@ -13,11 +13,16 @@ // limitations under the License. #include "gtest/gtest.h" +#include "gmock/gmock.h" #include "ppx/string_util.h" using namespace ppx::string_util; +// ------------------------------------------------------------------------------------------------- +// Misc +// ------------------------------------------------------------------------------------------------- + TEST(StringUtilTest, TrimLeft_NothingToTrim) { std::string toTrim = "No left space "; @@ -70,6 +75,58 @@ TEST(StringUtilTest, TrimBothEnds_LeftAndRightSpaces) EXPECT_EQ(toTrim, " Some spaces "); } +TEST(StringUtilTest, Split_EmptyString) +{ + std::string_view toSplit = ""; + auto res = Split(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, Split_NoDelimiter) +{ + std::string_view toSplit = "Apple"; + auto res = Split(toSplit, ','); + EXPECT_NE(res, std::nullopt); + EXPECT_EQ(res->size(), 1); + EXPECT_EQ(res->at(0), "Apple"); +} + +TEST(StringUtilTest, Split_OneDelimiter) +{ + std::string_view toSplit = "Apple,Banana"; + auto res = Split(toSplit, ','); + EXPECT_NE(res, std::nullopt); + EXPECT_EQ(res->size(), 2); + EXPECT_EQ(res->at(0), "Apple"); + EXPECT_EQ(res->at(1), "Banana"); +} + +TEST(StringUtilTest, Split_MultipleElements) +{ + std::string_view toSplit = "Apple,Banana,Orange,Pear"; + auto res = Split(toSplit, ','); + EXPECT_NE(res, std::nullopt); + EXPECT_EQ(res->size(), 4); + EXPECT_EQ(res->at(0), "Apple"); + EXPECT_EQ(res->at(1), "Banana"); + EXPECT_EQ(res->at(2), "Orange"); + EXPECT_EQ(res->at(3), "Pear"); +} + +TEST(StringUtilTest, Split_LeadingTrailingDelimiter) +{ + std::string_view toSplit = ",Apple,"; + auto res = Split(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, Split_ConsecutiveDelimiters) +{ + std::string_view toSplit = "Apple,,,Banana"; + auto res = Split(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + TEST(StringUtilTest, SplitInTwo_EmptyString) { std::string_view toSplit = ""; @@ -77,7 +134,7 @@ TEST(StringUtilTest, SplitInTwo_EmptyString) EXPECT_EQ(res, std::nullopt); } -TEST(StringUtilTest, SplitInTwo_OneDelimiter) +TEST(StringUtilTest, SplitInTwo_Pass) { std::string_view toSplit = "Apple,Banana"; auto res = SplitInTwo(toSplit, ','); @@ -86,15 +143,45 @@ TEST(StringUtilTest, SplitInTwo_OneDelimiter) EXPECT_EQ(res->second, "Banana"); } -TEST(StringUtilTest, SplitInTwo_MultipleDelimiter) +TEST(StringUtilTest, SplitInTwo_NoDelimiter) +{ + std::string_view toSplit = "Apple"; + auto res = SplitInTwo(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, SplitInTwo_MissingFirstHalf) +{ + std::string_view toSplit = ",Banana"; + auto res = SplitInTwo(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, SplitInTwo_MissingSecondHalf) +{ + std::string_view toSplit = "Apple,"; + auto res = SplitInTwo(toSplit, ','); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, SplitInTwo_MoreThanTwoElements) { std::string_view toSplit = "Apple,Banana,Orange"; auto res = SplitInTwo(toSplit, ','); - EXPECT_NE(res, std::nullopt); - EXPECT_EQ(res->first, "Apple"); - EXPECT_EQ(res->second, "Banana,Orange"); + EXPECT_EQ(res, std::nullopt); +} + +TEST(StringUtilTest, SplitInTwo_TwoElementsWithLeadingTrailingDelimeters) +{ + std::string_view toSplit = ",Apple,Banana,"; + auto res = SplitInTwo(toSplit, ','); + EXPECT_EQ(res, std::nullopt); } +// ------------------------------------------------------------------------------------------------- +// Formatting Strings +// ------------------------------------------------------------------------------------------------- + TEST(StringUtilTest, WrapText_EmptyString) { std::string toWrap = ""; @@ -286,3 +373,258 @@ TEST(StringUtilTest, ToString_VectorBool) std::string gotString = ToString(vb); EXPECT_EQ(gotString, wantString); } + +// ------------------------------------------------------------------------------------------------- +// Parsing Strings +// ------------------------------------------------------------------------------------------------- + +TEST(StringUtilTest, ParseOrDefault_String) +{ + std::string toParse = "foo"; + std::string defaultValue = "default"; + std::string wantValue = "foo"; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_StringWithSpace) +{ + std::string toParse = "foo bar"; + std::string defaultValue = "default"; + std::string wantValue = "foo bar"; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_StringView) +{ + std::string toParse = "foo bar"; + std::string_view defaultValue = "default"; + std::string_view wantValue = "foo bar"; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolTrueText) +{ + std::string toParse = "true"; + bool defaultValue = false; + bool wantValue = true; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolTrueOne) +{ + std::string toParse = "1"; + bool defaultValue = false; + bool wantValue = true; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolTrueEmpty) +{ + std::string toParse = ""; + bool defaultValue = false; + bool wantValue = true; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolFalseText) +{ + std::string toParse = "false"; + bool defaultValue = true; + bool wantValue = false; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolFalseZero) +{ + std::string toParse = "0"; + bool defaultValue = true; + bool wantValue = false; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_BoolFail) +{ + std::string toParse = "foo"; + bool defaultValue = true; + bool wantValue = true; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as bool")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_IntegerPass) +{ + std::string toParse = "-10"; + int defaultValue = 0; + int wantValue = -10; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_IntegerFail) +{ + std::string toParse = "foo"; + int defaultValue = 0; + int wantValue = 0; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as integral or float")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_IntegerEmptyFail) +{ + std::string toParse = ""; + int defaultValue = 1; + int wantValue = 1; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as integral or float")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_SizetPass) +{ + std::string toParse = "5"; + size_t defaultValue = 0; + size_t wantValue = 5; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_SizetFail) +{ + std::string toParse = "foo"; + size_t defaultValue = 0; + size_t wantValue = 0; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as integral or float")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_FloatPass) +{ + std::string toParse = "5.6"; + float defaultValue = 0.0f; + float wantValue = 5.6f; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_FloatFail) +{ + std::string toParse = "foo"; + float defaultValue = 0.0f; + float wantValue = 0.0f; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as integral or float")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ListIntPass) +{ + std::string toParse = "1,2,3"; + std::vector defaultValue; + std::vector wantValue = {1, 2, 3}; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ListIntFail) +{ + std::string toParse = "foo"; + std::vector defaultValue = {2, 3}; + std::vector wantValue = {2, 3}; + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("could not be parsed as integral or float")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ResolutionPass) +{ + std::string toParse = "100x200"; + std::pair defaultValue = std::make_pair(-1, -1); + std::pair wantValue = std::make_pair(100, 200); + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_EQ(result.second, std::nullopt); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ResolutionNoDelimeterFail) +{ + std::string toParse = "100X200"; + std::pair defaultValue = std::make_pair(-1, -1); + std::pair wantValue = std::make_pair(-1, -1); + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("resolution flag must be in format x")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ResolutionWidthFail) +{ + std::string toParse = "foox200"; + std::pair defaultValue = std::make_pair(-1, -1); + std::pair wantValue = std::make_pair(-1, -1); + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("width cannot be parsed")); + EXPECT_EQ(result.first, wantValue); +} + +TEST(StringUtilTest, ParseOrDefault_ResolutionHeightFail) +{ + std::string toParse = "100xfoo"; + std::pair defaultValue = std::make_pair(-1, -1); + std::pair wantValue = std::make_pair(-1, -1); + + auto result = ParseOrDefault(toParse, defaultValue); + EXPECT_NE(result.second, std::nullopt); + EXPECT_THAT(result.second->errorMsg, ::testing::HasSubstr("height cannot be parsed")); + EXPECT_EQ(result.first, wantValue); +}