Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/morph/forms/i18n.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Expand Down
6 changes: 6 additions & 0 deletions include/morph/forms/layout.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Expand Down
6 changes: 6 additions & 0 deletions include/morph/offline/reconnect_coordinator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 "?";
}
}
Expand Down
22 changes: 20 additions & 2 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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[@]}"
Expand All @@ -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"
Expand All @@ -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"

Expand Down
21 changes: 21 additions & 0 deletions tests/test_logger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]") {
Expand Down
40 changes: 35 additions & 5 deletions tests/test_outbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActionLog> 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<std::string> 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<LogEntry>{}; };
relay.markRelayed = [](std::span<const LogEntry>) {};
// 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<IActionLog>, so
// `sink->append(row)` on a null sink is a null-pointer virtual dispatch --
Expand All @@ -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]") {
Expand Down
Loading