mirror of
https://github.com/nlohmann/json.git
synced 2026-11-04 11:49:22 +00:00
* CI: measure line coverage without lcov's unexecuted-block adjustment Drop geninfo_unexecuted_blocks=1 from the lcov capture of ci_test_coverage: it reset every line of an inlined or templated function to 0 as soon as one copy had an unexecuted block, which understated the line coverage by about three points. Add -fvar-tracking-assignments to both coverage builds. At -O0, GCC inlines always_inline functions before it places the coverage counters, so the call sites of such helpers (put_literal() and others) got no line record and showed as 0 in an -O3 translation unit that never reaches them. With the flag, the call sites carry counts in every translation unit. The generated code is identical. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove unreachable code found by the coverage analysis Remove code that no input can reach, and mark the lines that cannot be covered with LCOV_EXCL_LINE: - BJData writer: the dry_run parameter of the ND-array element writers was always true; replace them by range checks. - BON8 writer: call throw_integer_out_of_range() directly instead of write_bon8_value() for values that always throw. - binary_writer: integer_bits() is only called for integers of up to 64 bits. - serializer, view serializer, lexer: replace guards that the callers make unreachable by assertions or by the simpler code (surrogate pairs under ensure_ascii, next_unget, '-' at the end of the buffer). - json_view: drop unreachable branches in scalar(), set(), check_utf8_scan(), append_input_impl() and unescape(). - Mark exception-cleanup braces, 2 GiB limits, big-endian and 32-bit code, and fall-through case labels with LCOV_EXCL_LINE. No behavior changes for any input that reaches these functions. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: cover the remaining lines of the library headers Add tests for the lines of include/nlohmann that were not covered: the binary readers and writers, the lexer and locale paths, the json_view modules, the custom number, key and array types, json_pointer, json_patch, diff and the iterators. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix test failures in the coverage tests - unit-custom-object-key-type: remove the unused make_strict_key(strict_key) overload, the unused range constructor and default constructor, and guard data() with JSON_DIAGNOSTICS (unused-function/-member-function errors with GCC and clang -Weverything). - unit-json_view_compare: move the two new sections to the new unit-json_view_compare_values.cpp, as the object file got too large for the MinGW assembler again, and use a std::map with std::greater instead of std::unordered_map, which does not accept the incomplete basic_json (clang 3.9, icpc). - unit-json_view_dump: rename the variables that hid outer ones (MSVC C4456). - unit-stream-bulk-scan: restore the real size of the oversized get area before the adapter is destroyed (JSON_PRECISE_STREAM_POSITION reads the character at the read position), and skip the test where the streambuf cannot hold a get area larger than an int (MSVC). - unit-regression1: with JSON_NO_THREAD_LOCAL, copies are built member by member and keep correct timestamps, so check the corresponding outcome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> * Return std::size_t from the hash helper in the ordered_json test MSVC (Win32) rejected the implicit conversion of the unsigned long long product to the 32-bit std::size_t (C4244 as an error). Also note when JSON_NO_THREAD_LOCAL is set in the regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me> Signed-off-by: Claude <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
281 lines
9.6 KiB
C++
281 lines
9.6 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"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
TEST_CASE("tests on very large JSONs")
|
|
{
|
|
SECTION("issue #1419 - Segmentation fault (stack overflow) due to unbounded recursion")
|
|
{
|
|
const auto depth = 500000;
|
|
|
|
std::string s(static_cast<std::size_t>(2 * depth), '[');
|
|
std::fill(s.begin() + depth, s.end(), ']');
|
|
|
|
json _;
|
|
CHECK_NOTHROW(_ = nlohmann::json::parse(s));
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
// Descend a chain of single-element containers and return the value at its end,
|
|
// reporting the number of levels traversed in @a depth.
|
|
//
|
|
// The values in the test case below are nested far deeper than the call stack
|
|
// can follow, so they must not be inspected with operator== or dump(): both are
|
|
// still recursive and would overflow the stack themselves.
|
|
const json* innermost_value(const json& j, std::size_t& depth)
|
|
{
|
|
const json* current = &j;
|
|
depth = 0;
|
|
|
|
while ((current->is_array() || current->is_object()) && !current->empty())
|
|
{
|
|
current = current->is_array()
|
|
? ¤t->front()
|
|
: ¤t->begin().value();
|
|
++depth;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("tests on deeply nested JSONs")
|
|
{
|
|
// deep enough to exhaust the call stack, but small enough to stay cheap:
|
|
// parsing is iterative, so building the values below costs little
|
|
const std::size_t depth = 100000;
|
|
|
|
SECTION("issue #5387 - stack overflow in the copy constructor")
|
|
{
|
|
SECTION("array")
|
|
{
|
|
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
|
|
|
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
|
|
std::size_t copy_depth = 0;
|
|
CHECK(*innermost_value(copy, copy_depth) == 0);
|
|
CHECK(copy_depth == depth);
|
|
}
|
|
|
|
SECTION("object")
|
|
{
|
|
std::string s;
|
|
s.reserve((6 * depth) + 1);
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
s += "{\"a\":";
|
|
}
|
|
s += '1';
|
|
s.append(depth, '}');
|
|
|
|
const json j = json::parse(s);
|
|
|
|
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
|
|
std::size_t copy_depth = 0;
|
|
CHECK(*innermost_value(copy, copy_depth) == 1);
|
|
CHECK(copy_depth == depth);
|
|
}
|
|
|
|
SECTION("copy assignment")
|
|
{
|
|
// operator=(basic_json) takes its argument by value, so the deep
|
|
// copy happens in the copy constructor
|
|
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
|
|
|
json target;
|
|
target = j;
|
|
|
|
std::size_t target_depth = 0;
|
|
CHECK(*innermost_value(target, target_depth) == 0);
|
|
CHECK(target_depth == depth);
|
|
}
|
|
|
|
SECTION("depths around the bound of the recursive descent")
|
|
{
|
|
// The copy constructor descends into a bounded number of levels and
|
|
// completes whatever is below that without the call stack. Cover
|
|
// every depth around that bound, so that the two ways of copying
|
|
// are known to meet cleanly - wherever the bound is set.
|
|
for (std::size_t d = 1; d <= 300; ++d)
|
|
{
|
|
CAPTURE(d)
|
|
|
|
const json array = json::parse(std::string(d, '[') + '0' + std::string(d, ']'));
|
|
const json array_copy(array); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
std::size_t array_depth = 0;
|
|
CHECK(*innermost_value(array_copy, array_depth) == 0);
|
|
CHECK(array_depth == d);
|
|
|
|
std::string object_text;
|
|
for (std::size_t i = 0; i < d; ++i)
|
|
{
|
|
object_text += "{\"a\":";
|
|
}
|
|
object_text += '1';
|
|
object_text.append(d, '}');
|
|
|
|
const json object = json::parse(object_text);
|
|
const json object_copy(object); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
std::size_t object_depth = 0;
|
|
CHECK(*innermost_value(object_copy, object_depth) == 1);
|
|
CHECK(object_depth == d);
|
|
}
|
|
}
|
|
|
|
SECTION("leaf values of every type below the bound of the recursive descent")
|
|
{
|
|
// the elements and members of the containers below the bound are
|
|
// copied one by one, whatever their type
|
|
const json leaves = json::array(
|
|
{
|
|
nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3})
|
|
});
|
|
const json members =
|
|
{
|
|
{"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}},
|
|
{"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})}
|
|
};
|
|
|
|
for (const bool objects :
|
|
{
|
|
false, true
|
|
})
|
|
{
|
|
CAPTURE(objects)
|
|
|
|
json j = objects ? members : leaves;
|
|
for (std::size_t i = 0; i < 300; ++i)
|
|
{
|
|
json wrapper = objects ? json::object() : json::array();
|
|
if (objects)
|
|
{
|
|
wrapper["a"] = std::move(j);
|
|
}
|
|
else
|
|
{
|
|
wrapper.push_back(std::move(j));
|
|
}
|
|
j = std::move(wrapper);
|
|
}
|
|
|
|
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
|
|
const json* current = ©
|
|
for (std::size_t i = 0; i < 300; ++i)
|
|
{
|
|
current = objects ? ¤t->at("a") : ¤t->front();
|
|
}
|
|
|
|
// the innermost value is shallow: it is safe to compare
|
|
CHECK(*current == (objects ? members : leaves));
|
|
CHECK(current->size() == (objects ? members.size() : leaves.size()));
|
|
}
|
|
}
|
|
|
|
SECTION("a value that is deep in one place only")
|
|
{
|
|
json j = json::object();
|
|
j["shallow"] = 1;
|
|
j["deep"] = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
|
j["also_shallow"] = json::array({1, 2, 3});
|
|
|
|
const json copy(j);
|
|
|
|
CHECK(copy["shallow"] == 1);
|
|
CHECK(copy["also_shallow"] == json::array({1, 2, 3}));
|
|
|
|
std::size_t deep_depth = 0;
|
|
CHECK(*innermost_value(copy["deep"], deep_depth) == 0);
|
|
CHECK(deep_depth == depth);
|
|
}
|
|
|
|
SECTION("comparing")
|
|
{
|
|
// Comparing used to descend once per level, and an ordered
|
|
// comparison used to compare every pair of elements twice, once in
|
|
// each direction, which took exponentially long in the nesting
|
|
// depth. Both are gone: these finish in milliseconds, where the
|
|
// second used to take longer than anyone would wait even for a
|
|
// value nested only a few dozen levels deep.
|
|
const std::string text = std::string(depth, '[') + '0' + std::string(depth, ']');
|
|
const json j = json::parse(text);
|
|
const json same = json::parse(text);
|
|
const json larger = json::parse(std::string(depth, '[') + '1' + std::string(depth, ']'));
|
|
|
|
CHECK(j == same);
|
|
CHECK_FALSE(j == larger);
|
|
CHECK(j != larger);
|
|
|
|
CHECK(j < larger);
|
|
CHECK_FALSE(larger < j);
|
|
CHECK(larger > j);
|
|
CHECK(j <= same);
|
|
CHECK(j >= same);
|
|
|
|
// a value that ends earlier is the smaller one
|
|
const json shorter = json::parse(std::string(depth - 1, '[') + '0' + std::string(depth - 1, ']'));
|
|
CHECK_FALSE(j == shorter);
|
|
}
|
|
|
|
SECTION("comparing objects")
|
|
{
|
|
std::string text;
|
|
text.reserve((6 * depth) + 1);
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += "{\"a\":";
|
|
}
|
|
text += '1';
|
|
text.append(depth, '}');
|
|
|
|
const json j = json::parse(text);
|
|
const json same = json::parse(text);
|
|
|
|
CHECK(j == same);
|
|
CHECK_FALSE(j != same);
|
|
CHECK(j <= same);
|
|
CHECK(j >= same);
|
|
}
|
|
|
|
SECTION("the copy is independent of the original")
|
|
{
|
|
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
|
|
|
json copy(j);
|
|
|
|
// reach the innermost value without recursing and replace it
|
|
json* current = ©
|
|
while (current->is_array() && !current->empty())
|
|
{
|
|
current = ¤t->front();
|
|
}
|
|
*current = 42;
|
|
|
|
std::size_t unused = 0;
|
|
CHECK(*innermost_value(copy, unused) == 42);
|
|
CHECK(*innermost_value(j, unused) == 0);
|
|
}
|
|
}
|
|
}
|