From 703ac4c3ec81dab73aa6b6f2b0b248c56204f359 Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 17:05:52 +0200 Subject: [PATCH 1/6] Add unit tests --- .gitignore | 1 + Test/CMakeLists.txt | 25 ++++++++++++++++++++----- Test/tests/TestEmptyObject.cpp | 13 +++++++++++++ Test/tests/TestNestedObjects.cpp | 15 +++++++++++++++ Test/tests/TestPrimitives.cpp | 16 ++++++++++++++++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 Test/tests/TestEmptyObject.cpp create mode 100644 Test/tests/TestNestedObjects.cpp create mode 100644 Test/tests/TestPrimitives.cpp diff --git a/.gitignore b/.gitignore index c1d1196..26afbb4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ CMakeUserPresets.json # CMake build output /out +/TestResults diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt index c04d300..900eefd 100644 --- a/Test/CMakeLists.txt +++ b/Test/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable(cppjson-Test) +add_executable(cppjson-Test) target_link_libraries(cppjson-Test PRIVATE cppjson::cppjson @@ -8,8 +8,23 @@ target_sources(cppjson-Test PRIVATE Test.cpp ) -add_test( - NAME cppjson-Test - COMMAND cppjson-Test - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" +set(TEST_SOURCES + TestEmptyObject.cpp + TestPrimitives.cpp + TestNestedObjects.cpp ) + +if(MSVC) + add_compile_options("/Zi") + add_link_options("/PROFILE") +endif() + + +foreach(TEST_SRC IN LISTS TEST_SOURCES) + message(Adding ${TEST_SRC}) + get_filename_component(TEST_NAME ${TEST_SRC} NAME_WE) + add_executable(${TEST_NAME} tests/${TEST_SRC}) + target_link_libraries(${TEST_NAME} PRIVATE cppjson::cppjson) + + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) +endforeach() diff --git a/Test/tests/TestEmptyObject.cpp b/Test/tests/TestEmptyObject.cpp new file mode 100644 index 0000000..18fa150 --- /dev/null +++ b/Test/tests/TestEmptyObject.cpp @@ -0,0 +1,13 @@ +#include +#include + +int main() +{ + cppjson::Object object{}; + if (!object.IsEmpty()) + { + std::println("TestEmptyObject failed"); + return 1; + } + return 0; +} diff --git a/Test/tests/TestNestedObjects.cpp b/Test/tests/TestNestedObjects.cpp new file mode 100644 index 0000000..ab3cebf --- /dev/null +++ b/Test/tests/TestNestedObjects.cpp @@ -0,0 +1,15 @@ +#include +#include + +int main() +{ + cppjson::Object object{}; + object["sub"]["veryNested"] = 6.0; + + if ((double)object["sub"]["veryNested"] != 6.0) + { + std::println("TestNestedObjects failed"); + return 1; + } + return 0; +} diff --git a/Test/tests/TestPrimitives.cpp b/Test/tests/TestPrimitives.cpp new file mode 100644 index 0000000..995e9c5 --- /dev/null +++ b/Test/tests/TestPrimitives.cpp @@ -0,0 +1,16 @@ +#include +#include + +int main() +{ + cppjson::Object object{}; + object["test1"] = "Hello World"; + object["test2"] = 123.0; + + if ((std::string)object["test1"] != "Hello World" || (double)object["test2"] != 123.0) + { + std::println("TestPrimitives failed"); + return 1; + } + return 0; +} From 16b50c3800b0b35348d9bc0cecc72ca82731828f Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 17:06:04 +0200 Subject: [PATCH 2/6] Fix As<> for cvref types --- cppjson/include/cppjson/object.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cppjson/include/cppjson/object.hpp b/cppjson/include/cppjson/object.hpp index c192cbd..2bc1a02 100644 --- a/cppjson/include/cppjson/object.hpp +++ b/cppjson/include/cppjson/object.hpp @@ -67,6 +67,8 @@ namespace cppjson Object& operator=(Object&&) = default; ~Object() = default; + [[nodiscard]] bool IsEmpty() const noexcept { return this->_nodes.empty(); } + class ObjectProxy { public: @@ -76,14 +78,14 @@ namespace cppjson requires(!std::same_as, JsonObject>) explicit(false) operator T&() { - return this->_object.get().As(); + return this->_object.get().As>(); } template requires(!std::same_as, JsonObject>) explicit(false) operator const T&() const { - return this->_object.get().As(); + return this->_object.get().As>(); } template From a9c9123350c046d2ff75d56ef0e2952779579058 Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 17:09:23 +0200 Subject: [PATCH 3/6] Ambiguous overload for basic_string (MSVC is fine with it of course, silly) --- Test/tests/TestPrimitives.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/tests/TestPrimitives.cpp b/Test/tests/TestPrimitives.cpp index 995e9c5..823af52 100644 --- a/Test/tests/TestPrimitives.cpp +++ b/Test/tests/TestPrimitives.cpp @@ -7,7 +7,7 @@ int main() object["test1"] = "Hello World"; object["test2"] = 123.0; - if ((std::string)object["test1"] != "Hello World" || (double)object["test2"] != 123.0) + if (static_cast(object["test1"]) != "Hello World" || (double)object["test2"] != 123.0) { std::println("TestPrimitives failed"); return 1; From 2d8b3588a78cb88c999c0a28272413a7bf9c483e Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 17:10:50 +0200 Subject: [PATCH 4/6] uh more ambiguous (probably due to const-T-ref overload resolution) --- Test/tests/TestPrimitives.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/tests/TestPrimitives.cpp b/Test/tests/TestPrimitives.cpp index 823af52..9207381 100644 --- a/Test/tests/TestPrimitives.cpp +++ b/Test/tests/TestPrimitives.cpp @@ -7,7 +7,7 @@ int main() object["test1"] = "Hello World"; object["test2"] = 123.0; - if (static_cast(object["test1"]) != "Hello World" || (double)object["test2"] != 123.0) + if (static_cast(object["test1"]) != "Hello World" || (double)object["test2"] != 123.0) { std::println("TestPrimitives failed"); return 1; From 6bcc9a3f6261dc6c14c93781db5f55ece180c1e3 Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 21:34:42 +0200 Subject: [PATCH 5/6] Testing framework + dependency: Google Test --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 26afbb4..70c721b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ CMakeUserPresets.json # CMake build output /out /TestResults +/Testing From d1f5f3df1f29f1d986898ffaa75f6e3bf69eb2d1 Mon Sep 17 00:00:00 2001 From: TymianekPL Date: Sun, 14 Sep 2025 21:38:51 +0200 Subject: [PATCH 6/6] tests part 2 (VS choke on itself) --- Test/CMakeLists.txt | 27 +++++----- Test/Test.cpp | 83 +++++++++++++++++++++--------- Test/tests/TestEmptyObject.cpp | 13 ----- Test/tests/TestNestedObjects.cpp | 15 ------ Test/tests/TestPrimitives.cpp | 16 ------ cppjson-Test-results.xml | 9 ++++ cppjson/include/cppjson/object.hpp | 37 +++++++++++++ cppjson/src/object.cpp | 26 ++++++++++ 8 files changed, 144 insertions(+), 82 deletions(-) delete mode 100644 Test/tests/TestEmptyObject.cpp delete mode 100644 Test/tests/TestNestedObjects.cpp delete mode 100644 Test/tests/TestPrimitives.cpp create mode 100644 cppjson-Test-results.xml diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt index 900eefd..fb7f542 100644 --- a/Test/CMakeLists.txt +++ b/Test/CMakeLists.txt @@ -4,27 +4,24 @@ target_link_libraries(cppjson-Test PRIVATE cppjson::cppjson ) +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/heads/main.zip +) +FetchContent_MakeAvailable(googletest) + + target_sources(cppjson-Test PRIVATE Test.cpp ) -set(TEST_SOURCES - TestEmptyObject.cpp - TestPrimitives.cpp - TestNestedObjects.cpp -) +target_link_libraries(cppjson-Test PRIVATE gtest_main) + +include(GoogleTest) +gtest_discover_tests(cppjson-Test) if(MSVC) add_compile_options("/Zi") add_link_options("/PROFILE") endif() - - -foreach(TEST_SRC IN LISTS TEST_SOURCES) - message(Adding ${TEST_SRC}) - get_filename_component(TEST_NAME ${TEST_SRC} NAME_WE) - add_executable(${TEST_NAME} tests/${TEST_SRC}) - target_link_libraries(${TEST_NAME} PRIVATE cppjson::cppjson) - - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) -endforeach() diff --git a/Test/Test.cpp b/Test/Test.cpp index 789fd22..0500636 100644 --- a/Test/Test.cpp +++ b/Test/Test.cpp @@ -1,30 +1,67 @@ +#include #include -#include -int main() +TEST(BasicTests, ArraySize) +{ + cppjson::Array array{}; + array[] = 1; + array[] = 2; + array[] = 3.0; + EXPECT_TRUE(array.Size() == 3); +} + +TEST(BasicTests, InvalidAssignment) +{ + cppjson::Object obj{}; + obj["number"] = 123.0; + EXPECT_THROW({ obj["number"] = "NaN"; }, std::logic_error); +} + +TEST(BasicTests, NestedObjects) +{ + cppjson::Object object{}; + object["sub"]["veryNested"] = 6.0; + + EXPECT_EQ(6.0, (double)object["sub"]["veryNested"]); +} + +TEST(BasicTests, ObjectTypes) +{ + cppjson::Object obj{}; + obj["string"] = "Hello"; + obj["number"] = 42.0; + obj["boolean"] = true; + obj["null"] = nullptr; + + EXPECT_TRUE(IsType(obj["string"])); + EXPECT_TRUE(IsType(obj["number"])); + EXPECT_TRUE(IsType(obj["boolean"])); + EXPECT_TRUE(IsType(obj["null"])); +} + +TEST(BasicTests, Primitives) { cppjson::Object object{}; - std::println("{}", object); object["test1"] = "Hello World"; object["test2"] = 123.0; - object["sub"]["veryNested"] = 6.0; - cppjson::Array& array = object["array"]; - array[] = 2; - array[] = 6.0; - array[0] = 1; - array[] = "Stirng"; - array.EmplaceBack(nullptr); - try - { - array[2] = true; - } - catch (const std::logic_error& error) - { - std::println("Error = {}", error.what()); - } - - std::println("{}", object); - std::println("object[\"test1\"] = {}", object["test1"]); - const std::string test = object["test1"]; - std::println("test = {}", test); + + EXPECT_EQ("Hello World", static_cast(object["test1"])); + EXPECT_EQ(123.0, (double)object["test2"]); +} + +TEST(BasicTests, ValueComparisons) +{ + cppjson::Object obj{}; + obj["a"] = 5.0; + obj["b"] = 5.0; + obj["c"] = 10.0; + + EXPECT_TRUE(obj["a"] == obj["b"]); + EXPECT_FALSE(obj["a"] == obj["c"]); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/Test/tests/TestEmptyObject.cpp b/Test/tests/TestEmptyObject.cpp deleted file mode 100644 index 18fa150..0000000 --- a/Test/tests/TestEmptyObject.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -int main() -{ - cppjson::Object object{}; - if (!object.IsEmpty()) - { - std::println("TestEmptyObject failed"); - return 1; - } - return 0; -} diff --git a/Test/tests/TestNestedObjects.cpp b/Test/tests/TestNestedObjects.cpp deleted file mode 100644 index ab3cebf..0000000 --- a/Test/tests/TestNestedObjects.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include - -int main() -{ - cppjson::Object object{}; - object["sub"]["veryNested"] = 6.0; - - if ((double)object["sub"]["veryNested"] != 6.0) - { - std::println("TestNestedObjects failed"); - return 1; - } - return 0; -} diff --git a/Test/tests/TestPrimitives.cpp b/Test/tests/TestPrimitives.cpp deleted file mode 100644 index 9207381..0000000 --- a/Test/tests/TestPrimitives.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - -int main() -{ - cppjson::Object object{}; - object["test1"] = "Hello World"; - object["test2"] = 123.0; - - if (static_cast(object["test1"]) != "Hello World" || (double)object["test2"] != 123.0) - { - std::println("TestPrimitives failed"); - return 1; - } - return 0; -} diff --git a/cppjson-Test-results.xml b/cppjson-Test-results.xml new file mode 100644 index 0000000..108af66 --- /dev/null +++ b/cppjson-Test-results.xml @@ -0,0 +1,9 @@ + + diff --git a/cppjson/include/cppjson/object.hpp b/cppjson/include/cppjson/object.hpp index 2bc1a02..4270f20 100644 --- a/cppjson/include/cppjson/object.hpp +++ b/cppjson/include/cppjson/object.hpp @@ -38,6 +38,8 @@ namespace cppjson template const T& As() const noexcept(false); + [[nodiscard]] bool operator==(const JsonObject& other) const; + private: JsonType _dataType{}; std::byte* _dataStorage{}; @@ -55,6 +57,9 @@ namespace cppjson } friend struct std::formatter; + + template + friend bool IsType(const JsonObject& object) noexcept; }; class Object @@ -69,6 +74,8 @@ namespace cppjson [[nodiscard]] bool IsEmpty() const noexcept { return this->_nodes.empty(); } + [[nodiscard]] bool operator==(const Object& other) const; + class ObjectProxy { public: @@ -108,11 +115,14 @@ namespace cppjson { return (*this)[std::string{key}]; } + [[nodiscard]] bool operator==(const ObjectProxy& other) const { return this->_object.get() == other._object.get(); } private: std::reference_wrapper _object; friend struct std::formatter; + template + friend bool IsType(const Object::ObjectProxy& proxy) noexcept; }; class ConstObjectProxy @@ -180,10 +190,37 @@ namespace cppjson return Object::ConstObjectProxy{this->_objects.at(index)}; } + [[nodiscard]] std::size_t Size() const noexcept { return this->_objects.size(); } + + [[nodiscard]] bool operator==(const Array& other) const + { + if (this->_objects.size() != other._objects.size()) return false; + return std::equal(this->_objects.begin(), this->_objects.end(), other._objects.begin()); + } + private: std::vector _objects{}; friend struct std::formatter; friend struct std::formatter; }; + + template + [[nodiscard]] bool IsType(const JsonObject& object) noexcept + { + if constexpr (std::same_as, std::nullptr_t>) return object._dataType == JsonType::Null; + else if constexpr (std::same_as, std::string>) return object._dataType == JsonType::String; + else if constexpr (std::same_as, Object>) return object._dataType == JsonType::Object; + else if constexpr (std::same_as, double>) return object._dataType == JsonType::Number; + else if constexpr (std::same_as, bool>) return object._dataType == JsonType::Bool; + else if constexpr (std::same_as, Array>) return object._dataType == JsonType::Array; + else + return false; + } + + template + [[nodiscard]] bool IsType(const Object::ObjectProxy& proxy) noexcept + { + return IsType(proxy._object.get()); + } } // namespace cppjson diff --git a/cppjson/src/object.cpp b/cppjson/src/object.cpp index 955950d..070bfe6 100644 --- a/cppjson/src/object.cpp +++ b/cppjson/src/object.cpp @@ -46,6 +46,21 @@ cppjson::JsonObject::~JsonObject() ::operator delete(this->_dataStorage); } +bool cppjson::JsonObject::operator==(const JsonObject& other) const +{ + if (other._dataType != this->_dataType) return false; + switch (this->_dataType) + { + case JsonType::Null: return true; + case JsonType::Number: return this->DangerousAs() == other.DangerousAs(); + case JsonType::Bool: return this->DangerousAs() == other.DangerousAs(); + case JsonType::String: return this->DangerousAs() == other.DangerousAs(); + case JsonType::Object: return this->DangerousAs() == other.DangerousAs(); + case JsonType::Array: return this->DangerousAs() == other.DangerousAs(); + default: return false; + } +} + void cppjson::JsonObject::Destroy(void) { using cppjson::Array; @@ -185,3 +200,14 @@ cppjson::Object::ConstObjectProxy cppjson::Object::ConstObjectProxy::operator[]( { return ConstObjectProxy{this->_object.get().As()[key]}; } + +bool cppjson::Object::operator==(const Object& other) const +{ + if (this->_nodes.size() != other._nodes.size()) return false; + for (const auto& [key, value] : this->_nodes) + { + if (!other._nodes.contains(key)) return false; + if (!(value == other._nodes.at(key))) return false; + } + return true; +}