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

+75
View File
@@ -82,8 +82,20 @@ class huge_string_t : public std::string
return result;
}
// returns a copy of @a s whose size() pretends to be @a n
static huge_string_t as_size(const std::string& s, const size_type n)
{
huge_string_t result(s);
result.fake_size = n;
return result;
}
size_type size() const noexcept
{
if (fake_size != 0)
{
return fake_size;
}
if (pretend_huge)
{
// one byte more than the BSON length field can represent
@@ -94,6 +106,7 @@ class huge_string_t : public std::string
private:
bool pretend_huge = false;
size_type fake_size = 0;
};
using huge_string_json = nlohmann::json::with_string_t<huge_string_t>;
@@ -268,6 +281,17 @@ TEST_CASE("BSON")
CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&);
}
SECTION("an array element that no longer fits the document")
{
// the string fits the length field, but not the document that
// holds it; nothing of it is written
const auto limit = static_cast<std::string::size_type>((std::numeric_limits<std::int32_t>::max)());
huge_string_json j;
j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)});
CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&);
}
SECTION("document")
{
// an oversized string nested one level deep makes the
@@ -899,6 +923,38 @@ TEST_CASE("BSON")
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
}
SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length")
{
// the inner length field is cut off by the end of the input
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'a', 0x00, // entry: binary
0x08, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x01 // first byte of the inner length
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("string member that is cut off by the end of the input")
{
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x02, 'a', 0x00, // entry: string
0x0A, 0x00, 0x00, 0x00, // size of the string (little endian)
'b', 'c' // the string ends early
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("Some more complex document")
{
json const j =
@@ -2174,6 +2230,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
CHECK(output.empty());
}
#if !defined(JSON_NOEXCEPTION)
SECTION("U+0000 in a long key is found at any position")
{
// the key is searched eight characters at a time
for (const std::string& key :
{
std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13),
std::string("abcdefghijklmno\0", 16)
})
{
CAPTURE(key)
const json value = json::object({{key, 1}});
std::vector<std::uint8_t> output;
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
CHECK(output.empty());
}
}
#endif
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
{
// the offending value is nested, so this also covers that the check