mirror of
https://github.com/nlohmann/json.git
synced 2026-10-12 10:13:13 +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>
511 lines
15 KiB
C++
511 lines
15 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-FileCopyrightText: 2018 Vitaliy Manushkin <agri@akamo.info>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// This file closes a test-coverage gap described in GitHub issue #5421:
|
|
// nlohmann::ordered_json (and other non-default basic_json specializations,
|
|
// such as the alt_string-based one from unit-alt-string.cpp) were never
|
|
// exercised through the binary formats (CBOR/MessagePack/UBJSON/BSON/BJData)
|
|
// or through flatten()/unflatten()/diff()/patch()/merge_patch().
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// alt_json: a second, independent copy of the custom-string_t basic_json
|
|
// specialization defined in unit-alt-string.cpp.
|
|
//
|
|
// It is duplicated here (rather than shared via a header) because every
|
|
// unit-*.cpp file in this test suite is compiled into its own standalone
|
|
// executable (see tests/CMakeLists.txt), so there is no ODR concern in
|
|
// having the same class name defined in multiple translation units.
|
|
//
|
|
// Two members had to be added relative to the original alt_string
|
|
// (a constructor from std::string, and a find(char, pos) overload) because
|
|
// the original type was never used with the binary writers/readers before
|
|
// this file: BSON's array/document writer converts std::to_string() results
|
|
// and checks for embedded NUL characters via find(char), and the UBJSON/BSON
|
|
// high-precision-number path constructs the SAX string_t argument from a
|
|
// std::string. Neither path is exercised anywhere else in the test suite for
|
|
// this type, which is presumably why the gap was never noticed.
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
namespace unit_ordered_json2_detail
|
|
{
|
|
class alt_string;
|
|
bool operator<(const char* op1, const alt_string& op2) noexcept; // NOLINT(misc-use-internal-linkage)
|
|
void int_to_string(alt_string& target, std::size_t value); // NOLINT(misc-use-internal-linkage)
|
|
|
|
class alt_string
|
|
{
|
|
public:
|
|
using value_type = std::string::value_type;
|
|
|
|
static constexpr auto npos = (std::numeric_limits<std::size_t>::max)();
|
|
|
|
alt_string(const char* str): str_impl(str) {}
|
|
alt_string(const char* str, std::size_t count): str_impl(str, count) {}
|
|
alt_string(std::string str): str_impl(std::move(str)) {}
|
|
alt_string(size_t count, char chr): str_impl(count, chr) {}
|
|
alt_string() = default;
|
|
|
|
alt_string& append(char ch)
|
|
{
|
|
str_impl.push_back(ch);
|
|
return *this;
|
|
}
|
|
|
|
alt_string& append(const alt_string& str)
|
|
{
|
|
str_impl.append(str.str_impl);
|
|
return *this;
|
|
}
|
|
|
|
alt_string& append(const char* s, std::size_t length)
|
|
{
|
|
str_impl.append(s, length);
|
|
return *this;
|
|
}
|
|
|
|
void push_back(char c)
|
|
{
|
|
str_impl.push_back(c);
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator==(const op_type& op) const
|
|
{
|
|
return str_impl == op;
|
|
}
|
|
|
|
bool operator==(const alt_string& op) const
|
|
{
|
|
return str_impl == op.str_impl;
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator!=(const op_type& op) const
|
|
{
|
|
return str_impl != op;
|
|
}
|
|
|
|
bool operator!=(const alt_string& op) const
|
|
{
|
|
return str_impl != op.str_impl;
|
|
}
|
|
|
|
std::size_t size() const noexcept
|
|
{
|
|
return str_impl.size();
|
|
}
|
|
|
|
void resize(std::size_t n)
|
|
{
|
|
str_impl.resize(n);
|
|
}
|
|
|
|
void resize(std::size_t n, char c)
|
|
{
|
|
str_impl.resize(n, c);
|
|
}
|
|
|
|
template <typename op_type>
|
|
bool operator<(const op_type& op) const noexcept
|
|
{
|
|
return str_impl < op;
|
|
}
|
|
|
|
bool operator<(const alt_string& op) const noexcept
|
|
{
|
|
return str_impl < op.str_impl;
|
|
}
|
|
|
|
const char* c_str() const
|
|
{
|
|
return str_impl.c_str();
|
|
}
|
|
|
|
char& operator[](std::size_t index)
|
|
{
|
|
return str_impl[index];
|
|
}
|
|
|
|
const char& operator[](std::size_t index) const
|
|
{
|
|
return str_impl[index];
|
|
}
|
|
|
|
char& back()
|
|
{
|
|
return str_impl.back();
|
|
}
|
|
|
|
const char& back() const
|
|
{
|
|
return str_impl.back();
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
str_impl.clear();
|
|
}
|
|
|
|
const value_type* data() const
|
|
{
|
|
return str_impl.data();
|
|
}
|
|
|
|
bool empty() const
|
|
{
|
|
return str_impl.empty();
|
|
}
|
|
|
|
std::size_t find(const alt_string& str, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find(str.str_impl, pos);
|
|
}
|
|
|
|
// needed by binary_writer's BSON support, which probes string keys for
|
|
// embedded NUL characters via find(char)
|
|
std::size_t find(char c, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find(c, pos);
|
|
}
|
|
|
|
std::size_t find_first_of(char c, std::size_t pos = 0) const
|
|
{
|
|
return str_impl.find_first_of(c, pos);
|
|
}
|
|
|
|
alt_string substr(std::size_t pos = 0, std::size_t count = npos) const
|
|
{
|
|
const std::string s = str_impl.substr(pos, count);
|
|
return {s.data(), s.size()};
|
|
}
|
|
|
|
alt_string& replace(std::size_t pos, std::size_t count, const alt_string& str)
|
|
{
|
|
str_impl.replace(pos, count, str.str_impl);
|
|
return *this;
|
|
}
|
|
|
|
void reserve(std::size_t new_cap = 0)
|
|
{
|
|
str_impl.reserve(new_cap);
|
|
}
|
|
|
|
private:
|
|
std::string str_impl {}; // NOLINT(readability-redundant-member-init)
|
|
|
|
friend bool operator<(const char* /*op1*/, const alt_string& /*op2*/) noexcept;
|
|
};
|
|
|
|
void int_to_string(alt_string& target, std::size_t value)
|
|
{
|
|
target = std::to_string(value).c_str();
|
|
}
|
|
|
|
using ordered_json2_alt_json = nlohmann::basic_json <
|
|
std::map,
|
|
std::vector,
|
|
alt_string,
|
|
bool,
|
|
std::int64_t,
|
|
std::uint64_t,
|
|
double,
|
|
std::allocator,
|
|
nlohmann::adl_serializer >;
|
|
|
|
bool operator<(const char* op1, const alt_string& op2) noexcept
|
|
{
|
|
return op1 < op2.str_impl;
|
|
}
|
|
} // namespace unit_ordered_json2_detail
|
|
|
|
using unit_ordered_json2_detail::ordered_json2_alt_json;
|
|
|
|
namespace
|
|
{
|
|
|
|
// collects the object keys of j, in iteration order
|
|
std::vector<std::string> collect_keys(const ordered_json& j)
|
|
{
|
|
std::vector<std::string> result;
|
|
for (auto it = j.cbegin(); it != j.cend(); ++it)
|
|
{
|
|
result.push_back(it.key());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// a nested object/array value with keys inserted in non-alphabetical order,
|
|
// used to check both round-trip equality and (for ordered_json) that
|
|
// insertion order survives a trip through a binary format
|
|
ordered_json make_rich_ordered_json()
|
|
{
|
|
ordered_json j;
|
|
j["zebra"] = 1;
|
|
j["apple"] = ordered_json::array({1, 2, 3});
|
|
j["mango"]["z_nested"] = true;
|
|
j["mango"]["a_nested"] = nullptr;
|
|
j["banana"] = "some text";
|
|
j["cherry"] = 3.14;
|
|
return j;
|
|
}
|
|
|
|
ordered_json2_alt_json make_rich_alt_json()
|
|
{
|
|
ordered_json2_alt_json j;
|
|
j["zebra"] = 1;
|
|
j["apple"] = ordered_json2_alt_json::array({1, 2, 3});
|
|
j["mango"]["z_nested"] = true;
|
|
j["mango"]["a_nested"] = nullptr;
|
|
j["banana"] = "some text";
|
|
j["cherry"] = 3.14;
|
|
return j;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("ordered_json across binary formats")
|
|
{
|
|
const ordered_json original = make_rich_ordered_json();
|
|
const std::vector<std::string> original_keys = collect_keys(original);
|
|
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
|
|
SECTION("CBOR")
|
|
{
|
|
const auto bytes = ordered_json::to_cbor(original);
|
|
const auto restored = ordered_json::from_cbor(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("MessagePack")
|
|
{
|
|
const auto bytes = ordered_json::to_msgpack(original);
|
|
const auto restored = ordered_json::from_msgpack(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("UBJSON")
|
|
{
|
|
const auto bytes = ordered_json::to_ubjson(original);
|
|
const auto restored = ordered_json::from_ubjson(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BSON")
|
|
{
|
|
const auto bytes = ordered_json::to_bson(original);
|
|
const auto restored = ordered_json::from_bson(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BJData")
|
|
{
|
|
const auto bytes = ordered_json::to_bjdata(original);
|
|
const auto restored = ordered_json::from_bjdata(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
SECTION("BON8")
|
|
{
|
|
const auto bytes = ordered_json::to_bon8(original);
|
|
const auto restored = ordered_json::from_bon8(bytes);
|
|
CHECK(restored == original);
|
|
CHECK(collect_keys(restored) == original_keys);
|
|
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("alt_json (custom string_t) across binary formats")
|
|
{
|
|
const ordered_json2_alt_json original = make_rich_alt_json();
|
|
|
|
SECTION("CBOR")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_cbor(original);
|
|
const auto restored = ordered_json2_alt_json::from_cbor(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("MessagePack")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_msgpack(original);
|
|
const auto restored = ordered_json2_alt_json::from_msgpack(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("UBJSON")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_ubjson(original);
|
|
const auto restored = ordered_json2_alt_json::from_ubjson(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BON8")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_bon8(original);
|
|
const auto restored = ordered_json2_alt_json::from_bon8(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BSON")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_bson(original);
|
|
const auto restored = ordered_json2_alt_json::from_bson(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
|
|
SECTION("BJData")
|
|
{
|
|
const auto bytes = ordered_json2_alt_json::to_bjdata(original);
|
|
const auto restored = ordered_json2_alt_json::from_bjdata(bytes);
|
|
CHECK(restored == original);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ordered_json operator== is sensitive to key order")
|
|
{
|
|
// Unlike nlohmann::json (whose object_t is a std::map, so equality never
|
|
// depends on insertion order), ordered_json's object_t (ordered_map) is a
|
|
// std::vector<std::pair<Key, T>> under the hood, and does not define its
|
|
// own operator==: it inherits std::vector's element-wise comparison. As a
|
|
// result, two ordered_json objects holding the very same key/value pairs
|
|
// in different insertion order compare *unequal*. This is the property
|
|
// that makes the round-trip `CHECK(restored == original)` checks above a
|
|
// meaningful order-preservation check by themselves (the explicit
|
|
// collect_keys() comparisons make that check explicit/readable, and
|
|
// guard against this operator== behavior ever changing).
|
|
ordered_json a;
|
|
a["x"] = 1;
|
|
a["y"] = 2;
|
|
|
|
ordered_json b;
|
|
b["y"] = 2;
|
|
b["x"] = 1;
|
|
|
|
CHECK(a.size() == b.size());
|
|
CHECK(a["x"] == b["x"]);
|
|
CHECK(a["y"] == b["y"]);
|
|
CHECK_FALSE(a == b);
|
|
}
|
|
|
|
TEST_CASE("duplicate keys in a binary-encoded object")
|
|
{
|
|
// CBOR encoding of a map with two entries under the same key "a": {"a": 1, "a": 2}
|
|
const std::vector<std::uint8_t> cbor_bytes
|
|
{
|
|
0xA2, 0x61, 'a', 0x01, 0x61, 'a', 0x02
|
|
};
|
|
|
|
// Both json (std::map, via operator[]) and ordered_json (ordered_map, via
|
|
// operator[]) build binary-decoded objects by looking up/creating the
|
|
// entry for each incoming key and then assigning the value into it. This
|
|
// means a repeated key does *not* produce two entries in either case;
|
|
// instead, the *first* occurrence's position is kept (relevant only for
|
|
// ordered_json) while the *last* occurrence's value wins (for both) --
|
|
// this matches operator[]'s "assign the referenced slot" semantics, and
|
|
// is worth noting because it differs from the initializer-list
|
|
// construction path (`ordered_json{{"a",1},{"a",2}}`), which builds
|
|
// through insert()/emplace() and therefore keeps the *first* value, not
|
|
// the last (see the "There are no dup keys..." case in
|
|
// unit-ordered_json.cpp).
|
|
const auto j = json::from_cbor(cbor_bytes);
|
|
const auto oj = ordered_json::from_cbor(cbor_bytes);
|
|
|
|
CHECK(j.size() == 1);
|
|
CHECK(oj.size() == 1);
|
|
CHECK(j["a"] == 2);
|
|
CHECK(oj["a"] == 2);
|
|
CHECK(j == json(oj));
|
|
}
|
|
|
|
TEST_CASE("ordered_json through flatten/unflatten")
|
|
{
|
|
const ordered_json original = make_rich_ordered_json();
|
|
const std::vector<std::string> original_keys = collect_keys(original);
|
|
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
|
|
const ordered_json flat = original.flatten();
|
|
const ordered_json unflattened = flat.unflatten();
|
|
|
|
CHECK(unflattened == original);
|
|
// flatten() walks the value depth-first in iteration order and
|
|
// unflatten() re-inserts each flattened key via operator[] in the flat
|
|
// object's iteration order, so for ordered_json the original key order
|
|
// (both top-level and nested) is preserved end-to-end.
|
|
CHECK(collect_keys(unflattened) == original_keys);
|
|
CHECK(collect_keys(unflattened["mango"]) == original_mango_keys);
|
|
}
|
|
|
|
TEST_CASE("ordered_json through diff/patch/patch_inplace")
|
|
{
|
|
ordered_json original;
|
|
original["one"] = 1;
|
|
original["two"] = 2;
|
|
original["three"] = 3;
|
|
|
|
ordered_json target = original;
|
|
target["one"] = 100; // replace
|
|
target.erase("two"); // remove
|
|
target["four"] = 4; // add
|
|
|
|
const ordered_json patch = ordered_json::diff(original, target);
|
|
|
|
SECTION("patch")
|
|
{
|
|
const ordered_json patched = original.patch(patch);
|
|
CHECK(patched == target);
|
|
}
|
|
|
|
SECTION("patch_inplace")
|
|
{
|
|
ordered_json copy = original;
|
|
copy.patch_inplace(patch);
|
|
CHECK(copy == target);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ordered_json through merge_patch")
|
|
{
|
|
ordered_json original;
|
|
original["a"] = 1;
|
|
original["b"] = 2;
|
|
|
|
const ordered_json patch = {{"b", nullptr}, {"c", 3}};
|
|
|
|
original.merge_patch(patch);
|
|
|
|
ordered_json expected;
|
|
expected["a"] = 1;
|
|
expected["c"] = 3;
|
|
|
|
CHECK(original == expected);
|
|
CHECK(collect_keys(original) == collect_keys(expected));
|
|
}
|