morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
step_executor.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstddef>
5#include <deque>
6#include <functional>
7#include <mutex>
8#include <stdexcept>
9#include <utility>
10
11#include "../core/executor.hpp"
12
13/// @file
14/// @brief `morph::testing::StepExecutor`: an executor whose posted tasks run only when a test steps it.
15
16namespace morph::testing {
17
18/// @brief `IExecutor` that queues every posted task and runs them only when the
19/// test explicitly asks, one at a time.
20///
21/// The public interleaving-test harness: any server
22/// component built on `morph::exec::IExecutor` — `RemoteServer` included — can
23/// be driven with fully deterministic, hand-stepped task ordering by
24/// constructing it against a `StepExecutor` instead of a `ThreadPoolExecutor`.
25/// `RemoteServer` posts every dispatch (both the top-level `handle()` post and
26/// the per-model strand dispatch its internal `ModelStrands` performs) onto
27/// whichever `IExecutor` it was constructed with, so controlling that one
28/// executor is enough to control ordering end-to-end — no need to name
29/// `morph::exec::detail::ModelStrands` or `morph::exec::detail::ModelId` to
30/// get there. A test picks which of several pending tasks (e.g. two different
31/// models' queued work) to run next via `runOne()`, observing `RemoteServer`'s
32/// real per-model serialisation (a strand never posts its next task until the
33/// previous one has run) while still controlling the order two *different*
34/// models' work interleaves in.
35///
36/// Not thread-safe against concurrent `runOne()`/`runAll()` calls — intended
37/// for single-threaded, single-stepping test code, mirroring `MainThreadExecutor`'s
38/// "owning thread" contract but without its wall-clock `runFor()` drain.
40public:
41 /// @brief Enqueues @p task; does not run it.
42 /// @param task Callable to run on a later `runOne()`/`runAll()` call.
43 void post(std::function<void()> task) override {
44 std::scoped_lock const lock{_mtx};
45 _queue.push_back(std::move(task));
46 }
47
48 /// @brief Runs exactly one queued task, oldest first (FIFO).
49 /// @return `true` if a task was run, `false` if the queue was empty.
50 bool runOne() {
51 std::function<void()> task;
52 {
53 std::scoped_lock const lock{_mtx};
54 if (_queue.empty()) {
55 return false;
56 }
57 task = std::move(_queue.front());
58 _queue.pop_front();
59 }
60 // States the scope around the task, as `MainThreadExecutor` does, so
61 // `exec::runningOn(*this)` and every owner check holds inside it.
62 ::core::async::ExecutorScope const scope{coreExecutor()};
63 task();
64 return true;
65 }
66
67 /// @brief Runs every task currently queued, including ones a running task
68 /// itself posts (e.g. a strand re-arming for its next queued item).
69 ///
70 /// Bounded at @p maxSteps rather than looping until the queue is empty: a
71 /// task that keeps re-posting more work to this executor (a bug in the
72 /// code under test, or a harness misuse) would otherwise turn this into an
73 /// undetectable infinite loop, hanging the test process with no assertion
74 /// failure and no compile-time signal. Real drains in this suite finish
75 /// within a handful of steps, so the default is generous headroom, not a
76 /// tight bound callers need to reason about.
77 /// @param maxSteps Upper bound on tasks run before giving up.
78 /// @return Number of tasks run.
79 std::size_t runAll(std::size_t maxSteps = 10'000) {
80 std::size_t ran = 0;
81 while (ran < maxSteps && runOne()) {
82 ++ran;
83 }
84 if (ran == maxSteps) {
85 throw std::runtime_error(
86 "StepExecutor::runAll: exceeded maxSteps -- a task is likely re-posting "
87 "indefinitely; use runOne() to step through and find it");
88 }
89 return ran;
90 }
91
92 /// @brief Number of tasks currently queued, awaiting a `runOne()`/`runAll()`.
93 /// @return Queue depth.
94 [[nodiscard]] std::size_t pending() const {
95 std::scoped_lock const lock{_mtx};
96 return _queue.size();
97 }
98
99private:
100 mutable std::mutex _mtx;
101 std::deque<std::function<void()>> _queue;
102};
103
104} // namespace morph::testing
IExecutor that queues every posted task and runs them only when the test explicitly asks,...
std::size_t runAll(std::size_t maxSteps=10 '000)
Runs every task currently queued, including ones a running task itself posts (e.g....
void post(std::function< void()> task) override
Enqueues task; does not run it.
std::size_t pending() const
Number of tasks currently queued, awaiting a runOne()/runAll().
bool runOne()
Runs exactly one queued task, oldest first (FIFO).
Abstract executor interface.
Definition executor.hpp:70
::core::async::IExecutor & coreExecutor() noexcept
This executor as a core-cpp executor: the identity an core::async::ExecutorScope names.
Definition executor.hpp:100