mirror of
https://github.com/nlohmann/json.git
synced 2026-11-03 03:15:34 +00:00
* CI: measure line coverage without lcov's unexecuted-block adjustment Drop geninfo_unexecuted_blocks=1 from the lcov capture of ci_test_coverage: it reset every line of an inlined or templated function to 0 as soon as one copy had an unexecuted block, which understated the line coverage by about three points. Add -fvar-tracking-assignments to both coverage builds. At -O0, GCC inlines always_inline functions before it places the coverage counters, so the call sites of such helpers (put_literal() and others) got no line record and showed as 0 in an -O3 translation unit that never reaches them. With the flag, the call sites carry counts in every translation unit. The generated code is identical. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove unreachable code found by the coverage analysis Remove code that no input can reach, and mark the lines that cannot be covered with LCOV_EXCL_LINE: - BJData writer: the dry_run parameter of the ND-array element writers was always true; replace them by range checks. - BON8 writer: call throw_integer_out_of_range() directly instead of write_bon8_value() for values that always throw. - binary_writer: integer_bits() is only called for integers of up to 64 bits. - serializer, view serializer, lexer: replace guards that the callers make unreachable by assertions or by the simpler code (surrogate pairs under ensure_ascii, next_unget, '-' at the end of the buffer). - json_view: drop unreachable branches in scalar(), set(), check_utf8_scan(), append_input_impl() and unescape(). - Mark exception-cleanup braces, 2 GiB limits, big-endian and 32-bit code, and fall-through case labels with LCOV_EXCL_LINE. No behavior changes for any input that reaches these functions. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: cover the remaining lines of the library headers Add tests for the lines of include/nlohmann that were not covered: the binary readers and writers, the lexer and locale paths, the json_view modules, the custom number, key and array types, json_pointer, json_patch, diff and the iterators. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix test failures in the coverage tests - unit-custom-object-key-type: remove the unused make_strict_key(strict_key) overload, the unused range constructor and default constructor, and guard data() with JSON_DIAGNOSTICS (unused-function/-member-function errors with GCC and clang -Weverything). - unit-json_view_compare: move the two new sections to the new unit-json_view_compare_values.cpp, as the object file got too large for the MinGW assembler again, and use a std::map with std::greater instead of std::unordered_map, which does not accept the incomplete basic_json (clang 3.9, icpc). - unit-json_view_dump: rename the variables that hid outer ones (MSVC C4456). - unit-stream-bulk-scan: restore the real size of the oversized get area before the adapter is destroyed (JSON_PRECISE_STREAM_POSITION reads the character at the read position), and skip the test where the streambuf cannot hold a get area larger than an int (MSVC). - unit-regression1: with JSON_NO_THREAD_LOCAL, copies are built member by member and keep correct timestamps, so check the corresponding outcome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> * Return std::size_t from the hash helper in the ordered_json test MSVC (Win32) rejected the implicit conversion of the unsigned long long product to the 32-bit std::size_t (C4244 as an error). Also note when JSON_NO_THREAD_LOCAL is set in the regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me> Signed-off-by: Claude <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
339 lines
14 KiB
C++
339 lines
14 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json_view.hpp>
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
using nlohmann::json_document;
|
|
using nlohmann::ordered_json_editable_document;
|
|
using nlohmann::json_view;
|
|
using nlohmann::ordered_json_document;
|
|
using nlohmann::ordered_json_view;
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <map>
|
|
#include <random>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
|
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
|
// with C++17, as it mentions JSON_HAS_CPP_17.
|
|
|
|
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
|
|
{
|
|
SECTION("members in document order, all of them")
|
|
{
|
|
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
|
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
|
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
|
// dump() writes all members, though the lookup finds the first
|
|
CHECK(d.root()["b"].dump() == "1");
|
|
}
|
|
|
|
SECTION("deep nesting")
|
|
{
|
|
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
|
const json_document deep_document = json_document::parse(deep);
|
|
CHECK(deep_document.root().dump() == deep);
|
|
}
|
|
|
|
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
|
{
|
|
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
|
for (const char* const special :
|
|
{
|
|
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
|
})
|
|
{
|
|
for (std::size_t length = 0; length <= 40; ++length)
|
|
{
|
|
for (std::size_t position = 0; position <= length; ++position)
|
|
{
|
|
std::string content(length, 'a');
|
|
content.insert(position, special);
|
|
const std::array<std::string, 3> texts =
|
|
{
|
|
{
|
|
"\"" + content + "\"",
|
|
"[\"" + content + "\"]",
|
|
"{\"" + content + "\":1}",
|
|
}
|
|
};
|
|
for (const std::string& text : texts)
|
|
{
|
|
CAPTURE(text)
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
const ordered_json j = ordered_json::parse(text);
|
|
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
|
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
|
CHECK(d.root().dump() == j.dump());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
|
{
|
|
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
|
std::string text = "[";
|
|
std::size_t in_chunk = 0;
|
|
const auto append = [&](std::uint32_t cp)
|
|
{
|
|
if (cp < 0x80)
|
|
{
|
|
if (cp < 0x20 || cp == '"' || cp == '\\')
|
|
{
|
|
return;
|
|
}
|
|
text += static_cast<char>(cp);
|
|
}
|
|
else if (cp < 0x800)
|
|
{
|
|
text += static_cast<char>(0xC0 | (cp >> 6));
|
|
text += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
else if (cp < 0x10000)
|
|
{
|
|
text += static_cast<char>(0xE0 | (cp >> 12));
|
|
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
text += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
else
|
|
{
|
|
text += static_cast<char>(0xF0 | (cp >> 18));
|
|
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
|
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
text += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
};
|
|
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
|
{
|
|
if (cp >= 0xD800 && cp < 0xE000)
|
|
{
|
|
continue;
|
|
}
|
|
if (in_chunk == 0)
|
|
{
|
|
text += text.size() > 1 ? ",\"" : "\"";
|
|
}
|
|
append(cp);
|
|
if (++in_chunk == 37)
|
|
{
|
|
text += "\"";
|
|
in_chunk = 0;
|
|
}
|
|
}
|
|
text += in_chunk != 0 ? "\"]" : "]";
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
const ordered_json j = ordered_json::parse(text);
|
|
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
|
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
|
CHECK(d.root().dump() == j.dump());
|
|
CHECK(d.root().dump(2) == j.dump(2));
|
|
}
|
|
|
|
SECTION("deep nesting, indented")
|
|
{
|
|
// indentations of up to and beyond the width of the writer's fixed-size copy
|
|
for (const int depth :
|
|
{
|
|
1, 5, 20, 70, 300
|
|
})
|
|
{
|
|
std::string text;
|
|
for (int i = 0; i < depth; ++i)
|
|
{
|
|
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
|
}
|
|
text += "[]";
|
|
for (int i = depth - 1; i >= 0; --i)
|
|
{
|
|
text += i % 2 == 0 ? "]" : "}";
|
|
}
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
const ordered_json j = ordered_json::parse(text);
|
|
for (const int indent :
|
|
{
|
|
0, 1, 2, 3, 8
|
|
})
|
|
{
|
|
CHECK(d.root().dump(indent) == j.dump(indent));
|
|
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("the output buffer of a small value is small")
|
|
{
|
|
// an escaped key after the value: its node lies in the arena, so the
|
|
// source extent of the value cannot be read from the next node
|
|
const std::string big(100000, 'a');
|
|
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
|
const json_document d = json_document::parse(text);
|
|
const auto small = d.root()["small"].dump();
|
|
CHECK(small == "1");
|
|
CHECK(small.capacity() < 4096);
|
|
const auto list = d.root()["list"].dump();
|
|
CHECK(list == "[1,2,3]");
|
|
CHECK(list.capacity() < 4096);
|
|
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
|
|
|
// the whole document and the large value are unaffected
|
|
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
|
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
|
}
|
|
|
|
SECTION("output that outgrows the estimate")
|
|
{
|
|
// ensure_ascii writes six bytes for each two-byte character
|
|
std::string chars;
|
|
for (int i = 0; i < 5000; ++i)
|
|
{
|
|
chars += "\xC3\xA9";
|
|
}
|
|
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
|
const json expected = json::parse("\"" + chars + "\"");
|
|
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
|
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
|
}
|
|
|
|
SECTION("the estimate for a value that is followed by many decoded strings")
|
|
{
|
|
// (the end of the value in the source is not known when the nodes after it have decoded strings)
|
|
std::string text = "[7";
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
text += R"(,"\n")";
|
|
}
|
|
text += ",8]";
|
|
const json_document d = json_document::parse(text);
|
|
CHECK(d.root()[0].dump() == "7");
|
|
CHECK(d.root()[1].dump() == R"("\n")");
|
|
CHECK(d.root().dump() == json::parse(text).dump());
|
|
// the same with a container
|
|
const json_document nested = json_document::parse(R"([[7],)" + text.substr(3));
|
|
CHECK(nested.root()[0].dump() == "[7]");
|
|
}
|
|
|
|
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
|
{
|
|
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
|
// the second text has a DEL, which ensure_ascii writes as an escape
|
|
for (const char* const del :
|
|
{"", "\x7F"
|
|
})
|
|
{
|
|
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
|
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
|
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
|
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
|
+ "\"n\":[1,2.5,-0,true,null]}";
|
|
const ordered_json j = ordered_json::parse(text);
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
|
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
|
CHECK(d.root().dump() == j.dump());
|
|
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
|
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
|
|
|
// strings that an edit wrote (the source strings that they replace were plain)
|
|
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
|
ordered_json k = j;
|
|
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
|
k["plain"] = "\xC3\xA4gypten";
|
|
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
|
k["list"][0] = "ascii after edit";
|
|
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
|
k["list"][1] = "\xC3\xA4";
|
|
e.set(e.root()["del"], std::string("tab\there"));
|
|
k["del"] = "tab\there";
|
|
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
|
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
|
CHECK(e.root().dump() == k.dump());
|
|
|
|
// strings that were edited from or to ASCII text
|
|
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
|
k["list"][2] = "\xC3\xA4";
|
|
e.set(e.root()["list"][4], std::string("plain again"));
|
|
k["list"][4] = "plain again";
|
|
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
|
}
|
|
}
|
|
|
|
SECTION("the length of the run that ensure_ascii copies")
|
|
{
|
|
// (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here)
|
|
for (const std::size_t length :
|
|
{
|
|
std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50}
|
|
})
|
|
{
|
|
CAPTURE(length)
|
|
// a run of printable characters, followed by a byte that is not copied and more text
|
|
std::string text(length, 'a');
|
|
text += "\"abcdefghijklmnopqrstuvwxyz";
|
|
const auto* const bytes = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
|
const auto* const end = bytes + text.size();
|
|
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length);
|
|
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length);
|
|
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length);
|
|
// the end of the buffer right after the run
|
|
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length);
|
|
}
|
|
}
|
|
|
|
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
|
{
|
|
// the check whether a string of the source has no byte >= 0x7F reads
|
|
// eight bytes at a time and then the rest one by one
|
|
for (std::size_t length = 1; length <= 20; ++length)
|
|
{
|
|
for (std::size_t pos = 0; pos < length; ++pos)
|
|
{
|
|
for (const char* const c :
|
|
{
|
|
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
|
})
|
|
{
|
|
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
|
std::string text = "[\"";
|
|
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
|
const json_document d = json_document::parse(text);
|
|
const json j = json::parse(text);
|
|
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
|
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("streams and discarded views")
|
|
{
|
|
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
|
std::ostringstream compact;
|
|
compact << d.root();
|
|
CHECK(compact.str() == R"({"a":[1,2]})");
|
|
std::ostringstream pretty;
|
|
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
|
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
|
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
|
}
|
|
}
|