mirror of
https://github.com/nlohmann/json.git
synced 2026-11-13 07:25:23 +00:00
Merge develop into fix/ubjson-valueless-budget and avoid MSVC C4127
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
94 files changed
+2871
-1461
No files matched your search
@@ -355,6 +355,20 @@ json_test_add_test_for(src/unit-comparison.cpp
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test the walks over deeply nested values with the extreme settings of
|
||||
# JSON_RECURSION_DEPTH_LIMIT: 0 (every walk uses its explicit stack from the
|
||||
# first level on), 1, and 254 (the largest limit that fits the byte that counts
|
||||
# the nesting of a copy)
|
||||
foreach(recursion_depth_limit 0 1 254)
|
||||
json_test_set_test_options(test-recursion_depth_limit_${recursion_depth_limit}
|
||||
COMPILE_DEFINITIONS JSON_RECURSION_DEPTH_LIMIT=${recursion_depth_limit}
|
||||
)
|
||||
json_test_add_test_for(src/unit-recursion_depth_limit.cpp
|
||||
NAME test-recursion_depth_limit_${recursion_depth_limit}
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# test the parser again with JSON_DIAGNOSTIC_POSITIONS enabled
|
||||
json_test_set_test_options(test-class_parser_diagnostic_positions
|
||||
COMPILE_DEFINITIONS JSON_DIAGNOSTIC_POSITIONS=1
|
||||
@@ -364,6 +378,16 @@ json_test_add_test_for(src/unit-class_parser.cpp
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test the conversions again without std::filesystem::path support: a
|
||||
# std::u8string must be a JSON string in this configuration, too
|
||||
json_test_set_test_options(test-conversions2_no_filesystem
|
||||
COMPILE_DEFINITIONS JSON_HAS_FILESYSTEM=0 JSON_HAS_EXPERIMENTAL_FILESYSTEM=0
|
||||
)
|
||||
json_test_add_test_for(src/unit-conversions2.cpp
|
||||
NAME test-conversions2_no_filesystem
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test diagnostic positions again without regular diagnostics (JSON pointer paths)
|
||||
json_test_set_test_options(test-diagnostic-positions_only
|
||||
COMPILE_DEFINITIONS JSON_DIAGNOSTICS=0
|
||||
|
||||
@@ -56,9 +56,26 @@ int main()
|
||||
const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "list": [1, 2, 3]})");
|
||||
const std::size_t doc_size = doc.root().size();
|
||||
|
||||
// editable documents and views, view_mode, is_json_view and the ordered variants
|
||||
nlohmann::json_editable_document edoc = nlohmann::json_editable_document::parse(R"({"a": 1, "list": [1, 2, 3]})");
|
||||
edoc.set(edoc.root(), "b", 2);
|
||||
edoc.push_back(edoc.root()["list"], 4);
|
||||
const nlohmann::json_editable_view eroot = edoc.root();
|
||||
const std::size_t edit_size = eroot.size();
|
||||
const std::size_t list_size = eroot["list"].size();
|
||||
nlohmann::ordered_json_editable_document oedoc = nlohmann::ordered_json_editable_document::parse(R"({"z": 1, "y": 2})");
|
||||
oedoc.erase(oedoc.root(), "z");
|
||||
const nlohmann::ordered_json_editable_view oeroot = oedoc.root();
|
||||
nlohmann::basic_json_document<nlohmann::json, nlohmann::view_mode::editable> mdoc = nlohmann::basic_json_document<nlohmann::json, nlohmann::view_mode::editable>::parse("[]");
|
||||
mdoc.push_back(mdoc.root(), 1);
|
||||
static_assert(nlohmann::is_json_view<nlohmann::json_view>::value, "json_view is a view");
|
||||
static_assert(nlohmann::is_json_view<nlohmann::json_editable_view>::value, "json_editable_view is a view");
|
||||
static_assert(!nlohmann::is_json_view<nlohmann::json>::value, "json is not a view");
|
||||
|
||||
// use every result so the references cannot be optimized away
|
||||
return (a == 1 && last == 3 && b == 2 && lit.size() == 3
|
||||
&& m.size() == 1 && !dumped.empty() && !os.str().empty()
|
||||
&& doc_size == 2)
|
||||
&& doc_size == 2
|
||||
&& edit_size == 3 && list_size == 4 && oeroot.size() == 1 && mdoc.root().size() == 1)
|
||||
? 0 : 1;
|
||||
}
|
||||
@@ -236,7 +236,8 @@ TEST_CASE("controlled bad_alloc")
|
||||
|
||||
next_construct_fails = false;
|
||||
|
||||
// deeper than the 128 levels the copy constructor descends into
|
||||
// deeper than the levels the copy constructor descends into
|
||||
// (recursion_depth_limit(): 128 by default, 254 at most)
|
||||
const std::size_t depth = 300;
|
||||
|
||||
my_json j = 1;
|
||||
@@ -300,8 +301,9 @@ struct nth_alloc_fails_allocator : std::allocator<T>
|
||||
};
|
||||
};
|
||||
|
||||
// builds a value nested more than 128 levels deep - the bound the copy
|
||||
// constructor descends into before it continues without the call stack - and
|
||||
// builds a value nested more than recursion_depth_limit() (128 by default)
|
||||
// levels deep - the bound the copy constructor descends into before it
|
||||
// continues without the call stack - and
|
||||
// checks that a copy survives any single allocation of it failing: every
|
||||
// attempt either throws std::bad_alloc, without crashing or leaving the
|
||||
// source altered, or completes the copy
|
||||
@@ -312,9 +314,9 @@ void check_deep_copy_survives_failing_allocation(bool nest_objects)
|
||||
|
||||
fail_at_alloc_call = -1;
|
||||
|
||||
// [[[ ... [1] ... ]]], or the same nesting with objects, 130 levels deep
|
||||
// [[[ ... [1] ... ]]], or the same nesting with objects, two levels deeper than the limit
|
||||
BasicJsonType src = 1;
|
||||
for (std::size_t i = 0; i < 130; ++i)
|
||||
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 2; ++i)
|
||||
{
|
||||
if (nest_objects)
|
||||
{
|
||||
@@ -440,8 +442,9 @@ TEST_CASE("deep copy uses the provided allocator")
|
||||
{
|
||||
using counting_json = nlohmann::json::with_allocator_t<scratch_counting_allocator>;
|
||||
|
||||
// deeper than the 128 levels the copy constructor descends into, so the
|
||||
// innermost objects are copied by the iterative deep copy
|
||||
// deeper than the levels the copy constructor descends into (128 by
|
||||
// default, 254 at most), so the innermost objects are copied by the
|
||||
// iterative deep copy
|
||||
counting_json j = 1;
|
||||
for (std::size_t i = 0; i < 300; ++i)
|
||||
{
|
||||
@@ -499,11 +502,12 @@ TEST_CASE("converting a deeply nested value from another specialization fails cl
|
||||
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
||||
using countdown_json = nlohmann::json::with_allocator_t<countdown_allocator>;
|
||||
|
||||
// deeper than the 128 levels the converting constructor descends into, so
|
||||
// that failures land on both sides of the bound - or, built with
|
||||
// JSON_NO_THREAD_LOCAL, all in the iterative conversion
|
||||
// deeper than the levels the converting constructor descends into
|
||||
// (recursion_depth_limit(), 128 by default), so that failures land on both
|
||||
// sides of the bound - or, built with JSON_NO_THREAD_LOCAL, all in the
|
||||
// iterative conversion
|
||||
json j = {1, "two", {{"three", 3}}};
|
||||
for (std::size_t i = 0; i < 150; ++i)
|
||||
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 22; ++i)
|
||||
{
|
||||
j = json{{"a", json::array({j, "sibling"})}};
|
||||
}
|
||||
|
||||
@@ -531,9 +531,28 @@ TEST_CASE("alternative string type with json_view")
|
||||
CHECK(d.root().materialize() == expected);
|
||||
CHECK(alt_json::parse(d.root().dump()) == expected);
|
||||
|
||||
// save() and load() do not exist on big-endian targets
|
||||
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
const alt_editable_document loaded = alt_editable_document::load(d.save());
|
||||
CHECK(loaded.root().materialize() == expected);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// a std::u8string is a JSON string, whatever the string type of the JSON value
|
||||
// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file)
|
||||
#if defined(__cpp_lib_char8_t)
|
||||
TEST_CASE("alternative string type and std::u8string")
|
||||
{
|
||||
const std::u8string s = u8"abc";
|
||||
const alt_json j = s;
|
||||
CHECK(j.is_string());
|
||||
CHECK(j.get<alt_string>() == alt_string("abc"));
|
||||
CHECK(j.get<std::u8string>() == s);
|
||||
|
||||
// the array of code units written by versions before 3.13.0
|
||||
CHECK(alt_json::array({0x61, 0x62, 0x63}).get<std::u8string>() == s);
|
||||
}
|
||||
#endif
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
@@ -94,6 +94,37 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS")
|
||||
CHECK(j3 == pair_shaped);
|
||||
}
|
||||
|
||||
SECTION("push_back and operator+= with a single braced JSON value")
|
||||
{
|
||||
// with the macro, the result does not depend on the compiler;
|
||||
// without it, GCC wraps a JSON value and Clang does not (a literal
|
||||
// such as {1} is wrapped by both)
|
||||
json const obj = {{"key", "value"}};
|
||||
json const arr = {1, 2};
|
||||
|
||||
json a1 = json::array();
|
||||
a1.push_back({obj});
|
||||
CHECK(a1 == json::array({obj}));
|
||||
|
||||
json a2 = json::array();
|
||||
a2 += {obj};
|
||||
CHECK(a2 == json::array({obj}));
|
||||
|
||||
json b1 = json::array();
|
||||
b1.push_back({arr});
|
||||
CHECK(b1 == json::array({arr}));
|
||||
|
||||
json b2 = json::array();
|
||||
b2 += {arr};
|
||||
CHECK(b2 == json::array({arr}));
|
||||
|
||||
// a literal is copied as well
|
||||
json c = json::array();
|
||||
c.push_back({1});
|
||||
c += {2};
|
||||
CHECK(c == json::array({1, 2}));
|
||||
}
|
||||
|
||||
SECTION("what the macro does not change")
|
||||
{
|
||||
// lists with more than one element are unaffected
|
||||
|
||||
@@ -3232,3 +3232,40 @@ TEST_CASE("object keys are delivered decoded and validated")
|
||||
CHECK(keys_of_format(std::string("\x87\xC3\x28\x91"), json::input_format_t::bon8).empty());
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// a plain function (not a lambda or std::function) used as parser callback
|
||||
bool keep_everything_callback(int /*depth*/, json::parse_event_t /*event*/, json& /*parsed*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("a plain function is accepted as parser callback for containers")
|
||||
{
|
||||
// ADL finds basic_json's friend operator!= for "container != function
|
||||
// pointer", which must not make the iterator/sentinel overloads viable
|
||||
|
||||
const json expected = {1, 2};
|
||||
const std::string text = "[1,2]";
|
||||
const std::vector<std::uint8_t> bytes(text.begin(), text.end());
|
||||
const std::vector<char> chars(text.begin(), text.end());
|
||||
|
||||
CHECK(json::parse(text, keep_everything_callback) == expected);
|
||||
CHECK(json::parse(text, &keep_everything_callback) == expected);
|
||||
CHECK(json::parse(std::string("[1,2]"), keep_everything_callback) == expected);
|
||||
CHECK(json::parse(bytes, keep_everything_callback) == expected);
|
||||
CHECK(json::parse(bytes, &keep_everything_callback) == expected);
|
||||
CHECK(json::parse(chars, keep_everything_callback) == expected);
|
||||
CHECK(json::parse(text.begin(), text.end(), keep_everything_callback) == expected);
|
||||
|
||||
// the sentinel overloads (shared constraint of parse, accept, sax_parse and
|
||||
// the from_* functions) are not viable for a container and a function
|
||||
// pointer, but still are for iterator pairs
|
||||
using callback_pointer = decltype(&keep_everything_callback);
|
||||
CHECK_FALSE((nlohmann::detail::can_compare_ne<std::string, callback_pointer>::value));
|
||||
CHECK_FALSE((nlohmann::detail::can_compare_ne<std::vector<std::uint8_t>, callback_pointer>::value));
|
||||
CHECK((nlohmann::detail::can_compare_ne<std::string::const_iterator, std::string::const_iterator>::value));
|
||||
CHECK((nlohmann::detail::can_compare_ne<const char*, const char*>::value));
|
||||
}
|
||||
@@ -62,6 +62,15 @@ template<> struct StringMaker<std::partial_ordering>
|
||||
|
||||
namespace
|
||||
{
|
||||
// nesting depths on either side of detail::recursion_depth_limit(): none, the
|
||||
// last one within the limit, the first one past it, and well past it (0, 127,
|
||||
// 128, and 200 by default)
|
||||
std::vector<std::size_t> depths_around_recursion_limit()
|
||||
{
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
return {0, limit > 0 ? limit - 1 : 0, limit, limit + 72};
|
||||
}
|
||||
|
||||
// helper function to check std::less<json::value_t>
|
||||
// see https://en.cppreference.com/w/cpp/utility/functional/less
|
||||
template <typename A, typename B, typename U = std::less<json::value_t>>
|
||||
@@ -968,7 +977,7 @@ TEST_CASE("copying an object preserves its comparator's state")
|
||||
object["a"] = 3;
|
||||
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
|
||||
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
for (const std::size_t depth : depths_around_recursion_limit())
|
||||
{
|
||||
CAPTURE(depth)
|
||||
|
||||
@@ -1011,7 +1020,7 @@ TEST_CASE("equality of an object whose comparator treats different keys as equiv
|
||||
// sanity check: the object type's own comparison already disagrees
|
||||
CHECK_FALSE(a.get_ref<const ci_json::object_t&>() == b.get_ref<const ci_json::object_t&>());
|
||||
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
for (const std::size_t depth : depths_around_recursion_limit())
|
||||
{
|
||||
CAPTURE(depth)
|
||||
|
||||
@@ -1135,9 +1144,10 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend
|
||||
return result;
|
||||
};
|
||||
|
||||
// 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not,
|
||||
// and must still agree with the levels that do
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
// one level less than the recursion depth limit (127 by default) stays within it;
|
||||
// the limit itself and more (128 and 200) do not, and must still agree with the
|
||||
// levels that do
|
||||
for (const std::size_t depth : depths_around_recursion_limit())
|
||||
{
|
||||
CAPTURE(depth)
|
||||
const json x = deep(a, depth);
|
||||
|
||||
@@ -27,10 +27,12 @@ using nlohmann::json;
|
||||
#include <deque>
|
||||
#include <forward_list>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <valarray>
|
||||
#include <vector>
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
@@ -707,38 +709,113 @@ TEST_CASE("std::filesystem::path")
|
||||
}
|
||||
#endif
|
||||
|
||||
// the ADL to_json overload for std::u8string only exists under the same guard
|
||||
// as std::filesystem::path support (it is otherwise only reached indirectly,
|
||||
// via std::filesystem::path::u8string()) -- mirror both #if conditions from
|
||||
// include/nlohmann/detail/conversions/to_json.hpp exactly
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
// a std::u8string is a JSON string since 3.13.0 (it was an array of code units
|
||||
// before); independent of whether std::filesystem::path is supported, see the
|
||||
// test-conversions2_no_filesystem target
|
||||
// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file)
|
||||
#if defined(__cpp_lib_char8_t)
|
||||
TEST_CASE("std::u8string")
|
||||
{
|
||||
// use \u universal-character-names (rather than raw \x byte escapes
|
||||
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
|
||||
// encoding -- MSVC treats \x escapes used that way inside a u8
|
||||
// literal as a nonstandard extension (warning C5321), which some of
|
||||
// our CI configs promote to an error; \u is portable and produces
|
||||
// the exact same encoded bytes without depending on the source
|
||||
// file's encoding
|
||||
const std::u8string utf8 = u8"P\u011B\u0161ina";
|
||||
|
||||
SECTION("ascii")
|
||||
{
|
||||
const std::u8string s = u8"Path";
|
||||
json const j = s;
|
||||
|
||||
CHECK(j.is_string());
|
||||
CHECK(j.template get<std::string>() == "Path");
|
||||
}
|
||||
|
||||
SECTION("utf-8")
|
||||
{
|
||||
// use \u universal-character-names (rather than raw \x byte escapes
|
||||
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
|
||||
// encoding -- MSVC treats \x escapes used that way inside a u8
|
||||
// literal as a nonstandard extension (warning C5321), which some of
|
||||
// our CI configs promote to an error; \u is portable and produces
|
||||
// the exact same encoded bytes without depending on the source
|
||||
// file's encoding
|
||||
const std::u8string s = u8"P\u011B\u0161ina";
|
||||
json const j = s;
|
||||
json const j = utf8;
|
||||
|
||||
CHECK(j.is_string());
|
||||
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("round trip")
|
||||
{
|
||||
json const j = utf8;
|
||||
CHECK(j.template get<std::u8string>() == utf8);
|
||||
|
||||
std::u8string back;
|
||||
j.get_to(back);
|
||||
CHECK(back == utf8);
|
||||
|
||||
// from a string
|
||||
json const j_string = "P\xc4\x9b\xc5\xa1ina";
|
||||
CHECK(j_string.template get<std::u8string>() == utf8);
|
||||
|
||||
// empty, and with an embedded NUL
|
||||
CHECK(json(std::u8string()).template get<std::u8string>().empty());
|
||||
const std::u8string with_nul(u8"a\0b", 3);
|
||||
CHECK(json(with_nul).template get<std::u8string>() == with_nul);
|
||||
|
||||
// the lvalue and rvalue paths, and in containers
|
||||
CHECK(json(utf8).template get<std::u8string>() == utf8);
|
||||
CHECK(json(std::u8string(utf8)).template get<std::u8string>() == utf8);
|
||||
const std::vector<std::u8string> vec = {utf8, u8"x"};
|
||||
json const j_vec = vec;
|
||||
CHECK(j_vec.is_array());
|
||||
CHECK(j_vec[0].is_string());
|
||||
CHECK(j_vec.template get<std::vector<std::u8string>>() == vec);
|
||||
const std::map<std::string, std::u8string> map = {{"k", utf8}};
|
||||
CHECK(json(map).template get<std::map<std::string, std::u8string>>() == map);
|
||||
}
|
||||
|
||||
SECTION("array of code units (written by versions before 3.13.0)")
|
||||
{
|
||||
json const j = json::array({0x50, 0xC4, 0x9B, 0xC5, 0xA1, 0x69, 0x6E, 0x61});
|
||||
CHECK(j.template get<std::u8string>() == utf8);
|
||||
CHECK(json::array().template get<std::u8string>().empty());
|
||||
CHECK(json::parse("[97, 98]").template get<std::u8string>() == u8"ab");
|
||||
|
||||
std::u8string s = u8"overwritten";
|
||||
j.get_to(s);
|
||||
CHECK(s == utf8);
|
||||
}
|
||||
|
||||
SECTION("other types")
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK_THROWS_WITH_AS(json(1).template get<std::u8string>(), "[json.exception.type_error.302] type must be string or array, but is number", json::type_error);
|
||||
CHECK_THROWS_WITH_AS(json(nullptr).template get<std::u8string>(), "[json.exception.type_error.302] type must be string or array, but is null", json::type_error);
|
||||
CHECK_THROWS_AS(json::object().template get<std::u8string>(), json::type_error);
|
||||
// an element that is not a number
|
||||
CHECK_THROWS_AS(json::array({"a"}).template get<std::u8string>(), json::type_error);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("ordered_json")
|
||||
{
|
||||
nlohmann::ordered_json const j = utf8;
|
||||
CHECK(j.is_string());
|
||||
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
|
||||
CHECK(j.template get<std::u8string>() == utf8);
|
||||
CHECK(nlohmann::ordered_json::array({0x61, 0x62}).template get<std::u8string>() == u8"ab");
|
||||
}
|
||||
|
||||
SECTION("std::u8string_view and char8_t arrays are unchanged (arrays of code units)")
|
||||
{
|
||||
const std::u8string_view view = utf8;
|
||||
json const j_view = view;
|
||||
CHECK(j_view.is_array());
|
||||
CHECK(j_view.size() == 8);
|
||||
|
||||
const char8_t arr[] = u8"ab"; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
json const j_arr = arr;
|
||||
CHECK(j_arr == json::array({0x61, 0x62, 0}));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
@@ -217,16 +218,17 @@ TEST_CASE("array type with std::deque nested beyond the recursion limit")
|
||||
// recursion_depth_limit() with an explicit stack of frames; with the
|
||||
// iterators of std::deque, these frames are copied and moved through
|
||||
// their own special members
|
||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default
|
||||
deque_json j = 1;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
deque_json outer = deque_json::array();
|
||||
outer.push_back(std::move(j));
|
||||
j = std::move(outer);
|
||||
}
|
||||
|
||||
// 200 arrays of one element each (0x81) around the integer 1
|
||||
std::vector<std::uint8_t> expected(200, 0x81);
|
||||
// `depth` arrays of one element each (0x81) around the integer 1
|
||||
std::vector<std::uint8_t> expected(depth, 0x81);
|
||||
expected.push_back(0x01);
|
||||
CHECK(deque_json::to_cbor(j) == expected);
|
||||
}
|
||||
@@ -475,10 +475,10 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require
|
||||
|
||||
SECTION("nested deeper than the copy constructor's descent bound")
|
||||
{
|
||||
// beyond nesting_depth_limit() (128) levels, the copy constructor
|
||||
// beyond recursion_depth_limit() (128 by default) levels, the copy constructor
|
||||
// copies without the call stack (copy_iteratively / copy_array_level),
|
||||
// which used to assign the base class of every element it created
|
||||
const std::size_t depth = 300;
|
||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 172;
|
||||
|
||||
const json_with_const_base j = make_nested_array(depth);
|
||||
const json_with_const_base copy = j; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
|
||||
@@ -128,16 +128,19 @@ TEST_CASE("Better diagnostics with positions")
|
||||
check_copy(depth, true);
|
||||
};
|
||||
|
||||
// around detail::recursion_depth_limit() (127, 128, and 129 by default)
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
|
||||
check_arrays(1);
|
||||
check_arrays(127);
|
||||
check_arrays(128);
|
||||
check_arrays(129);
|
||||
check_arrays(limit > 0 ? limit - 1 : 0);
|
||||
check_arrays(limit);
|
||||
check_arrays(limit + 1);
|
||||
check_arrays(300);
|
||||
|
||||
check_objects(1);
|
||||
check_objects(127);
|
||||
check_objects(128);
|
||||
check_objects(129);
|
||||
check_objects(limit > 0 ? limit - 1 : 0);
|
||||
check_objects(limit);
|
||||
check_objects(limit + 1);
|
||||
check_objects(300);
|
||||
}
|
||||
|
||||
@@ -186,10 +189,13 @@ TEST_CASE("Better diagnostics with positions")
|
||||
}
|
||||
};
|
||||
|
||||
// around detail::recursion_depth_limit() (127, 128, and 129 by default)
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
|
||||
check_conversion(1);
|
||||
check_conversion(127);
|
||||
check_conversion(128);
|
||||
check_conversion(129);
|
||||
check_conversion(limit > 0 ? limit - 1 : 0);
|
||||
check_conversion(limit);
|
||||
check_conversion(limit + 1);
|
||||
check_conversion(300);
|
||||
}
|
||||
|
||||
|
||||
@@ -515,9 +515,11 @@ TEST_CASE("Regression tests for extended diagnostics")
|
||||
|
||||
// update(j, true) around its descent bound, where the nested vectors
|
||||
// grow while the objects are merged without recursing
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
// (the limit can be 0, where "one level less" is no level)
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), nlohmann::detail::recursion_depth_limit() + 2
|
||||
limit > 0 ? limit - 1 : 0, limit, limit + 2
|
||||
})
|
||||
{
|
||||
ordered_json j = {{"z", {{"x", 1}}}};
|
||||
|
||||
@@ -14,9 +14,12 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -1975,14 +1978,15 @@ TEST_CASE("JSON patch: diff of deeply nested values")
|
||||
SECTION("the diff reproduces the target at every depth")
|
||||
{
|
||||
// depths on either side of the nesting depth up to which diff()
|
||||
// recurses (detail::recursion_depth_limit(), 128); not every depth up
|
||||
// recurses (detail::recursion_depth_limit(), 128 by default); not every depth up
|
||||
// to 300, as the test would then time out under Valgrind
|
||||
std::vector<std::size_t> depths;
|
||||
for (std::size_t depth = 0; depth <= 16; ++depth)
|
||||
{
|
||||
depths.push_back(depth);
|
||||
}
|
||||
for (std::size_t depth = 120; depth <= 136; ++depth)
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth)
|
||||
{
|
||||
depths.push_back(depth);
|
||||
}
|
||||
@@ -2524,7 +2528,7 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des
|
||||
// the values at the innermost level are diffed without recursion: equal
|
||||
// leaves yield nothing, leaves of different types and different leaves
|
||||
// are replaced
|
||||
const std::size_t depth = 200;
|
||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default
|
||||
|
||||
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
|
||||
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
|
||||
@@ -2551,3 +2555,422 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des
|
||||
CHECK(object_patch.size() == 2);
|
||||
CHECK(object_source.patch(object_patch) == object_target);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// all pointers to values below `value` (not to `value` itself)
|
||||
void collect_pointers(const json& value, const std::string& path, std::vector<std::string>& pointers)
|
||||
{
|
||||
if (value.is_object())
|
||||
{
|
||||
for (const auto& element : value.items())
|
||||
{
|
||||
std::string child = path;
|
||||
child += '/';
|
||||
for (const char c : element.key())
|
||||
{
|
||||
if (c == '~')
|
||||
{
|
||||
child += "~0";
|
||||
}
|
||||
else if (c == '/')
|
||||
{
|
||||
child += "~1";
|
||||
}
|
||||
else
|
||||
{
|
||||
child += c;
|
||||
}
|
||||
}
|
||||
pointers.push_back(child);
|
||||
collect_pointers(element.value(), child, pointers);
|
||||
}
|
||||
}
|
||||
else if (value.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < value.size(); ++i)
|
||||
{
|
||||
std::string child = path;
|
||||
child += '/';
|
||||
child += std::to_string(i);
|
||||
pointers.push_back(child);
|
||||
collect_pointers(value[i], child, pointers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a random document of at most `depth` levels, with the names as keys
|
||||
json random_document(std::mt19937& gen, const std::vector<std::string>& names, const int depth)
|
||||
{
|
||||
const int kind = std::uniform_int_distribution<int>(0, 9)(gen);
|
||||
if (depth == 0 || kind < 2)
|
||||
{
|
||||
return std::uniform_int_distribution<int>(0, 3)(gen);
|
||||
}
|
||||
const int size = std::uniform_int_distribution<int>(1, 3)(gen);
|
||||
json result = kind < 6 ? json::array() : json::object();
|
||||
for (int i = 0; i < size; ++i)
|
||||
{
|
||||
if (kind < 6)
|
||||
{
|
||||
result.push_back(random_document(gen, names, depth - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
result[names[std::uniform_int_distribution<std::size_t>(0, names.size() - 1)(gen)]] = random_document(gen, names, depth - 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// the strings, one after the other
|
||||
std::string concat(std::string first, const std::string& second, const std::string& third = std::string())
|
||||
{
|
||||
first += second;
|
||||
first += third;
|
||||
return first;
|
||||
}
|
||||
|
||||
// the best of five times in seconds that std::clock() measures to apply the patch
|
||||
double patch_cpu_time(json& doc, const json& patch)
|
||||
{
|
||||
double best = 1e30;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
const std::clock_t start = std::clock();
|
||||
doc.patch_inplace(patch);
|
||||
best = (std::min)(best, static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("JSON patch - consecutive operations with paths that share a prefix")
|
||||
{
|
||||
SECTION("tokens that start the same way")
|
||||
{
|
||||
const json doc = {{"a", {{"bc", {{"x", 1}}}, {"b", {{"x", 2}}}, {"b2", 3}}}};
|
||||
|
||||
// the second path shares "/a/b" with the first, but not the token "bc"
|
||||
const json patch = json::parse(R"([
|
||||
{"op": "test", "path": "/a/bc/x", "value": 1},
|
||||
{"op": "test", "path": "/a/b/x", "value": 2},
|
||||
{"op": "test", "path": "/a/bc/x", "value": 1},
|
||||
{"op": "test", "path": "/a/b2", "value": 3},
|
||||
{"op": "test", "path": "/a/b/x", "value": 2}
|
||||
])");
|
||||
CHECK(doc.patch(patch) == doc);
|
||||
|
||||
json inplace = doc;
|
||||
inplace.patch_inplace(patch);
|
||||
CHECK(inplace == doc);
|
||||
|
||||
// a path that is a prefix of the previous one, and the other way round
|
||||
const json prefixes = json::parse(R"([
|
||||
{"op": "test", "path": "/a/bc/x", "value": 1},
|
||||
{"op": "test", "path": "/a/bc", "value": {"x": 1}},
|
||||
{"op": "test", "path": "/a/bc/x", "value": 1},
|
||||
{"op": "test", "path": "/a", "value": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}},
|
||||
{"op": "test", "path": "/a/b/x", "value": 2},
|
||||
{"op": "test", "path": "", "value": {"a": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}}}
|
||||
])");
|
||||
CHECK(doc.patch(prefixes) == doc);
|
||||
}
|
||||
|
||||
SECTION("operations above a path that was resolved before")
|
||||
{
|
||||
const json doc = json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})");
|
||||
|
||||
// an element inserted before the elements of an array moves them
|
||||
const json inserted = json::parse(R"([
|
||||
{"op": "test", "path": "/a/0/b/2", "value": 3},
|
||||
{"op": "add", "path": "/a/0", "value": {"b": [9]}},
|
||||
{"op": "test", "path": "/a/1/b/2", "value": 3},
|
||||
{"op": "test", "path": "/a/0/b/0", "value": 9},
|
||||
{"op": "test", "path": "/a/2/b/0", "value": 4}
|
||||
])");
|
||||
CHECK(doc.patch(inserted) == json::parse(R"({"a": [{"b": [9]}, {"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})"));
|
||||
|
||||
// an element removed from the front
|
||||
const json removed_front = json::parse(R"([
|
||||
{"op": "test", "path": "/a/1/b/2", "value": 6},
|
||||
{"op": "remove", "path": "/a/0"},
|
||||
{"op": "test", "path": "/a/0/b/2", "value": 6},
|
||||
{"op": "replace", "path": "/a/0/b/2", "value": 60},
|
||||
{"op": "test", "path": "/a/0/b/2", "value": 60}
|
||||
])");
|
||||
CHECK(doc.patch(removed_front) == json::parse(R"({"a": [{"b": [4, 5, 60]}], "c": {"d": {"e": 7}}})"));
|
||||
|
||||
// a value replaced above the path that was resolved before
|
||||
const json replaced_above = json::parse(R"([
|
||||
{"op": "test", "path": "/c/d/e", "value": 7},
|
||||
{"op": "replace", "path": "/c/d", "value": {"e": 8, "f": {"g": 9}}},
|
||||
{"op": "test", "path": "/c/d/e", "value": 8},
|
||||
{"op": "test", "path": "/c/d/f/g", "value": 9},
|
||||
{"op": "remove", "path": "/c/d/f"},
|
||||
{"op": "add", "path": "/c/d/f", "value": [1]},
|
||||
{"op": "test", "path": "/c/d/f/0", "value": 1}
|
||||
])");
|
||||
CHECK(doc.patch(replaced_above) == json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 8, "f": [1]}}})"));
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// a value removed above the path that was resolved before is gone
|
||||
const json removed = json::parse(R"([
|
||||
{"op": "test", "path": "/c/d/e", "value": 7},
|
||||
{"op": "remove", "path": "/c/d"},
|
||||
{"op": "test", "path": "/c/d/e", "value": 7}
|
||||
])");
|
||||
CHECK_THROWS_AS(doc.patch(removed), json::other_error&);
|
||||
json inplace = doc;
|
||||
CHECK_THROWS_AS(inplace.patch_inplace(removed), json::other_error&);
|
||||
|
||||
const json emptied = json::parse(R"([
|
||||
{"op": "test", "path": "/a/1/b/2", "value": 6},
|
||||
{"op": "remove", "path": "/a/1"},
|
||||
{"op": "test", "path": "/a/1/b/2", "value": 6}
|
||||
])");
|
||||
CHECK_THROWS_AS(doc.patch(emptied), json::other_error&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("paths with more than 16 tokens")
|
||||
{
|
||||
// 40 levels, with an array at the bottom and a second branch at level 20
|
||||
std::string prefix20;
|
||||
std::string prefix40;
|
||||
json doc = {{"leaf", {1, 2, 3}}};
|
||||
for (int i = 40; i > 0; --i)
|
||||
{
|
||||
if (i == 20)
|
||||
{
|
||||
doc["other"] = {{"x", 5}};
|
||||
}
|
||||
doc = {{"n", doc}};
|
||||
}
|
||||
for (int i = 1; i <= 40; ++i)
|
||||
{
|
||||
prefix40 += "/n";
|
||||
if (i == 20)
|
||||
{
|
||||
prefix20 = prefix40;
|
||||
}
|
||||
}
|
||||
|
||||
const auto op = [](const char* name, const std::string & path, const json & value)
|
||||
{
|
||||
return json({{"op", name}, {"path", path}, {"value", value}});
|
||||
};
|
||||
const json patch =
|
||||
{
|
||||
op("test", concat(prefix40, "/leaf/2"), 3),
|
||||
op("test", concat(prefix20, "/other/x"), 5),
|
||||
op("test", concat(prefix40, "/leaf/0"), 1),
|
||||
op("add", concat(prefix40, "/leaf/0"), 0),
|
||||
op("test", concat(prefix40, "/leaf/3"), 3),
|
||||
op("replace", concat(prefix20, "/other"), {{"x", 6}}),
|
||||
op("test", concat(prefix20, "/other/x"), 6),
|
||||
op("test", concat(prefix40, "/leaf/1"), 1),
|
||||
{{"op", "remove"}, {"path", concat(prefix20, "/other")}},
|
||||
op("test", concat(prefix40, "/leaf/1"), 1)
|
||||
};
|
||||
json expected = doc;
|
||||
for (const auto& operation : patch)
|
||||
{
|
||||
expected = expected.patch(json::array({operation}));
|
||||
}
|
||||
CHECK(doc.patch(patch) == expected);
|
||||
json inplace = doc;
|
||||
inplace.patch_inplace(patch);
|
||||
CHECK(inplace == expected);
|
||||
CHECK(expected[json::json_pointer(concat(prefix40, "/leaf"))] == json({0, 1, 2, 3}));
|
||||
CHECK(!expected[json::json_pointer(prefix20)].contains("other"));
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// the value that was resolved before is gone with the member that held it
|
||||
const json gone =
|
||||
{
|
||||
op("test", concat(prefix40, "/leaf/2"), 3),
|
||||
{{"op", "remove"}, {"path", prefix20}},
|
||||
op("test", concat(prefix40, "/leaf/2"), 3)
|
||||
};
|
||||
CHECK_THROWS_AS(doc.patch(gone), json::other_error&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("random sequences of operations give the same result as one operation at a time")
|
||||
{
|
||||
std::mt19937 gen(20261110); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
const std::vector<std::string> names = {"a", "b", "ab", "abc", "a/b", "a~b", "1", "10", "token_longer_than_eight_bytes"};
|
||||
const auto pick = [&gen](const std::size_t n)
|
||||
{
|
||||
return std::uniform_int_distribution<std::size_t>(0, n - 1)(gen);
|
||||
};
|
||||
|
||||
for (int round = 0; round < 300; ++round)
|
||||
{
|
||||
const json start = random_document(gen, names, 7);
|
||||
json current = start;
|
||||
json patch = json::array();
|
||||
std::string previous;
|
||||
|
||||
for (int step = 0; step < 25; ++step)
|
||||
{
|
||||
std::vector<std::string> pointers;
|
||||
collect_pointers(current, "", pointers);
|
||||
if (pointers.empty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// most of the time, stay below a part of the previous path
|
||||
std::vector<std::string> candidates;
|
||||
if (!previous.empty() && pick(10) < 8)
|
||||
{
|
||||
std::string prefix = previous;
|
||||
const std::size_t cut = prefix.find('/', 1 + pick(prefix.size()));
|
||||
prefix = prefix.substr(0, cut == std::string::npos ? prefix.size() : cut);
|
||||
for (const auto& p : pointers)
|
||||
{
|
||||
if (p.compare(0, prefix.size(), prefix) == 0)
|
||||
{
|
||||
candidates.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidates.empty())
|
||||
{
|
||||
candidates = pointers;
|
||||
}
|
||||
const std::string path = candidates[pick(candidates.size())];
|
||||
const json::json_pointer pointer(path);
|
||||
|
||||
json operation;
|
||||
switch (pick(5))
|
||||
{
|
||||
case 0:
|
||||
operation = {{"op", "test"}, {"path", path}, {"value", current[pointer]}};
|
||||
break;
|
||||
case 1:
|
||||
operation = {{"op", "replace"}, {"path", path}, {"value", static_cast<int>(pick(100))}};
|
||||
break;
|
||||
case 2:
|
||||
operation = {{"op", "remove"}, {"path", path}};
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// add below an array or object, which may be the root
|
||||
const json::json_pointer parent_pointer = pick(4) == 0 ? json::json_pointer() : pointer;
|
||||
const std::string parent_path = parent_pointer.to_string();
|
||||
const json& parent = current[parent_pointer];
|
||||
std::string token = "-";
|
||||
if (parent.is_array())
|
||||
{
|
||||
token = pick(3) == 0 ? "-" : std::to_string(pick(parent.size() + 1));
|
||||
}
|
||||
else if (parent.is_object())
|
||||
{
|
||||
token = pick(2) == 0 ? "new" : "ab";
|
||||
}
|
||||
else
|
||||
{
|
||||
// a primitive parent: replace it instead
|
||||
operation = {{"op", "replace"}, {"path", parent_path}, {"value", json::array({1, 2})}};
|
||||
break;
|
||||
}
|
||||
operation = {{"op", "add"}, {"path", concat(parent_path, "/", token)}, {"value", json::object({{"k", {1, 2}}})}};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (operation["path"].get<std::string>().empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// one operation at a time: the cache is empty at the start of each
|
||||
current = current.patch(json::array({operation}));
|
||||
patch.push_back(operation);
|
||||
previous = operation["path"].get<std::string>();
|
||||
}
|
||||
|
||||
CHECK(start.patch(patch) == current);
|
||||
json inplace = start;
|
||||
inplace.patch_inplace(patch);
|
||||
CHECK(inplace == current);
|
||||
|
||||
// the same for a type that keeps the order of the members, which makes
|
||||
// objects that were built in a different order unequal: leave out the
|
||||
// tests of structured values
|
||||
json without_tests = json::array();
|
||||
for (const auto& operation : patch)
|
||||
{
|
||||
if (operation["op"] != "test" || !operation["value"].is_structured())
|
||||
{
|
||||
without_tests.push_back(operation);
|
||||
}
|
||||
}
|
||||
const nlohmann::ordered_json ordered_patch = without_tests;
|
||||
nlohmann::ordered_json ordered_inplace = start;
|
||||
ordered_inplace.patch_inplace(ordered_patch);
|
||||
CHECK(json(ordered_inplace) == current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - time is linear for long paths that share a prefix")
|
||||
{
|
||||
// The time of the patch is compared with the time of a patch that is
|
||||
// linear for sure, because a bound in seconds would depend on the machine:
|
||||
// both test the same two values at the end of paths with 2n tokens. The
|
||||
// second path of the first patch shares n tokens with the first one and
|
||||
// continues for n tokens: the ratio is 1 to 2, and about 60 when every
|
||||
// step is compared with the start of the path. The time that std::clock()
|
||||
// measures is mostly the time of the processor, which a busy machine does
|
||||
// not inflate (on MSVC, it is the time that has passed).
|
||||
constexpr std::size_t n = 25000;
|
||||
|
||||
const auto nest = [](const std::size_t depth, const std::string & inner)
|
||||
{
|
||||
std::string text;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
text += "{\"a\":";
|
||||
}
|
||||
text += inner;
|
||||
text.append(depth, '}');
|
||||
return text;
|
||||
};
|
||||
std::string as;
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
as += "/a";
|
||||
}
|
||||
const std::string deep = nest(n, "1");
|
||||
std::string members = "{\"x\":";
|
||||
members += deep;
|
||||
members += ",\"y\":";
|
||||
members += deep;
|
||||
members += "}";
|
||||
json doc = json::parse(nest(n, members));
|
||||
|
||||
const auto test_op = [](const std::string & path)
|
||||
{
|
||||
return json({{"op", "test"}, {"path", path}, {"value", 1}});
|
||||
};
|
||||
const json shared = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/y", as))});
|
||||
const json same = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/x", as))});
|
||||
|
||||
// the operations do not change the document, which is too deep to copy;
|
||||
// a busy machine may spoil one comparison, hardly two
|
||||
bool linear = false;
|
||||
double t_shared = 0;
|
||||
double t_same = 0;
|
||||
for (int attempt = 0; attempt < 2 && !linear; ++attempt)
|
||||
{
|
||||
t_shared = patch_cpu_time(doc, shared);
|
||||
t_same = (std::max)(patch_cpu_time(doc, same), 1e-3);
|
||||
linear = t_shared < 10 * t_same;
|
||||
}
|
||||
CAPTURE(t_shared)
|
||||
CAPTURE(t_same)
|
||||
CHECK(linear);
|
||||
}
|
||||
@@ -906,3 +906,72 @@ TEST_CASE("json_view node array size limit")
|
||||
d->reserve(cap);
|
||||
CHECK(d->nodes == nodes);
|
||||
}
|
||||
|
||||
TEST_CASE("json_view document_data::holds() (the debug check of read())")
|
||||
{
|
||||
std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
|
||||
const std::string other = "[1,2,3]";
|
||||
// an empty document holds nothing
|
||||
CHECK(!d->holds(other.data(), other.size()));
|
||||
|
||||
SECTION("owned text")
|
||||
{
|
||||
d->owned = R"({"a":"some text that does not fit a short string"})";
|
||||
const char* const b = d->owned.data();
|
||||
CHECK(d->holds(b, 1));
|
||||
CHECK(d->holds(b + 5, 3));
|
||||
CHECK(d->holds(b + d->owned.size() - 1, 1));
|
||||
// also after clear(), which keeps the buffer that the next parse writes
|
||||
d->owned.clear();
|
||||
// (b is only compared with the buffer, never read)
|
||||
CHECK(d->holds(b, 4)); // NOLINT(clang-analyzer-cplusplus.InnerPointer)
|
||||
CHECK(!d->holds(other.data(), other.size()));
|
||||
CHECK(!d->holds(b, 0));
|
||||
}
|
||||
|
||||
SECTION("decoded strings")
|
||||
{
|
||||
d->arena.assign(32, 'x');
|
||||
CHECK(d->holds(d->arena.data() + 31, 1));
|
||||
CHECK(d->holds(d->arena.data() + 31, 8)); // (overlapping the end)
|
||||
CHECK(!d->holds(other.data(), other.size()));
|
||||
}
|
||||
|
||||
SECTION("owned image")
|
||||
{
|
||||
d->owned_image.assign(16, 0);
|
||||
CHECK(d->holds(reinterpret_cast<const char*>(d->owned_image.data()) + 8, 2)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
|
||||
SECTION("current edit buffer")
|
||||
{
|
||||
d->edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
CHECK(!d->holds(other.data(), other.size()));
|
||||
d->edits->texts.emplace_back(new char[16]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-owning-memory)
|
||||
d->edits->text_cap = 16;
|
||||
CHECK(d->holds(d->edits->texts.back().get() + 3, 3));
|
||||
CHECK(!d->holds(other.data(), other.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view read() of inputs the check must accept")
|
||||
{
|
||||
// in a debug build each read() below runs the check; none of the inputs
|
||||
// is held by the document
|
||||
const std::string text = R"({"a":"x\ny","b":[1,2]})";
|
||||
nlohmann::json_document d;
|
||||
d.read(text); // borrowed
|
||||
d.read(text); // the same borrowed text again
|
||||
CHECK(d.root()["b"][1].get<int>() == 2);
|
||||
const std::string copy(d.root()["a"].get_string()); // a decoded string, copied first
|
||||
d.read(std::string(text)); // owned
|
||||
d.read(R"(["x"])");
|
||||
d.read(text);
|
||||
CHECK(copy == "x\ny");
|
||||
nlohmann::json_editable_document e;
|
||||
e.read(text);
|
||||
e.set(e.root(), "a", "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]");
|
||||
const std::string edited(e.root()["a"].get_string()); // an edited string, copied first
|
||||
e.read(edited);
|
||||
CHECK(e.root().size() == 16);
|
||||
}
|
||||
@@ -206,6 +206,44 @@ TEST_CASE("json_view edits: views and values")
|
||||
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
|
||||
}
|
||||
|
||||
SECTION("a NaN written by an edit is not equal to itself")
|
||||
{
|
||||
// as for basic_json: NaN != NaN, so a view of it, of the array that holds it, and the same view twice agree
|
||||
json_editable_document d = json_editable_document::parse("[0, 0, 1.5]");
|
||||
d.set(d.root(), 0, std::numeric_limits<double>::quiet_NaN());
|
||||
d.set(d.root(), 1, std::numeric_limits<double>::quiet_NaN());
|
||||
json_editable_document other = json_editable_document::parse("[0, 0, 1.5]");
|
||||
other.set(other.root(), 0, std::numeric_limits<double>::quiet_NaN());
|
||||
other.set(other.root(), 1, std::numeric_limits<double>::quiet_NaN());
|
||||
const json value = d.root().materialize();
|
||||
|
||||
CHECK_FALSE(value == value);
|
||||
CHECK_FALSE(d.root()[0] == d.root()[0]);
|
||||
CHECK_FALSE(d.root()[0] == d.root()[1]);
|
||||
CHECK_FALSE(d.root() == d.root());
|
||||
CHECK_FALSE(d.root() == other.root());
|
||||
CHECK_FALSE(d.root()[0] == other.root()[0]);
|
||||
CHECK(d.root() != d.root());
|
||||
CHECK(d.root()[0] != d.root()[0]);
|
||||
CHECK_FALSE(d.root() == value);
|
||||
CHECK_FALSE(d.root()[0] == value[0]);
|
||||
// the other elements are equal as before
|
||||
CHECK(d.root()[2] == d.root()[2]);
|
||||
CHECK(d.root()[2] == other.root()[2]);
|
||||
// finite and infinite floats written by an edit stay equal to themselves
|
||||
d.set(d.root(), 0, 0.1);
|
||||
d.set(d.root(), 1, std::numeric_limits<double>::infinity());
|
||||
other.set(other.root(), 0, 0.1);
|
||||
other.set(other.root(), 1, std::numeric_limits<double>::infinity());
|
||||
CHECK(d.root() == d.root());
|
||||
CHECK(d.root() == other.root());
|
||||
CHECK(d.root()[1] == other.root()[1]);
|
||||
// a document that was never edited
|
||||
const json_editable_document untouched = json_editable_document::parse("[0.5, [1, 2]]");
|
||||
CHECK(untouched.root() == untouched.root());
|
||||
CHECK(untouched.root()[1] == untouched.root()[1]);
|
||||
}
|
||||
|
||||
SECTION("numbers of other float types")
|
||||
{
|
||||
// doubles have their own path to the output; other float types are
|
||||
|
||||
@@ -759,6 +759,48 @@ TEST_CASE("json_view images: errors")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("float tokens: an integer token is a float node only if it does not fit 64 bits")
|
||||
{
|
||||
for (const char* const token :
|
||||
{
|
||||
"18446744073709551616", "-9223372036854775809", "100000000000000000000", "-18446744073709551616", "123456789012345678901234567890"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string text = std::string("[") + token + "]";
|
||||
const json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0].is_number_float());
|
||||
const std::vector<std::uint8_t> image = d.save();
|
||||
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||
for (std::size_t pos = token[0] == '-' ? 1 : 0; token[pos] != '\0'; ++pos) // (not the sign: that changes the digits)
|
||||
{
|
||||
for (char digit = '0'; digit <= '9'; ++digit)
|
||||
{
|
||||
std::vector<std::uint8_t> changed = image;
|
||||
changed[at + pos] = static_cast<std::uint8_t>(digit);
|
||||
std::string t = token;
|
||||
t[pos] = digit;
|
||||
CAPTURE(pos)
|
||||
CAPTURE(digit)
|
||||
CAPTURE(t)
|
||||
// the parser stores an integer for the changed token if it is one (and fits)
|
||||
const bool float_node = json::accept(t) && json::parse(t).is_number_float();
|
||||
const std::string result = load_result(changed, image_check::full);
|
||||
if (float_node)
|
||||
{
|
||||
CHECK(result.empty());
|
||||
CHECK(loaded_dump(changed) == json::parse("[" + t + "]").dump());
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(result == check_failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
||||
|
||||
@@ -50,13 +50,18 @@ json nested_object(const std::size_t depth, json leaf)
|
||||
|
||||
TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
||||
{
|
||||
// 200 is past the point where the writers stop recursing, and still
|
||||
// shallow enough that from_* and operator== (which still recurse) are fine.
|
||||
const json deep_array = nested_array(200, json(0));
|
||||
const json deep_object = nested_object(200, json("x"));
|
||||
const json empty_array = nested_array(200, json::array());
|
||||
const json empty_object = nested_object(200, json::object());
|
||||
const json mixed = nested_object(80, nested_array(80, json(true)));
|
||||
// The limit (128 by default) of the levels that the writers recurse into.
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
|
||||
// 72 levels more than the limit (200 by default) is past the point where
|
||||
// the writers stop recursing, and still shallow enough that from_* (which
|
||||
// does not recurse) and operator== (which does, up to the limit) are quick.
|
||||
const json deep_array = nested_array(limit + 72, json(0));
|
||||
const json deep_object = nested_object(limit + 72, json("x"));
|
||||
const json empty_array = nested_array(limit + 72, json::array());
|
||||
const json empty_object = nested_object(limit + 72, json::object());
|
||||
// 2 * (limit / 2 + 16) levels: 160 by default, and more than the limit for every limit
|
||||
const json mixed = nested_object((limit / 2) + 16, nested_array((limit / 2) + 16, json(true)));
|
||||
|
||||
SECTION("roundtrip past the recursion bound")
|
||||
{
|
||||
@@ -95,7 +100,7 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
||||
|
||||
SECTION("the two ways of writing a value meet at the bound")
|
||||
{
|
||||
for (std::size_t depth = 120; depth <= 140; ++depth)
|
||||
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 12; ++depth)
|
||||
{
|
||||
CAPTURE(depth)
|
||||
|
||||
@@ -118,8 +123,8 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
||||
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||
const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}});
|
||||
|
||||
const json deep_ndarray = nested_array(140, ndarray);
|
||||
const json deep_invalid = nested_array(140, invalid);
|
||||
const json deep_ndarray = nested_array(limit + 12, ndarray);
|
||||
const json deep_invalid = nested_array(limit + 12, invalid);
|
||||
|
||||
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
|
||||
CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid);
|
||||
@@ -131,10 +136,10 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
|
||||
// nested one-element arrays around the recursion bound: the exact
|
||||
// bytes a writer produces do not depend on whether it stayed on the
|
||||
// call stack or moved to the heap one partway through
|
||||
// (the limit can be 0, where "one level less" is no level)
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(),
|
||||
nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2
|
||||
limit > 0 ? limit - 1 : 0, limit, limit + 1, limit + 2
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
|
||||
@@ -115,7 +115,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations")
|
||||
// members that the two object types enumerate in different orders.
|
||||
const auto bury = [](nlohmann::ordered_json value)
|
||||
{
|
||||
for (std::size_t i = 0; i < 200; ++i)
|
||||
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i)
|
||||
{
|
||||
value = nlohmann::ordered_json::array({std::move(value)});
|
||||
}
|
||||
@@ -124,7 +124,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations")
|
||||
const auto dig = [](const json & value)
|
||||
{
|
||||
const json* current = &value;
|
||||
for (std::size_t i = 0; i < 200; ++i)
|
||||
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i)
|
||||
{
|
||||
current = ¤t->at(0);
|
||||
}
|
||||
|
||||
@@ -930,14 +930,15 @@ TEST_CASE("JSON Merge Patch diff of deeply nested values")
|
||||
SECTION("the patch is the same past the descent bound")
|
||||
{
|
||||
// depths on either side of the nesting depth up to which merge_diff()
|
||||
// recurses (detail::recursion_depth_limit(), 128); not every depth up
|
||||
// recurses (detail::recursion_depth_limit(), 128 by default); not every depth up
|
||||
// to 300, to keep the test fast under Valgrind
|
||||
std::vector<std::size_t> depths;
|
||||
for (std::size_t depth = 0; depth <= 16; ++depth)
|
||||
{
|
||||
depths.push_back(depth);
|
||||
}
|
||||
for (std::size_t depth = 120; depth <= 136; ++depth)
|
||||
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
|
||||
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth)
|
||||
{
|
||||
depths.push_back(depth);
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ TEST_CASE("JSON Merge Patch on deeply nested values")
|
||||
SECTION("patching past the descent bound gives the same result")
|
||||
{
|
||||
// every depth on either side of where the iterative version takes
|
||||
// over (detail::recursion_depth_limit(), 128)
|
||||
// over (detail::recursion_depth_limit(), 128 by default)
|
||||
for (std::size_t depth = 0; depth <= 300; ++depth)
|
||||
{
|
||||
CAPTURE(depth)
|
||||
@@ -396,7 +396,7 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
|
||||
SECTION("a patch nested past the iterative descent bound aliases *this")
|
||||
{
|
||||
// every depth on either side of where the iterative version takes
|
||||
// over (detail::recursion_depth_limit(), 128); patching *this with
|
||||
// over (detail::recursion_depth_limit(), 128 by default); patching *this with
|
||||
// itself is idempotent, aliased or not
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
|
||||
@@ -1124,7 +1124,7 @@ TEST_CASE("update() on deeply nested values")
|
||||
SECTION("merging past the descent bound gives the same result")
|
||||
{
|
||||
// every depth on either side of where the iterative version takes
|
||||
// over (detail::recursion_depth_limit(), 128)
|
||||
// over (detail::recursion_depth_limit(), 128 by default)
|
||||
for (std::size_t depth = 0; depth <= 300; ++depth)
|
||||
{
|
||||
CAPTURE(depth)
|
||||
@@ -1215,7 +1215,7 @@ TEST_CASE("update() with an argument that aliases *this (#5641)")
|
||||
SECTION("nested past the iterative descent bound aliases *this")
|
||||
{
|
||||
// every depth on either side of where the iterative version takes
|
||||
// over (detail::recursion_depth_limit(), 128); merging *this into
|
||||
// over (detail::recursion_depth_limit(), 128 by default); merging *this into
|
||||
// itself is idempotent, aliased or not
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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 <functional>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// JSON_RECURSION_DEPTH_LIMIT changes only how much call stack the walks over a
|
||||
// value use, never what they return. This file checks both: the macro is
|
||||
// honoured (the number of levels the containers handle themselves is the limit)
|
||||
// and every walk returns what it returns with the default limit. The expected
|
||||
// results are built here, without the library, so that the same file passes for
|
||||
// every limit; tests/CMakeLists.txt builds it with limits 0, 1, and 254 besides
|
||||
// the default. Values are nested up to 300 levels (and past every limit, so that
|
||||
// the explicit stack of every walk is exercised, even for the limit of 254) and
|
||||
// are built and taken apart without recursion. Keep the depths moderate: walks
|
||||
// like diff and dump with indentation are quadratic in the depth.
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t limit()
|
||||
{
|
||||
return nlohmann::detail::recursion_depth_limit();
|
||||
}
|
||||
|
||||
// a value nested as @a kinds says, outermost level first: 'a' is an array of
|
||||
// one element, 'o' an object with the single key "a"; @a leaf is innermost
|
||||
json build(const std::string& kinds, json leaf)
|
||||
{
|
||||
json j = std::move(leaf);
|
||||
for (std::size_t i = kinds.size(); i-- > 0;)
|
||||
{
|
||||
if (kinds[i] == 'a')
|
||||
{
|
||||
json a = json::array();
|
||||
a.push_back(std::move(j));
|
||||
j = std::move(a);
|
||||
}
|
||||
else
|
||||
{
|
||||
json o = json::object();
|
||||
o["a"] = std::move(j);
|
||||
j = std::move(o);
|
||||
}
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
std::string repeat(const std::string& s, std::size_t n)
|
||||
{
|
||||
std::string result;
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
result += s;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// the nesting of the given shape and depth: arrays, objects, or both alternating
|
||||
std::string make_kinds(char shape, std::size_t depth)
|
||||
{
|
||||
std::string kinds;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
if (shape == 'm')
|
||||
{
|
||||
kinds += i % 2 == 0 ? 'a' : 'o';
|
||||
}
|
||||
else
|
||||
{
|
||||
kinds += shape;
|
||||
}
|
||||
}
|
||||
return kinds;
|
||||
}
|
||||
|
||||
std::string expected_compact(const std::string& kinds, const std::string& leaf)
|
||||
{
|
||||
std::string s;
|
||||
for (const char kind : kinds)
|
||||
{
|
||||
s += kind == 'a' ? "[" : "{\"a\":";
|
||||
}
|
||||
s += leaf;
|
||||
for (std::size_t i = kinds.size(); i-- > 0;)
|
||||
{
|
||||
s += kinds[i] == 'a' ? "]" : "}";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// dump(1)
|
||||
std::string expected_indented(const std::string& kinds, const std::string& leaf)
|
||||
{
|
||||
std::string s;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i)
|
||||
{
|
||||
s += kinds[i] == 'a' ? "[\n" : "{\n";
|
||||
s += std::string(i + 1, ' ');
|
||||
if (kinds[i] == 'o')
|
||||
{
|
||||
s += "\"a\": ";
|
||||
}
|
||||
}
|
||||
s += leaf;
|
||||
for (std::size_t i = kinds.size(); i-- > 0;)
|
||||
{
|
||||
s += "\n" + std::string(i, ' ') + (kinds[i] == 'a' ? "]" : "}");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// the JSON pointer to the innermost value
|
||||
std::string pointer(const std::string& kinds)
|
||||
{
|
||||
std::string s;
|
||||
for (const char kind : kinds)
|
||||
{
|
||||
s += kind == 'a' ? "/0" : "/a";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// the binary formats: the bytes that open and close a level, and the leaf 1
|
||||
std::vector<std::uint8_t> expected_cbor(const std::string& kinds)
|
||||
{
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (const char kind : kinds)
|
||||
{
|
||||
if (kind == 'a')
|
||||
{
|
||||
bytes.push_back(0x81);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes.push_back(0xA1);
|
||||
bytes.push_back(0x61);
|
||||
bytes.push_back('a');
|
||||
}
|
||||
}
|
||||
bytes.push_back(0x01);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> expected_msgpack(const std::string& kinds)
|
||||
{
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (const char kind : kinds)
|
||||
{
|
||||
if (kind == 'a')
|
||||
{
|
||||
bytes.push_back(0x91);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes.push_back(0x81);
|
||||
bytes.push_back(0xA1);
|
||||
bytes.push_back('a');
|
||||
}
|
||||
}
|
||||
bytes.push_back(0x01);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> expected_ubjson(const std::string& kinds)
|
||||
{
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (const char kind : kinds)
|
||||
{
|
||||
if (kind == 'a')
|
||||
{
|
||||
bytes.push_back('[');
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes.push_back('{');
|
||||
bytes.push_back('i');
|
||||
bytes.push_back(1);
|
||||
bytes.push_back('a');
|
||||
}
|
||||
}
|
||||
bytes.push_back('i');
|
||||
bytes.push_back(1);
|
||||
for (std::size_t i = kinds.size(); i-- > 0;)
|
||||
{
|
||||
bytes.push_back(kinds[i] == 'a' ? ']' : '}');
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// the depths worth trying for the limit: either side of it, twice it (if that
|
||||
// is not too deep to be quick), and a value nested well past all limits
|
||||
std::vector<std::size_t> depths()
|
||||
{
|
||||
std::vector<std::size_t> result;
|
||||
result.push_back(1);
|
||||
result.push_back(2);
|
||||
result.push_back(limit() > 0 ? limit() - 1 : 3);
|
||||
result.push_back(limit());
|
||||
result.push_back(limit() + 1);
|
||||
if ((2 * limit()) + 5 <= 300)
|
||||
{
|
||||
result.push_back((2 * limit()) + 5);
|
||||
}
|
||||
result.push_back(300);
|
||||
std::sort(result.begin(), result.end());
|
||||
result.erase(std::unique(result.begin(), result.end()), result.end());
|
||||
result.erase(std::remove(result.begin(), result.end(), static_cast<std::size_t>(0)), result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
// the call for every shape and depth
|
||||
void for_each_value(const std::function<void(const std::string&)>& check)
|
||||
{
|
||||
const std::string shapes = "aom";
|
||||
for (const char shape : shapes)
|
||||
{
|
||||
for (const std::size_t depth : depths())
|
||||
{
|
||||
CAPTURE(shape)
|
||||
CAPTURE(depth)
|
||||
check(make_kinds(shape, depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An array type that counts how many of its copies are being made at once. A
|
||||
// copy of a value copies one level at a time through the array type until the
|
||||
// limit has been reached, so the peak is the number of levels that were copied
|
||||
// with the call stack.
|
||||
std::size_t copies_going_on = 0;
|
||||
std::size_t copies_peak = 0;
|
||||
std::size_t equal_going_on = 0;
|
||||
std::size_t equal_peak = 0;
|
||||
|
||||
template<class T, class Allocator = std::allocator<T>>
|
||||
class counting_vector : public std::vector<T, Allocator>
|
||||
{
|
||||
public:
|
||||
counting_vector() = default;
|
||||
|
||||
template<class InputIt>
|
||||
counting_vector(InputIt first, InputIt last) : std::vector<T, Allocator>(first, last) {}
|
||||
|
||||
counting_vector(const counting_vector& other) : std::vector<T, Allocator>()
|
||||
{
|
||||
++copies_going_on;
|
||||
copies_peak = (std::max)(copies_peak, copies_going_on);
|
||||
this->std::vector<T, Allocator>::operator=(static_cast<const std::vector<T, Allocator>&>(other));
|
||||
--copies_going_on;
|
||||
}
|
||||
|
||||
counting_vector(counting_vector&&) = default;
|
||||
counting_vector& operator=(const counting_vector&) = default;
|
||||
counting_vector& operator=(counting_vector&&) = default;
|
||||
~counting_vector() = default;
|
||||
|
||||
friend bool operator==(const counting_vector& lhs, const counting_vector& rhs)
|
||||
{
|
||||
++equal_going_on;
|
||||
equal_peak = (std::max)(equal_peak, equal_going_on);
|
||||
const bool result = static_cast<const std::vector<T, Allocator>&>(lhs) == static_cast<const std::vector<T, Allocator>&>(rhs);
|
||||
--equal_going_on;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
using counting_json = json::with_array_t<counting_vector>;
|
||||
|
||||
// A base class whose copy constructor copies a whole value of the same
|
||||
// specialization when it copies a marked element, as user code may do from a
|
||||
// hook. That copy runs while the copy that it is part of is going on.
|
||||
struct copy_hook // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
|
||||
{
|
||||
int mark = 0;
|
||||
|
||||
copy_hook() = default;
|
||||
copy_hook(const copy_hook& other);
|
||||
copy_hook(copy_hook&&) = default;
|
||||
copy_hook& operator=(const copy_hook&) = default;
|
||||
copy_hook& operator=(copy_hook&&) = default;
|
||||
};
|
||||
|
||||
using hook_json = nlohmann::basic_json<std::map, counting_vector, std::string, bool, std::int64_t, std::uint64_t, double,
|
||||
std::allocator, nlohmann::adl_serializer, std::vector<std::uint8_t>, copy_hook>;
|
||||
|
||||
const hook_json* hook_copy_source = nullptr;
|
||||
int hook_copies_going_on = 0;
|
||||
|
||||
copy_hook::copy_hook(const copy_hook& other) : mark(other.mark)
|
||||
{
|
||||
// the value that is copied is marked, too: its copy copies it again, once
|
||||
if (other.mark == 1 && hook_copy_source != nullptr && hook_copies_going_on < 2)
|
||||
{
|
||||
++hook_copies_going_on;
|
||||
const hook_json nested = *hook_copy_source; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
static_cast<void>(nested);
|
||||
--hook_copies_going_on;
|
||||
}
|
||||
}
|
||||
|
||||
// an array nested @a depth levels around the integer 1, which is marked if @a marked
|
||||
hook_json hook_nested(std::size_t depth, bool marked)
|
||||
{
|
||||
hook_json j = 1;
|
||||
if (marked)
|
||||
{
|
||||
static_cast<copy_hook&>(j).mark = 1;
|
||||
}
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
hook_json a = hook_json::array();
|
||||
a.push_back(std::move(j));
|
||||
j = std::move(a);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// the macro is the limit, and is in the range the library supports
|
||||
static_assert(nlohmann::detail::recursion_depth_limit() == static_cast<std::size_t>(JSON_RECURSION_DEPTH_LIMIT),
|
||||
"the library does not use JSON_RECURSION_DEPTH_LIMIT");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit() <= 254, "the nesting depth must fit in a byte");
|
||||
|
||||
// the macro must not be a character or bool: every character type is refused
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<bool>::value, "bool is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char>::value, "char is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<signed char>::value, "signed char is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<unsigned char>::value, "unsigned char is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<wchar_t>::value, "wchar_t is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char16_t>::value, "char16_t is refused");
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char32_t>::value, "char32_t is refused");
|
||||
#if defined(__cpp_char8_t)
|
||||
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char8_t>::value, "char8_t is refused");
|
||||
#endif
|
||||
static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool<int>::value, "int is accepted");
|
||||
static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool<unsigned long long>::value, "unsigned long long is accepted");
|
||||
|
||||
TEST_CASE("JSON_RECURSION_DEPTH_LIMIT is honoured")
|
||||
{
|
||||
SECTION("copying and comparing stop descending at the limit")
|
||||
{
|
||||
// copy and compare a value nested deeper than the limit, with the array type that counts
|
||||
counting_json inner = 1;
|
||||
for (std::size_t i = 0; i < (2 * limit()) + 5; ++i)
|
||||
{
|
||||
counting_json a = counting_json::array();
|
||||
a.push_back(std::move(inner));
|
||||
inner = std::move(a);
|
||||
}
|
||||
|
||||
copies_going_on = copies_peak = 0;
|
||||
const counting_json copy = inner;
|
||||
CHECK(copies_going_on == 0);
|
||||
|
||||
equal_going_on = equal_peak = 0;
|
||||
CHECK((inner == copy));
|
||||
CHECK(equal_going_on == 0);
|
||||
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// without the counters of the levels, no level is descended into
|
||||
CHECK(copies_peak == 0);
|
||||
CHECK(equal_peak == 0);
|
||||
#else
|
||||
// copying descends into as many levels as the limit says, and nothing further
|
||||
CHECK(copies_peak == limit());
|
||||
CHECK(equal_peak == limit());
|
||||
#endif
|
||||
|
||||
// a value that is not nested deeper than the limit is not cut short
|
||||
const std::size_t shallow = limit() > 0 ? limit() : 1;
|
||||
counting_json small = 1;
|
||||
for (std::size_t i = 0; i < shallow; ++i)
|
||||
{
|
||||
counting_json a = counting_json::array();
|
||||
a.push_back(std::move(small));
|
||||
small = std::move(a);
|
||||
}
|
||||
copies_going_on = copies_peak = 0;
|
||||
const counting_json small_copy = small;
|
||||
CHECK(copies_going_on == 0);
|
||||
#ifndef JSON_NO_THREAD_LOCAL
|
||||
CHECK(copies_peak == limit());
|
||||
#endif
|
||||
CHECK(small_copy.dump() == expected_compact(make_kinds('a', shallow), "1"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("a copy made while a copy is going on is bounded, too")
|
||||
{
|
||||
// The innermost element of the value is marked, and copying it (which happens
|
||||
// after limit() levels have been entered, when the copy continues without the
|
||||
// call stack) copies the value again, and that copy once more. Those copies
|
||||
// must not be given limit() levels of their own: the count of the levels entered
|
||||
// never exceeds the limit, not even by one, which for the largest limit would
|
||||
// wrap the byte that holds it around to 0.
|
||||
const hook_json marked = hook_nested((2 * limit()) + 5, true);
|
||||
hook_copy_source = &marked;
|
||||
|
||||
copies_going_on = copies_peak = 0;
|
||||
const hook_json copy = marked; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
hook_copy_source = nullptr;
|
||||
CHECK(copies_going_on == 0);
|
||||
CHECK(static_cast<const copy_hook&>(copy).mark == 0); // (the top level is not marked)
|
||||
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
CHECK(copies_peak == 0);
|
||||
#else
|
||||
CHECK(copies_peak == limit());
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("walks over values nested around the recursion depth limit")
|
||||
{
|
||||
SECTION("copying and comparing")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const json b = a; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK((a == b));
|
||||
CHECK_FALSE((a != b));
|
||||
CHECK_FALSE((a < b));
|
||||
CHECK((a <= b));
|
||||
CHECK((a >= b));
|
||||
CHECK_FALSE((a > b));
|
||||
|
||||
json assigned;
|
||||
assigned = a;
|
||||
CHECK((assigned == a));
|
||||
|
||||
// differing only in the innermost value
|
||||
const json c = build(kinds, 2);
|
||||
CHECK((a != c));
|
||||
CHECK((a < c));
|
||||
CHECK((c > a));
|
||||
CHECK_FALSE((c < a));
|
||||
CHECK_FALSE((a == c));
|
||||
|
||||
// a same-shape array or object that is one level shallower
|
||||
const json shallower = build(kinds.substr(1), 1);
|
||||
CHECK((shallower != a));
|
||||
const json outer = build(kinds.substr(0, 1), 1);
|
||||
CHECK((outer != a) == (kinds.size() > 1));
|
||||
|
||||
const json moved = std::move(assigned);
|
||||
CHECK((moved == a));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("serializing")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const std::string compact = expected_compact(kinds, "1");
|
||||
const std::string indented = expected_indented(kinds, "1");
|
||||
|
||||
CHECK(a.dump() == compact);
|
||||
CHECK(a.dump(1) == indented);
|
||||
|
||||
std::ostringstream compact_stream;
|
||||
compact_stream << a;
|
||||
CHECK(compact_stream.str() == compact);
|
||||
std::ostringstream indented_stream;
|
||||
indented_stream << std::setw(1) << a;
|
||||
CHECK(indented_stream.str() == indented);
|
||||
|
||||
CHECK((json::parse(compact) == a));
|
||||
CHECK((json::parse(indented) == a));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("hashing")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const std::hash<json> hash{};
|
||||
CHECK(hash(a) == hash(json(a)));
|
||||
CHECK(hash(a) == hash(json::parse(expected_compact(kinds, "1"))));
|
||||
CHECK(hash(a) != hash(build(kinds, 2)));
|
||||
CHECK(hash(a) != hash(build(kinds.substr(1), 1)));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("diff and patch")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const json b = build(kinds, 2);
|
||||
|
||||
const json difference = json::diff(a, b);
|
||||
CHECK(difference.dump() == "[{\"op\":\"replace\",\"path\":\"" + pointer(kinds) + "\",\"value\":2}]");
|
||||
CHECK((a.patch(difference) == b));
|
||||
CHECK((json::diff(a, a) == json::array()));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("flatten and unflatten")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const json flat = a.flatten();
|
||||
CHECK(flat.dump() == "{\"" + pointer(kinds) + "\":1}");
|
||||
CHECK((flat.unflatten() == a));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("merge_patch and update")
|
||||
{
|
||||
for (const std::size_t depth : depths())
|
||||
{
|
||||
CAPTURE(depth)
|
||||
const std::string kinds = make_kinds('o', depth);
|
||||
const std::string all_but_leaf = repeat("{\"a\":", depth - 1);
|
||||
|
||||
// the patch removes the innermost member, or replaces it
|
||||
json target = build(kinds, 1);
|
||||
target.merge_patch(build(kinds, json(nullptr)));
|
||||
CHECK(target.dump() == all_but_leaf + "{}" + repeat("}", depth - 1));
|
||||
|
||||
target = build(kinds, 1);
|
||||
target.merge_patch(build(kinds, 2));
|
||||
CHECK(target.dump() == expected_compact(kinds, "2"));
|
||||
|
||||
// merging the objects of two values nested alike, and replacing the first level
|
||||
const json base = build(kinds.substr(1), json::parse("{\"x\":1}"));
|
||||
const json other = build(kinds.substr(1), json::parse("{\"y\":2}"));
|
||||
json merged = json{{"a", base}};
|
||||
merged.update(json{{"a", other}}, true);
|
||||
CHECK(merged.dump() == expected_compact(kinds, "{\"x\":1,\"y\":2}"));
|
||||
|
||||
json replaced = json{{"a", base}};
|
||||
replaced.update(json{{"a", other}});
|
||||
CHECK(replaced.dump() == expected_compact(kinds, "{\"y\":2}"));
|
||||
|
||||
// update from a range
|
||||
json from_range = json{{"a", base}};
|
||||
const json source = json{{"a", other}};
|
||||
from_range.update(source.begin(), source.end(), true);
|
||||
CHECK((from_range == merged));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("binary formats")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
|
||||
const std::vector<std::uint8_t> cbor = json::to_cbor(a);
|
||||
CHECK(cbor == expected_cbor(kinds));
|
||||
CHECK((json::from_cbor(cbor) == a));
|
||||
|
||||
const std::vector<std::uint8_t> msgpack = json::to_msgpack(a);
|
||||
CHECK(msgpack == expected_msgpack(kinds));
|
||||
CHECK((json::from_msgpack(msgpack) == a));
|
||||
|
||||
const std::vector<std::uint8_t> ubjson = json::to_ubjson(a);
|
||||
CHECK(ubjson == expected_ubjson(kinds));
|
||||
CHECK((json::from_ubjson(ubjson) == a));
|
||||
|
||||
CHECK((json::from_bjdata(json::to_bjdata(a)) == a));
|
||||
CHECK((json::from_bon8(json::to_bon8(a)) == a));
|
||||
|
||||
// BSON documents are objects
|
||||
const json document = json{{"a", a}};
|
||||
CHECK((json::from_bson(json::to_bson(document)) == document));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("conversion to another specialization")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
const json a = build(kinds, 1);
|
||||
const nlohmann::ordered_json ordered = a;
|
||||
CHECK(ordered.dump() == expected_compact(kinds, "1"));
|
||||
const json back = ordered;
|
||||
CHECK((back == a));
|
||||
});
|
||||
}
|
||||
|
||||
SECTION("destroying")
|
||||
{
|
||||
for_each_value([](const std::string & kinds)
|
||||
{
|
||||
json a = build(kinds, 1);
|
||||
a = nullptr;
|
||||
CHECK(a.is_null());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1554,8 +1554,10 @@ TEST_CASE("regression tests 1")
|
||||
SECTION("a missing member of an object")
|
||||
{
|
||||
my_json j = make();
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// (set by the user, or by the library for clang with MinGW)
|
||||
#if defined(JSON_NO_THREAD_LOCAL) || JSON_RECURSION_DEPTH_LIMIT < 2
|
||||
// (set by the user, or by the library for clang with MinGW; or fewer than two
|
||||
// levels are copied by the object type itself, as for a recursion depth limit
|
||||
// of 0 or 1: the objects below the outermost one are the second level)
|
||||
// the objects below the outermost one are not copied by their
|
||||
// object_t here, but member by member, which keeps their
|
||||
// timestamps correct: "c" is not mistaken for "b" and is inserted
|
||||
|
||||
Reference in new issue
Block a user