1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2021 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtTest module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #ifndef QPROPERTYTESTHELPER_P_H |
41 | #define QPROPERTYTESTHELPER_P_H |
42 | |
43 | // |
44 | // W A R N I N G |
45 | // ------------- |
46 | // |
47 | // This file is not part of the Qt API. It exists purely as an |
48 | // implementation detail. This header file may change from version to |
49 | // version without notice, or even be removed. |
50 | // |
51 | // We mean it. |
52 | // |
53 | |
54 | #include <QtCore/QObject> |
55 | #include <QtTest/QSignalSpy> |
56 | #include <QTest> |
57 | |
58 | QT_BEGIN_NAMESPACE |
59 | |
60 | namespace QTestPrivate { |
61 | |
62 | /*! |
63 | \internal |
64 | |
65 | This helper macro is used as a wrapper around \l QVERIFY2() to provide a |
66 | detailed error message in case of failure. It is intended to be used \e only |
67 | in the helper functions below. |
68 | |
69 | The custom \a comparator method is used to check if the \a actual and |
70 | \a expected values are equal or not. |
71 | |
72 | The macro uses a custom \a represent callback to generate the string |
73 | representation of \a actual and \a expected. |
74 | |
75 | The error message is close to the one provided by the \l QCOMPARE() macro. |
76 | Specifically the implementation is taken from the \c formatFailMessage() |
77 | function, which is defined in the \c qtestresult.cpp file. |
78 | */ |
79 | #define QPROPERTY_TEST_COMPARISON_HELPER(actual, expected, comparator, represent) \ |
80 | do { \ |
81 | const size_t maxMsgLen = 1024; \ |
82 | char msg[maxMsgLen] = { '\0' }; \ |
83 | auto actualStr = represent(actual); \ |
84 | auto expectedStr = represent(expected); \ |
85 | const size_t len1 = mbstowcs(nullptr, #actual, maxMsgLen); \ |
86 | const size_t len2 = mbstowcs(nullptr, #expected, maxMsgLen); \ |
87 | qsnprintf(msg, maxMsgLen, "\n%s\n Actual (%s)%*s %s\n Expected (%s)%*s %s\n", \ |
88 | "Comparison failed!", #actual, qMax(len1, len2) - len1 + 1, ":", \ |
89 | actualStr ? actualStr : "<null>", #expected, qMax(len1, len2) - len2 + 1, ":", \ |
90 | expectedStr ? expectedStr : "<null>"); \ |
91 | delete[] actualStr; \ |
92 | delete[] expectedStr; \ |
93 | QVERIFY2(comparator(actual, expected), msg); \ |
94 | } while (false) |
95 | |
96 | /*! |
97 | \internal |
98 | Basic testing of a bindable property. |
99 | |
100 | This helper function tests the behavior of bindable read/write property |
101 | \a propertyName, of type \c PropertyType, in class \c TestedClass. |
102 | The caller must supply an \a instance of \c TestedClass and two distinct |
103 | values, \a initial and \a changed, of \c PropertyType. |
104 | |
105 | Since the first part of the test sets the property to \a initial, it |
106 | \e {must not} be the default value of the property, or the check that it |
107 | was set will be vacuous. |
108 | |
109 | By default \c {operator==()} is used to compare values of the property and |
110 | \c {QTest::toString()} is used to generate proper error messages. |
111 | |
112 | If such comparison is not supported for \c PropertyType, or the comparison |
113 | it supports is not appropriate to this property, a custom \a comparator can |
114 | be supplied. |
115 | |
116 | Apart from that, a custom \a represent callback can also be specified to |
117 | generate a string representation of \c PropertyType. If supplied, it must |
118 | allocate its returned string using \c {new char[]}, so that it can be used |
119 | in place of \l {QTest::toString()}. |
120 | |
121 | \note Any test calling this method will need to call |
122 | \code |
123 | if (QTest::currentTestFailed()) |
124 | return; |
125 | \endcode |
126 | after doing so, if there is any later code in the test. If testing several |
127 | properties in one test method, emitting a debug message saying which |
128 | property failed, before returning, is a kindness to readers of the output. |
129 | */ |
130 | template<typename TestedClass, typename PropertyType> |
131 | void testReadWritePropertyBasics( |
132 | TestedClass &instance, const PropertyType &initial, const PropertyType &changed, |
133 | const char *propertyName, |
134 | std::function<bool(const PropertyType &, const PropertyType &)> comparator = |
135 | [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; }, |
136 | std::function<char *(const PropertyType &)> represent = |
137 | [](const PropertyType &val) { return QTest::toString(val); }) |
138 | { |
139 | // get the property |
140 | const QMetaObject *metaObject = instance.metaObject(); |
141 | QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName)); |
142 | QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(), |
143 | QByteArray("Preconditions not met for " ) + propertyName + '\n' + |
144 | "The type of initial and changed value does not match the type of the property.\n" |
145 | "Please ensure that the types match exactly (convertability is not enough).\n" |
146 | "You can provide the template types to the " |
147 | "function explicitly to force a certain type.\n" |
148 | "Expected was a " + metaProperty.metaType().name() |
149 | + " but " + QMetaType::fromType<PropertyType>().name() + " was provided." ); |
150 | |
151 | // in case the TestedClass has setProperty()/property() methods. |
152 | QObject &testedObj = static_cast<QObject &>(instance); |
153 | |
154 | QVERIFY2(metaProperty.isBindable() && metaProperty.isWritable(), |
155 | "Preconditions not met for " + QByteArray(propertyName)); |
156 | |
157 | QScopedPointer<QSignalSpy> spy(nullptr); |
158 | if (metaProperty.hasNotifySignal()) |
159 | spy.reset(new QSignalSpy(&instance, metaProperty.notifySignal())); |
160 | |
161 | testedObj.setProperty(propertyName, QVariant::fromValue(initial)); |
162 | QPROPERTY_TEST_COMPARISON_HELPER( |
163 | testedObj.property(propertyName).template value<PropertyType>(), initial, comparator, |
164 | represent); |
165 | if (spy) |
166 | QCOMPARE(spy->count(), 1); |
167 | |
168 | QUntypedBindable bindable = metaProperty.bindable(&instance); |
169 | |
170 | // Bind to the object's property (using both lambda and |
171 | // Qt:makePropertyBinding). |
172 | QProperty<PropertyType> propObserver(changed); |
173 | propObserver.setBinding(bindable.makeBinding()); |
174 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent); |
175 | |
176 | QProperty<PropertyType> propObserverLambda(changed); |
177 | propObserverLambda.setBinding( |
178 | [&]() { return testedObj.property(propertyName).template value<PropertyType>(); }); |
179 | QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent); |
180 | |
181 | testedObj.setProperty(propertyName, QVariant::fromValue(changed)); |
182 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent); |
183 | QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent); |
184 | if (spy) |
185 | QCOMPARE(spy->count(), 2); |
186 | |
187 | // Bind object's property to other property |
188 | QProperty<PropertyType> propSetter(initial); |
189 | QVERIFY(!bindable.hasBinding()); |
190 | bindable.setBinding(Qt::makePropertyBinding(propSetter)); |
191 | |
192 | QVERIFY(bindable.hasBinding()); |
193 | QPROPERTY_TEST_COMPARISON_HELPER( |
194 | testedObj.property(propertyName).template value<PropertyType>(), initial, comparator, |
195 | represent); |
196 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent); |
197 | QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent); |
198 | if (spy) |
199 | QCOMPARE(spy->count(), 3); |
200 | |
201 | // Count notifications triggered; should only happen on actual change. |
202 | int updateCount = 0; |
203 | auto handler = bindable.onValueChanged([&updateCount]() { ++updateCount; }); |
204 | Q_UNUSED(handler) |
205 | |
206 | propSetter.setValue(changed); |
207 | QPROPERTY_TEST_COMPARISON_HELPER( |
208 | testedObj.property(propertyName).template value<PropertyType>(), changed, comparator, |
209 | represent); |
210 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent); |
211 | QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent); |
212 | QCOMPARE(updateCount, 1); |
213 | if (spy) |
214 | QCOMPARE(spy->count(), 4); |
215 | |
216 | // Test that manually setting the value (even the same one) breaks the |
217 | // binding. |
218 | testedObj.setProperty(propertyName, QVariant::fromValue(changed)); |
219 | QVERIFY(!bindable.hasBinding()); |
220 | // Setting the same value should have no impact on udpateCount. |
221 | QCOMPARE(updateCount, 1); |
222 | |
223 | // value didn't change -> the signal should not be emitted |
224 | if (spy) |
225 | QCOMPARE(spy->count(), 4); |
226 | } |
227 | |
228 | /*! |
229 | \internal |
230 | Basic testing of a read-only bindable property. |
231 | |
232 | This helper function tests the behavior of bindable read-only property |
233 | \a propertyName, of type \c PropertyType, in class \c TestedClass. |
234 | The caller must supply an \a instance of \c TestedClass and two distinct |
235 | values, \a initial and \a changed, of \c PropertyType. |
236 | |
237 | When this function is called, the property's value must be \a initial. |
238 | The \a mutator must, when called, cause the property's value to be revised |
239 | to \a changed. |
240 | |
241 | By default \c {operator==()} is used to compare values of the property and |
242 | \c {QTest::toString()} is used to generate proper error messages. |
243 | |
244 | If such comparison is not supported for \c PropertyType, or the comparison |
245 | it supports is not appropriate to this property, a custom \a comparator can |
246 | be supplied. |
247 | |
248 | Apart from that, a custom \a represent callback can also be specified to |
249 | generate a string representation of \c PropertyType. If supplied, it must |
250 | allocate its returned string using \c {new char[]}, so that it can be used |
251 | in place of \l {QTest::toString()}. |
252 | |
253 | \note Any test calling this method will need to call |
254 | \code |
255 | if (QTest::currentTestFailed()) |
256 | return; |
257 | \endcode |
258 | after doing so, if there is any later code in the test. If testing several |
259 | properties in one test method, emitting a debug message saying which |
260 | property failed, before returning, is a kindness to readers of the output. |
261 | */ |
262 | template<typename TestedClass, typename PropertyType> |
263 | void testReadOnlyPropertyBasics( |
264 | TestedClass &instance, const PropertyType &initial, const PropertyType &changed, |
265 | const char *propertyName, |
266 | std::function<void()> mutator = []() { QFAIL("Data modifier function must be provided" ); }, |
267 | std::function<bool(const PropertyType &, const PropertyType &)> comparator = |
268 | [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; }, |
269 | std::function<char *(const PropertyType &)> represent = |
270 | [](const PropertyType &val) { return QTest::toString(val); }) |
271 | { |
272 | // get the property |
273 | const QMetaObject *metaObject = instance.metaObject(); |
274 | QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName)); |
275 | |
276 | // in case the TestedClass has setProperty()/property() methods. |
277 | QObject &testedObj = static_cast<QObject &>(instance); |
278 | |
279 | QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(), |
280 | QByteArray("Preconditions not met for " ) + propertyName + '\n' + |
281 | "The type of initial and changed value does not match the type of the property.\n" |
282 | "Please ensure that the types match exactly (convertability is not enough).\n" |
283 | "You can provide the template types to the " |
284 | "function explicitly to force a certain type.\n" |
285 | "Expected was a " + metaProperty.metaType().name() |
286 | + " but " + QMetaType::fromType<PropertyType>().name() + " was provided." ); |
287 | |
288 | QVERIFY2(metaProperty.isBindable(), "Preconditions not met for " + QByteArray(propertyName)); |
289 | |
290 | QUntypedBindable bindable = metaProperty.bindable(&instance); |
291 | |
292 | QScopedPointer<QSignalSpy> spy(nullptr); |
293 | if (metaProperty.hasNotifySignal()) |
294 | spy.reset(new QSignalSpy(&instance, metaProperty.notifySignal())); |
295 | |
296 | QPROPERTY_TEST_COMPARISON_HELPER( |
297 | testedObj.property(propertyName).template value<PropertyType>(), initial, comparator, |
298 | represent); |
299 | |
300 | // Check that attempting to bind this read-only property to another property has no effect: |
301 | QProperty<PropertyType> propSetter(initial); |
302 | QVERIFY(!bindable.hasBinding()); |
303 | bindable.setBinding(Qt::makePropertyBinding(propSetter)); |
304 | QVERIFY(!bindable.hasBinding()); |
305 | propSetter.setValue(changed); |
306 | QPROPERTY_TEST_COMPARISON_HELPER( |
307 | testedObj.property(propertyName).template value<PropertyType>(), initial, comparator, |
308 | represent); |
309 | if (spy) |
310 | QCOMPARE(spy->count(), 0); |
311 | |
312 | QProperty<PropertyType> propObserver; |
313 | propObserver.setBinding(bindable.makeBinding()); |
314 | |
315 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent); |
316 | |
317 | // Invoke mutator function. Now property value should be changed. |
318 | mutator(); |
319 | |
320 | QPROPERTY_TEST_COMPARISON_HELPER( |
321 | testedObj.property(propertyName).template value<PropertyType>(), changed, comparator, |
322 | represent); |
323 | QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent); |
324 | |
325 | if (spy) |
326 | QCOMPARE(spy->count(), 1); |
327 | } |
328 | |
329 | } // namespace QTestPrivate |
330 | |
331 | QT_END_NAMESPACE |
332 | |
333 | #endif // QPROPERTYTESTHELPER_P_H |
334 | |