diff options
Diffstat (limited to 'tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp')
-rw-r--r-- | tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp | 1044 |
1 files changed, 894 insertions, 150 deletions
diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index b1f5a780a6..cc7edb8bf2 100644 --- a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp +++ b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp @@ -1,47 +1,23 @@ -/**************************************************************************** -** -** 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_CLANG_QDOC) +#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>) && __cplusplus >= 201703L && !defined(Q_CLANG_QDOC) +#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; @@ -53,10 +29,12 @@ 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(); @@ -83,7 +61,6 @@ private slots: void genericPropertyBindingBool(); void setBindingFunctor(); void multipleObservers(); - void propertyAlias(); void arrowAndStarOperator(); void notifiedProperty(); void typeNoOperatorEqual(); @@ -91,26 +68,103 @@ private slots: 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; }); @@ -198,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); @@ -234,6 +317,7 @@ void tst_QProperty::bindingAfterUse() void tst_QProperty::bindingFunctionDtorCalled() { + DtorCounter::counter = 0; DtorCounter dc; { QProperty<int> prop; @@ -372,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); } @@ -415,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); @@ -707,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()); @@ -715,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); @@ -735,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; @@ -791,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"); @@ -896,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(); @@ -926,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(); @@ -934,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(); @@ -943,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(); @@ -1182,12 +1220,12 @@ void tst_QProperty::qobjectBindableManualNotify() object.fooData.setValueBypassingBindings(42); // there is no change. QCOMPARE(fooChangeCount, 0); - QCOMPARE(fooChangedSpy.count(), 0); + QCOMPARE(fooChangedSpy.size(), 0); // Once we notify manually object.fooData.notify(); // observers are notified and the signal arrives. QCOMPARE(fooChangeCount, 1); - QCOMPARE(fooChangedSpy.count(), 1); + QCOMPARE(fooChangedSpy.size(), 1); // If we set a binding int i = 1; @@ -1196,20 +1234,37 @@ void tst_QProperty::qobjectBindableManualNotify() QCOMPARE(object.foo(), 1); // and the change and signal count are incremented. QCOMPARE(fooChangeCount, 2); - QCOMPARE(fooChangedSpy.count(), 2); + QCOMPARE(fooChangedSpy.size(), 2); // Changing a non-property won't trigger any notification. i = 2; QCOMPARE(fooChangeCount, 2); - QCOMPARE(fooChangedSpy.count(), 2); + QCOMPARE(fooChangedSpy.size(), 2); // Manually triggering the notification object.fooData.notify(); // increments the change count QCOMPARE(fooChangeCount, 3); - QCOMPARE(fooChangedSpy.count(), 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, @@ -1445,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 { @@ -1499,6 +1518,7 @@ void tst_QProperty::modifyObserverListWhileIterating() DestructingObserver observer = {}; char* memory; ~ObserverOrUninit() {} + ObserverOrUninit() {} }; { // observer deletes itself while running the notification @@ -1532,12 +1552,29 @@ 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) { } @@ -1565,9 +1602,25 @@ public: } 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) @@ -1576,6 +1629,10 @@ private: 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() @@ -1602,8 +1659,8 @@ void tst_QProperty::compatPropertySignals() tester.setProp2(10); QCOMPARE(prop2Observer.value(), 10); - QCOMPARE(prop2Spy.count(), 1); - const QList<QVariant> arguments = prop2Spy.takeFirst(); + 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); @@ -1618,7 +1675,41 @@ void tst_QProperty::compatPropertySignals() tester.setProp3(5); QCOMPARE(prop3Observer.value(), 5); - QCOMPARE(prop3Spy.count(), 1); + 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 @@ -1670,6 +1761,323 @@ void tst_QProperty::noFakeDependencies() 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; @@ -1781,27 +2189,29 @@ void tst_QProperty::groupedNotifications() QCOMPARE(nNotifications, 1); expected = 2; - Qt::beginPropertyUpdateGroup(); - a = 1; - QCOMPARE(b.value(), 0); - QCOMPARE(c.value(), 0); - QCOMPARE(d.value(), 0); - QCOMPARE(nNotifications, 1); - Qt::endPropertyUpdateGroup(); + { + 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; - Qt::beginPropertyUpdateGroup(); - a = 2; - d = 3; - QCOMPARE(b.value(), 1); - QCOMPARE(c.value(), 1); - QCOMPARE(d.value(), 3); - QCOMPARE(nNotifications, 2); - Qt::endPropertyUpdateGroup(); + { + 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); @@ -1824,13 +2234,347 @@ void tst_QProperty::groupedNotificationConsistency() j = 1; QVERIFY(!areEqual); // value changed runs before j = 1 - Qt::beginPropertyUpdateGroup(); - i = 2; - j = 2; - Qt::endPropertyUpdateGroup(); + { + 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 |