diff --git a/include/xrpl/beast/core/SemanticVersion.h b/include/xrpl/beast/core/SemanticVersion.h index 338942c252..c2e395e3f4 100644 --- a/include/xrpl/beast/core/SemanticVersion.h +++ b/include/xrpl/beast/core/SemanticVersion.h @@ -17,14 +17,14 @@ namespace beast { class SemanticVersion { public: - using identifier_list = std::vector; + using IdentifierList = std::vector; int majorVersion; int minorVersion; int patchVersion; - identifier_list preReleaseIdentifiers; - identifier_list metaData; + IdentifierList preReleaseIdentifiers; + IdentifierList metaData; SemanticVersion(); diff --git a/src/libxrpl/beast/core/SemanticVersion.cpp b/src/libxrpl/beast/core/SemanticVersion.cpp index a99437f8f2..f902a14b07 100644 --- a/src/libxrpl/beast/core/SemanticVersion.cpp +++ b/src/libxrpl/beast/core/SemanticVersion.cpp @@ -15,7 +15,7 @@ namespace beast { std::string -printIdentifiers(SemanticVersion::identifier_list const& list) +printIdentifiers(SemanticVersion::IdentifierList const& list) { std::string ret; @@ -115,7 +115,7 @@ extractIdentifier(std::string& value, bool allowLeadingZeroes, std::string& inpu bool extractIdentifiers( - SemanticVersion::identifier_list& identifiers, + SemanticVersion::IdentifierList& identifiers, bool allowLeadingZeroes, std::string& input) { diff --git a/src/test/beast/SemanticVersion_test.cpp b/src/test/beast/SemanticVersion_test.cpp deleted file mode 100644 index d7079080c8..0000000000 --- a/src/test/beast/SemanticVersion_test.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include -#include - -#include - -namespace beast { - -class SemanticVersion_test : public unit_test::Suite -{ - using identifier_list = SemanticVersion::identifier_list; - -public: - void - checkPass(std::string const& input, bool shouldPass = true) - { - SemanticVersion v; - - if (shouldPass) - { - BEAST_EXPECT(v.parse(input)); - BEAST_EXPECT(v.print() == input); - } - else - { - BEAST_EXPECT(!v.parse(input)); - } - } - - void - checkFail(std::string const& input) - { - checkPass(input, false); - } - - // check input and input with appended metadata - void - checkMeta(std::string const& input, bool shouldPass) - { - checkPass(input, shouldPass); - - checkPass(input + "+a", shouldPass); - checkPass(input + "+1", shouldPass); - checkPass(input + "+a.b", shouldPass); - checkPass(input + "+ab.cd", shouldPass); - - checkFail(input + "!"); - checkFail(input + "+"); - checkFail(input + "++"); - checkFail(input + "+!"); - checkFail(input + "+."); - checkFail(input + "+a.!"); - } - - void - checkMetaFail(std::string const& input) - { - checkMeta(input, false); - } - - // check input, input with appended release data, - // input with appended metadata, and input with both - // appended release data and appended metadata - // - void - checkRelease(std::string const& input, bool shouldPass = true) - { - checkMeta(input, shouldPass); - - checkMeta(input + "-1", shouldPass); - checkMeta(input + "-a", shouldPass); - checkMeta(input + "-a1", shouldPass); - checkMeta(input + "-a1.b1", shouldPass); - checkMeta(input + "-ab.cd", shouldPass); - checkMeta(input + "--", shouldPass); - - checkMetaFail(input + "+"); - checkMetaFail(input + "!"); - checkMetaFail(input + "-"); - checkMetaFail(input + "-!"); - checkMetaFail(input + "-."); - checkMetaFail(input + "-a.!"); - checkMetaFail(input + "-0.a"); - } - - // Checks the major.minor.version string alone and with all - // possible combinations of release identifiers and metadata. - // - void - check(std::string const& input, bool shouldPass = true) - { - checkRelease(input, shouldPass); - } - - void - negcheck(std::string const& input) - { - check(input, false); - } - - void - testParse() - { - testcase("parsing"); - - check("0.0.0"); - check("1.2.3"); - check("2147483647.2147483647.2147483647"); // max int - - // negative values - negcheck("-1.2.3"); - negcheck("1.-2.3"); - negcheck("1.2.-3"); - - // missing parts - negcheck(""); - negcheck("1"); - negcheck("1."); - negcheck("1.2"); - negcheck("1.2."); - negcheck(".2.3"); - - // whitespace - negcheck(" 1.2.3"); - negcheck("1 .2.3"); - negcheck("1.2 .3"); - negcheck("1.2.3 "); - - // leading zeroes - negcheck("01.2.3"); - negcheck("1.02.3"); - negcheck("1.2.03"); - } - - static identifier_list - ids() - { - return identifier_list(); - } - - static identifier_list - ids(std::string const& s1) - { - identifier_list v; - v.push_back(s1); - return v; - } - - static identifier_list - ids(std::string const& s1, std::string const& s2) - { - identifier_list v; - v.push_back(s1); - v.push_back(s2); - return v; - } - - static identifier_list - ids(std::string const& s1, std::string const& s2, std::string const& s3) - { - identifier_list v; - v.push_back(s1); - v.push_back(s2); - v.push_back(s3); - return v; - } - - // Checks the decomposition of the input into appropriate values - void - checkValues( - std::string const& input, - int majorVersion, - int minorVersion, - int patchVersion, - identifier_list const& preReleaseIdentifiers = identifier_list(), - identifier_list const& metaData = identifier_list()) - { - SemanticVersion v; - - BEAST_EXPECT(v.parse(input)); - - BEAST_EXPECT(v.majorVersion == majorVersion); - BEAST_EXPECT(v.minorVersion == minorVersion); - BEAST_EXPECT(v.patchVersion == patchVersion); - - BEAST_EXPECT(v.preReleaseIdentifiers == preReleaseIdentifiers); - BEAST_EXPECT(v.metaData == metaData); - } - - void - testValues() - { - testcase("values"); - - checkValues("0.1.2", 0, 1, 2); - checkValues("1.2.3", 1, 2, 3); - checkValues("1.2.3-rc1", 1, 2, 3, ids("rc1")); - checkValues("1.2.3-rc1.debug", 1, 2, 3, ids("rc1", "debug")); - checkValues("1.2.3-rc1.debug.asm", 1, 2, 3, ids("rc1", "debug", "asm")); - checkValues("1.2.3+full", 1, 2, 3, ids(), ids("full")); - checkValues("1.2.3+full.prod", 1, 2, 3, ids(), ids("full", "prod")); - checkValues("1.2.3+full.prod.x86", 1, 2, 3, ids(), ids("full", "prod", "x86")); - checkValues( - "1.2.3-rc1.debug.asm+full.prod.x86", - 1, - 2, - 3, - ids("rc1", "debug", "asm"), - ids("full", "prod", "x86")); - } - - // makes sure the left version is less than the right - void - checkLessInternal(std::string const& lhs, std::string const& rhs) - { - SemanticVersion left; - SemanticVersion right; - - BEAST_EXPECT(left.parse(lhs)); - BEAST_EXPECT(right.parse(rhs)); - - BEAST_EXPECT(compare(left, left) == 0); - BEAST_EXPECT(compare(right, right) == 0); - BEAST_EXPECT(compare(left, right) < 0); - BEAST_EXPECT(compare(right, left) > 0); - - BEAST_EXPECT(left < right); - BEAST_EXPECT(right > left); - BEAST_EXPECT(left == left); - BEAST_EXPECT(right == right); - } - - void - checkLess(std::string const& lhs, std::string const& rhs) - { - checkLessInternal(lhs, rhs); - checkLessInternal(lhs + "+meta", rhs); - checkLessInternal(lhs, rhs + "+meta"); - checkLessInternal(lhs + "+meta", rhs + "+meta"); - } - - void - testCompare() - { - testcase("comparisons"); - - checkLess("1.0.0-alpha", "1.0.0-alpha.1"); - checkLess("1.0.0-alpha.1", "1.0.0-alpha.beta"); - checkLess("1.0.0-alpha.beta", "1.0.0-beta"); - checkLess("1.0.0-beta", "1.0.0-beta.2"); - checkLess("1.0.0-beta.2", "1.0.0-beta.11"); - checkLess("1.0.0-beta.11", "1.0.0-rc.1"); - checkLess("1.0.0-rc.1", "1.0.0"); - checkLess("0.9.9", "1.0.0"); - } - - void - run() override - { - testParse(); - testValues(); - testCompare(); - } -}; - -BEAST_DEFINE_TESTSUITE(SemanticVersion, beast, beast); -} // namespace beast diff --git a/src/tests/libxrpl/beast/SemanticVersion.cpp b/src/tests/libxrpl/beast/SemanticVersion.cpp new file mode 100644 index 0000000000..21b33c9476 --- /dev/null +++ b/src/tests/libxrpl/beast/SemanticVersion.cpp @@ -0,0 +1,333 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace { + +using IdentifierList = SemanticVersion::IdentifierList; + +// Version strings are not valid C++ identifiers, so squash their punctuation to +// turn one into a gtest parameter name. +std::string +identifierFor(std::string_view version) +{ + std::string name{version}; + std::ranges::replace_if( + name, [](char c) { return !std::isalnum(c, std::locale::classic()); }, '_'); + if (!name.empty() && std::isdigit(name.front(), std::locale::classic())) + name.insert(0, "v_"); + return name; +} + +// Pre-release and metadata suffixes, each applied to a "major.minor.patch" base. +// The valid ones leave a well-formed base well-formed; the invalid ones make any +// base malformed. +constexpr auto kValidPreRelease = + std::to_array({"", "-1", "-a", "-a1", "-a1.b1", "-ab.cd", "--"}); +constexpr auto kInvalidPreRelease = + std::to_array({"+", "!", "-", "-!", "-.", "-a.!", "-0.a"}); +constexpr auto kValidMetaData = std::to_array({"", "+a", "+1", "+a.b", "+ab.cd"}); +constexpr auto kInvalidMetaData = + std::to_array({"!", "+", "++", "+!", "+.", "+a.!"}); + +// Assembles base + preRelease + metaData and checks whether it parses. A version +// we accept must also round-trip through print(). +void +expectParse( + std::string_view base, + std::string_view preRelease, + std::string_view metaData, + bool shouldPass) +{ + auto const input = std::string{base}.append(preRelease).append(metaData); + SCOPED_TRACE(::testing::Message() << '"' << input << '"'); + + SemanticVersion v; + + if (shouldPass) + { + EXPECT_TRUE(v.parse(input)); + EXPECT_EQ(v.print(), input); + } + else + { + EXPECT_FALSE(v.parse(input)); + } +} + +struct ParseCase +{ + std::string_view testName; + std::string_view base; + bool shouldPass; +}; + +std::string +parseCaseName(::testing::TestParamInfo const& info) +{ + return std::string{info.param.testName}; +} + +constexpr auto kParseCases = std::to_array({ + {.testName = "zeroes", .base = "0.0.0", .shouldPass = true}, + {.testName = "simple", .base = "1.2.3", .shouldPass = true}, + {.testName = "max_int", .base = "2147483647.2147483647.2147483647", .shouldPass = true}, + + // negative values + {.testName = "negative_major", .base = "-1.2.3", .shouldPass = false}, + {.testName = "negative_minor", .base = "1.-2.3", .shouldPass = false}, + {.testName = "negative_patch", .base = "1.2.-3", .shouldPass = false}, + + // missing parts + {.testName = "empty", .base = "", .shouldPass = false}, + {.testName = "major_only", .base = "1", .shouldPass = false}, + {.testName = "major_then_dot", .base = "1.", .shouldPass = false}, + {.testName = "major_and_minor", .base = "1.2", .shouldPass = false}, + {.testName = "major_minor_then_dot", .base = "1.2.", .shouldPass = false}, + {.testName = "missing_major", .base = ".2.3", .shouldPass = false}, + + // whitespace + {.testName = "leading_space", .base = " 1.2.3", .shouldPass = false}, + {.testName = "space_after_major", .base = "1 .2.3", .shouldPass = false}, + {.testName = "space_after_minor", .base = "1.2 .3", .shouldPass = false}, + {.testName = "trailing_space", .base = "1.2.3 ", .shouldPass = false}, + + // leading zeroes + {.testName = "leading_zero_in_major", .base = "01.2.3", .shouldPass = false}, + {.testName = "leading_zero_in_minor", .base = "1.02.3", .shouldPass = false}, + {.testName = "leading_zero_in_patch", .base = "1.2.03", .shouldPass = false}, +}); + +struct ValuesCase +{ + std::string_view testName; + std::string_view input; + int majorVersion; + int minorVersion; + int patchVersion; + IdentifierList preReleaseIdentifiers{}; // NOLINT(readability-redundant-member-init) + IdentifierList metaData{}; // NOLINT(readability-redundant-member-init) +}; + +std::string +valuesCaseName(::testing::TestParamInfo const& info) +{ + return std::string{info.param.testName}; +} + +std::vector const kValuesCases{ + { + .testName = "zero_major", + .input = "0.1.2", + .majorVersion = 0, + .minorVersion = 1, + .patchVersion = 2, + }, + { + .testName = "simple", + .input = "1.2.3", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + }, + { + .testName = "one_pre_release_identifier", + .input = "1.2.3-rc1", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1"}, + }, + { + .testName = "two_pre_release_identifiers", + .input = "1.2.3-rc1.debug", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug"}, + }, + { + .testName = "three_pre_release_identifiers", + .input = "1.2.3-rc1.debug.asm", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug", "asm"}, + }, + { + .testName = "one_metadata_identifier", + .input = "1.2.3+full", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full"}, + }, + { + .testName = "two_metadata_identifiers", + .input = "1.2.3+full.prod", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full", "prod"}, + }, + { + .testName = "three_metadata_identifiers", + .input = "1.2.3+full.prod.x86", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full", "prod", "x86"}, + }, + { + .testName = "pre_release_and_metadata", + .input = "1.2.3-rc1.debug.asm+full.prod.x86", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug", "asm"}, + .metaData = {"full", "prod", "x86"}, + }, +}; + +struct OrderCase +{ + std::string_view lesser; + std::string_view greater; +}; + +std::string +orderCaseName(::testing::TestParamInfo const& info) +{ + return identifierFor(info.param.lesser) + "_below_" + identifierFor(info.param.greater); +} + +constexpr auto kOrderCases = std::to_array({ + {.lesser = "1.0.0-alpha", .greater = "1.0.0-alpha.1"}, + {.lesser = "1.0.0-alpha.1", .greater = "1.0.0-alpha.beta"}, + {.lesser = "1.0.0-alpha.beta", .greater = "1.0.0-beta"}, + {.lesser = "1.0.0-beta", .greater = "1.0.0-beta.2"}, + {.lesser = "1.0.0-beta.2", .greater = "1.0.0-beta.11"}, + {.lesser = "1.0.0-beta.11", .greater = "1.0.0-rc.1"}, + {.lesser = "1.0.0-rc.1", .greater = "1.0.0"}, + {.lesser = "0.9.9", .greater = "1.0.0"}, +}); + +} // namespace + +class SemanticVersionParse : public ::testing::TestWithParam +{ +}; + +// Exercises the base string on its own and with every combination of appended +// pre-release identifiers and metadata. +TEST_P(SemanticVersionParse, pre_release_and_metadata_combinations) +{ + auto const& [testName, base, shouldPass] = GetParam(); + + for (auto const preRelease : kValidPreRelease) + { + for (auto const metaData : kValidMetaData) + expectParse(base, preRelease, metaData, shouldPass); + + for (auto const metaData : kInvalidMetaData) + expectParse(base, preRelease, metaData, false); + } + + // A malformed pre-release section poisons the whole string, whatever + // metadata follows it. + for (auto const preRelease : kInvalidPreRelease) + { + for (auto const metaData : kValidMetaData) + expectParse(base, preRelease, metaData, false); + + for (auto const metaData : kInvalidMetaData) + expectParse(base, preRelease, metaData, false); + } +} + +INSTANTIATE_TEST_SUITE_P( + Inputs, + SemanticVersionParse, + ::testing::ValuesIn(kParseCases), + parseCaseName); + +class SemanticVersionValues : public ::testing::TestWithParam +{ +}; + +TEST_P(SemanticVersionValues, decomposes_into_components) +{ + auto const& expected = GetParam(); + + SemanticVersion v; + EXPECT_TRUE(v.parse(expected.input)); + + EXPECT_EQ(v.majorVersion, expected.majorVersion); + EXPECT_EQ(v.minorVersion, expected.minorVersion); + EXPECT_EQ(v.patchVersion, expected.patchVersion); + + EXPECT_EQ(v.preReleaseIdentifiers, expected.preReleaseIdentifiers); + EXPECT_EQ(v.metaData, expected.metaData); +} + +INSTANTIATE_TEST_SUITE_P( + Inputs, + SemanticVersionValues, + ::testing::ValuesIn(kValuesCases), + valuesCaseName); + +class SemanticVersionOrder : public ::testing::TestWithParam +{ +}; + +TEST_P(SemanticVersionOrder, lesser_precedes_greater) +{ + auto const& [lesser, greater] = GetParam(); + + // Metadata takes no part in precedence, so attaching it to either side must + // leave the ordering untouched. + static constexpr auto kMetaData = std::to_array({"", "+meta"}); + + for (auto const lesserMetaData : kMetaData) + { + for (auto const greaterMetaData : kMetaData) + { + auto const lesserInput = std::string{lesser}.append(lesserMetaData); + auto const greaterInput = std::string{greater}.append(greaterMetaData); + SCOPED_TRACE( + ::testing::Message() << '"' << lesserInput << "\" < \"" << greaterInput << '"'); + + SemanticVersion lesserVersion; + SemanticVersion greaterVersion; + EXPECT_TRUE(lesserVersion.parse(lesserInput)); + EXPECT_TRUE(greaterVersion.parse(greaterInput)); + + EXPECT_EQ(compare(lesserVersion, lesserVersion), 0); + EXPECT_EQ(compare(greaterVersion, greaterVersion), 0); + EXPECT_LT(compare(lesserVersion, greaterVersion), 0); + EXPECT_GT(compare(greaterVersion, lesserVersion), 0); + + EXPECT_LT(lesserVersion, greaterVersion); + EXPECT_GT(greaterVersion, lesserVersion); + EXPECT_EQ(lesserVersion, lesserVersion); + EXPECT_EQ(greaterVersion, greaterVersion); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + Pairs, + SemanticVersionOrder, + ::testing::ValuesIn(kOrderCases), + orderCaseName); + +} // namespace beast