From 3caf0e7cdba9fdbf59831a788b68fa4370983f32 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:07:53 +0200 Subject: [PATCH 1/8] Mark the unsigned long of the MSVC bit scans for cpplint Signed-off-by: Niels Lohmann --- include/nlohmann/detail/bit_ops.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/bit_ops.hpp b/include/nlohmann/detail/bit_ops.hpp index b3bf08557..3daefbe9d 100644 --- a/include/nlohmann/detail/bit_ops.hpp +++ b/include/nlohmann/detail/bit_ops.hpp @@ -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(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(index); #else From 571b6553573d3245d45a4eee087c787eb9202887 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:08:03 +0200 Subject: [PATCH 2/8] Silence MSVC C4127 for a platform-constant check in the to_chars test Signed-off-by: Niels Lohmann --- tests/src/unit-to_chars.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/unit-to_chars.cpp b/tests/src/unit-to_chars.cpp index d8eb44434..c9b72fc97 100644 --- a/tests/src/unit-to_chars.cpp +++ b/tests/src/unit-to_chars.cpp @@ -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::digits == 53 && std::numeric_limits::is_iec559) { using long_double_json = nlohmann::json::with_float_t; @@ -850,5 +853,6 @@ TEST_CASE("choice of the conversion") } CHECK(long_double_json(5.3165205877497296e+16L).dump() == "5.31652058774973e+16"); } + DOCTEST_MSVC_SUPPRESS_WARNING_POP } } From 1c79a536d1ba9815f8367d813756ec8453a8f7bc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:09:16 +0200 Subject: [PATCH 3/8] Index strings with size_t in the colliding-keys test (MSVC C4244 on Win32) Signed-off-by: Niels Lohmann --- tests/src/unit-json_view.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index e95158346..72adbf2f4 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -1759,7 +1759,8 @@ TEST_CASE("json_view large objects") 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) + std::uint64_t x = counter; + for (std::size_t i = 0; i < 8; ++i, x /= 26) { key[i] = static_cast('a' + (x % 26)); } From e6f9f6b07adfeb6148f9112b6f6d0c737ecd75f6 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:09:24 +0200 Subject: [PATCH 4/8] Fix MSVC C4244 and C2017 in the image test Signed-off-by: Niels Lohmann --- tests/src/unit-json_view_image.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 522a82be0..5b4360ad4 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -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('a' + (x % 26)); } @@ -1062,7 +1063,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 layout = same; node f4 = node_at(layout, 4); f4.extra = 0x0100u; // the layout of "1.", and not that of "1.25" From 7355b090afd1aa3660991927f45a22c55801a352 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:10:48 +0200 Subject: [PATCH 5/8] Keep a raw string with a backslash out of a CHECK (MSVC C2017) Signed-off-by: Niels Lohmann --- tests/src/unit-json_view_edit.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 215afd6aa..2e60b1b11 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -676,7 +676,9 @@ TEST_CASE("json_view edits: views and values") 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}]})"); + // (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()); From fc0d39ca9d73727084b72ae3dd13b71c979a588e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:34:42 +0200 Subject: [PATCH 6/8] Split the dump and comparison tests off unit-json_view.cpp The MinGW linker of the Windows clang jobs cannot link object files with more than 32767 sections ("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'"). unit-json_view.cpp reaches that limit as the stack grows, so its "json_view dump" and "json_view comparison" test cases move into unit-json_view_dump.cpp. The test generator and has_duplicate_keys() that both files use move into json_view_test_helpers.hpp. The new file mentions JSON_HAS_CPP_17, so it is built for C++17 like the file it was split from. Signed-off-by: Niels Lohmann --- .github/labeler.yml | 1 + tests/src/json_view_test_helpers.hpp | 89 ++++++++ tests/src/unit-json_view.cpp | 309 +-------------------------- tests/src/unit-json_view_dump.cpp | 275 ++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 305 deletions(-) create mode 100644 tests/src/json_view_test_helpers.hpp create mode 100644 tests/src/unit-json_view_dump.cpp diff --git a/.github/labeler.yml b/.github/labeler.yml index 7884e69fb..38682796c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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\\.cpp" - "tools/amalgamate/config_json_view\\.json" - "docs/mkdocs/docs/features/json_view\\.md" diff --git a/tests/src/json_view_test_helpers.hpp b/tests/src/json_view_test_helpers.hpp new file mode 100644 index 000000000..4a2481e47 --- /dev/null +++ b/tests/src/json_view_test_helpers.hpp @@ -0,0 +1,89 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// 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 + +#include // any_of +#include // mt19937 +#include // 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(rng() % static_cast(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); + }); +} +} // namespace json_view_test diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index e0a6b6cb8..9e48e9661 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -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 #include #include @@ -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(rng() % static_cast(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,242 +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]"); - - // 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; - const nlohmann::basic_json_document float_document = nlohmann::basic_json_document::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 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()); - } -} diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp new file mode 100644 index 000000000..ac3133e17 --- /dev/null +++ b/tests/src/unit-json_view_dump.cpp @@ -0,0 +1,275 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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]"); + + // 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; + const nlohmann::basic_json_document float_document = nlohmann::basic_json_document::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 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()); + } +} From 5e84bf6814efe8125da8f901eee7dd5c093d2cd5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:43:14 +0200 Subject: [PATCH 7/8] Fix the indentation of a loop in the image test Signed-off-by: Niels Lohmann --- tests/src/unit-json_view_image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 5b4360ad4..f470ae9e9 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -302,7 +302,7 @@ TEST_CASE("json_view images: round trips") { std::string key(8, 'a'); std::uint64_t x = counter; - for (std::size_t i = 0; i < 8; ++i, x /= 26) + for (std::size_t i = 0; i < 8; ++i, x /= 26) { key[i] = static_cast('a' + (x % 26)); } From bd7a232d9fa1a7a67f882ea9eb45d3774a2c1e9a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 11:49:53 +0200 Subject: [PATCH 8/8] Split the value and pointer edit tests off unit-json_view_edit.cpp The MinGW linker of the Windows clang jobs cannot link object files with more than 32767 sections ("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'"). unit-json_view_edit.cpp has 35409 sections since the fast dump instantiates more code for editable documents, so its test cases "views and values", "deeply nested values", "pointers below a null value", and "strings of other documents are checked" move into unit-json_view_edit_values.cpp. The helper exception_of_call() that both files use moves into json_view_test_helpers.hpp. Signed-off-by: Niels Lohmann --- tests/src/json_view_test_helpers.hpp | 17 ++ tests/src/unit-json_view_edit.cpp | 347 +--------------------- tests/src/unit-json_view_edit_values.cpp | 357 +++++++++++++++++++++++ 3 files changed, 379 insertions(+), 342 deletions(-) create mode 100644 tests/src/unit-json_view_edit_values.cpp diff --git a/tests/src/json_view_test_helpers.hpp b/tests/src/json_view_test_helpers.hpp index 4a2481e47..b5af61798 100644 --- a/tests/src/json_view_test_helpers.hpp +++ b/tests/src/json_view_test_helpers.hpp @@ -15,6 +15,7 @@ #include #include // any_of +#include // function #include // mt19937 #include // string @@ -86,4 +87,20 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v) 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& f) +{ + try + { + f(); + } + catch (const nlohmann::json::exception& e) + { + return e.what(); + } + return ""; +} +#endif } // namespace json_view_test diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index e262dc98d..edd793cc8 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -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 #include #include #include -#include #include #include #include @@ -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& 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,329 +560,7 @@ TEST_CASE("json_view edits: errors") static_cast(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() == 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() == 20); - CHECK(a[2].get() == 20); - d.erase(a, 2); - CHECK(twenty.get() == 20); // an erased value keeps its last value - d.set(c, 7); - CHECK(c.get() == 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(-1)); // a new value - CHECK(d.root()["a"][1].source_offset() != static_cast(-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(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::quiet_NaN()); - d.push_back(d.root(), -std::numeric_limits::infinity()); - d.push_back(d.root(), (std::numeric_limits::max)()); - d.push_back(d.root(), (std::numeric_limits::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())); - CHECK(std::isinf(d.root()[4].get())); - CHECK(d.root()[2].number_token() == "0.1"); - CHECK(d.root()[5].get() == 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; - using document_float = nlohmann::basic_json_document; - document_float d = document_float::parse("[1.5]"); - d.push_back(d.root(), std::numeric_limits::quiet_NaN()); - d.push_back(d.root(), -std::numeric_limits::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() == 1); - CHECK(d.root()["k199"].get() == 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() == 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 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(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; diff --git a/tests/src/unit-json_view_edit_values.cpp b/tests/src/unit-json_view_edit_values.cpp new file mode 100644 index 000000000..59aee3a9d --- /dev/null +++ b/tests/src/unit-json_view_edit_values.cpp @@ -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 +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +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 +#include +#include +#include +#include +#include +#include +#include +#include + +// 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() == 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() == 20); + CHECK(a[2].get() == 20); + d.erase(a, 2); + CHECK(twenty.get() == 20); // an erased value keeps its last value + d.set(c, 7); + CHECK(c.get() == 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(-1)); // a new value + CHECK(d.root()["a"][1].source_offset() != static_cast(-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(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::quiet_NaN()); + d.push_back(d.root(), -std::numeric_limits::infinity()); + d.push_back(d.root(), (std::numeric_limits::max)()); + d.push_back(d.root(), (std::numeric_limits::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())); + CHECK(std::isinf(d.root()[4].get())); + CHECK(d.root()[2].number_token() == "0.1"); + CHECK(d.root()[5].get() == 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; + using document_float = nlohmann::basic_json_document; + document_float d = document_float::parse("[1.5]"); + d.push_back(d.root(), std::numeric_limits::quiet_NaN()); + d.push_back(d.root(), -std::numeric_limits::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() == 1); + CHECK(d.root()["k199"].get() == 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() == 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 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(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