Add tests for uncovered code paths

Cover code the test suite did not reach, found from the Coveralls report
of develop and a local coverage run of HEAD:

- dump() of every kind of value below the bound of the recursive descent
  (pretty-printed objects, binary values, discarded values, scalars), and
  flushes of the escape and write buffers mid-string and mid-binary
- the iterative comparison: objects with different keys, containers that
  are a prefix of each other, and elements that cannot be ordered, each
  both at the top level and below the nesting bound
- SAX handlers that stop at any event, including the end of a nested
  container, in the BSON, CBOR, MessagePack, UBJSON and BJData readers
- from_bson/cbor/msgpack/ubjson/bjdata returning a discarded value
  through the iterator and pointer overloads
- JSON Patch, diff, merge_patch and update(..., true) on ordered_json
- smaller gaps: get_allocator(), to_ubjson/to_bjdata into a string,
  value() with an unresolvable JSON pointer, integer/float comparison
  below the integer range and with negative fractions, conversion to a
  custom binary type, std::formatter::parse on a spec without '}',
  unescape() of a lone '~', and the callback parser's start_array()

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-25 21:11:50 +02:00
parent 465407f3ce
commit 959c362bef
14 changed files with 680 additions and 0 deletions
+6
View File
@@ -37,6 +37,12 @@ struct bad_allocator : std::allocator<T>
};
} // 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")
+102
View File
@@ -3763,6 +3763,49 @@ TEST_CASE("BJData")
}
}
TEST_CASE("BJData input that cannot be read is discarded by every overload")
{
std::vector<std::uint8_t> input = json::to_bjdata(json({{"a", {1, 2}}}));
input.pop_back();
json _;
CHECK_THROWS_AS(_ = json::from_bjdata(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_bjdata(input, true, false).is_discarded());
CHECK(json::from_bjdata(input.begin(), input.end(), true, false).is_discarded());
}
TEST_CASE("BJData SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::bjdata))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_bjdata(j)) == 20);
CHECK(count_events(json::to_bjdata(j, true)) == 20);
CHECK(count_events(json::to_bjdata(j, true, true)) == 20);
// an ND-array is announced as an annotated object: start_object, then
// _ArrayType_, _ArraySize_ and _ArrayData_ with its elements
const json ndarray = json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})");
CHECK(count_events(json::to_bjdata(ndarray, true, true)) == 16);
}
TEST_CASE("issue #5405 - array reserve for definite-length BJData arrays")
{
#if !defined(JSON_NOEXCEPTION)
@@ -4247,6 +4290,54 @@ TEST_CASE("all BJData first bytes")
}
#endif
TEST_CASE("BJData and UBJSON can be written to a string")
{
const std::vector<json> values =
{
{{"a", {1, 2.5, "x", nullptr}}, {"b", json::binary({1, 2})}},
// an annotated ND-array, and objects that only look like one
json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"),
json::parse(R"({"_ArrayType_": 1, "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"),
json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": 4, "_ArrayData_": [1, 2, 3, 4]})"),
json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, -2], "_ArrayData_": [1, 2, 3, 4]})"),
json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3]})"),
json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": 1})"),
};
for (const auto& j : values)
{
CAPTURE(j.dump());
for (const bool use_size :
{
false, true
})
{
for (const bool use_type :
{
false, true
})
{
if (use_type && !use_size)
{
continue;
}
CAPTURE(use_size);
CAPTURE(use_type);
const auto bjdata = json::to_bjdata(j, use_size, use_type);
std::string bjdata_string;
json::to_bjdata(j, bjdata_string, use_size, use_type);
CHECK(bjdata_string == std::string(bjdata.begin(), bjdata.end()));
const auto ubjson = json::to_ubjson(j, use_size, use_type);
std::string ubjson_string;
json::to_ubjson(j, ubjson_string, use_size, use_type);
CHECK(ubjson_string == std::string(ubjson.begin(), ubjson.end()));
}
}
}
}
TEST_CASE("BJData use_type requires use_size")
{
SECTION("non-empty object throws other_error.502")
@@ -4265,6 +4356,17 @@ TEST_CASE("BJData use_type requires use_size")
json::other_error&);
}
SECTION("non-empty binary value throws other_error.502")
{
const json j = json::binary({1, 2, 3});
CHECK_THROWS_WITH_AS(json::to_bjdata(j, false, true),
"[json.exception.other_error.502] use_type requires use_size = true",
json::other_error&);
CHECK_THROWS_WITH_AS(json::to_ubjson(j, false, true),
"[json.exception.other_error.502] use_type requires use_size = true",
json::other_error&);
}
SECTION("scalars do not throw with use_type=true, use_count=false")
{
CHECK_NOTHROW(json::to_bjdata(42, false, true));
+38
View File
@@ -1244,6 +1244,44 @@ TEST_CASE("BSON nesting does not consume the call stack")
}
}
TEST_CASE("BSON input that cannot be read is discarded by every overload")
{
std::vector<std::uint8_t> input = json::to_bson(json({{"a", {1, 2}}}));
input.pop_back();
json _;
CHECK_THROWS_AS(_ = json::from_bson(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
CHECK(json::from_bson(input.begin(), input.end(), true, false).is_discarded());
CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded());
CHECK(json::from_bson({input.data(), input.size()}, true, false).is_discarded());
}
TEST_CASE("BSON SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::bson))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_bson(j)) == 20);
}
TEST_CASE("BSON numerical data")
{
SECTION("number")
+39
View File
@@ -2122,6 +2122,45 @@ TEST_CASE("CBOR nesting does not consume the call stack")
}
}
TEST_CASE("CBOR input that cannot be read is discarded by every overload")
{
std::vector<std::uint8_t> input = json::to_cbor(json({{"a", {1, 2}}}));
input.pop_back();
json _;
CHECK_THROWS_AS(_ = json::from_cbor(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_cbor(input, true, false).is_discarded());
CHECK(json::from_cbor(input.begin(), input.end(), true, false).is_discarded());
CHECK(json::from_cbor(input.data(), input.size(), true, false).is_discarded());
CHECK(json::from_cbor({input.data(), input.size()}, true, false).is_discarded());
}
TEST_CASE("CBOR SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::cbor))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_cbor(j)) == 20);
CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6);
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{
// Reading an indefinite-length string or byte array used to call itself
+90
View File
@@ -359,6 +359,15 @@ TEST_CASE("lexicographical comparison operators")
CHECK(json(1) < json(1.5));
CHECK(json(1.5) < json(2));
CHECK(json(2) > json(1.5));
CHECK(json(-1) > json(-1.5));
CHECK(json(-1.5) < json(-1));
CHECK(json(-2) < json(-1.5));
// a float below the range of the integer type
CHECK(json(0) > json(-1e30));
CHECK(json(-1e30) < json(0));
CHECK(json(0u) > json(-0.5));
CHECK(json(-0.5) < json(0u));
// a NaN operand stays unordered against either integer kind
CHECK_FALSE(json(1) == json(nan));
@@ -735,3 +744,84 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24
}
}
#endif
TEST_CASE("containers are compared element by element")
{
// Containers nested deeper than a bound are compared without the call
// stack, by code of their own; every relation is checked both at the top
// level and below that bound.
const auto deep = [](const json & j, const std::size_t depth)
{
json result = j;
for (std::size_t i = 0; i < depth; ++i)
{
result = json::array({std::move(result)});
}
return result;
};
for (const std::size_t depth : std::vector<std::size_t> {0, 200})
{
CAPTURE(depth);
// objects with different keys
{
const json a = deep({{"a", 1}}, depth);
const json b = deep({{"b", 1}}, depth);
CHECK_FALSE(a == b);
CHECK(a != b);
CHECK(a < b);
CHECK(b > a);
CHECK_FALSE(b < a);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD*
CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD*
CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD*
#endif
}
// a container that is a prefix of the other one
{
// the one that runs out of elements first is the smaller one
const json shorter = deep({1}, depth);
const json longer = deep({1, 2}, depth);
CHECK(shorter < longer);
CHECK(longer > shorter);
CHECK_FALSE(longer < shorter);
CHECK_FALSE(shorter == longer);
const json smaller_object = deep({{"a", 1}}, depth);
const json larger_object = deep({{"a", 1}, {"b", 2}}, depth);
CHECK(smaller_object < larger_object);
CHECK(larger_object > smaller_object);
CHECK_FALSE(smaller_object == larger_object);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD*
CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD*
#endif
}
// elements that cannot be ordered
{
const double nan = std::numeric_limits<double>::quiet_NaN();
const json lhs = deep({nan, 1}, depth);
const json rhs = deep({nan, 2}, depth);
CHECK_FALSE(lhs == lhs);
CHECK_FALSE(rhs < lhs);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
// operator<=> stops there, as std::lexicographical_compare_three_way
// does, and operator< is derived from it
CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD*
CHECK_FALSE(lhs < rhs);
#else
// operator< skips a pair of elements that cannot be ordered, as
// std::lexicographical_compare does, and the next pair decides
CHECK(lhs < rhs);
#endif
}
}
}
+10
View File
@@ -49,6 +49,16 @@ TEST_CASE("binary type whose value type is not std::uint8_t")
CHECK(char_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})");
}
SECTION("a value is converted to the binary type if it is binary or an array")
{
const std::vector<char> chars{'\0', '\x01', '\x7F'};
CHECK(char_binary_json::binary(chars).get<std::vector<char>>() == chars);
CHECK(char_binary_json({0, 1, 127}).get<std::vector<char>>() == chars);
CHECK_THROWS_WITH_AS(char_binary_json(1).get<std::vector<char>>(),
"[json.exception.type_error.302] type must be binary or array, but is number",
char_binary_json::type_error&);
}
SECTION("the default binary type is unchanged")
{
CHECK(nlohmann::json::binary({0, 1, 255}, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})");
+10
View File
@@ -1517,6 +1517,16 @@ TEST_CASE_TEMPLATE("element access 2 (throwing tests)", Json, nlohmann::json, nl
CHECK(j.value("/not/existing"_json_pointer, Json({{"foo", "bar"}})) == Json({{"foo", "bar"}}));
CHECK(j.value("/not/existing"_json_pointer, Json({10, 100})) == Json({10, 100}));
// an array index that is out of range, too large to be
// represented, or "-", and a token below a scalar
CHECK(j.value("/array/3"_json_pointer, 2) == 2);
CHECK(j.value("/array/-"_json_pointer, 2) == 2);
CHECK(j.value("/array/99999999999999999999999999"_json_pointer, 2) == 2);
CHECK(j.value("/integer/0"_json_pointer, 2) == 2);
CHECK(j.value("/string/x"_json_pointer, 2) == 2);
CHECK(j.value("/null/x"_json_pointer, 2) == 2);
CHECK(j.value("/array/0"_json_pointer, 2) == 1);
CHECK(j_const.value("/not/existing"_json_pointer, 2) == 2);
CHECK(j_const.value("/not/existing"_json_pointer, 2u) == 2u);
CHECK(j_const.value("/not/existing"_json_pointer, false) == false);
+69
View File
@@ -1751,3 +1751,72 @@ TEST_CASE("JSON patch - diff emits array removals in descending index order")
CHECK(source.patch(patch) == target);
}
}
TEST_CASE("JSON patch - every operation on ordered_json")
{
using nlohmann::ordered_json;
const ordered_json doc = {{"foo", "bar"}, {"arr", {1, 2, 3}}, {"obj", {{"a", 1}}}};
SECTION("successful operations")
{
const ordered_json patch = ordered_json::parse(R"([
{"op": "add", "path": "/obj/b", "value": 2},
{"op": "add", "path": "/arr/1", "value": 9},
{"op": "add", "path": "/arr/-", "value": 4},
{"op": "remove", "path": "/arr/0"},
{"op": "remove", "path": "/obj/a"},
{"op": "replace", "path": "/foo", "value": "baz"},
{"op": "move", "from": "/foo", "path": "/moved"},
{"op": "copy", "from": "/obj", "path": "/copied"},
{"op": "test", "path": "/copied/b", "value": 2}
])");
const ordered_json expected = ordered_json::parse(R"({
"arr": [9, 2, 3, 4], "obj": {"b": 2}, "moved": "baz", "copied": {"b": 2}
})");
CHECK(doc.patch(patch) == expected);
// adding to the root replaces the document
CHECK(doc.patch(ordered_json::parse(R"([{"op": "add", "path": "", "value": [1]}])")) == ordered_json({1}));
}
SECTION("failing operations")
{
ordered_json _;
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")),
"[json.exception.out_of_range.401] array index 4 is out of range", ordered_json::out_of_range&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/nope/x", "value": 1}])")),
"[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/obj/nope"}])")),
"[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")),
"[json.exception.out_of_range.401] array index 3 is out of range", ordered_json::out_of_range&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")),
"[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")),
"[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", ordered_json::parse_error&);
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "move", "from": "/obj", "path": "/obj/a/b"}])")),
"[json.exception.out_of_range.414] cannot move value: 'from' path '/obj' is a proper prefix of 'path' '/obj/a/b'", ordered_json::out_of_range&);
}
SECTION("diff reproduces the target")
{
const ordered_json source = {{"a", 1}, {"b", 2}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}};
const std::vector<ordered_json> targets =
{
// a key removed, a key added, a nested change, a shorter array
{{"a", 1}, {"c", {{"x", 2}}}, {"l", {1}}, {"d", 4}},
// the same keys in another order
{{"c", {{"x", 1}}}, {"a", 1}, {"b", 2}, {"l", {1, 2, 3}}},
// new keys ahead of the common ones
{{"new", true}, {"a", 1}, {"b", 3}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}},
};
for (const auto& target : targets)
{
CAPTURE(target.dump());
CHECK(source.patch(ordered_json::diff(source, target)) == target);
}
}
}
+13
View File
@@ -872,3 +872,16 @@ TEST_CASE("JSON pointers")
}
#endif
}
TEST_CASE("unescaping keeps a '~' that does not start an escape sequence")
{
// the parser of a JSON pointer rejects such reference tokens before it
// unescapes them, so this is only reachable by calling unescape directly
std::string s = "a~2b~";
nlohmann::detail::unescape(s);
CHECK(s == "a~2b~");
s = "~0~1~";
nlohmann::detail::unescape(s);
CHECK(s == "~/~");
}
+29
View File
@@ -345,3 +345,32 @@ TEST_CASE("JSON Merge Patch on deeply nested values")
CHECK(p->at("x") == 1);
}
}
TEST_CASE("JSON Merge Patch and update on ordered_json")
{
using nlohmann::ordered_json;
SECTION("merge_patch")
{
ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": 2}, "d": 3, "e": [1]})");
target.merge_patch(ordered_json::parse(R"({"a": {"b": null, "f": 4}, "d": {"x": {"y": null}}, "e": null, "g": {"h": 5}})"));
CHECK(target == ordered_json::parse(R"({"a": {"c": 2, "f": 4}, "d": {"x": {}}, "g": {"h": 5}})"));
// a patch that is not an object replaces the target
target.merge_patch(ordered_json({1, 2}));
CHECK(target == ordered_json({1, 2}));
// an object patch turns a target that is not an object into one
target.merge_patch(ordered_json::parse(R"({"k": {"l": null}})"));
CHECK(target == ordered_json::parse(R"({"k": {}})"));
}
SECTION("update with merge_objects")
{
ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2}}, "e": 3})");
target.update(ordered_json::parse(R"({"a": {"c": {"x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"), true);
CHECK(target == ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2, "x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"));
target.update(ordered_json::parse(R"({"a": 1})"), false);
CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})"));
}
}
+38
View File
@@ -1759,6 +1759,44 @@ TEST_CASE("MessagePack nesting does not consume the call stack")
}
}
TEST_CASE("MessagePack input that cannot be read is discarded by every overload")
{
std::vector<std::uint8_t> input = json::to_msgpack(json({{"a", {1, 2}}}));
input.pop_back();
json _;
CHECK_THROWS_AS(_ = json::from_msgpack(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_msgpack(input, true, false).is_discarded());
CHECK(json::from_msgpack(input.begin(), input.end(), true, false).is_discarded());
CHECK(json::from_msgpack(input.data(), input.size(), true, false).is_discarded());
CHECK(json::from_msgpack({input.data(), input.size()}, true, false).is_discarded());
}
TEST_CASE("MessagePack SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::msgpack))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_msgpack(j)) == 20);
}
TEST_CASE("single MessagePack roundtrip")
{
SECTION("sample.json")
+150
View File
@@ -629,3 +629,153 @@ TEST_CASE("serialization of deeply nested values")
}
}
}
namespace
{
// wraps @a inner into @a depth single-element arrays
json wrap_in_arrays(const json& inner, const std::size_t depth)
{
json j = inner;
for (std::size_t i = 0; i < depth; ++i)
{
j = json::array({std::move(j)});
}
return j;
}
// what wrap_in_arrays(inner, depth).dump(2) is expected to be: the arrays
// around inner.dump(2), with inner's own lines indented by the depth
std::string expected_pretty_in_arrays(const json& inner, const std::size_t depth)
{
std::string expected;
for (std::size_t i = 0; i < depth; ++i)
{
expected += std::string(2 * i, ' ') + "[\n";
}
const std::string indent(2 * depth, ' ');
expected += indent;
for (const char c : inner.dump(2))
{
expected += c;
if (c == '\n')
{
expected += indent;
}
}
for (std::size_t i = depth; i > 0; --i)
{
expected += '\n' + std::string(2 * (i - 1), ' ') + ']';
}
return expected;
}
} // namespace
TEST_CASE("serialization of every kind of value below the bound of the descent")
{
// Values nested deeper than the bound are written without the call stack,
// by code of their own; each kind of value must come out the same there as
// it does at the top level, compact and pretty-printed.
std::vector<json> values =
{
json::parse(R"({"a": 1, "b": [1, 2, {"c": "x"}], "d": {}, "e": []})"),
json::parse(R"([1, [2, 3], {"k": null}, "s"])"),
json::object(),
json::array(),
json::binary({1, 2, 3}, 42),
json::binary({1, 2, 3}),
json::binary({}, 7),
json::binary({}),
"a string with \"escapes\"\n",
true,
false,
-42,
42u,
1.5,
nullptr,
json(json::value_t::discarded),
};
// a pretty-printed object whose members are themselves deep
values.push_back({{"x", wrap_in_arrays(1, 5)}, {"y", {{"z", 2}}}});
for (const std::size_t depth : std::vector<std::size_t> {1, 200})
{
CAPTURE(depth);
for (const auto& inner : values)
{
CAPTURE(inner.dump());
const json j = wrap_in_arrays(inner, depth);
CHECK(j.dump() == std::string(depth, '[') + inner.dump() + std::string(depth, ']'));
CHECK(j.dump(2) == expected_pretty_in_arrays(inner, depth));
}
}
SECTION("pretty-printed objects across the bound")
{
for (std::size_t d = 120; d <= 140; ++d)
{
CAPTURE(d);
// built from the inside out: {"k": <level below>, "n": <level>}
json j = 7;
std::string expected = "7";
for (std::size_t i = d; i > 0; --i)
{
j = json({{"k", std::move(j)}, {"n", i}});
const std::string indent(2 * i, ' ');
const std::string outer_indent(2 * (i - 1), ' ');
expected = "{\n" + indent + "\"k\": " + expected + ",\n"
+ indent + "\"n\": " + std::to_string(i) + "\n" + outer_indent + "}";
}
CHECK(j.dump(2) == expected);
CHECK(json::parse(j.dump(2)) == j);
CHECK(json::parse(j.dump()) == j);
}
}
}
TEST_CASE("serializer buffers are flushed mid-string and mid-binary")
{
SECTION("a long run of escaped characters")
{
// each character is escaped on its own, so the escape buffer fills up
const json newlines = std::string(600, '\n');
std::string expected = "\"";
for (int i = 0; i < 600; ++i)
{
expected += "\\n";
}
expected += '"';
CHECK(newlines.dump() == expected);
// every character is \u-escaped under ensure_ascii
std::string umlauts;
std::string escaped_umlauts = "\"";
for (int i = 0; i < 300; ++i)
{
umlauts += "\xC3\xA4";
escaped_umlauts += "\\u00e4";
}
escaped_umlauts += '"';
CHECK(json(umlauts).dump(-1, ' ', true) == escaped_umlauts);
}
SECTION("a large binary value")
{
std::vector<std::uint8_t> bytes(3000);
std::string expected_bytes;
std::string expected_pretty_bytes;
for (std::size_t i = 0; i < bytes.size(); ++i)
{
bytes[i] = static_cast<std::uint8_t>(i % 256);
expected_bytes += (i == 0 ? "" : ",") + std::to_string(i % 256);
expected_pretty_bytes += (i == 0 ? "" : ", ") + std::to_string(i % 256);
}
const json j = json::binary(bytes);
CHECK(j.dump() == "{\"bytes\":[" + expected_bytes + "],\"subtype\":null}");
CHECK(j.dump(2) == "{\n \"bytes\": [" + expected_pretty_bytes + "],\n \"subtype\": null\n}");
}
}
+23
View File
@@ -102,6 +102,29 @@ TEST_CASE("std::formatter<nlohmann::json>")
CHECK_THROWS_AS(std::vformat("{:{}}", std::make_format_args(j, dynamic_width)), std::format_error); // dynamic width
}
SECTION("a format spec may run to the end of the parse context")
{
// std::format always hands parse() a range that still holds the closing
// '}', but a parse context may also end right after the spec
const auto parse = [](const char* spec)
{
std::format_parse_context ctx(spec);
std::formatter<json> f;
CHECK(f.parse(ctx) == ctx.end());
return f;
};
CHECK(parse("").indent == -1);
CHECK(parse(">").indent == -1);
CHECK(parse("#").indent == 4);
CHECK(parse("3").indent == 3);
CHECK(parse("#12").indent == 12);
const auto f = parse(".>");
CHECK(f.indent == -1);
CHECK(f.indent_char == '.');
}
SECTION("std::format_to writes through an arbitrary output iterator")
{
const json j = {{"foo", 1}, {"bar", {1, 2, 3}}};
+63
View File
@@ -1640,6 +1640,29 @@ TEST_CASE("UBJSON")
});
CHECK_THROWS_AS(_ = json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson), json::out_of_range&);
}
SECTION("array with a known size, read with a callback")
{
// a sized array announces its length to start_array()
std::vector<uint8_t> const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2};
json j;
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> scp(j, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
{
return true;
});
CHECK(json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson));
CHECK(j == json({1, 2}));
// the readers reject a size this large before they announce
// it, so it can only reach start_array() directly (the largest
// value stands for an unknown size and is never checked)
json k;
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> scp2(k, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
{
return true;
});
CHECK_THROWS_AS(scp2.start_array((std::numeric_limits<std::size_t>::max)() - 1), json::out_of_range&);
}
}
}
@@ -2255,6 +2278,46 @@ TEST_CASE("UBJSON nesting does not consume the call stack")
}
}
TEST_CASE("UBJSON input that cannot be read is discarded by every overload")
{
std::vector<std::uint8_t> input = json::to_ubjson(json({{"a", {1, 2}}}));
input.pop_back();
json _;
CHECK_THROWS_AS(_ = json::from_ubjson(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_ubjson(input, true, false).is_discarded());
CHECK(json::from_ubjson(input.begin(), input.end(), true, false).is_discarded());
CHECK(json::from_ubjson(input.data(), input.size(), true, false).is_discarded());
CHECK(json::from_ubjson({input.data(), input.size()}, true, false).is_discarded());
}
TEST_CASE("UBJSON SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::ubjson))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_ubjson(j)) == 20);
CHECK(count_events(json::to_ubjson(j, true)) == 20);
CHECK(count_events(json::to_ubjson(j, true, true)) == 20);
}
TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
{
// An element of type 'Z', 'T' or 'F' is encoded by its marker alone, so an