mirror of
https://github.com/nlohmann/json.git
synced 2026-08-22 00:53:18 +00:00
The copy constructor descends into 128 levels before it finishes a value without the call stack, so the iterative path is otherwise only reached by the few tests that nest deeper than that. JSON_NO_THREAD_LOCAL switches the descent off, which sends every value down that path. Running the whole test suite that way covers it with every object type, string type, allocator, and base class the suite already exercises. The new ci_test_no_thread_local target does that; the macro had no build coverage at all before. Copying a nested value also has to carry over what the element-wise copy constructor would have copied: the parents that JSON_DIAGNOSTICS relies on, and the positions that JSON_DIAGNOSTIC_POSITIONS reports. Both are now checked on either side of the descent bound, for objects and arrays. Neither was tested before, and dropping either one makes the new tests fail. Also quantify what JSON_NO_THREAD_LOCAL costs a copy instead of calling it "measurably slower". Signed-off-by: Niels Lohmann <mail@nlohmann.me>
136 lines
5.0 KiB
C++
136 lines
5.0 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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"
|
|
|
|
#define JSON_DIAGNOSTICS 1
|
|
#define JSON_DIAGNOSTIC_POSITIONS 1
|
|
#include <nlohmann/json.hpp>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
TEST_CASE("Better diagnostics with positions")
|
|
{
|
|
SECTION("invalid type")
|
|
{
|
|
const std::string json_invalid_string = R"(
|
|
{
|
|
"address": {
|
|
"street": "Fake Street",
|
|
"housenumber": "1"
|
|
}
|
|
}
|
|
)";
|
|
json j = json::parse(json_invalid_string);
|
|
CHECK_THROWS_WITH_AS(j.at("address").at("housenumber").get<int>(),
|
|
"[json.exception.type_error.302] (/address/housenumber) (bytes 108-111) type must be number, but is string", json::type_error);
|
|
}
|
|
|
|
SECTION("invalid type without positions")
|
|
{
|
|
const json j = "foo";
|
|
CHECK_THROWS_WITH_AS(j.get<int>(),
|
|
"[json.exception.type_error.302] type must be number, but is string", json::type_error);
|
|
}
|
|
|
|
SECTION("positions of strings containing escape sequences")
|
|
{
|
|
// escape sequences make the token longer than the string it parses to,
|
|
// so the positions must not be derived from the parsed value's length
|
|
const auto check = [](const std::string & text, const std::string & token)
|
|
{
|
|
CAPTURE(text)
|
|
CAPTURE(token)
|
|
const json j = json::parse(text);
|
|
const json& v = j.at("a");
|
|
CHECK(text.substr(v.start_pos(), v.end_pos() - v.start_pos()) == token);
|
|
};
|
|
|
|
check(R"({"a":"plain"})", R"("plain")");
|
|
check(R"({"a":"tab\there"})", R"("tab\there")");
|
|
check(R"({"a":"\n\n\n\n\n\n"})", R"("\n\n\n\n\n\n")");
|
|
check(R"({"a":"\""})", R"("\"")");
|
|
check(R"({"a":"\\"})", R"("\\")");
|
|
check(R"({"a":"é"})", R"("é")");
|
|
check(R"({"a":"🌞"})", R"("🌞")");
|
|
check("{\"a\":\"\xc3\xa9\"}", "\"\xc3\xa9\""); // multi-byte UTF-8, no escapes
|
|
|
|
// a string at the root, where an escape would otherwise push the
|
|
// reported start position past the opening quote
|
|
const std::string root = R"("a\tb")";
|
|
const json j = json::parse(root);
|
|
CHECK(j.start_pos() == 0);
|
|
CHECK(j.end_pos() == root.size());
|
|
}
|
|
|
|
SECTION("copying keeps the positions of nested values (#5387)")
|
|
{
|
|
// Values nested deeper than the copy constructor's descent bound are
|
|
// copied without the call stack, on a path that has to carry the
|
|
// positions over itself; shallower ones copy their containers, which
|
|
// bring the positions along. Both sides of the bound are checked here.
|
|
const auto check_copy = [](std::size_t depth, bool objects)
|
|
{
|
|
CAPTURE(depth)
|
|
CAPTURE(objects)
|
|
|
|
const std::string open = objects ? R"({"a":)" : "[";
|
|
const std::string close = objects ? "}" : "]";
|
|
|
|
std::string text;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += open;
|
|
}
|
|
text += "12";
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += close;
|
|
}
|
|
|
|
const json original = json::parse(text);
|
|
const json copy(original); // NOLINT(performance-unnecessary-copy-initialization)
|
|
|
|
const json* o = &original;
|
|
const json* c = ©
|
|
for (std::size_t level = 0; level <= depth; ++level)
|
|
{
|
|
CAPTURE(level)
|
|
REQUIRE(c->start_pos() == o->start_pos());
|
|
REQUIRE(c->end_pos() == o->end_pos());
|
|
|
|
if (level < depth)
|
|
{
|
|
o = objects ? &o->at("a") : &o->at(0);
|
|
c = objects ? &c->at("a") : &c->at(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
const bool shapes[] = {false, true};
|
|
for (const bool objects : shapes)
|
|
{
|
|
check_copy(1, objects);
|
|
check_copy(127, objects);
|
|
check_copy(128, objects);
|
|
check_copy(129, objects);
|
|
check_copy(300, objects);
|
|
}
|
|
}
|
|
|
|
SECTION("JSON patch add to primitive parent (#4292)")
|
|
{
|
|
// the JSON Patch "add" target /foo/bar/baz has a string parent
|
|
// (/foo/bar); the position of that parent is reported in the message
|
|
const json doc = json::parse(R"({"foo":{"bar":"a string"}})");
|
|
const json patch = json::parse(R"([{"op":"add","path":"/foo/bar/baz","value":1}])");
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch),
|
|
"[json.exception.out_of_range.411] (/foo/bar) (bytes 14-24) cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range);
|
|
}
|
|
}
|