test_doc_binary.cpp 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. #include <cassert>
  2. #include <iostream>
  3. #include <optional>
  4. #include <string>
  5. #include <nlohmann/json.hpp>
  6. #include "doc_binary.hpp"
  7. using nlohmann::json;
  8. using smartbotic::db::doc_binary::Doc;
  9. using smartbotic::db::doc_binary::encode;
  10. using smartbotic::db::doc_binary::decode;
  11. using smartbotic::db::doc_binary::get_field;
  12. using smartbotic::db::doc_binary::to_json_text;
  13. namespace {
  14. void check(bool cond, const char* msg) {
  15. if (!cond) {
  16. std::cerr << "FAIL: " << msg << "\n";
  17. std::abort();
  18. }
  19. }
  20. void roundtrip(const json& j, const char* label) {
  21. auto doc = encode(j);
  22. auto back = decode(doc);
  23. check(back == j, label);
  24. }
  25. void test_default_doc_is_empty() {
  26. Doc d;
  27. check(d.empty(), "default-constructed Doc is empty");
  28. check(d.root() == nullptr, "empty Doc has null root");
  29. auto back = decode(d);
  30. check(back.is_null(), "decoding empty Doc returns null");
  31. }
  32. void test_primitives_roundtrip() {
  33. roundtrip(json(nullptr), "null");
  34. roundtrip(json(true), "true");
  35. roundtrip(json(false), "false");
  36. roundtrip(json(0), "int zero");
  37. roundtrip(json(42), "int 42");
  38. roundtrip(json(-7), "negative int");
  39. roundtrip(json(9223372036854775807LL), "int64 max");
  40. roundtrip(json(3.14), "double");
  41. roundtrip(json("hello"), "string");
  42. roundtrip(json(""), "empty string");
  43. }
  44. void test_collections_roundtrip() {
  45. roundtrip(json::array(), "empty array");
  46. roundtrip(json::object(), "empty object");
  47. roundtrip(json::array({1, 2, 3}), "int array");
  48. roundtrip(json::array({1, "two", 3.0, true, nullptr}), "mixed array");
  49. roundtrip(json({{"k", "v"}, {"n", 1}}), "small object");
  50. }
  51. void test_zoe_shape_roundtrip() {
  52. json zoe = {
  53. {"_id", "doc-1"},
  54. {"_created_at", 1731628800123LL},
  55. {"name", "Zoe"},
  56. {"tags", json::array({"hot", "pinned"})},
  57. {"_vector", json::array({0.1, -0.2, 0.3, 0.4})},
  58. {"meta", {{"flag", true}, {"score", 0.93}}},
  59. };
  60. roundtrip(zoe, "Zoe-shape");
  61. }
  62. void test_unicode_roundtrip() {
  63. roundtrip(
  64. json({{"hu", "Árvíztűrő tükörfúrógép"}, {"emoji", "🚀"}}),
  65. "unicode strings");
  66. }
  67. void test_get_field_present() {
  68. json j = {{"name", "Zoe"}, {"count", 42}, {"flag", true}};
  69. auto doc = encode(j);
  70. auto name = get_field(doc, "name");
  71. check(name.has_value(), "name present");
  72. check(name->get<std::string>() == "Zoe", "name value");
  73. auto count = get_field(doc, "count");
  74. check(count.has_value(), "count present");
  75. check(count->get<int>() == 42, "count value");
  76. auto flag = get_field(doc, "flag");
  77. check(flag.has_value(), "flag present");
  78. check(flag->get<bool>() == true, "flag value");
  79. }
  80. void test_get_field_missing() {
  81. json j = {{"name", "Zoe"}};
  82. auto doc = encode(j);
  83. auto absent = get_field(doc, "nonexistent");
  84. check(!absent.has_value(), "missing field returns nullopt");
  85. }
  86. void test_get_field_on_non_object() {
  87. auto doc = encode(json::array({1, 2, 3}));
  88. auto v = get_field(doc, "anything");
  89. check(!v.has_value(), "get_field on array returns nullopt");
  90. }
  91. void test_get_field_on_empty() {
  92. Doc d;
  93. auto v = get_field(d, "anything");
  94. check(!v.has_value(), "get_field on empty Doc returns nullopt");
  95. }
  96. void test_to_json_text_roundtrip() {
  97. json j = {{"a", 1}, {"b", "two"}, {"c", json::array({1, 2})}};
  98. auto doc = encode(j);
  99. std::string text = to_json_text(doc);
  100. json parsed = json::parse(text);
  101. check(parsed == j, "to_json_text round-trips through nlohmann::json::parse");
  102. }
  103. void test_to_json_text_empty() {
  104. Doc d;
  105. check(to_json_text(d) == "null", "to_json_text of empty Doc is \"null\"");
  106. }
  107. void test_doc_supports_move() {
  108. auto d1 = encode(json({{"a", 1}}));
  109. check(!d1.empty(), "encoded doc not empty");
  110. Doc d2 = std::move(d1);
  111. check(d1.empty(), "moved-from Doc is empty");
  112. check(!d2.empty(), "moved-to Doc holds the data");
  113. auto field = get_field(d2, "a");
  114. check(field.has_value() && field->get<int>() == 1, "moved Doc still readable");
  115. }
  116. void test_doc_copy_semantics() {
  117. // Phase B T3 follow-up: Doc gained copy support because Document is
  118. // copied by value across the codebase (vector<Document>, replication
  119. // paths, etc.). Copies must be independent — mutating the source must
  120. // not affect the copy, and vice versa.
  121. auto src = encode(json({{"name", "Zoe"}, {"count", 42}}));
  122. check(!src.empty(), "source doc not empty");
  123. // Copy-construct.
  124. Doc copy = src;
  125. check(!copy.empty(), "copied Doc holds data");
  126. check(!src.empty(), "source Doc still holds data after copy");
  127. // Both should decode to the same JSON tree.
  128. json src_json = decode(src);
  129. json copy_json = decode(copy);
  130. check(src_json == copy_json, "copy and source decode to equal trees");
  131. // Independence check: replace the source with new content via move-
  132. // assignment; the copy must remain unchanged.
  133. auto replacement = encode(json({{"name", "Other"}, {"count", 0}}));
  134. src = std::move(replacement);
  135. json copy_after = decode(copy);
  136. check(copy_after == json({{"name", "Zoe"}, {"count", 42}}),
  137. "copy unaffected by source mutation");
  138. // Copy-assignment with self-assignment safety.
  139. Doc d = encode(json({{"k", "v"}}));
  140. d = d; // NOLINT(clang-diagnostic-self-assign-overloaded)
  141. auto field = get_field(d, "k");
  142. check(field.has_value() && field->get<std::string>() == "v",
  143. "self-copy-assignment preserves contents");
  144. // Copy-assignment from a non-empty to another non-empty.
  145. Doc a = encode(json({{"a", 1}}));
  146. Doc b = encode(json({{"b", 2}}));
  147. a = b;
  148. check(decode(a) == json({{"b", 2}}), "copy-assignment overwrites target");
  149. check(decode(b) == json({{"b", 2}}), "copy-assignment leaves source intact");
  150. // Copy from an empty Doc → target becomes empty.
  151. Doc empty;
  152. Doc target = encode(json({{"x", 1}}));
  153. target = empty;
  154. check(target.empty(), "copy-assignment from empty Doc empties target");
  155. }
  156. } // namespace
  157. int main() {
  158. test_default_doc_is_empty();
  159. test_primitives_roundtrip();
  160. test_collections_roundtrip();
  161. test_zoe_shape_roundtrip();
  162. test_unicode_roundtrip();
  163. test_get_field_present();
  164. test_get_field_missing();
  165. test_get_field_on_non_object();
  166. test_get_field_on_empty();
  167. test_to_json_text_roundtrip();
  168. test_to_json_text_empty();
  169. test_doc_supports_move();
  170. test_doc_copy_semantics();
  171. std::cout << "test_doc_binary: all passed\n";
  172. return 0;
  173. }