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("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.");
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 "Preconditions not met for " + QByteArray(propertyName));
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("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");
346
347 QVERIFY2(metaProperty.isBindable(), "Preconditions not met for " + QByteArray(propertyName));
348
349 QUntypedBindable bindable = metaProperty.bindable(&instance);
350
351 QTestPrivate::OptionalWrapper<QSignalSpy> spy = std::nullopt;
352 if (metaProperty.hasNotifySignal())
353 spy.emplace(&instance, metaProperty.notifySignal());
354
356 testedObj.property(propertyName).template value<PropertyType>(), prior, comparator,
357 represent);
358
359 QProperty<PropertyType> propObserver;
360 propObserver.setBinding(bindable.makeBinding());
361 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), prior, comparator, represent);
362
363 // Create a binding that sets the 'changed' value to the property.
364 // This also tests binding loops.
365 QVERIFY(!bindable.hasBinding());
366 std::unique_ptr<TestedClass> helperObj = helperConstructor();
367 QProperty<PropertyType> propSetter(changed); // if the helperConstructor() returns nullptr
368 const QPropertyBinding<PropertyType> binding = helperObj
369 ? Qt::makePropertyBinding([&]() {
370 QObject *obj = static_cast<QObject *>(helperObj.get());
371 obj->setProperty(propertyName, QVariant::fromValue(changed));
372 return obj->property(propertyName).template value<PropertyType>();
373 })
374 : Qt::makePropertyBinding(propSetter);
375 bindable.setBinding(binding);
376 QVERIFY(bindable.hasBinding());
377
379 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
380 represent);
381 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
382 if (spy)
383 QCOMPARE(spy->size(), 1);
384
385 // Attempt to set back the 'prior' value and verify that it has no effect
386 testedObj.setProperty(propertyName, QVariant::fromValue(prior));
388 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
389 represent);
390 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
391 if (spy)
392 QCOMPARE(spy->size(), 1);
393 if (bindingPreservedOnWrite)
394 QVERIFY(bindable.hasBinding());
395 else
396 QVERIFY(!bindable.hasBinding());
397}
398
399/*!
400 \internal
401 \overload
402
403 This overload supports the case where the caller only needs to override
404 the default for \a helperConstructor. It uses the defaults for all the other
405 parameters.
406*/
407template<typename TestedClass, typename PropertyType>
409 TestedClass &instance, const PropertyType &prior, const PropertyType &changed,
410 const char *propertyName,
411 bool bindingPreservedOnWrite,
412 std::function<std::unique_ptr<TestedClass>(void)> helperConstructor)
413{
414 testWriteOncePropertyBasics<TestedClass, PropertyType>(
415 instance, prior, changed, propertyName, bindingPreservedOnWrite,
416 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
417 [](const PropertyType &val) { return QTest::toString(val); },
418 helperConstructor);
419}
420
421/*!
422 \internal
423 Basic testing of a read-only bindable property.
424
425 This helper function tests the behavior of bindable read-only property
426 \a propertyName, of type \c PropertyType, in class \c TestedClass.
427 The caller must supply an \a instance of \c TestedClass and two distinct
428 values, \a initial and \a changed, of \c PropertyType.
429
430 When this function is called, the property's value must be \a initial.
431 The \a mutator must, when called, cause the property's value to be revised
432 to \a changed.
433
434 By default \c {operator==()} is used to compare values of the property and
435 \c {QTest::toString()} is used to generate proper error messages.
436
437 If such comparison is not supported for \c PropertyType, or the comparison
438 it supports is not appropriate to this property, a custom \a comparator can
439 be supplied.
440
441 Apart from that, a custom \a represent callback can also be specified to
442 generate a string representation of \c PropertyType. If supplied, it must
443 allocate its returned string using \c {new char[]}, so that it can be used
444 in place of \l {QTest::toString()}.
445
446 \note Any test calling this method will need to call
447 \code
448 if (QTest::currentTestFailed())
449 return;
450 \endcode
451 after doing so, if there is any later code in the test. If testing several
452 properties in one test method, emitting a warning message saying which
453 property failed, before returning, is a kindness to readers of the output.
454*/
455template<typename TestedClass, typename PropertyType>
457 TestedClass &instance, const PropertyType &initial, const PropertyType &changed,
458 const char *propertyName,
459 std::function<void()> mutator = []() { QFAIL("Data modifier function must be provided"); },
460 std::function<bool(const PropertyType &, const PropertyType &)> comparator =
461 [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
462 std::function<char *(const PropertyType &)> represent =
463 [](const PropertyType &val) { return QTest::toString(val); })
464{
465 // get the property
466 const QMetaObject *metaObject = instance.metaObject();
467 QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
468
469 // in case the TestedClass has setProperty()/property() methods.
470 QObject &testedObj = static_cast<QObject &>(instance);
471
472 QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
473 QByteArray("Preconditions not met for ") + propertyName + "\n"
474 "The type of initial and changed value does not match the type of the property.\n"
475 "Please ensure that the types match exactly (convertability is not enough).\n"
476 "You can provide the template types to the "
477 "function explicitly to force a certain type.\n"
478 "Expected was a " + metaProperty.metaType().name()
479 + " but " + QMetaType::fromType<PropertyType>().name() + " was provided.");
480
481 QVERIFY2(metaProperty.isBindable(), "Preconditions not met for " + QByteArray(propertyName));
482
483 QUntypedBindable bindable = metaProperty.bindable(&instance);
484
485 QTestPrivate::OptionalWrapper<QSignalSpy> spy = std::nullopt;
486 if (metaProperty.hasNotifySignal())
487 spy.emplace(&instance, metaProperty.notifySignal());
488
490 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
491 represent);
492
493 // Check that attempting to bind this read-only property to another property has no effect:
494 QProperty<PropertyType> propSetter(initial);
495 QVERIFY(!bindable.hasBinding());
496 bindable.setBinding(Qt::makePropertyBinding(propSetter));
497 QVERIFY(!bindable.hasBinding());
498 propSetter.setValue(changed);
500 testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
501 represent);
502 if (spy)
503 QCOMPARE(spy->size(), 0);
504
505 QProperty<PropertyType> propObserver;
506 propObserver.setBinding(bindable.makeBinding());
507
508 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
509
510 // Invoke mutator function. Now property value should be changed.
511 mutator();
512
514 testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
515 represent);
516 QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
517
518 if (spy)
519 QCOMPARE(spy->size(), 1);
520}
521
522} // namespace QTestPrivate
523
524QT_END_NAMESPACE
525
526#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)
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)