diff --git a/include/morph/forms/i18n.hpp b/include/morph/forms/i18n.hpp index f6f57fd8..78233252 100644 --- a/include/morph/forms/i18n.hpp +++ b/include/morph/forms/i18n.hpp @@ -50,6 +50,12 @@ enum class FieldSlot : std::uint8_t { case FieldSlot::Placeholder: return "placeholder"; default: + // Unreachable through any real code path: FieldSlot is a closed + // enum and every enumerator is already handled explicitly above. + // This arm only exists to satisfy the compiler that the function + // returns on every enum value, including one manufactured by an + // out-of-range `static_cast` -- mirrors ruleKindName's identical + // default: arm in forms.hpp. return "label"; } } diff --git a/include/morph/forms/layout.hpp b/include/morph/forms/layout.hpp index 5efc85dd..c718c220 100644 --- a/include/morph/forms/layout.hpp +++ b/include/morph/forms/layout.hpp @@ -51,6 +51,12 @@ enum class GroupKind : std::uint8_t { case GroupKind::Accordion: return "accordion"; default: + // Unreachable through any real code path: GroupKind is a closed + // enum and every enumerator is already handled explicitly above. + // This arm only exists to satisfy the compiler that the function + // returns on every enum value, including one manufactured by an + // out-of-range `static_cast` -- mirrors ruleKindName's identical + // default: arm in forms.hpp. return "section"; } } diff --git a/include/morph/offline/reconnect_coordinator.hpp b/include/morph/offline/reconnect_coordinator.hpp index 4e087e0e..090619e1 100644 --- a/include/morph/offline/reconnect_coordinator.hpp +++ b/include/morph/offline/reconnect_coordinator.hpp @@ -33,6 +33,12 @@ constexpr std::string_view reconnectOutcomeName(ReconnectOutcome outcome) noexce case ReconnectOutcome::Aborted: return "Aborted"; default: + // Unreachable through any real code path: ReconnectOutcome is a + // closed enum and every enumerator is already handled explicitly + // above. This arm only exists to satisfy the compiler that the + // function returns on every enum value, including one + // manufactured by an out-of-range `static_cast` -- mirrors + // ruleKindName's identical default: arm in forms.hpp. return "?"; } } diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 023080c1..601441c9 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -49,8 +49,8 @@ fi # the same reason. AUTOMOC's generated # mocs_compilation.cpp lives under $OUT (the build tree), never under a # source-tree path named here, so moc output is excluded automatically — -# no separate exclusion mechanism needed. Test files, demo src/, system -# headers and fetched dependencies are excluded the same way. +# no separate exclusion mechanism needed. Demo src/, system headers and +# fetched dependencies are excluded the same way. SOURCES=(include/morph) if [ -x "$LADDER_TEST_EXE" ]; then SOURCES+=(examples/common) @@ -66,6 +66,20 @@ if [ -x "$PASTEBIN_TEST_EXE" ]; then SOURCES+=(examples/pastebin/include examples/pastebin/src examples/pastebin/gui_lib) fi +# examples/common/testkit/ mixes real, reusable test-support headers/.cpp +# (backend_rig.hpp, db_fixture.hpp, strand_interleaver.hpp, ...) with actual +# Catch2 test files (test_event_poller.cpp, test_presenter.cpp, ...) in the +# same directory — unlike include/morph and examples/pastebin's SOURCES +# entries above, which contain no test files at all. `SOURCES+=(examples/ +# common)` swept both in indiscriminately: a TEST_CASE body's own untaken +# assertion/lambda branches (a REQUIRE's fail arm, a "must not run" callback +# proving itself unreachable) were being measured as if they were product +# code, manufacturing the exact "phantom uncovered branch" noise this +# project has repeatedly had to hand-verify file by file. Test files +# genuinely are not part of what examples/IMPLEMENTATION.md rule 5's 100% +# bar means to hold to that standard — only the real testkit/GUI code is. +IGNORE_REGEX='.*/testkit/test_[^/]+\.cpp$' + PROFILES=$(find "$OUT" -name "*.profraw" 2>/dev/null | tr '\n' ' ') if [ -z "$PROFILES" ]; then echo "ERROR: No .profraw files found in $OUT." >&2 @@ -79,6 +93,7 @@ mkdir -p "$REPORT_DIR" ${LLVM_COV} show "$TEST_EXE" \ "${OBJECT_ARGS[@]}" \ -instr-profile="$MERGED" \ + -ignore-filename-regex="$IGNORE_REGEX" \ -format=html \ -output-dir="$REPORT_DIR" \ "${SOURCES[@]}" @@ -88,11 +103,13 @@ echo "Coverage report: $REPORT_DIR/index.html" ${LLVM_COV} report "$TEST_EXE" \ "${OBJECT_ARGS[@]}" \ -instr-profile="$MERGED" \ + -ignore-filename-regex="$IGNORE_REGEX" \ "${SOURCES[@]}" ${LLVM_COV} export "$TEST_EXE" \ "${OBJECT_ARGS[@]}" \ -instr-profile="$MERGED" \ + -ignore-filename-regex="$IGNORE_REGEX" \ -format=lcov \ "${SOURCES[@]}" \ > "$OUT/coverage.lcov.raw" @@ -106,6 +123,7 @@ ${LLVM_COV} export "$TEST_EXE" \ ${LLVM_COV} export "$TEST_EXE" \ "${OBJECT_ARGS[@]}" \ -instr-profile="$MERGED" \ + -ignore-filename-regex="$IGNORE_REGEX" \ "${SOURCES[@]}" \ > "$OUT/coverage.json" diff --git a/tests/test_logger.cpp b/tests/test_logger.cpp index ef27437d..3205c195 100644 --- a/tests/test_logger.cpp +++ b/tests/test_logger.cpp @@ -167,6 +167,27 @@ TEST_CASE("log(level, msg) respects threshold and delegates to sink", "[logger]" REQUIRE(last == "emitted"); } +// ── logFormat (format-string level helpers) ─────────────────────────────────── + +TEST_CASE("morph::log::logWarn(fmt, args...): suppressed below threshold without formatting or invoking the sink", + "[logger]") { + LogGuard guard; + int callCount = 0; + morph::log::setLogger([&](morph::log::LogLevel, std::string_view) { ++callCount; }); + morph::log::setLogLevel(morph::log::LogLevel::error); + + // Below the "error" threshold: logFormat's own level check must reject + // this before std::format runs or the sink is touched. + morph::log::logWarn("value={}", 42); + REQUIRE(callCount == 0); + + morph::log::setLogLevel(morph::log::LogLevel::warn); + std::string last; + morph::log::setLogger([&](morph::log::LogLevel, std::string_view msg) { last = std::string{msg}; }); + morph::log::logWarn("value={}", 42); + REQUIRE(last == "value=42"); +} + // ── Thread safety ───────────────────────────────────────────────────────────── TEST_CASE("concurrent log calls are thread-safe", "[logger]") { diff --git a/tests/test_outbox.cpp b/tests/test_outbox.cpp index 103a7768..833bd4ed 100644 --- a/tests/test_outbox.cpp +++ b/tests/test_outbox.cpp @@ -361,7 +361,37 @@ TEST_CASE("OutboxRelay::relay(): a null drainOutbox is logged, not rejected, at REQUIRE(sink->entries().empty()); // drainOutbox() threw before anything reached the sink } -// A null `sink` is deliberately NOT exercised into relay()'s body here: unlike +TEST_CASE("OutboxRelay::relay(): a null sink is logged, not rejected, at call time", "[outbox][relay]") { + // Unlike the null-drainOutbox/markRelayed cases above, relay() must not + // actually dereference the null sink (sink->append() on a null + // shared_ptr is a null-pointer virtual dispatch -- real UB, + // not a catchable exception -- see the comment below). Pairing the null + // sink with an empty drainOutbox() keeps relay() on its early-return path + // (`if (rows.empty()) return {};`), which runs *after* + // logIfAnyDepNull() but *before* any sink use -- so the null-sink warning + // fires without ever calling through the null pointer. + std::vector logged; + morph::log::ScopedLoggerOverride guard{ + [&](morph::log::LogLevel, std::string_view msg) { logged.emplace_back(msg); }, + morph::log::LogLevel::debug, + }; + + OutboxRelay relay; + relay.drainOutbox = [] { return std::vector{}; }; + relay.markRelayed = [](std::span) {}; + // relay.sink left null on purpose. + + auto result = relay.relay(); + REQUIRE(result.relayed == 0); + + bool sawWarning = std::any_of(logged.begin(), logged.end(), [](const std::string& line) { + return line.find("null sink") != std::string::npos; + }); + REQUIRE(sawWarning); +} + +// A null `sink` reaching relay()'s *sink-using* path (i.e. with drainOutbox() +// returning rows) is deliberately NOT exercised here: unlike // `drainOutbox`/`markRelayed` (null std::function -> catchable // std::bad_function_call), `sink` is a std::shared_ptr, so // `sink->append(row)` on a null sink is a null-pointer virtual dispatch -- @@ -373,10 +403,10 @@ TEST_CASE("OutboxRelay::relay(): a null drainOutbox is logged, not rejected, at // unit test would need a signal/guard-page harness like // test_bridge_lifetime.cpp's POSIX-only hasSubscribers() case, which is far // more machinery than this one branch warrants and still would not run on -// Windows/MSVC, where this suite also builds. The logIfAnyDepNull() call -// itself (the actual branch under test) is still reached and its warning -// still fires -- only the subsequent crash is left unexercised. Tracked as -// LASTRADA-Software/morph#95. +// Windows/MSVC, where this suite also builds. The logIfAnyDepNull() call's +// null-sink warning line itself is now covered by the empty-drainOutbox test +// above -- only the subsequent crash on a non-empty drain is left +// unexercised. Tracked as LASTRADA-Software/morph#95. TEST_CASE("OutboxRelay + FileActionLog: re-relay after a simulated process restart dedups via the sink", "[outbox][relay][file_action_log]") {