From ccab70a8cdeb20ff735ca2a654c32f01f7bfabb6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 31 Jul 2026 17:51:34 +0400 Subject: [PATCH] Make chats-list wait non-fatal Task: 2026/07/31/survive-unloaded-chats-list-in-test-harness --- .agents/shared/test-loop.md | 7 +- Telegram/SourceFiles/test/test_runner.cpp | 134 +++++++++++++++++++++- Telegram/SourceFiles/test/test_runner.h | 1 + 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/.agents/shared/test-loop.md b/.agents/shared/test-loop.md index db57230787..ce8aa00caa 100644 --- a/.agents/shared/test-loop.md +++ b/.agents/shared/test-loop.md @@ -313,9 +313,10 @@ The repository carries a permanent test harness under (`Test::Active()`), with all of its `#ifdef`s inside the harness itself: - `test_runner.h` — the staged scenario engine: `Stage{name, run, until, then, timeout}`, - `waitEvent`, `waitForSessionReady`, `waitForChatsLoaded`; built-in per-stage timeouts, a - wall-clock watchdog (default 120s, `TDESKTOP_TEST_WATCHDOG` override), and guaranteed - `TEST_COMPLETE` + quit on every exit path including timeout. + `waitEvent`, `waitForSessionReady`, the normal bounded non-fatal `waitForChatsLoaded()`, and + explicit strict `waitForChatsLoadedStrict()`; timing out an ordinary `Stage` ends the whole + scenario, while the wall-clock watchdog (default 120s, `TDESKTOP_TEST_WATCHDOG` override) + guarantees `TEST_COMPLETE` + quit on every exit path including timeout. - `test_log.h` — evidence dir from `TDESKTOP_TEST_EVIDENCE_DIR` (the workspace `test-run` helper sets it), flushed absolute-path logging, `Step/Pass/Fail/Check/Note`, `CheckNear` tolerance assertions, `LogGeometry`, the standard markers. diff --git a/Telegram/SourceFiles/test/test_runner.cpp b/Telegram/SourceFiles/test/test_runner.cpp index 388a56727d..6b3402461d 100644 --- a/Telegram/SourceFiles/test/test_runner.cpp +++ b/Telegram/SourceFiles/test/test_runner.cpp @@ -51,6 +51,113 @@ constexpr auto kFinishDrainDelay = crl::time(500); return domain.started() && domain.active().sessionExists(); } +[[nodiscard]] bool ChatsLoaded() { + return SessionReady() + && Core::App().domain().active().session().data().chatsListLoaded(); +} + +enum class ChatsLoadedWaitOutcome { + Pending, + Loaded, + TimedOut, +}; + +struct ChatsLoadedWaitState { + ChatsLoadedWaitOutcome outcome = ChatsLoadedWaitOutcome::Pending; + crl::time deadline = 0; + base::Timer deadlineTimer; + rpl::lifetime loadedLifetime; +}; + +void ResolveChatsLoadedWait( + const std::shared_ptr &state, + ChatsLoadedWaitOutcome outcome) { + if (state->outcome != ChatsLoadedWaitOutcome::Pending) { + return; + } + state->outcome = outcome; + state->deadlineTimer.cancel(); + state->loadedLifetime.destroy(); +} + +void ResolveChatsLoadedWaitAt( + const std::shared_ptr &state, + crl::time observedAt) { + ResolveChatsLoadedWait( + state, + (observedAt < state->deadline) + ? ChatsLoadedWaitOutcome::Loaded + : ChatsLoadedWaitOutcome::TimedOut); +} + +void ArmChatsLoadedDeadline( + const std::shared_ptr &state) { + const auto now = crl::now(); + if (now >= state->deadline) { + ResolveChatsLoadedWait( + state, + ChatsLoadedWaitOutcome::TimedOut); + } else { + state->deadlineTimer.callOnce(state->deadline - now); + } +} + +void StartChatsLoadedWait( + const std::shared_ptr &state, + crl::time stageStarted, + crl::time timeout) { + state->deadline = stageStarted + timeout; + const auto weak = std::weak_ptr(state); + state->deadlineTimer.setCallback([weak] { + if (const auto state = weak.lock()) { + ArmChatsLoadedDeadline(state); + } + }); + if (ChatsLoaded()) { + ResolveChatsLoadedWaitAt(state, crl::now()); + return; + } + Core::App().domain().activeSessionValue( + ) | rpl::map([](Main::Session *session) { + if (!session) { + return rpl::never(); + } + return session->data().chatsListLoaded() + ? rpl::single(nullptr) + : session->data().chatsListLoadedEvents(); + }) | rpl::flatten_latest( + ) | rpl::filter([](Data::Folder *folder) { + return !folder; + }) | rpl::on_next([weak] { + if (const auto state = weak.lock()) { + ResolveChatsLoadedWaitAt(state, crl::now()); + } + }, state->loadedLifetime); + ArmChatsLoadedDeadline(state); +} + +void ObserveChatsLoadedDeadline( + const std::shared_ptr &state) { + if (state->outcome == ChatsLoadedWaitOutcome::Pending + && crl::now() >= state->deadline) { + ResolveChatsLoadedWait( + state, + ChatsLoadedWaitOutcome::TimedOut); + } +} + +[[nodiscard]] bool ChatsLoadedWaitFinished( + const std::shared_ptr &state) { + ObserveChatsLoadedDeadline(state); + return state->outcome != ChatsLoadedWaitOutcome::Pending; +} + +[[nodiscard]] bool ChatsLoadedWaitSucceeded( + const std::shared_ptr &state) { + ObserveChatsLoadedDeadline(state); + return state->outcome == ChatsLoadedWaitOutcome::Loaded; +} + } // namespace void Runner::add(Stage stage) { @@ -76,13 +183,32 @@ void Runner::waitForSessionReady(crl::time timeout) { } void Runner::waitForChatsLoaded(crl::time timeout) { + const auto state = std::make_shared(); add({ .name = u"wait for chats loaded"_q, - .until = [] { - return SessionReady() - && Core::App().domain().active().session().data( - ).chatsListLoaded(); + .run = [=] { + StartChatsLoadedWait(state, _stageStarted, timeout); }, + .until = [=] { return ChatsLoadedWaitFinished(state); }, + .then = [=] { + Note(u"chats loaded wait: loaded=%1 elapsedMs=%2"_q.arg( + (state->outcome == ChatsLoadedWaitOutcome::Loaded) + ? u"true"_q + : u"false"_q, + QString::number(crl::now() - _stageStarted))); + }, + .timeout = timeout, + }); +} + +void Runner::waitForChatsLoadedStrict(crl::time timeout) { + const auto state = std::make_shared(); + add({ + .name = u"wait for chats loaded (strict)"_q, + .run = [=] { + StartChatsLoadedWait(state, _stageStarted, timeout); + }, + .until = [=] { return ChatsLoadedWaitSucceeded(state); }, .timeout = timeout, }); } diff --git a/Telegram/SourceFiles/test/test_runner.h b/Telegram/SourceFiles/test/test_runner.h index c09532849b..1b6b9d5cc9 100644 --- a/Telegram/SourceFiles/test/test_runner.h +++ b/Telegram/SourceFiles/test/test_runner.h @@ -36,6 +36,7 @@ public: crl::time timeout = kStartupStageTimeout); void waitForSessionReady(crl::time timeout = kStartupStageTimeout); void waitForChatsLoaded(crl::time timeout = kStartupStageTimeout); + void waitForChatsLoadedStrict(crl::time timeout = kStartupStageTimeout); [[nodiscard]] bool empty() const;