1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qsignalspy.h" |
5 | |
6 | QT_BEGIN_NAMESPACE |
7 | |
8 | /*! |
9 | \class QSignalSpy |
10 | \inmodule QtTest |
11 | |
12 | \brief The QSignalSpy class enables introspection of signal emission. |
13 | |
14 | QSignalSpy can connect to any signal of any object and records its emission. |
15 | QSignalSpy itself is a list of QVariant lists. Each emission of the signal |
16 | will append one item to the list, containing the arguments of the signal. |
17 | |
18 | The following example records all signal emissions for the \c clicked() signal |
19 | of a QCheckBox: |
20 | |
21 | \snippet code/doc_src_qsignalspy.cpp 0 |
22 | |
23 | \c{spy.takeFirst()} returns the arguments for the first emitted signal, as a |
24 | list of QVariant objects. The \c clicked() signal has a single bool argument, |
25 | which is stored as the first entry in the list of arguments. |
26 | |
27 | The example below catches a signal from a custom object: |
28 | |
29 | \snippet code/doc_src_qsignalspy.cpp 1 |
30 | |
31 | \note Non-standard data types need to be registered, using |
32 | the qRegisterMetaType() function, before you can create a |
33 | QSignalSpy. For example: |
34 | |
35 | \snippet code/doc_src_qsignalspy.cpp 2 |
36 | |
37 | To retrieve the instance, you can use qvariant_cast: |
38 | |
39 | \snippet code/doc_src_qsignalspy.cpp 3 |
40 | |
41 | \section1 Verifying Signal Emissions |
42 | |
43 | The QSignalSpy class provides an elegant mechanism for capturing the list |
44 | of signals emitted by an object. However, you should verify its validity |
45 | after construction. The constructor does a number of sanity checks, such as |
46 | verifying that the signal to be spied upon actually exists. To make the |
47 | diagnosis of test failures easier, the results of these checks should be |
48 | checked by calling \c QVERIFY(spy.isValid()) before proceeding further with |
49 | a test. |
50 | |
51 | \sa QVERIFY() |
52 | */ |
53 | |
54 | /*! \fn QSignalSpy::QSignalSpy(const QObject *object, const char *signal) |
55 | |
56 | Constructs a new QSignalSpy that listens for emissions of the \a signal |
57 | from the QObject \a object. If QSignalSpy is not able to listen for a |
58 | valid signal (for example, because \a object is \nullptr or \a signal does |
59 | not denote a valid signal of \a object), an explanatory warning message |
60 | will be output using qWarning() and subsequent calls to \c isValid() will |
61 | return false. |
62 | |
63 | Example: |
64 | \snippet code/doc_src_qsignalspy.cpp 4 |
65 | */ |
66 | |
67 | /*! \fn template <typename PointerToMemberFunction> QSignalSpy::QSignalSpy(const QObject *object, PointerToMemberFunction signal) |
68 | \since 5.4 |
69 | |
70 | Constructs a new QSignalSpy that listens for emissions of the \a signal |
71 | from the QObject \a object. If QSignalSpy is not able to listen for a |
72 | valid signal (for example, because \a object is \nullptr or \a signal does |
73 | not denote a valid signal of \a object), an explanatory warning message |
74 | will be output using qWarning() and subsequent calls to \c isValid() will |
75 | return false. |
76 | |
77 | Example: |
78 | \snippet code/doc_src_qsignalspy.cpp 6 |
79 | */ |
80 | |
81 | /*! \fn QSignalSpy::QSignalSpy(const QObject *obj, QMetaMethod signal) |
82 | \since 5.14 |
83 | |
84 | Constructs a new QSignalSpy that listens for emissions of the \a signal |
85 | from the QObject \a obj. If QSignalSpy is not able to listen for a |
86 | valid signal (for example, because \a obj is \nullptr or \a signal does |
87 | not denote a valid signal of \a obj), an explanatory warning message |
88 | will be output using qWarning() and subsequent calls to \c isValid() will |
89 | return false. |
90 | |
91 | This constructor is convenient to use when Qt's meta-object system is |
92 | heavily used in a test. |
93 | |
94 | Basic usage example: |
95 | \snippet code/doc_src_qsignalspy.cpp 7 |
96 | |
97 | Imagine we need to check whether all properties of the QWindow class |
98 | that represent minimum and maximum dimensions are properly writable. |
99 | The following example demonstrates one of the approaches: |
100 | \snippet code/doc_src_qsignalspy.cpp 8 |
101 | */ |
102 | |
103 | /*! \fn QSignalSpy::isValid() const |
104 | |
105 | Returns \c true if the signal spy listens to a valid signal, otherwise false. |
106 | */ |
107 | |
108 | /*! \fn QSignalSpy::signal() const |
109 | |
110 | Returns the normalized signal the spy is currently listening to. |
111 | */ |
112 | |
113 | /*! \fn bool QSignalSpy::wait(int timeout) |
114 | \since 5.0 |
115 | |
116 | This is an overloaded function, equivalent passing \a timeout to the |
117 | chrono overload: |
118 | \code |
119 | wait(std::chrono::milliseconds{timeout}); |
120 | \endcode |
121 | |
122 | Returns \c true if the signal was emitted at least once in \a timeout, |
123 | otherwise returns \c false. |
124 | */ |
125 | |
126 | /*! |
127 | \since 6.6 |
128 | |
129 | Starts an event loop that runs until the given signal is received |
130 | or \a timeout has passed, whichever happens first. |
131 | |
132 | \a timeout is any valid std::chrono::duration (std::chrono::seconds, |
133 | std::chrono::milliseconds ...etc). |
134 | |
135 | Returns \c true if the signal was emitted at least once in \a timeout, |
136 | otherwise returns \c false. |
137 | |
138 | Example: |
139 | \code |
140 | using namespace std::chrono_literals; |
141 | QSignalSpy spy(object, signal); |
142 | spy.wait(2s); |
143 | \endcode |
144 | */ |
145 | bool QSignalSpy::wait(std::chrono::milliseconds timeout) |
146 | { |
147 | QMutexLocker locker(&m_mutex); |
148 | Q_ASSERT(!m_waiting); |
149 | const qsizetype origCount = size(); |
150 | m_waiting = true; |
151 | locker.unlock(); |
152 | |
153 | m_loop.enterLoop(msecs: timeout); |
154 | |
155 | locker.relock(); |
156 | m_waiting = false; |
157 | return size() > origCount; |
158 | } |
159 | |
160 | static bool isSignalMetaMethodValid(QMetaMethod signal) |
161 | { |
162 | if (!signal.isValid()) { |
163 | qWarning(msg: "QSignalSpy: Null signal is not valid" ); |
164 | return false; |
165 | } |
166 | |
167 | if (signal.methodType() != QMetaMethod::Signal) { |
168 | qWarning(msg: "QSignalSpy: Not a signal: '%s'" , signal.methodSignature().constData()); |
169 | return false; |
170 | } |
171 | |
172 | return true; |
173 | } |
174 | |
175 | static bool isObjectValid(const QObject *object) |
176 | { |
177 | const bool valid = !!object; |
178 | |
179 | if (!valid) |
180 | qWarning(msg: "QSignalSpy: Cannot spy on a null object" ); |
181 | |
182 | return valid; |
183 | } |
184 | |
185 | QSignalSpy::ObjectSignal QSignalSpy::verify(const QObject *obj, const char *aSignal) |
186 | { |
187 | if (!isObjectValid(object: obj)) |
188 | return {}; |
189 | |
190 | if (!aSignal) { |
191 | qWarning(msg: "QSignalSpy: Null signal name is not valid" ); |
192 | return {}; |
193 | } |
194 | |
195 | if (((aSignal[0] - '0') & 0x03) != QSIGNAL_CODE) { |
196 | qWarning(msg: "QSignalSpy: Not a valid signal, use the SIGNAL macro" ); |
197 | return {}; |
198 | } |
199 | |
200 | const QByteArray ba = QMetaObject::normalizedSignature(method: aSignal + 1); |
201 | const QMetaObject * const mo = obj->metaObject(); |
202 | const int sigIndex = mo->indexOfMethod(method: ba.constData()); |
203 | if (sigIndex < 0) { |
204 | qWarning(msg: "QSignalSpy: No such signal: '%s'" , ba.constData()); |
205 | return {}; |
206 | } |
207 | |
208 | return verify(obj, signal: mo->method(index: sigIndex)); |
209 | } |
210 | |
211 | QSignalSpy::ObjectSignal QSignalSpy::verify(const QObject *obj, QMetaMethod signal) |
212 | { |
213 | if (isObjectValid(object: obj) && isSignalMetaMethodValid(signal)) |
214 | return {.obj: obj, .sig: signal}; |
215 | else |
216 | return {}; |
217 | } |
218 | |
219 | static QList<int> makeArgs(QMetaMethod member, const QObject *obj) |
220 | { |
221 | QList<int> result; |
222 | result.reserve(asize: member.parameterCount()); |
223 | for (int i = 0; i < member.parameterCount(); ++i) { |
224 | QMetaType tp = member.parameterMetaType(index: i); |
225 | if (!tp.isValid() && obj) { |
226 | void *argv[] = { &tp, &i }; |
227 | QMetaObject::metacall(const_cast<QObject*>(obj), |
228 | QMetaObject::RegisterMethodArgumentMetaType, |
229 | member.methodIndex(), argv); |
230 | } |
231 | if (!tp.isValid()) { |
232 | qWarning(msg: "QSignalSpy: Unable to handle parameter '%s' of type '%s' of method '%s'," |
233 | " use qRegisterMetaType to register it." , |
234 | member.parameterNames().at(i).constData(), |
235 | member.parameterTypes().at(i).constData(), |
236 | member.name().constData()); |
237 | } |
238 | result.append(t: tp.id()); |
239 | } |
240 | return result; |
241 | } |
242 | |
243 | class QSignalSpyPrivate : public QObject |
244 | { |
245 | QSignalSpy * const q; |
246 | public: |
247 | explicit QSignalSpyPrivate(QSignalSpy *qq) : q(qq) {} |
248 | |
249 | int qt_metacall(QMetaObject::Call call, int methodId, void **a) override; |
250 | }; |
251 | |
252 | QSignalSpy::QSignalSpy(ObjectSignal os) |
253 | : sig(os.sig.methodSignature()), |
254 | args(os.obj ? makeArgs(member: os.sig, obj: os.obj) : QList<int>{}) |
255 | { |
256 | if (!os.obj) |
257 | return; |
258 | |
259 | auto i = std::make_unique<QSignalSpyPrivate>(args: this); |
260 | |
261 | const auto signalIndex = os.sig.methodIndex(); |
262 | const auto slotIndex = QObject::staticMetaObject.methodCount(); |
263 | if (!QMetaObject::connect(sender: os.obj, signal_index: signalIndex, |
264 | receiver: i.get(), method_index: slotIndex, type: Qt::DirectConnection)) { |
265 | qWarning(msg: "QSignalSpy: QMetaObject::connect returned false. Unable to connect." ); |
266 | return; |
267 | } |
268 | |
269 | d_ptr = std::move(i); |
270 | } |
271 | |
272 | /*! |
273 | Destructor. |
274 | */ |
275 | QSignalSpy::~QSignalSpy() |
276 | = default; |
277 | |
278 | void QSignalSpy::appendArgs(void **a) |
279 | { |
280 | QList<QVariant> list; |
281 | list.reserve(asize: args.size()); |
282 | for (qsizetype i = 0; i < args.size(); ++i) { |
283 | const QMetaType::Type type = static_cast<QMetaType::Type>(args.at(i)); |
284 | if (type == QMetaType::QVariant) |
285 | list << *reinterpret_cast<QVariant *>(a[i + 1]); |
286 | else |
287 | list << QVariant(QMetaType(type), a[i + 1]); |
288 | } |
289 | QMutexLocker locker(&m_mutex); |
290 | append(t: std::move(list)); |
291 | |
292 | if (m_waiting) { |
293 | locker.unlock(); |
294 | m_loop.exitLoop(); |
295 | } |
296 | } |
297 | |
298 | /*! |
299 | \reimp |
300 | \internal |
301 | */ |
302 | int QSignalSpyPrivate::qt_metacall(QMetaObject::Call call, int methodId, void **a) |
303 | { |
304 | methodId = QObject::qt_metacall(call, methodId, a); |
305 | if (methodId < 0) |
306 | return methodId; |
307 | |
308 | if (call == QMetaObject::InvokeMetaMethod) { |
309 | if (methodId == 0) { |
310 | q->appendArgs(a); |
311 | } |
312 | --methodId; |
313 | } |
314 | return methodId; |
315 | } |
316 | |
317 | QT_END_NAMESPACE |
318 | |