chore: Move semantic version tests to gtest (#7872)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Alex Kremer
2026-07-27 19:48:53 +01:00
committed by GitHub
parent ff7fff2cf2
commit 86832edc70
4 changed files with 338 additions and 271 deletions

View File

@@ -17,14 +17,14 @@ namespace beast {
class SemanticVersion
{
public:
using identifier_list = std::vector<std::string>;
using IdentifierList = std::vector<std::string>;
int majorVersion;
int minorVersion;
int patchVersion;
identifier_list preReleaseIdentifiers;
identifier_list metaData;
IdentifierList preReleaseIdentifiers;
IdentifierList metaData;
SemanticVersion();

View File

@@ -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)
{

View File

@@ -1,266 +0,0 @@
#include <xrpl/beast/core/SemanticVersion.h>
#include <xrpl/beast/unit_test/suite.h>
#include <string>
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

View File

@@ -0,0 +1,333 @@
#include <xrpl/beast/core/SemanticVersion.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <locale>
#include <string>
#include <string_view>
#include <vector>
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<std::string_view>({"", "-1", "-a", "-a1", "-a1.b1", "-ab.cd", "--"});
constexpr auto kInvalidPreRelease =
std::to_array<std::string_view>({"+", "!", "-", "-!", "-.", "-a.!", "-0.a"});
constexpr auto kValidMetaData = std::to_array<std::string_view>({"", "+a", "+1", "+a.b", "+ab.cd"});
constexpr auto kInvalidMetaData =
std::to_array<std::string_view>({"!", "+", "++", "+!", "+.", "+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<ParseCase> const& info)
{
return std::string{info.param.testName};
}
constexpr auto kParseCases = std::to_array<ParseCase>({
{.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<ValuesCase> const& info)
{
return std::string{info.param.testName};
}
std::vector<ValuesCase> 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<OrderCase> const& info)
{
return identifierFor(info.param.lesser) + "_below_" + identifierFor(info.param.greater);
}
constexpr auto kOrderCases = std::to_array<OrderCase>({
{.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<ParseCase>
{
};
// 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<ValuesCase>
{
};
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<OrderCase>
{
};
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<std::string_view>({"", "+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