// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include "../../shared/bttestutil_p.h" #include #include #include #include #include #include QT_USE_NAMESPACE // Maximum time to for bluetooth device scan const int MaxScanTime = 5 * 60 * 1000; // 5 minutes in ms class tst_QBluetoothServiceDiscoveryAgent : public QObject { Q_OBJECT public: tst_QBluetoothServiceDiscoveryAgent(); ~tst_QBluetoothServiceDiscoveryAgent(); public slots: void deviceDiscoveryDebug(const QBluetoothDeviceInfo &info); void serviceDiscoveryDebug(const QBluetoothServiceInfo &info); void serviceError(const QBluetoothServiceDiscoveryAgent::Error err); private slots: void initTestCase(); void tst_invalidBtAddress(); void tst_serviceDiscovery_data(); void tst_serviceDiscovery(); void tst_serviceDiscoveryStop(); void tst_serviceDiscoveryAdapters(); private: QList devices; bool localDeviceAvailable; }; tst_QBluetoothServiceDiscoveryAgent::tst_QBluetoothServiceDiscoveryAgent() { QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); if (androidBluetoothEmulator()) return; // start Bluetooth if not started #ifndef Q_OS_MACOS QBluetoothLocalDevice *device = new QBluetoothLocalDevice(); localDeviceAvailable = device->isValid(); if (localDeviceAvailable) { device->powerOn(); // wait for the device to switch bluetooth mode. QTest::qWait(1000); } delete device; #else QBluetoothLocalDevice device; localDeviceAvailable = QBluetoothLocalDevice().hostMode() != QBluetoothLocalDevice::HostPoweredOff; #endif qRegisterMetaType(); qRegisterMetaType >(); qRegisterMetaType(); qRegisterMetaType(); } tst_QBluetoothServiceDiscoveryAgent::~tst_QBluetoothServiceDiscoveryAgent() { } void tst_QBluetoothServiceDiscoveryAgent::deviceDiscoveryDebug(const QBluetoothDeviceInfo &info) { qDebug() << "Discovered device:" << info.address().toString() << info.name(); } void tst_QBluetoothServiceDiscoveryAgent::serviceError(const QBluetoothServiceDiscoveryAgent::Error err) { qDebug() << "Service discovery error" << err; } void tst_QBluetoothServiceDiscoveryAgent::initTestCase() { if (androidBluetoothEmulator()) QSKIP("Skipping test on Android 12+ emulator, CI can timeout waiting for user input"); if (localDeviceAvailable) { QBluetoothDeviceDiscoveryAgent discoveryAgent; QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished())); QSignalSpy errorSpy(&discoveryAgent, SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error))); QSignalSpy discoveredSpy(&discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo))); discoveryAgent.start(QBluetoothDeviceDiscoveryAgent::ClassicMethod); // Wait for up to MaxScanTime for the scan to finish int scanTime = MaxScanTime; while (finishedSpy.isEmpty() && scanTime > 0) { QTest::qWait(1000); scanTime -= 1000; } // Expect finished signal with no error QVERIFY(finishedSpy.size() == 1); QVERIFY(errorSpy.isEmpty()); devices = discoveryAgent.discoveredDevices(); } } void tst_QBluetoothServiceDiscoveryAgent::tst_serviceDiscoveryStop() { if (!localDeviceAvailable) QSKIP("This test requires Bluetooth adapter in powered ON state"); QBluetoothServiceDiscoveryAgent discoveryAgent; QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished())); QSignalSpy canceledSpy(&discoveryAgent, SIGNAL(canceled())); // Verify we get the correct signals on start-stop discoveryAgent.start(QBluetoothServiceDiscoveryAgent::FullDiscovery); QVERIFY(discoveryAgent.isActive()); discoveryAgent.stop(); QTRY_COMPARE(canceledSpy.size(), 1); QVERIFY(!discoveryAgent.isActive()); // Wait a bit to see that there are no latent signals QTest::qWait(200); QCOMPARE(canceledSpy.size(), 1); QCOMPARE(finishedSpy.size(), 0); } void tst_QBluetoothServiceDiscoveryAgent::tst_invalidBtAddress() { #ifdef Q_OS_MACOS if (!localDeviceAvailable) QSKIP("On OS X this test requires Bluetooth adapter in powered ON state"); #endif QBluetoothServiceDiscoveryAgent *discoveryAgent = new QBluetoothServiceDiscoveryAgent(QBluetoothAddress("11:11:11:11:11:11")); QCOMPARE(discoveryAgent->error(), QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError); discoveryAgent->start(); QCOMPARE(discoveryAgent->isActive(), false); delete discoveryAgent; discoveryAgent = new QBluetoothServiceDiscoveryAgent(QBluetoothAddress()); QCOMPARE(discoveryAgent->error(), QBluetoothServiceDiscoveryAgent::NoError); if (!QBluetoothLocalDevice::allDevices().isEmpty()) { discoveryAgent->start(); QCOMPARE(discoveryAgent->isActive(), true); } delete discoveryAgent; } void tst_QBluetoothServiceDiscoveryAgent::serviceDiscoveryDebug(const QBluetoothServiceInfo &info) { qDebug() << "Discovered service on" << info.device().name() << info.device().address().toString(); qDebug() << "\tService name:" << info.serviceName() << "cached" << info.device().isCached(); qDebug() << "\tDescription:" << info.attribute(QBluetoothServiceInfo::ServiceDescription).toString(); qDebug() << "\tProvider:" << info.attribute(QBluetoothServiceInfo::ServiceProvider).toString(); qDebug() << "\tL2CAP protocol service multiplexer:" << info.protocolServiceMultiplexer(); qDebug() << "\tRFCOMM server channel:" << info.serverChannel(); } #if 0 static void dumpAttributeVariant(const QVariant &var, const QString indent) { if (!var.isValid()) { qDebug("%sEmpty", indent.toLocal8Bit().constData()); return; } if (var.typeId() == qMetaTypeId()) { qDebug("%sSequence", indent.toLocal8Bit().constData()); const QBluetoothServiceInfo::Sequence *sequence = static_cast(var.data()); for (const QVariant &v : *sequence) dumpAttributeVariant(v, indent + '\t'); } else if (var.typeId() == qMetaTypeId()) { qDebug("%sAlternative", indent.toLocal8Bit().constData()); const QBluetoothServiceInfo::Alternative *alternative = static_cast(var.data()); for (const QVariant &v : *alternative) dumpAttributeVariant(v, indent + '\t'); } else if (var.typeId() == qMetaTypeId()) { QBluetoothUuid uuid = var.value(); switch (uuid.minimumSize()) { case 0: qDebug("%suuid NULL", indent.toLocal8Bit().constData()); break; case 2: qDebug("%suuid %04x", indent.toLocal8Bit().constData(), uuid.toUInt16()); break; case 4: qDebug("%suuid %08x", indent.toLocal8Bit().constData(), uuid.toUInt32()); break; case 16: { qDebug("%suuid %s", indent.toLocal8Bit().constData(), uuid.toByteArray(QUuid::Id128).constData()); break; } default: qDebug("%suuid ???", indent.toLocal8Bit().constData()); } } else { switch (var.typeId()) { case QMetaType::UInt: qDebug("%suint %u", indent.toLocal8Bit().constData(), var.toUInt()); break; case QMetaType::Int: qDebug("%sint %d", indent.toLocal8Bit().constData(), var.toInt()); break; case QMetaType::QString: qDebug("%sstring %s", indent.toLocal8Bit().constData(), var.toString().toLocal8Bit().constData()); break; case QMetaType::Bool: qDebug("%sbool %d", indent.toLocal8Bit().constData(), var.toBool()); break; case QMetaType::QUrl: qDebug("%surl %s", indent.toLocal8Bit().constData(), var.toUrl().toString().toLocal8Bit().constData()); break; default: qDebug("%sunknown", indent.toLocal8Bit().constData()); } } } static inline void dumpServiceInfoAttributes(const QBluetoothServiceInfo &info) { const QList attributes = info.attributes(); for (quint16 id : attributes) { dumpAttributeVariant(info.attribute(id), QString("\t")); } } #endif void tst_QBluetoothServiceDiscoveryAgent::tst_serviceDiscovery_data() { if (devices.isEmpty()) QSKIP("This test requires an in-range bluetooth device"); QTest::addColumn("deviceInfo"); QTest::addColumn >("uuidFilter"); QTest::addColumn("serviceDiscoveryError"); // Only need to test the first 5 live devices int max = 5; for (const QBluetoothDeviceInfo &info : std::as_const(devices)) { if (info.isCached()) continue; QTest::newRow("default filter") << info << QList() << QBluetoothServiceDiscoveryAgent::NoError; if (!--max) break; //QTest::newRow("public browse group") << info << (QList() << QBluetoothUuid::ServiceClassUuid::PublicBrowseGroup); //QTest::newRow("l2cap") << info << (QList() << QBluetoothUuid::ProtocolUuid::L2cap); } QTest::newRow("all devices") << QBluetoothDeviceInfo() << QList() << QBluetoothServiceDiscoveryAgent::NoError; } void tst_QBluetoothServiceDiscoveryAgent::tst_serviceDiscoveryAdapters() { QBluetoothLocalDevice localDevice; const QList adapters = localDevice.allDevices(); if (adapters.size() > 1) { if (devices.isEmpty()) QSKIP("This test requires an in-range bluetooth device"); QVarLengthArray addresses; for (const auto &adapter : adapters) { addresses.append(adapter.address()); } QBluetoothServer server(QBluetoothServiceInfo::RfcommProtocol); QBluetoothUuid uuid(QBluetoothUuid::ProtocolUuid::Ftp); server.listen(addresses[0]); QBluetoothServiceInfo serviceInfo; serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceName, "serviceName"); QBluetoothServiceInfo::Sequence publicBrowse; publicBrowse << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::PublicBrowseGroup)); serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList, publicBrowse); QBluetoothServiceInfo::Sequence profileSequence; QBluetoothServiceInfo::Sequence classId; classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort)); classId << QVariant::fromValue(quint16(0x100)); profileSequence.append(QVariant::fromValue(classId)); serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, profileSequence); serviceInfo.setServiceUuid(uuid); QBluetoothServiceInfo::Sequence protocolDescriptorList; QBluetoothServiceInfo::Sequence protocol; protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap)); protocolDescriptorList.append(QVariant::fromValue(protocol)); protocol.clear(); protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Rfcomm)) << QVariant::fromValue(quint8(server.serverPort())); protocolDescriptorList.append(QVariant::fromValue(protocol)); serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); QVERIFY(serviceInfo.registerService()); QVERIFY(server.isListening()); qDebug() << "Scanning address " << addresses[0].toString(); QBluetoothServiceDiscoveryAgent discoveryAgent(addresses[1]); bool setAddress = discoveryAgent.setRemoteAddress(addresses[0]); QVERIFY(setAddress); QVERIFY(!discoveryAgent.isActive()); QVERIFY(discoveryAgent.discoveredServices().isEmpty()); QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished())); discoveryAgent.start(); int scanTime = MaxScanTime; while (finishedSpy.isEmpty() && scanTime > 0) { QTest::qWait(1000); scanTime -= 1000; } const QList discServices = discoveryAgent.discoveredServices(); QVERIFY(!discServices.empty()); qsizetype counter = 0; for (const QBluetoothServiceInfo &service : discServices) { if (uuid == service.serviceUuid()) counter++; } QCOMPARE(counter, 1); } } void tst_QBluetoothServiceDiscoveryAgent::tst_serviceDiscovery() { if (devices.isEmpty()) QSKIP("This test requires an in-range bluetooth device"); QFETCH(QBluetoothDeviceInfo, deviceInfo); QFETCH(QList, uuidFilter); QFETCH(QBluetoothServiceDiscoveryAgent::Error, serviceDiscoveryError); QBluetoothLocalDevice localDevice; qDebug() << "Scanning address" << deviceInfo.address().toString() << deviceInfo.name(); QBluetoothServiceDiscoveryAgent discoveryAgent(localDevice.address()); bool setAddress = discoveryAgent.setRemoteAddress(deviceInfo.address()); QVERIFY(setAddress); QVERIFY(!discoveryAgent.isActive()); QVERIFY(discoveryAgent.discoveredServices().isEmpty()); QVERIFY(discoveryAgent.uuidFilter().isEmpty()); discoveryAgent.setUuidFilter(uuidFilter); QVERIFY(discoveryAgent.uuidFilter() == uuidFilter); QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished())); QSignalSpy errorSpy(&discoveryAgent, SIGNAL(errorOccurred(QBluetoothServiceDiscoveryAgent::Error))); QSignalSpy discoveredSpy(&discoveryAgent, SIGNAL(serviceDiscovered(QBluetoothServiceInfo))); connect(&discoveryAgent, SIGNAL(errorOccurred(QBluetoothServiceDiscoveryAgent::Error)), this, SLOT(serviceError(QBluetoothServiceDiscoveryAgent::Error))); discoveryAgent.start(QBluetoothServiceDiscoveryAgent::FullDiscovery); /* * Either we wait for discovery agent to run its course (e.g. Bluez 4) or * we have an immediate result (e.g. Bluez 5) */ QVERIFY(discoveryAgent.isActive() || !finishedSpy.isEmpty()); // Wait for up to MaxScanTime for the scan to finish int scanTime = 20000; while (finishedSpy.isEmpty() && scanTime > 0) { QTest::qWait(1000); scanTime -= 1000; } if (discoveryAgent.error()) { qDebug() << "Device failed to respond to SDP, skipping device" << discoveryAgent.error() << discoveryAgent.errorString(); return; } QVERIFY(discoveryAgent.error() == serviceDiscoveryError); QVERIFY(discoveryAgent.errorString() == QString()); // Expect finished signal with no error if (scanTime) QVERIFY(finishedSpy.size() == 1); QVERIFY(errorSpy.isEmpty()); //if (!discoveryAgent.discoveredServices().isEmpty() && expected_failures++ <2){ if (discoveredSpy.isEmpty()) { qDebug() << "Device failed to return any results, skipping device" << discoveryAgent.discoveredServices().size(); return; } // All returned QBluetoothServiceInfo should be valid. bool servicesFound = !discoveredSpy.isEmpty(); while (!discoveredSpy.isEmpty()) { const QVariant v = discoveredSpy.takeFirst().at(0); // Work around limitation in QMetaType and moc. // QBluetoothServiceInfo is registered with metatype as QBluetoothServiceInfo // moc sees it as the unqualified QBluetoothServiceInfo. if (v.userType() == qMetaTypeId()) { const QBluetoothServiceInfo info = *reinterpret_cast(v.constData()); QVERIFY(info.isValid()); QVERIFY(!info.isRegistered()); #if 0 qDebug() << info.device().name() << info.device().address().toString(); qDebug() << "\tService name:" << info.serviceName(); if (info.protocolServiceMultiplexer() >= 0) qDebug() << "\tL2CAP protocol service multiplexer:" << info.protocolServiceMultiplexer(); if (info.serverChannel() >= 0) qDebug() << "\tRFCOMM server channel:" << info.serverChannel(); //dumpServiceInfoAttributes(info); #endif } else { QFAIL("Unknown type returned by service discovery"); } } if (servicesFound) QVERIFY(!discoveryAgent.discoveredServices().isEmpty()); discoveryAgent.clear(); QVERIFY(discoveryAgent.discoveredServices().isEmpty()); discoveryAgent.stop(); QVERIFY(!discoveryAgent.isActive()); } QTEST_MAIN(tst_QBluetoothServiceDiscoveryAgent) #include "tst_qbluetoothservicediscoveryagent.moc"