From 34b5de17447a267591606efd22514d9fcaa457a8 Mon Sep 17 00:00:00 2001 From: Andrew Noyes Date: Mon, 18 Aug 2025 06:27:24 -0400 Subject: [PATCH] Use reference parser for tests --- CMakeLists.txt | 4 +- benchmarks/test_data.cpp | 27 +- src/nlohmann_reference_parser.cpp | 269 +++++++ src/nlohmann_reference_parser.hpp | 43 ++ src/parser_comparison.cpp | 147 ++++ src/parser_comparison.hpp | 74 ++ tests/test_commit_request.cpp | 1189 ++++++++++------------------- 7 files changed, 961 insertions(+), 792 deletions(-) create mode 100644 src/nlohmann_reference_parser.cpp create mode 100644 src/nlohmann_reference_parser.hpp create mode 100644 src/parser_comparison.cpp create mode 100644 src/parser_comparison.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d8ef79c..cc59f4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ enable_testing() # Create shared test data library add_library(test_data STATIC benchmarks/test_data.cpp) target_include_directories(test_data PUBLIC benchmarks) +target_link_libraries(test_data simdutf::simdutf) add_executable(test_arena_allocator tests/test_arena_allocator.cpp src/arena_allocator.cpp) @@ -109,10 +110,11 @@ target_include_directories(test_arena_allocator PRIVATE src) add_executable( test_commit_request tests/test_commit_request.cpp src/json_commit_request_parser.cpp + src/nlohmann_reference_parser.cpp src/parser_comparison.cpp src/arena_allocator.cpp ${CMAKE_BINARY_DIR}/json_tokens.cpp) add_dependencies(test_commit_request generate_json_tokens) target_link_libraries(test_commit_request doctest::doctest weaseljson test_data - simdutf::simdutf) + nlohmann_json::nlohmann_json simdutf::simdutf) target_include_directories(test_commit_request PRIVATE src) add_executable(bench_arena_allocator benchmarks/bench_arena_allocator.cpp diff --git a/benchmarks/test_data.cpp b/benchmarks/test_data.cpp index 89de1da..3ab03b0 100644 --- a/benchmarks/test_data.cpp +++ b/benchmarks/test_data.cpp @@ -1,4 +1,6 @@ #include "test_data.hpp" +#include +#include namespace weaseldb::test_data { @@ -89,6 +91,21 @@ const std::string COMPLEX_JSON = R"({ ] })"; +// Helper function to encode a string as base64 +std::string encode_base64(const std::string &input) { + if (input.empty()) { + return ""; + } + + size_t max_output_size = simdutf::base64_length_from_binary(input.size()); + std::vector output(max_output_size); + + size_t written = simdutf::binary_to_base64( + input.data(), input.size(), output.data(), simdutf::base64_default); + + return std::string(output.data(), written); +} + // Generate a large JSON with many operations for stress testing std::string generate_large_json(int num_operations) { std::string json = R"({ @@ -101,13 +118,19 @@ std::string generate_large_json(int num_operations) { for (int i = 0; i < num_operations; ++i) { if (i > 0) json += ","; + + std::string key = "key" + std::to_string(i); + std::string value = "value" + std::to_string(i); + std::string key_b64 = encode_base64(key); + std::string value_b64 = encode_base64(value); + json += R"( { "type": "write", "key": ")" + - std::string("key") + std::to_string(i) + R"(", + key_b64 + R"(", "value": ")" + - std::string("value") + std::to_string(i) + R"(" + value_b64 + R"(" })"; } diff --git a/src/nlohmann_reference_parser.cpp b/src/nlohmann_reference_parser.cpp new file mode 100644 index 0000000..06d0491 --- /dev/null +++ b/src/nlohmann_reference_parser.cpp @@ -0,0 +1,269 @@ +#include "nlohmann_reference_parser.hpp" +#include +#include +#include + +NlohmannReferenceParser::ParseResult +NlohmannReferenceParser::parse(CommitRequest &request, + const std::string &json_str) { + error_message_.clear(); + request.reset(); + + try { + nlohmann::json j = nlohmann::json::parse(json_str); + + // Parse required fields + if (!j.contains("leader_id") || !j["leader_id"].is_string()) { + error_message_ = "Missing or invalid leader_id"; + return ParseResult::ValidationError; + } + + std::string leader_id = j["leader_id"]; + if (leader_id.empty()) { + error_message_ = "Empty leader_id"; + return ParseResult::ValidationError; + } + request.set_leader_id(request.copy_to_arena(leader_id)); + + if (!j.contains("read_version") || + !j["read_version"].is_number_unsigned()) { + error_message_ = "Missing or invalid read_version"; + return ParseResult::ValidationError; + } + request.set_read_version(j["read_version"]); + + // Parse optional request_id + if (j.contains("request_id")) { + if (!j["request_id"].is_string()) { + error_message_ = "Invalid request_id type"; + return ParseResult::ValidationError; + } + std::string request_id = j["request_id"]; + request.set_request_id(request.copy_to_arena(request_id)); + } + + // Parse optional preconditions + if (j.contains("preconditions")) { + if (!j["preconditions"].is_array()) { + error_message_ = "Preconditions must be an array"; + return ParseResult::ValidationError; + } + if (!parse_preconditions(request, j["preconditions"])) { + return ParseResult::ValidationError; + } + } + + // Parse optional operations + if (j.contains("operations")) { + if (!j["operations"].is_array()) { + error_message_ = "Operations must be an array"; + return ParseResult::ValidationError; + } + if (!parse_operations(request, j["operations"])) { + return ParseResult::ValidationError; + } + } + + request.finalize(); + return ParseResult::Success; + + } catch (const nlohmann::json::parse_error &e) { + error_message_ = "JSON parse error: " + std::string(e.what()); + return ParseResult::ParseError; + } catch (const nlohmann::json::exception &e) { + error_message_ = "JSON error: " + std::string(e.what()); + return ParseResult::ParseError; + } +} + +bool NlohmannReferenceParser::parse_preconditions( + CommitRequest &request, const nlohmann::json &preconditions_array) { + for (const auto &precondition : preconditions_array) { + if (!precondition.is_object()) { + error_message_ = "Precondition must be an object"; + return false; + } + + // Parse type + if (!precondition.contains("type") || !precondition["type"].is_string()) { + error_message_ = "Precondition missing type"; + return false; + } + + std::string type_str = precondition["type"]; + Precondition::Type type = parse_precondition_type(type_str); + if (type_str != "point_read" && type_str != "range_read") { + error_message_ = "Invalid precondition type: " + type_str; + return false; + } + + // Parse version (optional, defaults to 0) + uint64_t version = 0; + if (precondition.contains("version")) { + if (!precondition["version"].is_number_unsigned()) { + error_message_ = "Invalid precondition version"; + return false; + } + version = precondition["version"]; + } + + if (type == Precondition::Type::PointRead) { + // Point read requires key field + if (!precondition.contains("key") || !precondition["key"].is_string()) { + error_message_ = "Point read precondition missing key"; + return false; + } + + std::string key_b64 = precondition["key"]; + std::string key_decoded = decode_base64(key_b64); + + request.add_precondition(type, version, + request.copy_to_arena(key_decoded)); + } else { // RangeRead + // Range read requires begin and end fields + if (!precondition.contains("begin") || + !precondition["begin"].is_string()) { + error_message_ = "Range read precondition missing begin"; + return false; + } + if (!precondition.contains("end") || !precondition["end"].is_string()) { + error_message_ = "Range read precondition missing end"; + return false; + } + + std::string begin_b64 = precondition["begin"]; + std::string end_b64 = precondition["end"]; + std::string begin_decoded = decode_base64(begin_b64); + std::string end_decoded = decode_base64(end_b64); + + request.add_precondition(type, version, + request.copy_to_arena(begin_decoded), + request.copy_to_arena(end_decoded)); + } + } + return true; +} + +bool NlohmannReferenceParser::parse_operations( + CommitRequest &request, const nlohmann::json &operations_array) { + for (const auto &operation : operations_array) { + if (!operation.is_object()) { + error_message_ = "Operation must be an object"; + return false; + } + + // Parse type + if (!operation.contains("type") || !operation["type"].is_string()) { + error_message_ = "Operation missing type"; + return false; + } + + std::string type_str = operation["type"]; + Operation::Type type = parse_operation_type(type_str); + if (type_str != "write" && type_str != "delete" && + type_str != "range_delete") { + error_message_ = "Invalid operation type: " + type_str; + return false; + } + + if (type == Operation::Type::Write) { + // Write requires key and value + if (!operation.contains("key") || !operation["key"].is_string()) { + error_message_ = "Write operation missing key"; + return false; + } + if (!operation.contains("value") || !operation["value"].is_string()) { + error_message_ = "Write operation missing value"; + return false; + } + + std::string key_b64 = operation["key"]; + std::string value_b64 = operation["value"]; + std::string key_decoded = decode_base64(key_b64); + std::string value_decoded = decode_base64(value_b64); + + request.add_operation(type, request.copy_to_arena(key_decoded), + request.copy_to_arena(value_decoded)); + } else if (type == Operation::Type::Delete) { + // Delete requires key + if (!operation.contains("key") || !operation["key"].is_string()) { + error_message_ = "Delete operation missing key"; + return false; + } + + std::string key_b64 = operation["key"]; + std::string key_decoded = decode_base64(key_b64); + + request.add_operation(type, request.copy_to_arena(key_decoded)); + } else { // RangeDelete + // Range delete requires begin and end + if (!operation.contains("begin") || !operation["begin"].is_string()) { + error_message_ = "Range delete operation missing begin"; + return false; + } + if (!operation.contains("end") || !operation["end"].is_string()) { + error_message_ = "Range delete operation missing end"; + return false; + } + + std::string begin_b64 = operation["begin"]; + std::string end_b64 = operation["end"]; + std::string begin_decoded = decode_base64(begin_b64); + std::string end_decoded = decode_base64(end_b64); + + request.add_operation(type, request.copy_to_arena(begin_decoded), + request.copy_to_arena(end_decoded)); + } + } + return true; +} + +std::string +NlohmannReferenceParser::decode_base64(const std::string &base64_str) { + if (base64_str.empty()) { + return ""; + } + + // Calculate required output size for worst case + size_t max_output_size = simdutf::maximal_binary_length_from_base64( + base64_str.data(), base64_str.size()); + + // Allocate output buffer + std::vector output(max_output_size); + + // Decode using simdutf + simdutf::result result = + simdutf::base64_to_binary(base64_str.data(), base64_str.size(), + output.data(), simdutf::base64_default); + + if (result.error == simdutf::error_code::SUCCESS) { + return std::string(output.data(), result.count); + } else { + // Return original string if decode fails (for non-base64 strings) + return base64_str; + } +} + +Precondition::Type +NlohmannReferenceParser::parse_precondition_type(const std::string &type_str) { + if (type_str == "point_read") { + return Precondition::Type::PointRead; + } else if (type_str == "range_read") { + return Precondition::Type::RangeRead; + } + // Default fallback (should not happen if validation is correct) + return Precondition::Type::PointRead; +} + +Operation::Type +NlohmannReferenceParser::parse_operation_type(const std::string &type_str) { + if (type_str == "write") { + return Operation::Type::Write; + } else if (type_str == "delete") { + return Operation::Type::Delete; + } else if (type_str == "range_delete") { + return Operation::Type::RangeDelete; + } + // Default fallback (should not happen if validation is correct) + return Operation::Type::Write; +} \ No newline at end of file diff --git a/src/nlohmann_reference_parser.hpp b/src/nlohmann_reference_parser.hpp new file mode 100644 index 0000000..f2fa908 --- /dev/null +++ b/src/nlohmann_reference_parser.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "commit_request.hpp" +#include +#include +#include + +/** + * @brief Reference implementation of CommitRequest parser using nlohmann/json. + * + * This parser serves as a reference implementation for testing and validation. + * It implements the same parsing logic as JsonCommitRequestParser but uses + * the well-tested nlohmann/json library for simplicity and correctness. + */ +class NlohmannReferenceParser { +public: + enum class ParseResult { Success, ParseError, ValidationError }; + + /** + * @brief Parse a JSON string into a CommitRequest. + * @param request The request object to populate + * @param json_str The JSON string to parse + * @return ParseResult indicating success or failure type + */ + ParseResult parse(CommitRequest &request, const std::string &json_str); + + /** + * @brief Get the last error message if parsing failed. + * @return Error message string, empty if no error + */ + const std::string &get_error() const { return error_message_; } + +private: + std::string error_message_; + + bool parse_preconditions(CommitRequest &request, + const nlohmann::json &preconditions_array); + bool parse_operations(CommitRequest &request, + const nlohmann::json &operations_array); + std::string decode_base64(const std::string &base64_str); + Precondition::Type parse_precondition_type(const std::string &type_str); + Operation::Type parse_operation_type(const std::string &type_str); +}; \ No newline at end of file diff --git a/src/parser_comparison.cpp b/src/parser_comparison.cpp new file mode 100644 index 0000000..8e88eb8 --- /dev/null +++ b/src/parser_comparison.cpp @@ -0,0 +1,147 @@ +#include "parser_comparison.hpp" +#include + +std::string ParserComparison::last_error_; + +ParserComparison::ComparisonResult +ParserComparison::compare_parsers(const std::string &json_str) { + last_error_.clear(); + + // Parse with weaseljson parser + CommitRequest weasel_request; + JsonCommitRequestParser weasel_parser; + std::string mutable_json = + json_str; // JsonCommitRequestParser needs mutable data + auto weasel_result = weasel_parser.parse(weasel_request, mutable_json.data(), + mutable_json.size()); + bool weasel_success = + (weasel_result == CommitRequestParser::ParseResult::Success); + + // Parse with nlohmann reference parser + CommitRequest nlohmann_request; + NlohmannReferenceParser nlohmann_parser; + auto nlohmann_result = nlohmann_parser.parse(nlohmann_request, json_str); + bool nlohmann_success = + (nlohmann_result == NlohmannReferenceParser::ParseResult::Success); + + // Compare results + if (weasel_success && nlohmann_success) { + // Both succeeded - check if they produce equivalent results + if (requests_equal(weasel_request, nlohmann_request)) { + return ComparisonResult::BothSuccess; + } else { + std::ostringstream oss; + oss << "Parsers produced different results. "; + oss << "Weasel: request_id=" + << (weasel_request.request_id().has_value() + ? weasel_request.request_id().value() + : "none"); + oss << ", leader_id='" << weasel_request.leader_id() << "'"; + oss << ", read_version=" << weasel_request.read_version(); + oss << ", preconditions=" << weasel_request.preconditions().size(); + oss << ", operations=" << weasel_request.operations().size() << ". "; + oss << "Nlohmann: request_id=" + << (nlohmann_request.request_id().has_value() + ? nlohmann_request.request_id().value() + : "none"); + oss << ", leader_id='" << nlohmann_request.leader_id() << "'"; + oss << ", read_version=" << nlohmann_request.read_version(); + oss << ", preconditions=" << nlohmann_request.preconditions().size(); + oss << ", operations=" << nlohmann_request.operations().size(); + last_error_ = oss.str(); + return ComparisonResult::DifferentResults; + } + } else if (!weasel_success && !nlohmann_success) { + // Both failed - this is expected for invalid JSON + return ComparisonResult::BothFailure; + } else if (weasel_success && !nlohmann_success) { + // Weasel succeeded but nlohmann failed + last_error_ = "Weasel parser succeeded but nlohmann failed: " + + nlohmann_parser.get_error(); + return ComparisonResult::WeaselSuccessNlohmannFail; + } else { + // Nlohmann succeeded but weasel failed + last_error_ = "Nlohmann parser succeeded but weasel failed"; + if (weasel_parser.get_parse_error()) { + last_error_ += ": " + std::string(weasel_parser.get_parse_error()); + } + return ComparisonResult::NlohmannSuccessWeaselFail; + } +} + +std::string ParserComparison::result_to_string(ComparisonResult result) { + switch (result) { + case ComparisonResult::BothSuccess: + return "Both parsers succeeded with equivalent results"; + case ComparisonResult::BothFailure: + return "Both parsers failed (as expected)"; + case ComparisonResult::WeaselSuccessNlohmannFail: + return "Weasel succeeded but nlohmann failed"; + case ComparisonResult::NlohmannSuccessWeaselFail: + return "Nlohmann succeeded but weasel failed"; + case ComparisonResult::DifferentResults: + return "Both succeeded but produced different results"; + default: + return "Unknown result"; + } +} + +bool ParserComparison::requests_equal(const CommitRequest &req1, + const CommitRequest &req2) { + // Compare request_id + if (req1.request_id().has_value() != req2.request_id().has_value()) { + return false; + } + if (req1.request_id().has_value() && + req1.request_id().value() != req2.request_id().value()) { + return false; + } + + // Compare leader_id + if (req1.leader_id() != req2.leader_id()) { + return false; + } + + // Compare read_version + if (req1.read_version() != req2.read_version()) { + return false; + } + + // Compare preconditions + auto prec1 = req1.preconditions(); + auto prec2 = req2.preconditions(); + if (prec1.size() != prec2.size()) { + return false; + } + for (size_t i = 0; i < prec1.size(); ++i) { + if (!preconditions_equal(prec1[i], prec2[i])) { + return false; + } + } + + // Compare operations + auto ops1 = req1.operations(); + auto ops2 = req2.operations(); + if (ops1.size() != ops2.size()) { + return false; + } + for (size_t i = 0; i < ops1.size(); ++i) { + if (!operations_equal(ops1[i], ops2[i])) { + return false; + } + } + + return true; +} + +bool ParserComparison::preconditions_equal(const Precondition &p1, + const Precondition &p2) { + return p1.type == p2.type && p1.version == p2.version && + p1.begin == p2.begin && p1.end == p2.end; +} + +bool ParserComparison::operations_equal(const Operation &op1, + const Operation &op2) { + return op1.type == op2.type && op1.param1 == op2.param1 && + op1.param2 == op2.param2; +} \ No newline at end of file diff --git a/src/parser_comparison.hpp b/src/parser_comparison.hpp new file mode 100644 index 0000000..fe9c32a --- /dev/null +++ b/src/parser_comparison.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "commit_request.hpp" +#include "json_commit_request_parser.hpp" +#include "nlohmann_reference_parser.hpp" +#include + +/** + * @brief Test utility for comparing parser implementations. + * + * This class provides functionality to test that the weaseljson-based parser + * and the nlohmann/json reference parser produce equivalent results. + */ +class ParserComparison { +public: + enum class ComparisonResult { + BothSuccess, // Both parsers succeeded and produced equivalent results + BothFailure, // Both parsers failed (as expected) + WeaselSuccessNlohmannFail, // Weasel succeeded but nlohmann failed + // (potential bug) + NlohmannSuccessWeaselFail, // Nlohmann succeeded but weasel failed + // (potential bug) + DifferentResults // Both succeeded but produced different CommitRequests + }; + + /** + * @brief Compare two parser implementations on the same JSON input. + * @param json_str The JSON string to parse with both parsers + * @return ComparisonResult indicating the outcome of the comparison + */ + static ComparisonResult compare_parsers(const std::string &json_str); + + /** + * @brief Get a human-readable description of the comparison result. + * @param result The comparison result + * @return String description + */ + static std::string result_to_string(ComparisonResult result); + + /** + * @brief Get the last error message from comparison. + * @return Error message or empty string + */ + static const std::string &get_last_error() { return last_error_; } + +private: + static std::string last_error_; + + /** + * @brief Compare two CommitRequest objects for equality. + * @param req1 First request + * @param req2 Second request + * @return true if equivalent, false otherwise + */ + static bool requests_equal(const CommitRequest &req1, + const CommitRequest &req2); + + /** + * @brief Compare two preconditions for equality. + * @param p1 First precondition + * @param p2 Second precondition + * @return true if equal, false otherwise + */ + static bool preconditions_equal(const Precondition &p1, + const Precondition &p2); + + /** + * @brief Compare two operations for equality. + * @param op1 First operation + * @param op2 Second operation + * @return true if equal, false otherwise + */ + static bool operations_equal(const Operation &op1, const Operation &op2); +}; \ No newline at end of file diff --git a/tests/test_commit_request.cpp b/tests/test_commit_request.cpp index dc20007..66d5801 100644 --- a/tests/test_commit_request.cpp +++ b/tests/test_commit_request.cpp @@ -1,32 +1,52 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "../benchmarks/test_data.hpp" -#include "commit_request.hpp" -#include "json_commit_request_parser.hpp" +#include "parser_comparison.hpp" #include -#include +#include -TEST_CASE("CommitRequest basic parsing") { - CommitRequest request; - JsonCommitRequestParser parser; +/** + * @brief Test helper that uses parser comparison to validate both parsers. + * + * This function tests that either both parsers reject the input, or both + * accept it and produce equivalent CommitRequest objects. + */ +void test_parser_comparison(const std::string &json_str, + const std::string &test_name) { + INFO("Testing: " << test_name); + INFO("JSON: " << json_str); - SUBCASE("Simple commit request") { - std::string json = R"({ + auto result = ParserComparison::compare_parsers(json_str); + + INFO("Comparison result: " << ParserComparison::result_to_string(result)); + if (!ParserComparison::get_last_error().empty()) { + INFO("Error details: " << ParserComparison::get_last_error()); + } + + // Accept either both success or both failure - reject inconsistencies + REQUIRE((result == ParserComparison::ComparisonResult::BothSuccess || + result == ParserComparison::ComparisonResult::BothFailure)); +} + +TEST_CASE("Parser Comparison - Basic Cases") { + SUBCASE("Simple valid request") { + test_parser_comparison(R"({ "request_id": "test123", "leader_id": "leader456", "read_version": 12345 - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - REQUIRE(request.request_id().has_value()); - REQUIRE(request.request_id().value() == "test123"); - REQUIRE(request.leader_id() == "leader456"); - REQUIRE(request.read_version() == 12345); + })", + "Simple valid request"); } - SUBCASE("With preconditions") { - std::string json = R"({ + SUBCASE("Minimal valid request (no request_id)") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345 + })", + "Minimal valid request"); + } + + SUBCASE("Request with preconditions") { + test_parser_comparison(R"({ "leader_id": "leader456", "read_version": 12345, "preconditions": [ @@ -36,20 +56,12 @@ TEST_CASE("CommitRequest basic parsing") { "key": "dGVzdA==" } ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - REQUIRE(request.preconditions().size() == 1); - REQUIRE(request.preconditions()[0].type == Precondition::Type::PointRead); - REQUIRE(request.preconditions()[0].version == 12340); - REQUIRE(request.preconditions()[0].begin == "test"); // "dGVzdA==" decoded - REQUIRE(request.preconditions()[0].end == ""); + })", + "Request with preconditions"); } - SUBCASE("With operations") { - std::string json = R"({ + SUBCASE("Request with operations") { + test_parser_comparison(R"({ "leader_id": "leader456", "read_version": 12345, "operations": [ @@ -63,489 +75,13 @@ TEST_CASE("CommitRequest basic parsing") { "key": "dGVzdDI=" } ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - REQUIRE(request.operations().size() == 2); - - REQUIRE(request.operations()[0].type == Operation::Type::Write); - REQUIRE(request.operations()[0].param1 == "test"); // key - REQUIRE(request.operations()[0].param2 == "value"); // value - - REQUIRE(request.operations()[1].type == Operation::Type::Delete); - REQUIRE(request.operations()[1].param1 == "test2"); // key - REQUIRE(request.operations()[1].param2 == ""); // unused for delete + })", + "Request with operations"); } - SUBCASE("Invalid JSON") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": "not_a_number" - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Missing required leader_id") { - std::string json = R"({ - "request_id": "test123", - "read_version": 12345 - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - // Check completion based on required fields - REQUIRE(request.leader_id().empty()); - } - - SUBCASE("Missing required read_version") { - std::string json = R"({ - "request_id": "test123", - "leader_id": "leader456" - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - // Check completion based on required fields - REQUIRE(!parser.has_read_version_been_set()); - } - - SUBCASE("Empty leader_id") { - std::string json = R"({ - "request_id": "test123", - "leader_id": "", - "read_version": 12345 - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - // Check completion based on required fields - REQUIRE(request.leader_id().empty()); - } - - SUBCASE("Missing both leader_id and read_version") { - std::string json = R"({ - "request_id": "test123" - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - // Check completion based on required fields - bool missing_leader = request.leader_id().empty(); - bool missing_version = !parser.has_read_version_been_set(); - REQUIRE((missing_leader || missing_version)); - } - - SUBCASE("request_id is optional") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345 - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - REQUIRE_FALSE(request.request_id().has_value()); - REQUIRE(request.leader_id() == "leader456"); - REQUIRE(request.read_version() == 12345); - } -} - -TEST_CASE("CommitRequest precondition and operation validation") { - CommitRequest request; - JsonCommitRequestParser parser; - - SUBCASE("Valid point_read precondition") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "point_read", - "key": "dGVzdA==" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid point_read precondition - missing key") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "point_read" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid point_read precondition - empty key") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "point_read", - "key": "" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid range_read precondition") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "range_read", - "version": 12340, - "begin": "dGVzdA==", - "end": "dGVzdFo=" - } - ] - })"; - - auto parse_result = - parser.parse(request, const_cast(json.data()), json.size()); - INFO("Parse result: " << (parse_result == - CommitRequestParser::ParseResult::Success)); - INFO("Parse error: " << parser.has_parse_error()); - const char *error_msg = parser.get_parse_error(); - INFO("Parse error message: " << (error_msg ? std::string(error_msg) - : "none")); - INFO("Leader ID: '" << request.leader_id() << "'"); - INFO("Read version: " << request.read_version()); - - REQUIRE(parse_result == CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid range_read precondition - empty begin/end") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "range_read", - "begin": "", - "end": "" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid range_read precondition - missing begin") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "range_read", - "end": "dGVzdFo=" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid range_read precondition - missing end") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "preconditions": [ - { - "type": "range_read", - "begin": "dGVzdA==" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid write operation") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "key": "dGVzdA==", - "value": "dmFsdWU=" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid write operation - empty key and value") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "key": "", - "value": "" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid write operation - missing key") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "value": "dmFsdWU=" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid write operation - missing value") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "key": "dGVzdA==" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid delete operation") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "delete", - "key": "dGVzdA==" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid delete operation - missing key") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "delete" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Valid range_delete operation") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "range_delete", - "begin": "dGVzdA==", - "end": "dGVzdFo=" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid range_delete operation - missing begin") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "range_delete", - "end": "dGVzdFo=" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Invalid range_delete operation - missing end") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "range_delete", - "begin": "dGVzdA==" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } - - SUBCASE("Mixed valid and invalid operations") { - std::string json = R"({ - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "key": "dGVzdA==", - "value": "dmFsdWU=" - }, - { - "type": "delete" - } - ] - })"; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) != - CommitRequestParser::ParseResult::Success); - } -} - -TEST_CASE("CommitRequest memory management") { - CommitRequest request; - JsonCommitRequestParser parser; - - std::string json = R"({ - "request_id": "test123", - "leader_id": "leader456", - "read_version": 12345, - "operations": [ - { - "type": "write", - "key": "dGVzdA==", - "value": "dmFsdWU=" - } - ] - })"; - - REQUIRE(parser.parse(request, json.data(), json.size()) == - CommitRequestParser::ParseResult::Success); - - // Check that arena allocation worked - REQUIRE(request.total_allocated() > 0); - REQUIRE(request.used_bytes() > 0); - - // Test reset - request.reset(); - REQUIRE(request.request_id().has_value() == false); - REQUIRE(request.leader_id().empty()); - REQUIRE(request.read_version() == 0); - REQUIRE(request.operations().empty()); -} - -TEST_CASE("CommitRequest streaming parsing") { - CommitRequest request; - JsonCommitRequestParser parser; - - SUBCASE("Simple streaming parse") { - std::string json = R"({ - "request_id": "test123", - "leader_id": "leader456", - "read_version": 12345 - })"; - - REQUIRE(parser.begin_streaming_parse(request)); - - // Parse in small chunks to simulate network reception - std::string mutable_json = json; - size_t chunk_size = 10; - size_t offset = 0; - - CommitRequestParser::ParseStatus status = - CommitRequestParser::ParseStatus::Incomplete; - - while (offset < mutable_json.size() && - status == CommitRequestParser::ParseStatus::Incomplete) { - size_t len = std::min(chunk_size, mutable_json.size() - offset); - status = parser.parse_chunk(mutable_json.data() + offset, len); - offset += len; - } - - if (status == CommitRequestParser::ParseStatus::Incomplete) { - status = parser.finish_streaming_parse(); - } - - REQUIRE(status == CommitRequestParser::ParseStatus::Complete); - REQUIRE_FALSE(parser.has_parse_error()); - - REQUIRE(request.request_id().has_value()); - REQUIRE(request.request_id().value() == "test123"); - REQUIRE(request.leader_id() == "leader456"); - REQUIRE(request.read_version() == 12345); - } - - SUBCASE("Streaming parse with complex data") { - std::string json = R"({ - "request_id": "streaming-test", + SUBCASE("Complex request with both preconditions and operations") { + test_parser_comparison(R"({ + "request_id": "complex-test", "leader_id": "leader789", "read_version": 98765, "preconditions": [ @@ -553,6 +89,11 @@ TEST_CASE("CommitRequest streaming parsing") { "type": "point_read", "version": 98764, "key": "dGVzdEtleQ==" + }, + { + "type": "range_read", + "begin": "c3RhcnQ=", + "end": "ZW5k" } ], "operations": [ @@ -564,308 +105,378 @@ TEST_CASE("CommitRequest streaming parsing") { { "type": "delete", "key": "ZGVsZXRlS2V5" + }, + { + "type": "range_delete", + "begin": "cmFuZ2VTdGFydA==", + "end": "cmFuZ2VFbmQ=" } ] - })"; - - REQUIRE(parser.begin_streaming_parse(request)); - - // Parse one character at a time to really stress test streaming - std::string mutable_json = json; - CommitRequestParser::ParseStatus status = - CommitRequestParser::ParseStatus::Incomplete; - - for (size_t i = 0; i < mutable_json.size() && - status == CommitRequestParser::ParseStatus::Incomplete; - ++i) { - status = parser.parse_chunk(mutable_json.data() + i, 1); - } - - if (status == CommitRequestParser::ParseStatus::Incomplete) { - status = parser.finish_streaming_parse(); - } - - REQUIRE(status == CommitRequestParser::ParseStatus::Complete); - - REQUIRE(request.request_id().value() == "streaming-test"); - REQUIRE(request.leader_id() == "leader789"); - REQUIRE(request.read_version() == 98765); - REQUIRE(request.preconditions().size() == 1); - REQUIRE(request.operations().size() == 2); - - // Verify precondition was parsed correctly - REQUIRE(request.preconditions()[0].type == Precondition::Type::PointRead); - REQUIRE(request.preconditions()[0].version == 98764); - REQUIRE(request.preconditions()[0].begin == - "testKey"); // key stored in begin for point_read - REQUIRE(request.preconditions()[0].end == ""); - - // Verify operations were parsed correctly - REQUIRE(request.operations()[0].type == Operation::Type::Write); - REQUIRE(request.operations()[0].param1 == "testKey"); // key - REQUIRE(request.operations()[0].param2 == "testValue"); // value - - REQUIRE(request.operations()[1].type == Operation::Type::Delete); - REQUIRE(request.operations()[1].param1 == "deleteKey"); // key - REQUIRE(request.operations()[1].param2 == ""); // unused for delete + })", + "Complex request"); } +} - SUBCASE("Streaming parse error handling") { - std::string invalid_json = R"({ +TEST_CASE("Parser Comparison - Invalid Cases") { + SUBCASE("Invalid JSON syntax") { + test_parser_comparison(R"({ "leader_id": "leader456", - "read_version": "invalid_number" - })"; - - REQUIRE(parser.begin_streaming_parse(request)); - - std::string mutable_json = invalid_json; - CommitRequestParser::ParseStatus status = - parser.parse_chunk(mutable_json.data(), mutable_json.size()); - - if (status == CommitRequestParser::ParseStatus::Incomplete) { - status = parser.finish_streaming_parse(); - } - - REQUIRE(status == CommitRequestParser::ParseStatus::Error); - REQUIRE(parser.has_parse_error()); + "read_version": "not_a_number" + })", + "Invalid JSON - string for number"); } - SUBCASE("Complete document in single chunk") { - std::string json = R"({"leader_id": "test", "read_version": 123})"; - - REQUIRE(parser.begin_streaming_parse(request)); - - std::string mutable_json = json; - CommitRequestParser::ParseStatus status = - parser.parse_chunk(mutable_json.data(), mutable_json.size()); - - // Should still be incomplete (streaming parser doesn't know if more data is - // coming) - REQUIRE(status == CommitRequestParser::ParseStatus::Incomplete); - - // Signal end of input to complete parsing - status = parser.finish_streaming_parse(); - REQUIRE(status == CommitRequestParser::ParseStatus::Complete); - REQUIRE(request.leader_id() == "test"); - REQUIRE(request.read_version() == 123); + SUBCASE("Missing required leader_id") { + test_parser_comparison(R"({ + "request_id": "test123", + "read_version": 12345 + })", + "Missing leader_id"); } - SUBCASE("Streaming parse missing required leader_id") { - std::string json = R"({"request_id": "test", "read_version": 123})"; - - REQUIRE(parser.begin_streaming_parse(request)); - - std::string mutable_json = json; - CommitRequestParser::ParseStatus status = - parser.parse_chunk(mutable_json.data(), mutable_json.size()); - - if (status == CommitRequestParser::ParseStatus::Incomplete) { - status = parser.finish_streaming_parse(); - } - - REQUIRE(status == CommitRequestParser::ParseStatus::Complete); - // Check that required field is missing - REQUIRE(request.leader_id().empty()); + SUBCASE("Missing required read_version") { + test_parser_comparison(R"({ + "request_id": "test123", + "leader_id": "leader456" + })", + "Missing read_version"); } - SUBCASE("Streaming parse missing required read_version") { - std::string json = R"({"request_id": "test", "leader_id": "leader123"})"; + SUBCASE("Empty leader_id") { + test_parser_comparison(R"({ + "request_id": "test123", + "leader_id": "", + "read_version": 12345 + })", + "Empty leader_id"); + } - REQUIRE(parser.begin_streaming_parse(request)); + SUBCASE("Invalid precondition - missing key") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "point_read" + } + ] + })", + "Invalid precondition - missing key"); + } - std::string mutable_json = json; - CommitRequestParser::ParseStatus status = - parser.parse_chunk(mutable_json.data(), mutable_json.size()); + SUBCASE("Invalid precondition - missing begin for range_read") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "range_read", + "end": "dGVzdFo=" + } + ] + })", + "Invalid precondition - missing begin"); + } - if (status == CommitRequestParser::ParseStatus::Incomplete) { - status = parser.finish_streaming_parse(); - } + SUBCASE("Invalid precondition - missing end for range_read") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "range_read", + "begin": "dGVzdA==" + } + ] + })", + "Invalid precondition - missing end"); + } - REQUIRE(status == CommitRequestParser::ParseStatus::Complete); - // Check that required field is missing - REQUIRE(!parser.has_read_version_been_set()); + SUBCASE("Invalid operation - missing key for write") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "write", + "value": "dmFsdWU=" + } + ] + })", + "Invalid operation - missing key"); + } + + SUBCASE("Invalid operation - missing value for write") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "write", + "key": "dGVzdA==" + } + ] + })", + "Invalid operation - missing value"); + } + + SUBCASE("Invalid operation - missing key for delete") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "delete" + } + ] + })", + "Invalid operation - missing key for delete"); + } + + SUBCASE("Invalid operation - missing begin for range_delete") { + test_parser_comparison( + R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "range_delete", + "end": "dGVzdFo=" + } + ] + })", + "Invalid operation - missing begin for range_delete"); + } + + SUBCASE("Invalid operation - missing end for range_delete") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "range_delete", + "begin": "dGVzdA==" + } + ] + })", + "Invalid operation - missing end for range_delete"); + } + + SUBCASE("Malformed JSON") { + test_parser_comparison(R"({ + "leader_id": "leader456" + "read_version": 12345 + })", + "Malformed JSON - missing comma"); + } + + SUBCASE("Invalid precondition type") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "invalid_type", + "key": "dGVzdA==" + } + ] + })", + "Invalid precondition type"); + } + + SUBCASE("Invalid operation type") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "invalid_operation", + "key": "dGVzdA==" + } + ] + })", + "Invalid operation type"); } } -TEST_CASE("CommitRequest arena debug dump") { - CommitRequest request; - JsonCommitRequestParser parser; - - SUBCASE("Arena debug dump with COMPLEX_JSON") { - // Parse the complex JSON to populate the arena with various data structures - std::string json = weaseldb::test_data::COMPLEX_JSON; - - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - - // Verify the request was parsed correctly - REQUIRE(request.request_id().has_value()); - REQUIRE(request.request_id().value() == "complex-batch-operation-12345"); - REQUIRE(request.leader_id() == "leader789abcdef"); - REQUIRE(request.read_version() == 999999999); - REQUIRE(request.preconditions().size() == 3); - REQUIRE(request.operations().size() == 5); - - // Check that arena has been used - REQUIRE(request.total_allocated() > 0); - REQUIRE(request.used_bytes() > 0); - - // Capture debug output to string stream with content visualization - std::ostringstream debug_output; - request.arena().debug_dump(debug_output, true, true, - 512); // Include memory map and content - - std::string debug_str = debug_output.str(); - - // Verify debug output contains expected information - REQUIRE(debug_str.find("=== Arena Debug Dump ===") != std::string::npos); - REQUIRE(debug_str.find("Total allocated:") != std::string::npos); - REQUIRE(debug_str.find("Currently used:") != std::string::npos); - REQUIRE(debug_str.find("Block Chain") != std::string::npos); - REQUIRE(debug_str.find("Block addresses") != std::string::npos); - REQUIRE(debug_str.find("Memory Contents:") != std::string::npos); - REQUIRE(debug_str.find("0x0000:") != - std::string::npos); // Hex dump should be present - - // Print debug output to console for manual inspection - MESSAGE("Arena Debug Dump for COMPLEX_JSON:"); - MESSAGE(debug_str); - - // Test that multiple blocks might be allocated for complex data - MESSAGE("Arena statistics:"); - MESSAGE(" Total allocated: " << request.total_allocated() << " bytes"); - MESSAGE(" Used: " << request.used_bytes() << " bytes"); - MESSAGE(" Utilization: " - << (100.0 * request.used_bytes() / request.total_allocated()) - << "%"); - MESSAGE(" Number of blocks: " << request.arena().num_blocks()); +TEST_CASE("Parser Comparison - Edge Cases") { + SUBCASE("Empty arrays") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [], + "operations": [] + })", + "Empty arrays"); } - SUBCASE("Arena debug dump comparison: before and after parsing") { - // Debug dump of empty arena - std::ostringstream empty_output; - request.arena().debug_dump(empty_output); - std::string empty_debug = empty_output.str(); - - REQUIRE(empty_debug.find("Arena is empty") != std::string::npos); - - // Parse complex JSON - std::string json = weaseldb::test_data::COMPLEX_JSON; - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - - // Debug dump after parsing - std::ostringstream used_output; - request.arena().debug_dump(used_output, true); - std::string used_debug = used_output.str(); - - // Compare the outputs - they should be different - REQUIRE(empty_debug != used_debug); - REQUIRE(used_debug.find("Arena is empty") == std::string::npos); - - MESSAGE("Empty arena debug:"); - MESSAGE(empty_debug); - MESSAGE("After parsing COMPLEX_JSON:"); - MESSAGE(used_debug); + SUBCASE("Empty base64 strings") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "operations": [ + { + "type": "write", + "key": "", + "value": "" + } + ] + })", + "Empty base64 strings"); } - SUBCASE("Arena debug dump after reset") { - // Parse complex JSON first - std::string json = weaseldb::test_data::COMPLEX_JSON; - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); - - size_t allocated_before_reset = request.total_allocated(); - size_t used_before_reset = request.used_bytes(); - - // Reset the request (this should reset the arena) - request.reset(); - - // Arena should still have some allocated memory (first block is kept) - size_t allocated_after_reset = request.total_allocated(); - size_t used_after_reset = request.used_bytes(); - - // After reset, used bytes should be 0 or very small - REQUIRE(used_after_reset < used_before_reset); - // Total allocated should be less than or equal to before (extra blocks - // freed) - REQUIRE(allocated_after_reset <= allocated_before_reset); - - std::ostringstream reset_output; - request.arena().debug_dump(reset_output); - std::string reset_debug = reset_output.str(); - - MESSAGE("Arena after reset:"); - MESSAGE(reset_debug); - - // Verify debug output shows minimal usage - if (allocated_after_reset > 0) { - REQUIRE(reset_debug.find("Total allocated:") != std::string::npos); - // Should show very low utilization - double utilization = - allocated_after_reset > 0 - ? (100.0 * used_after_reset / allocated_after_reset) - : 0.0; - REQUIRE(utilization < 10.0); // Less than 10% utilization after reset - } else { - REQUIRE(reset_debug.find("Arena is empty") != std::string::npos); - } + SUBCASE("Zero read_version") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 0 + })", + "Zero read_version"); } - SUBCASE("Arena memory content visualization") { - // Parse COMPLEX_JSON to get diverse content in memory - std::string json = weaseldb::test_data::COMPLEX_JSON; - REQUIRE( - parser.parse(request, const_cast(json.data()), json.size()) == - CommitRequestParser::ParseResult::Success); + SUBCASE("Large read_version") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 18446744073709551615 + })", + "Large read_version"); + } - // Test different content visualization options - std::ostringstream no_content; - request.arena().debug_dump(no_content, false, - false); // No content visualization - std::string no_content_str = no_content.str(); - REQUIRE(no_content_str.find("Memory Contents:") == std::string::npos); + SUBCASE("Precondition with zero version") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "point_read", + "version": 0, + "key": "dGVzdA==" + } + ] + })", + "Precondition with zero version"); + } - std::ostringstream with_content; - request.arena().debug_dump(with_content, true, true, - 128); // Limited content - std::string content_str = with_content.str(); - REQUIRE(content_str.find("Memory Contents:") != std::string::npos); - - std::ostringstream full_content; - request.arena().debug_dump(full_content, true, true, 2048); // Full content - std::string full_content_str = full_content.str(); - - MESSAGE("Arena with limited content visualization (128 bytes):"); - MESSAGE(content_str); - - // Verify that hex dump contains some expected strings from COMPLEX_JSON - // The arena should contain the parsed strings like request_id, leader_id, - // etc. - REQUIRE(content_str.find("|") != - std::string::npos); // ASCII section markers - - // Check that we can see some of the parsed data in ASCII representation - // Note: The exact strings might be base64 decoded or processed - bool found_readable_content = - content_str.find("complex") != std::string::npos || - content_str.find("leader") != std::string::npos || - content_str.find("operation") != std::string::npos || - content_str.find("precondition") != std::string::npos; - - if (found_readable_content) { - MESSAGE("Found readable content in memory dump - strings are visible!"); - } else { - MESSAGE("Strings may be encoded or fragmented in memory - hex dump shows " - "raw allocation patterns"); - } - - // At minimum, we should see some hex content - REQUIRE(content_str.find("0x0000:") != std::string::npos); - REQUIRE(content_str.find("0x00") != - std::string::npos); // Should have hex addresses + SUBCASE("Range operations with empty keys") { + test_parser_comparison(R"({ + "leader_id": "leader456", + "read_version": 12345, + "preconditions": [ + { + "type": "range_read", + "begin": "", + "end": "" + } + ], + "operations": [ + { + "type": "range_delete", + "begin": "", + "end": "" + } + ] + })", + "Range operations with empty keys"); } } + +TEST_CASE("Parser Comparison - Test Data") { + SUBCASE("SIMPLE_JSON") { + test_parser_comparison(weaseldb::test_data::SIMPLE_JSON, + "SIMPLE_JSON test data"); + } + + SUBCASE("MEDIUM_JSON") { + test_parser_comparison(weaseldb::test_data::MEDIUM_JSON, + "MEDIUM_JSON test data"); + } + + SUBCASE("COMPLEX_JSON") { + test_parser_comparison(weaseldb::test_data::COMPLEX_JSON, + "COMPLEX_JSON test data"); + } + + SUBCASE("Generated large JSON") { + // Test with a reasonably sized generated JSON to avoid extremely long test + // times + std::string large_json = weaseldb::test_data::generate_large_json(100); + test_parser_comparison(large_json, "Generated large JSON (100 operations)"); + } +} + +TEST_CASE("Parser Comparison - Stress Tests") { + SUBCASE("Multiple operations of each type") { + test_parser_comparison(R"({ + "request_id": "stress-test-operations", + "leader_id": "stress-leader", + "read_version": 1000000, + "operations": [ + { + "type": "write", + "key": "a2V5MQ==", + "value": "dmFsdWUx" + }, + { + "type": "write", + "key": "a2V5Mg==", + "value": "dmFsdWUy" + }, + { + "type": "delete", + "key": "a2V5Mw==" + }, + { + "type": "delete", + "key": "a2V5NA==" + }, + { + "type": "range_delete", + "begin": "cmFuZ2UxX3N0YXJ0", + "end": "cmFuZ2UxX2VuZA==" + }, + { + "type": "range_delete", + "begin": "cmFuZ2UyX3N0YXJ0", + "end": "cmFuZ2UyX2VuZA==" + } + ] + })", + "Multiple operations of each type"); + } + + SUBCASE("Multiple preconditions of each type") { + test_parser_comparison(R"({ + "leader_id": "stress-leader", + "read_version": 2000000, + "preconditions": [ + { + "type": "point_read", + "version": 1999999, + "key": "cG9pbnQx" + }, + { + "type": "point_read", + "version": 1999998, + "key": "cG9pbnQy" + }, + { + "type": "range_read", + "version": 1999997, + "begin": "cmFuZ2UxX3N0YXJ0", + "end": "cmFuZ2UxX2VuZA==" + }, + { + "type": "range_read", + "begin": "cmFuZ2UyX3N0YXJ0", + "end": "cmFuZ2UyX2VuZA==" + } + ] + })", + "Multiple preconditions of each type"); + } + + SUBCASE("Deep nesting with many operations") { + // Use the generate_large_json function which now properly encodes base64 + std::string stress_json = weaseldb::test_data::generate_large_json(50); + test_parser_comparison(stress_json, "Deep nesting with many operations"); + } +} \ No newline at end of file