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>
This commit is contained in:
Niels LohmannandClaude Opus 5.5 authored and GitHub committed 2026-11-02 17:54:38 +01:00
1 parent ff0bdfdef3
commit 1d69ef9ab4
55 files changed
+1892 -408

No files matched your search

+86
View File
@@ -16,6 +16,7 @@ using nlohmann::ordered_json;
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
TEST_CASE("ordered_json")
{
@@ -515,3 +516,88 @@ TEST_CASE("parsing random trees of arrays and objects")
CHECK(json::parse(text).dump() == expected.dump());
}
}
TEST_CASE("object_key_index")
{
// the hash table of the DOM parser finds the position of a key in an
// ordered_json object; the parser creates it for exactly 64 elements, but
// it is correct for any number of elements it is given
using nlohmann::detail::object_key_index;
const std::size_t not_found = object_key_index::npos;
for (const std::size_t initial :
{
std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300}
})
{
CAPTURE(initial)
// the hashes are not distinct on purpose: elements with the same
// hash are told apart by the caller
const auto hash_of = [](std::size_t position) -> std::size_t
{
// truncated to 32 bits where std::size_t is that small
return static_cast<std::size_t>((position % 50) * 0x9E3779B97F4A7C15ull);
};
std::vector<std::size_t> hashes;
for (std::size_t i = 0; i < initial; ++i)
{
hashes.push_back(hash_of(i));
}
object_key_index index(std::move(hashes));
// the elements that are there
for (std::size_t i = 0; i < initial; ++i)
{
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(1), [](std::size_t /*position*/)
{
return false;
}) == not_found);
// elements that are added later, until the table grows
for (std::size_t i = initial; i < initial + 600; ++i)
{
index.add(hash_of(i));
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(initial), [initial](std::size_t position)
{
return position == initial;
}) == initial);
}
}
TEST_CASE("JSON pointers on ordered_json")
{
using pointer = ordered_json::json_pointer;
SECTION("operator[] inserts a missing key at the end")
{
ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}};
j[pointer("/a")] = 3;
j[pointer("/x/b")] = 4;
CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}}));
CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})");
}
SECTION("unflatten of a flattened object whose keys are not sorted")
{
// the prefixes /a and /c denote arrays; a key of /a follows one of /c
const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}};
const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}};
CHECK(flat.unflatten() == expected);
CHECK(expected.flatten().unflatten() == expected);
}
}