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

+70
View File
@@ -20,6 +20,7 @@ using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
@@ -448,6 +449,31 @@ TEST_CASE("json_view materialize() source positions")
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
}
SECTION("a damaged text: the end of a container is not found")
{
// (the text of an image is not checked with image_check::none)
const std::string array_text = "[1,2]";
const std::vector<std::uint8_t> original = json_document::parse(array_text).save();
const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end());
REQUIRE(at != original.end());
const auto closing = static_cast<std::size_t>(at - original.begin()) + array_text.size() - 1;
// not a closing bracket, a slash that starts no comment, and the end of the text
for (const char replacement :
{
'x', '/', ' '
})
{
CAPTURE(replacement)
std::vector<std::uint8_t> damaged = original;
damaged[closing] = static_cast<std::uint8_t>(replacement);
const json_document d = json_document::load(damaged, json_document::image_check::none);
const json m = d.root().materialize();
CHECK(range(m) == "none");
CHECK(range(m[0]) == "1-2");
CHECK(range(m[1]) == "3-4");
}
}
#if !defined(JSON_NOEXCEPTION)
SECTION("a decoded string's position must be within the text")
{
@@ -482,6 +508,7 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
{
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json)
R"([1 /* ] */, {"a": [] // }
, "b": {} } , ])",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
@@ -533,6 +560,49 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
}
SECTION("views of values that are no longer part of the document, and of no value")
{
const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string";
static_cast<void>(expected_string);
// (a value that is not found has no path, and with it no bytes)
{
json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])");
const auto removed = d.root()[0];
d.erase(d.root(), 0);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
// (the root is a scalar now: the value cannot be in it)
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
CHECK(d.root().get<int>() == 5);
}
{
json_editable_document d = json_editable_document::parse(R"(["x"])");
const auto removed = d.root()[0];
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
}
// a view of no value has no context
CHECK(exception_of_call([&] { static_cast<void>(nlohmann::json_view().get<int>()); }) == "[json.exception.type_error.302] type must be number, but is discarded");
}
SECTION("an array whose last element was replaced by a new container has no bytes")
{
json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})");
d.set(d.root()["a"][1], json::array({3}));
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a) ";
#endif
expected += "type must be number, but is array";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"].get<int>()); }) == expected);
}
SECTION("deep documents with siblings after every nested container")
{
// the search for the path pushes and pops one frame per level (the