diff options
Diffstat (limited to 'tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp')
-rw-r--r-- | tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp | 1579 |
1 files changed, 1426 insertions, 153 deletions
diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index 21e0955ba5..cc7edb8bf2 100644 --- a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp +++ b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp @@ -1,52 +1,48 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QObject> +#include <QSignalSpy> #include <qtest.h> #include <qproperty.h> #include <private/qproperty_p.h> +#include <private/qobject_p.h> + +#if __has_include(<source_location>) && __cplusplus >= 202002L && !defined(Q_QDOC) +#include <source_location> +#define QT_SOURCE_LOCATION_NAMESPACE std +#elif __has_include(<experimental/source_location>) && !defined(Q_QDOC) +#include <experimental/source_location> +#define QT_SOURCE_LOCATION_NAMESPACE std::experimental +#endif using namespace QtPrivate; +using namespace Qt::StringLiterals; + +struct DtorCounter { + static inline int counter = 0; + bool shouldIncrement = false; + ~DtorCounter() {if (shouldIncrement) ++counter;} +}; class tst_QProperty : public QObject { Q_OBJECT private slots: + void inheritQUntypedPropertyData(); void functorBinding(); void basicDependencies(); void multipleDependencies(); void bindingWithDeletedDependency(); + void dependencyChangeDuringDestruction(); void recursiveDependency(); void bindingAfterUse(); + void bindingFunctionDtorCalled(); void switchBinding(); void avoidDependencyAllocationAfterFirstEval(); void boolProperty(); void takeBinding(); + void stickyBinding(); void replaceBinding(); void changeHandler(); void propertyChangeHandlerApi(); @@ -65,21 +61,110 @@ private slots: void genericPropertyBindingBool(); void setBindingFunctor(); void multipleObservers(); - void propertyAlias(); void arrowAndStarOperator(); void notifiedProperty(); void typeNoOperatorEqual(); void bindingValueReplacement(); + void quntypedBindableApi(); + void readonlyConstQBindable(); + void qobjectBindableManualNotify(); + void qobjectBindableReallocatedBindingStorage(); + void qobjectBindableSignalTakingNewValue(); void testNewStuff(); void qobjectObservers(); void compatBindings(); void metaProperty(); - void aliasOnMetaProperty(); void modifyObserverListWhileIterating(); + void noDoubleCapture(); + void compatPropertyNoDobuleNotification(); + void compatPropertySignals(); + + void noFakeDependencies(); +#if QT_CONFIG(thread) + void threadSafety(); + void threadSafety2(); +#endif // QT_CONFIG(thread) + + void bindablePropertyWithInitialization(); + void noDoubleNotification(); + void groupedNotifications(); + void groupedNotificationConsistency(); + void bindingGroupMovingBindingData(); + void bindingGroupBindingDeleted(); + void uninstalledBindingDoesNotEvaluate(); + + void notify(); + + void bindableInterfaceOfCompatPropertyUsesSetter(); + + void selfBindingShouldNotCrash(); + + void qpropertyAlias(); + void scheduleNotify(); + + void notifyAfterAllDepsGone(); + + void propertyAdaptorBinding(); + void propertyUpdateViaSignaledProperty(); + + void derefFromObserver(); }; +namespace { +template <class T> +constexpr auto isDerivedFromQUntypedPropertyData = std::is_base_of_v<QUntypedPropertyData, T>; + +template <typename Property> +constexpr auto isDerivedFromQUntypedPropertyDataFunc(const Property &property) +{ + Q_UNUSED(property); + return isDerivedFromQUntypedPropertyData<Property>; +} + +template <typename Property> +constexpr auto isDerivedFromQUntypedPropertyDataFunc(Property *property) +{ + Q_UNUSED(property); + return isDerivedFromQUntypedPropertyData<Property>; +} +} // namespace + +void tst_QProperty::inheritQUntypedPropertyData() +{ + class propertyPublic : public QUntypedPropertyData + { + }; + class propertyPrivate : private QUntypedPropertyData + { + }; + + // Compile time test + static_assert(isDerivedFromQUntypedPropertyData<propertyPublic>); + static_assert(isDerivedFromQUntypedPropertyData<propertyPrivate>); + static_assert(isDerivedFromQUntypedPropertyData<QPropertyData<int>>); + static_assert(isDerivedFromQUntypedPropertyData<QProperty<int>>); + + // Run time test + propertyPublic _propertyPublic; + propertyPrivate _propertyPrivate; + QPropertyData<int> qpropertyData; + QProperty<int> qproperty; + std::unique_ptr<propertyPublic> _propertyPublicPtr{ new propertyPublic }; + std::unique_ptr<propertyPrivate> _propertyPrivatePtr{ new propertyPrivate }; + std::unique_ptr<QPropertyData<int>> qpropertyDataPtr{ new QPropertyData<int> }; + std::unique_ptr<QProperty<int>> qpropertyPtr{ new QProperty<int> }; + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(_propertyPublic)); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(_propertyPrivate)); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(qpropertyData)); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(qproperty)); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(_propertyPublicPtr.get())); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(_propertyPrivatePtr.get())); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(qpropertyDataPtr.get())); + QVERIFY(isDerivedFromQUntypedPropertyDataFunc(qpropertyPtr.get())); +} + void tst_QProperty::functorBinding() { QProperty<int> property([]() { return 42; }); @@ -111,8 +196,8 @@ void tst_QProperty::multipleDependencies() QProperty<int> sum; sum.setBinding([&]() { return firstDependency + secondDependency; }); - QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 0); - QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 0); + QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); + QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); QCOMPARE(sum.value(), int(3)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); @@ -167,6 +252,35 @@ void tst_QProperty::bindingWithDeletedDependency() QCOMPARE(propertySelector.value(), staticProperty.value()); } +class ChangeDuringDtorTester : public QObject +{ + Q_OBJECT + Q_PROPERTY(int prop READ prop WRITE setProp BINDABLE bindableProp) + +public: + void setProp(int i) { m_prop = i;} + int prop() const { return m_prop; } + QBindable<int> bindableProp() { return &m_prop; } +private: + Q_OBJECT_COMPAT_PROPERTY(ChangeDuringDtorTester, int, m_prop, &ChangeDuringDtorTester::setProp) +}; + +void tst_QProperty::dependencyChangeDuringDestruction() +{ + auto tester = std::make_unique<ChangeDuringDtorTester>(); + QProperty<int> iprop {42}; + tester->bindableProp().setBinding(Qt::makePropertyBinding(iprop)); + QObject::connect(tester.get(), &QObject::destroyed, [&](){ + iprop = 12; + }); + bool failed = false; + auto handler = tester->bindableProp().onValueChanged([&](){ + failed = true; + }); + tester.reset(); + QVERIFY(!failed); +} + void tst_QProperty::recursiveDependency() { QProperty<int> first(1); @@ -201,6 +315,21 @@ void tst_QProperty::bindingAfterUse() QCOMPARE(QPropertyBindingDataPointer::get(propWithBindingLater).observerCount(), 1); } +void tst_QProperty::bindingFunctionDtorCalled() +{ + DtorCounter::counter = 0; + DtorCounter dc; + { + QProperty<int> prop; + prop.setBinding([dc]() mutable { + dc.shouldIncrement = true; + return 42; + }); + QCOMPARE(prop.value(), 42); + } + QCOMPARE(DtorCounter::counter, 1); +} + void tst_QProperty::switchBinding() { QProperty<int> first(1); @@ -228,12 +357,12 @@ void tst_QProperty::avoidDependencyAllocationAfterFirstEval() QCOMPARE(propWithBinding.value(), int(11)); - QVERIFY(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()); - QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()->dependencyObserverCount, 2u); + QVERIFY(QPropertyBindingDataPointer::get(propWithBinding).binding()); + QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); firstDependency = 100; QCOMPARE(propWithBinding.value(), int(110)); - QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()->dependencyObserverCount, 2u); + QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); } void tst_QProperty::boolProperty() @@ -273,6 +402,30 @@ void tst_QProperty::takeBinding() QVERIFY(!existingBinding.isNull()); } +void tst_QProperty::stickyBinding() +{ + QProperty<int> prop; + QProperty<int> prop2 {2}; + prop.setBinding([&](){ return prop2.value(); }); + QCOMPARE(prop.value(), 2); + auto privBinding = QPropertyBindingPrivate::get(prop.binding()); + // If we make a binding sticky, + privBinding->setSticky(); + // then writing to the property does not remove it + prop = 1; + QVERIFY(prop.hasBinding()); + // but the value still changes. + QCOMPARE(prop.value(), 1); + // The binding continues to work normally. + prop2 = 3; + QCOMPARE(prop.value(), 3); + // If we remove the stickiness + privBinding->setSticky(false); + // the binding goes away on the next write + prop = 42; + QVERIFY(!prop.hasBinding()); +} + void tst_QProperty::replaceBinding() { QProperty<int> first(100); @@ -303,7 +456,7 @@ void tst_QProperty::changeHandler() } testProperty = 3; - QCOMPARE(recordedValues.count(), 2); + QCOMPARE(recordedValues.size(), 2); QCOMPARE(recordedValues.at(0), 1); QCOMPARE(recordedValues.at(1), 2); } @@ -346,7 +499,7 @@ void tst_QProperty::subscribe() } testProperty = 3; - QCOMPARE(recordedValues.count(), 3); + QCOMPARE(recordedValues.size(), 3); QCOMPARE(recordedValues.at(0), 42); QCOMPARE(recordedValues.at(1), 1); QCOMPARE(recordedValues.at(2), 2); @@ -405,7 +558,7 @@ void tst_QProperty::dontTriggerDependenciesIfUnchangedValue() void tst_QProperty::bindingSourceLocation() { #if defined(QT_PROPERTY_COLLECT_BINDING_LOCATION) - auto bindingLine = std::experimental::source_location::current().line() + 1; + auto bindingLine = QT_SOURCE_LOCATION_NAMESPACE::source_location::current().line() + 1; auto binding = Qt::makePropertyBinding([]() { return 42; }); QCOMPARE(QPropertyBindingPrivate::get(binding)->sourceLocation().line, bindingLine); #else @@ -437,44 +590,54 @@ class BindingLoopTester : public QObject eagerData2.setBinding(Qt::makePropertyBinding([&](){ return eagerData.value() + 1; } ) ); i->setValue(42); } + BindingLoopTester() {} int eagerProp() {return eagerData.value();} - void setEagerProp(int i) { eagerData = i; } + void setEagerProp(int i) { eagerData.setValue(i); eagerData.notify(); } QBindable<int> bindableEagerProp() {return QBindable<int>(&eagerData);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData, &BindingLoopTester::setEagerProp) int eagerProp2() {return eagerData2.value();} - void setEagerProp2(int i) { eagerData2 = i; } + void setEagerProp2(int i) { eagerData2.setValue(i); eagerData2.notify(); } QBindable<int> bindableEagerProp2() {return QBindable<int>(&eagerData2);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData2, &BindingLoopTester::setEagerProp2) }; void tst_QProperty::bindingLoop() { - QScopedPointer<QProperty<int>> firstProp; + QProperty<int> firstProp; QProperty<int> secondProp([&]() -> int { - return firstProp ? firstProp->value() : 0; + return firstProp.value(); }); QProperty<int> thirdProp([&]() -> int { return secondProp.value(); }); - firstProp.reset(new QProperty<int>()); - firstProp->setBinding([&]() -> int { - return secondProp.value(); + firstProp.setBinding([&]() -> int { + return secondProp.value() + thirdProp.value(); }); - QCOMPARE(thirdProp.value(), 0); - QCOMPARE(secondProp.binding().error().type(), QPropertyBindingError::BindingLoop); + thirdProp.setValue(10); + QCOMPARE(firstProp.binding().error().type(), QPropertyBindingError::BindingLoop); - QProperty<int> i; - BindingLoopTester tester(&i); - QCOMPARE(tester.bindableEagerProp().binding().error().type(), QPropertyBindingError::BindingLoop); - QEXPECT_FAIL("", "Only the first property in a dependency cycle is set to the error state", Continue); - QCOMPARE(tester.bindableEagerProp2().binding().error().type(), QPropertyBindingError::BindingLoop); + { + QProperty<int> i; + BindingLoopTester tester(&i); + QCOMPARE(tester.bindableEagerProp().binding().error().type(), QPropertyBindingError::BindingLoop); + QCOMPARE(tester.bindableEagerProp2().binding().error().type(), QPropertyBindingError::BindingLoop); + } + { + BindingLoopTester tester; + auto handler = tester.bindableEagerProp().onValueChanged([&]() { + tester.bindableEagerProp().setBinding([](){return 42;}); + }); + tester.bindableEagerProp().setBinding([]() {return 42;}); + QCOMPARE(tester.bindableEagerProp().binding().error().type(), QPropertyBindingError::BindingLoop); + QCOMPARE(tester.bindableEagerProp().binding().error().description(), "Binding set during binding evaluation!"); + } } class ReallocTester : public QObject @@ -496,7 +659,7 @@ public: #define GEN(N) \ int prop##N() {return propData##N.value();} \ - void setProp##N(int i) { propData##N = i; } \ + void setProp##N(int i) { if (i == propData##N) return; propData##N.setValue(i); propData##N.notify(); } \ QBindable<int> bindableProp##N() {return QBindable<int>(&propData##N);} \ Q_OBJECT_COMPAT_PROPERTY(ReallocTester, int, propData##N, &ReallocTester::setProp##N) GEN(1) @@ -530,11 +693,12 @@ void tst_QProperty::realloc() QCOMPARE(tester.prop1(), 12); tester.bindableProp1().setBinding([&](){return tester.prop5();}); + QCOMPARE(modificationCount, 2); tester.bindableProp2().setBinding([&](){return tester.prop5();}); tester.bindableProp3().setBinding([&](){return tester.prop5();}); tester.bindableProp4().setBinding([&](){return tester.prop5();}); tester.bindableProp5().setBinding([&]() -> int{return 42;}); - QCOMPARE(modificationCount, 2); + QCOMPARE(modificationCount, 3); } }; @@ -578,7 +742,8 @@ void tst_QProperty::changePropertyFromWithinChangeHandlerThroughDependency() resetPropertyOnChange = true; sourceProperty = 42; - QCOMPARE(property.value(), 100); + QVERIFY(property.value() == 100 || property.value() == 42); + QVERIFY(property.binding().error().type() == QPropertyBindingError::BindingLoop); // changing the property value inside the change handler won't result in the change // handler being called again. QCOMPARE(changeHandlerCallCount, 1); @@ -599,6 +764,7 @@ void tst_QProperty::changePropertyFromWithinChangeHandler2() property = 42; QCOMPARE(property.value(), 43); + QVERIFY(!property.hasBinding()); // setting the value in the change handler removed the binding } void tst_QProperty::settingPropertyValueDoesRemoveBinding() @@ -625,7 +791,7 @@ void tst_QProperty::genericPropertyBinding() { QUntypedPropertyBinding doubleBinding(QMetaType::fromType<double>(), - [](const QMetaType &, void *) -> bool { + [](QMetaType , void *) -> bool { Q_ASSERT(false); return true; }, QPropertyBindingSourceLocation()); @@ -633,7 +799,7 @@ void tst_QProperty::genericPropertyBinding() } QUntypedPropertyBinding intBinding(QMetaType::fromType<int>(), - [](const QMetaType &metaType, void *dataPtr) -> bool { + [](QMetaType metaType, void *dataPtr) -> bool { Q_ASSERT(metaType.id() == qMetaTypeId<int>()); int *intPtr = reinterpret_cast<int*>(dataPtr); @@ -653,7 +819,7 @@ void tst_QProperty::genericPropertyBindingBool() QVERIFY(!property.value()); QUntypedPropertyBinding boolBinding(QMetaType::fromType<bool>(), - [](const QMetaType &, void *dataPtr) -> bool { + [](QMetaType, void *dataPtr) -> bool { auto boolPtr = reinterpret_cast<bool *>(dataPtr); *boolPtr = true; return true; @@ -709,52 +875,6 @@ void tst_QProperty::multipleObservers() QCOMPARE(property.value(), 22); } -void tst_QProperty::propertyAlias() -{ - QScopedPointer<QProperty<int>> property(new QProperty<int>); - property->setValue(5); - QPropertyAlias alias(property.get()); - QVERIFY(alias.isValid()); - QCOMPARE(alias.value(), 5); - - int value1 = 1; - auto changeHandler = alias.onValueChanged([&]() { value1 = alias.value(); }); - QCOMPARE(value1, 1); - - int value2 = 2; - auto subscribeHandler = alias.subscribe([&]() { value2 = alias.value(); }); - QCOMPARE(value2, 5); - - alias.setValue(6); - QVERIFY(alias.isValid()); - QCOMPARE(alias.value(), 6); - QCOMPARE(value1, 6); - QCOMPARE(value2, 6); - - alias.setBinding([]() { return 12; }); - QCOMPARE(value1, 12); - QCOMPARE(value2, 12); - QCOMPARE(alias.value(), 12); - - alias.setValue(22); - QCOMPARE(value1, 22); - QCOMPARE(value2, 22); - QCOMPARE(alias.value(), 22); - - property.reset(); - - QVERIFY(!alias.isValid()); - QCOMPARE(alias.value(), int()); - QCOMPARE(value1, 22); - QCOMPARE(value2, 22); - - // Does not crash - alias.setValue(25); - QCOMPARE(alias.value(), int()); - QCOMPARE(value1, 22); - QCOMPARE(value2, 22); -} - void tst_QProperty::arrowAndStarOperator() { QString str("Hello"); @@ -814,7 +934,7 @@ void tst_QProperty::notifiedProperty() check(); instance.property.setValue(42); - QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 42); instance.recordedValues.clear(); check(); @@ -844,7 +964,7 @@ void tst_QProperty::notifiedProperty() subscribedCount = 0; QCOMPARE(instance.property.value(), 100); - QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 100); instance.recordedValues.clear(); check(); @@ -852,7 +972,7 @@ void tst_QProperty::notifiedProperty() injectedValue = 200; QCOMPARE(instance.property.value(), 200); - QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 200); instance.recordedValues.clear(); check(); @@ -861,7 +981,7 @@ void tst_QProperty::notifiedProperty() injectedValue = 400; QCOMPARE(instance.property.value(), 400); - QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 400); instance.recordedValues.clear(); check(); @@ -968,25 +1088,79 @@ void tst_QProperty::bindingValueReplacement() // QCOMPARE(test.iconText.value(), 42); } +void tst_QProperty::quntypedBindableApi() +{ + QProperty<int> iprop; + QUntypedBindable bindable(&iprop); + QVERIFY(!bindable.hasBinding()); + QVERIFY(bindable.binding().isNull()); + bindable.setBinding(Qt::makePropertyBinding([]() -> int {return 42;})); + QVERIFY(bindable.hasBinding()); + QVERIFY(!bindable.binding().isNull()); + QUntypedPropertyBinding binding = bindable.takeBinding(); + QVERIFY(!bindable.hasBinding()); + bindable.setBinding(binding); + QCOMPARE(iprop.value(), 42); + QUntypedBindable propLess; + QVERIFY(propLess.takeBinding().isNull()); + + QUntypedBindable invalidBindable; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is invalid."); +#endif + invalidBindable.setBinding(Qt::makePropertyBinding(iprop)); + + QUntypedBindable readOnlyBindable(static_cast<const QProperty<int> *>(&iprop) ); + QVERIFY(readOnlyBindable.isReadOnly()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is read-only."); +#endif + readOnlyBindable.setBinding(Qt::makePropertyBinding(iprop)); + + QProperty<float> fprop; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding as the property expects it to be of type int but got float instead."); +#endif + bindable.setBinding(Qt::makePropertyBinding(fprop)); +} + +void tst_QProperty::readonlyConstQBindable() +{ + QProperty<int> i {42}; + const QBindable<int> bindableI(const_cast<const QProperty<int> *>(&i)); + + // check that read-only operations work with a const QBindable + QVERIFY(bindableI.isReadOnly()); + QVERIFY(!bindableI.hasBinding()); + // we can still create a binding to a read only bindable through the interface + QProperty<int> j; + j.setBinding(bindableI.makeBinding()); + QCOMPARE(j.value(), bindableI.value()); + int counter = 0; + auto observer = bindableI.subscribe([&](){++counter;}); + QCOMPARE(counter, 1); + auto observer2 = bindableI.onValueChanged([&](){++counter;}); + i = 0; + QCOMPARE(counter, 3); +} + class MyQObject : public QObject { Q_OBJECT Q_PROPERTY(int foo READ foo WRITE setFoo BINDABLE bindableFoo NOTIFY fooChanged) Q_PROPERTY(int bar READ bar WRITE setBar BINDABLE bindableBar NOTIFY barChanged) - Q_PROPERTY(int read READ read NOTIFY readChanged) + Q_PROPERTY(int read READ read) Q_PROPERTY(int computed READ computed STORED false) Q_PROPERTY(int compat READ compat WRITE setCompat NOTIFY compatChanged) signals: - void fooChanged(); + void fooChanged(int newFoo); void barChanged(); - void readChanged(); void compatChanged(); public slots: void fooHasChanged() { fooChangedCount++; } void barHasChanged() { barChangedCount++; } - void readHasChanged() { readChangedCount++; } void compatHasChanged() { compatChangedCount++; } public: @@ -1005,11 +1179,13 @@ public: ++setCompatCalled; if (i < 0) i = 0; - compatData = i; + compatData.setValue(i); + compatData.notify(); emit compatChanged(); } QBindable<int> bindableFoo() { return QBindable<int>(&fooData); } + const QBindable<int> bindableFoo() const { return QBindable<int>(&fooData); } QBindable<int> bindableBar() { return QBindable<int>(&barData); } QBindable<int> bindableRead() { return QBindable<int>(&readData); } QBindable<int> bindableComputed() { return QBindable<int>(&computedData); } @@ -1018,23 +1194,114 @@ public: public: int fooChangedCount = 0; int barChangedCount = 0; - int readChangedCount = 0; int compatChangedCount = 0; int setCompatCalled = 0; Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, fooData, &MyQObject::fooChanged); Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, barData, &MyQObject::barChanged); - Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, readData, &MyQObject::readChanged); + Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, readData); Q_OBJECT_COMPUTED_PROPERTY(MyQObject, int, computedData, &MyQObject::computed); Q_OBJECT_COMPAT_PROPERTY(MyQObject, int, compatData, &MyQObject::setCompat) }; + +void tst_QProperty::qobjectBindableManualNotify() +{ + // Given an object of type MyQObject, + MyQObject object; + // track its foo property's change count + auto bindable = object.bindableFoo(); + int fooChangeCount = 0; + auto changeHandler = bindable.onValueChanged([&](){++fooChangeCount;}); + // and how many changed signals it emits. + QSignalSpy fooChangedSpy(&object, &MyQObject::fooChanged); + + // If we bypass the bindings system, + object.fooData.setValueBypassingBindings(42); + // there is no change. + QCOMPARE(fooChangeCount, 0); + QCOMPARE(fooChangedSpy.size(), 0); + // Once we notify manually + object.fooData.notify(); + // observers are notified and the signal arrives. + QCOMPARE(fooChangeCount, 1); + QCOMPARE(fooChangedSpy.size(), 1); + + // If we set a binding + int i = 1; + object.fooData.setBinding([&](){return i;}); + // then the value changes + QCOMPARE(object.foo(), 1); + // and the change and signal count are incremented. + QCOMPARE(fooChangeCount, 2); + QCOMPARE(fooChangedSpy.size(), 2); + // Changing a non-property won't trigger any notification. + i = 2; + QCOMPARE(fooChangeCount, 2); + QCOMPARE(fooChangedSpy.size(), 2); + // Manually triggering the notification + object.fooData.notify(); + // increments the change count + QCOMPARE(fooChangeCount, 3); + QCOMPARE(fooChangedSpy.size(), 3); + // but doesn't actually cause a binding reevaluation. + QCOMPARE(object.foo(), 1); +} + + +struct ReallocObject : QObject { + ReallocObject() + { v.setBinding([this] { return x.value() + y.value() + z.value(); }); } + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, v) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, x) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, y) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, z) +}; + +void tst_QProperty::qobjectBindableReallocatedBindingStorage() +{ + ReallocObject object; + object.x = 1; + QCOMPARE(object.v.value(), 1); +} + +void tst_QProperty::qobjectBindableSignalTakingNewValue() +{ + // Given an object of type MyQObject, + MyQObject object; + // and tracking the values emitted via its fooChanged signal, + int newValue = -1; + QObject::connect(&object, &MyQObject::fooChanged, [&](int i){ newValue = i; } ); + + // when we change the property's value via the bindable interface + object.bindableFoo().setValue(1); + // we obtain the newly set value. + QCOMPARE(newValue, 1); + + // The same holds true when we set a binding + QProperty<int> i {2}; + object.bindableFoo().setBinding(Qt::makePropertyBinding(i)); + QCOMPARE(newValue, 2); + // and when the binding gets reevaluated to a new value + i = 3; + QCOMPARE(newValue, 3); +} + void tst_QProperty::testNewStuff() { + MyQObject testReadOnly; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is read-only."); +#endif + testReadOnly.bindableFoo().setBinding([](){return 42;}); + auto bindable = const_cast<const MyQObject&>(testReadOnly).bindableFoo(); + QVERIFY(bindable.hasBinding()); + QVERIFY(bindable.isReadOnly()); + MyQObject object; + QVERIFY(!object.bindableFoo().isReadOnly()); QObject::connect(&object, &MyQObject::fooChanged, &object, &MyQObject::fooHasChanged); QObject::connect(&object, &MyQObject::barChanged, &object, &MyQObject::barHasChanged); - QObject::connect(&object, &MyQObject::readChanged, &object, &MyQObject::readHasChanged); QCOMPARE(object.fooChangedCount, 0); object.setFoo(10); @@ -1088,9 +1355,23 @@ void tst_QProperty::testNewStuff() object.readData.setValue(111); QCOMPARE(object.computed(), 111); + object.bindableComputed().setBinding(object.bindableBar().makeBinding()); + QCOMPARE(object.computed(), 111); + object.bindableComputed().setValue(10); + QCOMPARE(object.computed(), 111); + QCOMPARE(object.bindableFoo().value(), 111); object.bindableFoo().setValue(24); QCOMPARE(object.foo(), 24); + + auto isCurrentlyEvaluatingBinding = []() { + return QtPrivate::isAnyBindingEvaluating(); + }; + QVERIFY(!isCurrentlyEvaluatingBinding()); + QProperty<bool> evaluationDetector {false}; + evaluationDetector.setBinding(isCurrentlyEvaluatingBinding); + QVERIFY(evaluationDetector.value()); + QVERIFY(!isCurrentlyEvaluatingBinding()); } void tst_QProperty::qobjectObservers() @@ -1098,7 +1379,9 @@ void tst_QProperty::qobjectObservers() MyQObject object; int onValueChangedCalled = 0; { - auto handler = object.bindableFoo().onValueChanged([&onValueChangedCalled]() { ++onValueChangedCalled;}); + auto handler = object.bindableFoo().onValueChanged([&onValueChangedCalled]() { + ++onValueChangedCalled; + }); QCOMPARE(onValueChangedCalled, 0); object.setFoo(10); @@ -1123,7 +1406,7 @@ void tst_QProperty::compatBindings() QCOMPARE(object.compatData, 0); // setting data through the private interface should not call the changed signal or the public setter - object.compatData = 10; + object.compatData.setValue(10); QCOMPARE(object.compatChangedCount, 0); QCOMPARE(object.setCompatCalled, 0); // going through the public API should emit the signal @@ -1217,42 +1500,6 @@ void tst_QProperty::metaProperty() QCOMPARE(object.fooData.value(), 1); } -void tst_QProperty::aliasOnMetaProperty() -{ - MyQObject object; - QPropertyAlias<int> alias(object.bindableFoo()); - - QVERIFY(alias.isValid()); - QCOMPARE(alias.value(), object.foo()); - QVERIFY(!alias.hasBinding()); - - object.setFoo(42); - QCOMPARE(alias.value(), 42); - - auto f = [&object]() -> int { - return object.barData; - }; - object.bindableFoo().setBinding(f); - QVERIFY(alias.hasBinding()); - QCOMPARE(alias.value(), object.bar()); - - object.setBar(111); - QCOMPARE(alias.value(), 111); - - int changedCount = 0; - auto observer = alias.onValueChanged([&changedCount]() { ++changedCount; }); - QCOMPARE(changedCount, 0); - object.setBar(666); - QCOMPARE(changedCount, 1); - - alias.setBinding([&object]() { return object.read(); }); - QCOMPARE(changedCount, 2); - QCOMPARE(alias.value(), 0); - object.readData = 100; - QCOMPARE(changedCount, 3); - QCOMPARE(alias.value(), 100); -} - void tst_QProperty::modifyObserverListWhileIterating() { struct DestructingObserver : QPropertyObserver { @@ -1271,6 +1518,7 @@ void tst_QProperty::modifyObserverListWhileIterating() DestructingObserver observer = {}; char* memory; ~ObserverOrUninit() {} + ObserverOrUninit() {} }; { // observer deletes itself while running the notification @@ -1304,6 +1552,1031 @@ void tst_QProperty::modifyObserverListWhileIterating() } } +void tst_QProperty::noDoubleCapture() +{ + QProperty<long long> size; + size = 3; + QProperty<int> max; + max.setBinding([&size]() -> int { + // each loop run attempts to capture size + for (int i = 0; i < size; ++i) {} + return size.value(); + }); + auto bindingPriv = QPropertyBindingPrivate::get(max.binding()); + QCOMPARE(bindingPriv->dependencyObserverCount, 1U); + size = 4; // should not crash + QCOMPARE(max.value(), 4); +} + +class CompatPropertyTester : public QObject +{ + Q_OBJECT + Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 BINDABLE bindableProp1) + Q_PROPERTY(int prop2 READ prop2 WRITE setProp2 NOTIFY prop2Changed BINDABLE bindableProp2) + Q_PROPERTY(int prop3 READ prop3 WRITE setProp3 NOTIFY prop3Changed BINDABLE bindableProp3) + Q_PROPERTY(int prop4 READ prop4 WRITE setProp4 NOTIFY prop4Changed BINDABLE bindableProp4) +public: + CompatPropertyTester(QObject *parent = nullptr) : QObject(parent) { } + + int prop1() {return prop1Data.value();} + void setProp1(int i) { if (i == prop1Data) return; prop1Data.setValue(i); prop1Data.notify(); } + QBindable<int> bindableProp1() {return QBindable<int>(&prop1Data);} + + int prop2() { return prop2Data.value(); } + void setProp2(int i) + { + if (i == prop2Data) + return; + prop2Data.setValue(i); + prop2Data.notify(); + } + QBindable<int> bindableProp2() { return QBindable<int>(&prop2Data); } + + int prop3() { return prop3Data.value(); } + void setProp3(int i) + { + if (i == prop3Data) + return; + prop3Data.setValue(i); + prop3Data.notify(); + } + QBindable<int> bindableProp3() { return QBindable<int>(&prop3Data); } + + int prop4() const + { + auto val = prop4Data.value(); + return val == 0 ? 42 : val; + } + + void setProp4(int i) + { + if (i == prop4Data) + return; + prop4Data.setValue(i); + prop4Data.notify(); + } + QBindable<int> bindableProp4() { return QBindable<int>(&prop4Data); } + +signals: + void prop2Changed(int value); + void prop3Changed(); + void prop4Changed(int value); + +private: + Q_OBJECT_COMPAT_PROPERTY(CompatPropertyTester, int, prop1Data, &CompatPropertyTester::setProp1) + Q_OBJECT_COMPAT_PROPERTY(CompatPropertyTester, int, prop2Data, &CompatPropertyTester::setProp2, + &CompatPropertyTester::prop2Changed) + Q_OBJECT_COMPAT_PROPERTY_WITH_ARGS(CompatPropertyTester, int, prop3Data, + &CompatPropertyTester::setProp3, + &CompatPropertyTester::prop3Changed, 1) + Q_OBJECT_COMPAT_PROPERTY_WITH_ARGS(CompatPropertyTester, int, prop4Data, + &CompatPropertyTester::setProp4, + &CompatPropertyTester::prop4Changed, + &CompatPropertyTester::prop4, 0) +}; + +void tst_QProperty::compatPropertyNoDobuleNotification() +{ + CompatPropertyTester tester; + int counter = 0; + QProperty<int> iprop {1}; + tester.bindableProp1().setBinding([&]() -> int {return iprop;}); + auto observer = tester.bindableProp1().onValueChanged([&](){++counter;}); + iprop.setValue(2); + QCOMPARE(counter, 1); +} + +void tst_QProperty::compatPropertySignals() +{ + CompatPropertyTester tester; + + // Compat property with signal. Signal has parameter. + QProperty<int> prop2Observer; + prop2Observer.setBinding(tester.bindableProp2().makeBinding()); + + QSignalSpy prop2Spy(&tester, &CompatPropertyTester::prop2Changed); + + tester.setProp2(10); + + QCOMPARE(prop2Observer.value(), 10); + QCOMPARE(prop2Spy.size(), 1); + QList<QVariant> arguments = prop2Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 10); + + // Compat property with signal and default value. Signal has no parameter. + QProperty<int> prop3Observer; + prop3Observer.setBinding(tester.bindableProp3().makeBinding()); + QCOMPARE(prop3Observer.value(), 1); + + QSignalSpy prop3Spy(&tester, &CompatPropertyTester::prop3Changed); + + tester.setProp3(5); + + QCOMPARE(prop3Observer.value(), 5); + QCOMPARE(prop3Spy.size(), 1); + + // Compat property with signal, default value, and custom setter. Signal has parameter. + QProperty<int> prop4Observer; + prop4Observer.setBinding(tester.bindableProp4().makeBinding()); + QCOMPARE(prop4Observer.value(), 42); + + QSignalSpy prop4Spy(&tester, &CompatPropertyTester::prop4Changed); + + tester.setProp4(10); + + QCOMPARE(prop4Observer.value(), 10); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 10); + + tester.setProp4(42); + + QCOMPARE(prop4Observer.value(), 42); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 42); + + tester.setProp4(0); + + QCOMPARE(prop4Observer.value(), 42); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 42); +} + +class FakeDependencyCreator : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 NOTIFY prop1Changed BINDABLE bindableProp1) + Q_PROPERTY(int prop2 READ prop2 WRITE setProp2 NOTIFY prop2Changed BINDABLE bindableProp2) + Q_PROPERTY(int prop3 READ prop3 WRITE setProp3 NOTIFY prop3Changed BINDABLE bindableProp3) + +signals: + void prop1Changed(); + void prop2Changed(); + void prop3Changed(); + +public: + void setProp1(int val) { prop1Data.setValue(val); prop1Data.notify();} + void setProp2(int val) { prop2Data.setValue(val); prop2Data.notify();} + void setProp3(int val) { prop3Data.setValue(val); prop3Data.notify();} + + int prop1() { return prop1Data; } + int prop2() { return prop2Data; } + int prop3() { return prop3Data; } + + QBindable<int> bindableProp1() { return QBindable<int>(&prop1Data); } + QBindable<int> bindableProp2() { return QBindable<int>(&prop2Data); } + QBindable<int> bindableProp3() { return QBindable<int>(&prop3Data); } + +private: + Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop1Data, &FakeDependencyCreator::setProp1, &FakeDependencyCreator::prop1Changed); + Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop2Data, &FakeDependencyCreator::setProp2, &FakeDependencyCreator::prop2Changed); + Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop3Data, &FakeDependencyCreator::setProp3, &FakeDependencyCreator::prop3Changed); +}; + +void tst_QProperty::noFakeDependencies() +{ + FakeDependencyCreator fdc; + int bindingFunctionCalled = 0; + fdc.bindableProp1().setBinding([&]() -> int {++bindingFunctionCalled; return fdc.prop2();}); + fdc.setProp2(42); + QCOMPARE(fdc.prop1(), 42); // basic binding works + + int slotCounter = 0; + QObject::connect(&fdc, &FakeDependencyCreator::prop1Changed, &fdc, [&](){ (void) fdc.prop3(); ++slotCounter;}); + fdc.setProp2(13); + QCOMPARE(slotCounter, 1); // sanity check + int old = bindingFunctionCalled; + fdc.setProp3(100); + QCOMPARE(old, bindingFunctionCalled); +} + +class PropertyAdaptorTester : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int foo READ foo WRITE setFoo NOTIFY fooChanged) + Q_PROPERTY(int foo1 READ foo WRITE setFoo) + +signals: + void dummySignal1(); + void dummySignal2(); + void dummySignal3(); + void dummySignal4(); + void dummySignal5(); + void dummySignal6(); + void dummySignal7(); + void dummySignal8(); + void fooChanged(int newFoo); + +public slots: + void fooHasChanged() { fooChangedCount++; } + +public: + int foo() const { return fooData; } + void setFoo(int i) + { + if (i != fooData) { + fooData = i; + fooChanged(fooData); + } + } + +public: + int fooData = 0; + int fooChangedCount = 0; +}; + +void tst_QProperty::propertyAdaptorBinding() +{ + QProperty<int> source { 5 }; + QProperty<int> dest1 { 99 }; + QProperty<int> dest2 { 98 }; + + // Check binding of non BINDABLE property + PropertyAdaptorTester object; + // set up a dummy connection (needed to verify that the QBindable avoids an out-of-bounds read) + QObject::connect(&object, &PropertyAdaptorTester::dummySignal1, [](){}); + QBindable<int> binding(&object, "foo"); + QObject::connect(&object, &PropertyAdaptorTester::fooChanged, &object, + &PropertyAdaptorTester::fooHasChanged); + binding.setBinding([&]() { return source + 1; }); + QCOMPARE(object.foo(), 6); + QCOMPARE(object.fooChangedCount, 1); + + struct MyBindable : QBindable<int> { + using QBindable<int>::QBindable; + QtPrivate::QPropertyAdaptorSlotObject* data() { + return static_cast<QtPrivate::QPropertyAdaptorSlotObject*>(QUntypedBindable::data); + } + } dataBinding(&object, "foo"); + QPropertyBindingDataPointer data{&dataBinding.data()->bindingData()}; + + QCOMPARE(data.observerCount(), 0); + dest1.setBinding(binding.makeBinding()); + QCOMPARE(data.observerCount(), 1); + dest2.setBinding([=]() { return binding.value() + 1; }); + binding = {}; + QCOMPARE(data.observerCount(), 2); + + // Check addNotifer + { + int local_foo = 0; + auto notifier = QBindable<int>(&object, "foo").addNotifier([&]() { local_foo++; }); + QCOMPARE(data.observerCount(), 3); + QCOMPARE(object.foo(), 6); + QCOMPARE(dest1.value(), 6); + QCOMPARE(dest2.value(), 7); + QCOMPARE(local_foo, 0); + QCOMPARE(object.fooChangedCount, 1); + + source = 7; + QCOMPARE(object.foo(), 8); + QCOMPARE(dest1.value(), 8); + QCOMPARE(dest2.value(), 9); + QCOMPARE(local_foo, 1); + QCOMPARE(object.fooChangedCount, 2); + } + + QCOMPARE(data.observerCount(), 2); + + // Check a new QBindable object can override the existing binding + QBindable<int>(&object, "foo").setValue(10); + QCOMPARE(object.foo(), 10); + QCOMPARE(dest1.value(), 10); + QCOMPARE(dest2.value(), 11); + QCOMPARE(object.fooChangedCount, 3); + source.setValue(99); + QCOMPARE(object.foo(), 10); + QCOMPARE(dest1.value(), 10); + QCOMPARE(dest2.value(), 11); + QCOMPARE(object.fooChangedCount, 3); + object.setFoo(12); + QCOMPARE(object.foo(), 12); + QCOMPARE(dest1.value(), 12); + QCOMPARE(dest2.value(), 13); + QCOMPARE(object.fooChangedCount, 4); + + // Check binding multiple notifiers + QProperty<int> source2 { 20 }; + source.setValue(21); + binding = QBindable<int>(&object, "foo"); + binding.setBinding([&]() { return source + source2; }); + QCOMPARE(object.foo(), 41); + QCOMPARE(dest1.value(), 41); + QCOMPARE(object.fooChangedCount, 5); + source.setValue(22); + QCOMPARE(object.foo(), 42); + QCOMPARE(dest1.value(), 42); + QCOMPARE(object.fooChangedCount, 6); + source2.setValue(21); + QCOMPARE(object.foo(), 43); + QCOMPARE(dest1.value(), 43); + QCOMPARE(object.fooChangedCount, 7); + + // Check update group + { + const QScopedPropertyUpdateGroup guard; + source.setValue(23); + source2.setValue(22); + QCOMPARE(object.foo(), 43); + QCOMPARE(dest1.value(), 43); + QCOMPARE(object.fooChangedCount, 7); + } + QCOMPARE(object.foo(), 45); + QCOMPARE(dest1.value(), 45); + QCOMPARE(object.fooChangedCount, 8); + + PropertyAdaptorTester object2; + PropertyAdaptorTester object3; + + // Check multiple observers + QBindable<int> binding2(&object2, "foo"); + QBindable<int> binding3(&object3, "foo"); + binding.setBinding([=]() { return binding2.value(); }); + binding3.setBinding([=]() { return binding.value(); }); + QCOMPARE(object.foo(), 0); + QCOMPARE(object2.foo(), 0); + QCOMPARE(object3.foo(), 0); + QCOMPARE(dest1.value(), 0); + object2.setFoo(1); + QCOMPARE(object.foo(), 1); + QCOMPARE(object2.foo(), 1); + QCOMPARE(object3.foo(), 1); + QCOMPARE(dest1.value(), 1); + + // Check interoperation with BINDABLE properties + MyQObject bindableObject; + bindableObject.fooData.setBinding([]() { return 5; }); + QVERIFY(bindableObject.fooData.hasBinding()); + QVERIFY(!bindableObject.barData.hasBinding()); + QVERIFY(QBindable<int>(&bindableObject, "foo").hasBinding()); + QBindable<int> bindableBar(&bindableObject, "bar"); + QVERIFY(!bindableBar.hasBinding()); + bindableBar.setBinding([]() { return 6; }); + QVERIFY(bindableBar.hasBinding()); + QVERIFY(bindableObject.barData.hasBinding()); + + // Check bad arguments +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property is not valid"); +#endif + QVERIFY(!QBindable<int>(&object, QMetaProperty{}).isValid()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property foo1 has no notify signal"); +#endif + QVERIFY(!QBindable<int>(&object, "foo1").isValid()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property foo of type int does not match requested type bool"); +#endif + QVERIFY(!QBindable<bool>(&object, "foo").isValid()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, + "QUntypedBindable: Property foo does not belong to this object"); +#endif + QObject qobj; + QVERIFY(!QBindable<int>( + &qobj, + object.metaObject()->property(object.metaObject()->indexOfProperty("foo"))) + .isValid()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: No property named fizz"); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property is not valid"); +#endif + QVERIFY(!QBindable<int>(&object, "fizz").isValid()); +} + +#if QT_CONFIG(thread) +struct ThreadSafetyTester : public QObject +{ + Q_OBJECT + +public: + ThreadSafetyTester(QObject *parent = nullptr) : QObject(parent) {} + + Q_INVOKABLE bool hasCorrectStatus() const + { + return qGetBindingStorage(this)->status({}) == QtPrivate::getBindingStatus({}); + } + + Q_INVOKABLE bool bindingTest() + { + QProperty<QString> name(u"inThread"_s); + bindableObjectName().setBinding([&]() -> QString { return name; }); + name = u"inThreadChanged"_s; + const bool nameChangedCorrectly = objectName() == name; + bindableObjectName().takeBinding(); + return nameChangedCorrectly; + } +}; + + +void tst_QProperty::threadSafety() +{ + QThread workerThread; + auto cleanup = qScopeGuard([&](){ + QMetaObject::invokeMethod(&workerThread, "quit"); + workerThread.wait(); + }); + QScopedPointer<ThreadSafetyTester> scopedObj1(new ThreadSafetyTester); + auto obj1 = scopedObj1.data(); + auto child1 = new ThreadSafetyTester(obj1); + obj1->moveToThread(&workerThread); + const auto mainThreadBindingStatus = QtPrivate::getBindingStatus({}); + QCOMPARE(qGetBindingStorage(child1)->status({}), nullptr); + workerThread.start(); + + bool correctStatus = false; + bool ok = QMetaObject::invokeMethod(obj1, "hasCorrectStatus", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, correctStatus)); + QVERIFY(ok); + QVERIFY(correctStatus); + + bool bindingWorks = false; + ok = QMetaObject::invokeMethod(obj1, "bindingTest", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, bindingWorks)); + QVERIFY(ok); + QVERIFY(bindingWorks); + + correctStatus = false; + ok = QMetaObject::invokeMethod(child1, "hasCorrectStatus", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, correctStatus)); + QVERIFY(ok); + QVERIFY(correctStatus); + + QScopedPointer scopedObj2(new ThreadSafetyTester); + auto obj2 = scopedObj2.data(); + QCOMPARE(qGetBindingStorage(obj2)->status({}), mainThreadBindingStatus); + + obj2->setObjectName("moved"); + QCOMPARE(obj2->objectName(), "moved"); + + obj2->moveToThread(&workerThread); + correctStatus = false; + ok = QMetaObject::invokeMethod(obj2, "hasCorrectStatus", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, correctStatus)); + + QVERIFY(ok); + QVERIFY(correctStatus); + // potentially unsafe, but should still work (no writes in owning thread) + QCOMPARE(obj2->objectName(), "moved"); + + + QScopedPointer scopedObj3(new ThreadSafetyTester); + auto obj3 = scopedObj3.data(); + obj3->setObjectName("moved"); + QCOMPARE(obj3->objectName(), "moved"); + obj3->moveToThread(nullptr); + QCOMPARE(obj2->objectName(), "moved"); + obj3->setObjectName("moved again"); + QCOMPARE(obj3->objectName(), "moved again"); +} + +class QPropertyUsingThread : public QThread +{ +public: + QPropertyUsingThread(QObject **dest, QThread *destThread) : dest(dest), destThread(destThread) {} + void run() override + { + scopedObj1.reset(new ThreadSafetyTester()); + scopedObj1->setObjectName("test"); + QObject *child = new ThreadSafetyTester(scopedObj1.get()); + child->setObjectName("child"); + exec(); + scopedObj1->moveToThread(destThread); + *dest = scopedObj1.release(); + } + std::unique_ptr<ThreadSafetyTester> scopedObj1; + QObject **dest; + QThread *destThread; +}; + +void tst_QProperty::threadSafety2() +{ + std::unique_ptr<QObject> movedObj; + { + QObject *tmp = nullptr; + QPropertyUsingThread workerThread(&tmp, QThread::currentThread()); + workerThread.start(); + workerThread.quit(); + workerThread.wait(); + movedObj.reset(tmp); + } + + QCOMPARE(movedObj->objectName(), "test"); + QCOMPARE(movedObj->children().first()->objectName(), "child"); +} +#endif // QT_CONFIG(thread) + +struct CustomType +{ + CustomType() = default; + CustomType(int val) : value(val) { } + CustomType(int val, int otherVal) : value(val), anotherValue(otherVal) { } + CustomType(const CustomType &) = default; + CustomType(CustomType &&) = default; + ~CustomType() = default; + CustomType &operator=(const CustomType &) = default; + CustomType &operator=(CustomType &&) = default; + bool operator==(const CustomType &other) const + { + return (value == other.value) && (anotherValue == other.anotherValue); + } + + int value = 0; + int anotherValue = 0; +}; + +class PropertyWithInitializationTester : public QObject +{ + Q_OBJECT + Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 NOTIFY prop1Changed BINDABLE bindableProp1) + Q_PROPERTY(CustomType prop2 READ prop2 WRITE setProp2 BINDABLE bindableProp2) + Q_PROPERTY(CustomType prop3 READ prop3 WRITE setProp3 BINDABLE bindableProp3) +signals: + void prop1Changed(); + +public: + PropertyWithInitializationTester(QObject *parent = nullptr) : QObject(parent) { } + + int prop1() { return prop1Data.value(); } + void setProp1(int i) { prop1Data = i; } + QBindable<int> bindableProp1() { return QBindable<int>(&prop1Data); } + + CustomType prop2() { return prop2Data.value(); } + void setProp2(CustomType val) { prop2Data = val; } + QBindable<CustomType> bindableProp2() { return QBindable<CustomType>(&prop2Data); } + + CustomType prop3() { return prop3Data.value(); } + void setProp3(CustomType val) { prop3Data.setValue(val); } + QBindable<CustomType> bindableProp3() { return QBindable<CustomType>(&prop3Data); } + + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(PropertyWithInitializationTester, int, prop1Data, 5, + &PropertyWithInitializationTester::prop1Changed) + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(PropertyWithInitializationTester, CustomType, prop2Data, + CustomType(5)) + Q_OBJECT_COMPAT_PROPERTY_WITH_ARGS(PropertyWithInitializationTester, CustomType, prop3Data, + &PropertyWithInitializationTester::setProp3, + CustomType(10, 20)) +}; + +void tst_QProperty::bindablePropertyWithInitialization() +{ + PropertyWithInitializationTester tester; + + QCOMPARE(tester.prop1(), 5); + QCOMPARE(tester.prop2().value, 5); + QCOMPARE(tester.prop3().value, 10); + QCOMPARE(tester.prop3().anotherValue, 20); +} + +void tst_QProperty::noDoubleNotification() +{ + /* dependency graph for this test + x --> y means y depends on x + a-->b-->d + \ ^ + \->c--/ + */ + QProperty<int> a(0); + QProperty<int> b; + b.setBinding([&](){ return a.value(); }); + QProperty<int> c; + c.setBinding([&](){ return a.value(); }); + QProperty<int> d; + d.setBinding([&](){ return b.value() + c.value(); }); + int nNotifications = 0; + int expected = 0; + auto connection = d.subscribe([&](){ + ++nNotifications; + QCOMPARE(d.value(), expected); + }); + QCOMPARE(nNotifications, 1); + expected = 2; + a = 1; + QCOMPARE(nNotifications, 2); + expected = 4; + a = 2; + QCOMPARE(nNotifications, 3); +} + +void tst_QProperty::groupedNotifications() +{ + QProperty<int> a(0); + QProperty<int> b; + b.setBinding([&](){ return a.value(); }); + QProperty<int> c; + c.setBinding([&](){ return a.value(); }); + QProperty<int> d; + QProperty<int> e; + e.setBinding([&](){ return b.value() + c.value() + d.value(); }); + int nNotifications = 0; + int expected = 0; + auto connection = e.subscribe([&](){ + ++nNotifications; + QCOMPARE(e.value(), expected); + }); + QCOMPARE(nNotifications, 1); + + expected = 2; + { + const QScopedPropertyUpdateGroup guard; + a = 1; + QCOMPARE(b.value(), 0); + QCOMPARE(c.value(), 0); + QCOMPARE(d.value(), 0); + QCOMPARE(nNotifications, 1); + } + QCOMPARE(b.value(), 1); + QCOMPARE(c.value(), 1); + QCOMPARE(e.value(), 2); + QCOMPARE(nNotifications, 2); + + expected = 7; + { + const QScopedPropertyUpdateGroup guard; + a = 2; + d = 3; + QCOMPARE(b.value(), 1); + QCOMPARE(c.value(), 1); + QCOMPARE(d.value(), 3); + QCOMPARE(nNotifications, 2); + } + QCOMPARE(b.value(), 2); + QCOMPARE(c.value(), 2); + QCOMPARE(e.value(), 7); + QCOMPARE(nNotifications, 3); + + +} + +void tst_QProperty::groupedNotificationConsistency() +{ + QProperty<int> i(0); + QProperty<int> j(0); + bool areEqual = true; + + auto observer = i.onValueChanged([&](){ + areEqual = i == j; + }); + + i = 1; + j = 1; + QVERIFY(!areEqual); // value changed runs before j = 1 + + { + const QScopedPropertyUpdateGroup guard; + i = 2; + j = 2; + } + QVERIFY(areEqual); // value changed runs after everything has been evaluated +} + +void tst_QProperty::bindingGroupMovingBindingData() +{ + auto tester = std::make_unique<ClassWithNotifiedProperty>(); + auto testerPriv = QObjectPrivate::get(tester.get()); + + auto dummyNotifier = tester->property.addNotifier([](){}); + auto bindingData = testerPriv->bindingStorage.bindingData(&tester->property); + QVERIFY(bindingData); // we have a notifier, so there should be binding data + + Qt::beginPropertyUpdateGroup(); + auto cleanup = qScopeGuard([](){ Qt::endPropertyUpdateGroup(); }); + tester->property = 42; + QCOMPARE(testerPriv->bindingStorage.bindingData(&tester->property), bindingData); + auto proxyData = QPropertyBindingDataPointer::proxyData(bindingData); + // as we've modified the property, we now should have a proxy for the delayed notification + QVERIFY(proxyData); + // trigger binding data reallocation + std::array<QUntypedPropertyData, 10> propertyDataArray; + for (auto&& data: propertyDataArray) + testerPriv->bindingStorage.bindingData(&data, true); + // binding data has moved + QVERIFY(testerPriv->bindingStorage.bindingData(&tester->property) != bindingData); + bindingData = testerPriv->bindingStorage.bindingData(&tester->property); + // the proxy data has been updated + QCOMPARE(proxyData->originalBindingData, bindingData); + + tester.reset(); + // the property data is gone, proxyData should have been informed + QCOMPARE(proxyData->originalBindingData, nullptr); + QVERIFY(proxyData); +} + +void tst_QProperty::bindingGroupBindingDeleted() +{ + auto deleter = std::make_unique<ClassWithNotifiedProperty>(); + auto toBeDeleted = std::make_unique<ClassWithNotifiedProperty>(); + + bool calledHandler = false; + deleter->property.setBinding([&](){ + int newValue = toBeDeleted->property; + if (newValue == 42) + toBeDeleted.reset(); + return newValue; + }); + auto handler = toBeDeleted->property.onValueChanged([&]() { calledHandler = true; } ); + { + Qt::beginPropertyUpdateGroup(); + auto cleanup = qScopeGuard([](){ Qt::endPropertyUpdateGroup(); }); + QVERIFY(toBeDeleted); + toBeDeleted->property = 42; + // ASAN should not complain here + } + QVERIFY(!toBeDeleted); + // the change notification is sent, even if the binding is deleted during evaluation + QVERIFY(calledHandler); +} + +void tst_QProperty::uninstalledBindingDoesNotEvaluate() +{ + QProperty<int> i; + QProperty<int> j; + int bindingEvaluationCounter = 0; + i.setBinding([&](){ + bindingEvaluationCounter++; + return j.value(); + }); + QCOMPARE(bindingEvaluationCounter, 1); + // Sanity check: if we force a binding reevaluation, + j = 42; + // the binding function will be called again. + QCOMPARE(bindingEvaluationCounter, 2); + // If we keep referencing the binding + auto keptBinding = i.binding(); + // but have it not installed on a property + i = 10; + QVERIFY(!i.hasBinding()); + QVERIFY(!keptBinding.isNull()); + // then changing a dependency + j = 12; + // does not lead to the binding being reevaluated. + QCOMPARE(bindingEvaluationCounter, 2); +} + +void tst_QProperty::notify() +{ + QProperty<int> testProperty(0); + QList<int> recordedValues; + int value = 0; + QPropertyNotifier notifier; + + { + QPropertyNotifier handler = testProperty.addNotifier([&]() { + recordedValues << testProperty; + }); + notifier = testProperty.addNotifier([&]() { + value = testProperty; + }); + + testProperty = 1; + testProperty = 2; + } + QCOMPARE(value, 2); + testProperty = 3; + QCOMPARE(value, 3); + notifier = {}; + testProperty = 4; + QCOMPARE(value, 3); + + QCOMPARE(recordedValues.size(), 2); + QCOMPARE(recordedValues.at(0), 1); + QCOMPARE(recordedValues.at(1), 2); +} + +void tst_QProperty::bindableInterfaceOfCompatPropertyUsesSetter() +{ + MyQObject obj; + QBindable<int> bindable = obj.bindableCompat(); + QCOMPARE(obj.setCompatCalled, 0); + bindable.setValue(42); + QCOMPARE(obj.setCompatCalled, 1); +} + +void tst_QProperty::selfBindingShouldNotCrash() +{ + QProperty<int> i; + i.setBinding([&](){ return i+1; }); + QVERIFY(i.binding().error().hasError()); +} + +void tst_QProperty::qpropertyAlias() +{ +#if QT_DEPRECATED_SINCE(6, 6) + QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED + std::unique_ptr<QProperty<int>> i {new QProperty<int>}; + QPropertyAlias<int> alias(i.get()); + QVERIFY(alias.isValid()); + alias.setValue(42); + QCOMPARE(i->value(), 42); + QProperty<int> j; + bool notifierCalled = false; + auto myNotifier = alias.addNotifier([&](){notifierCalled = true;}); + i->setBinding([&]() -> int { return j; }); + QVERIFY(notifierCalled); + j.setValue(42); + QCOMPARE(alias.value(), 42); + i.reset(); + QVERIFY(!alias.isValid()); + QT_WARNING_POP +#endif +} + +void tst_QProperty::scheduleNotify() +{ + int notifications = 0; + QProperty<int> p; + QCOMPARE(p.value(), 0); + const auto handler = p.addNotifier([&](){ ++notifications; }); + QCOMPARE(notifications, 0); + QPropertyBinding<int> b([]() { return 0; }, QPropertyBindingSourceLocation()); + QPropertyBindingPrivate::get(b)->scheduleNotify(); + QCOMPARE(notifications, 0); + p.setBinding(b); + QCOMPARE(notifications, 1); + QCOMPARE(p.value(), 0); +} + +void tst_QProperty::notifyAfterAllDepsGone() +{ + bool b = true; + QProperty<int> iprop; + QProperty<int> jprop(42); + iprop.setBinding([&](){ + if (b) + return jprop.value(); + return 13; + }); + int changeCounter = 0; + auto keepAlive = iprop.onValueChanged([&](){ changeCounter++; }); + QCOMPARE(iprop.value(), 42); + jprop = 44; + QCOMPARE(iprop.value(), 44); + QCOMPARE(changeCounter, 1); + b = false; + jprop = 43; + QCOMPARE(iprop.value(), 13); + QCOMPARE(changeCounter, 2); +} + +class TestObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(int signaled READ signaled WRITE setSignaled NOTIFY signaledChanged FINAL) + Q_PROPERTY(int bindable1 READ bindable1 WRITE setBindable1 BINDABLE bindable1Bindable NOTIFY bindable1Changed FINAL) + Q_PROPERTY(int bindable2 READ bindable2 WRITE setBindable2 BINDABLE bindable2Bindable NOTIFY bindable2Changed FINAL) + +public: + int signaled() const + { + return m_signaled; + } + + void setSignaled(int newSignaled) + { + if (m_signaled == newSignaled) + return; + m_signaled = newSignaled; + emit signaledChanged(); + } + + int bindable1() const + { + return m_bindable1; + } + + void setBindable1(int newBindable1) + { + if (m_bindable1 == newBindable1) + return; + m_bindable1 = newBindable1; + emit bindable1Changed(); + } + + QBindable<int> bindable1Bindable() + { + return QBindable<int>(&m_bindable1); + } + + int bindable2() const + { + return m_bindable2; + } + + void setBindable2(int newBindable2) + { + if (m_bindable2 == newBindable2) + return; + m_bindable2 = newBindable2; + emit bindable2Changed(); + } + + QBindable<int> bindable2Bindable() + { + return QBindable<int>(&m_bindable2); + } + +signals: + void signaledChanged(); + void bindable1Changed(); + void bindable2Changed(); + +private: + int m_signaled = 0; + Q_OBJECT_COMPAT_PROPERTY(TestObject, int, m_bindable1, &TestObject::setBindable1, &TestObject::bindable1Changed); + Q_OBJECT_COMPAT_PROPERTY(TestObject, int, m_bindable2, &TestObject::setBindable2, &TestObject::bindable2Changed); +}; + +void tst_QProperty::propertyUpdateViaSignaledProperty() +{ + TestObject o; + QProperty<int> rootTrigger; + QProperty<int> signalTrigger; + + o.bindable1Bindable().setBinding([&]() { + return rootTrigger.value(); + }); + + QObject::connect(&o, &TestObject::bindable1Changed, &o, [&]() { + // Signaled changes only once, doesn't actually depend on bindable1. + // In reality, there could be some complicated calculation behind this that changes + // on certain checkpoints, but not on every iteration. + o.setSignaled(40); + }); + + o.bindable2Bindable().setBinding([&]() { + return signalTrigger.value() - o.bindable1(); + }); + + QObject::connect(&o, &TestObject::signaledChanged, &o, [&]() { + signalTrigger.setValue(o.signaled()); + }); + + rootTrigger.setValue(2); + QCOMPARE(o.bindable1(), 2); + QCOMPARE(o.bindable2(), 38); + rootTrigger.setValue(3); + QCOMPARE(o.bindable1(), 3); + QCOMPARE(o.bindable2(), 37); + rootTrigger.setValue(4); + QCOMPARE(o.bindable1(), 4); + QCOMPARE(o.bindable2(), 36); +} + +void tst_QProperty::derefFromObserver() +{ + int triggered = 0; + QProperty<int> source(11); + + DtorCounter::counter = 0; + DtorCounter dc; + + QProperty<int> target([&triggered, &source, dc]() mutable { + dc.shouldIncrement = true; + return ++triggered + source.value(); + }); + QCOMPARE(triggered, 1); + + { + auto propObserver = std::make_unique<QPropertyObserver>(); + QPropertyObserverPointer propObserverPtr { propObserver.get() }; + propObserverPtr.setBindingToNotify(QPropertyBindingPrivate::get(target.binding())); + + QBindingObserverPtr bindingPtr(propObserver.get()); + + QCOMPARE(triggered, 1); + source = 25; + QCOMPARE(triggered, 2); + QCOMPARE(target, 27); + + target.setBinding([]() { return 8; }); + QCOMPARE(target, 8); + + // The QBindingObserverPtr still holds on to the binding. + QCOMPARE(dc.counter, 0); + } + + // The binding is actually gone now. + QCOMPARE(dc.counter, 1); + + source = 26; + QCOMPARE(triggered, 2); + QCOMPARE(target, 8); +} + QTEST_MAIN(tst_QProperty); +#undef QT_SOURCE_LOCATION_NAMESPACE + #include "tst_qproperty.moc" |