From b319e0d555f134f75ec523d9fa72eb42671ab3e8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 00:58:59 -0800 Subject: [PATCH 1/3] Common: test that an empty JSON object survives dump and parse JsonValue::dump() writes an empty JsonObject as "null", so a document re-dumped after parsing loses every "{}" and the reader sees null. This test is red until to_nlohmann() builds objects as objects. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- Common/Cpp/Json/JsonTools_Tests.cpp | 60 +++++++++++++++++++ Common/Cpp/Json/JsonTools_Tests.h | 21 +++++++ .../ComputerPrograms_UnitTestRunner.cpp | 2 + SerialPrograms/cmake/SourceFiles.cmake | 2 + 4 files changed, 85 insertions(+) create mode 100644 Common/Cpp/Json/JsonTools_Tests.cpp create mode 100644 Common/Cpp/Json/JsonTools_Tests.h diff --git a/Common/Cpp/Json/JsonTools_Tests.cpp b/Common/Cpp/Json/JsonTools_Tests.cpp new file mode 100644 index 0000000000..97ea48d765 --- /dev/null +++ b/Common/Cpp/Json/JsonTools_Tests.cpp @@ -0,0 +1,60 @@ +/* JSON Tools Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "JsonObject.h" +#include "JsonValue.h" +#include "JsonTools_Tests.h" + +namespace PokemonAutomation{ +namespace{ + + + +class Test_JsonTools_EmptyObjectRoundTrip : public UnitTest{ +public: + Test_JsonTools_EmptyObjectRoundTrip() + : UnitTest("Common::JsonTools - Empty object round trip") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + const std::string empty = JsonValue(JsonObject()).dump(-1); + if (empty != "{}"){ + return UnitTestResult("dump of an empty object: expected {}, got " + empty); + } + + const std::string text = parse_json(R"({"array":[],"object":{}})").dump(-1); + const JsonValue reparsed = parse_json(text); + const JsonObject* root = reparsed.to_object(); + if (root == nullptr){ + return UnitTestResult("reparsed document: expected an object, got " + text); + } + const JsonValue* object = root->get_value("object"); + if (object == nullptr || !object->is_object()){ + return UnitTestResult("nested empty object: expected an object, got " + text); + } + const JsonValue* array = root->get_value("array"); + if (array == nullptr || !array->is_array()){ + return UnitTestResult("nested empty array: expected an array, got " + text); + } + return true; + } +}; + + + +} // anonymous namespace + + + +void add_tests_JsonTools(UnitTestDatabase& database){ + database.add(); +} + + + +} diff --git a/Common/Cpp/Json/JsonTools_Tests.h b/Common/Cpp/Json/JsonTools_Tests.h new file mode 100644 index 0000000000..fdf4d826ac --- /dev/null +++ b/Common/Cpp/Json/JsonTools_Tests.h @@ -0,0 +1,21 @@ +/* JSON Tools Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_JsonTools_Tests_H +#define PokemonAutomation_JsonTools_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ + + + +void add_tests_JsonTools(UnitTestDatabase& database); + + + +} +#endif diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 17a842be52..0a83d660fd 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -4,6 +4,7 @@ * */ +#include "Common/Cpp/Json/JsonTools_Tests.h" #include "Common/Cpp/ListenerSet.h" #include "Common/Cpp/MoveOnlyFunction_Tests.h" #include "Common/Cpp/ScopeExit.h" @@ -56,6 +57,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ add_tests_Sockets(ret, GlobalThreadPools::unlimited_normal()); add_tests_BlackBorderDetector(ret); add_tests_ListenerSet(ret); + add_tests_JsonTools(ret); add_tests_MoveOnlyFunction(ret); SysbotBase::add_tests_SysbotBasePing(ret); SysbotBase::add_tests_SysbotBaseReceiveLine(ret); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index db863c369f..b9832f5266 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -102,6 +102,8 @@ file(GLOB CORE_LIBRARY_SOURCES ../Common/Cpp/Json/JsonObject.h ../Common/Cpp/Json/JsonTools.cpp ../Common/Cpp/Json/JsonTools.h + ../Common/Cpp/Json/JsonTools_Tests.cpp + ../Common/Cpp/Json/JsonTools_Tests.h ../Common/Cpp/Json/JsonValue.cpp ../Common/Cpp/Json/JsonValue.h ../Common/Cpp/LifetimeSanitizer.cpp From e321063428acead759ebe460c50dba5c7eebd85c Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 00:59:35 -0800 Subject: [PATCH 2/3] Common: dump an empty JsonObject as {} instead of null to_nlohmann() started each object as a default nlohmann::json, which is null until a key is added, so an empty object came out as null. Build it as object_t, as arrays already use array_t. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- Common/Cpp/Json/JsonTools.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Cpp/Json/JsonTools.cpp b/Common/Cpp/Json/JsonTools.cpp index 24313a92c7..b1cb5beccc 100644 --- a/Common/Cpp/Json/JsonTools.cpp +++ b/Common/Cpp/Json/JsonTools.cpp @@ -84,7 +84,7 @@ nlohmann::json to_nlohmann(const JsonValue& json){ return ret; } if (json.is_object()){ - nlohmann::json ret; + nlohmann::json::object_t ret; for (const auto& item : *json.to_object()){ ret[item.first] = to_nlohmann(item.second); } From 6ebd110d72a9c0144437aeff03d0feeb0405b5a9 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 01:01:49 -0800 Subject: [PATCH 3/3] Common: table the JSON round-trip test cases One case per container shape, registered from a named table, so a failure names the shape that lost its type. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- Common/Cpp/Json/JsonTools_Tests.cpp | 65 ++++++++++++++++++----------- Common/Cpp/Json/JsonTools_Tests.h | 3 +- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/Common/Cpp/Json/JsonTools_Tests.cpp b/Common/Cpp/Json/JsonTools_Tests.cpp index 97ea48d765..f548ac8a34 100644 --- a/Common/Cpp/Json/JsonTools_Tests.cpp +++ b/Common/Cpp/Json/JsonTools_Tests.cpp @@ -4,9 +4,10 @@ * */ +#include #include +#include #include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "JsonObject.h" #include "JsonValue.h" #include "JsonTools_Tests.h" @@ -15,46 +16,62 @@ namespace{ -class Test_JsonTools_EmptyObjectRoundTrip : public UnitTest{ +struct DumpInput{ + std::string_view name; + std::string_view text; +}; +struct DumpExpectation{ + std::string_view dumped; +}; +struct DumpCase{ + DumpInput input; + DumpExpectation expectation; +}; + +// Empty containers must keep their type through dump() and parse_json(). +const DumpCase CASES[] = { + {.input = {.name = "Empty object", .text = "{}"}, .expectation = {.dumped = "{}"}}, + {.input = {.name = "Nested empty object", .text = R"({"object":{}})"}, .expectation = {.dumped = R"({"object":{}})"}}, + {.input = {.name = "Nested empty array", .text = R"({"array":[]})"}, .expectation = {.dumped = R"({"array":[]})"}}, +}; + + + +class Test_JsonTools_DumpRoundTrip : public UnitTest{ public: - Test_JsonTools_EmptyObjectRoundTrip() - : UnitTest("Common::JsonTools - Empty object round trip") + explicit Test_JsonTools_DumpRoundTrip(DumpCase test) + : UnitTest(std::format("Common::JsonTools - {} round trip", test.input.name)) + , m_test(test) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - const std::string empty = JsonValue(JsonObject()).dump(-1); - if (empty != "{}"){ - return UnitTestResult("dump of an empty object: expected {}, got " + empty); - } - - const std::string text = parse_json(R"({"array":[],"object":{}})").dump(-1); - const JsonValue reparsed = parse_json(text); - const JsonObject* root = reparsed.to_object(); - if (root == nullptr){ - return UnitTestResult("reparsed document: expected an object, got " + text); - } - const JsonValue* object = root->get_value("object"); - if (object == nullptr || !object->is_object()){ - return UnitTestResult("nested empty object: expected an object, got " + text); + const std::string dumped = parse_json(std::string(m_test.input.text)).dump(-1); + if (dumped != m_test.expectation.dumped){ + return UnitTestResult(std::format("dump: expected {}, got {}", m_test.expectation.dumped, dumped)); } - const JsonValue* array = root->get_value("array"); - if (array == nullptr || !array->is_array()){ - return UnitTestResult("nested empty array: expected an array, got " + text); + const std::string again = parse_json(dumped).dump(-1); + if (again != dumped){ + return UnitTestResult(std::format("dump after reparse: expected {}, got {}", dumped, again)); } return true; } + +private: + DumpCase m_test; }; -} // anonymous namespace +} // namespace void add_tests_JsonTools(UnitTestDatabase& database){ - database.add(); + for (const DumpCase& test : CASES){ + database.add(test); + } } -} +} // namespace PokemonAutomation diff --git a/Common/Cpp/Json/JsonTools_Tests.h b/Common/Cpp/Json/JsonTools_Tests.h index fdf4d826ac..76d61d4b7c 100644 --- a/Common/Cpp/Json/JsonTools_Tests.h +++ b/Common/Cpp/Json/JsonTools_Tests.h @@ -13,9 +13,10 @@ namespace PokemonAutomation{ +// Register the JSON dump/parse round-trip tests. void add_tests_JsonTools(UnitTestDatabase& database); -} +} // namespace PokemonAutomation #endif