From 4b2440bfc58a791fc1c7fc9a13e371d6da0be74b Mon Sep 17 00:00:00 2001 From: Angela Chen Date: Tue, 15 Aug 2023 19:12:47 -0400 Subject: [PATCH] Refactor string parsing logic from CommandLineParser into string_util --- include/ppx/command_line_parser.h | 62 ++--- include/ppx/string_util.h | 90 ++++++- src/ppx/command_line_parser.cpp | 95 +++---- src/ppx/string_util.cpp | 94 ++++++- src/test/command_line_parser_test.cpp | 26 +- src/test/string_util_test.cpp | 352 +++++++++++++++++++++++++- 6 files changed, 596 insertions(+), 123 deletions(-) diff --git a/include/ppx/command_line_parser.h b/include/ppx/command_line_parser.h index 1c6a84b5d..364e178be 100644 --- a/include/ppx/command_line_parser.h +++ b/include/ppx/command_line_parser.h @@ -16,6 +16,8 @@ #define ppx_command_line_parser_h #include "nlohmann/json.hpp" +#include "ppx/log.h" +#include "ppx/string_util.h" #include #include @@ -36,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. @@ -64,11 +66,15 @@ class CliOptions 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(const std::string& optionName, const std::vector& defaultValues) const { @@ -79,15 +85,15 @@ 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(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, @@ -108,33 +114,6 @@ class CliOptions // Same as above, but appends an array of values at the same key void AddOption(std::string_view optionName, const std::vector& valueArray); - 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); - } - - // 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 std::string(valueStr); - } - std::stringstream ss((std::string(valueStr))); - T valueAsNum; - ss >> valueAsNum; - if (ss.fail()) { - return defaultValue; - } - return valueAsNum; - } - private: // All flag names (string) and parameters (vector of strings) specified on the command line std::unordered_map> mAllOptions; @@ -148,20 +127,13 @@ 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[]); // Adds all options specified within jsonConfig to mOpts. - std::optional AddJsonOptions(const nlohmann::json& jsonConfig); + std::optional AddJsonOptions(const nlohmann::json& jsonConfig); std::string GetJsonConfigFlagName() const { return mJsonConfigFlagName; } const CliOptions& GetOptions() const { return mOpts; } @@ -172,7 +144,7 @@ class CommandLineParser private: // Adds an option to mOpts and handles the special --no-flag-name case. // Expects option names without the "--" prefix. - std::optional AddOption(std::string_view optionName, std::string_view valueStr); + std::optional AddOption(std::string_view optionName, std::string_view valueStr); CliOptions mOpts; std::string mJsonConfigFlagName = "config-json-path"; 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/command_line_parser.cpp b/src/ppx/command_line_parser.cpp index 305156c6a..99e5b21ca 100644 --- a/src/ppx/command_line_parser.cpp +++ b/src/ppx/command_line_parser.cpp @@ -21,8 +21,6 @@ #include #include "ppx/command_line_parser.h" -#include "ppx/log.h" -#include "ppx/string_util.h" namespace { @@ -38,23 +36,6 @@ bool StartsWithDoubleDash(std::string_view s) namespace ppx { -std::pair CliOptions::GetOptionValueOrDefault(const std::string& optionName, const std::pair& defaultValue) const -{ - 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; - } - 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 value) { std::string optionNameStr(optionName); @@ -80,26 +61,7 @@ void CliOptions::AddOption(std::string_view optionName, const std::vector> 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; - } - } - return valueAsBool; -} - -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) { @@ -112,31 +74,28 @@ std::optional CommandLineParser::Parse(int argc 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 (StartsWithDoubleDash(argString) && - argString == "--" + mJsonConfigFlagName && - i + 1 < argc && - !StartsWithDoubleDash(argv[i + 1])) { - mOpts.AddOption(mJsonConfigFlagName, ppx::string_util::TrimBothEnds(argv[i + 1])); - ++i; + if (argString.find('=') != std::string_view::npos) { + auto res = ppx::string_util::SplitInTwo(argString, '='); + if (res == std::nullopt) { + return "Malformed flag with '=': \"" + std::string(argString) + "\""; + } + if (StartsWithDoubleDash(res->first) && res->first == "--" + mJsonConfigFlagName) { + mOpts.AddOption(mJsonConfigFlagName, ppx::string_util::TrimBothEnds(res->second)); continue; } - args.emplace_back(argString); + args.emplace_back(res->first); + args.emplace_back(res->second); continue; } - if (res->first.empty() || res->second.empty()) { - return "Malformed flag with '=': \"" + std::string(argString) + "\""; - } - if (res->second.find('=') != std::string_view::npos) { - return "Unexpected number of '=' symbols in the following string: \"" + std::string(argString) + "\""; - } - if (StartsWithDoubleDash(res->first) && res->first == "--" + mJsonConfigFlagName) { - mOpts.AddOption(mJsonConfigFlagName, ppx::string_util::TrimBothEnds(res->second)); + if (StartsWithDoubleDash(argString) && + argString == "--" + mJsonConfigFlagName && + i + 1 < argc && + !StartsWithDoubleDash(argv[i + 1])) { + mOpts.AddOption(mJsonConfigFlagName, ppx::string_util::TrimBothEnds(argv[i + 1])); + ++i; continue; } - args.emplace_back(res->first); - args.emplace_back(res->second); + args.emplace_back(argString); } // Flags inside JSON files are processed first @@ -194,7 +153,7 @@ std::optional CommandLineParser::Parse(int argc return std::nullopt; } -std::optional CommandLineParser::AddJsonOptions(const nlohmann::json& jsonConfig) +std::optional CommandLineParser::AddJsonOptions(const nlohmann::json& jsonConfig) { std::stringstream ss; for (auto it = jsonConfig.cbegin(); it != jsonConfig.cend(); ++it) { @@ -213,14 +172,12 @@ std::optional CommandLineParser::AddJsonOptions ss << it.value(); std::string value = ss.str(); ss.str(""); - if (auto error = AddOption(it.key(), ppx::string_util::TrimBothEnds(value, " \t\""))) { - return error; - } + mOpts.AddOption(it.key(), ppx::string_util::TrimBothEnds(value, " \t\"")); } return std::nullopt; } -std::optional CommandLineParser::AddOption(std::string_view optionName, std::string_view valueStr) +std::optional CommandLineParser::AddOption(std::string_view optionName, std::string_view valueStr) { if (optionName.length() > 2 && optionName.substr(0, 3) == "no-") { if (valueStr.length() > 0) { @@ -229,6 +186,18 @@ std::optional CommandLineParser::AddOption(std: 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 mOpts to avoid inserting element by element + std::vector substringStringArray(substringArray->cbegin(), substringArray->cend()); + mOpts.AddOption(optionName, substringStringArray); + return std::nullopt; + } + mOpts.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 b2d2c0efd..ddc60ac52 100644 --- a/src/test/command_line_parser_test.cpp +++ b/src/test/command_line_parser_test.cpp @@ -61,7 +61,7 @@ TEST(CommandLineParserTest, BooleansSuccessfullyParsed) TEST(CommandLineParserTest, StringsSuccessfullyParsed) { 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,7 +69,7 @@ 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"), ""); } @@ -104,7 +104,7 @@ TEST(CommandLineParserTest, FloatsSuccessfullyParsed) EXPECT_EQ(gotOptions.GetOptionValueOrDefault("c", 0.0f), 300.0f); } -TEST(CommandLineParserTest, StringListSuccesfullyParsed) +TEST(CommandLineParserTest, StringListMultipleFlagsSuccesfullyParsed) { CommandLineParser parser; const char* args[] = {"/path/to/executable", "--a", "some-path", "--a", "some-other-path", "--a", "last-path"}; @@ -122,6 +122,24 @@ TEST(CommandLineParserTest, StringListSuccesfullyParsed) } } +TEST(CommandLineParserTest, StringListCommaSeparatedSuccesfullyParsed) +{ + 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, ResolutionSuccesfullyParsed) { CommandLineParser parser; @@ -171,7 +189,7 @@ TEST(CommandLineParserTest, EqualSignsMultipleFailedParsed) 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) 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); +}