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
qpropertytesthelper_p.h
Go to the documentation of this file.
1// Copyright (C) 2021 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#ifndef QPROPERTYTESTHELPER_P_H
5#define QPROPERTYTESTHELPER_P_H
6
7//
8// W A R N I N G
9// -------------
10//
11// This file is not part of the Qt API. It exists purely as an
12// implementation detail. This header file may change from version to
13// version without notice, or even be removed.
14//
15// We mean it.
16//
17
18#include <QtCore/QObject>
19#include <QtCore/QProperty>
20#include <QtTest/QSignalSpy>
21#include <QTest>
22#include <private/qglobal_p.h>
23
24#include <cstdio>
25#include <memory>
26#include <optional>
27
28QT_BEGIN_NAMESPACE
29
30namespace QTestPrivate {
31
32#ifdef Q_OS_VXWORKS
33template <typename T>
34class OptionalWrapper : private std::unique_ptr<T>
35{
36 using Base = std::unique_ptr<T>;
37
38 Base &as_base() { return *this; }
39 const Base &as_base() const { return *this; }
40public:
42
43 using Base::operator->;
44 using Base::operator*;
45 using Base::operator bool;
46
47 template <typename...Args>
48 T &emplace(Args&&...args)
49 { as_base() = std::make_unique<T>(std::forward<Args>(args)...); return **this; }
50};
51#else
52template <typename T>
53using OptionalWrapper = std::optional<T>;
54#endif // Q_OS_VXWORKS
55
56
57/*!
58 \internal
59
60 This helper macro is used as a wrapper around \l QVERIFY2() to provide a
61 detailed error message in case of failure. It is intended to be used \e only
62 in the helper functions below.
63
64 The custom \a comparator method is used to check if the \a actual and
65 \a expected values are equal or not.
66
67 The macro uses a custom \a represent callback to generate the string
68 representation of \a actual and \a expected.
69
70 The error message is close to the one provided by the \l QCOMPARE() macro.
71 Specifically the implementation is taken from the \c formatFailMessage()
72 function, which is defined in the \c qtestresult.cpp file.
73*/
74#define QPROPERTY_TEST_COMPARISON_HELPER(actual, expected, comparator, represent)
75 do {
76 char qprop_tst_cmp_hlp_buf[1024];
77 const auto qprop_tst_cmp_hlp_act = std::unique_ptr<char[]>(represent(actual));
78 const auto qprop_tst_cmp_hlp_exp = std::unique_ptr<char[]>(represent(expected));
79 QVERIFY2(comparator(actual, expected),
80 QTest::Internal::formatPropertyTestHelperFailure(qprop_tst_cmp_hlp_buf,
81 sizeof qprop_tst_cmp_hlp_buf,
82 qprop_tst_cmp_hlp_act.get(),
83 qprop_tst_cmp_hlp_exp.get(),
84 #actual, #expected));
85 } while (false)
86
87/*!
88 \internal
89 Basic testing of a bindable property.
90
91 This helper function tests the behavior of bindable read/write property
92 \a propertyName, of type \c PropertyType, in class \c TestedClass.
93 The caller must supply an \a instance of \c TestedClass and two distinct
94 values, \a initial and \a changed, of \c PropertyType.
95
96 Since the first part of the test sets the property to \a initial, it
97 \e {must not} be the default value of the property, or the check that it
98 was set will be vacuous.
99
100 By default \c {operator==()} is used to compare values of the property and
101 \c {QTest::toString()} is used to generate proper error messages.
102
103 If such comparison is not supported for \c PropertyType, or the comparison
104 it supports is not appropriate to this property, a custom \a comparator can
105 be supplied.
106
107 Apart from that, a custom \a represent callback can also be specified to
108 generate a string representation of \c PropertyType. If supplied, it must
109 allocate its returned string using \c {new char[]}, so that it can be used
110 in place of \l {QTest::toString()}.
111
112 The \a helperConstructor method is used to create another instance of
113 \c TestedClass. This instance is used to test for binding loops. By default,
114 the method returns a default-constructed \c TestedClass. A custom
115 \a helperConstructor should be provided if \c TestedClass is not
116 default-constructible. Some very specific properties cannot be tested for
117 binding loops. Pass a lambda that returns an \c {std::nullptr} as
118 \a helperConstructor in such case.
119
120 \note Any test calling this method will need to call
121 \code
122 if (QTest::currentTestFailed())
123 return;
124 \endcode
125 after doing so, if there is any later code in the test. If testing several
126 properties in one test method, emitting a warning message saying which
127 property failed, before returning, is a kindness to readers of the output.
128*/
129template<typename TestedClass, typename PropertyType>
131 TestedClass &instance, const PropertyType &initial, const PropertyType &changed,
132 const char *propertyName,
133 std::function<bool(const PropertyType &, const PropertyType &)> comparator =
134 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
135 std::function<char *(const PropertyType &)> represent =
136 [](const PropertyType &val) { return QTest::toString(val); },
137 std::function<std::unique_ptr<TestedClass>(void)> helperConstructor =
138 []() { return std::make_unique<TestedClass>(); })
139{
140 // get the property
141 const QMetaObject *metaObject = instance.metaObject();
142 QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
143 QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
144 QByteArray{QByteArray("Preconditions not met for ") + propertyName + "\n"
145 "The type of initial and changed value does not match the type of the property.\n"
146 "Please ensure that the types match exactly (convertability is not enough).\n"
147 "You can provide the template types to the "
148 "function explicitly to force a certain type.\n"
149 "Expected was a " + metaProperty.metaType().name()
150 + " but " + QMetaType::fromType<PropertyType>().name() + " was provided."}.constData());
151
152 // in case the TestedClass has setProperty()/property() methods.
153 QObject &testedObj = static_cast<QObject &>(instance);
154
155 QVERIFY2(metaProperty.isBindable() && metaProperty.isWritable(),
156 QByteArray{QByteArray("Preconditions not met for ") + propertyName}.constData());
157
158 QTestPrivate::OptionalWrapper<QSignalSpy> spy = std::nullopt;
159 if (metaProperty.hasNotifySignal())
160 spy.emplace(&instance, metaProperty.notifySignal());
161
162 testedObj.setProperty(propertyName, QVariant::fromValue(initial));
164 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
165 represent);
166 if (spy)
167 QCOMPARE(spy->size(), 1);
168
169 QUntypedBindable bindable = metaProperty.bindable(&instance);
170
171 // Bind to the object's property (using both lambda and
172 // Qt:makePropertyBinding).
173 QProperty<PropertyType> propObserver(changed);
174 propObserver.setBinding(bindable.makeBinding());
175 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
176
177 QProperty<PropertyType> propObserverLambda(changed);
178 propObserverLambda.setBinding(
179 [&]() { return testedObj.property(propertyName).template value<PropertyType>(); });
180 QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent);
181
182 testedObj.setProperty(propertyName, QVariant::fromValue(changed));
183 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
184 QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent);
185 if (spy)
186 QCOMPARE(spy->size(), 2);
187
188 // Bind object's property to other property
189 QProperty<PropertyType> propSetter(initial);
190 QVERIFY(!bindable.hasBinding());
191 bindable.setBinding(Qt::makePropertyBinding(propSetter));
192
193 QVERIFY(bindable.hasBinding());
195 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
196 represent);
197 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
198 QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent);
199 if (spy)
200 QCOMPARE(spy->size(), 3);
201
202 // Count notifications triggered; should only happen on actual change.
203 int updateCount = 0;
204 auto handler = bindable.onValueChanged([&updateCount]() { ++updateCount; });
205 Q_UNUSED(handler)
206
207 propSetter.setValue(changed);
209 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
210 represent);
211 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
212 QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent);
213 QCOMPARE(updateCount, 1);
214 if (spy)
215 QCOMPARE(spy->size(), 4);
216
217 // Test that manually setting the value (even the same one) breaks the
218 // binding.
219 testedObj.setProperty(propertyName, QVariant::fromValue(changed));
220 QVERIFY(!bindable.hasBinding());
221 // Setting the same value should have no impact on udpateCount.
222 QCOMPARE(updateCount, 1);
223
224 // value didn't change -> the signal should not be emitted
225 if (spy)
226 QCOMPARE(spy->size(), 4);
227
228 // test binding loop
229 if (std::unique_ptr<TestedClass> helperObj = helperConstructor()) {
230 // Reset to 'initial', so that the binding loop test could check the
231 // 'changed' value, because some tests already rely on the 'instance' to
232 // have the 'changed' value once this test passes
233 testedObj.setProperty(propertyName, QVariant::fromValue(initial));
234 const QPropertyBinding<PropertyType> binding([&]() {
235 QObject *obj = static_cast<QObject *>(helperObj.get());
236 obj->setProperty(propertyName, QVariant::fromValue(changed));
237 return obj->property(propertyName).template value<PropertyType>();
238 }, {});
239 bindable.setBinding(binding);
241 testedObj.property(propertyName).template value<PropertyType>(), changed,
242 comparator, represent);
243 QVERIFY2(!binding.error().hasError(), qPrintable(binding.error().description()));
244 }
245}
246
247/*!
248 \internal
249 \overload
250
251 This overload supports the case where the caller only needs to override
252 the default for \a helperConstructor. It uses the defaults for all the other
253 parameters.
254*/
255template<typename TestedClass, typename PropertyType>
257 TestedClass &instance, const PropertyType &initial, const PropertyType &changed,
258 const char *propertyName,
259 std::function<std::unique_ptr<TestedClass>(void)> helperConstructor)
260{
261 testReadWritePropertyBasics<TestedClass, PropertyType>(
262 instance, initial, changed, propertyName,
263 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
264 [](const PropertyType &val) { return QTest::toString(val); },
265 helperConstructor);
266}
267
268/*!
269 \internal
270 Basic testing of a bindable property that is writable only once.
271
272 The write-once properties are writable properties which accept only
273 one valid setting of the value ("write"), after which later attempts
274 are ignored.
275
276 This helper function tests the behavior of bindable write-once property
277 \a propertyName, of type \c PropertyType, in class \c TestedClass.
278 The caller must supply an \a instance of \c TestedClass and two distinct
279 values, \a initial and \a changed, of \c PropertyType.
280
281 The property of \a instance must not yet have been set when this function
282 is called. The value it has before being set should be passed as \a prior
283 and a distinct value, that this test can set it to, as \a changed.
284
285 The \a bindingPreservedOnWrite parameter controls whether this function
286 expects the binding set by this function to be preserved when setting a value
287 directly. The default value is 'true'.
288
289 By default \c {operator==()} is used to compare values of the property and
290 \c {QTest::toString()} is used to generate proper error messages.
291
292 If such comparison is not supported for \c PropertyType, or the comparison
293 it supports is not appropriate to this property, a custom \a comparator can
294 be supplied.
295
296 Apart from that, a custom \a represent callback can also be specified to
297 generate a string representation of \c PropertyType. If supplied, it must
298 allocate its returned string using \c {new char[]}, so that it can be used
299 in place of \l {QTest::toString()}.
300
301 The \a helperConstructor method is used to create another instance of
302 \c TestedClass. This instance is used to test for binding loops. By default,
303 the method returns a default-constructed \c TestedClass. A custom
304 \a helperConstructor should be provided if \c TestedClass is not
305 default-constructible. Some very specific properties cannot be tested for
306 binding loops. Pass a lambda that returns an \c {std::nullptr} as
307 \a helperConstructor in such case.
308
309 \note Any test calling this method will need to call
310 \code
311 if (QTest::currentTestFailed())
312 return;
313 \endcode
314 after doing so, if there is any later code in the test. If testing several
315 properties in one test method, emitting a warning message saying which
316 property failed, before returning, is a kindness to readers of the output.
317*/
318
319template<typename TestedClass, typename PropertyType>
321 TestedClass &instance, const PropertyType &prior, const PropertyType &changed,
322 const char *propertyName,
323 bool bindingPreservedOnWrite = true,
324 std::function<bool(const PropertyType &, const PropertyType &)> comparator =
325 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
326 std::function<char *(const PropertyType &)> represent =
327 [](const PropertyType &val) { return QTest::toString(val); },
328 std::function<std::unique_ptr<TestedClass>(void)> helperConstructor =
329 []() { return std::make_unique<TestedClass>(); })
330{
331 // get the property
332 const QMetaObject *metaObject = instance.metaObject();
333 QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
334
335 // in case the TestedClass has setProperty()/property() methods.
336 QObject &testedObj = static_cast<QObject &>(instance);
337
338 QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
339 QByteArray{QByteArray("Preconditions not met for ") + propertyName + "\n"
340 "The type of prior and changed value does not match the type of the property.\n"
341 "Please ensure that the types match exactly (convertability is not enough).\n"
342 "You can provide the template types to the "
343 "function explicitly to force a certain type.\n"
344 "Property is " + metaProperty.metaType().name()
345 + " but parameters are " + QMetaType::fromType<PropertyType>().name() + ".\n"}.constData());
346
347 QVERIFY2(metaProperty.isBindable(),
348 QByteArray{"Preconditions not met for " + QByteArray(propertyName)}.constData());
349
350 QUntypedBindable bindable = metaProperty.bindable(&instance);
351
352 QTestPrivate::OptionalWrapper<QSignalSpy> spy = std::nullopt;
353 if (metaProperty.hasNotifySignal())
354 spy.emplace(&instance, metaProperty.notifySignal());
355
357 testedObj.property(propertyName).template value<PropertyType>(), prior, comparator,
358 represent);
359
360 QProperty<PropertyType> propObserver;
361 propObserver.setBinding(bindable.makeBinding());
362 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), prior, comparator, represent);
363
364 // Create a binding that sets the 'changed' value to the property.
365 // This also tests binding loops.
366 QVERIFY(!bindable.hasBinding());
367 std::unique_ptr<TestedClass> helperObj = helperConstructor();
368 QProperty<PropertyType> propSetter(changed); // if the helperConstructor() returns nullptr
369 const QPropertyBinding<PropertyType> binding = helperObj
370 ? Qt::makePropertyBinding([&]() {
371 QObject *obj = static_cast<QObject *>(helperObj.get());
372 obj->setProperty(propertyName, QVariant::fromValue(changed));
373 return obj->property(propertyName).template value<PropertyType>();
374 })
375 : Qt::makePropertyBinding(propSetter);
376 bindable.setBinding(binding);
377 QVERIFY(bindable.hasBinding());
378
380 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
381 represent);
382 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
383 if (spy)
384 QCOMPARE(spy->size(), 1);
385
386 // Attempt to set back the 'prior' value and verify that it has no effect
387 testedObj.setProperty(propertyName, QVariant::fromValue(prior));
389 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
390 represent);
391 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
392 if (spy)
393 QCOMPARE(spy->size(), 1);
394 if (bindingPreservedOnWrite)
395 QVERIFY(bindable.hasBinding());
396 else
397 QVERIFY(!bindable.hasBinding());
398}
399
400/*!
401 \internal
402 \overload
403
404 This overload supports the case where the caller only needs to override
405 the default for \a helperConstructor. It uses the defaults for all the other
406 parameters.
407*/
408template<typename TestedClass, typename PropertyType>
410 TestedClass &instance, const PropertyType &prior, const PropertyType &changed,
411 const char *propertyName,
412 bool bindingPreservedOnWrite,
413 std::function<std::unique_ptr<TestedClass>(void)> helperConstructor)
414{
415 testWriteOncePropertyBasics<TestedClass, PropertyType>(
416 instance, prior, changed, propertyName, bindingPreservedOnWrite,
417 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
418 [](const PropertyType &val) { return QTest::toString(val); },
419 helperConstructor);
420}
421
422/*!
423 \internal
424 Basic testing of a read-only bindable property.
425
426 This helper function tests the behavior of bindable read-only property
427 \a propertyName, of type \c PropertyType, in class \c TestedClass.
428 The caller must supply an \a instance of \c TestedClass and two distinct
429 values, \a initial and \a changed, of \c PropertyType.
430
431 When this function is called, the property's value must be \a initial.
432 The \a mutator must, when called, cause the property's value to be revised
433 to \a changed.
434
435 By default \c {operator==()} is used to compare values of the property and
436 \c {QTest::toString()} is used to generate proper error messages.
437
438 If such comparison is not supported for \c PropertyType, or the comparison
439 it supports is not appropriate to this property, a custom \a comparator can
440 be supplied.
441
442 Apart from that, a custom \a represent callback can also be specified to
443 generate a string representation of \c PropertyType. If supplied, it must
444 allocate its returned string using \c {new char[]}, so that it can be used
445 in place of \l {QTest::toString()}.
446
447 \note Any test calling this method will need to call
448 \code
449 if (QTest::currentTestFailed())
450 return;
451 \endcode
452 after doing so, if there is any later code in the test. If testing several
453 properties in one test method, emitting a warning message saying which
454 property failed, before returning, is a kindness to readers of the output.
455*/
456template<typename TestedClass, typename PropertyType>
458 TestedClass &instance, const PropertyType &initial, const PropertyType &changed,
459 const char *propertyName,
460 std::function<void()> mutator = []() { QFAIL("Data modifier function must be provided"); },
461 std::function<bool(const PropertyType &, const PropertyType &)> comparator =
462 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
463 std::function<char *(const PropertyType &)> represent =
464 [](const PropertyType &val) { return QTest::toString(val); })
465{
466 // get the property
467 const QMetaObject *metaObject = instance.metaObject();
468 QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
469
470 // in case the TestedClass has setProperty()/property() methods.
471 QObject &testedObj = static_cast<QObject &>(instance);
472
473 QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
474 QByteArray{QByteArray("Preconditions not met for ") + propertyName + "\n"
475 "The type of initial and changed value does not match the type of the property.\n"
476 "Please ensure that the types match exactly (convertability is not enough).\n"
477 "You can provide the template types to the "
478 "function explicitly to force a certain type.\n"
479 "Expected was a " + metaProperty.metaType().name()
480 + " but " + QMetaType::fromType<PropertyType>().name() + " was provided."}.constData());
481
482 QVERIFY2(metaProperty.isBindable(),
483 QByteArray{"Preconditions not met for " + QByteArray(propertyName)}.constData());
484
485 QUntypedBindable bindable = metaProperty.bindable(&instance);
486
487 QTestPrivate::OptionalWrapper<QSignalSpy> spy = std::nullopt;
488 if (metaProperty.hasNotifySignal())
489 spy.emplace(&instance, metaProperty.notifySignal());
490
492 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
493 represent);
494
495 // Check that attempting to bind this read-only property to another property has no effect:
496 QProperty<PropertyType> propSetter(initial);
497 QVERIFY(!bindable.hasBinding());
498 bindable.setBinding(Qt::makePropertyBinding(propSetter));
499 QVERIFY(!bindable.hasBinding());
500 propSetter.setValue(changed);
502 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
503 represent);
504 if (spy)
505 QCOMPARE(spy->size(), 0);
506
507 QProperty<PropertyType> propObserver;
508 propObserver.setBinding(bindable.makeBinding());
509
510 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
511
512 // Invoke mutator function. Now property value should be changed.
513 mutator();
514
516 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
517 represent);
518 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
519
520 if (spy)
521 QCOMPARE(spy->size(), 1);
522}
523
524} // namespace QTestPrivate
525
526QT_END_NAMESPACE
527
528#endif // QPROPERTYTESTHELPER_P_H
void testWriteOncePropertyBasics(TestedClass &instance, const PropertyType &prior, const PropertyType &changed, const char *propertyName, bool bindingPreservedOnWrite, std::function< std::unique_ptr< TestedClass >(void)> helperConstructor)
void testReadWritePropertyBasics(TestedClass &instance, const PropertyType &initial, const PropertyType &changed, const char *propertyName, std::function< bool(const PropertyType &, const PropertyType &)> comparator=[](const PropertyType &lhs, const PropertyType &rhs) { return lhs==rhs;}, std::function< char *(const PropertyType &)> represent=[](const PropertyType &val) { return QTest::toString(val);}, std::function< std::unique_ptr< TestedClass >(void)> helperConstructor=[]() { return std::make_unique< TestedClass >();})
void testReadOnlyPropertyBasics(TestedClass &instance, const PropertyType &initial, const PropertyType &changed, const char *propertyName, std::function< void()> mutator=[]() { QFAIL("Data modifier function must be provided");}, std::function< bool(const PropertyType &, const PropertyType &)> comparator=[](const PropertyType &lhs, const PropertyType &rhs) { return lhs==rhs;}, std::function< char *(const PropertyType &)> represent=[](const PropertyType &val) { return QTest::toString(val);})
void testReadWritePropertyBasics(TestedClass &instance, const PropertyType &initial, const PropertyType &changed, const char *propertyName, std::function< std::unique_ptr< TestedClass >(void)> helperConstructor)
std::optional< T > OptionalWrapper
void testWriteOncePropertyBasics(TestedClass &instance, const PropertyType &prior, const PropertyType &changed, const char *propertyName, bool bindingPreservedOnWrite=true, std::function< bool(const PropertyType &, const PropertyType &)> comparator=[](const PropertyType &lhs, const PropertyType &rhs) { return lhs==rhs;}, std::function< char *(const PropertyType &)> represent=[](const PropertyType &val) { return QTest::toString(val);}, std::function< std::unique_ptr< TestedClass >(void)> helperConstructor=[]() { return std::make_unique< TestedClass >();})
#define QPROPERTY_TEST_COMPARISON_HELPER(actual, expected, comparator, represent)