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
58QT_BEGIN_NAMESPACE
59
60namespace 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*/
130template<typename TestedClass, typename PropertyType>
131void 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*/
262template<typename TestedClass, typename PropertyType>
263void 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
331QT_END_NAMESPACE
332
333#endif // QPROPERTYTESTHELPER_P_H
334