Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qmessagespy_p.h
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#ifndef QMESSAGESPY_P_H
5#define QMESSAGESPY_P_H
6
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qstring.h>
9#include <QtCore/qspan.h>
10#include <QtCore/qmutex.h>
11#include <QtCore/qregularexpression.h>
12#include <QtTest/qtest.h>
13
14#include <QtCore/q20algorithm.h>
15#include <QtCore/q20vector.h>
16#include <bitset>
17#include <chrono>
18#include <memory>
19#include <variant>
20#include <vector>
21
22//
23// W A R N I N G
24// -------------
25//
26// This file is not part of the Qt API. It exists purely as an
27// implementation detail. This header file may change from version to
28// version without notice, or even be removed.
29//
30// We mean it.
31//
32
33QT_BEGIN_NAMESPACE
34
35namespace QtMultimediaPrivate {
36
37/*!
38 \internal
39
40 QLoggingCategoryEnabler temporarily enables a logging category for all
41 message types, and restores the original state on destruction.
42
43 Usage:
44 \code
45 QLoggingCategoryEnabler enable(qLcMyCategory());
46 // all message types for qLcMyCategory are now active for this scope
47 \endcode
48*/
50{
52public:
54 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
55 : m_category(const_cast<QLoggingCategory &>(category))
56 {
57 for (int type = QtDebugMsg; type <= QtFatalMsg; ++type) {
58 auto msgType = static_cast<QtMsgType>(type);
60 if (!m_wasEnabled[type])
62 }
63 }
64
66 {
67 for (int type = QtDebugMsg; type <= QtFatalMsg; ++type) {
68 auto msgType = static_cast<QtMsgType>(type);
69 if (!m_wasEnabled[type])
71 }
72 }
73
74private:
76 std::bitset<QtFatalMsg + 1> m_wasEnabled;
77};
78
79/*!
80 \internal
81
82 QMessageSpy intercepts Qt logging messages via qInstallMessageHandler.
83 Messages matching a pending expect() are silently consumed; all other
84 messages are forwarded to the previous handler (e.g. QTest's default).
85
86 Usage:
87 \code
88 QMessageSpy spy;
89
90 // Register expectation BEFORE triggering the message:
91 auto token = spy.expect(QtDebugMsg, "expected message");
92 triggerCodeThatLogs();
93 QVERIFY(token.wait());
94
95 // Regex variant:
96 auto token2 = spy.expect(QtWarningMsg, QRegularExpression("file: .*\\.wav"));
97 triggerCode();
98 QVERIFY(token2.wait(2000ms));
99 \endcode
100
101 Multiple waiters are supported simultaneously.
102*/
104{
106
107 struct PendingExpect
108 {
109 const QtMsgType type;
111 bool matched = false;
112 };
113
114public:
115 /*!
116 \internal
117 WaitToken is returned by expect(). Call wait() on it after the
118 message-generating code runs.
119 */
121 {
122 public:
123 WaitToken() = delete;
124 WaitToken(WaitToken &&) noexcept = default;
125
126 [[nodiscard]] bool
127 wait(std::chrono::milliseconds timeout = std::chrono::milliseconds{ 5000 }) const
128 {
129 return QTest::qWaitFor([this]() {
130 return m_state->matched;
131 }, timeout);
132 }
133
134 [[nodiscard]] bool matched() const { return m_state->matched; }
135
136 private:
137 friend class QMessageSpy;
138 explicit WaitToken(std::shared_ptr<PendingExpect> state) : m_state(std::move(state))
139 {
140 Q_ASSERT(m_state);
141 }
142 const std::shared_ptr<PendingExpect> m_state;
143 };
144
145 explicit QMessageSpy(const QLoggingCategory &category)
146 : QMessageSpy(QLatin1StringView(category.categoryName()))
147 {
148 }
149
150 explicit QMessageSpy(QLatin1StringView category = {})
153 }
154 {
155 Q_ASSERT(!s_instance);
156 s_instance = this;
157 m_previousHandler = qInstallMessageHandler(messageHandler);
158 }
159
161 {
162 qInstallMessageHandler(m_previousHandler);
163 s_instance = nullptr;
164 }
165
166 /*!
167 Register an expectation for a message of \a type matching \a message
168 exactly. Returns a WaitToken; call wait() on it after triggering the
169 message. The matching message is consumed (not forwarded to the previous
170 handler).
171 */
172 [[nodiscard]] WaitToken expect(QtMsgType type, const QString &message)
173 {
174 auto state = std::make_shared<PendingExpect>(PendingExpect{ type, message, false });
175 std::lock_guard lock(m_mutex);
176 m_pending.push_back(state);
177 return WaitToken{
178 std::move(state),
179 };
180 }
181
182 /*!
183 Register an expectation for a message of \a type matching \a pattern.
184 */
185 [[nodiscard]] WaitToken expect(QtMsgType type, const QRegularExpression &pattern)
186 {
187 auto state =
188 std::make_shared<PendingExpect>(PendingExpect{ type, pattern, false });
189 std::lock_guard lock(m_mutex);
190 m_pending.push_back(state);
191 return WaitToken{
192 std::move(state),
193 };
194 }
195
196private:
197 static bool matchesPending(PendingExpect &p, QtMsgType type, const QString &msg)
198 {
199 if (p.matched || p.type != type)
200 return false;
201 return std::visit([&](auto &m) -> bool {
202 using T = std::decay_t<decltype(m)>;
203 if constexpr (std::is_same_v<T, QString>)
204 return m == msg;
205 else
206 return m.match(msg).hasMatch();
207 }, p.matcher);
208 }
209
210 static void messageHandler(QtMsgType type, const QMessageLogContext &context,
211 const QString &msg)
212 {
213 Q_ASSERT(s_instance);
214
215 if (!(s_instance->m_categoryFilter)
216 || QByteArrayView{ context.category } == s_instance->m_categoryFilter) {
217 std::lock_guard lock(s_instance->m_mutex);
218 bool doCleanupOnExit = false;
219 auto cleanup = qScopeGuard([&] {
220 if (!doCleanupOnExit)
221 q20::erase_if(s_instance->m_pending, [](const std::weak_ptr<PendingExpect> &p) {
222 auto locked = p.lock();
223 return !locked || p.lock()->matched;
224 });
225 });
226
227 // Check pending expects against the raw message first; first match wins and is consumed
228 for (std::weak_ptr p : s_instance->m_pending) {
229 auto pending = p.lock();
230 if (!pending) {
231 doCleanupOnExit = true;
232 continue;
233 }
234 if (matchesPending(*pending, type, msg)) {
235 pending->matched = true;
236 doCleanupOnExit = true;
237 return; // consumed — do NOT forward
238 }
239 }
240 }
241
242 // Not consumed — forward to previous handler
243 if (s_instance->m_previousHandler)
244 s_instance->m_previousHandler(type, context, msg);
245 }
246
247 static inline QMessageSpy *s_instance = nullptr;
248
249 QtMessageHandler m_previousHandler = nullptr;
250 QMutex m_mutex;
251 const std::optional<QByteArray> m_categoryFilter;
252 std::vector<std::weak_ptr<PendingExpect>> m_pending;
253};
254
255} // namespace QtMultimediaPrivate
256
257QT_END_NAMESPACE
258
259#endif // QMESSAGESPY_P_H
WaitToken(WaitToken &&) noexcept=default
bool wait(std::chrono::milliseconds timeout=std::chrono::milliseconds{ 5000 }) const
QMessageSpy(const QLoggingCategory &category)
WaitToken expect(QtMsgType type, const QString &message)
Register an expectation for a message of type matching message exactly.
QMessageSpy(QLatin1StringView category={})