mirror of
https://github.com/nlohmann/json.git
synced 2026-10-09 16:07:14 +00:00
Two changes roughly halve the CPU time needed to build the unit tests (clang 21: ~300 s -> ~150 s, GCC 16: ~750-820 s -> ~395-420 s, Debug): - Move the C++14/17/20-dependent tests into separate unit-<name>-cpp<N>.cpp files. A test file is built for every standard whose JSON_HAS_CPP_<N> macro it mentions, so far whole large files were rebuilt for C++14/17/20 because of a few #ifdef sections. The main files are now built for C++11 only; the ci_test_*_cxx<N> jobs still build every file for every standard. - Add the CMake option JSON_TestUnityBuild (ON by default, OFF with MinGW): compatible test files are compiled in batches as one translation unit so they share the template instantiations of the library. Each file keeps its own CTest test, which runs the batch executable filtered to that file's test cases. The binary-format tests form an explicit group; the rest is batched by JSON_TestUnityBatchSize. Fix the name clashes that merging files exposed, document the rules for test files in tests/README.md, and point CONTRIBUTING.md to it. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
788 lines
26 KiB
C++
788 lines
26 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_TESTS_PRIVATE
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <valarray>
|
|
|
|
namespace
|
|
{
|
|
// special test case to check if memory is leaked if constructor throws
|
|
template<class T>
|
|
struct bad_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
bad_allocator() = default;
|
|
template<class U> bad_allocator(const bad_allocator<U>& /*unused*/) { }
|
|
|
|
template<class... Args>
|
|
[[noreturn]] void construct(T* /*unused*/, Args&& ... /*unused*/) // NOLINT(cppcoreguidelines-missing-std-forward)
|
|
{
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = bad_allocator<U>;
|
|
};
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("get_allocator")
|
|
{
|
|
const auto alloc = nlohmann::json::get_allocator();
|
|
CHECK(alloc == std::allocator<nlohmann::json>());
|
|
}
|
|
|
|
TEST_CASE("bad_alloc")
|
|
{
|
|
SECTION("bad_alloc")
|
|
{
|
|
// create JSON type using the throwing allocator
|
|
using bad_json = nlohmann::json::with_allocator_t<bad_allocator>;
|
|
|
|
// creating an object should throw
|
|
CHECK_THROWS_AS(bad_json(bad_json::value_t::object), std::bad_alloc&);
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
bool next_construct_fails = false;
|
|
bool next_destroy_fails = false;
|
|
bool next_deallocate_fails = false;
|
|
|
|
template<class T>
|
|
struct my_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
template<class... Args>
|
|
void construct(T* p, Args&& ... args)
|
|
{
|
|
if (next_construct_fails)
|
|
{
|
|
next_construct_fails = false;
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
::new (reinterpret_cast<void*>(p)) T(std::forward<Args>(args)...);
|
|
}
|
|
|
|
void deallocate(T* p, std::size_t n)
|
|
{
|
|
if (next_deallocate_fails)
|
|
{
|
|
next_deallocate_fails = false;
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
std::allocator<T>::deallocate(p, n);
|
|
}
|
|
|
|
void destroy(T* p)
|
|
{
|
|
if (next_destroy_fails)
|
|
{
|
|
next_destroy_fails = false;
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
static_cast<void>(p); // fix MSVC's C4100 warning
|
|
p->~T();
|
|
}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = my_allocator<U>;
|
|
};
|
|
};
|
|
|
|
// allows deletion of raw pointer, usually hold by json_value
|
|
template<class T>
|
|
void my_allocator_clean_up(T* p)
|
|
{
|
|
assert(p != nullptr);
|
|
my_allocator<T> alloc;
|
|
alloc.destroy(p);
|
|
alloc.deallocate(p, 1);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("controlled bad_alloc")
|
|
{
|
|
// create JSON type using the throwing allocator
|
|
using my_json = nlohmann::json::with_allocator_t<my_allocator>;
|
|
|
|
SECTION("class json_value")
|
|
{
|
|
SECTION("json_value(value_t)")
|
|
{
|
|
SECTION("object")
|
|
{
|
|
next_construct_fails = false;
|
|
auto t = my_json::value_t::object;
|
|
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).object));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
SECTION("array")
|
|
{
|
|
next_construct_fails = false;
|
|
auto t = my_json::value_t::array;
|
|
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).array));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
SECTION("string")
|
|
{
|
|
next_construct_fails = false;
|
|
auto t = my_json::value_t::string;
|
|
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).string));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
}
|
|
|
|
SECTION("json_value(const string_t&)")
|
|
{
|
|
next_construct_fails = false;
|
|
const my_json::string_t v("foo");
|
|
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(v).string));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json::json_value(v), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
}
|
|
|
|
SECTION("class basic_json")
|
|
{
|
|
SECTION("basic_json(const CompatibleObjectType&)")
|
|
{
|
|
next_construct_fails = false;
|
|
const std::map<std::string, std::string> v {{"foo", "bar"}};
|
|
CHECK_NOTHROW(my_json(v));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json(v), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
SECTION("basic_json(const CompatibleArrayType&)")
|
|
{
|
|
next_construct_fails = false;
|
|
const std::vector<std::string> v {"foo", "bar", "baz"};
|
|
CHECK_NOTHROW(my_json(v));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json(v), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
SECTION("basic_json(const typename string_t::value_type*)")
|
|
{
|
|
next_construct_fails = false;
|
|
CHECK_NOTHROW(my_json("foo"));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json("foo"), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
SECTION("basic_json(const typename string_t::value_type*)")
|
|
{
|
|
next_construct_fails = false;
|
|
const std::string s("foo");
|
|
CHECK_NOTHROW(my_json(s));
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(my_json(s), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
SECTION("basic_json(const basic_json&) of a deeply nested value (#5387)")
|
|
{
|
|
// Copying a value nested deeper than the descent bound builds the
|
|
// copy from the top down: every value whose own copy has not been
|
|
// made yet stays a null value until it is. Failing an allocation
|
|
// part-way through is what proves such a half-built copy can still
|
|
// be destroyed.
|
|
//
|
|
// Which path the failure lands in depends on the build: the first
|
|
// allocation of a copy belongs to the outermost level, so here it
|
|
// is the descending one. Built with JSON_NO_THREAD_LOCAL - as the
|
|
// ci_test_no_thread_local target builds the whole suite - no
|
|
// descent is made at all and the very same failure lands in the
|
|
// iterative path instead, part-way through its worklist.
|
|
const auto check_deep_copy = [](bool objects)
|
|
{
|
|
CAPTURE(objects)
|
|
|
|
next_construct_fails = false;
|
|
|
|
// deeper than the 128 levels the copy constructor descends into
|
|
const std::size_t depth = 300;
|
|
|
|
my_json j = 1;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
if (objects)
|
|
{
|
|
my_json wrapper = my_json::object();
|
|
wrapper["a"] = std::move(j);
|
|
j = std::move(wrapper);
|
|
}
|
|
else
|
|
{
|
|
j = my_json::array({std::move(j)});
|
|
}
|
|
}
|
|
|
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
CHECK_NOTHROW(my_json(j));
|
|
|
|
next_construct_fails = true;
|
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
CHECK_THROWS_AS(my_json(j), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
};
|
|
|
|
check_deep_copy(false);
|
|
check_deep_copy(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// counts every allocation made on behalf of a basic_json value (of its own
|
|
// object_t/array_t/string_t/binary_t or of its own type), and can be told to
|
|
// fail one of them: the n-th call to allocate() throws std::bad_alloc instead
|
|
// of allocating, whichever type it is allocating for
|
|
std::size_t alloc_call_count = 0;
|
|
long fail_at_alloc_call = -1; // -1: never fail
|
|
|
|
template<class T>
|
|
struct nth_alloc_fails_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
T* allocate(std::size_t n)
|
|
{
|
|
const auto index = alloc_call_count++;
|
|
if (fail_at_alloc_call >= 0 && index == static_cast<std::size_t>(fail_at_alloc_call))
|
|
{
|
|
throw std::bad_alloc();
|
|
}
|
|
return std::allocator<T>::allocate(n);
|
|
}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = nth_alloc_fails_allocator<U>;
|
|
};
|
|
};
|
|
|
|
// builds a value nested more than 128 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
|
|
template<class BasicJsonType>
|
|
void check_deep_copy_survives_failing_allocation(bool nest_objects)
|
|
{
|
|
CAPTURE(nest_objects)
|
|
|
|
fail_at_alloc_call = -1;
|
|
|
|
// [[[ ... [1] ... ]]], or the same nesting with objects, 130 levels deep
|
|
BasicJsonType src = 1;
|
|
for (std::size_t i = 0; i < 130; ++i)
|
|
{
|
|
if (nest_objects)
|
|
{
|
|
BasicJsonType wrapper = BasicJsonType::object();
|
|
wrapper["a"] = std::move(src);
|
|
src = std::move(wrapper);
|
|
}
|
|
else
|
|
{
|
|
src = BasicJsonType::array({std::move(src)});
|
|
}
|
|
}
|
|
|
|
const std::string original_dump = src.dump();
|
|
|
|
// first measure how many allocations an unhindered copy takes
|
|
alloc_call_count = 0;
|
|
{
|
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is measured
|
|
const BasicJsonType measure(src);
|
|
}
|
|
const std::size_t total_allocations = alloc_call_count;
|
|
REQUIRE(total_allocations > 0);
|
|
REQUIRE(src.dump() == original_dump);
|
|
|
|
// let the 0th, 1st, 2nd, ... allocation of the copy fail in turn; every
|
|
// such copy must throw std::bad_alloc rather than crash, and the source
|
|
// must come out exactly as it went in
|
|
for (std::size_t n = 0; n < total_allocations; ++n)
|
|
{
|
|
CAPTURE(n)
|
|
alloc_call_count = 0;
|
|
fail_at_alloc_call = static_cast<long>(n);
|
|
|
|
CHECK_THROWS_AS(BasicJsonType(src), std::bad_alloc&);
|
|
|
|
fail_at_alloc_call = -1;
|
|
CHECK(src.dump() == original_dump);
|
|
}
|
|
|
|
// once no allocation is made to fail, the copy itself must succeed
|
|
fail_at_alloc_call = -1;
|
|
const BasicJsonType copy(src);
|
|
CHECK(copy.dump() == original_dump);
|
|
CHECK(src.dump() == original_dump);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("copy of a deeply nested value survives a failing allocation (#5640)")
|
|
{
|
|
// With iterator debugging (MSVC STL debug builds, also used by clang-cl),
|
|
// containers allocate a debug proxy through the allocator inside their
|
|
// noexcept move constructors, so failing that allocation terminates the
|
|
// program instead of throwing std::bad_alloc. Nothing to check there.
|
|
#if !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
|
SECTION("std::map-backed object_t")
|
|
{
|
|
using bad_alloc_json = nlohmann::json::with_allocator_t<nth_alloc_fails_allocator>;
|
|
|
|
check_deep_copy_survives_failing_allocation<bad_alloc_json>(false);
|
|
check_deep_copy_survives_failing_allocation<bad_alloc_json>(true);
|
|
}
|
|
|
|
SECTION("ordered_map-backed object_t")
|
|
{
|
|
using bad_alloc_ordered_json = nlohmann::ordered_json::with_allocator_t<nth_alloc_fails_allocator>;
|
|
|
|
check_deep_copy_survives_failing_allocation<bad_alloc_ordered_json>(false);
|
|
check_deep_copy_survives_failing_allocation<bad_alloc_ordered_json>(true);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// counts the allocations of pairs with a non-const first member: the object
|
|
// types store std::pair<const Key, T>, so only the scratch space of the
|
|
// iterative deep copy allocates std::pair<Key, T>
|
|
std::size_t scratch_pair_allocations = 0;
|
|
|
|
template<class T>
|
|
struct is_scratch_pair : std::false_type {};
|
|
|
|
template<class K, class V>
|
|
struct is_scratch_pair<std::pair<K, V>> : std::integral_constant < bool, !std::is_const<K>::value > {};
|
|
|
|
template<class T>
|
|
struct scratch_counting_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
T* allocate(std::size_t n)
|
|
{
|
|
if (is_scratch_pair<T>::value)
|
|
{
|
|
++scratch_pair_allocations;
|
|
}
|
|
return std::allocator<T>::allocate(n);
|
|
}
|
|
|
|
#ifdef __cpp_lib_allocate_at_least
|
|
// std::allocator<T>::allocate_at_least would bypass the counting, and
|
|
// libc++'s containers prefer it over allocate from C++23 on
|
|
auto allocate_at_least(std::size_t n)
|
|
{
|
|
if (is_scratch_pair<T>::value)
|
|
{
|
|
++scratch_pair_allocations;
|
|
}
|
|
return std::allocator<T>::allocate_at_least(n);
|
|
}
|
|
#endif
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = scratch_counting_allocator<U>;
|
|
};
|
|
};
|
|
} // namespace
|
|
|
|
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
|
|
counting_json j = 1;
|
|
for (std::size_t i = 0; i < 300; ++i)
|
|
{
|
|
counting_json wrapper = counting_json::object();
|
|
wrapper["a"] = std::move(j);
|
|
j = std::move(wrapper);
|
|
}
|
|
|
|
scratch_pair_allocations = 0;
|
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
|
|
const counting_json copy(j);
|
|
CHECK(scratch_pair_allocations > 0);
|
|
CHECK(copy == j);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// the number of constructions countdown_allocator lets happen, including the
|
|
// one that fails; 0 means none ever fails
|
|
std::size_t constructions_until_failure = 0;
|
|
|
|
template<class T>
|
|
struct countdown_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
template<class U, class... Args>
|
|
void construct(U* p, Args&& ... args)
|
|
{
|
|
if (constructions_until_failure != 0)
|
|
{
|
|
--constructions_until_failure;
|
|
if (constructions_until_failure == 0)
|
|
{
|
|
throw std::bad_alloc();
|
|
}
|
|
}
|
|
|
|
::new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
|
|
}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = countdown_allocator<U>;
|
|
};
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("converting a deeply nested value from another specialization fails cleanly (#5650)")
|
|
{
|
|
// MSVC 2015's debug STL constructs the containers' debug proxies through
|
|
// the allocator in noexcept constructors, so a failing construction crashes
|
|
// the program there instead of throwing std::bad_alloc. Nothing to check.
|
|
#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
|
|
json j = {1, "two", {{"three", 3}}};
|
|
for (std::size_t i = 0; i < 150; ++i)
|
|
{
|
|
j = json{{"a", json::array({j, "sibling"})}};
|
|
}
|
|
|
|
// Fail every construction in turn. Each failure has to reach the caller,
|
|
// and everything built until then has to be destroyed cleanly.
|
|
std::size_t failures = 0;
|
|
for (std::size_t n = 1;; ++n)
|
|
{
|
|
constructions_until_failure = n;
|
|
try
|
|
{
|
|
const countdown_json converted = j;
|
|
constructions_until_failure = 0;
|
|
CHECK(converted.dump() == j.dump());
|
|
break;
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
++failures;
|
|
}
|
|
}
|
|
CHECK(failures > 0);
|
|
#endif
|
|
}
|
|
|
|
namespace
|
|
{
|
|
template<class T>
|
|
struct allocator_no_forward : std::allocator<T>
|
|
{
|
|
allocator_no_forward() = default;
|
|
template <class U>
|
|
allocator_no_forward(const allocator_no_forward<U>& /*unused*/) {}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = allocator_no_forward<U>;
|
|
};
|
|
|
|
template <class... Args>
|
|
void construct(T* p, const Args& ... args) noexcept(noexcept(::new (static_cast<void*>(p)) T(args...)))
|
|
{
|
|
// force copy even if move is available
|
|
::new (static_cast<void*>(p)) T(args...);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("bad my_allocator::construct")
|
|
{
|
|
SECTION("my_allocator::construct doesn't forward")
|
|
{
|
|
using bad_alloc_json = nlohmann::json::with_allocator_t<allocator_no_forward>;
|
|
|
|
bad_alloc_json j;
|
|
j["test"] = bad_alloc_json::array_t();
|
|
j["test"].push_back("should not leak");
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
std::size_t counting_allocator_allocations = 0;
|
|
std::size_t counting_allocator_deallocations = 0;
|
|
|
|
template<class T>
|
|
struct counting_allocator : std::allocator<T>
|
|
{
|
|
using std::allocator<T>::allocator;
|
|
|
|
T* allocate(std::size_t n)
|
|
{
|
|
++counting_allocator_allocations;
|
|
return std::allocator<T>::allocate(n);
|
|
}
|
|
|
|
void deallocate(T* p, std::size_t n)
|
|
{
|
|
++counting_allocator_deallocations;
|
|
std::allocator<T>::deallocate(p, n);
|
|
}
|
|
|
|
template <class U>
|
|
struct rebind
|
|
{
|
|
using other = counting_allocator<U>;
|
|
};
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("destructor performs no allocation, only deallocation")
|
|
{
|
|
// see https://github.com/nlohmann/json/issues/4842 and
|
|
// https://github.com/nlohmann/json/issues/5135: destroying nested
|
|
// arrays/objects used to allocate a temporary stack (first with
|
|
// std::allocator, later - after #4842 - with the provided allocator).
|
|
// Since that stack could itself throw bad_alloc from inside the
|
|
// noexcept destructor (#5135), destroy() no longer allocates anything:
|
|
// it only ever frees what is already there.
|
|
using counting_json = nlohmann::json::with_allocator_t<counting_allocator>;
|
|
|
|
SECTION("array")
|
|
{
|
|
auto* j = new counting_json({1, {2, {3, 4}}, 5}); // NOLINT(cppcoreguidelines-owning-memory)
|
|
const auto allocations_before = counting_allocator_allocations;
|
|
const auto deallocations_before = counting_allocator_deallocations;
|
|
delete j; // NOLINT(cppcoreguidelines-owning-memory)
|
|
CHECK(counting_allocator_allocations == allocations_before);
|
|
CHECK(counting_allocator_deallocations > deallocations_before);
|
|
}
|
|
|
|
SECTION("object")
|
|
{
|
|
auto* j = new counting_json({{"a", {{"b", {1, 2}}}}, {"c", 3}}); // NOLINT(cppcoreguidelines-owning-memory)
|
|
const auto allocations_before = counting_allocator_allocations;
|
|
const auto deallocations_before = counting_allocator_deallocations;
|
|
delete j; // NOLINT(cppcoreguidelines-owning-memory)
|
|
CHECK(counting_allocator_allocations == allocations_before);
|
|
CHECK(counting_allocator_deallocations > deallocations_before);
|
|
}
|
|
|
|
SECTION("mixed tree of empty/non-empty arrays and objects")
|
|
{
|
|
auto* j = new counting_json( // NOLINT(cppcoreguidelines-owning-memory)
|
|
{
|
|
{"empty_obj", counting_json::object()},
|
|
{"empty_arr", counting_json::array()},
|
|
{"nested", {{"a", counting_json::array({1, 2, counting_json::object()})}, {"b", 3}}},
|
|
{"tail", counting_json::array({counting_json::array({1}), 2, counting_json::array({3})})}
|
|
});
|
|
const auto allocations_before = counting_allocator_allocations;
|
|
const auto deallocations_before = counting_allocator_deallocations;
|
|
delete j; // NOLINT(cppcoreguidelines-owning-memory)
|
|
CHECK(counting_allocator_allocations == allocations_before);
|
|
CHECK(counting_allocator_deallocations > deallocations_before);
|
|
}
|
|
}
|
|
|
|
// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave
|
|
// next_construct_fails set for the next allocation outside a check
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
TEST_CASE("a failed allocation leaves the value unchanged")
|
|
{
|
|
// create JSON type using the throwing allocator
|
|
using my_json = nlohmann::json::with_allocator_t<my_allocator>;
|
|
|
|
// Each of these creates a string, array, object, or binary value. The
|
|
// value must be created before the type is changed: otherwise, a failed
|
|
// creation left a value of the new type without anything behind it (an
|
|
// assertion in its destructor, a null pointer everywhere else) or, when
|
|
// an old value was destroyed first, with a pointer to that destroyed one.
|
|
|
|
SECTION("creating a binary value")
|
|
{
|
|
const std::vector<std::uint8_t> bytes = {1, 2, 3};
|
|
my_json _;
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(_ = my_json::binary(bytes), std::bad_alloc&);
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(_ = my_json::binary(bytes, 42), std::bad_alloc&);
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(_ = my_json::binary(std::vector<std::uint8_t>(bytes)), std::bad_alloc&);
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(_ = my_json::binary(std::vector<std::uint8_t>(bytes), 42), std::bad_alloc&);
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
SECTION("turning a null value into an array or object")
|
|
{
|
|
my_json j;
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j[0], std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j["key"], std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.push_back(my_json(1)), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
const my_json one = 1;
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.push_back(one), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.push_back(my_json::object_t::value_type("key", 1)), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.emplace_back(1), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.emplace("key", 1), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
const my_json object = {{"key", 1}};
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(j.update(object), std::bad_alloc&);
|
|
CHECK(j.is_null());
|
|
|
|
next_construct_fails = false;
|
|
}
|
|
|
|
// With iterator debugging, VS 2015's containers construct a proxy with the
|
|
// allocator in constructors that cannot report its failure, so a failing
|
|
// allocator crashes this section there (SIGSEGV with VS 2015 Debug x86).
|
|
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
|
SECTION("converting into an existing value")
|
|
{
|
|
// to_json replaces the value it is given; the old one must survive a
|
|
// failed creation of the new one
|
|
my_json j = "old";
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::string("new")), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::vector<int> {1, 2}), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::vector<bool> {true, false}), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::map<std::string, int> {{"a", 1}}), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, my_json::binary_t({1, 2})), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
// the overloads for lvalues of the value types, for the value types
|
|
// themselves, and for the remaining compatible types
|
|
const std::string string = "new";
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, string), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, "new"), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
// to_json only moves a binary value that it converted from another
|
|
// container type, which my_json's std::vector<std::uint8_t> is not
|
|
using binary_constructor = nlohmann::detail::external_constructor<nlohmann::detail::value_t::binary>;
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(binary_constructor::construct(j, my_json::binary_t({1, 2})), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
my_json::array_t array = {1, 2};
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, array), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::move(array)), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
my_json::object_t object = {{"a", 1}};
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, object), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::move(object)), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = true;
|
|
CHECK_THROWS_AS(nlohmann::to_json(j, std::valarray<int> {1, 2}), std::bad_alloc&);
|
|
CHECK(j == "old");
|
|
|
|
next_construct_fails = false;
|
|
nlohmann::to_json(j, std::vector<int> {1, 2});
|
|
CHECK(j == my_json({1, 2}));
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|