morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
owner_probe_recorder.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <atomic>
6#include <core/async/ExecutorContext.hpp>
7#include <core/async/IExecutor.hpp>
8#include <cstddef>
9#include <mutex>
10#include <string>
11#include <string_view>
12#include <thread>
13#include <vector>
14
16
17/// @file
18/// @brief `morph::testing::OwnerProbeRecorder`: observes the owner checks a test's code makes, instead of
19/// letting a debug build assert on them.
20
21namespace morph::testing {
22
23/// @brief Records every owner check a loop-owned body makes while this object is
24/// alive (`morph::exec::detail::noteOwner`), and what the calling thread's
25/// executor scope said at that moment.
26///
27/// What a posted-call test reads: a verb called from a thread that is not the
28/// loop, whose body then shows up here with `onOwner == true`, was posted. A
29/// body run inline on the caller's thread shows up with `onOwner == false`.
30/// The scope is read here, by the test, not taken from the component.
32public:
33 /// @brief One owner check.
34 struct Seen {
35 /// @brief The site the check named.
36 std::string site;
37 /// @brief Whether the checking thread was inside a task of the owner the check named.
38 bool onOwner{false};
39 /// @brief Whether the check named the owner this recorder expects.
40 bool expectedOwner{false};
41 };
42
43 /// @brief Installs this recorder as the process's owner probe.
44 /// @param expected The owner every check is expected to name: the
45 /// `IoLoop`'s loop.
46 explicit OwnerProbeRecorder(::core::async::IExecutor const& expected) : _expected{&expected} {
47 current().store(this);
48 ::morph::exec::detail::ownerProbe().store(&OwnerProbeRecorder::record);
49 }
50
51 /// @brief Uninstalls the probe, then waits for every thread still inside `record()`.
53 ::morph::exec::detail::ownerProbe().store(nullptr);
54 current().store(nullptr);
55 // A thread that read `current()` before it was cleared may still be
56 // inside `record()`, about to lock this object's mutex. Its members
57 // must outlive it.
58 while (inRecord().load() != 0) {
59 std::this_thread::yield();
60 }
61 }
62
64 OwnerProbeRecorder& operator=(const OwnerProbeRecorder&) = delete;
66 OwnerProbeRecorder& operator=(OwnerProbeRecorder&&) = delete;
67
68 /// @brief The checks made at one site.
69 /// @param site The site name.
70 /// @return Every check made at @p site so far.
71 [[nodiscard]] std::vector<Seen> at(std::string_view site) const {
72 std::scoped_lock const lock{_mtx};
73 std::vector<Seen> matching;
74 for (auto const& seen : _seen) {
75 if (seen.site == site) {
76 matching.push_back(seen);
77 }
78 }
79 return matching;
80 }
81
82 /// @brief How many checks were made at one site.
83 /// @param site The site name.
84 /// @return How many checks were made at @p site.
85 [[nodiscard]] std::size_t count(std::string_view site) const { return at(site).size(); }
86
87 /// @brief Whether every check at one site ran posted to the expected owner.
88 /// @param site The site name.
89 /// @return Whether at least one check was made at @p site, and every one
90 /// of them ran inside a task of the expected owner.
91 [[nodiscard]] bool allPosted(std::string_view site) const {
92 auto const seen = at(site);
93 if (seen.empty()) {
94 return false;
95 }
96 return std::ranges::all_of(seen, [](auto const& one) { return one.onOwner && one.expectedOwner; });
97 }
98
99private:
100 static std::atomic<OwnerProbeRecorder*>& current() {
101 static std::atomic<OwnerProbeRecorder*> recorder{nullptr};
102 return recorder;
103 }
104
105 /// How many threads are inside `record()`. Counted before `current()` is
106 /// read, so the destructor, which clears `current()` and then waits for
107 /// this to reach zero, cannot miss a caller that has seen the pointer.
108 static std::atomic<std::size_t>& inRecord() {
109 static std::atomic<std::size_t> count{0};
110 return count;
111 }
112
113 static void record(char const* site, ::core::async::IExecutor const& owner) noexcept {
114 struct InRecord {
115 InRecord() { inRecord().fetch_add(1); }
116 ~InRecord() { inRecord().fetch_sub(1); }
117 InRecord(const InRecord&) = delete;
118 InRecord& operator=(const InRecord&) = delete;
119 InRecord(InRecord&&) = delete;
120 InRecord& operator=(InRecord&&) = delete;
121 } const counted;
122 OwnerProbeRecorder* const self = current().load();
123 if (self == nullptr) {
124 return;
125 }
126 bool const onOwner = ::core::async::ExecutorScope::anyInForce(
127 [&owner](::core::async::ExecutorScope const& scope) { return &scope.executor() == &owner; });
128 try {
129 std::scoped_lock const lock{self->_mtx};
130 self->_seen.push_back(Seen{.site = site, .onOwner = onOwner, .expectedOwner = &owner == self->_expected});
131 } catch (...) { // NOLINT(bugprone-empty-catch)
132 // An allocation failure loses one record; the test then sees too
133 // few, never a false pass.
134 }
135 }
136
137 ::core::async::IExecutor const* _expected;
138 mutable std::mutex _mtx;
139 std::vector<Seen> _seen;
140};
141
142} // namespace morph::testing
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
Records every owner check a loop-owned body makes while this object is alive (morph::exec::detail::no...
std::vector< Seen > at(std::string_view site) const
The checks made at one site.
bool allPosted(std::string_view site) const
Whether every check at one site ran posted to the expected owner.
OwnerProbeRecorder(::core::async::IExecutor const &expected)
Installs this recorder as the process's owner probe.
std::size_t count(std::string_view site) const
How many checks were made at one site.
~OwnerProbeRecorder()
Uninstalls the probe, then waits for every thread still inside record().
The check every loop-owned body makes on entry: "am I on my owner?".
std::string site
The site the check named.
bool expectedOwner
Whether the check named the owner this recorder expects.
bool onOwner
Whether the checking thread was inside a task of the owner the check named.