Revision control

++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ #include <QtTest/QtTest>
++ #include <QtTest/private/qpropertytesthelper_p.h>
++ #include <QtPositioningQuick/private/qdeclarativepositionsource_p.h>
++
++ QT_USE_NAMESPACE
++
++ class tst_DeclarativePositionSource : public QObject
++ {
++ Q_OBJECT
++
++ private slots:
++ void init();
++
++ void nameBinding();
++ void supportedMethodsBinding();
++ void sourceErrorBinding();
++ void validBinding();
++ void positionBinding();
++ void activeBinding();
++ void activeBindingAutomated();
++ void startBreaksActiveBinding();
++ void activeBindingBreak();
++
++ void intervalOnSourceDependency();
++ void preferredMethodsOnSourceDependency();
++
++ void updateAfterStart();
++ void startAfterUpdate();
++ void stopAfterUpdate();
++ void startStopAfterUpdate();
++ void updateTimedOut();
++ void updateWithStartTimedOut();
++ void startUpdateStopWithNoIntervals();
++
++ private:
++ std::unique_ptr<QDeclarativePositionSource> m_positionSource = nullptr;
++ };
++
++ void tst_DeclarativePositionSource::init()
++ {
++ // create a fresh instance of QDeclarativePositionSource before each test
++ m_positionSource.reset(new QDeclarativePositionSource);
++ m_positionSource->componentComplete(); // simulate QML loading
++ }
++
++ void tst_DeclarativePositionSource::nameBinding()
++ {
++ m_positionSource->setName("test.source");
++ QTestPrivate::testReadWritePropertyBasics<QDeclarativePositionSource, QString>(
++ *m_positionSource.get(), "invalid source", "test.source", "name");
++ }
++
++ void tst_DeclarativePositionSource::supportedMethodsBinding()
++ {
++ // Invalid source has no positioning methods.
++ // "test.source" has all positioning methods.
++ m_positionSource->setName("invalid name");
++ QTestPrivate::testReadOnlyPropertyBasics<QDeclarativePositionSource,
++ QDeclarativePositionSource::PositioningMethods>(
++ *m_positionSource.get(), QDeclarativePositionSource::NoPositioningMethods,
++ QDeclarativePositionSource::AllPositioningMethods, "supportedPositioningMethods",
++ [&]() { m_positionSource->setName("test.source"); });
++ }
++
++ void tst_DeclarativePositionSource::sourceErrorBinding()
++ {
++ // "dummy.source" has a minimum update interval of 100, and calling
++ // update() with a smaller timeout immediately result in a timeout error
++ m_positionSource->setName("dummy.source");
++ QTestPrivate::testReadOnlyPropertyBasics<QDeclarativePositionSource,
++ QDeclarativePositionSource::SourceError>(
++ *m_positionSource.get(), QDeclarativePositionSource::NoError,
++ QDeclarativePositionSource::UpdateTimeoutError, "sourceError",
++ [&]() { m_positionSource->update(10); });
++ if (QTest::currentTestFailed())
++ return;
++
++ // Test that we can't bind sourceError to smth, as it's read-only
++ QProperty<QDeclarativePositionSource::SourceError> errorSetter;
++ m_positionSource->bindableSourceError().setBinding(Qt::makePropertyBinding(errorSetter));
++ QCOMPARE(m_positionSource->bindableSourceError().hasBinding(), false);
++ }
++
++ void tst_DeclarativePositionSource::validBinding()
++ {
++ // Invalid source name results in no position source -> invalid object.
++ // Setting the name to "test.source" results in creating a new position
++ // source -> valid object
++ m_positionSource->setName("invalid name");
++ QTestPrivate::testReadOnlyPropertyBasics<QDeclarativePositionSource, bool>(
++ *m_positionSource.get(), false, true, "valid",
++ [&]() { m_positionSource->setName("test.source"); });
++ }
++
++ static char *printPosition(const QDeclarativePosition *position)
++ {
++ // For this test we need to print only coordinate, so that we get a nice
++ // error message if the below test fails.
++ return qstrdup(QDebug::toString(position->coordinate()).toLatin1().constData());
++ }
++
++ void tst_DeclarativePositionSource::positionBinding()
++ {
++ // "test.source" udpates its position, starting from (0, 0) coordinate, and
++ // adding 0.1 lat and 0.1 log at every step
++ m_positionSource->setName("test.source");
++ m_positionSource->setUpdateInterval(200);
++ const QGeoCoordinate c1(0.1, 0.1, 0);
++ const QGeoCoordinate c2(0.2, 0.2, 0);
++ m_positionSource->update();
++ QTRY_COMPARE_WITH_TIMEOUT(m_positionSource->position()->coordinate(), c1, 5000);
++
++ QGeoPositionInfo posInfo;
++ posInfo.setCoordinate(c1);
++ QDeclarativePosition pos1;
++ pos1.setPosition(posInfo);
++
++ posInfo.setCoordinate(c2);
++ QDeclarativePosition pos2;
++ pos2.setPosition(posInfo);
++
++ QTestPrivate::testReadOnlyPropertyBasics<QDeclarativePositionSource, QDeclarativePosition *>(
++ *m_positionSource.get(), &pos1, &pos2, "position",
++ [&]() {
++ QSignalSpy spy(m_positionSource.get(),
++ &QDeclarativePositionSource::positionChanged);
++ m_positionSource->update();
++ // Wait for the update to happen. It should take around 200 ms,
++ // but we want to be on a safe side in case of high CI load.
++ QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 5000);
++ },
++ [](const QDeclarativePosition *p1, const QDeclarativePosition *p2) {
++ return p1->coordinate() == p2->coordinate();
++ },
++ printPosition);
++ if (QTest::currentTestFailed())
++ return;
++
++ QProperty<QDeclarativePosition *> prop(nullptr);
++ m_positionSource->bindablePosition().setBinding(Qt::makePropertyBinding(prop));
++ // We can't have bindings on read-only properties
++ QCOMPARE(m_positionSource->bindablePosition().hasBinding(), false);
++ }
++
++ void tst_DeclarativePositionSource::activeBinding()
++ {
++ m_positionSource->setName("test.source");
++
++ QProperty<bool> activeObserver;
++ activeObserver.setBinding(m_positionSource->bindableActive().makeBinding());
++
++ QProperty<bool> activeLambdaObserver;
++ activeLambdaObserver.setBinding([&]() { return m_positionSource->isActive(); });
++
++ QSignalSpy activeSpy(m_positionSource.get(), &QDeclarativePositionSource::activeChanged);
++
++ QCOMPARE(activeSpy.size(), 0);
++ QCOMPARE(activeObserver.value(), false);
++ QCOMPARE(activeLambdaObserver.value(), false);
++
++ QProperty<bool> activeSetter(false);
++ m_positionSource->bindableActive().setBinding(Qt::makePropertyBinding(activeSetter));
++
++ activeSetter = true;
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++
++ QCOMPARE(activeSpy.size(), 1);
++ QCOMPARE(activeObserver.value(), true);
++ QCOMPARE(activeLambdaObserver.value(), true);
++
++ activeSetter = false;
++
++ QCOMPARE(activeSpy.size(), 2);
++ QCOMPARE(activeObserver.value(), false);
++ QCOMPARE(activeLambdaObserver.value(), false);
++
++ // calling update() does not break the binding
++ m_positionSource->update(50);
++ QCOMPARE(activeSpy.size(), 3);
++ QCOMPARE(activeObserver.value(), true);
++ QCOMPARE(activeLambdaObserver.value(), true);
++
++ QTRY_COMPARE_WITH_TIMEOUT(m_positionSource->sourceError(),
++ QDeclarativePositionSource::UpdateTimeoutError, 5000);
++
++ QCOMPARE(activeSpy.size(), 4);
++ QCOMPARE(activeObserver.value(), false);
++ QCOMPARE(activeLambdaObserver.value(), false);
++
++ activeSetter = true;
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++
++ QCOMPARE(activeSpy.size(), 5);
++ QCOMPARE(activeObserver.value(), true);
++ QCOMPARE(activeLambdaObserver.value(), true);
++
++ // calling stop() will break the binding
++ m_positionSource->stop();
++
++ QCOMPARE(activeSpy.size(), 6);
++ QCOMPARE(m_positionSource->isActive(), false);
++ QCOMPARE(activeObserver.value(), false);
++ QCOMPARE(activeLambdaObserver.value(), false);
++
++ activeSetter = false;
++ activeSetter = true;
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++
++ // nothing changed, as the binding is broken
++ QCOMPARE(activeSpy.size(), 6);
++ QCOMPARE(m_positionSource->isActive(), false);
++ QCOMPARE(activeObserver.value(), false);
++ QCOMPARE(activeLambdaObserver.value(), false);
++ }
++
++ void tst_DeclarativePositionSource::activeBindingAutomated()
++ {
++ QTestPrivate::testReadWritePropertyBasics(*m_positionSource.get(), true, false, "active");
++ }
++
++ void tst_DeclarativePositionSource::startBreaksActiveBinding()
++ {
++ m_positionSource->setName("test.source");
++ QSignalSpy activeSpy(m_positionSource.get(), &QDeclarativePositionSource::activeChanged);
++
++ QProperty<bool> activeSetter(true);
++ m_positionSource->bindableActive().setBinding(Qt::makePropertyBinding(activeSetter));
++
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++
++ QCOMPARE(m_positionSource->isActive(), true);
++ QCOMPARE(activeSpy.size(), 1);
++ QVERIFY(m_positionSource->bindableActive().hasBinding());
++
++ // Call start() explicitly, which should break the binding for 'active'
++ // property.
++ m_positionSource->start();
++ activeSetter = false; // this should have no effect
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++
++ QCOMPARE(m_positionSource->isActive(), true);
++ QCOMPARE(activeSpy.size(), 1);
++ QVERIFY(!m_positionSource->bindableActive().hasBinding());
++ }
++
++ void tst_DeclarativePositionSource::activeBindingBreak()
++ {
++ // This test checks a tricky scenario of breaking the binding for
++ // 'active' property.
++ // 0. active has a binding to some property Prop.
++ // 1. Prop is set to true => active = true => start getting position info.
++ // 2. Calling update() for a single update => the binding of active is not
++ // broken.
++ // 3. Calling stop() explicitly before the update() call is finished =>
++ // the active binding is supposed to be broken, but the active state
++ // should still be true, because we have a pending update.
++ // 4. The pending update finishes => the active is set to false.
++
++ m_positionSource->setName("test.source");
++
++ QProperty<bool> setter(false);
++ m_positionSource->bindableActive().setBinding(Qt::makePropertyBinding(setter));
++ QVERIFY(m_positionSource->bindableActive().hasBinding());
++
++ setter = true;
++ QTest::qWait(0); // to trigger singleShot timer in setActive
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++
++ m_positionSource->stop();
++ QVERIFY(!m_positionSource->bindableActive().hasBinding());
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++
++ QCOMPARE(m_positionSource->isActive(), false);
++ QVERIFY(!m_positionSource->bindableActive().hasBinding());
++ }
++
++ void tst_DeclarativePositionSource::intervalOnSourceDependency()
++ {
++ // updateInterval can be modified if the new source does not support that
++ // large one, or implements some calculation
++ m_positionSource->setName("invalid_source"); // reset the source
++
++ QSignalSpy intervalSpy(m_positionSource.get(),
++ &QDeclarativePositionSource::updateIntervalChanged);
++
++ m_positionSource->setUpdateInterval(100);
++ QCOMPARE(m_positionSource->updateInterval(), 100);
++ QCOMPARE(intervalSpy.size(), 1);
++
++ // "test.source" has a minimum update interval of 200
++ m_positionSource->setName("test.source");
++ QCOMPARE(m_positionSource->updateInterval(), 200);
++ QCOMPARE(intervalSpy.size(), 2);
++
++ // "dummy.source" has a minimum update interval of 100, so we expect our
++ // desired interval to be applied
++ m_positionSource->setName("dummy.source");
++ QCOMPARE(m_positionSource->updateInterval(), 100);
++ QCOMPARE(intervalSpy.size(), 3);
++ }
++
++ void tst_DeclarativePositionSource::preferredMethodsOnSourceDependency()
++ {
++ m_positionSource->setName("invalid_source"); // reset the source
++
++ QSignalSpy methodsSpy(m_positionSource.get(),
++ &QDeclarativePositionSource::preferredPositioningMethodsChanged);
++
++ m_positionSource->setPreferredPositioningMethods(
++ QDeclarativePositionSource::SatellitePositioningMethods);
++ QCOMPARE(m_positionSource->preferredPositioningMethods(),
++ QDeclarativePositionSource::SatellitePositioningMethods);
++ QCOMPARE(methodsSpy.size(), 1);
++
++ // "dummy.source" has only non-satellite methods, so they will be used
++ m_positionSource->setName("dummy.source");
++ QCOMPARE(m_positionSource->preferredPositioningMethods(),
++ QDeclarativePositionSource::NonSatellitePositioningMethods);
++ QCOMPARE(methodsSpy.size(), 2);
++
++ // "test.source" has all positioning methods, so satellite will be used,
++ // as we initially wanted
++ m_positionSource->setName("test.source");
++ QCOMPARE(m_positionSource->preferredPositioningMethods(),
++ QDeclarativePositionSource::SatellitePositioningMethods);
++ QCOMPARE(methodsSpy.size(), 3);
++ }
++
++ void tst_DeclarativePositionSource::updateAfterStart()
++ {
++ // When update() is called after start(), it should not invalidate any
++ // state. The active state must still be true when the single update() is
++ // completed.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ m_positionSource->start();
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->stop();
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::startAfterUpdate()
++ {
++ // When start() is called after update(), the position source should remain
++ // active even when the signle update is completed.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->start();
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->stop();
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::stopAfterUpdate()
++ {
++ // When stop() is called after update(), and the update() is still in
++ // progress, the position source should remain active until the update()
++ // is completed.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->stop();
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::startStopAfterUpdate()
++ {
++ // Quite artificial example. Calling start() and stop() after update(),
++ // while still waiting for the update() to complete, should still result in
++ // the position source to be active until the update() is completed.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->start();
++ m_positionSource->stop();
++
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::updateTimedOut()
++ {
++ // This test checks that we reset to inactive state when the single update()
++ // request times out without providing the position info
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ m_positionSource->update(50); // too small timeout -> will return an error
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ QTRY_COMPARE_WITH_TIMEOUT(m_positionSource->sourceError(),
++ QDeclarativePositionSource::UpdateTimeoutError, 5000);
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::updateWithStartTimedOut()
++ {
++ // This test checks that if single update() times out, but the regular
++ // updates are running, we still remain in active state.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ m_positionSource->start();
++
++ m_positionSource->update(50); // too small timeout -> will return an error
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ QTRY_COMPARE_WITH_TIMEOUT(m_positionSource->sourceError(),
++ QDeclarativePositionSource::UpdateTimeoutError, 5000);
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ m_positionSource->stop();
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ void tst_DeclarativePositionSource::startUpdateStopWithNoIntervals()
++ {
++ // This test checks that a sequence of calls start() -> update() -> stop()
++ // without any waits between them will result in expected behavior.
++ // Specifically, the position source should remain active until it gets
++ // the position response.
++
++ m_positionSource->setName("test.source");
++
++ QCOMPARE(m_positionSource->isActive(), false);
++
++ m_positionSource->start();
++ QSignalSpy posSpy(m_positionSource.get(), &QDeclarativePositionSource::positionChanged);
++ QSignalSpy errSpy(m_positionSource.get(), &QDeclarativePositionSource::sourceErrorChanged);
++ m_positionSource->update(210);
++ QCOMPARE(m_positionSource->isActive(), true);
++ m_positionSource->stop();
++ QCOMPARE(m_positionSource->isActive(), true);
++
++ // Wait for the single update to complete. It can be either position update
++ // or timeout.
++ QTRY_VERIFY_WITH_TIMEOUT(posSpy.size() > 0 || errSpy.size() > 0, 5000);
++ QCOMPARE(m_positionSource->isActive(), false);
++ }
++
++ QTEST_MAIN(tst_DeclarativePositionSource)
++
++ #include "tst_qdeclarativepositionsource.moc"