summaryrefslogtreecommitdiffstats
path: root/src/testlib/qpropertytesthelper_p.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/testlib/qpropertytesthelper_p.h')
-rw-r--r--src/testlib/qpropertytesthelper_p.h501
1 files changed, 501 insertions, 0 deletions
diff --git a/src/testlib/qpropertytesthelper_p.h b/src/testlib/qpropertytesthelper_p.h
new file mode 100644
index 0000000000..c691802a39
--- /dev/null
+++ b/src/testlib/qpropertytesthelper_p.h
@@ -0,0 +1,501 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPROPERTYTESTHELPER_P_H
+#define QPROPERTYTESTHELPER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/QObject>
+#include <QtCore/QProperty>
+#include <QtTest/QSignalSpy>
+#include <QTest>
+#include <private/qglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+namespace QTestPrivate {
+
+/*!
+ \internal
+
+ This helper macro is used as a wrapper around \l QVERIFY2() to provide a
+ detailed error message in case of failure. It is intended to be used \e only
+ in the helper functions below.
+
+ The custom \a comparator method is used to check if the \a actual and
+ \a expected values are equal or not.
+
+ The macro uses a custom \a represent callback to generate the string
+ representation of \a actual and \a expected.
+
+ The error message is close to the one provided by the \l QCOMPARE() macro.
+ Specifically the implementation is taken from the \c formatFailMessage()
+ function, which is defined in the \c qtestresult.cpp file.
+*/
+#define QPROPERTY_TEST_COMPARISON_HELPER(actual, expected, comparator, represent) \
+ do { \
+ const size_t maxMsgLen = 1024; \
+ char msg[maxMsgLen] = { '\0' }; \
+ auto actualStr = represent(actual); \
+ auto expectedStr = represent(expected); \
+ const size_t len1 = mbstowcs(nullptr, #actual, maxMsgLen); \
+ const size_t len2 = mbstowcs(nullptr, #expected, maxMsgLen); \
+ qsnprintf(msg, maxMsgLen, "\n%s\n Actual (%s)%*s %s\n Expected (%s)%*s %s\n", \
+ "Comparison failed!", #actual, qMax(len1, len2) - len1 + 1, ":", \
+ actualStr ? actualStr : "<null>", #expected, qMax(len1, len2) - len2 + 1, ":", \
+ expectedStr ? expectedStr : "<null>"); \
+ delete[] actualStr; \
+ delete[] expectedStr; \
+ QVERIFY2(comparator(actual, expected), msg); \
+ } while (false)
+
+/*!
+ \internal
+ Basic testing of a bindable property.
+
+ This helper function tests the behavior of bindable read/write property
+ \a propertyName, of type \c PropertyType, in class \c TestedClass.
+ The caller must supply an \a instance of \c TestedClass and two distinct
+ values, \a initial and \a changed, of \c PropertyType.
+
+ Since the first part of the test sets the property to \a initial, it
+ \e {must not} be the default value of the property, or the check that it
+ was set will be vacuous.
+
+ By default \c {operator==()} is used to compare values of the property and
+ \c {QTest::toString()} is used to generate proper error messages.
+
+ If such comparison is not supported for \c PropertyType, or the comparison
+ it supports is not appropriate to this property, a custom \a comparator can
+ be supplied.
+
+ Apart from that, a custom \a represent callback can also be specified to
+ generate a string representation of \c PropertyType. If supplied, it must
+ allocate its returned string using \c {new char[]}, so that it can be used
+ in place of \l {QTest::toString()}.
+
+ The \a helperConstructor method is used to create another instance of
+ \c TestedClass. This instance is used to test for binding loops. By default,
+ the method returns a default-constructed \c TestedClass. A custom
+ \a helperConstructor should be provided if \c TestedClass is not
+ default-constructible. Some very specific properties cannot be tested for
+ binding loops. Pass a lambda that returns an \c {std::nullptr} as
+ \a helperConstructor in such case.
+
+ \note Any test calling this method will need to call
+ \code
+ if (QTest::currentTestFailed())
+ return;
+ \endcode
+ after doing so, if there is any later code in the test. If testing several
+ properties in one test method, emitting a warning message saying which
+ property failed, before returning, is a kindness to readers of the output.
+*/
+template<typename TestedClass, typename PropertyType>
+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>(); })
+{
+ // get the property
+ const QMetaObject *metaObject = instance.metaObject();
+ QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
+ QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
+ QByteArray("Preconditions not met for ") + propertyName + "\n"
+ "The type of initial and changed value does not match the type of the property.\n"
+ "Please ensure that the types match exactly (convertability is not enough).\n"
+ "You can provide the template types to the "
+ "function explicitly to force a certain type.\n"
+ "Expected was a " + metaProperty.metaType().name()
+ + " but " + QMetaType::fromType<PropertyType>().name() + " was provided.");
+
+ // in case the TestedClass has setProperty()/property() methods.
+ QObject &testedObj = static_cast<QObject &>(instance);
+
+ QVERIFY2(metaProperty.isBindable() && metaProperty.isWritable(),
+ "Preconditions not met for " + QByteArray(propertyName));
+
+ QScopedPointer<QSignalSpy> spy(nullptr);
+ if (metaProperty.hasNotifySignal())
+ spy.reset(new QSignalSpy(&instance, metaProperty.notifySignal()));
+
+ testedObj.setProperty(propertyName, QVariant::fromValue(initial));
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
+ represent);
+ if (spy)
+ QCOMPARE(spy->size(), 1);
+
+ QUntypedBindable bindable = metaProperty.bindable(&instance);
+
+ // Bind to the object's property (using both lambda and
+ // Qt:makePropertyBinding).
+ QProperty<PropertyType> propObserver(changed);
+ propObserver.setBinding(bindable.makeBinding());
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
+
+ QProperty<PropertyType> propObserverLambda(changed);
+ propObserverLambda.setBinding(
+ [&]() { return testedObj.property(propertyName).template value<PropertyType>(); });
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent);
+
+ testedObj.setProperty(propertyName, QVariant::fromValue(changed));
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent);
+ if (spy)
+ QCOMPARE(spy->size(), 2);
+
+ // Bind object's property to other property
+ QProperty<PropertyType> propSetter(initial);
+ QVERIFY(!bindable.hasBinding());
+ bindable.setBinding(Qt::makePropertyBinding(propSetter));
+
+ QVERIFY(bindable.hasBinding());
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
+ represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), initial, comparator, represent);
+ if (spy)
+ QCOMPARE(spy->size(), 3);
+
+ // Count notifications triggered; should only happen on actual change.
+ int updateCount = 0;
+ auto handler = bindable.onValueChanged([&updateCount]() { ++updateCount; });
+ Q_UNUSED(handler)
+
+ propSetter.setValue(changed);
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
+ represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserverLambda.value(), changed, comparator, represent);
+ QCOMPARE(updateCount, 1);
+ if (spy)
+ QCOMPARE(spy->size(), 4);
+
+ // Test that manually setting the value (even the same one) breaks the
+ // binding.
+ testedObj.setProperty(propertyName, QVariant::fromValue(changed));
+ QVERIFY(!bindable.hasBinding());
+ // Setting the same value should have no impact on udpateCount.
+ QCOMPARE(updateCount, 1);
+
+ // value didn't change -> the signal should not be emitted
+ if (spy)
+ QCOMPARE(spy->size(), 4);
+
+ // test binding loop
+ if (std::unique_ptr<TestedClass> helperObj = helperConstructor()) {
+ // Reset to 'initial', so that the binding loop test could check the
+ // 'changed' value, because some tests already rely on the 'instance' to
+ // have the 'changed' value once this test passes
+ testedObj.setProperty(propertyName, QVariant::fromValue(initial));
+ const QPropertyBinding<PropertyType> binding([&]() {
+ QObject *obj = static_cast<QObject *>(helperObj.get());
+ obj->setProperty(propertyName, QVariant::fromValue(changed));
+ return obj->property(propertyName).template value<PropertyType>();
+ }, {});
+ bindable.setBinding(binding);
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), changed,
+ comparator, represent);
+ QVERIFY2(!binding.error().hasError(), qPrintable(binding.error().description()));
+ }
+}
+
+/*!
+ \internal
+ \overload
+
+ This overload supports the case where the caller only needs to override
+ the default for \a helperConstructor. It uses the defaults for all the other
+ parameters.
+*/
+template<typename TestedClass, typename PropertyType>
+void testReadWritePropertyBasics(
+ TestedClass &instance, const PropertyType &initial, const PropertyType &changed,
+ const char *propertyName,
+ std::function<std::unique_ptr<TestedClass>(void)> helperConstructor)
+{
+ testReadWritePropertyBasics<TestedClass, PropertyType>(
+ instance, initial, changed, propertyName,
+ [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
+ [](const PropertyType &val) { return QTest::toString(val); },
+ helperConstructor);
+}
+
+/*!
+ \internal
+ Basic testing of a bindable property that is writable only once.
+
+ The write-once properties are writable properties which accept only
+ one valid setting of the value ("write"), after which later attempts
+ are ignored.
+
+ This helper function tests the behavior of bindable write-once property
+ \a propertyName, of type \c PropertyType, in class \c TestedClass.
+ The caller must supply an \a instance of \c TestedClass and two distinct
+ values, \a initial and \a changed, of \c PropertyType.
+
+ The property of \a instance must not yet have been set when this function
+ is called. The value it has before being set should be passed as \a prior
+ and a distinct value, that this test can set it to, as \a changed.
+
+ The \a bindingPreservedOnWrite parameter controls whether this function
+ expects the binding set by this function to be preserved when setting a value
+ directly. The default value is 'true'.
+
+ By default \c {operator==()} is used to compare values of the property and
+ \c {QTest::toString()} is used to generate proper error messages.
+
+ If such comparison is not supported for \c PropertyType, or the comparison
+ it supports is not appropriate to this property, a custom \a comparator can
+ be supplied.
+
+ Apart from that, a custom \a represent callback can also be specified to
+ generate a string representation of \c PropertyType. If supplied, it must
+ allocate its returned string using \c {new char[]}, so that it can be used
+ in place of \l {QTest::toString()}.
+
+ The \a helperConstructor method is used to create another instance of
+ \c TestedClass. This instance is used to test for binding loops. By default,
+ the method returns a default-constructed \c TestedClass. A custom
+ \a helperConstructor should be provided if \c TestedClass is not
+ default-constructible. Some very specific properties cannot be tested for
+ binding loops. Pass a lambda that returns an \c {std::nullptr} as
+ \a helperConstructor in such case.
+
+ \note Any test calling this method will need to call
+ \code
+ if (QTest::currentTestFailed())
+ return;
+ \endcode
+ after doing so, if there is any later code in the test. If testing several
+ properties in one test method, emitting a warning message saying which
+ property failed, before returning, is a kindness to readers of the output.
+*/
+
+template<typename TestedClass, typename PropertyType>
+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>(); })
+{
+ // get the property
+ const QMetaObject *metaObject = instance.metaObject();
+ QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
+
+ // in case the TestedClass has setProperty()/property() methods.
+ QObject &testedObj = static_cast<QObject &>(instance);
+
+ QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
+ QByteArray("Preconditions not met for ") + propertyName + "\n"
+ "The type of prior and changed value does not match the type of the property.\n"
+ "Please ensure that the types match exactly (convertability is not enough).\n"
+ "You can provide the template types to the "
+ "function explicitly to force a certain type.\n"
+ "Property is " + metaProperty.metaType().name()
+ + " but parameters are " + QMetaType::fromType<PropertyType>().name() + ".\n");
+
+ QVERIFY2(metaProperty.isBindable(), "Preconditions not met for " + QByteArray(propertyName));
+
+ QUntypedBindable bindable = metaProperty.bindable(&instance);
+
+ QScopedPointer<QSignalSpy> spy(nullptr);
+ if (metaProperty.hasNotifySignal())
+ spy.reset(new QSignalSpy(&instance, metaProperty.notifySignal()));
+
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), prior, comparator,
+ represent);
+
+ QProperty<PropertyType> propObserver;
+ propObserver.setBinding(bindable.makeBinding());
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), prior, comparator, represent);
+
+ // Create a binding that sets the 'changed' value to the property.
+ // This also tests binding loops.
+ QVERIFY(!bindable.hasBinding());
+ std::unique_ptr<TestedClass> helperObj = helperConstructor();
+ QProperty<PropertyType> propSetter(changed); // if the helperConstructor() returns nullptr
+ const QPropertyBinding<PropertyType> binding = helperObj
+ ? Qt::makePropertyBinding([&]() {
+ QObject *obj = static_cast<QObject *>(helperObj.get());
+ obj->setProperty(propertyName, QVariant::fromValue(changed));
+ return obj->property(propertyName).template value<PropertyType>();
+ })
+ : Qt::makePropertyBinding(propSetter);
+ bindable.setBinding(binding);
+ QVERIFY(bindable.hasBinding());
+
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
+ represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
+ if (spy)
+ QCOMPARE(spy->size(), 1);
+
+ // Attempt to set back the 'prior' value and verify that it has no effect
+ testedObj.setProperty(propertyName, QVariant::fromValue(prior));
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
+ represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
+ if (spy)
+ QCOMPARE(spy->size(), 1);
+ if (bindingPreservedOnWrite)
+ QVERIFY(bindable.hasBinding());
+ else
+ QVERIFY(!bindable.hasBinding());
+}
+
+/*!
+ \internal
+ \overload
+
+ This overload supports the case where the caller only needs to override
+ the default for \a helperConstructor. It uses the defaults for all the other
+ parameters.
+*/
+template<typename TestedClass, typename PropertyType>
+void testWriteOncePropertyBasics(
+ TestedClass &instance, const PropertyType &prior, const PropertyType &changed,
+ const char *propertyName,
+ bool bindingPreservedOnWrite,
+ std::function<std::unique_ptr<TestedClass>(void)> helperConstructor)
+{
+ testWriteOncePropertyBasics<TestedClass, PropertyType>(
+ instance, prior, changed, propertyName, bindingPreservedOnWrite,
+ [](const PropertyType &lhs, const PropertyType &rhs) { return lhs == rhs; },
+ [](const PropertyType &val) { return QTest::toString(val); },
+ helperConstructor);
+}
+
+/*!
+ \internal
+ Basic testing of a read-only bindable property.
+
+ This helper function tests the behavior of bindable read-only property
+ \a propertyName, of type \c PropertyType, in class \c TestedClass.
+ The caller must supply an \a instance of \c TestedClass and two distinct
+ values, \a initial and \a changed, of \c PropertyType.
+
+ When this function is called, the property's value must be \a initial.
+ The \a mutator must, when called, cause the property's value to be revised
+ to \a changed.
+
+ By default \c {operator==()} is used to compare values of the property and
+ \c {QTest::toString()} is used to generate proper error messages.
+
+ If such comparison is not supported for \c PropertyType, or the comparison
+ it supports is not appropriate to this property, a custom \a comparator can
+ be supplied.
+
+ Apart from that, a custom \a represent callback can also be specified to
+ generate a string representation of \c PropertyType. If supplied, it must
+ allocate its returned string using \c {new char[]}, so that it can be used
+ in place of \l {QTest::toString()}.
+
+ \note Any test calling this method will need to call
+ \code
+ if (QTest::currentTestFailed())
+ return;
+ \endcode
+ after doing so, if there is any later code in the test. If testing several
+ properties in one test method, emitting a warning message saying which
+ property failed, before returning, is a kindness to readers of the output.
+*/
+template<typename TestedClass, typename PropertyType>
+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); })
+{
+ // get the property
+ const QMetaObject *metaObject = instance.metaObject();
+ QMetaProperty metaProperty = metaObject->property(metaObject->indexOfProperty(propertyName));
+
+ // in case the TestedClass has setProperty()/property() methods.
+ QObject &testedObj = static_cast<QObject &>(instance);
+
+ QVERIFY2(metaProperty.metaType() == QMetaType::fromType<PropertyType>(),
+ QByteArray("Preconditions not met for ") + propertyName + "\n"
+ "The type of initial and changed value does not match the type of the property.\n"
+ "Please ensure that the types match exactly (convertability is not enough).\n"
+ "You can provide the template types to the "
+ "function explicitly to force a certain type.\n"
+ "Expected was a " + metaProperty.metaType().name()
+ + " but " + QMetaType::fromType<PropertyType>().name() + " was provided.");
+
+ QVERIFY2(metaProperty.isBindable(), "Preconditions not met for " + QByteArray(propertyName));
+
+ QUntypedBindable bindable = metaProperty.bindable(&instance);
+
+ QScopedPointer<QSignalSpy> spy(nullptr);
+ if (metaProperty.hasNotifySignal())
+ spy.reset(new QSignalSpy(&instance, metaProperty.notifySignal()));
+
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
+ represent);
+
+ // Check that attempting to bind this read-only property to another property has no effect:
+ QProperty<PropertyType> propSetter(initial);
+ QVERIFY(!bindable.hasBinding());
+ bindable.setBinding(Qt::makePropertyBinding(propSetter));
+ QVERIFY(!bindable.hasBinding());
+ propSetter.setValue(changed);
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), initial, comparator,
+ represent);
+ if (spy)
+ QCOMPARE(spy->size(), 0);
+
+ QProperty<PropertyType> propObserver;
+ propObserver.setBinding(bindable.makeBinding());
+
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), initial, comparator, represent);
+
+ // Invoke mutator function. Now property value should be changed.
+ mutator();
+
+ QPROPERTY_TEST_COMPARISON_HELPER(
+ testedObj.property(propertyName).template value<PropertyType>(), changed, comparator,
+ represent);
+ QPROPERTY_TEST_COMPARISON_HELPER(propObserver.value(), changed, comparator, represent);
+
+ if (spy)
+ QCOMPARE(spy->size(), 1);
+}
+
+} // namespace QTestPrivate
+
+QT_END_NAMESPACE
+
+#endif // QPROPERTYTESTHELPER_P_H