morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
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
16
namespace
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.
39
class
StepExecutor
:
public
::morph::exec::IExecutor
{
40
public
:
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
99
private
:
100
mutable
std::mutex _mtx;
101
std::deque<std::function<void()>> _queue;
102
};
103
104
}
// namespace morph::testing
morph::testing::StepExecutor
IExecutor that queues every posted task and runs them only when the test explicitly asks,...
Definition
step_executor.hpp:39
morph::testing::StepExecutor::runAll
std::size_t runAll(std::size_t maxSteps=10 '000)
Runs every task currently queued, including ones a running task itself posts (e.g....
Definition
step_executor.hpp:79
morph::testing::StepExecutor::post
void post(std::function< void()> task) override
Enqueues task; does not run it.
Definition
step_executor.hpp:43
morph::testing::StepExecutor::pending
std::size_t pending() const
Number of tasks currently queued, awaiting a runOne()/runAll().
Definition
step_executor.hpp:94
morph::testing::StepExecutor::runOne
bool runOne()
Runs exactly one queued task, oldest first (FIFO).
Definition
step_executor.hpp:50
morph::exec::IExecutor
Abstract executor interface.
Definition
executor.hpp:70
morph::exec::IExecutor::coreExecutor
::core::async::IExecutor & coreExecutor() noexcept
This executor as a core-cpp executor: the identity an core::async::ExecutorScope names.
Definition
executor.hpp:100
morph
testing
step_executor.hpp
Generated by
1.17.0