Files
json/tests/src/unit-custom-array-type.cpp
T
Niels LohmannandClaude Opus 5.5 1d69ef9ab4 Raise line coverage to 99.9 %: fix the measurement, remove unreachable code, add tests (#5997)
* 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>
2026-11-02 17:54:38 +01:00

233 lines
8.2 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.hpp>
using nlohmann::json;
#include <cstdint>
#include <deque>
#include <map>
#include <memory>
#include <string>
#include <type_traits>
#include <vector>
namespace
{
// std::deque has no capacity() member function, which the library only needs
// to detect a reallocation for JSON_DIAGNOSTICS
using deque_json = nlohmann::json::with_array_t<std::deque>;
// a std::vector whose at() is hidden: the library performs its own bounds
// check and must not fall back to the container's checked accessor
template<class T, class Allocator = std::allocator<T>>
class vector_without_at : public std::vector<T, Allocator>
{
public:
vector_without_at() = default;
// the array of an initializer list is built from a range
template<class InputIt>
vector_without_at(InputIt first, InputIt last) : std::vector<T, Allocator>(first, last) {}
void at() = delete;
};
using no_at_json = nlohmann::json::with_array_t<vector_without_at>;
} // namespace
TEST_CASE("array type without capacity()")
{
SECTION("the iterators take their exception specification from the container")
{
// basic_json's iterators move exactly as the container iterators do:
// their move operations are defaulted without a declared noexcept,
// because an array or object type whose iterator is not nothrow move
// constructible would otherwise have them deleted (std::deque's is not
// with libstdc++ before 11, and neither are MSVC's debug iterators)
CHECK(std::is_nothrow_move_constructible<nlohmann::json::iterator>::value ==
(std::is_nothrow_move_constructible<nlohmann::json::object_t::iterator>::value
&& std::is_nothrow_move_constructible<nlohmann::json::array_t::iterator>::value));
CHECK(std::is_nothrow_move_assignable<nlohmann::json::iterator>::value ==
(std::is_nothrow_move_assignable<nlohmann::json::object_t::iterator>::value
&& std::is_nothrow_move_assignable<nlohmann::json::array_t::iterator>::value));
CHECK(std::is_nothrow_move_constructible<nlohmann::json::const_iterator>::value ==
(std::is_nothrow_move_constructible<nlohmann::json::object_t::const_iterator>::value
&& std::is_nothrow_move_constructible<nlohmann::json::array_t::const_iterator>::value));
// and they are movable at all, which is what dropping the declared
// noexcept buys for a std::deque array
CHECK(std::is_move_constructible<deque_json::iterator>::value);
CHECK(std::is_move_assignable<deque_json::iterator>::value);
}
SECTION("adding elements")
{
deque_json j = deque_json::array();
j.push_back(1);
j.push_back("two");
j.emplace_back(3);
j += 4;
CHECK(j.size() == 4);
CHECK(j == deque_json({1, "two", 3, 4}));
CHECK(j.back() == 4);
CHECK(j.front() == 1);
}
SECTION("accessing and modifying elements")
{
auto j = deque_json::parse(R"([1,2,3])");
CHECK(j[1] == 2);
CHECK(j.at(2) == 3);
// growing through operator[] fills up with null values
j[5] = 6;
CHECK(j.size() == 6);
CHECK(j[4].is_null());
CHECK(j[5] == 6);
j.erase(0);
CHECK(j == deque_json({2, 3, nullptr, nullptr, 6}));
auto it = j.erase(j.begin());
CHECK(*it == 3);
j.insert(j.begin(), 1);
CHECK(j.front() == 1);
}
SECTION("serialization and deserialization")
{
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
CHECK(deque_json::parse(j.dump()) == j);
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
// empty containers are flattened to null and cannot be restored
const auto nested = deque_json::parse(R"({"a":[1,[2,3]]})");
CHECK(nested.flatten().unflatten() == nested);
}
SECTION("insert(pos, initializer_list) compiles and works without reserve()")
{
// std::deque has no reserve() either; insert(pos, ilist) must not
// require it (regression test for #5656, which also covers an ilist
// that refers to elements of the array being inserted into)
deque_json j = deque_json::array();
j.push_back("a");
j.push_back("b");
j.push_back("c");
const deque_json& cj = j;
auto it = j.insert(j.begin(), {cj[0], cj[1]});
CHECK(*it == deque_json("a"));
CHECK(j == deque_json({"a", "b", "a", "b", "c"}));
}
SECTION("references stay valid while the array grows")
{
deque_json j = deque_json::array();
j.push_back(1);
auto& first = j[0];
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
CHECK(&first == &j[0]);
CHECK(first == 1);
}
}
TEST_CASE("array type without at()")
{
// built in memory rather than parsed, so that the exception message does
// not gain a byte range with JSON_DIAGNOSTIC_POSITIONS
no_at_json j = {1, 2, 3};
const auto& jc = j;
CHECK(j.at(0) == 1);
CHECK(j.at(2) == 3);
CHECK(jc.at(2) == 3);
CHECK_THROWS_WITH_AS(j.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", no_at_json::out_of_range);
CHECK_THROWS_WITH_AS(jc.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", no_at_json::out_of_range);
CHECK(j.at(no_at_json::json_pointer("/1")) == 2);
CHECK_THROWS_AS(j.at(no_at_json::json_pointer("/3")), no_at_json::out_of_range);
}
TEST_CASE("regression test #5476 - array type without reserve()")
{
// the capacity reserved for definite-length arrays must not require the
// array type to have a reserve() member function
SECTION("std::deque")
{
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
// the binary formats pass a definite length to start_array()
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j);
CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j);
// parse() instantiates the callback parser as well, which reserves too
const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept
{
return true;
});
CHECK(with_callback == deque_json({1, 2, 3}));
}
SECTION("std::vector still reserves")
{
json array = json::array();
for (int i = 0; i < 100; ++i)
{
array.push_back(i);
}
const auto j = json::from_cbor(json::to_cbor(array));
CHECK(j == array);
CHECK(j.get_ref<const json::array_t&>().capacity() >= 100);
}
SECTION("the reservation stays capped")
{
// CBOR array announcing 2^32-1 elements, but truncated right after the
// header: the input must be rejected without reserving that capacity
const std::vector<std::uint8_t> truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
CHECK(json::from_cbor(truncated, true, false).is_discarded());
}
}
TEST_CASE("array type with std::deque nested beyond the recursion limit")
{
// the binary writers finish a value nested deeper than
// 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
deque_json j = 1;
for (int i = 0; i < 200; ++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);
expected.push_back(0x01);
CHECK(deque_json::to_cbor(j) == expected);
}