// Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #if __has_include() && __cplusplus >= 202002L && !defined(Q_QDOC) #include #define QT_SOURCE_LOCATION_NAMESPACE std #elif __has_include() && !defined(Q_QDOC) #include #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(); void subscribe(); void changeHandlerThroughBindings(); void dontTriggerDependenciesIfUnchangedValue(); void bindingSourceLocation(); void bindingError(); void bindingLoop(); void realloc(); void changePropertyFromWithinChangeHandler(); void changePropertyFromWithinChangeHandlerThroughDependency(); void changePropertyFromWithinChangeHandler2(); void settingPropertyValueDoesRemoveBinding(); void genericPropertyBinding(); void genericPropertyBindingBool(); void setBindingFunctor(); void multipleObservers(); 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 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 constexpr auto isDerivedFromQUntypedPropertyData = std::is_base_of_v; template constexpr auto isDerivedFromQUntypedPropertyDataFunc(const Property &property) { Q_UNUSED(property); return isDerivedFromQUntypedPropertyData; } template constexpr auto isDerivedFromQUntypedPropertyDataFunc(Property *property) { Q_UNUSED(property); return isDerivedFromQUntypedPropertyData; } } // namespace void tst_QProperty::inheritQUntypedPropertyData() { class propertyPublic : public QUntypedPropertyData { }; class propertyPrivate : private QUntypedPropertyData { }; // Compile time test static_assert(isDerivedFromQUntypedPropertyData); static_assert(isDerivedFromQUntypedPropertyData); static_assert(isDerivedFromQUntypedPropertyData>); static_assert(isDerivedFromQUntypedPropertyData>); // Run time test propertyPublic _propertyPublic; propertyPrivate _propertyPrivate; QPropertyData qpropertyData; QProperty qproperty; std::unique_ptr _propertyPublicPtr{ new propertyPublic }; std::unique_ptr _propertyPrivatePtr{ new propertyPrivate }; std::unique_ptr> qpropertyDataPtr{ new QPropertyData }; std::unique_ptr> qpropertyPtr{ new QProperty }; 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 property([]() { return 42; }); QCOMPARE(property.value(), int(42)); property.setBinding([]() { return 100; }); QCOMPARE(property.value(), int(100)); property.setBinding([]() { return 50; }); QCOMPARE(property.value(), int(50)); } void tst_QProperty::basicDependencies() { QProperty right(100); QProperty left(Qt::makePropertyBinding(right)); QCOMPARE(left.value(), int(100)); right = 42; QCOMPARE(left.value(), int(42)); } void tst_QProperty::multipleDependencies() { QProperty firstDependency(1); QProperty secondDependency(2); QProperty sum; sum.setBinding([&]() { return firstDependency + secondDependency; }); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); QCOMPARE(sum.value(), int(3)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); firstDependency = 10; QCOMPARE(sum.value(), int(12)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); secondDependency = 20; QCOMPARE(sum.value(), int(30)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); firstDependency = 1; secondDependency = 1; QCOMPARE(sum.value(), int(2)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); } void tst_QProperty::bindingWithDeletedDependency() { QScopedPointer> dynamicProperty(new QProperty(100)); QProperty staticProperty(1000); QProperty bindingReturnsDynamicProperty(false); QProperty propertySelector([&]() { if (bindingReturnsDynamicProperty && !dynamicProperty.isNull()) return dynamicProperty->value(); else return staticProperty.value(); }); QCOMPARE(propertySelector.value(), staticProperty.value()); bindingReturnsDynamicProperty = true; QCOMPARE(propertySelector.value(), dynamicProperty->value()); dynamicProperty.reset(); QCOMPARE(propertySelector.value(), 100); bindingReturnsDynamicProperty = false; 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 bindableProp() { return &m_prop; } private: Q_OBJECT_COMPAT_PROPERTY(ChangeDuringDtorTester, int, m_prop, &ChangeDuringDtorTester::setProp) }; void tst_QProperty::dependencyChangeDuringDestruction() { auto tester = std::make_unique(); QProperty 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 first(1); QProperty second; second.setBinding(Qt::makePropertyBinding(first)); QProperty third; third.setBinding(Qt::makePropertyBinding(second)); QCOMPARE(third.value(), int(1)); first = 2; QCOMPARE(third.value(), int(2)); } void tst_QProperty::bindingAfterUse() { QProperty propWithBindingLater(1); QProperty propThatUsesFirstProp; propThatUsesFirstProp.setBinding(Qt::makePropertyBinding(propWithBindingLater)); QCOMPARE(propThatUsesFirstProp.value(), int(1)); QCOMPARE(QPropertyBindingDataPointer::get(propWithBindingLater).observerCount(), 1); QProperty injectedValue(42); propWithBindingLater.setBinding(Qt::makePropertyBinding(injectedValue)); QCOMPARE(propThatUsesFirstProp.value(), int(42)); QCOMPARE(QPropertyBindingDataPointer::get(propWithBindingLater).observerCount(), 1); } void tst_QProperty::bindingFunctionDtorCalled() { DtorCounter::counter = 0; DtorCounter dc; { QProperty prop; prop.setBinding([dc]() mutable { dc.shouldIncrement = true; return 42; }); QCOMPARE(prop.value(), 42); } QCOMPARE(DtorCounter::counter, 1); } void tst_QProperty::switchBinding() { QProperty first(1); QProperty propWithChangingBinding; propWithChangingBinding.setBinding(Qt::makePropertyBinding(first)); QCOMPARE(propWithChangingBinding.value(), 1); QProperty output; output.setBinding(Qt::makePropertyBinding(propWithChangingBinding)); QCOMPARE(output.value(), 1); QProperty second(2); propWithChangingBinding.setBinding(Qt::makePropertyBinding(second)); QCOMPARE(output.value(), 2); } void tst_QProperty::avoidDependencyAllocationAfterFirstEval() { QProperty firstDependency(1); QProperty secondDependency(10); QProperty propWithBinding([&]() { return firstDependency + secondDependency; }); QCOMPARE(propWithBinding.value(), int(11)); QVERIFY(QPropertyBindingDataPointer::get(propWithBinding).binding()); QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); firstDependency = 100; QCOMPARE(propWithBinding.value(), int(110)); QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); } void tst_QProperty::boolProperty() { QProperty first(true); QProperty second(false); QProperty all([&]() { return first && second; }); QCOMPARE(all.value(), false); second = true; QCOMPARE(all.value(), true); } void tst_QProperty::takeBinding() { QPropertyBinding existingBinding; QVERIFY(existingBinding.isNull()); QProperty first(100); QProperty second(Qt::makePropertyBinding(first)); QCOMPARE(second.value(), int(100)); existingBinding = second.takeBinding(); QVERIFY(!existingBinding.isNull()); first = 10; QCOMPARE(second.value(), int(100)); second = 25; QCOMPARE(second.value(), int(25)); second.setBinding(existingBinding); QCOMPARE(second.value(), int(10)); QVERIFY(!existingBinding.isNull()); } void tst_QProperty::stickyBinding() { QProperty prop; QProperty 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 first(100); QProperty second(Qt::makePropertyBinding(first)); QCOMPARE(second.value(), 100); auto constantBinding = Qt::makePropertyBinding([]() { return 42; }); auto oldBinding = second.setBinding(constantBinding); QCOMPARE(second.value(), 42); second.setBinding(oldBinding); QCOMPARE(second.value(), 100); } void tst_QProperty::changeHandler() { QProperty testProperty(0); QList recordedValues; { auto handler = testProperty.onValueChanged([&]() { recordedValues << testProperty; }); testProperty = 1; testProperty = 2; } testProperty = 3; QCOMPARE(recordedValues.size(), 2); QCOMPARE(recordedValues.at(0), 1); QCOMPARE(recordedValues.at(1), 2); } void tst_QProperty::propertyChangeHandlerApi() { int changeHandlerCallCount = 0; QPropertyChangeHandler handler([&changeHandlerCallCount]() { ++changeHandlerCallCount; }); QProperty source1; QProperty source2; handler.setSource(source1); source1 = 100; QCOMPARE(changeHandlerCallCount, 1); handler.setSource(source2); source1 = 101; QCOMPARE(changeHandlerCallCount, 1); source2 = 200; QCOMPARE(changeHandlerCallCount, 2); } void tst_QProperty::subscribe() { QProperty testProperty(42); QList recordedValues; { auto handler = testProperty.subscribe([&]() { recordedValues << testProperty; }); testProperty = 1; testProperty = 2; } testProperty = 3; QCOMPARE(recordedValues.size(), 3); QCOMPARE(recordedValues.at(0), 42); QCOMPARE(recordedValues.at(1), 1); QCOMPARE(recordedValues.at(2), 2); } void tst_QProperty::changeHandlerThroughBindings() { QProperty trigger(false); QProperty blockTrigger(false); QProperty condition([&]() { bool triggerValue = trigger; bool blockTriggerValue = blockTrigger; return triggerValue && !blockTriggerValue; }); bool changeHandlerCalled = false; auto handler = condition.onValueChanged([&]() { changeHandlerCalled = true; }); QVERIFY(!condition); QVERIFY(!changeHandlerCalled); trigger = true; QVERIFY(condition); QVERIFY(changeHandlerCalled); changeHandlerCalled = false; trigger = false; QVERIFY(!condition); QVERIFY(changeHandlerCalled); changeHandlerCalled = false; blockTrigger = true; QVERIFY(!condition); QVERIFY(!changeHandlerCalled); } void tst_QProperty::dontTriggerDependenciesIfUnchangedValue() { QProperty property(42); bool triggered = false; QProperty observer([&]() { triggered = true; return property.value(); }); QCOMPARE(observer.value(), 42); QVERIFY(triggered); triggered = false; property = 42; QCOMPARE(observer.value(), 42); QVERIFY(!triggered); } void tst_QProperty::bindingSourceLocation() { #if defined(QT_PROPERTY_COLLECT_BINDING_LOCATION) 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 QSKIP("Skipping this in the light of missing binding source location support"); #endif } void tst_QProperty::bindingError() { QProperty prop([]() -> int { QPropertyBindingError error(QPropertyBindingError::UnknownError, QLatin1String("my error")); QPropertyBindingPrivate::currentlyEvaluatingBinding()->setError(std::move(error)); return 0; }); QCOMPARE(prop.value(), 0); QCOMPARE(prop.binding().error().description(), QString("my error")); } class BindingLoopTester : public QObject { Q_OBJECT Q_PROPERTY(int eagerProp READ eagerProp WRITE setEagerProp BINDABLE bindableEagerProp) Q_PROPERTY(int eagerProp2 READ eagerProp2 WRITE setEagerProp2 BINDABLE bindableEagerProp2) public: BindingLoopTester(QProperty *i, QObject *parent = nullptr) : QObject(parent) { eagerData.setBinding(Qt::makePropertyBinding([&](){ return eagerData2.value() + i->value(); } ) ); eagerData2.setBinding(Qt::makePropertyBinding([&](){ return eagerData.value() + 1; } ) ); i->setValue(42); } BindingLoopTester() {} int eagerProp() {return eagerData.value();} void setEagerProp(int i) { eagerData.setValue(i); eagerData.notify(); } QBindable bindableEagerProp() {return QBindable(&eagerData);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData, &BindingLoopTester::setEagerProp) int eagerProp2() {return eagerData2.value();} void setEagerProp2(int i) { eagerData2.setValue(i); eagerData2.notify(); } QBindable bindableEagerProp2() {return QBindable(&eagerData2);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData2, &BindingLoopTester::setEagerProp2) }; void tst_QProperty::bindingLoop() { QProperty firstProp; QProperty secondProp([&]() -> int { return firstProp.value(); }); QProperty thirdProp([&]() -> int { return secondProp.value(); }); firstProp.setBinding([&]() -> int { return secondProp.value() + thirdProp.value(); }); thirdProp.setValue(10); QCOMPARE(firstProp.binding().error().type(), QPropertyBindingError::BindingLoop); { QProperty 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 { /* * This class and the realloc test rely on the fact that the internal property hashmap has an * initial capacity of 8 and a load factor of 0.5. Thus, it is necessary to cause actions which * allocate 5 different QPropertyBindingData * */ Q_OBJECT Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 BINDABLE bindableProp1) Q_PROPERTY(int prop2 READ prop2 WRITE setProp2 BINDABLE bindableProp2) Q_PROPERTY(int prop3 READ prop3 WRITE setProp3 BINDABLE bindableProp3) Q_PROPERTY(int prop4 READ prop4 WRITE setProp4 BINDABLE bindableProp4) Q_PROPERTY(int prop5 READ prop5 WRITE setProp5 BINDABLE bindableProp5) public: ReallocTester(QObject *parent = nullptr) : QObject(parent) {} #define GEN(N) \ int prop##N() {return propData##N.value();} \ void setProp##N(int i) { if (i == propData##N) return; propData##N.setValue(i); propData##N.notify(); } \ QBindable bindableProp##N() {return QBindable(&propData##N);} \ Q_OBJECT_COMPAT_PROPERTY(ReallocTester, int, propData##N, &ReallocTester::setProp##N) GEN(1) GEN(2) GEN(3) GEN(4) GEN(5) #undef GEN }; void tst_QProperty::realloc() { { // Triggering a reallocation does not crash ReallocTester tester; tester.bindableProp1().setBinding([&](){return tester.prop5();}); 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(tester.prop1(), 42); } { // After a reallocation, property observers still work ReallocTester tester; int modificationCount = 0; QPropertyChangeHandler observer {[&](){ ++modificationCount; }}; tester.bindableProp1().observe(&observer); tester.setProp1(12); QCOMPARE(modificationCount, 1); 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, 3); } }; void tst_QProperty::changePropertyFromWithinChangeHandler() { QProperty property(100); bool resetPropertyOnChange = false; int changeHandlerCallCount = 0; auto handler = property.onValueChanged([&]() { ++changeHandlerCallCount; if (resetPropertyOnChange) property = 100; }); QCOMPARE(property.value(), 100); resetPropertyOnChange = true; property = 42; QCOMPARE(property.value(), 100); // changing the property value inside the change handler won't result in the change // handler being called again. QCOMPARE(changeHandlerCallCount, 1); changeHandlerCallCount = 0; } void tst_QProperty::changePropertyFromWithinChangeHandlerThroughDependency() { QProperty sourceProperty(100); QProperty property(Qt::makePropertyBinding(sourceProperty)); bool resetPropertyOnChange = false; int changeHandlerCallCount = 0; auto handler = property.onValueChanged([&]() { ++changeHandlerCallCount; if (resetPropertyOnChange) sourceProperty = 100; }); QCOMPARE(property.value(), 100); resetPropertyOnChange = true; sourceProperty = 42; 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); changeHandlerCallCount = 0; } void tst_QProperty::changePropertyFromWithinChangeHandler2() { QProperty property(100); int changeHandlerCallCount = 0; auto handler = property.onValueChanged([&]() { ++changeHandlerCallCount; property = property.value() + 1; }); QCOMPARE(property.value(), 100); property = 42; QCOMPARE(property.value(), 43); QVERIFY(!property.hasBinding()); // setting the value in the change handler removed the binding } void tst_QProperty::settingPropertyValueDoesRemoveBinding() { QProperty source(42); QProperty property(Qt::makePropertyBinding(source)); QCOMPARE(property.value(), 42); QVERIFY(!property.binding().isNull()); property = 100; QCOMPARE(property.value(), 100); QVERIFY(property.binding().isNull()); source = 1; QCOMPARE(property.value(), 100); QVERIFY(property.binding().isNull()); } void tst_QProperty::genericPropertyBinding() { QProperty property; { QUntypedPropertyBinding doubleBinding(QMetaType::fromType(), [](QMetaType , void *) -> bool { Q_ASSERT(false); return true; }, QPropertyBindingSourceLocation()); QVERIFY(!property.setBinding(doubleBinding)); } QUntypedPropertyBinding intBinding(QMetaType::fromType(), [](QMetaType metaType, void *dataPtr) -> bool { Q_ASSERT(metaType.id() == qMetaTypeId()); int *intPtr = reinterpret_cast(dataPtr); *intPtr = 100; return true; }, QPropertyBindingSourceLocation()); QVERIFY(property.setBinding(intBinding)); QCOMPARE(property.value(), 100); } void tst_QProperty::genericPropertyBindingBool() { QProperty property; QVERIFY(!property.value()); QUntypedPropertyBinding boolBinding(QMetaType::fromType(), [](QMetaType, void *dataPtr) -> bool { auto boolPtr = reinterpret_cast(dataPtr); *boolPtr = true; return true; }, QPropertyBindingSourceLocation()); QVERIFY(property.setBinding(boolBinding)); QVERIFY(property.value()); } void tst_QProperty::setBindingFunctor() { QProperty property; QProperty injectedValue(100); // Make sure that this picks the setBinding overload that takes a functor and // moves it correctly. property.setBinding([&injectedValue]() { return injectedValue.value(); }); injectedValue = 200; QCOMPARE(property.value(), 200); } void tst_QProperty::multipleObservers() { QProperty property; property.setValue(5); QCOMPARE(property.value(), 5); int value1 = 1; auto changeHandler = property.onValueChanged([&]() { value1 = property.value(); }); QCOMPARE(value1, 1); int value2 = 2; auto subscribeHandler = property.subscribe([&]() { value2 = property.value(); }); QCOMPARE(value2, 5); property.setValue(6); QCOMPARE(property.value(), 6); QCOMPARE(value1, 6); QCOMPARE(value2, 6); property.setBinding([]() { return 12; }); QCOMPARE(value1, 12); QCOMPARE(value2, 12); QCOMPARE(property.value(), 12); property.setBinding(QPropertyBinding()); QCOMPARE(value1, 12); QCOMPARE(value2, 12); QCOMPARE(property.value(), 12); property.setValue(22); QCOMPARE(value1, 22); QCOMPARE(value2, 22); QCOMPARE(property.value(), 22); } void tst_QProperty::arrowAndStarOperator() { QString str("Hello"); QProperty prop(&str); QCOMPARE(prop->size(), str.size()); QCOMPARE(**prop, str); struct Dereferenceable { QString x; QString *operator->() { return &x; } const QString *operator->() const { return &x; } }; static_assert(QTypeTraits::is_dereferenceable_v); QProperty prop2(Dereferenceable{str}); QCOMPARE(prop2->size(), str.size()); QCOMPARE(**prop, str); QObject *object = new QObject; object->setObjectName("Hello"); QProperty> prop3(QSharedPointer{object}); QCOMPARE(prop3->objectName(), str); QCOMPARE(*prop3, object); } struct ClassWithNotifiedProperty : public QObject { QList recordedValues; void callback() { recordedValues << property.value(); } int getProp() { return 0; } Q_OBJECT_BINDABLE_PROPERTY(ClassWithNotifiedProperty, int, property, &ClassWithNotifiedProperty::callback); }; void tst_QProperty::notifiedProperty() { ClassWithNotifiedProperty instance; std::array, 5> otherProperties = { QProperty([&]() { return instance.property + 1; }), QProperty([&]() { return instance.property + 2; }), QProperty([&]() { return instance.property + 3; }), QProperty([&]() { return instance.property + 4; }), QProperty([&]() { return instance.property + 5; }), }; auto check = [&] { const int val = instance.property.value(); for (int i = 0; i < int(otherProperties.size()); ++i) QCOMPARE(otherProperties[i].value(), val + i + 1); }; QVERIFY(instance.recordedValues.isEmpty()); check(); instance.property.setValue(42); QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 42); instance.recordedValues.clear(); check(); instance.property.setValue(42); QVERIFY(instance.recordedValues.isEmpty()); check(); int subscribedCount = 0; QProperty injectedValue(100); instance.property.setBinding([&injectedValue]() { return injectedValue.value(); }); auto subscriber = [&] { ++subscribedCount; }; std::array, 10> subscribers = { instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber), instance.property.subscribe(subscriber) }; QCOMPARE(subscribedCount, 10); subscribedCount = 0; QCOMPARE(instance.property.value(), 100); QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 100); instance.recordedValues.clear(); check(); QCOMPARE(subscribedCount, 0); injectedValue = 200; QCOMPARE(instance.property.value(), 200); QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 200); instance.recordedValues.clear(); check(); QCOMPARE(subscribedCount, 10); subscribedCount = 0; injectedValue = 400; QCOMPARE(instance.property.value(), 400); QCOMPARE(instance.recordedValues.size(), 1); QCOMPARE(instance.recordedValues.at(0), 400); instance.recordedValues.clear(); check(); QCOMPARE(subscribedCount, 10); } void tst_QProperty::typeNoOperatorEqual() { struct Uncomparable { int data = -1; bool changedCalled = false; Uncomparable(int value = 0) : data(value) {} Uncomparable(const Uncomparable &other) { data = other.data; changedCalled = false; } Uncomparable(Uncomparable &&other) { data = other.data; changedCalled = false; other.data = -1; other.changedCalled = false; } Uncomparable &operator=(const Uncomparable &other) { data = other.data; return *this; } Uncomparable &operator=(Uncomparable &&other) { data = other.data; changedCalled = false; other.data = -1; other.changedCalled = false; return *this; } bool operator==(const Uncomparable&) = delete; bool operator!=(const Uncomparable&) = delete; void changed() { changedCalled = true; } }; Uncomparable u1 = { 13 }; Uncomparable u2 = { 27 }; QProperty p1; QProperty p2(Qt::makePropertyBinding(p1)); QCOMPARE(p1.value().data, p2.value().data); p1.setValue(u1); QCOMPARE(p1.value().data, u1.data); QCOMPARE(p1.value().data, p2.value().data); p2.setValue(u2); QCOMPARE(p1.value().data, u1.data); QCOMPARE(p2.value().data, u2.data); QProperty p3(Qt::makePropertyBinding(p1)); p1.setValue(u1); QCOMPARE(p1.value().data, p3.value().data); // QNotifiedProperty np; // QVERIFY(np.value().data != u1.data); // np.setValue(&u1, u1); // QVERIFY(u1.changedCalled); // u1.changedCalled = false; // QCOMPARE(np.value().data, u1.data); // np.setValue(&u1, u1); // QVERIFY(u1.changedCalled); } //struct Test { // void notify() {}; // bool bindText(int); // bool bindIconText(int); // QProperty text; // QNotifiedProperty iconText; //}; //bool Test::bindIconText(int) { // Q_UNUSED(iconText.value()); // force read // if (!iconText.hasBinding()) { // iconText.setBinding(this, [=]() { return 0; }); // } // return true; //} void tst_QProperty::bindingValueReplacement() { // Test test; // test.text = 0; // test.bindIconText(0); // test.iconText.setValue(&test, 42); // should not crash // QCOMPARE(test.iconText.value(), 42); // test.text = 1; // QCOMPARE(test.iconText.value(), 42); } void tst_QProperty::quntypedBindableApi() { QProperty 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 *>(&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 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 i {42}; const QBindable bindableI(const_cast *>(&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 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) Q_PROPERTY(int computed READ computed STORED false) Q_PROPERTY(int compat READ compat WRITE setCompat NOTIFY compatChanged) signals: void fooChanged(int newFoo); void barChanged(); void compatChanged(); public slots: void fooHasChanged() { fooChangedCount++; } void barHasChanged() { barChangedCount++; } void compatHasChanged() { compatChangedCount++; } public: int foo() const { return fooData.value(); } void setFoo(int i) { fooData.setValue(i); } int bar() const { return barData.value(); } void setBar(int i) { barData.setValue(i); } int read() const { return readData.value(); } int computed() const { return readData.value(); } int compat() const { return compatData; } void setCompat(int i) { if (compatData == i) return; // implement some side effect and clamping ++setCompatCalled; if (i < 0) i = 0; compatData.setValue(i); compatData.notify(); emit compatChanged(); } QBindable bindableFoo() { return QBindable(&fooData); } const QBindable bindableFoo() const { return QBindable(&fooData); } QBindable bindableBar() { return QBindable(&barData); } QBindable bindableRead() { return QBindable(&readData); } QBindable bindableComputed() { return QBindable(&computedData); } QBindable bindableCompat() { return QBindable(&compatData); } public: int fooChangedCount = 0; int barChangedCount = 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); 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 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(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); QCOMPARE(object.fooChangedCount, 0); object.setFoo(10); QCOMPARE(object.fooChangedCount, 1); QCOMPARE(object.foo(), 10); auto f = [&object]() -> int { return object.barData; }; QCOMPARE(object.barChangedCount, 0); object.setBar(42); QCOMPARE(object.barChangedCount, 1); QCOMPARE(object.fooChangedCount, 1); object.fooData.setBinding(f); QCOMPARE(object.fooChangedCount, 2); QCOMPARE(object.fooData.value(), 42); object.setBar(666); QCOMPARE(object.fooChangedCount, 3); QCOMPARE(object.barChangedCount, 2); QCOMPARE(object.fooData.value(), 666); QCOMPARE(object.fooChangedCount, 3); auto f2 = [&object]() -> int { return object.barData / 2; }; object.bindableFoo().setBinding(Qt::makePropertyBinding(f2)); QVERIFY(object.bindableFoo().hasBinding()); QCOMPARE(object.foo(), 333); auto oldBinding = object.bindableFoo().setBinding(QPropertyBinding()); QVERIFY(!object.bindableFoo().hasBinding()); QVERIFY(!oldBinding.isNull()); QCOMPARE(object.foo(), 333); object.setBar(222); QCOMPARE(object.foo(), 333); object.bindableFoo().setBinding(oldBinding); QCOMPARE(object.foo(), 111); auto b = object.bindableRead().makeBinding(); object.bindableFoo().setBinding(b); QCOMPARE(object.foo(), 0); object.readData.setValue(10); QCOMPARE(object.foo(), 10); QCOMPARE(object.computed(), 10); object.readData.setValue(42); QCOMPARE(object.computed(), 42); object.bindableBar().setBinding(object.bindableComputed().makeBinding()); QCOMPARE(object.computed(), 42); 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 evaluationDetector {false}; evaluationDetector.setBinding(isCurrentlyEvaluatingBinding); QVERIFY(evaluationDetector.value()); QVERIFY(!isCurrentlyEvaluatingBinding()); } void tst_QProperty::qobjectObservers() { MyQObject object; int onValueChangedCalled = 0; { auto handler = object.bindableFoo().onValueChanged([&onValueChangedCalled]() { ++onValueChangedCalled; }); QCOMPARE(onValueChangedCalled, 0); object.setFoo(10); QCOMPARE(onValueChangedCalled, 1); object.bindableFoo().setBinding(object.bindableBar().makeBinding()); QCOMPARE(onValueChangedCalled, 2); object.setBar(42); QCOMPARE(onValueChangedCalled, 3); } object.setBar(0); QCOMPARE(onValueChangedCalled, 3); } void tst_QProperty::compatBindings() { MyQObject object; QObject::connect(&object, &MyQObject::fooChanged, &object, &MyQObject::fooHasChanged); QObject::connect(&object, &MyQObject::barChanged, &object, &MyQObject::barHasChanged); QObject::connect(&object, &MyQObject::compatChanged, &object, &MyQObject::compatHasChanged); QCOMPARE(object.compatData, 0); // setting data through the private interface should not call the changed signal or the public setter object.compatData.setValue(10); QCOMPARE(object.compatChangedCount, 0); QCOMPARE(object.setCompatCalled, 0); // going through the public API should emit the signal object.setCompat(42); QCOMPARE(object.compatChangedCount, 1); QCOMPARE(object.setCompatCalled, 1); // setting the same value again does nothing object.setCompat(42); QCOMPARE(object.compatChangedCount, 1); QCOMPARE(object.setCompatCalled, 1); object.setFoo(111); // just setting the binding. For a compat property, this should already trigger evaluation object.compatData.setBinding(object.bindableFoo().makeBinding()); QCOMPARE(object.compatData.valueBypassingBindings(), 111); QCOMPARE(object.compatChangedCount, 2); QCOMPARE(object.setCompatCalled, 2); QCOMPARE(object.compat(), 111); QCOMPARE(object.compatChangedCount, 2); QCOMPARE(object.setCompatCalled, 2); object.setFoo(666); QCOMPARE(object.compatData.valueBypassingBindings(), 666); QCOMPARE(object.compatChangedCount, 3); QCOMPARE(object.setCompatCalled, 3); QCOMPARE(object.compat(), 666); QCOMPARE(object.compatChangedCount, 3); QCOMPARE(object.setCompatCalled, 3); object.setFoo(-42); QCOMPARE(object.compatChangedCount, 4); QCOMPARE(object.setCompatCalled, 4); QCOMPARE(object.compat(), 0); QCOMPARE(object.compatChangedCount, 4); QCOMPARE(object.setCompatCalled, 4); object.setCompat(0); QCOMPARE(object.compat(), 0); QCOMPARE(object.compatChangedCount, 4); QCOMPARE(object.setCompatCalled, 4); } void tst_QProperty::metaProperty() { MyQObject object; QObject::connect(&object, &MyQObject::fooChanged, &object, &MyQObject::fooHasChanged); QObject::connect(&object, &MyQObject::barChanged, &object, &MyQObject::barHasChanged); QObject::connect(&object, &MyQObject::compatChanged, &object, &MyQObject::compatHasChanged); QCOMPARE(object.fooChangedCount, 0); object.setFoo(10); QCOMPARE(object.fooChangedCount, 1); QCOMPARE(object.foo(), 10); auto f = [&object]() -> int { return object.barData; }; QCOMPARE(object.barChangedCount, 0); object.setBar(42); QCOMPARE(object.barChangedCount, 1); QCOMPARE(object.fooChangedCount, 1); int fooIndex = object.metaObject()->indexOfProperty("foo"); QVERIFY(fooIndex >= 0); QMetaProperty fooProp = object.metaObject()->property(fooIndex); QVERIFY(fooProp.isValid()); auto fooBindable = fooProp.bindable(&object); QVERIFY(fooBindable.isValid()); QVERIFY(fooBindable.isBindable()); QVERIFY(!fooBindable.hasBinding()); fooBindable.setBinding(Qt::makePropertyBinding(f)); QVERIFY(fooBindable.hasBinding()); QCOMPARE(object.fooChangedCount, 2); QCOMPARE(object.fooData.value(), 42); object.setBar(666); QCOMPARE(object.fooChangedCount, 3); QCOMPARE(object.barChangedCount, 2); QCOMPARE(object.fooData.value(), 666); QCOMPARE(object.fooChangedCount, 3); fooBindable.setBinding(QUntypedPropertyBinding()); QVERIFY(!fooBindable.hasBinding()); QCOMPARE(object.fooData.value(), 666); object.setBar(0); QCOMPARE(object.fooData.value(), 666); object.setFoo(1); QCOMPARE(object.fooData.value(), 1); } void tst_QProperty::modifyObserverListWhileIterating() { struct DestructingObserver : QPropertyObserver { DestructingObserver() : QPropertyObserver([](QPropertyObserver *self, QUntypedPropertyData *) { auto This = static_cast(self); (*This)(); }), m_target(this){} void operator()() { (*counter)++; std::destroy_at(m_target); } DestructingObserver *m_target; int *counter = nullptr; }; union ObserverOrUninit { DestructingObserver observer = {}; char* memory; ~ObserverOrUninit() {} ObserverOrUninit() {} }; { // observer deletes itself while running the notification // while explicitly calling the destructor is rather unusual // it is completely plausible for this to happen because the object to which a // propertyobserver belongs has been destroyed ObserverOrUninit data; int counter = 0; data.observer.counter = &counter; QProperty prop; QUntypedBindable bindableProp(&prop); bindableProp.observe(&data.observer); prop = 42; // should not crash QCOMPARE(counter, 1); } { // observer deletes the next observer in the list ObserverOrUninit data1; ObserverOrUninit data2; QProperty prop; QUntypedBindable bindableProp(&prop); bindableProp.observe(&data1.observer); bindableProp.observe(&data2.observer); int counter = 0; data1.observer.m_target = &data2.observer; data1.observer.counter = &counter; data2.observer.m_target = &data1.observer; data2.observer.counter = &counter; prop = 42; // should not crash QCOMPARE(counter, 1); // only one trigger should run as the other has been deleted } } void tst_QProperty::noDoubleCapture() { QProperty size; size = 3; QProperty 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 bindableProp1() {return QBindable(&prop1Data);} int prop2() { return prop2Data.value(); } void setProp2(int i) { if (i == prop2Data) return; prop2Data.setValue(i); prop2Data.notify(); } QBindable bindableProp2() { return QBindable(&prop2Data); } int prop3() { return prop3Data.value(); } void setProp3(int i) { if (i == prop3Data) return; prop3Data.setValue(i); prop3Data.notify(); } QBindable bindableProp3() { return QBindable(&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 bindableProp4() { return QBindable(&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 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 prop2Observer; prop2Observer.setBinding(tester.bindableProp2().makeBinding()); QSignalSpy prop2Spy(&tester, &CompatPropertyTester::prop2Changed); tester.setProp2(10); QCOMPARE(prop2Observer.value(), 10); QCOMPARE(prop2Spy.size(), 1); QList 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 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 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 bindableProp1() { return QBindable(&prop1Data); } QBindable bindableProp2() { return QBindable(&prop2Data); } QBindable bindableProp3() { return QBindable(&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 source { 5 }; QProperty dest1 { 99 }; QProperty 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 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 { using QBindable::QBindable; QtPrivate::QPropertyAdaptorSlotObject* data() { return static_cast(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(&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(&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 source2 { 20 }; source.setValue(21); binding = QBindable(&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 binding2(&object2, "foo"); QBindable 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(&bindableObject, "foo").hasBinding()); QBindable 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(&object, QMetaProperty{}).isValid()); #ifndef QT_NO_DEBUG QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property foo1 has no notify signal"); #endif QVERIFY(!QBindable(&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(&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( &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(&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 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 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 scopedObj1; QObject **dest; QThread *destThread; }; void tst_QProperty::threadSafety2() { std::unique_ptr 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 bindableProp1() { return QBindable(&prop1Data); } CustomType prop2() { return prop2Data.value(); } void setProp2(CustomType val) { prop2Data = val; } QBindable bindableProp2() { return QBindable(&prop2Data); } CustomType prop3() { return prop3Data.value(); } void setProp3(CustomType val) { prop3Data.setValue(val); } QBindable bindableProp3() { return QBindable(&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 a(0); QProperty b; b.setBinding([&](){ return a.value(); }); QProperty c; c.setBinding([&](){ return a.value(); }); QProperty 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 a(0); QProperty b; b.setBinding([&](){ return a.value(); }); QProperty c; c.setBinding([&](){ return a.value(); }); QProperty d; QProperty 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 i(0); QProperty 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(); 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 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(); auto toBeDeleted = std::make_unique(); 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 i; QProperty 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 testProperty(0); QList 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 bindable = obj.bindableCompat(); QCOMPARE(obj.setCompatCalled, 0); bindable.setValue(42); QCOMPARE(obj.setCompatCalled, 1); } void tst_QProperty::selfBindingShouldNotCrash() { QProperty 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> i {new QProperty}; QPropertyAlias alias(i.get()); QVERIFY(alias.isValid()); alias.setValue(42); QCOMPARE(i->value(), 42); QProperty 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 p; QCOMPARE(p.value(), 0); const auto handler = p.addNotifier([&](){ ++notifications; }); QCOMPARE(notifications, 0); QPropertyBinding 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 iprop; QProperty 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 bindable1Bindable() { return QBindable(&m_bindable1); } int bindable2() const { return m_bindable2; } void setBindable2(int newBindable2) { if (m_bindable2 == newBindable2) return; m_bindable2 = newBindable2; emit bindable2Changed(); } QBindable bindable2Bindable() { return QBindable(&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 rootTrigger; QProperty 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 source(11); DtorCounter::counter = 0; DtorCounter dc; QProperty target([&triggered, &source, dc]() mutable { dc.shouldIncrement = true; return ++triggered + source.value(); }); QCOMPARE(triggered, 1); { auto propObserver = std::make_unique(); 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"