diff options
Diffstat (limited to 'tests/auto/corelib/kernel/qproperty')
-rw-r--r-- | tests/auto/corelib/kernel/qproperty/CMakeLists.txt | 11 | ||||
-rw-r--r-- | tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp | 1391 |
2 files changed, 1196 insertions, 206 deletions
diff --git a/tests/auto/corelib/kernel/qproperty/CMakeLists.txt b/tests/auto/corelib/kernel/qproperty/CMakeLists.txt index 55a5d71752..177465d2ee 100644 --- a/tests/auto/corelib/kernel/qproperty/CMakeLists.txt +++ b/tests/auto/corelib/kernel/qproperty/CMakeLists.txt @@ -1,12 +1,19 @@ -# Generated from qproperty.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## tst_qproperty Test: ##################################################################### +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qproperty LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + qt_internal_add_test(tst_qproperty SOURCES tst_qproperty.cpp - PUBLIC_LIBRARIES + LIBRARIES Qt::CorePrivate ) diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index 6062c23189..cc7edb8bf2 100644 --- a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp +++ b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp @@ -1,38 +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_QDOC) +#include <source_location> +#define QT_SOURCE_LOCATION_NAMESPACE std +#elif __has_include(<experimental/source_location>) && !defined(Q_QDOC) +#include <experimental/source_location> +#define QT_SOURCE_LOCATION_NAMESPACE std::experimental +#endif using namespace QtPrivate; - +using namespace Qt::StringLiterals; struct DtorCounter { static inline int counter = 0; @@ -44,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(); @@ -55,6 +42,7 @@ private slots: void avoidDependencyAllocationAfterFirstEval(); void boolProperty(); void takeBinding(); + void stickyBinding(); void replaceBinding(); void changeHandler(); void propertyChangeHandlerApi(); @@ -73,28 +61,110 @@ private slots: void genericPropertyBindingBool(); void setBindingFunctor(); void multipleObservers(); - void propertyAlias(); void arrowAndStarOperator(); void notifiedProperty(); void typeNoOperatorEqual(); void bindingValueReplacement(); void quntypedBindableApi(); + void readonlyConstQBindable(); + void qobjectBindableManualNotify(); + void qobjectBindableReallocatedBindingStorage(); + void qobjectBindableSignalTakingNewValue(); void testNewStuff(); void qobjectObservers(); void compatBindings(); void metaProperty(); - void aliasOnMetaProperty(); void modifyObserverListWhileIterating(); + void noDoubleCapture(); void compatPropertyNoDobuleNotification(); + void compatPropertySignals(); void noFakeDependencies(); +#if QT_CONFIG(thread) + void threadSafety(); + void threadSafety2(); +#endif // QT_CONFIG(thread) void bindablePropertyWithInitialization(); - void markDirty(); + 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; }); @@ -126,8 +196,8 @@ void tst_QProperty::multipleDependencies() QProperty<int> sum; sum.setBinding([&]() { return firstDependency + secondDependency; }); - QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 0); - QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 0); + QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); + QCOMPARE(QPropertyBindingDataPointer::get(secondDependency).observerCount(), 1); QCOMPARE(sum.value(), int(3)); QCOMPARE(QPropertyBindingDataPointer::get(firstDependency).observerCount(), 1); @@ -182,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); @@ -218,6 +317,7 @@ void tst_QProperty::bindingAfterUse() void tst_QProperty::bindingFunctionDtorCalled() { + DtorCounter::counter = 0; DtorCounter dc; { QProperty<int> prop; @@ -257,12 +357,12 @@ void tst_QProperty::avoidDependencyAllocationAfterFirstEval() QCOMPARE(propWithBinding.value(), int(11)); - QVERIFY(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()); - QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()->dependencyObserverCount, 2u); + QVERIFY(QPropertyBindingDataPointer::get(propWithBinding).binding()); + QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); firstDependency = 100; QCOMPARE(propWithBinding.value(), int(110)); - QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).bindingPtr()->dependencyObserverCount, 2u); + QCOMPARE(QPropertyBindingDataPointer::get(propWithBinding).binding()->dependencyObserverCount, 2u); } void tst_QProperty::boolProperty() @@ -302,6 +402,30 @@ void tst_QProperty::takeBinding() QVERIFY(!existingBinding.isNull()); } +void tst_QProperty::stickyBinding() +{ + QProperty<int> prop; + QProperty<int> prop2 {2}; + prop.setBinding([&](){ return prop2.value(); }); + QCOMPARE(prop.value(), 2); + auto privBinding = QPropertyBindingPrivate::get(prop.binding()); + // If we make a binding sticky, + privBinding->setSticky(); + // then writing to the property does not remove it + prop = 1; + QVERIFY(prop.hasBinding()); + // but the value still changes. + QCOMPARE(prop.value(), 1); + // The binding continues to work normally. + prop2 = 3; + QCOMPARE(prop.value(), 3); + // If we remove the stickiness + privBinding->setSticky(false); + // the binding goes away on the next write + prop = 42; + QVERIFY(!prop.hasBinding()); +} + void tst_QProperty::replaceBinding() { QProperty<int> first(100); @@ -332,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); } @@ -375,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); @@ -434,7 +558,7 @@ void tst_QProperty::dontTriggerDependenciesIfUnchangedValue() void tst_QProperty::bindingSourceLocation() { #if defined(QT_PROPERTY_COLLECT_BINDING_LOCATION) - auto bindingLine = std::experimental::source_location::current().line() + 1; + auto bindingLine = QT_SOURCE_LOCATION_NAMESPACE::source_location::current().line() + 1; auto binding = Qt::makePropertyBinding([]() { return 42; }); QCOMPARE(QPropertyBindingPrivate::get(binding)->sourceLocation().line, bindingLine); #else @@ -469,35 +593,34 @@ class BindingLoopTester : public QObject BindingLoopTester() {} int eagerProp() {return eagerData.value();} - void setEagerProp(int i) { eagerData.setValue(i); } + void setEagerProp(int i) { eagerData.setValue(i); eagerData.notify(); } QBindable<int> bindableEagerProp() {return QBindable<int>(&eagerData);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData, &BindingLoopTester::setEagerProp) int eagerProp2() {return eagerData2.value();} - void setEagerProp2(int i) { eagerData2.setValue(i); } + void setEagerProp2(int i) { eagerData2.setValue(i); eagerData2.notify(); } QBindable<int> bindableEagerProp2() {return QBindable<int>(&eagerData2);} Q_OBJECT_COMPAT_PROPERTY(BindingLoopTester, int, eagerData2, &BindingLoopTester::setEagerProp2) }; void tst_QProperty::bindingLoop() { - QScopedPointer<QProperty<int>> firstProp; + QProperty<int> firstProp; QProperty<int> secondProp([&]() -> int { - return firstProp ? firstProp->value() : 0; + return firstProp.value(); }); QProperty<int> thirdProp([&]() -> int { return secondProp.value(); }); - firstProp.reset(new QProperty<int>()); - firstProp->setBinding([&]() -> int { - return secondProp.value(); + firstProp.setBinding([&]() -> int { + return secondProp.value() + thirdProp.value(); }); - QCOMPARE(thirdProp.value(), 0); - QCOMPARE(secondProp.binding().error().type(), QPropertyBindingError::BindingLoop); + thirdProp.setValue(10); + QCOMPARE(firstProp.binding().error().type(), QPropertyBindingError::BindingLoop); { @@ -536,7 +659,7 @@ public: #define GEN(N) \ int prop##N() {return propData##N.value();} \ - void setProp##N(int i) { propData##N.setValue(i); } \ + void setProp##N(int i) { if (i == propData##N) return; propData##N.setValue(i); propData##N.notify(); } \ QBindable<int> bindableProp##N() {return QBindable<int>(&propData##N);} \ Q_OBJECT_COMPAT_PROPERTY(ReallocTester, int, propData##N, &ReallocTester::setProp##N) GEN(1) @@ -668,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()); @@ -676,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); @@ -696,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; @@ -752,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"); @@ -857,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(); @@ -887,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(); @@ -895,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(); @@ -904,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(); @@ -1026,6 +1103,45 @@ void tst_QProperty::quntypedBindableApi() QCOMPARE(iprop.value(), 42); QUntypedBindable propLess; QVERIFY(propLess.takeBinding().isNull()); + + QUntypedBindable invalidBindable; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is invalid."); +#endif + invalidBindable.setBinding(Qt::makePropertyBinding(iprop)); + + QUntypedBindable readOnlyBindable(static_cast<const QProperty<int> *>(&iprop) ); + QVERIFY(readOnlyBindable.isReadOnly()); +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is read-only."); +#endif + readOnlyBindable.setBinding(Qt::makePropertyBinding(iprop)); + + QProperty<float> fprop; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding as the property expects it to be of type int but got float instead."); +#endif + bindable.setBinding(Qt::makePropertyBinding(fprop)); +} + +void tst_QProperty::readonlyConstQBindable() +{ + QProperty<int> i {42}; + const QBindable<int> bindableI(const_cast<const QProperty<int> *>(&i)); + + // check that read-only operations work with a const QBindable + QVERIFY(bindableI.isReadOnly()); + QVERIFY(!bindableI.hasBinding()); + // we can still create a binding to a read only bindable through the interface + QProperty<int> j; + j.setBinding(bindableI.makeBinding()); + QCOMPARE(j.value(), bindableI.value()); + int counter = 0; + auto observer = bindableI.subscribe([&](){++counter;}); + QCOMPARE(counter, 1); + auto observer2 = bindableI.onValueChanged([&](){++counter;}); + i = 0; + QCOMPARE(counter, 3); } class MyQObject : public QObject @@ -1038,7 +1154,7 @@ class MyQObject : public QObject Q_PROPERTY(int compat READ compat WRITE setCompat NOTIFY compatChanged) signals: - void fooChanged(); + void fooChanged(int newFoo); void barChanged(); void compatChanged(); @@ -1064,6 +1180,7 @@ public: if (i < 0) i = 0; compatData.setValue(i); + compatData.notify(); emit compatChanged(); } @@ -1087,9 +1204,95 @@ public: Q_OBJECT_COMPAT_PROPERTY(MyQObject, int, compatData, &MyQObject::setCompat) }; + +void tst_QProperty::qobjectBindableManualNotify() +{ + // Given an object of type MyQObject, + MyQObject object; + // track its foo property's change count + auto bindable = object.bindableFoo(); + int fooChangeCount = 0; + auto changeHandler = bindable.onValueChanged([&](){++fooChangeCount;}); + // and how many changed signals it emits. + QSignalSpy fooChangedSpy(&object, &MyQObject::fooChanged); + + // If we bypass the bindings system, + object.fooData.setValueBypassingBindings(42); + // there is no change. + QCOMPARE(fooChangeCount, 0); + QCOMPARE(fooChangedSpy.size(), 0); + // Once we notify manually + object.fooData.notify(); + // observers are notified and the signal arrives. + QCOMPARE(fooChangeCount, 1); + QCOMPARE(fooChangedSpy.size(), 1); + + // If we set a binding + int i = 1; + object.fooData.setBinding([&](){return i;}); + // then the value changes + QCOMPARE(object.foo(), 1); + // and the change and signal count are incremented. + QCOMPARE(fooChangeCount, 2); + QCOMPARE(fooChangedSpy.size(), 2); + // Changing a non-property won't trigger any notification. + i = 2; + QCOMPARE(fooChangeCount, 2); + QCOMPARE(fooChangedSpy.size(), 2); + // Manually triggering the notification + object.fooData.notify(); + // increments the change count + QCOMPARE(fooChangeCount, 3); + QCOMPARE(fooChangedSpy.size(), 3); + // but doesn't actually cause a binding reevaluation. + QCOMPARE(object.foo(), 1); +} + + +struct ReallocObject : QObject { + ReallocObject() + { v.setBinding([this] { return x.value() + y.value() + z.value(); }); } + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, v) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, x) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, y) + Q_OBJECT_BINDABLE_PROPERTY(ReallocObject, int, z) +}; + +void tst_QProperty::qobjectBindableReallocatedBindingStorage() +{ + ReallocObject object; + object.x = 1; + QCOMPARE(object.v.value(), 1); +} + +void tst_QProperty::qobjectBindableSignalTakingNewValue() +{ + // Given an object of type MyQObject, + MyQObject object; + // and tracking the values emitted via its fooChanged signal, + int newValue = -1; + QObject::connect(&object, &MyQObject::fooChanged, [&](int i){ newValue = i; } ); + + // when we change the property's value via the bindable interface + object.bindableFoo().setValue(1); + // we obtain the newly set value. + QCOMPARE(newValue, 1); + + // The same holds true when we set a binding + QProperty<int> i {2}; + object.bindableFoo().setBinding(Qt::makePropertyBinding(i)); + QCOMPARE(newValue, 2); + // and when the binding gets reevaluated to a new value + i = 3; + QCOMPARE(newValue, 3); +} + void tst_QProperty::testNewStuff() { MyQObject testReadOnly; +#ifndef QT_NO_DEBUG + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "setBinding: Could not set binding via bindable interface. The QBindable is read-only."); +#endif testReadOnly.bindableFoo().setBinding([](){return 42;}); auto bindable = const_cast<const MyQObject&>(testReadOnly).bindableFoo(); QVERIFY(bindable.hasBinding()); @@ -1176,7 +1379,9 @@ void tst_QProperty::qobjectObservers() MyQObject object; int onValueChangedCalled = 0; { - auto handler = object.bindableFoo().onValueChanged([&onValueChangedCalled]() { ++onValueChangedCalled;}); + auto handler = object.bindableFoo().onValueChanged([&onValueChangedCalled]() { + ++onValueChangedCalled; + }); QCOMPARE(onValueChangedCalled, 0); object.setFoo(10); @@ -1295,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 { @@ -1349,6 +1518,7 @@ void tst_QProperty::modifyObserverListWhileIterating() DestructingObserver observer = {}; char* memory; ~ObserverOrUninit() {} + ObserverOrUninit() {} }; { // observer deletes itself while running the notification @@ -1382,18 +1552,87 @@ 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) - public: + 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) { prop1Data.setValue(i); } + void setProp1(int i) { if (i == prop1Data) return; prop1Data.setValue(i); prop1Data.notify(); } QBindable<int> bindableProp1() {return QBindable<int>(&prop1Data);} - Q_OBJECT_COMPAT_PROPERTY(CompatPropertyTester, int, prop1Data, &CompatPropertyTester::setProp1) + int prop2() { return prop2Data.value(); } + void setProp2(int i) + { + if (i == prop2Data) + return; + prop2Data.setValue(i); + prop2Data.notify(); + } + QBindable<int> bindableProp2() { return QBindable<int>(&prop2Data); } + + int prop3() { return prop3Data.value(); } + void setProp3(int i) + { + if (i == prop3Data) + return; + prop3Data.setValue(i); + prop3Data.notify(); + } + QBindable<int> bindableProp3() { return QBindable<int>(&prop3Data); } + + int prop4() const + { + auto val = prop4Data.value(); + return val == 0 ? 42 : val; + } + + void setProp4(int i) + { + if (i == prop4Data) + return; + prop4Data.setValue(i); + prop4Data.notify(); + } + QBindable<int> bindableProp4() { return QBindable<int>(&prop4Data); } + +signals: + void prop2Changed(int value); + void prop3Changed(); + void prop4Changed(int value); + +private: + Q_OBJECT_COMPAT_PROPERTY(CompatPropertyTester, int, prop1Data, &CompatPropertyTester::setProp1) + Q_OBJECT_COMPAT_PROPERTY(CompatPropertyTester, int, prop2Data, &CompatPropertyTester::setProp2, + &CompatPropertyTester::prop2Changed) + Q_OBJECT_COMPAT_PROPERTY_WITH_ARGS(CompatPropertyTester, int, prop3Data, + &CompatPropertyTester::setProp3, + &CompatPropertyTester::prop3Changed, 1) + Q_OBJECT_COMPAT_PROPERTY_WITH_ARGS(CompatPropertyTester, int, prop4Data, + &CompatPropertyTester::setProp4, + &CompatPropertyTester::prop4Changed, + &CompatPropertyTester::prop4, 0) }; void tst_QProperty::compatPropertyNoDobuleNotification() @@ -1407,6 +1646,72 @@ void tst_QProperty::compatPropertyNoDobuleNotification() QCOMPARE(counter, 1); } +void tst_QProperty::compatPropertySignals() +{ + CompatPropertyTester tester; + + // Compat property with signal. Signal has parameter. + QProperty<int> prop2Observer; + prop2Observer.setBinding(tester.bindableProp2().makeBinding()); + + QSignalSpy prop2Spy(&tester, &CompatPropertyTester::prop2Changed); + + tester.setProp2(10); + + QCOMPARE(prop2Observer.value(), 10); + QCOMPARE(prop2Spy.size(), 1); + QList<QVariant> arguments = prop2Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 10); + + // Compat property with signal and default value. Signal has no parameter. + QProperty<int> prop3Observer; + prop3Observer.setBinding(tester.bindableProp3().makeBinding()); + QCOMPARE(prop3Observer.value(), 1); + + QSignalSpy prop3Spy(&tester, &CompatPropertyTester::prop3Changed); + + tester.setProp3(5); + + QCOMPARE(prop3Observer.value(), 5); + QCOMPARE(prop3Spy.size(), 1); + + // Compat property with signal, default value, and custom setter. Signal has parameter. + QProperty<int> prop4Observer; + prop4Observer.setBinding(tester.bindableProp4().makeBinding()); + QCOMPARE(prop4Observer.value(), 42); + + QSignalSpy prop4Spy(&tester, &CompatPropertyTester::prop4Changed); + + tester.setProp4(10); + + QCOMPARE(prop4Observer.value(), 10); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 10); + + tester.setProp4(42); + + QCOMPARE(prop4Observer.value(), 42); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 42); + + tester.setProp4(0); + + QCOMPARE(prop4Observer.value(), 42); + QCOMPARE(prop4Spy.size(), 1); + arguments = prop4Spy.takeFirst(); + QCOMPARE(arguments.size(), 1); + QCOMPARE(arguments.at(0).metaType().id(), QMetaType::Int); + QCOMPARE(arguments.at(0).toInt(), 42); +} + class FakeDependencyCreator : public QObject { Q_OBJECT @@ -1421,9 +1726,9 @@ signals: void prop3Changed(); public: - void setProp1(int val) { prop1Data.setValue(val); emit prop1Changed();} - void setProp2(int val) { prop2Data.setValue(val); emit prop2Changed();} - void setProp3(int val) { prop3Data.setValue(val); emit prop3Changed();} + 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; } @@ -1434,9 +1739,9 @@ public: QBindable<int> bindableProp3() { return QBindable<int>(&prop3Data); } private: - Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop1Data, &FakeDependencyCreator::setProp1); - Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop2Data, &FakeDependencyCreator::setProp2); - Q_OBJECT_COMPAT_PROPERTY(FakeDependencyCreator, int, prop3Data, &FakeDependencyCreator::setProp3); + 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() @@ -1456,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; @@ -1518,82 +2140,443 @@ void tst_QProperty::bindablePropertyWithInitialization() QCOMPARE(tester.prop3().anotherValue, 20); } -class MarkDirtyTester : public QObject +void tst_QProperty::noDoubleNotification() { - Q_OBJECT -public: - Q_PROPERTY(int value1 READ value1 WRITE setValue1 BINDABLE bindableValue1) - Q_PROPERTY(int value2 READ value2 WRITE setValue1 BINDABLE bindableValue2) - Q_PROPERTY(int computed READ computed BINDABLE bindableComputed) + /* dependency graph for this test + x --> y means y depends on x + a-->b-->d + \ ^ + \->c--/ + */ + QProperty<int> a(0); + QProperty<int> b; + b.setBinding([&](){ return a.value(); }); + QProperty<int> c; + c.setBinding([&](){ return a.value(); }); + QProperty<int> d; + d.setBinding([&](){ return b.value() + c.value(); }); + int nNotifications = 0; + int expected = 0; + auto connection = d.subscribe([&](){ + ++nNotifications; + QCOMPARE(d.value(), expected); + }); + QCOMPARE(nNotifications, 1); + expected = 2; + a = 1; + QCOMPARE(nNotifications, 2); + expected = 4; + a = 2; + QCOMPARE(nNotifications, 3); +} - inline static int staticValue = 0; +void tst_QProperty::groupedNotifications() +{ + QProperty<int> a(0); + QProperty<int> b; + b.setBinding([&](){ return a.value(); }); + QProperty<int> c; + c.setBinding([&](){ return a.value(); }); + QProperty<int> d; + QProperty<int> e; + e.setBinding([&](){ return b.value() + c.value() + d.value(); }); + int nNotifications = 0; + int expected = 0; + auto connection = e.subscribe([&](){ + ++nNotifications; + QCOMPARE(e.value(), expected); + }); + QCOMPARE(nNotifications, 1); - int value1() const {return m_value1;} - void setValue1(int val) {m_value1 = val;} - QBindable<int> bindableValue1() {return { &m_value1 };} + 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); - int value2() const {return m_value2;} - void setValue2(int val) { m_value2.setValue(val); } - QBindable<int> bindableValue2() {return { &m_value2 };} + 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); - int computed() const { return staticValue + m_value1; } - QBindable<int> bindableComputed() {return {&m_computed};} - void incrementStaticValue() { - ++staticValue; - m_computed.markDirty(); - } +} - void markValue1Dirty() { - m_value1.markDirty(); - } +void tst_QProperty::groupedNotificationConsistency() +{ + QProperty<int> i(0); + QProperty<int> j(0); + bool areEqual = true; + + auto observer = i.onValueChanged([&](){ + areEqual = i == j; + }); + + i = 1; + j = 1; + QVERIFY(!areEqual); // value changed runs before j = 1 - void markValue2Dirty() { - m_value2.markDirty(); + { + const QScopedPropertyUpdateGroup guard; + i = 2; + j = 2; } -private: - Q_OBJECT_BINDABLE_PROPERTY(MarkDirtyTester, int, m_value1, nullptr) - Q_OBJECT_COMPAT_PROPERTY(MarkDirtyTester, int, m_value2, &MarkDirtyTester::setValue2) - Q_OBJECT_COMPUTED_PROPERTY(MarkDirtyTester, int, m_computed, &MarkDirtyTester::computed) -}; + QVERIFY(areEqual); // value changed runs after everything has been evaluated +} -void tst_QProperty::markDirty() +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; } ); { - QProperty<int> testProperty; - int changeCounter = 0; - auto handler = testProperty.onValueChanged([&](){++changeCounter;}); - testProperty.markDirty(); - QCOMPARE(changeCounter, 1); + 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; + { - MarkDirtyTester dirtyTester; - int computedChangeCounter = 0; - int value1ChangeCounter = 0; - auto handler = dirtyTester.bindableComputed().onValueChanged([&](){ - computedChangeCounter++; + QPropertyNotifier handler = testProperty.addNotifier([&]() { + recordedValues << testProperty; }); - auto handler2 = dirtyTester.bindableValue1().onValueChanged([&](){ - value1ChangeCounter++; + notifier = testProperty.addNotifier([&]() { + value = testProperty; }); - dirtyTester.incrementStaticValue(); - QCOMPARE(computedChangeCounter, 1); - QCOMPARE(dirtyTester.computed(), 1); - dirtyTester.markValue1Dirty(); - QCOMPARE(value1ChangeCounter, 1); - QCOMPARE(computedChangeCounter, 1); + + 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) { - MarkDirtyTester dirtyTester; - int changeCounter = 0; - auto handler = dirtyTester.bindableValue2().onValueChanged([&](){ - changeCounter++; - }); - dirtyTester.markValue2Dirty(); - QCOMPARE(changeCounter, 1); + if (m_signaled == newSignaled) + return; + m_signaled = newSignaled; + emit signaledChanged(); + } + + int bindable1() const + { + return m_bindable1; } + + void setBindable1(int newBindable1) + { + if (m_bindable1 == newBindable1) + return; + m_bindable1 = newBindable1; + emit bindable1Changed(); + } + + QBindable<int> bindable1Bindable() + { + return QBindable<int>(&m_bindable1); + } + + int bindable2() const + { + return m_bindable2; + } + + void setBindable2(int newBindable2) + { + if (m_bindable2 == newBindable2) + return; + m_bindable2 = newBindable2; + emit bindable2Changed(); + } + + QBindable<int> bindable2Bindable() + { + return QBindable<int>(&m_bindable2); + } + +signals: + void signaledChanged(); + void bindable1Changed(); + void bindable2Changed(); + +private: + int m_signaled = 0; + Q_OBJECT_COMPAT_PROPERTY(TestObject, int, m_bindable1, &TestObject::setBindable1, &TestObject::bindable1Changed); + Q_OBJECT_COMPAT_PROPERTY(TestObject, int, m_bindable2, &TestObject::setBindable2, &TestObject::bindable2Changed); +}; + +void tst_QProperty::propertyUpdateViaSignaledProperty() +{ + TestObject o; + QProperty<int> rootTrigger; + QProperty<int> signalTrigger; + + o.bindable1Bindable().setBinding([&]() { + return rootTrigger.value(); + }); + + QObject::connect(&o, &TestObject::bindable1Changed, &o, [&]() { + // Signaled changes only once, doesn't actually depend on bindable1. + // In reality, there could be some complicated calculation behind this that changes + // on certain checkpoints, but not on every iteration. + o.setSignaled(40); + }); + + o.bindable2Bindable().setBinding([&]() { + return signalTrigger.value() - o.bindable1(); + }); + + QObject::connect(&o, &TestObject::signaledChanged, &o, [&]() { + signalTrigger.setValue(o.signaled()); + }); + + rootTrigger.setValue(2); + QCOMPARE(o.bindable1(), 2); + QCOMPARE(o.bindable2(), 38); + rootTrigger.setValue(3); + QCOMPARE(o.bindable1(), 3); + QCOMPARE(o.bindable2(), 37); + rootTrigger.setValue(4); + QCOMPARE(o.bindable1(), 4); + QCOMPARE(o.bindable2(), 36); +} + +void tst_QProperty::derefFromObserver() +{ + int triggered = 0; + QProperty<int> source(11); + + DtorCounter::counter = 0; + DtorCounter dc; + + QProperty<int> target([&triggered, &source, dc]() mutable { + dc.shouldIncrement = true; + return ++triggered + source.value(); + }); + QCOMPARE(triggered, 1); + + { + auto propObserver = std::make_unique<QPropertyObserver>(); + QPropertyObserverPointer propObserverPtr { propObserver.get() }; + propObserverPtr.setBindingToNotify(QPropertyBindingPrivate::get(target.binding())); + + QBindingObserverPtr bindingPtr(propObserver.get()); + + QCOMPARE(triggered, 1); + source = 25; + QCOMPARE(triggered, 2); + QCOMPARE(target, 27); + + target.setBinding([]() { return 8; }); + QCOMPARE(target, 8); + + // The QBindingObserverPtr still holds on to the binding. + QCOMPARE(dc.counter, 0); + } + + // The binding is actually gone now. + QCOMPARE(dc.counter, 1); + + source = 26; + QCOMPARE(triggered, 2); + QCOMPARE(target, 8); } QTEST_MAIN(tst_QProperty); +#undef QT_SOURCE_LOCATION_NAMESPACE + #include "tst_qproperty.moc" |