Merge branch 'json-view/22-view-dump-fast' into json-view/15-view-bench

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-10-10 11:53:56 +02:00
10 changed files with 1076 additions and 964 deletions

1
.github/labeler.yml vendored
View File

@@ -51,6 +51,7 @@ labels:
- "include/nlohmann/detail/view/.*"
- "single_include/nlohmann/json_view\\.hpp"
- "tests/src/unit-json_view.*"
- "tests/src/json_view_test_helpers\\.hpp"
- "tests/src/fuzzer-(parse_json_view|json_view_image)\\.cpp"
- "tests/benchmarks/json_view/.*"
- "tools/amalgamate/config_json_view\\.json"

View File

@@ -30,7 +30,7 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#if defined(__GNUC__) || defined(__clang__)
return __builtin_clzll(x);
#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))
unsigned long index = 0;
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
_BitScanReverse64(&index, x);
return 63 - static_cast<int>(index);
#else
@@ -53,7 +53,7 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))
unsigned long index = 0;
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
_BitScanForward64(&index, x);
return static_cast<int>(index);
#else

View File

@@ -8884,7 +8884,7 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#if defined(__GNUC__) || defined(__clang__)
return __builtin_clzll(x);
#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))
unsigned long index = 0;
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
_BitScanReverse64(&index, x);
return 63 - static_cast<int>(index);
#else
@@ -8907,7 +8907,7 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))
unsigned long index = 0;
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
_BitScanForward64(&index, x);
return static_cast<int>(index);
#else

View File

@@ -0,0 +1,106 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#pragma once
// helpers shared by the unit-json_view*.cpp files, which are split to keep every
// object file small enough for the MinGW linker (it fails to link objects with
// more than 32767 sections)
#include <nlohmann/json_view.hpp>
#include <algorithm> // any_of
#include <functional> // function
#include <random> // mt19937
#include <string> // string
namespace json_view_test
{
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(5); n > 0; --n)
{
o += pieces[r(10)];
}
o += '"';
}
void value(std::string& o, int depth)
{
static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null"
};
const int k = depth > 5 ? 2 + r(4) : r(6);
if (k < 2)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
if (object)
{
str(o);
o += r(2) == 0 ? ":" : " : ";
}
value(o, depth + 1);
o += i > 1 ? ", " : "";
}
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else
{
o += scalars[r(13)];
}
}
};
// whether an object of the view repeats a key
inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
{
if (v.is_object() && v.size() != v.materialize().size())
{
return true;
}
return std::any_of(v.begin(), v.end(), [](const nlohmann::ordered_json_view e)
{
return e.is_structured() && has_duplicate_keys(e);
});
}
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
inline std::string exception_of_call(const std::function<void()>& f)
{
try
{
f();
}
catch (const nlohmann::json::exception& e)
{
return e.what();
}
return "";
}
#endif
} // namespace json_view_test

View File

@@ -16,6 +16,10 @@ using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
using json_view_test::has_duplicate_keys;
#include <algorithm>
#include <array>
#include <cmath>
@@ -121,60 +125,6 @@ std::string view_exception(const std::string& text, bool comments = false, bool
return "";
}
#endif
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(5); n > 0; --n)
{
o += pieces[r(10)];
}
o += '"';
}
void value(std::string& o, int depth)
{
static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null"
};
const int k = depth > 5 ? 2 + r(4) : r(6);
if (k < 2)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
if (object)
{
str(o);
o += r(2) == 0 ? ":" : " : ";
}
value(o, depth + 1);
o += i > 1 ? ", " : "";
}
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else
{
o += scalars[r(13)];
}
}
};
} // namespace
TEST_CASE("json_view")
@@ -1020,18 +970,6 @@ std::uint32_t bits(float x)
return r;
}
bool has_duplicate_keys(const ordered_json_view& v)
{
if (v.is_object() && v.size() != v.materialize().size())
{
return true;
}
return std::any_of(v.begin(), v.end(), [](const ordered_json_view e)
{
return e.is_structured() && has_duplicate_keys(e);
});
}
// compares the conversions of a view with those of ordered_json
void check_values(const ordered_json_view& v, const ordered_json& j, const std::string& text)
{
@@ -1376,555 +1314,3 @@ TEST_CASE("json_view JSON pointers")
}
#endif
}
TEST_CASE("json_view dump")
{
SECTION("the output of ordered_json::dump()")
{
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
const ordered_json_document d = ordered_json_document::parse(text);
if (has_duplicate_keys(d.root()))
{
continue;
}
CAPTURE(text)
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
-1, 0, 2
})
{
for (const bool ensure_ascii :
{
false, true
})
{
CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii));
}
}
// also of each element
for (const ordered_json_view e : d.root())
{
CHECK(e.dump() == e.materialize().dump());
}
}
}
SECTION("strings")
{
const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€￿😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
const std::string key_text = R"({"é\n": {"\"": [], "": {}}})";
const ordered_json_document keys = ordered_json_document::parse(key_text);
const ordered_json key_json = ordered_json::parse(key_text);
CHECK(keys.root().dump(2, ' ', true) == key_json.dump(2, ' ', true));
}
SECTION("numbers")
{
const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]";
const json_document d = json_document::parse(text);
CHECK(d.root().dump() == json::parse(text).dump());
CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]");
CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// also indented, and with ensure_ascii
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// float tokens of up to 17 significant digits in every spelling: those
// of at most 15 digits are written from their digits, the others
// through the conversion; both as dump() writes them
{
std::mt19937_64 tokens(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
// a number below n; the remainder is a std::uint64_t, which is
// std::size_t on some platforms and wider on others
const auto draw = [&tokens](std::size_t n)
{
const std::uint64_t r = tokens() % n;
return static_cast<std::size_t>(r);
};
std::string many_tokens = "[";
for (int i = 0; i < 20000; ++i)
{
const std::size_t length = 1 + draw(17);
std::string digits(1, static_cast<char>('1' + draw(9)));
for (std::size_t k = 1; k < length; ++k)
{
digits += static_cast<char>('0' + draw(10));
}
digits += std::string(draw(4), '0'); // trailing zeros
std::string token = draw(3) == 0 ? "-" : "";
const std::size_t point = draw(digits.size() + 1);
if (point == 0)
{
token += "0." + std::string(draw(5), '0') + digits;
}
else
{
token += digits.substr(0, point) + (point < digits.size() ? "." + digits.substr(point) : "");
}
// an exponent that keeps the value between about 1e-320 and 1e300
const int exponent = static_cast<int>(draw(600)) - 300 - static_cast<int>(point);
if (draw(4) != 0)
{
token += (draw(2) == 0 ? "e" : "E") + std::string(exponent >= 0 && draw(2) == 0 ? "+" : "") + std::to_string(exponent);
}
else if (point == digits.size())
{
token += ".0"; // (a float, not an integer)
}
many_tokens += (i != 0 ? "," : "") + token;
}
many_tokens += ']';
const json_document many_doc = json_document::parse(many_tokens);
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
}
// random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "[";
for (int i = 0; i < 5000; ++i)
{
const std::uint64_t bits = rng();
double x = 0;
std::memcpy(&x, &bits, sizeof(x));
if (std::isfinite(x))
{
many += (many.size() > 1 ? "," : "") + json(x).dump();
}
}
many += ']';
const json_document many_document = json_document::parse(many);
CHECK(many_document.root().dump() == json::parse(many).dump());
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
}
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == 1);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t slots = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
for (std::uint64_t x = counter, i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (slots - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (slots * sizeof(std::uint32_t)));
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}

View File

@@ -0,0 +1,590 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
using json_view_test::has_duplicate_keys;
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view.cpp, whose object file got too large
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump")
{
SECTION("the output of ordered_json::dump()")
{
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
const ordered_json_document d = ordered_json_document::parse(text);
if (has_duplicate_keys(d.root()))
{
continue;
}
CAPTURE(text)
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
-1, 0, 2
})
{
for (const bool ensure_ascii :
{
false, true
})
{
CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii));
}
}
// also of each element
for (const ordered_json_view e : d.root())
{
CHECK(e.dump() == e.materialize().dump());
}
}
}
SECTION("strings")
{
const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€￿😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
const std::string key_text = R"({"é\n": {"\"": [], "": {}}})";
const ordered_json_document keys = ordered_json_document::parse(key_text);
const ordered_json key_json = ordered_json::parse(key_text);
CHECK(keys.root().dump(2, ' ', true) == key_json.dump(2, ' ', true));
}
SECTION("numbers")
{
const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]";
const json_document d = json_document::parse(text);
CHECK(d.root().dump() == json::parse(text).dump());
CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]");
CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// also indented, and with ensure_ascii
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// float tokens of up to 17 significant digits in every spelling: those
// of at most 15 digits are written from their digits, the others
// through the conversion; both as dump() writes them
{
std::mt19937_64 tokens(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
// a number below n; the remainder is a std::uint64_t, which is
// std::size_t on some platforms and wider on others
const auto draw = [&tokens](std::size_t n)
{
const std::uint64_t r = tokens() % n;
return static_cast<std::size_t>(r);
};
std::string many_tokens = "[";
for (int i = 0; i < 20000; ++i)
{
const std::size_t length = 1 + draw(17);
std::string digits(1, static_cast<char>('1' + draw(9)));
for (std::size_t k = 1; k < length; ++k)
{
digits += static_cast<char>('0' + draw(10));
}
digits += std::string(draw(4), '0'); // trailing zeros
std::string token = draw(3) == 0 ? "-" : "";
const std::size_t point = draw(digits.size() + 1);
if (point == 0)
{
token += "0." + std::string(draw(5), '0') + digits;
}
else
{
token += digits.substr(0, point) + (point < digits.size() ? "." + digits.substr(point) : "");
}
// an exponent that keeps the value between about 1e-320 and 1e300
const int exponent = static_cast<int>(draw(600)) - 300 - static_cast<int>(point);
if (draw(4) != 0)
{
token += (draw(2) == 0 ? "e" : "E") + std::string(exponent >= 0 && draw(2) == 0 ? "+" : "") + std::to_string(exponent);
}
else if (point == digits.size())
{
token += ".0"; // (a float, not an integer)
}
many_tokens += (i != 0 ? "," : "") + token;
}
many_tokens += ']';
const json_document many_doc = json_document::parse(many_tokens);
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
}
// random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "[";
for (int i = 0; i < 5000; ++i)
{
const std::uint64_t bits = rng();
double x = 0;
std::memcpy(&x, &bits, sizeof(x));
if (std::isfinite(x))
{
many += (many.size() > 1 ? "," : "") + json(x).dump();
}
}
many += ']';
const json_document many_document = json_document::parse(many);
CHECK(many_document.root().dump() == json::parse(many).dump());
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
}
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == 1);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t slots = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (slots - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (slots * sizeof(std::uint32_t)));
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}

View File

@@ -19,11 +19,15 @@ using nlohmann::ordered_json_editable_document;
using nlohmann::ordered_json_editable_view;
using ptr_t = ordered_json::json_pointer;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cmath>
#include <cstring>
#include <cstdint>
#include <functional>
#include <iterator>
#include <limits>
#include <map>
@@ -501,25 +505,6 @@ TEST_CASE("json_view edits: differential")
}
}
namespace
{
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
std::string exception_of_call(const std::function<void()>& f)
{
try
{
f();
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
#endif
} // namespace
TEST_CASE("json_view edits: errors")
{
SECTION("an empty document")
@@ -575,327 +560,7 @@ TEST_CASE("json_view edits: errors")
static_cast<void>(other);
}
TEST_CASE("json_view edits: views and values")
{
SECTION("a value that is no longer part of the document")
{
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
const json_editable_view inner = d.root()[0][0];
d.set(d.root()[0], json::array({7}));
d.set(inner, 5);
CHECK(d.root().dump() == "[[7]]");
CHECK(inner.get<int>() == 5);
}
SECTION("views keep referring to their value")
{
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
const json_editable_view a = d.root()["a"];
const json_editable_view twenty = a[1];
const json_editable_view c = d.root()["b"]["c"];
d.insert(a, 0, 5);
d.push_back(a, 40);
CHECK(twenty.get<int>() == 20);
CHECK(a[2].get<int>() == 20);
d.erase(a, 2);
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
d.set(c, 7);
CHECK(c.get<int>() == 7); // a held view sees an assignment
d.set(d.root()["b"], json::array({1, 2}));
CHECK(d.root()["b"].dump() == "[1,2]");
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
}
SECTION("strings stay valid while more edits come")
{
json_editable_document d = json_editable_document::parse("[]");
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
for (int i = 0; i < 1000; ++i)
{
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
}
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
CHECK(d.root().size() == 1001);
}
SECTION("numbers")
{
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
d.set(d.root(), 2, 0.1);
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(std::isnan(d.root()[3].get<double>()));
CHECK(std::isinf(d.root()[4].get<double>()));
CHECK(d.root()[2].number_token() == "0.1");
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
CHECK(d.root()[6].number_token() == "-9223372036854775808");
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
}
SECTION("numbers of other float types")
{
// doubles have their own path to the output; other float types are
// written as basic_json writes them, non-finite values as null
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using document_float = nlohmann::basic_json_document<json_float, true>;
document_float d = document_float::parse("[1.5]");
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
CHECK(d.root().dump() == "[1.5,null,null]");
CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2));
}
SECTION("nulls become containers, and the root can be replaced")
{
json_editable_document d = json_editable_document::parse("[null, null]");
d.set(d.root()[0], "k", 1);
d.push_back(d.root()[1], true);
CHECK(d.root().dump() == R"([{"k":1},[true]])");
d.set(d.root(), "scalar");
CHECK(d.root().dump() == R"("scalar")");
d.set(d.root(), json{{"x", {1, 2}}});
d.set(json::json_pointer("/x/-"), 3);
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
d.set(json::json_pointer("/y"), false);
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
CHECK(d.erase(json::json_pointer("/y")) == 1);
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
}
SECTION("duplicate keys")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
CHECK(d.root().dump() == R"({"a":4,"b":2})");
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK(d.erase(d.root(), "a") == 2);
CHECK(d.root().dump() == R"({"b":2})");
}
SECTION("values from other documents")
{
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
json_editable_document edited = json_editable_document::parse("[0]");
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "copy", source.root()["list"]);
d.set(d.root(), "text", source.root()["text"]);
d.set(d.root(), "edited", edited.root()[0]);
d.set(d.root(), "self", d.root()["copy"]);
CHECK(d.root().dump() == R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})");
CHECK(d.root()["copy"] == source.root()["list"]);
CHECK(source.root()["list"] == d.root()["self"]);
CHECK(d.root() != source.root());
}
SECTION("large objects")
{
std::string text = "{";
for (int i = 0; i < 200; ++i)
{
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
json_editable_document d = json_editable_document::parse(text);
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
CHECK(d.root()["k7"].get_string() == "seven");
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
CHECK(d.root()["new"].get<int>() == 1);
CHECK(d.root()["k199"].get<int>() == 199);
d.erase(d.root(), "k0");
CHECK(!d.root().contains("k0"));
CHECK(d.root().size() == 200);
}
SECTION("reuse and memory")
{
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
const std::size_t before = d.memory_usage();
for (int i = 0; i < 100; ++i)
{
d.push_back(d.root(), "some text");
}
CHECK(d.memory_usage() > before);
const json_editable_view first = d.root()[0];
d.shrink_to_fit(); // (with edits, the index stays in place)
CHECK(first.get<int>() == 1);
d.read(std::string("[true]"));
CHECK(d.root().dump() == "[true]");
d.push_back(d.root(), false);
CHECK(d.root().dump() == "[true,false]");
}
}
TEST_CASE("json_view edits: deeply nested values")
{
// copying a value into a document must not recurse per nesting level
const std::size_t depth = 100000;
const std::string brackets = std::string(depth, '[') + std::string(depth, ']');
std::string braces;
for (std::size_t i = 0; i < depth; ++i)
{
braces += "{\"a\":";
}
braces += '1';
braces += std::string(depth, '}');
SECTION("a view of a read-only document")
{
const json_document source = json_document::parse(brackets);
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a view of an editable document")
{
const json_editable_document source = json_editable_document::parse(braces);
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", source.root());
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
SECTION("a view of an edited document (values behind links)")
{
const json_document source = json_document::parse(brackets);
json_editable_document edited = json_editable_document::parse("[[]]");
edited.push_back(edited.root()[0], source.root());
edited.push_back(edited.root(), source.root());
json_editable_document d = json_editable_document::parse("null");
d.set(d.root(), edited.root());
CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]");
}
SECTION("a basic_json value")
{
json deep = json::array();
json* inner = &deep;
for (std::size_t i = 1; i < depth; ++i)
{
inner->push_back(json::array());
inner = &inner->back();
}
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), deep);
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a basic_json value with objects")
{
json deep = 1;
for (std::size_t i = 0; i < depth; ++i)
{
json outer = json::object();
outer["a"] = std::move(deep);
deep = std::move(outer);
}
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", deep);
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: pointers below a null value")
{
// a null value on the way becomes what basic_json makes of it: an array
// for "-" and for digits, an object otherwise
struct test_case
{
const char* document;
const char* pointer;
};
const std::array<test_case, 16> cases =
{
{
{R"({"a":null})", "/a/0"},
{R"({"a":null})", "/a/-"},
{R"({"a":null})", "/a/3"},
{R"({"a":null})", "/a/x"},
{R"({"a":null})", "/a/+1"},
{R"({"a":null})", "/a/01"},
{R"({"a":null})", "/a/"},
{R"({"a":{"b":null}})", "/a/b/1"},
{R"({"a":{"b":null}})", "/a/b/-"},
{R"({"a":[null]})", "/a/0/0"},
{R"({"a":[null,null]})", "/a/1/k"},
{R"([null])", "/0"},
{"null", "/0"},
{"null", "/-"},
{"null", "/k"},
{"null", ""},
}
};
for (const test_case& c : cases)
{
CAPTURE(c.document)
CAPTURE(c.pointer)
json expected = json::parse(c.document);
const std::string error = exception_of_call([&]
{
expected[json::json_pointer(c.pointer)] = 1;
});
json_editable_document d = json_editable_document::parse(c.document);
if (error.empty())
{
d.set(json::json_pointer(c.pointer), 1);
CHECK(d.root().dump() == expected.dump());
CHECK(d.root().materialize() == expected);
}
else
{
// the same error, and the document is not changed
CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error);
CHECK(d.root().dump() == json::parse(c.document).dump());
}
}
}
TEST_CASE("json_view edits: strings of other documents are checked")
{
// A document borrows the text it was parsed from, and sees later changes
// of the text: a way to get ill-formed UTF-8 into a view. Copying it into
// an editable document is an error, as for any other string.
std::string text = R"({"key":"abc","list":["abc"]})";
const json_document source = json_document::parse(text);
const auto message_of = [](const std::string & bad)
{
return exception_of_call([&]
{
const std::string dumped = json(bad).dump();
static_cast<void>(dumped);
});
};
json_editable_document d = json_editable_document::parse("[1]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
text[text.find("abc") + 1] = '\xC3'; // "a\xC3c"
text[text.rfind("abc") + 1] = '\xC3';
const std::string bad_value = message_of(std::string("a\xC3" "c"));
CHECK(!bad_value.empty());
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value);
text[text.find("key") + 1] = '\xC3'; // a key is checked as well
const std::string bad_key = message_of(std::string("k\xC3" "y"));
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key);
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key);
// nothing of the failed edits is visible
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
}
TEST_CASE("json_view edits: the size of a text arena")
{
using nlohmann::detail::view::text_capacity;

View File

@@ -0,0 +1,357 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <utility>
#include <vector>
// These tests were split off unit-json_view_edit.cpp, whose object file got too
// large for the MinGW linker (see json_view_test_helpers.hpp).
TEST_CASE("json_view edits: views and values")
{
SECTION("a value that is no longer part of the document")
{
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
const json_editable_view inner = d.root()[0][0];
d.set(d.root()[0], json::array({7}));
d.set(inner, 5);
CHECK(d.root().dump() == "[[7]]");
CHECK(inner.get<int>() == 5);
}
SECTION("views keep referring to their value")
{
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
const json_editable_view a = d.root()["a"];
const json_editable_view twenty = a[1];
const json_editable_view c = d.root()["b"]["c"];
d.insert(a, 0, 5);
d.push_back(a, 40);
CHECK(twenty.get<int>() == 20);
CHECK(a[2].get<int>() == 20);
d.erase(a, 2);
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
d.set(c, 7);
CHECK(c.get<int>() == 7); // a held view sees an assignment
d.set(d.root()["b"], json::array({1, 2}));
CHECK(d.root()["b"].dump() == "[1,2]");
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
}
SECTION("strings stay valid while more edits come")
{
json_editable_document d = json_editable_document::parse("[]");
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
for (int i = 0; i < 1000; ++i)
{
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
}
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
CHECK(d.root().size() == 1001);
}
SECTION("numbers")
{
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
d.set(d.root(), 2, 0.1);
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(std::isnan(d.root()[3].get<double>()));
CHECK(std::isinf(d.root()[4].get<double>()));
CHECK(d.root()[2].number_token() == "0.1");
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
CHECK(d.root()[6].number_token() == "-9223372036854775808");
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
}
SECTION("numbers of other float types")
{
// doubles have their own path to the output; other float types are
// written as basic_json writes them, non-finite values as null
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using document_float = nlohmann::basic_json_document<json_float, true>;
document_float d = document_float::parse("[1.5]");
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
CHECK(d.root().dump() == "[1.5,null,null]");
CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2));
}
SECTION("nulls become containers, and the root can be replaced")
{
json_editable_document d = json_editable_document::parse("[null, null]");
d.set(d.root()[0], "k", 1);
d.push_back(d.root()[1], true);
CHECK(d.root().dump() == R"([{"k":1},[true]])");
d.set(d.root(), "scalar");
CHECK(d.root().dump() == R"("scalar")");
d.set(d.root(), json{{"x", {1, 2}}});
d.set(json::json_pointer("/x/-"), 3);
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
d.set(json::json_pointer("/y"), false);
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
CHECK(d.erase(json::json_pointer("/y")) == 1);
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
}
SECTION("duplicate keys")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
CHECK(d.root().dump() == R"({"a":4,"b":2})");
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK(d.erase(d.root(), "a") == 2);
CHECK(d.root().dump() == R"({"b":2})");
}
SECTION("values from other documents")
{
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
json_editable_document edited = json_editable_document::parse("[0]");
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "copy", source.root()["list"]);
d.set(d.root(), "text", source.root()["text"]);
d.set(d.root(), "edited", edited.root()[0]);
d.set(d.root(), "self", d.root()["copy"]);
// (a raw string with a backslash must not be a macro argument: MSVC C2017)
const std::string expected = R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})";
CHECK(d.root().dump() == expected);
CHECK(d.root()["copy"] == source.root()["list"]);
CHECK(source.root()["list"] == d.root()["self"]);
CHECK(d.root() != source.root());
}
SECTION("large objects")
{
std::string text = "{";
for (int i = 0; i < 200; ++i)
{
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
json_editable_document d = json_editable_document::parse(text);
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
CHECK(d.root()["k7"].get_string() == "seven");
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
CHECK(d.root()["new"].get<int>() == 1);
CHECK(d.root()["k199"].get<int>() == 199);
d.erase(d.root(), "k0");
CHECK(!d.root().contains("k0"));
CHECK(d.root().size() == 200);
}
SECTION("reuse and memory")
{
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
const std::size_t before = d.memory_usage();
for (int i = 0; i < 100; ++i)
{
d.push_back(d.root(), "some text");
}
CHECK(d.memory_usage() > before);
const json_editable_view first = d.root()[0];
d.shrink_to_fit(); // (with edits, the index stays in place)
CHECK(first.get<int>() == 1);
d.read(std::string("[true]"));
CHECK(d.root().dump() == "[true]");
d.push_back(d.root(), false);
CHECK(d.root().dump() == "[true,false]");
}
}
TEST_CASE("json_view edits: deeply nested values")
{
// copying a value into a document must not recurse per nesting level
const std::size_t depth = 100000;
const std::string brackets = std::string(depth, '[') + std::string(depth, ']');
std::string braces;
for (std::size_t i = 0; i < depth; ++i)
{
braces += "{\"a\":";
}
braces += '1';
braces += std::string(depth, '}');
SECTION("a view of a read-only document")
{
const json_document source = json_document::parse(brackets);
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a view of an editable document")
{
const json_editable_document source = json_editable_document::parse(braces);
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", source.root());
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
SECTION("a view of an edited document (values behind links)")
{
const json_document source = json_document::parse(brackets);
json_editable_document edited = json_editable_document::parse("[[]]");
edited.push_back(edited.root()[0], source.root());
edited.push_back(edited.root(), source.root());
json_editable_document d = json_editable_document::parse("null");
d.set(d.root(), edited.root());
CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]");
}
SECTION("a basic_json value")
{
json deep = json::array();
json* inner = &deep;
for (std::size_t i = 1; i < depth; ++i)
{
inner->push_back(json::array());
inner = &inner->back();
}
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), deep);
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a basic_json value with objects")
{
json deep = 1;
for (std::size_t i = 0; i < depth; ++i)
{
json outer = json::object();
outer["a"] = std::move(deep);
deep = std::move(outer);
}
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", deep);
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: pointers below a null value")
{
// a null value on the way becomes what basic_json makes of it: an array
// for "-" and for digits, an object otherwise
struct test_case
{
const char* document;
const char* pointer;
};
const std::array<test_case, 16> cases =
{
{
{R"({"a":null})", "/a/0"},
{R"({"a":null})", "/a/-"},
{R"({"a":null})", "/a/3"},
{R"({"a":null})", "/a/x"},
{R"({"a":null})", "/a/+1"},
{R"({"a":null})", "/a/01"},
{R"({"a":null})", "/a/"},
{R"({"a":{"b":null}})", "/a/b/1"},
{R"({"a":{"b":null}})", "/a/b/-"},
{R"({"a":[null]})", "/a/0/0"},
{R"({"a":[null,null]})", "/a/1/k"},
{R"([null])", "/0"},
{"null", "/0"},
{"null", "/-"},
{"null", "/k"},
{"null", ""},
}
};
for (const test_case& c : cases)
{
CAPTURE(c.document)
CAPTURE(c.pointer)
json expected = json::parse(c.document);
const std::string error = exception_of_call([&]
{
expected[json::json_pointer(c.pointer)] = 1;
});
json_editable_document d = json_editable_document::parse(c.document);
if (error.empty())
{
d.set(json::json_pointer(c.pointer), 1);
CHECK(d.root().dump() == expected.dump());
CHECK(d.root().materialize() == expected);
}
else
{
// the same error, and the document is not changed
CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error);
CHECK(d.root().dump() == json::parse(c.document).dump());
}
}
}
TEST_CASE("json_view edits: strings of other documents are checked")
{
// A document borrows the text it was parsed from, and sees later changes
// of the text: a way to get ill-formed UTF-8 into a view. Copying it into
// an editable document is an error, as for any other string.
std::string text = R"({"key":"abc","list":["abc"]})";
const json_document source = json_document::parse(text);
const auto message_of = [](const std::string & bad)
{
return exception_of_call([&]
{
const std::string dumped = json(bad).dump();
static_cast<void>(dumped);
});
};
json_editable_document d = json_editable_document::parse("[1]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
text[text.find("abc") + 1] = '\xC3'; // "a\xC3c"
text[text.rfind("abc") + 1] = '\xC3';
const std::string bad_value = message_of(std::string("a\xC3" "c"));
CHECK(!bad_value.empty());
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value);
text[text.find("key") + 1] = '\xC3'; // a key is checked as well
const std::string bad_key = message_of(std::string("k\xC3" "y"));
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key);
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key);
// nothing of the failed edits is visible
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
}
#endif

View File

@@ -301,7 +301,8 @@ TEST_CASE("json_view images: round trips")
for (std::uint64_t counter = 0; colliding.size() < colliding_count || spread.size() < spread_count; ++counter)
{
std::string key(8, 'a');
for (std::uint64_t x = counter, i = 0; i < 8; ++i, x /= 26)
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
@@ -1087,7 +1088,9 @@ TEST_CASE("json_view images: check")
set_node(same, 2, node_at(img2, 1));
set_node(same, 4, node_at(img2, 3));
CHECK(load_result(same, image_check::full).empty());
CHECK(loaded_dump(same) == R"(["a\"b","a\"b",1.25,1.25,1250.0])");
// (a raw string with a backslash must not be a macro argument: MSVC C2017)
const std::string expected_same = R"(["a\"b","a\"b",1.25,1.25,1250.0])";
CHECK(loaded_dump(same) == expected_same);
std::vector<std::uint8_t> layout = same;
node f4 = node_at(layout, 4);
f4.extra = 0x0100u; // the layout of "1.", and not that of "1.25"

View File

@@ -837,6 +837,9 @@ TEST_CASE("choice of the conversion")
SECTION("long double with the format of a double: Zmij")
{
// (on platforms where long double is wider, Grisu2 does not apply either: the snprintf fallback does)
// (constant on a given platform)
DOCTEST_MSVC_SUPPRESS_WARNING_PUSH
DOCTEST_MSVC_SUPPRESS_WARNING(4127)
if (std::numeric_limits<long double>::digits == 53 && std::numeric_limits<long double>::is_iec559)
{
using long_double_json = nlohmann::json::with_float_t<long double>;
@@ -850,6 +853,7 @@ TEST_CASE("choice of the conversion")
}
CHECK(long_double_json(5.3165205877497296e+16L).dump() == "5.31652058774973e+16");
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
}
}