From 5dbd8718a39595e7dd78b450fdc39c1bd3ffcd13 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 16 Aug 2026 09:16:26 +0300 Subject: [PATCH] tests(event_poller): cover start()'s fatal-guard, the only untested public method Every other test in this file only exercises the timer indirectly (the constructor's own _timer.start(interval), handleError's _timer.stop(), resume()'s own _timer.start()) -- start()/stop() (the pause/resume pair a hidden poll view uses, per event_poller.hpp's own doc comments) were never called directly by anything, so start()'s only branch (`if (!_fatal)`) never ran either arm. The other 79 flagged gap-units in this file (14 pure misses, ~66 partials) are confirmed tooling noise, not real gaps: every pure-miss line is either a FAIL("... must not run ...") body inside an onEvent/onFatalError lambda (whose entire purpose is to prove a scenario the test's own passing run already disproves), or the "other half" of a dispatch closure a sibling test's own scenario exercises instead of this one. The partials sampled are Catch2 macro-expansion branches, a ternary whose both arms are exercised across the file in aggregate just not within one closure instantiation, or defensive invariant conditionals proving a double-report bug can't happen -- none are real EventPoller-class gaps. --- examples/common/testkit/test_event_poller.cpp | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/examples/common/testkit/test_event_poller.cpp b/examples/common/testkit/test_event_poller.cpp index eeb0acf0..bc98a59e 100644 --- a/examples/common/testkit/test_event_poller.cpp +++ b/examples/common/testkit/test_event_poller.cpp @@ -463,6 +463,49 @@ TEST_CASE("EventPoller clears its in-flight flag even when onEvent throws", "[gu REQUIRE(morph::ladder::testkit::pumpUntil([&] { return !poller.busy(); })); } +TEST_CASE("EventPoller::start rearms the timer, but refuses once a fatal error has stopped it for good", + "[gui][event-poller]") { + // start()/stop() are the pause/resume pair a hidden poll view uses + // (event_poller.hpp's own doc comments on both) -- distinct from + // pollOnce() (drives one tick regardless of the timer) and from + // resume() (also clears _fatal). Nothing else in this file calls either + // one directly: every other test lets the constructor's own + // `_timer.start(interval)` and handleError's own `_timer.stop()` be the + // only callers, so start()'s `if (!_fatal)` guard -- the one branch + // start() has -- has never actually run either arm from here. + resetFeedControl(); + FeedModel::control.events = {{.id = 1, .summary = "a"}}; + + morph::ladder::gui::AppContext ctx{morph::ladder::gui::Local{}}; + auto handler = std::make_shared>(ctx.bridge(), ctx.executor()); + + int fatalCount = 0; + Poller poller{ctx.bridge(), /*startingCursor=*/0, makeDispatch(handler), [](const FeedEvent&) {}, + [&](const QString&) { ++fatalCount; }, std::chrono::hours{1}}; + + REQUIRE(poller.running()); + poller.stop(); + REQUIRE_FALSE(poller.running()); + + // Not fatal: start() rearms it, same as a poll view coming back into view. + poller.start(); + CHECK(poller.running()); + + // Now drive a genuinely fatal failure, stopping the timer for good. + poller.stop(); + FeedModel::control.throwNotFound = true; + poller.pollOnce(); + REQUIRE(morph::ladder::testkit::pumpUntil([&] { return !poller.busy(); })); + REQUIRE(fatalCount == 1); + REQUIRE(poller.fatalErrorReported()); + REQUIRE_FALSE(poller.running()); + + // start()'s own guard refuses to rearm a fatally-stopped poller -- only + // resume() can undo that (see the dedicated resume() test below). + poller.start(); + CHECK_FALSE(poller.running()); +} + TEST_CASE("EventPoller::resume clears a fatal error and polls again from a new cursor", "[gui][event-poller]") { resetFeedControl(); FeedModel::control.throwNotFound = true;