From 709a68aae7453cf1d5bc9c0597bfd0592abb3879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Han=C3=A7erli?= <burak.hancerli@qt.io> Date: Wed, 9 Oct 2024 06:41:31 +0000 Subject: [PATCH] QDS-13458 New Android UI and backend --- 3rdparty/qtquickdesigner-components | 2 +- CMakeLists.txt | 6 +- cicd/gitlab-ci.yml | 2 +- src/CMakeLists.txt | 2 - src/HomePage.qml | 10 +- src/SettingsPage.qml | 1 + src/backend/backend.cpp | 370 +++--------------- src/backend/backend.h | 49 +-- src/backend/constants.h | 44 --- src/backend/dsconnector/ds.cpp | 61 +-- src/backend/dsconnector/ds.h | 33 +- src/backend/dsconnector/dsdiscovery.cpp | 1 - src/backend/dsconnector/dsdiscovery.h | 5 +- src/backend/dsconnector/dsmanager.cpp | 262 +++---------- src/backend/dsconnector/dsmanager.h | 37 +- src/backend/logger.h | 1 - src/backend/main.cpp | 2 +- src/backend/projectmanager.cpp | 259 +----------- src/backend/projectmanager.h | 22 +- src/backend/qrscanner.cpp | 1 - src/backend/serviceconnector.cpp | 126 ------ src/backend/serviceconnector.h | 55 --- src/backend/settings.cpp | 67 ++-- src/backend/settings.h | 9 +- tests/CMakeLists.txt | 4 +- tests/main.cpp | 6 +- tests/tst_designstudio.cpp | 135 +++---- tests/tst_designstudio.h | 12 +- tests/tst_dsmanager.cpp | 59 --- tests/tst_projectmanager.cpp | 147 ------- tests/tst_projectmanager.h | 56 --- tests/tst_serviceconnector.cpp | 68 ---- ...st_serviceconnector.h => tst_settings.cpp} | 31 +- tests/{tst_dsmanager.h => tst_settings.h} | 13 +- 34 files changed, 300 insertions(+), 1658 deletions(-) delete mode 100644 src/backend/constants.h delete mode 100644 src/backend/serviceconnector.cpp delete mode 100644 src/backend/serviceconnector.h delete mode 100644 tests/tst_dsmanager.cpp delete mode 100644 tests/tst_projectmanager.cpp delete mode 100644 tests/tst_projectmanager.h delete mode 100644 tests/tst_serviceconnector.cpp rename tests/{tst_serviceconnector.h => tst_settings.cpp} (71%) rename tests/{tst_dsmanager.h => tst_settings.h} (81%) diff --git a/3rdparty/qtquickdesigner-components b/3rdparty/qtquickdesigner-components index 80b9e31..20397e2 160000 --- a/3rdparty/qtquickdesigner-components +++ b/3rdparty/qtquickdesigner-components @@ -1 +1 @@ -Subproject commit 80b9e31a8863c9bf2a57c3d569b761d9df71cdb3 +Subproject commit 20397e26370ff073125fd19f77c7ad013276b5bd diff --git a/CMakeLists.txt b/CMakeLists.txt index 976d49c..2d1ca30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,6 @@ project(qtuiviewer LANGUAGES CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) -#set(CMAKE_AUTOUIC ON) -#set(CMAKE_AUTOMOC ON) -#set(CMAKE_AUTORCC ON) - set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -25,7 +21,7 @@ find_package( find_package(Qt6 REQUIRED COMPONENTS Core) qt_policy(SET QTP0002 NEW) -set(QT_MINIMUM_VERSION 6.7.0) +set(QT_MINIMUM_VERSION 6.7.3) if(QT_VERSION VERSION_LESS QT_MINIMUM_VERSION) message(FATAL_ERROR "Minimum supported Qt version: ${QT_MINIMUM_VERSION}") endif() diff --git a/cicd/gitlab-ci.yml b/cicd/gitlab-ci.yml index 9a73acd..293b389 100644 --- a/cicd/gitlab-ci.yml +++ b/cicd/gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - QDS_CI_QT_VERSION: "6.7.0" + QDS_CI_QT_VERSION: "6.7.3" QDS_CI_ARTIFACTS_PATH: "${CI_PROJECT_DIR}/artifacts" DEBIAN_FRONTEND: non-interactive GIT_SUBMODULE_STRATEGY: recursive diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e1c917d..e071432 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,7 +11,6 @@ qt_add_executable(${PROJECT_NAME} backend/main.cpp backend/logger.h backend/backend.cpp backend/backend.h - backend/serviceconnector.cpp backend/serviceconnector.h backend/projectmanager.cpp backend/projectmanager.h backend/settings.cpp backend/settings.h backend/dsconnector/ds.cpp backend/dsconnector/ds.h @@ -60,7 +59,6 @@ target_link_libraries(${PROJECT_NAME} PRIVATE qt_add_library(qtuiviewerlib OBJECT EXCLUDE_FROM_ALL backend/projectmanager.cpp backend/projectmanager.h - backend/serviceconnector.cpp backend/serviceconnector.h backend/settings.cpp backend/settings.h backend/dsconnector/ds.cpp backend/dsconnector/ds.h backend/dsconnector/dsdiscovery.cpp backend/dsconnector/dsdiscovery.h diff --git a/src/HomePage.qml b/src/HomePage.qml index 2f21771..d6e328b 100644 --- a/src/HomePage.qml +++ b/src/HomePage.qml @@ -225,10 +225,18 @@ Flickable { Layout.alignment: Qt.AlignHCenter placeholderText: qsTr("IP Address") - text: "10.0.2.2" + text: backend.lastDesignStudioIp() validator: RegularExpressionValidator { regularExpression: /^(\d{1,3}\.){3}\d{1,3}$/ } + + Connections { + target: backend + + function onConnectedChanged(isConnected, ip) { + ipAddress.text = ip + } + } } Button { diff --git a/src/SettingsPage.qml b/src/SettingsPage.qml index c05c131..ef97860 100644 --- a/src/SettingsPage.qml +++ b/src/SettingsPage.qml @@ -31,6 +31,7 @@ Flickable { text: qsTr("Auto-scale") subText: qsTr("Scales the project to fit to current display and orientation") checked: backend.autoScaleProject() ? Qt.Checked : Qt.Unchecked + onToggled: backend.setAutoScaleProject(checked) } SettingsItem { diff --git a/src/backend/backend.cpp b/src/backend/backend.cpp index 7d0c602..2b87e24 100644 --- a/src/backend/backend.cpp +++ b/src/backend/backend.cpp @@ -26,15 +26,6 @@ #include "backend.h" #include <QDesktopServices> -#include <QEventLoop> -#include <QFileInfo> -#include <QGuiApplication> -#include <QJniObject> -#include <QJsonArray> -#include <QJsonObject> -#include <QSettings> -#include <QSysInfo> -#include <QTimer> #include "logger.h" @@ -50,32 +41,15 @@ Backend::Backend(QObject *parent) QDesktopServices::setUrlHandler("qtdesignstudio", this, "parseDesignViewerUrl"); QDesktopServices::setUrlHandler("https", this, "parseDesignViewerUrl"); - // Initialize background update - connect(&m_projectListUpdateTimer, &QTimer::timeout, this, &Backend::updateUserProjectList); - m_projectListUpdateTimer.setInterval(1000 * 10); - m_projectListUpdateTimer.start(); - updateUserProjectList(); - initDesignStudioManager(); - - connect(qApp, - &QGuiApplication::applicationStateChanged, - this, - [this](Qt::ApplicationState state) { - qDebug() << "Application state changed to:" << state; - if (state == Qt::ApplicationState::ApplicationActive) { - m_dsConnectorThread.start(); - m_projectListUpdateTimer.start(); - } else if (state == Qt::ApplicationState::ApplicationSuspended) { - m_dsConnectorThread.quit(); - m_projectListUpdateTimer.stop(); - } - }); + m_dsManagerThread.setParent(this); + connect(&m_dsManagerThread, &QThread::started, this, &Backend::initDsManager); + m_dsManagerThread.start(); connect(&Logger::instance(), &Logger::logMessage, this, [this](QtMsgType type, QString &msg) { // if we have any active project running, then reroute // all the logs to the dsmanager with the last project sender id if (m_projectManager) { - QMetaObject::invokeMethod(m_designStudioManager.get(), + QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectLogs", Qt::QueuedConnection, Q_ARG(QString, m_lastProjectSenderId), @@ -104,8 +78,12 @@ Backend::Backend(QObject *parent) qDebug() << "-- Build CPU architecture: " << QSysInfo::buildCpuArchitecture(); qDebug() << "-- Device serial: " << getDeviceSerial(); qDebug() << "-- Device unique ID: " << getDeviceUuid(); +} - qDebug() << "Thread id backend:" << QThread::currentThreadId(); +Backend::~Backend() +{ + m_dsManagerThread.quit(); + m_dsManagerThread.wait(); } // this function returns the device serial number (which is really unique) @@ -161,7 +139,7 @@ void Backend::initializeProjectManager() [&] { emit popupClose(); m_projectManager.reset(); - QMetaObject::invokeMethod(m_designStudioManager.get(), + QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStopped", Qt::QueuedConnection, Q_ARG(QString, m_lastProjectSenderId)); @@ -169,82 +147,57 @@ void Backend::initializeProjectManager() Qt::QueuedConnection); } -void Backend::initDesignStudioManager() +void Backend::initDsManager() { - connect(&m_dsConnectorThread, &QThread::started, [this] { - qDebug() << "Design Studio Manager thread started"; - m_designStudioManager.reset(new DesignStudioManager(nullptr, false, getDeviceUuid())); - - // signals goes from DS Manager to UI - connect(m_designStudioManager.get(), - &DesignStudioManager::pairingPinRequested, - this, - &Backend::pinRequested); - - connect(m_designStudioManager.get(), - &DesignStudioManager::projectReceived, - this, - &Backend::runDsProject); - - connect(m_designStudioManager.get(), - &DesignStudioManager::designStudioConnected, - this, - [this](const QString &id, const QString &ipAddr) { - emit connectedChanged(true, ipAddr); - }); - connect(m_designStudioManager.get(), - &DesignStudioManager::designStudioDisconnected, - this, - [this](const QString &id, const QString &ipAddr) { - if (id == m_lastProjectSenderId && m_projectManager) { - m_projectManager->stopProject(); - } - emit connectedChanged(false, ipAddr); - }); - - connect(m_designStudioManager.get(), - &DesignStudioManager::projectStopRequested, - this, - [this](const QString &id) { - if (m_projectManager) { - m_projectManager->stopProject(); - } - }); - - // signals goes from UI to DS Manager - connect(this, - &Backend::enterPin, - m_designStudioManager.get(), - &DesignStudioManager::enterPin); - }); + qDebug() << "Design Studio Manager thread started. Initializing Design Studio Manager"; + m_dsManager.reset(new DesignStudioManager(nullptr, false, getDeviceUuid())); - connect(&m_dsConnectorThread, &QThread::finished, this, [this] { - qDebug() << "Design Studio Manager thread finished"; - m_designStudioManager.reset(); - }); + connect(m_dsManager.get(), &DesignStudioManager::projectReceived, this, &Backend::runProject); - connect(&m_serviceConnector, - &ServiceConnector::downloadProgress, + connect(m_dsManager.get(), + &DesignStudioManager::designStudioConnected, this, - &Backend::downloadProgress); + [this](const QString &id, const QString &ipAddr) { + emit connectedChanged(true, ipAddr); + }); + connect(m_dsManager.get(), + &DesignStudioManager::designStudioDisconnected, + this, + [this](const QString &id, const QString &ipAddr) { + if (id == m_lastProjectSenderId && m_projectManager) { + m_projectManager->stopProject(); + } + emit connectedChanged(false, ipAddr); + }); + + connect(m_dsManager.get(), + &DesignStudioManager::projectStopRequested, + this, + [this](const QString &id) { + if (m_projectManager) { + m_projectManager->stopProject(); + } + }); + + m_dsManager->initialize(); + qDebug() << "Design Studio Manager initialized"; } void Backend::connectDesignStudio(const QString &ipAddr) { - QMetaObject::invokeMethod(m_designStudioManager.get(), - "designStudioFound", + QMetaObject::invokeMethod(m_dsManager.get(), + "initDesignStudio", Qt::QueuedConnection, Q_ARG(QString, ipAddr), - Q_ARG(QString, ""), - Q_ARG(bool, true)); + Q_ARG(QString, "")); emit popupOpen("Connecting in the background...", 1500); } -void Backend::runDsProject(const QString &id, const QByteArray &projectData) +void Backend::runProject(const QString &id, const QByteArray &projectData) { // we'll use this to notify the correct DS when the project started/stopped m_lastProjectSenderId = id; - QMetaObject::invokeMethod(m_designStudioManager.get(), + QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectRunning", Qt::QueuedConnection, Q_ARG(QString, id)); @@ -257,7 +210,7 @@ void Backend::runDsProject(const QString &id, const QByteArray &projectData) if (!m_projectManager->runProject(projectPath)) { qCritical() << "Could not run project. Please check the logs for more information."; - QMetaObject::invokeMethod(m_designStudioManager.get(), + QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStopped", Qt::QueuedConnection, Q_ARG(QString, id)); @@ -268,206 +221,6 @@ void Backend::runDsProject(const QString &id, const QByteArray &projectData) emit popupClose(); } -void Backend::runDemoProject(const QString &projectName) -{ - initializeProjectManager(); - qDebug() << "Checking if demo project is cached for " << projectName; - emit popupOpen(); - - // sample project info - // [{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}] - const std::optional<QJsonArray> projectList = m_serviceConnector.fetchDemoList(); - - if (projectList == std::nullopt) { - qCritical() - << "Could not fetch demo project list. Please check your internet connection and " - "try again."; - emit popupClose(); - return; - } - - QJsonObject projectInfo; - for (auto project : projectList.value()) { - if (projectName == project.toObject().value("name").toString().remove(".qmlrc")) { - projectInfo = project.toObject(); - break; - } - } - - const bool cached = m_projectManager->isDemoProjectCached(projectInfo); - - if (!cached) { - updatePopup("Downloading demo project...", false); - - const std::optional<QByteArray> project = m_serviceConnector.fetchDemo(projectName); - - if (project == std::nullopt) { - qCritical() << "Could not download demo project. Please check the logs for more " - "information."; - emit popupClose(); - return; - } - - updatePopup("Caching demo project..."); - if (!m_projectManager->cacheDemoProject(project.value(), projectInfo)) { - qCritical() - << "Could not cache demo project. Please check the logs for more information."; - emit popupClose(); - return; - } - } else { - qDebug() << "Demo project is cached. Running cached project..."; - } - - updatePopup("Running demo project..."); - if (!m_projectManager->runDemoProject(projectName)) - qCritical() << "Could not run demo project. Please check the logs for more information."; - else { - updatePopup("Showing the app window..."); - m_projectManager->showAppWindow(); - } - - emit popupClose(); -} - -void Backend::clearDemoCaches() -{ - emit popupOpen(); - updatePopup("Clearing demo caches..."); - ProjectManager().clearDemoCaches(); - emit popupClose(); -} - -void Backend::runUserProject(const QString &projectName, const QString &password) -{ - initializeProjectManager(); - updatePopup("Running user project"); - emit popupOpen(); - - qDebug() << "Running user project:" << projectName; - - // fetch the project list to check if the project is cached - const std::optional<QJsonArray> projectList = m_serviceConnector.fetchUserProjectList( - m_settings.userHash()); - - if (projectList == std::nullopt) { - qCritical() - << "Could not fetch user project list. Please check your internet connection and " - "try again."; - emit popupClose(); - return; - } - - QJsonObject projectInfo; - for (const auto &project : projectList.value()) { - if (projectName == project.toObject().value("appName").toString()) { - projectInfo = project.toObject(); - break; - } - } - - const bool projectCached = m_projectManager->isProjectCached(projectInfo); - - if (!projectCached) { - qDebug("Project is not cached. Downloading..."); - updatePopup("Project is not cached. Downloading...", false); - const std::optional<QByteArray> projectData - = m_serviceConnector.fetchUserProject(m_settings.userHash(), projectName, password); - - if (projectData == std::nullopt) { - qCritical() - << "Could not download project. Please check the logs for more information."; - emit popupClose(); - return; - } - - updatePopup("Caching user project..."); - if (!m_projectManager->cacheProject(projectData.value(), projectInfo)) { - qCritical() << "Could not cache project. Please check the logs for more information."; - emit popupClose(); - return; - } - } - - qDebug("Project is cached. Running cached project..."); - updatePopup("Running cached project..."); - if (!m_projectManager->runCachedProject(projectInfo)) - qCritical() << "Could not run project. Please check the logs for more information."; - else { - updatePopup("Showing the app window..."); - m_projectManager->showAppWindow(); - } - - emit popupClose(); -} - -void Backend::runOnlineProject(const QString &url) -{ - initializeProjectManager(); - emit popupOpen(); - updatePopup("Downloading...", false); - const std::optional<QByteArray> projectData = m_serviceConnector.fetchProject(url); - - if (projectData == std::nullopt) { - qCritical() << "Could not download project. Please check the logs for more information."; - emit popupClose(); - return; - } - - updatePopup("Unpacking project..."); - QString projectPath = m_projectManager->unpackProject(projectData.value()); - updatePopup("Running project..."); - - if (!m_projectManager->runProject(projectPath)) - qCritical() << "Could not run project. Please check the logs for more information."; - else { - m_projectManager->showAppWindow(); - } - - emit popupClose(); -} - -void Backend::updateUserProjectList() -{ - const QString userHash = m_settings.userHash(); - - if (userHash.isEmpty()) { - return; - } - - qDebug() << "Fetching available project list for user:" << userHash; - const std::optional<QJsonArray> projectList = m_serviceConnector.fetchUserProjectList(userHash); - - if (projectList == std::nullopt) { - qWarning( - "Could not fetch project list. Please check your internet connection and try again."); - } else if (projectList.value() == m_projectList) { - qDebug("No new projects are available"); - } else { - qDebug("List of available projects fetched:"); - for (const auto &project : projectList.value()) { - const QString projectName{project.toObject().value("appName").toString()}; - qDebug() << "--" << projectName; - } - - // check if any project is removed on the cloud - for (const auto &project : m_projectList) { - const QString projectName{project.toObject().value("appName").toString()}; - if (!projectList.value().contains(project)) { - qDebug() << "Project removed:" << projectName << ". Removing from cache..."; - // remove the project from the cache - ProjectManager().clearCachedProject(project.toObject()); - } - } - } - - // we need to set m_projectList even if it is empty - // because this triggers the onModelChanged function in the QML - // in order to update the UI - m_projectList = projectList.value(); - emit projectListChanged(); -} - void Backend::scanQrCode() { m_qrScanner.reset(new QrScanner); @@ -481,18 +234,7 @@ void Backend::scanQrCode() void Backend::parseDesignViewerUrl(const QUrl &url) { - // url format could be one of the following: - // - https://<url>/<project_name>.qmlrc - // - qtdesignviewer://<user_hash> - // - qtdesignstudio://<ipv4_address>?<design_studio_id> - - if (url.scheme() == "https") { - emit urlUpdated(url.toString()); - } else if (url.scheme() == "qtdesignviewer") { - qDebug() << "Registering user from QR code"; - m_settings.setUserHash(url.host()); - updateUserProjectList(); - } else if (url.scheme() == "qtdesignstudio") { + if (url.scheme() == "qtdesignstudio") { qDebug() << "Connecting to Design Studio from QR code"; if (url.host().isEmpty()) { @@ -506,7 +248,7 @@ void Backend::parseDesignViewerUrl(const QUrl &url) const QString ipv4Addr = url.host(); const QString designStudioId = url.query(); - QMetaObject::invokeMethod(m_designStudioManager.get(), + QMetaObject::invokeMethod(m_dsManager.get(), "designStudioFound", Qt::QueuedConnection, Q_ARG(QString, ipv4Addr), @@ -520,5 +262,19 @@ void Backend::parseDesignViewerUrl(const QUrl &url) void Backend::popupInterrupted() { qDebug() << "Popup closed prematurely. Interrupting active downloads (if any)"; - QMetaObject::invokeMethod(&m_serviceConnector, "interrupted", Qt::QueuedConnection); +} + +bool Backend::autoScaleProject() const +{ + return m_settings.autoScaleProject(); +} + +void Backend::setAutoScaleProject(bool autoScaleProject) +{ + m_settings.setAutoScaleProject(autoScaleProject); +} + +QString Backend::lastDesignStudioIp() const +{ + return m_dsManager ? m_dsManager->getDesignStudioIp({}) : QString(); } diff --git a/src/backend/backend.h b/src/backend/backend.h index 1351df8..4a24e3f 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -27,42 +27,30 @@ #define DV_ANDROID_H #include <QThread> -#include <QTimer> - -#include <memory> #include "dsconnector/dsmanager.h" #include "projectmanager.h" #include "qrscanner.h" -#include "serviceconnector.h" #include "settings.h" class Backend : public QObject { Q_OBJECT - Q_PROPERTY(QJsonArray projectList READ projectList NOTIFY projectListChanged) - public: explicit Backend(QObject *parent = nullptr); - - QJsonArray projectList() const { return m_projectList; } + ~Backend(); private: - // UI data - QJsonArray m_projectList; - // Other members - ServiceConnector m_serviceConnector; std::unique_ptr<ProjectManager> m_projectManager; std::unique_ptr<QrScanner> m_qrScanner; // DS Connector - QThread m_dsConnectorThread; - std::unique_ptr<DesignStudioManager> m_designStudioManager; + QScopedPointer<DesignStudioManager> m_dsManager; + QThread m_dsManagerThread; QString m_lastProjectSenderId; // Settings - QTimer m_projectListUpdateTimer; Settings m_settings; // member functions @@ -70,48 +58,35 @@ private: QByteArray getDeviceSerial(); QString getDeviceUuid(); -signals: - // UI signals - Home page - void projectListChanged(); - void urlUpdated(QString); - void userHashChanged(); + void initDsManager(); + void initializeProjectManager(); + + void runProject(const QString &id, const QByteArray &projectData); +signals: // UI signals - Popup - void downloadProgress(float); void popupProgressIndeterminateChanged(bool indeterminate); void popupTextChanged(QString text); void popupOpen(const QString &text = {}, int timeout = 0); void popupClose(); // UI signals - from DS Manager page - void pinRequested(const QString &id); - void pinPopupOpen(); void connectedChanged(bool connected, const QString &ipAddr); // UI signals - from UI - void enterPin(const QString &deviceId, const QString &pin); void connectToDesignStudio(const QString &url); public slots: QString buildInfo() const; void scanQrCode(); - - void runOnlineProject(const QString &url); - void runUserProject(const QString &projectName, const QString &password); - void runDemoProject(const QString &projectName); - void runDsProject(const QString &id, const QByteArray &projectData); - void clearDemoCaches(); - - void initDesignStudioManager(); void connectDesignStudio(const QString &ipAddr); - void parseDesignViewerUrl(const QUrl &url); - void popupInterrupted(); -private slots: - void initializeProjectManager(); - void updateUserProjectList(); + bool autoScaleProject() const; + void setAutoScaleProject(bool autoScaleProject); + + QString lastDesignStudioIp() const; }; #endif // DV_ANDROID_H diff --git a/src/backend/constants.h b/src/backend/constants.h deleted file mode 100644 index dfbf261..0000000 --- a/src/backend/constants.h +++ /dev/null @@ -1,44 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2024 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#pragma once - -#include <QStandardPaths> -#include <QString> - -namespace Constants { - -// Path Constants -namespace Paths { -const static QString WritableLocation = QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation); -const static QString ProjectCachePath = WritableLocation + "/projectCache"; -const static QString DemoProjectsPath = WritableLocation + "/demoProjects"; -const static QString ConfigPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); -const static QString ConfigPathDsManager = ConfigPath + "/dsmanager.json"; -const static QString ConfigPathSettings = ConfigPath + "/settings.json"; -} // namespace Paths - -} // namespace Constants diff --git a/src/backend/dsconnector/ds.cpp b/src/backend/dsconnector/ds.cpp index 5379af8..f3282b4 100644 --- a/src/backend/dsconnector/ds.cpp +++ b/src/backend/dsconnector/ds.cpp @@ -25,20 +25,14 @@ #include "ds.h" -#include <QDebug> -#include <QGuiApplication> #include <QJsonDocument> #include <QJsonObject> -#include <QRect> #include <QScreen> -#include <QSysInfo> -#include <QTimer> DesignStudio::DesignStudio(const QString &ipv4Addr, const quint16 port, const QString &designStudioId, const QString &deviceId, - const bool skipPairing, QObject *parent) : QObject(parent) , m_ipv4Addr(ipv4Addr) @@ -46,11 +40,9 @@ DesignStudio::DesignStudio(const QString &ipv4Addr, , m_url("ws://" + m_ipv4Addr + ":" + QString::number(m_port)) , m_id(designStudioId) , m_deviceUuid(deviceId) - , m_skipPairing(skipPairing) { initPingPong(); initSocket(); - qDebug() << "Design Studio" << m_id << "skip pairing" << m_skipPairing; } void DesignStudio::initPingPong() @@ -88,6 +80,7 @@ void DesignStudio::initSocket() m_socketWasConnected = true; m_pingTimer.start(15000); m_pongTimer.stop(); + emit connected(m_id); } else if (state == QAbstractSocket::UnconnectedState && m_socketWasConnected) { qDebug() << "Disconnected from Design Studio" << m_id; m_socketWasConnected = false; @@ -96,18 +89,21 @@ void DesignStudio::initSocket() emit disconnected(m_id); } }); +} +void DesignStudio::connectToDesignStudio() +{ m_socket.open(m_url); } -QString DesignStudio::ipv4Addr() const +void DesignStudio::disconnectFromDesignStudio() { - return m_ipv4Addr; + m_socket.close(); } -bool DesignStudio::skipPairing() const +QString DesignStudio::ipv4Addr() const { - return m_skipPairing; + return m_ipv4Addr; } quint16 DesignStudio::port() const @@ -125,12 +121,7 @@ QString DesignStudio::id() const return m_id; } -bool DesignStudio::isPaired() const -{ - return m_skipPairing || m_paired; -} - -bool DesignStudio::connected() const +bool DesignStudio::isConnected() const { return m_socket.state() == QAbstractSocket::ConnectedState; } @@ -143,11 +134,6 @@ void DesignStudio::setIpAddress(const QString &ipv4Addr) m_socket.open(m_url); } -void DesignStudio::setSkipPairing(const bool skipPairing) -{ - m_skipPairing = skipPairing; -} - void DesignStudio::setDesignStudioId(const QString &designStudioId) { m_id = designStudioId; @@ -165,19 +151,12 @@ void DesignStudio::sendDeviceInfo() deviceInfo["architecture"] = QSysInfo::currentCpuArchitecture(); deviceInfo["deviceId"] = m_deviceUuid; deviceInfo["appVersion"] = QString(CMAKE_VAR_GIT_VERSION); - deviceInfo["skipPairing"] = m_skipPairing; qDebug() << "Sending device info to Design Studio" << deviceInfo; sendData(PackageToDesignStudio::deviceInfo, deviceInfo); } -void DesignStudio::sendPairingPin(const QString &pin) -{ - qDebug() << "Sending pairing PIN to Design Studio with pin" << pin; - sendData(PackageToDesignStudio::pairingPin, pin); -} - void DesignStudio::sendData(const QLatin1String &dataType, const QJsonValue &data) { QJsonObject message; @@ -210,26 +189,10 @@ void DesignStudio::processTextMessage(const QString &message) if (m_id != newDesignStudioId) { qDebug() << "Design Studio" << m_id << "changed ID to" << newDesignStudioId << "with IP address" << m_ipv4Addr << "Notifying ds manager."; - emit idChanged(newDesignStudioId, m_ipv4Addr); - } else { - qDebug() << "Design Studio" << m_id << "is waiting for the info."; - sendDeviceInfo(); + m_id = newDesignStudioId; + emit idReceived(newDesignStudioId, m_ipv4Addr); } - } else if (dataType == PackageFromDesignStudio::pinRequested) { - qDebug() << "Registration PIN requested by Design Studio"; - emit pinRequested(m_id); - } else if (dataType == PackageFromDesignStudio::pinFailed) { - qDebug() << "Registration PIN verification failed"; - emit pinFailed(m_id); - } else if (dataType == PackageFromDesignStudio::deviceIdDeclined) { - qDebug() << "Design Studio unpaired the device"; - m_paired = false; - emit unpaired(m_id); - } else if (dataType == PackageFromDesignStudio::deviceIdAccepted) { - qDebug() << "Design Studio accepted the device ID. Connection alive."; - m_paired = true; - m_skipPairing = false; - emit paired(m_id); + sendDeviceInfo(); } else if (dataType == PackageFromDesignStudio::projectData) { qDebug() << "Project is expected"; emit projectIncoming(); diff --git a/src/backend/dsconnector/ds.h b/src/backend/dsconnector/ds.h index bb46137..a4cc12a 100644 --- a/src/backend/dsconnector/ds.h +++ b/src/backend/dsconnector/ds.h @@ -25,21 +25,13 @@ #pragma once -#include <QObject> +#include <QJsonValue> #include <QTimer> #include <QWebSocket> -#include <qjsonvalue.h> -#include <qlatin1stringview.h> - -#include <QLatin1String> namespace PackageFromDesignStudio { using namespace Qt::Literals; constexpr auto designStudioReady = "designStudioReady"_L1; -constexpr auto pinRequested = "pinRequested"_L1; -constexpr auto pinFailed = "pinVerificationFailed"_L1; -constexpr auto deviceIdDeclined = "deviceIdDeclined"_L1; -constexpr auto deviceIdAccepted = "deviceIdAccepted"_L1; constexpr auto projectData = "projectData"_L1; constexpr auto stopRunningProject = "stopRunningProject"_L1; }; // namespace PackageFromDesignStudio @@ -47,7 +39,6 @@ constexpr auto stopRunningProject = "stopRunningProject"_L1; namespace PackageToDesignStudio { using namespace Qt::Literals; constexpr auto deviceInfo = "deviceInfo"_L1; -constexpr auto pairingPin = "registrationPinResponse"_L1; constexpr auto projectRunning = "projectRunning"_L1; constexpr auto projectStopped = "projectStopped"_L1; constexpr auto projectLogs = "projectLogs"_L1; @@ -61,25 +52,24 @@ public: const quint16 port, const QString &id = QString(), const QString &deviceUuid = QString(), - const bool skipPairing = false, QObject *parent = nullptr); + // Socket ops + void connectToDesignStudio(); + void disconnectFromDesignStudio(); + // Getters QString ipv4Addr() const; quint16 port() const; QString id() const; QUrl url() const; - bool skipPairing() const; - bool isPaired() const; - bool connected() const; + bool isConnected() const; // Setters void setIpAddress(const QString &ipv4Addr); void setDesignStudioId(const QString &designStudioId); - void setSkipPairing(const bool skipPairing); // Send data - void sendPairingPin(const QString &pin); void sendDeviceInfo(); void sendProjectRunning(); void sendProjectStopped(); @@ -99,10 +89,6 @@ private: QString m_id; QString m_deviceUuid; - // Pairing - bool m_paired = false; - bool m_skipPairing = false; - // Settings constexpr static int m_reconnectTimeout = 5000; @@ -120,13 +106,10 @@ private slots: signals: // socket signals void disconnected(const QString &id); + void connected(const QString &id); // DS signals - void idChanged(const QString &id, const QString &ipv4Addr); - void pinRequested(const QString &id); - void pinFailed(const QString &id); - void paired(const QString &id); - void unpaired(const QString &id); + void idReceived(const QString &id, const QString &ipv4Addr); void projectIncoming(); void projectReceived(const QString &id, const QByteArray &data); diff --git a/src/backend/dsconnector/dsdiscovery.cpp b/src/backend/dsconnector/dsdiscovery.cpp index 9c0fcc5..09c7c6c 100644 --- a/src/backend/dsconnector/dsdiscovery.cpp +++ b/src/backend/dsconnector/dsdiscovery.cpp @@ -31,7 +31,6 @@ #include <QJsonDocument> #include <QJsonObject> #include <QJsonParseError> -#include <qjsondocument.h> DesignStudioDiscovery::DesignStudioDiscovery(QObject *parent) : QObject(parent) diff --git a/src/backend/dsconnector/dsdiscovery.h b/src/backend/dsconnector/dsdiscovery.h index 9ddedac..f3756c6 100644 --- a/src/backend/dsconnector/dsdiscovery.h +++ b/src/backend/dsconnector/dsdiscovery.h @@ -25,7 +25,6 @@ #pragma once -#include <QObject> #include <QUdpSocket> class DesignStudioDiscovery : public QObject @@ -38,9 +37,7 @@ private slots: void onReadyRead(); signals: - void designStudioFound(const QString &ipv4Addr, - const QString &id, - const bool skipPairing = false); + void designStudioFound(const QString &ipv4Addr, const QString &id); private: QUdpSocket m_udpSocket; diff --git a/src/backend/dsconnector/dsmanager.cpp b/src/backend/dsconnector/dsmanager.cpp index 9f753b2..777b85c 100644 --- a/src/backend/dsconnector/dsmanager.cpp +++ b/src/backend/dsconnector/dsmanager.cpp @@ -24,30 +24,36 @@ ****************************************************************************/ #include "dsmanager.h" -#include "../constants.h" #include "backend/dsconnector/ds.h" -#include <memory> #include <QFile> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> -#include <QLoggingCategory> -#include <QThread> -#include <qdebug.h> -#include <qjsonobject.h> -#include <qobject.h> +#include <QStandardPaths> DesignStudioManager::DesignStudioManager(QObject *parent, const bool enableDiscovery, const QString &deviceUuid) : QObject(parent) , m_deviceUuid(deviceUuid) + , m_enableDiscovery(enableDiscovery) + , m_settingsPath( + QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/dsmanager.json")) +{} + +void DesignStudioManager::initialize() { - qDebug() << ">>> THREAD ID _ dsmanger:" << QThread::currentThreadId(); - initializePairedDesignStudios(); + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; - if (enableDiscovery) { + initLastKnownDs(); + + qDebug() << "Device UUID:" << m_deviceUuid; + if (m_enableDiscovery) { qDebug() << "Starting Design Studio discovery"; m_discovery = std::make_unique<DesignStudioDiscovery>(); connect(m_discovery.get(), @@ -55,14 +61,11 @@ DesignStudioManager::DesignStudioManager(QObject *parent, this, &DesignStudioManager::designStudioFound); } - - qDebug() << "Design Studio Manager initialized"; - qDebug() << "Thread id dsmanger:" << QThread::currentThreadId(); } -void DesignStudioManager::initializePairedDesignStudios() +void DesignStudioManager::initLastKnownDs() { - QFile file(Constants::Paths::ConfigPathDsManager); + QFile file(m_settingsPath); if (!file.exists()) { qDebug() << "Settings file not found."; @@ -82,78 +85,67 @@ void DesignStudioManager::initializePairedDesignStudios() QJsonArray dsArray = obj["designStudios"].toArray(); for (const auto &ds : dsArray) { QJsonObject dsObj = ds.toObject(); - initDesignStudio(dsObj["ipv4Addr"].toString(), - dsObj["designStudioId"].toString(), - dsObj["skipPairing"].toBool()); + initDesignStudio(dsObj["ipv4Addr"].toString(), dsObj["designStudioId"].toString()); } } -void DesignStudioManager::initDesignStudio(const QString &ipv4Addr, - const QString &designStudioId, - const bool skipPairing) +void DesignStudioManager::initDesignStudio(const QString &ipv4Addr, const QString &designStudioId) { - qDebug() << "Initializing Design Studio" << designStudioId << "with IPv4 address" << ipv4Addr; - auto ds - = std::make_unique<DesignStudio>(ipv4Addr, 40000, designStudioId, m_deviceUuid, skipPairing); + if (m_designStudio) { + if (m_designStudio->ipv4Addr() == ipv4Addr && m_designStudio->isConnected()) { + qDebug() << "Design Studio" << ipv4Addr << "is already connected"; + return; + } + } - connect(ds.get(), &DesignStudio::paired, this, &DesignStudioManager::designStudioPaired); + emit designStudioDisconnected(designStudioId, ipv4Addr); - connect(ds.get(), &DesignStudio::unpaired, this, &DesignStudioManager::designStudioUnpaired); + qDebug() << "Initializing Design Studio" << designStudioId << "with IPv4 address" << ipv4Addr; + m_designStudio.reset(new DesignStudio(ipv4Addr, 40000, designStudioId, m_deviceUuid)); - connect(ds.get(), &DesignStudio::pinRequested, this, &DesignStudioManager::pairingPinRequested); + connect(m_designStudio.get(), + &DesignStudio::idReceived, + this, + [this](const QString &, const QString &) { updateConfigFile(); }); - connect(ds.get(), &DesignStudio::idChanged, this, &DesignStudioManager::dsIdChanged); + connect(m_designStudio.get(), + &DesignStudio::projectReceived, + this, + &DesignStudioManager::projectReceived); - connect(ds.get(), &DesignStudio::projectReceived, this, &DesignStudioManager::projectReceived); + connect(m_designStudio.get(), &DesignStudio::connected, this, [this](const QString &id) { + emit designStudioConnected(id, m_designStudio->ipv4Addr()); + }); - connect(ds.get(), &DesignStudio::disconnected, this, &DesignStudioManager::dsDisconnected); + connect(m_designStudio.get(), &DesignStudio::disconnected, this, [this](const QString &id) { + emit designStudioDisconnected(id, m_designStudio->ipv4Addr()); + }); - connect(ds.get(), + connect(m_designStudio.get(), &DesignStudio::projectStopRequested, this, &DesignStudioManager::projectStopRequested); - m_designStudios.push_back(std::move(ds)); + m_designStudio->connectToDesignStudio(); + updateConfigFile(); } -void DesignStudioManager::designStudioFound(const QString &ipv4Addr, - const QString &id, - const bool skipPairing) +void DesignStudioManager::designStudioFound(const QString &ipv4Addr, const QString &id) { qDebug() << "Design Studio found with IPv4 address" << ipv4Addr << "and ID" << id; - // check if the Design Studio is already in the list - for (const auto &ds : m_designStudios) { - if (ds->id() == id) { - if (ds->ipv4Addr() != ipv4Addr) { - qDebug() << "Design Studio" << id << "changed IP address from" << ds->ipv4Addr() - << "to" << ipv4Addr; - ds->setIpAddress(ipv4Addr); - updateConfigFile(); - } else { - qDebug() << "Design Studio" << id << "already in the list"; - } - return; - } - - if (ds->ipv4Addr() == ipv4Addr && ds->connected()) { - qDebug() << "Design Studio with IP address" << ipv4Addr << "already in the list with ID" - << ds->id() << "and connected"; - if (skipPairing) { - qDebug() << "Forcing pairing with Design Studio" << id; - ds->setSkipPairing(skipPairing); - ds->sendDeviceInfo(); - } - return; - } + // check if the Design Studio is already in the list + if (m_designStudio->id() == id && m_designStudio->ipv4Addr() != ipv4Addr) { + qDebug() << "Design Studio" << id << "changed IP address from" << m_designStudio->ipv4Addr() + << "to" << ipv4Addr; + m_designStudio->setIpAddress(ipv4Addr); + updateConfigFile(); } - - initDesignStudio(ipv4Addr, id, skipPairing); } void DesignStudioManager::updateConfigFile() { - QFile file(Constants::Paths::ConfigPathDsManager); + QFile file(m_settingsPath); if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { qDebug() << "Failed to open settings file for writing"; return; @@ -161,155 +153,29 @@ void DesignStudioManager::updateConfigFile() QJsonObject rootObj; QJsonArray dsArray; - - for (const auto &d : m_designStudios) { - if (!d->isPaired()) { - continue; - } - - QJsonObject dsObj; - dsObj["ipv4Addr"] = d->ipv4Addr(); - dsObj["id"] = d->id(); - dsObj["skipPairing"] = d->skipPairing(); - dsArray.append(dsObj); - } + dsArray.append( + QJsonObject{{"ipv4Addr", m_designStudio->ipv4Addr()}, {"id", m_designStudio->id()}}); rootObj["designStudios"] = dsArray; file.write(QJsonDocument(rootObj).toJson()); } -void DesignStudioManager::dsIdChanged(const QString &id, const QString &ipv4Addr) -{ - /* that can happen because of the following reasons; - 1. The IP address is pointing a different Design Studio (can happen because of DHCP or manual change) - 2. The Design Studio changed its ID (can happen because of a reinstall) - 3. Design Studio is initialized only with IP address and ID is received later - */ - - for (const auto &ds : m_designStudios) { - // case 1 and 3 - if (ds->ipv4Addr() == ipv4Addr && ds->id() != id) { - qDebug() << "Design Studio" << id << "changed ID to" << id << "with IP address" - << ipv4Addr; - ds->setDesignStudioId(id); - ds->sendDeviceInfo(); - break; - } - - // case 2 - if (ds->id() == id) { - qDebug() << "Design Studio" << id << "changed IP address from" << ds->ipv4Addr() << "to" - << ipv4Addr; - - // if there's another DS with the same IP address, remove it - m_designStudios.erase(std::remove_if(m_designStudios.begin(), - m_designStudios.end(), - [&](const auto &d) { - return d->ipv4Addr() == ipv4Addr; - }), - m_designStudios.end()); - ds->setIpAddress(ipv4Addr); - ds->sendDeviceInfo(); - break; - } - } - - updateConfigFile(); -} - -void DesignStudioManager::designStudioPaired(const QString &id) -{ - qDebug() << "Design Studio" << id << "paired. Updating settings."; - updateConfigFile(); - auto ds = std::find_if(m_designStudios.begin(), m_designStudios.end(), [&](const auto &d) { - return d->id() == id; - }); - if (ds == m_designStudios.end()) { - qCritical() << "Design Studio" << id << "not found in the list. Something went wrong."; - return; - } - - if (!(*ds)->connected()) { - qWarning() << "Design Studio" << id - << "is not connected. Not notifying UI about the connection update."; - return; - } - - emit designStudioConnected(id, (*ds)->ipv4Addr()); -} - -void DesignStudioManager::designStudioUnpaired(const QString &id) +void DesignStudioManager::sendProjectRunning(const QString &) { - qDebug() << "Design Studio" << id << "unpaired. Removing from settings."; - updateConfigFile(); - auto ds = std::find_if(m_designStudios.begin(), m_designStudios.end(), [&](const auto &d) { - return d->id() == id; - }); - if (ds == m_designStudios.end()) { - qCritical() << "Design Studio" << id << "not found in the list. Something went wrong."; - return; - } - - emit designStudioDisconnected(id, (*ds)->ipv4Addr()); + m_designStudio->sendProjectRunning(); } -void DesignStudioManager::enterPin(const QString &id, const QString &pin) +void DesignStudioManager::sendProjectStopped(const QString &) { - DesignStudio *ds = nullptr; - - for (const auto &d : m_designStudios) { - if (d->id() == id) { - ds = d.get(); - break; - } - } - - if (!ds) { - return; - } - - ds->sendPairingPin(pin); + m_designStudio->sendProjectStopped(); } -void DesignStudioManager::dsDisconnected(const QString &id) +void DesignStudioManager::sendProjectLogs(const QString &, const QString &logs) { - qDebug() << "Design Studio" << id << "disconnected"; - auto ds = std::find_if(m_designStudios.begin(), m_designStudios.end(), [&](const auto &d) { - return d->id() == id; - }); - - if (!(*ds)->isPaired()) { - qDebug() << "Design Studio" << id - << "is not paired. Not notifying UI about the disconnection."; - return; - } - - emit designStudioDisconnected(id, (*ds)->ipv4Addr()); + m_designStudio->sendProjectLogs(logs); } -void DesignStudioManager::sendProjectRunning(const QString &id) +QString DesignStudioManager::getDesignStudioIp(const QString &) const { - for (const auto &ds : m_designStudios) { - if (ds->id() == id) { - ds->sendProjectRunning(); - } - } -} - -void DesignStudioManager::sendProjectStopped(const QString &id) -{ - for (const auto &ds : m_designStudios) { - if (ds->id() == id) { - ds->sendProjectStopped(); - } - } -} - -void DesignStudioManager::sendProjectLogs(const QString &id, const QString &logs) -{ - for (const auto &ds : m_designStudios) { - if (ds->id() == id) { - ds->sendProjectLogs(logs); - } - } + return m_designStudio ? m_designStudio->ipv4Addr() : QString(); } diff --git a/src/backend/dsconnector/dsmanager.h b/src/backend/dsconnector/dsmanager.h index 48bc17c..913d372 100644 --- a/src/backend/dsconnector/dsmanager.h +++ b/src/backend/dsconnector/dsmanager.h @@ -23,15 +23,12 @@ ** ****************************************************************************/ -#ifndef DSCONNECTOR_H -#define DSCONNECTOR_H +#pragma once #include <QObject> -#include <qscopedpointer.h> #include "ds.h" #include "dsdiscovery.h" -#include <memory> class DesignStudioManager : public QObject { @@ -41,45 +38,35 @@ public: const bool enableDiscovery = false, const QString &deviceUuid = {}); + void initialize(); + public slots: void sendProjectRunning(const QString &id); void sendProjectStopped(const QString &id); void sendProjectLogs(const QString &id, const QString &logs); - - void enterPin(const QString &id, const QString &pin); - void designStudioFound(const QString &ipv4Addr, - const QString &id = {}, - const bool skipPairing = false); - -private slots: - void designStudioPaired(const QString &id); - void designStudioUnpaired(const QString &id); + void initDesignStudio(const QString &ipv4Addr, const QString &designStudioId = {}); + QString getDesignStudioIp(const QString &id) const; private: // Discovery object std::unique_ptr<DesignStudioDiscovery> m_discovery; + const bool m_enableDiscovery; - // Discovered Design Studio instances - std::vector<std::unique_ptr<DesignStudio>> m_designStudios; + // Active Design Studio. Only one can be active at a time. + std::unique_ptr<DesignStudio> m_designStudio; // other members const QString m_deviceUuid; + const QString m_settingsPath; - void dsIdChanged(const QString &id, const QString &ipv4Addr); - void initializePairedDesignStudios(); - void initDesignStudio(const QString &ipv4Addr, - const QString &designStudioId = {}, - const bool skipPairing = false); - void dsDisconnected(const QString &id); - + void initLastKnownDs(); void updateConfigFile(); + void dsIdReceived(const QString &id, const QString &ipv4Addr); + void designStudioFound(const QString &ipv4Addr, const QString &id); signals: - void pairingPinRequested(const QString &id); void projectReceived(const QString &id, const QByteArray &project); void designStudioConnected(const QString &id, const QString &ipAddr); void designStudioDisconnected(const QString &id, const QString &ipAddr); void projectStopRequested(const QString &id); }; - -#endif // DSCONNECTOR_H diff --git a/src/backend/logger.h b/src/backend/logger.h index 23efa64..6c233f7 100644 --- a/src/backend/logger.h +++ b/src/backend/logger.h @@ -26,7 +26,6 @@ #pragma once #include <QMessageBox> -#include <QObject> #include <android/log.h> diff --git a/src/backend/main.cpp b/src/backend/main.cpp index 734d471..2141249 100644 --- a/src/backend/main.cpp +++ b/src/backend/main.cpp @@ -27,13 +27,13 @@ #include <QApplication> #include <QQmlContext> +#include <QQmlEngine> #include "backend.h" #include "logger.h" int main(int argc, char *argv[]) { - // qInstallMessageHandler(messageHandler); Logger::instance(); qDebug() << "Starting Qt Design Viewer"; diff --git a/src/backend/projectmanager.cpp b/src/backend/projectmanager.cpp index a70c903..3aee233 100644 --- a/src/backend/projectmanager.cpp +++ b/src/backend/projectmanager.cpp @@ -26,53 +26,26 @@ #include "projectmanager.h" #include <QBuffer> -#include <QDir> #include <QDirIterator> -#include <QEventLoop> -#include <QFile> -#include <QFileInfo> -#include <QGuiApplication> -#include <QJsonDocument> +#include <QQmlEngine> #include <QQuickItem> #include <QRandomGenerator> #include <QRegularExpression> #include <QResource> -#include <QSettings> #include <QTemporaryDir> -#include <QTemporaryFile> - -#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) #include <QtCore/private/qzipreader_p.h> -#else -#include <QtGui/private/qzipreader_p.h> -#endif - -#include "constants.h" ProjectManager::ProjectManager(QObject *parent, bool autoScaleProject) : QObject(parent) , m_autoScaleProject(autoScaleProject) { - qDebug() << "ProjectManager created."; - qDebug() << "Project cache path: " << Constants::Paths::ProjectCachePath; - qDebug() << "Demo project cache path: " << Constants::Paths::DemoProjectsPath; - qDebug() << "Auto scale project: " << m_autoScaleProject; - - if (!QDir(Constants::Paths::ProjectCachePath).exists()) { - qDebug() << "Creating project cache path: " << Constants::Paths::ProjectCachePath; - QDir().mkpath(Constants::Paths::ProjectCachePath); - } - - if (!QDir(Constants::Paths::DemoProjectsPath).exists()) { - qDebug() << "Creating demo project cache path: " << Constants::Paths::DemoProjectsPath; - QDir().mkpath(Constants::Paths::DemoProjectsPath); - } + qDebug() << "ProjectManager created. Auto scale project: " << m_autoScaleProject; } ProjectManager::~ProjectManager() { + qDebug() << "ProjectManager destroyed. Cleaning up resources."; cleanupResources(); - qDebug() << "ProjectManager destroyed."; } void ProjectManager::cleanupResources() @@ -109,7 +82,7 @@ QString ProjectManager::unpackProject(const QByteArray &project, bool extractZip // maybe it was not a zip file so try it as resource binary if (projectPathDir.isEmpty()) { if (extractZip) - qDebug("File could not be extracted. Trying to open it as a resource file."); + qWarning("File could not be extracted. Trying to open it as a resource file."); cleanupResources(); @@ -122,7 +95,7 @@ QString ProjectManager::unpackProject(const QByteArray &project, bool extractZip m_projectPath = resourcePath; if (!QDir(resourcePath).removeRecursively()) { - qDebug() << "Could not remove resource path: " << resourcePath; + qWarning() << "Could not remove resource path: " << resourcePath; } if (!QResource::registerResource(data, resourcePath)) { @@ -305,228 +278,6 @@ bool ProjectManager::runProject(const QString &projectPath) return true; } -bool ProjectManager::cacheProject(const QByteArray &projectData, const QJsonObject &projectInfo) -{ - // sample project info - /* - { - "appName": "Testmcu", - "id": "0sadf8fa9s8df67s8998690a7sdf", - "owner": "", - "passwordHash": "", - "qdsIsEnterprise": true, - "qdsVersion": "Qt Design Studio 4.2.0", - "ttlDays": 31, - "uploadTime": "2023-10-27T13:57:22", - "userHash": "12038740912873462987" - }, - */ - - const QString projectId = projectInfo.value("id").toString(); - - qDebug() << "Caching project " << projectId << " with last modified " - << projectInfo.value("uploadTime"); - const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; - - // remove old cache - if (QDir(cachePath).exists()) { - qDebug() << "Removing old cache for project " << projectId; - if (!QDir(cachePath).removeRecursively()) { - qCritical() << "Could not remove old cache for project " << projectId; - return false; - } - } - - QDir().mkpath(cachePath); - const QString tempProjectPath = unpackProject(projectData); - - // copy all files from tempProjectPath to cachePath - QDirIterator it(tempProjectPath, QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - const QString filePath = it.filePath(); - const QString relativeFilePath = filePath.mid(tempProjectPath.length()); - const QString newFilePath = cachePath + relativeFilePath; - - if (QFileInfo(filePath).isDir()) { - QDir().mkpath(newFilePath); - } else { - QFile::copy(filePath, newFilePath); - } - } - - // write project info to cache - QFile lastModifiedFile(cachePath + "/projectInfo.json"); - if (!lastModifiedFile.open(QIODevice::WriteOnly)) { - qCritical() << "Could not open projectInfo file for writing"; - return false; - } - - lastModifiedFile.write(QJsonDocument(projectInfo).toJson()); - return true; -} - -bool ProjectManager::isProjectCached(const QJsonObject &projectInfo) -{ - const QString projectId = projectInfo.value("id").toString(); - const QString lastModified = projectInfo.value("uploadTime").toString(); - - qDebug() << "Checking if project " << projectId << " is cached"; - const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; - if (!QDir(cachePath).exists()) { - qDebug() << "Project " << projectId << " is not cached"; - return false; - } - - qDebug() << "Project " << projectId << " is cached. Checking if it is up to date"; - QFile lastModifiedFile(cachePath + "/projectInfo.json"); - if (!lastModifiedFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not open projectInfo file for reading"; - return false; - } - - const QJsonObject cachedProjectInfo - = QJsonDocument::fromJson(lastModifiedFile.readAll()).object(); - const QString cachedLastModified = cachedProjectInfo.value("uploadTime").toString(); - - qDebug() << "Project " << projectId << " last modified: " << lastModified; - qDebug() << "Cached project " << projectId << " last modified: " << cachedLastModified; - - if (lastModified != cachedLastModified) { - qDebug() << "Project " << projectId << " is not up to date"; - return false; - } - - qDebug() << "Project " << projectId << " is up to date"; - return true; -} - -void ProjectManager::clearCachedProject(const QJsonObject &projectInfo) -{ - const QString projectId = projectInfo.value("id").toString(); - qDebug() << "Clearing cache for project " << projectId; - - const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; - if (!QDir(cachePath).exists()) { - qDebug() << "Project " << projectId << " is not cached"; - } - - qDebug() << "Removing cache for project " << projectId; - QDir(cachePath).removeRecursively(); -} - -bool ProjectManager::runCachedProject(const QJsonObject &projectInfo) -{ - const QString projectId = projectInfo.value("id").toString(); - qDebug() << "Running cached project " << projectId; - - const QString projectPath = Constants::Paths::ProjectCachePath + "/" + projectId; - return runProject(projectPath); -} - -bool ProjectManager::cacheDemoProject(const QByteArray &projectData, const QJsonObject &projectInfo) -{ - // sample project info - //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}] - const QString projectName = projectInfo.value("name").toString().remove(".qmlrc"); - const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; - qDebug() << "Caching demo project " << projectName << " to " << demoProjectPath; - - // remove old cache - if (QDir(demoProjectPath).exists()) { - qDebug() << "Removing old cache for demo project " << projectName; - if (!QDir(demoProjectPath).removeRecursively()) { - qCritical() << "Could not remove old cache for demo project " << projectName; - return false; - } - } - - // create new cache - if (!QDir().mkpath(demoProjectPath)) { - qCritical() << "Could not create new cache for demo project " << projectName; - return false; - } - - const QString tempProjectPath = unpackProject(projectData); - - // copy all files from tempProjectPath to demoProjectPath - QDirIterator it(tempProjectPath, QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - const QString filePath = it.filePath(); - const QString relativeFilePath = filePath.mid(tempProjectPath.length()); - const QString newFilePath = demoProjectPath + relativeFilePath; - - if (QFileInfo(filePath).isDir()) { - QDir().mkpath(newFilePath); - } else if (!QFile::copy(filePath, newFilePath)) { - qCritical() << "Could not copy " << filePath << " to " << newFilePath; - return false; - } - } - - // write project info to cache - QFile demoProjectInfoFile(demoProjectPath + "/projectInfo.json"); - if (!demoProjectInfoFile.open(QIODevice::WriteOnly)) { - qCritical() << "Could not open demo project info file for writing"; - return false; - } - - demoProjectInfoFile.write(QJsonDocument(projectInfo).toJson()); - return true; -} - -void ProjectManager::clearDemoCaches() -{ - qDebug() << "Clearing demo caches:" << Constants::Paths::DemoProjectsPath; - QDir(Constants::Paths::DemoProjectsPath).removeRecursively(); -} - -bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo) -{ - // sample project info - //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}] - const QString projectName = projectInfo.value("name").toString().remove(".qmlrc"); - const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; - qDebug() << "Checking if demo project " << projectName << " is cached"; - - if (!QDir(demoProjectPath).exists()) { - qDebug() << "Demo project " << projectName << " is not cached"; - return false; - } - - qDebug() << "Demo project " << projectName << " is cached. Checking the last update time..."; - const QString demoProjectInfoPath = demoProjectPath + "/projectInfo.json"; - QFile demoProjectInfoFile(demoProjectInfoPath); - if (!demoProjectInfoFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not open demo project info file for reading"; - return false; - } - - const QJsonObject cachedProjectInfo - = QJsonDocument::fromJson(demoProjectInfoFile.readAll()).object(); - - const QJsonValue cachedLastModified = cachedProjectInfo.value("lastUpdate"); - const QJsonValue cloudLastModified = projectInfo.value("lastUpdate"); - qDebug() << "Demo project" << projectName << "cloud timestamp: " << cloudLastModified - << "local timestamp:" << cachedLastModified; - - if (cloudLastModified != cachedLastModified) { - qDebug() << "Demo project " << projectName << " is not up to date"; - return false; - } - - qDebug() << "Demo project " << projectName << " is up to date"; - return true; -} - -bool ProjectManager::runDemoProject(const QString &projectName) -{ - const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; - qDebug() << "Running demo project " << projectName << " from " << demoProjectPath; - return runProject(demoProjectPath); -} - void ProjectManager::orientateWindow(Qt::ScreenOrientation orientation) { if (!m_autoScaleProject) diff --git a/src/backend/projectmanager.h b/src/backend/projectmanager.h index 4767ce4..6a1a746 100644 --- a/src/backend/projectmanager.h +++ b/src/backend/projectmanager.h @@ -23,15 +23,9 @@ ** ****************************************************************************/ -#ifndef PROJECTMANAGER_H -#define PROJECTMANAGER_H +#pragma once -#include <QJsonObject> -#include <QObject> -#include <QQmlComponent> -#include <QQmlEngine> #include <QQuickView> -#include <QQuickWindow> class ProjectManager : public QObject { @@ -44,16 +38,6 @@ public: bool runProject(const QString &projectPath); void stopProject(); - bool cacheProject(const QByteArray &projectData, const QJsonObject &projectInfo); - bool isProjectCached(const QJsonObject &projectInfo); - bool runCachedProject(const QJsonObject &projectInfo); - void clearCachedProject(const QJsonObject &projectInfo); - - bool cacheDemoProject(const QByteArray &projectData, const QJsonObject &projectInfo); - bool isDemoProjectCached(const QJsonObject &projectName); - bool runDemoProject(const QString &projectName); - void clearDemoCaches(); - void showAppWindow(); void hideAppWindow(); @@ -70,7 +54,6 @@ private: QScopedPointer<QQmlEngine> m_qmlEngine; QScopedPointer<QQmlComponent> m_qmlComponent; QScopedPointer<QQuickWindow> m_quickWindow; - QJSValue m_consoleObject; // Member functions QString findFile(const QString &dir, const QString &filter); @@ -80,11 +63,8 @@ private: bool isQt6Project(const QString &qmlProjectFileContent); void parseQmlProjectFile(const QString &fileName, QString *mainFile, QStringList *importPaths); - void cacheProject(const QString &projectName, const QString &projectPath); void cleanupResources(); signals: void closingProject(); }; - -#endif // PROJECTMANAGER_H diff --git a/src/backend/qrscanner.cpp b/src/backend/qrscanner.cpp index 7d26ae2..3c507e4 100644 --- a/src/backend/qrscanner.cpp +++ b/src/backend/qrscanner.cpp @@ -28,7 +28,6 @@ #include <QApplication> #include <QCamera> #include <QCameraDevice> -#include <QElapsedTimer> #include <QImageCapture> #include <QMediaDevices> #include <QMessageBox> diff --git a/src/backend/serviceconnector.cpp b/src/backend/serviceconnector.cpp deleted file mode 100644 index f8314f2..0000000 --- a/src/backend/serviceconnector.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2023 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#include "serviceconnector.h" - -#include <QNetworkAccessManager> -#include <QNetworkReply> -#include <QEventLoop> -#include <QJsonDocument> -#include <QJsonObject> - -std::optional<QByteArray> ServiceConnector::fetchResource(const QString &url, const QString &auth) -{ - qDebug() << "Fetching resource from" << url; - - QNetworkRequest request(url); - request.setRawHeader("Authorization", auth.toUtf8()); - QSharedPointer<QNetworkReply> reply(m_manager.get(request)); - QObject::connect(reply.data(), - &QNetworkReply::sslErrors, - this, - [&](const QList<QSslError> &errors) { - qWarning() << errors.first().errorString(); - }); - - QEventLoop loop; - QObject::connect(this, &ServiceConnector::interrupted, reply.data(), [&]() { - qDebug() << "Aborting network request"; - reply->abort(); - loop.quit(); - }); - QObject::connect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(reply.data(), - &QNetworkReply::downloadProgress, - this, - [&](qint64 bytesReceived, qint64 bytesTotal) { - float percentage = roundf((float) bytesReceived / (float) bytesTotal * 100); - qDebug() << "Download progress" << QString::number(percentage) << "% -" - << QString::number(bytesReceived) << "/" - << QString::number(bytesTotal); - emit downloadProgress(percentage); - }); - loop.exec(); - - if (reply->error() != QNetworkReply::NoError) { - qWarning() << reply->errorString(); - return {}; - } - - qDebug() << "Resource fetched successfully"; - return reply->readAll(); -} - -std::optional<QByteArray> ServiceConnector::fetchProject(const QString &url) -{ - QString projectUrl = url; - if (projectUrl.startsWith(m_serviceUrl + "/#")) { - projectUrl = projectUrl.split("#").at(1); - projectUrl.prepend(m_serviceUrl + "/qmlprojects/"); - } - - return fetchResource(projectUrl); -} - -std::optional<QByteArray> ServiceConnector::fetchUserProject(const QString &userHash, - const QString &projectName, - const QString &password) -{ - const QString projectUrl = m_serviceUrl + "/qmlprojects/" + userHash + "/" + projectName - + ".qmlrc"; - - return fetchResource(projectUrl, password); -} - -std::optional<QJsonArray> ServiceConnector::fetchUserProjectList(const QString &userHash) -{ - auto reply = fetchResource(m_serviceUrl + "/api/v1/qmlrc/list/" + userHash + "?key=818815"); - - if (!reply) - return {}; - - QJsonArray projectList = QJsonDocument::fromJson(reply.value()) - .object() - .value("data") - .toObject() - .value("packages") - .toArray(); - - return projectList; -} - -std::optional<QJsonArray> ServiceConnector::fetchDemoList() -{ - auto reply = fetchResource(m_serviceUrl + "/api/v1/demos"); - if (!reply) - return {}; - - return QJsonDocument::fromJson(reply.value()).array(); -} - -std::optional<QByteArray> ServiceConnector::fetchDemo(const QString &demoName) -{ - return fetchResource(m_serviceUrl + "/qmlprojects/demos/" + demoName + ".qmlrc"); -} diff --git a/src/backend/serviceconnector.h b/src/backend/serviceconnector.h deleted file mode 100644 index 9d5d4f5..0000000 --- a/src/backend/serviceconnector.h +++ /dev/null @@ -1,55 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2023 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#ifndef SERVICECONNECTOR_H -#define SERVICECONNECTOR_H - -#include <QNetworkAccessManager> -#include <QJsonArray> - -class ServiceConnector : public QObject -{ - Q_OBJECT -public: - std::optional<QByteArray> fetchProject(const QString &url); - std::optional<QByteArray> fetchUserProject(const QString &userHash, - const QString &projectName, - const QString &password); - std::optional<QJsonArray> fetchUserProjectList(const QString &userHash); - std::optional<QJsonArray> fetchDemoList(); - std::optional<QByteArray> fetchDemo(const QString &demoName); - -private: - QNetworkAccessManager m_manager; - std::optional<QByteArray> fetchResource(const QString &url, const QString &auth = ""); - - const QString m_serviceUrl = "https://designviewer.qt.io"; - -signals: - void downloadProgress(float percentage); - void interrupted(); -}; - -#endif // SERVICECONNECTOR_H diff --git a/src/backend/settings.cpp b/src/backend/settings.cpp index 73299cd..3dfedfd 100644 --- a/src/backend/settings.cpp +++ b/src/backend/settings.cpp @@ -29,54 +29,58 @@ #include <QJsonDocument> #include <QStandardPaths> -#include "constants.h" - /* Example of a settings.json file: { "autoScale": true - "userHash": "1234567890", "deviceUuid": "1234567890" } */ Settings::Settings() - : m_settingsPath(Constants::Paths::ConfigPathSettings) + : m_settingsPath( + QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/settings.json")) { qDebug() << "Settings path:" << m_settingsPath; - loadSettings(); + if (!loadSettings()) { + qDebug() << "Failed to load settings. Applying default ones."; + applyDefaultSettings(); + } +} + +void Settings::applyDefaultSettings() +{ + m_settings["autoScale"] = true; + m_settings["deviceUuid"] = ""; + saveSettings(); } -void Settings::loadSettings() +bool Settings::loadSettings() { QFile file(m_settingsPath); - if (file.open(QIODevice::ReadOnly)) { - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - m_settings = doc.object(); - return; + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open settings file" << m_settingsPath + << "Reason:" << file.errorString(); + return false; } - qWarning() << "Failed to load settings from" << m_settingsPath; - m_settings.insert("projectManager", QJsonObject{{"autoScale", true}}); - m_settings.insert("backend", QJsonObject{{"userHash", ""}, {"deviceUuid", ""}}); - - if (!file.exists()) { - qDebug() << "Settings file not found. Creating a new one."; - saveSettings(); - } + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + m_settings = doc.object(); + return true; } bool Settings::saveSettings() { QFile file(m_settingsPath); - if (file.open(QIODevice::WriteOnly)) { - QJsonDocument doc(m_settings); - file.write(doc.toJson()); - return true; + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to save settings to" << m_settingsPath + << "Reason:" << file.errorString(); + return false; } - qWarning() << "Failed to save settings to" << m_settingsPath; - return false; + QJsonDocument doc(m_settings); + file.write(doc.toJson()); + return true; } void Settings::setAutoScaleProject(const bool &enabled) @@ -85,29 +89,18 @@ void Settings::setAutoScaleProject(const bool &enabled) saveSettings(); } -void Settings::setUserHash(const QString &userHash) -{ - m_settings["userHash"] = userHash; - saveSettings(); -} - void Settings::setDeviceUuid(const QString &deviceUuid) { m_settings["deviceUuid"] = deviceUuid; saveSettings(); } -QString Settings::userHash() -{ - return m_settings["userHash"].toString(); -} - -QString Settings::deviceUuid() +QString Settings::deviceUuid() const { return m_settings["deviceUuid"].toString(); } -bool Settings::autoScaleProject() +bool Settings::autoScaleProject() const { return m_settings["autoScale"].toBool(); } diff --git a/src/backend/settings.h b/src/backend/settings.h index 1a122b0..96f034f 100644 --- a/src/backend/settings.h +++ b/src/backend/settings.h @@ -32,17 +32,16 @@ public: Settings(); void setAutoScaleProject(const bool &enabled); - void setUserHash(const QString &userHash); void setDeviceUuid(const QString &deviceUuid); - bool autoScaleProject(); - QString userHash(); - QString deviceUuid(); + bool autoScaleProject() const; + QString deviceUuid() const; private: QJsonObject m_settings; const QString m_settingsPath; - void loadSettings(); + bool loadSettings(); bool saveSettings(); + void applyDefaultSettings(); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d76888c..b006a33 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,11 +4,9 @@ find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia WebSockets) enable_testing(true) qt_add_executable(${TARGET_NAME} - tst_projectmanager.cpp tst_projectmanager.h - tst_serviceconnector.cpp tst_serviceconnector.h tst_designstudio.cpp tst_designstudio.h tst_dsdiscovery.cpp tst_dsdiscovery.h - tst_dsmanager.cpp tst_dsmanager.h + tst_settings.cpp tst_settings.h mock.h main.cpp ) diff --git a/tests/main.cpp b/tests/main.cpp index e991d91..2947812 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -29,8 +29,7 @@ #include "tst_designstudio.h" #include "tst_dsdiscovery.h" -#include "tst_projectmanager.h" -#include "tst_serviceconnector.h" +#include "tst_settings.h" #define DEBUG_LINE qDebug() << "Debugline:" << __LINE__ @@ -91,10 +90,9 @@ int main(int argc, char *argv[]) outputFile.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); outputFile.write("<testsuites>\n"); - runTest(new TestServiceConnector); - runTest(new TestProjectManager); runTest(new TestDesignStudio); runTest(new TestDesignStudioDiscovery); + runTest(new TestSettings); outputFile.write("</testsuites>\n"); diff --git a/tests/tst_designstudio.cpp b/tests/tst_designstudio.cpp index e36bfc7..5e71c26 100644 --- a/tests/tst_designstudio.cpp +++ b/tests/tst_designstudio.cpp @@ -70,13 +70,6 @@ void TestDesignStudio::testUrl() QCOMPARE(ds->url(), QUrl("ws://localhost:40000")); } -void TestDesignStudio::testSkipPairing() -{ - QVERIFY(!ds->skipPairing()); - ds->setSkipPairing(true); - QVERIFY(ds->skipPairing()); -} - void TestDesignStudio::testConnected() { QHostAddress addr = QHostAddress::LocalHost; @@ -84,36 +77,33 @@ void TestDesignStudio::testConnected() QVERIFY(server.listen(addr, 40001)); DesignStudio ds(addr.toString(), 40001, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); } -void TestDesignStudio::testSendRegistrationPin() +void TestDesignStudio::testConnect() { + QHostAddress addr = QHostAddress::LocalHost; QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); - QVERIFY(server.listen(QHostAddress::LocalHost, 40002)); - - DesignStudio ds("localhost", 40002, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); - - QWebSocket *socket = server.nextPendingConnection(); - QVERIFY(socket != nullptr); + QVERIFY(server.listen(addr, 40002)); - connect(socket, &QWebSocket::textMessageReceived, [](const QString &message) { - qDebug() << "Received message" << message; + DesignStudio ds(addr.toString(), 40002, "id", "deviceId"); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); +} - QJsonObject obj = QJsonDocument::fromJson(message.toUtf8()).object(); - QVERIFY(obj.contains("dataType")); - QVERIFY(obj.contains("data")); - QVERIFY(obj.value("dataType").toString() == PackageToDesignStudio::pairingPin); - QVERIFY(obj.value("data").toString() == "1234"); - }); +void TestDesignStudio::testDisconnect() +{ + QHostAddress addr = QHostAddress::LocalHost; + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(addr, 40002)); - QSignalSpy spy(socket, &QWebSocket::textMessageReceived); - QVERIFY(spy.isValid()); + DesignStudio ds(addr.toString(), 40002, "id", "deviceId"); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); - ds.sendPairingPin("1234"); - QTRY_COMPARE(spy.count(), 1); - QTest::qWait(1000); + ds.disconnectFromDesignStudio(); + QTRY_VERIFY(!ds.isConnected()); } void TestDesignStudio::testSendDeviceInfo() @@ -122,7 +112,8 @@ void TestDesignStudio::testSendDeviceInfo() QVERIFY(server.listen(QHostAddress::LocalHost, 40003)); DesignStudio ds("localhost", 40003, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); QWebSocket *socket = server.nextPendingConnection(); QVERIFY(socket != nullptr); @@ -154,87 +145,47 @@ void TestDesignStudio::testSendDeviceInfo() QTest::qWait(500); } -void TestDesignStudio::testRegistrationPinRequestedSignal() -{ - QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); - QVERIFY(server.listen(QHostAddress::LocalHost, 40005)); - - DesignStudio ds("localhost", 40005, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); - - QWebSocket *socket = server.nextPendingConnection(); - QVERIFY(socket != nullptr); - - QSignalSpy spy(&ds, &DesignStudio::pinRequested); - QVERIFY(spy.isValid()); - - const QJsonObject pinRequestedPackage = createPackage(PackageFromDesignStudio::pinRequested, - "data"); - socket->sendTextMessage(QJsonDocument(pinRequestedPackage).toJson(QJsonDocument::Compact)); - - QTRY_COMPARE(spy.count(), 1); -} - -void TestDesignStudio::testPinVerificationFailedSignal() +void TestDesignStudio::testConnectedSignal() { QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); - QVERIFY(server.listen(QHostAddress::LocalHost, 40006)); - - DesignStudio ds("localhost", 40006, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); - - QWebSocket *socket = server.nextPendingConnection(); - QVERIFY(socket != nullptr); + QVERIFY(server.listen(QHostAddress::LocalHost, 40010)); - QSignalSpy spy(&ds, &DesignStudio::pinFailed); + DesignStudio ds("localhost", 40010, "id", "deviceId"); + QSignalSpy spy(&ds, &DesignStudio::connected); + QSignalSpy spy2(&ds, &DesignStudio::idReceived); QVERIFY(spy.isValid()); + QVERIFY(spy2.isValid()); - const QJsonObject pinVerificationFailedPackage - = createPackage(PackageFromDesignStudio::pinFailed, "data"); - socket->sendTextMessage( - QJsonDocument(pinVerificationFailedPackage).toJson(QJsonDocument::Compact)); - - QTRY_COMPARE(spy.count(), 1); -} - -void TestDesignStudio::testUnregisteredSignal() -{ - QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); - QVERIFY(server.listen(QHostAddress::LocalHost, 40009)); - - DesignStudio ds("localhost", 40009, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); QWebSocket *socket = server.nextPendingConnection(); QVERIFY(socket != nullptr); - QSignalSpy spy(&ds, &DesignStudio::unpaired); - QVERIFY(spy.isValid()); - - const QJsonObject deviceIdDeclinedPackage - = createPackage(PackageFromDesignStudio::deviceIdDeclined, "data"); - socket->sendTextMessage(QJsonDocument(deviceIdDeclinedPackage).toJson(QJsonDocument::Compact)); + const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::designStudioReady, + "data"); + socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact)); QTRY_COMPARE(spy.count(), 1); + QTRY_COMPARE(spy2.count(), 1); } -void TestDesignStudio::testConnectionAliveSignal() +void TestDesignStudio::testDisconnectedSignal() { QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); - QVERIFY(server.listen(QHostAddress::LocalHost, 40010)); + QVERIFY(server.listen(QHostAddress::LocalHost, 40011)); - DesignStudio ds("localhost", 40010, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + DesignStudio ds("localhost", 40011, "id", "deviceId"); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); QWebSocket *socket = server.nextPendingConnection(); QVERIFY(socket != nullptr); - QSignalSpy spy(&ds, &DesignStudio::paired); + QSignalSpy spy(&ds, &DesignStudio::disconnected); QVERIFY(spy.isValid()); - const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::deviceIdAccepted, - "data"); - socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact)); + ds.disconnectFromDesignStudio(); QTRY_COMPARE(spy.count(), 1); } @@ -245,7 +196,8 @@ void TestDesignStudio::testProjectIncomingSignal() QVERIFY(server.listen(QHostAddress::LocalHost, 40011)); DesignStudio ds("localhost", 40011, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); QWebSocket *socket = server.nextPendingConnection(); QVERIFY(socket != nullptr); @@ -266,7 +218,8 @@ void TestDesignStudio::testProjectReceivedSignal() QVERIFY(server.listen(QHostAddress::LocalHost, 40012)); DesignStudio ds("localhost", 40012, "id", "deviceId"); - QTRY_VERIFY(ds.connected()); + ds.connectToDesignStudio(); + QTRY_VERIFY(ds.isConnected()); QWebSocket *socket = server.nextPendingConnection(); QVERIFY(socket != nullptr); diff --git a/tests/tst_designstudio.h b/tests/tst_designstudio.h index 7573c6b..3abbf5f 100644 --- a/tests/tst_designstudio.h +++ b/tests/tst_designstudio.h @@ -43,20 +43,20 @@ private slots: void testId(); void testPort(); void testUrl(); - void testSkipPairing(); // test status check void testConnected(); + // test connect/disconnect + void testConnect(); + void testDisconnect(); + // test send data - void testSendRegistrationPin(); void testSendDeviceInfo(); // test signals - void testRegistrationPinRequestedSignal(); - void testPinVerificationFailedSignal(); - void testUnregisteredSignal(); - void testConnectionAliveSignal(); + void testConnectedSignal(); + void testDisconnectedSignal(); void testProjectIncomingSignal(); void testProjectReceivedSignal(); }; diff --git a/tests/tst_dsmanager.cpp b/tests/tst_dsmanager.cpp deleted file mode 100644 index 51f9a0c..0000000 --- a/tests/tst_dsmanager.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2024 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#include "tst_dsmanager.h" - -#include <QJsonDocument> -#include <QJsonObject> -#include <QTest> -#include <QUdpSocket> - -#include "backend/dsconnector/dsmanager.h" -#include "mock.h" - -void TestDesignStudioManager::testEnterPin() -{ - // Test enterPin() function -} - -void TestDesignStudioManager::testDesignStudioRegisteredSignal() -{ - // Test designStudioRegisteredSignal() function -} - -void TestDesignStudioManager::testDesignStudioUnregisteredSignal() -{ - // Test designStudioUnregisteredSignal() function -} - -void TestDesignStudioManager::testPinRequestedSignal() -{ - // Test pinRequestedSignal() function -} - -void TestDesignStudioManager::testProjectReceivedSignal() -{ - // Test projectReceivedSignal() function -} diff --git a/tests/tst_projectmanager.cpp b/tests/tst_projectmanager.cpp deleted file mode 100644 index c2caf9a..0000000 --- a/tests/tst_projectmanager.cpp +++ /dev/null @@ -1,147 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2024 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#include "tst_projectmanager.h" - -#include <QJsonArray> -#include <QTest> - -#include "backend/projectmanager.h" -#include "backend/serviceconnector.h" - -#define DEMO_PROJECT_NAME "ClusterTutorial" - -void TestProjectManager::initTestCase() -{ - qDebug() << "Initialize TestProjectManager"; - ServiceConnector sc; - std::optional<QJsonArray> demoProjectList = sc.fetchDemoList(); - QVERIFY(demoProjectList.has_value()); - - m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME); - QVERIFY(m_demoProjectData.has_value()); - - QJsonObject projectInfo; - for (auto project : demoProjectList.value()) { - if (DEMO_PROJECT_NAME == project.toObject().value("name").toString().remove(".qmlrc")) { - m_demoInfoCorrect = project.toObject(); - break; - } - } - - m_demoInfoIncorrect = QJsonObject{ - {{"lastUpdate", 1707822220200.017}, {"name", "ClusterTutorial.qmlrc"}}}; - - m_userProjectInfoCorrect = QJsonObject{{{"appName", "Testmcu"}, - {"id", "0sadf8fa9s8df67s8998690a7sdf"}, - {"owner", ""}, - {"passwordHash", ""}, - {"qdsIsEnterprise", true}, - {"qdsVersion", "Qt Design Studio 4.2.0"}, - {"ttlDays", 31}, - {"uploadTime", "2023-10-27T13:57:22"}, - {"userHash", "12038740912873462987"}}}; - - m_userProjectInfoIncorrect = QJsonObject{{{"appName", "Testmcu"}, - {"id", "0sadf8fa9s8df67s8998690a7sdf"}, - {"owner", ""}, - {"passwordHash", ""}, - {"qdsIsEnterprise", true}, - {"qdsVersion", "Qt Design Studio 4.2.0"}, - {"ttlDays", 31}, - {"uploadTime", "2023-10-27T13:58:22"}, - {"userHash", "12038740912873462987"}}}; - qDebug() << "TestProjectManager initialized"; -} - -void TestProjectManager::cacheDemoProject() -{ - ProjectManager pm; - - QCOMPARE(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect), true); -} - -void TestProjectManager::isDemoProjectCachedTrue() -{ - ProjectManager pm; - - QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect)); - QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), true); -} - -void TestProjectManager::isDemoProjectCachedFalse() -{ - ProjectManager pm; - - QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect)); - QCOMPARE(pm.isDemoProjectCached(m_demoInfoIncorrect), false); -} - -void TestProjectManager::clearDemoProjectCache() -{ - ProjectManager pm; - QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect)); - - pm.clearDemoCaches(); - QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), false); -} - -void TestProjectManager::cacheUserProject() -{ - ProjectManager pm; - QCOMPARE(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect), true); -} - -void TestProjectManager::isUserProjectCachedTrue() -{ - ProjectManager pm; - pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect); - QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), true); -} - -void TestProjectManager::isUserProjectCachedFalse() -{ - ProjectManager pm; - QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect)); - QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false); -} - -void TestProjectManager::clearUserProjectCache() -{ - ProjectManager pm; - QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect)); - QVERIFY(pm.isProjectCached(m_userProjectInfoCorrect)); - - pm.clearCachedProject(m_userProjectInfoCorrect); - QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), false); -} - -void TestProjectManager::clearNonExistentUserProjectCache() -{ - ProjectManager pm; - QVERIFY(!pm.isProjectCached(m_userProjectInfoIncorrect)); - pm.clearCachedProject(m_userProjectInfoIncorrect); - QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false); -} diff --git a/tests/tst_projectmanager.h b/tests/tst_projectmanager.h deleted file mode 100644 index 4e7635a..0000000 --- a/tests/tst_projectmanager.h +++ /dev/null @@ -1,56 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2024 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#pragma once - -#include <QJsonObject> - -class TestProjectManager : public QObject -{ - Q_OBJECT -private: - std::optional<QByteArray> m_demoProjectData; - QJsonObject m_demoInfoCorrect; - QJsonObject m_demoInfoIncorrect; - QJsonObject m_userProjectInfoCorrect; - QJsonObject m_userProjectInfoIncorrect; - -private slots: - // init - void initTestCase(); - - // project manager tests - demo project - void cacheDemoProject(); - void isDemoProjectCachedTrue(); - void isDemoProjectCachedFalse(); - void clearDemoProjectCache(); - - // project manager tests - user project - void cacheUserProject(); - void isUserProjectCachedTrue(); - void isUserProjectCachedFalse(); - void clearUserProjectCache(); - void clearNonExistentUserProjectCache(); -}; diff --git a/tests/tst_serviceconnector.cpp b/tests/tst_serviceconnector.cpp deleted file mode 100644 index 9b6c66e..0000000 --- a/tests/tst_serviceconnector.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2024 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** 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. -** -****************************************************************************/ - -#include "tst_serviceconnector.h" - -#include <QJsonObject> -#include <QTest> - -#include "backend/serviceconnector.h" - -#define DEMO_PROJECT_NAME "ClusterTutorial" - -void TestServiceConnector::initTestCase() -{ - qDebug() << "Initialize TestServiceConnector"; - ServiceConnector sc; - m_demoProjectList = sc.fetchDemoList(); - QVERIFY(m_demoProjectList.has_value()); - - m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME); - QVERIFY(m_demoProjectData.has_value()); - - qDebug() << "TestServiceConnector initialized"; -} - -void TestServiceConnector::fetchDemoProjectList() -{ - QStringList projectNames{"ClusterTutorial.qmlrc", - "CoffeeMachine.qmlrc", - "EBikeDesign.qmlrc", - "MaterialBundle.qmlrc", - "SideMenu.qmlrc", - "WebinarDemo.qmlrc"}; - - QStringList projectNamesFromJson; - for (const auto &project : m_demoProjectList.value()) { - projectNamesFromJson.append(project.toObject().value("name").toString()); - } - - QCOMPARE(projectNamesFromJson, projectNames); -} - -void TestServiceConnector::fetchDemoProject() -{ - QCOMPARE(m_demoProjectData.has_value(), true); -} diff --git a/tests/tst_serviceconnector.h b/tests/tst_settings.cpp similarity index 71% rename from tests/tst_serviceconnector.h rename to tests/tst_settings.cpp index b0dfa99..dcb5ffc 100644 --- a/tests/tst_serviceconnector.h +++ b/tests/tst_settings.cpp @@ -23,21 +23,26 @@ ** ****************************************************************************/ -#pragma once +#include "tst_settings.h" -#include <QJsonArray> +#include <QTest> -class TestServiceConnector : public QObject +#include "backend/settings.h" + +void TestSettings::testAutoScaleProject() { - Q_OBJECT -private: - std::optional<QJsonArray> m_demoProjectList; - std::optional<QByteArray> m_demoProjectData; + Settings settings; + QVERIFY(settings.autoScaleProject()); + + settings.setAutoScaleProject(false); + QVERIFY(!settings.autoScaleProject()); +} -private slots: - void initTestCase(); +void TestSettings::testDeviceUuid() +{ + Settings settings; + QVERIFY(settings.deviceUuid().isEmpty()); - // service connector tests - void fetchDemoProjectList(); - void fetchDemoProject(); -}; + settings.setDeviceUuid("1234567890"); + QCOMPARE(settings.deviceUuid(), QString("1234567890")); +} diff --git a/tests/tst_dsmanager.h b/tests/tst_settings.h similarity index 81% rename from tests/tst_dsmanager.h rename to tests/tst_settings.h index 07442f2..18ed10d 100644 --- a/tests/tst_dsmanager.h +++ b/tests/tst_settings.h @@ -27,18 +27,11 @@ #include <QObject> -class TestDesignStudioManager : public QObject +class TestSettings : public QObject { Q_OBJECT -private: private slots: - // test setters - void testEnterPin(); - - // test signals - void testDesignStudioRegisteredSignal(); - void testDesignStudioUnregisteredSignal(); - void testPinRequestedSignal(); - void testProjectReceivedSignal(); + void testAutoScaleProject(); + void testDeviceUuid(); }; -- GitLab