Skip to content

Commit 99763ed

Browse files
authored
add googletest to the project (#8)
* add googletest to the project * add clean functionality * add tests for leading zeroes * enable <==> operator * add more fields for testing * add comment * add more tests * check if non-digit components exist
1 parent 6a685cd commit 99763ed

File tree

7 files changed

+147
-58
lines changed

7 files changed

+147
-58
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
build
22
cmake-build-debug
3+
tests/Testing
4+
Testing

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ endif()
1010

1111
include(GNUInstallDirs)
1212

13+
include(FetchContent)
14+
FetchContent_Declare(
15+
googletest
16+
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
17+
)
18+
# For Windows: Prevent overriding the parent project's compiler/linker settings
19+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
20+
FetchContent_MakeAvailable(googletest)
21+
1322
add_subdirectory(src)
1423
enable_testing()
1524
add_subdirectory(tests)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ Usage:
88
```
99
cmake -B build
1010
cmake --build build
11-
ctest --test-dir build
11+
ctest --test-dir build --output-on-failure
1212
./build/benchmarks/benchmark
13-
```
13+
```

include/version_weaver.h

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ namespace version_weaver {
1111
static constexpr size_t MAX_VERSION_LENGTH = 256;
1212

1313
bool validate(std::string_view version);
14-
bool gt(std::string_view version1, std::string_view version2);
15-
bool lt(std::string_view version1, std::string_view version2);
1614
bool satisfies(std::string_view version, std::string_view range);
1715
std::string coerce(std::string_view version);
1816
std::string minimum(std::string_view range);
19-
std::string clean(std::string_view range);
2017

2118
// A normal version number MUST take the form X.Y.Z where X, Y, and Z are
2219
// non-negative integers, and MUST NOT contain leading zeroes.
@@ -58,6 +55,11 @@ enum ParseError {
5855
INVALID_INPUT,
5956
};
6057

58+
// This will return a cleaned and trimmed semver version.
59+
// If the provided version is not valid a null will be returned.
60+
// This does not work for ranges.
61+
std::expected<Version, ParseError> clean(std::string_view range);
62+
6163
std::expected<Version, ParseError> parse(std::string_view version);
6264
} // namespace version_weaver
6365

@@ -73,10 +75,15 @@ inline bool operator==(const version_weaver::Version& first,
7375
if (first.minor != second.minor) return first.minor == second.minor;
7476
return first.patch == second.patch;
7577
}
76-
/*
77-
inline auto operator<=>(const version_weaver::Version& first, const
78-
version_weaver::Version& second) { if (first.major != second.major) return
79-
first.major <=> second.major; if (first.minor != second.minor) return
80-
first.minor <=> second.minor; return first.patch <=> second.patch;
81-
}*/
78+
inline auto operator<=>(const version_weaver::Version& first,
79+
const version_weaver::Version& second) {
80+
if (first.major != second.major) {
81+
return first.major <=> second.major;
82+
}
83+
if (first.minor != second.minor) {
84+
return first.minor <=> second.minor;
85+
}
86+
return first.patch <=> second.patch;
87+
}
88+
8289
#endif // VERSION_WEAVER_H

src/version_weaver.cpp

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,14 @@
22
#include <algorithm>
33
#include <cctype>
44
namespace version_weaver {
5-
bool validate(std::string_view version) { return bool(parse(version)); }
6-
7-
bool gt(std::string_view version1, std::string_view version2) { return true; }
8-
bool lt(std::string_view version1, std::string_view version2) { return true; }
5+
bool validate(std::string_view version) { return parse(version).has_value(); }
96
bool satisfies(std::string_view version, std::string_view range) {
107
return true;
118
}
129
std::string coerce(std::string_view version) { return ""; }
1310
std::string minimum(std::string_view range) { return ""; }
14-
std::string clean(std::string_view range) { return ""; }
1511

16-
inline void trim_whitespace(std::string_view* input) noexcept {
12+
constexpr inline void trim_whitespace(std::string_view* input) noexcept {
1713
while (!input->empty() && std::isspace(input->front())) {
1814
input->remove_prefix(1);
1915
}
@@ -22,6 +18,29 @@ inline void trim_whitespace(std::string_view* input) noexcept {
2218
}
2319
}
2420

21+
constexpr inline bool contains_only_digits(std::string_view input) noexcept {
22+
// Optimization opportunity: Replace this with a hash table lookup.
23+
return input.find_first_not_of("0123456789") == std::string_view::npos;
24+
}
25+
26+
std::expected<Version, ParseError> clean(std::string_view input) {
27+
std::string_view range = input;
28+
trim_whitespace(&range);
29+
if (range.empty()) return std::unexpected(ParseError::INVALID_INPUT);
30+
31+
// Trim any leading value expect = and v.
32+
while (!range.empty() && (range.front() == '=' || range.front() == 'v')) {
33+
range.remove_prefix(1);
34+
}
35+
36+
// If range starts with a non-digit character, it is invalid.
37+
if (!range.empty() && !std::isdigit(range.front())) {
38+
return std::unexpected(ParseError::INVALID_INPUT);
39+
}
40+
41+
return parse(range);
42+
}
43+
2544
std::expected<Version, ParseError> parse(std::string_view input) {
2645
if (input.size() > MAX_VERSION_LENGTH) {
2746
return std::unexpected(ParseError::VERSION_LARGER_THAN_MAX_LENGTH);
@@ -42,6 +61,9 @@ std::expected<Version, ParseError> parse(std::string_view input) {
4261
// Version components can not have leading zeroes.
4362
return std::unexpected(ParseError::INVALID_INPUT);
4463
}
64+
if (!contains_only_digits(major)) {
65+
return std::unexpected(ParseError::INVALID_INPUT);
66+
}
4567
version.major = major;
4668
input_copy = input_copy.substr(dot_iterator + 1);
4769
dot_iterator = input_copy.find('.');
@@ -55,6 +77,9 @@ std::expected<Version, ParseError> parse(std::string_view input) {
5577
// Version components can not have leading zeroes.
5678
return std::unexpected(ParseError::INVALID_INPUT);
5779
}
80+
if (!contains_only_digits(minor)) {
81+
return std::unexpected(ParseError::INVALID_INPUT);
82+
}
5883
version.minor = minor;
5984
input_copy = input_copy.substr(dot_iterator + 1);
6085
dot_iterator = input_copy.find_first_of("-+");
@@ -64,6 +89,9 @@ std::expected<Version, ParseError> parse(std::string_view input) {
6489
if (patch.empty() || (patch.front() == '0' && patch.size() > 1)) {
6590
return std::unexpected(ParseError::INVALID_INPUT);
6691
}
92+
if (!contains_only_digits(patch)) {
93+
return std::unexpected(ParseError::INVALID_INPUT);
94+
}
6795
version.patch = patch;
6896
if (dot_iterator == std::string_view::npos) {
6997
return version;

tests/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
include(GoogleTest)
12
include(CTest)
23
add_executable(basictests basictests.cpp)
4+
target_link_libraries(
5+
basictests
6+
GTest::gtest_main
7+
)
38
target_link_libraries(basictests version_weaver)
4-
add_test(basictests_test basictests)
9+
add_test(basictests_test basictests)
10+
gtest_discover_tests(basictests)

tests/basictests.cpp

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,86 @@
11
#include "version_weaver.h"
2-
#include <cstdlib>
3-
#include <vector>
42
#include <format>
3+
#include <vector>
4+
5+
#include <gtest/gtest.h>
6+
7+
using TestData =
8+
std::pair<std::string, std::expected<version_weaver::Version,
9+
version_weaver::ParseError>>;
10+
std::vector<TestData> parse_values = {
11+
{"1.0.0", version_weaver::Version{"1", "0", "0"}},
12+
{"1.0.0-alpha", version_weaver::Version{"1", "0", "0", "alpha"}},
13+
{"1.0.0-alpha.1", version_weaver::Version{"1", "0", "0", "alpha.1"}},
14+
{"1.0.0-0.3.7", version_weaver::Version{"1", "0", "0", "0.3.7"}},
15+
{"1.0.0-x.7.z.92", version_weaver::Version{"1", "0", "0", "x.7.z.92"}},
16+
{"1.0.0-x-y-z.--", version_weaver::Version{"1", "0", "0", "x-y-z.--"}},
17+
{"1.0.0-alpha+001", version_weaver::Version{"1", "0", "0", "alpha", "001"}},
18+
{"1.0.0+20130313144700",
19+
version_weaver::Version{"1", "0", "0", std::nullopt, "20130313144700"}},
20+
{"1.0.0-beta+exp.sha.5114f85",
21+
version_weaver::Version{"1", "0", "0", "beta", "exp.sha.5114f85"}},
22+
{"1.0.0+21AF26D3----117B344092BD",
23+
version_weaver::Version{"1", "0", "0", std::nullopt,
24+
"21AF26D3----117B344092BD"}},
525

6-
std::vector<std::pair<std::string, std::expected<version_weaver::Version,
7-
version_weaver::ParseError>>>
8-
test_values = {
9-
{"1.0.0", version_weaver::Version{"1", "0", "0"}},
10-
{"1.0.0-alpha", version_weaver::Version{"1", "0", "0", "alpha"}},
11-
{"1.0.0-alpha.1", version_weaver::Version{"1", "0", "0", "alpha.1"}},
12-
{"1.0.0-0.3.7", version_weaver::Version{"1", "0", "0", "0.3.7"}},
13-
{"1.0.0-x.7.z.92", version_weaver::Version{"1", "0", "0", "x.7.z.92"}},
14-
{"1.0.0-x-y-z.--", version_weaver::Version{"1", "0", "0", "x-y-z.--"}},
15-
{"1.0.0-alpha+001",
16-
version_weaver::Version{"1", "0", "0", "alpha", "001"}},
17-
{"1.0.0+20130313144700",
18-
version_weaver::Version{"1", "0", "0", "", "20130313144700"}},
19-
{"1.0.0-beta+exp.sha.5114f85",
20-
version_weaver::Version{"1", "0", "0", "beta", "exp.sha.5114f85"}},
21-
{"1.0.0+21AF26D3----117B344092BD",
22-
version_weaver::Version{"1", "0", "0", "",
23-
"21AF26D3----117B344092BD"}},
2426
};
2527

26-
bool test_parse() {
27-
for (const auto& [input, expected] : test_values) {
28-
auto result = version_weaver::parse(input);
29-
if (bool(result) != bool(expected)) {
30-
std::printf("Expected %d, got %d\n", bool(expected), bool(result));
31-
return false;
32-
}
33-
if (bool(result)) {
34-
if (result.value() != expected.value()) {
35-
std::printf("Expected \n");
36-
return false;
37-
}
28+
TEST(basictests, parse) {
29+
for (const auto& [input, expected] : parse_values) {
30+
auto parse_result = version_weaver::parse(input);
31+
ASSERT_EQ(parse_result.has_value(), expected.has_value());
32+
if (parse_result.has_value()) {
33+
ASSERT_EQ(parse_result->major, expected->major);
34+
ASSERT_EQ(parse_result->minor, expected->minor);
35+
ASSERT_EQ(parse_result->patch, expected->patch);
36+
ASSERT_EQ(parse_result->pre_release, expected->pre_release);
37+
ASSERT_EQ(parse_result->build, expected->build);
38+
} else {
39+
ASSERT_EQ(parse_result.error(), expected.error());
3840
}
3941
}
40-
return true;
42+
43+
SUCCEED();
4144
}
42-
int main() {
43-
if (!test_parse()) {
44-
std::printf("Test failed\n");
45-
return EXIT_FAILURE;
45+
46+
// A normal version number MUST take the form X.Y.Z
47+
// where X, Y, and Z are non-negative integers, and
48+
// MUST NOT contain leading zeroes.
49+
TEST(basictests, leading_zeroes) {
50+
ASSERT_FALSE(version_weaver::parse("0.0.0").has_value());
51+
ASSERT_FALSE(version_weaver::parse("01.0.0").has_value());
52+
ASSERT_FALSE(version_weaver::parse("1.01.0").has_value());
53+
ASSERT_FALSE(version_weaver::parse("1.0.01").has_value());
54+
}
55+
56+
std::vector<TestData> clean_values = {
57+
{"1.2.3", version_weaver::Version{"1", "2", "3"}},
58+
{" 1.2.3 ", version_weaver::Version{"1", "2", "3"}},
59+
{" 1.2.3-4 ", version_weaver::Version{"1", "2", "3", "4"}},
60+
{" 1.2.3-pre ", version_weaver::Version{"1", "2", "3", "pre"}},
61+
{" =v1.2.3 ", version_weaver::Version{"1", "2", "3"}},
62+
{"v1.2.3", version_weaver::Version{"1", "2", "3"}},
63+
{" v1.2.3 ", version_weaver::Version{"1", "2", "3"}},
64+
{"\t1.2.3", version_weaver::Version{"1", "2", "3"}},
65+
{">1.2.3", std::unexpected(version_weaver::ParseError::INVALID_INPUT)},
66+
{"~1.2.3", std::unexpected(version_weaver::ParseError::INVALID_INPUT)},
67+
{"<=1.2.3", std::unexpected(version_weaver::ParseError::INVALID_INPUT)},
68+
{"1.2.x", std::unexpected(version_weaver::ParseError::INVALID_INPUT)},
69+
};
70+
71+
TEST(basictests, clean) {
72+
for (const auto& [input, expected] : clean_values) {
73+
auto cleaned_result = version_weaver::clean(input);
74+
std::printf("input: %s\n", input.c_str());
75+
ASSERT_EQ(cleaned_result.has_value(), expected.has_value());
76+
if (cleaned_result.has_value()) {
77+
ASSERT_EQ(cleaned_result->major, expected->major);
78+
ASSERT_EQ(cleaned_result->minor, expected->minor);
79+
ASSERT_EQ(cleaned_result->patch, expected->patch);
80+
ASSERT_EQ(cleaned_result->pre_release, expected->pre_release);
81+
ASSERT_EQ(cleaned_result->build, expected->build);
82+
} else {
83+
ASSERT_EQ(cleaned_result.error(), expected.error());
84+
}
4685
}
47-
std::printf("Tests succeeded!\n");
48-
return EXIT_SUCCESS;
49-
}
86+
}

0 commit comments

Comments
 (0)