From 3288a36da6997484dc7ea9e2e7983a92503fa673 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 30 Mar 2017 13:24:04 +0200 Subject: [PATCH] Add GUI testing SocketApi extension --- CMakeLists.txt | 2 + config.h.in | 2 + src/gui/settingsdialog.cpp | 10 +- src/gui/socketapi.cpp | 263 +++++++++++++++++++++--------- src/gui/socketapi.h | 14 +- src/gui/socketapi_p.h | 142 ++++++++++++++++ src/gui/wizard/owncloudwizard.cpp | 2 + 7 files changed, 357 insertions(+), 78 deletions(-) create mode 100644 src/gui/socketapi_p.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f926c25cf..0e0d948cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,6 +196,8 @@ if(APPLE) endif() if(BUILD_CLIENT) + OPTION(GUI_TESTING "Build with gui introspection features of socket api" OFF) + if(APPLE AND BUILD_UPDATER) find_package(Sparkle) endif() diff --git a/config.h.in b/config.h.in index 2dcf2d6f1..0872b8ced 100644 --- a/config.h.in +++ b/config.h.in @@ -35,4 +35,6 @@ #cmakedefine SHAREDIR "@SHAREDIR@" #cmakedefine PLUGINDIR "@PLUGINDIR@" +#cmakedefine01 GUI_TESTING + #endif diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index e577dece3..b3b3a5d91 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -224,7 +224,11 @@ void SettingsDialog::accountAdded(AccountState *s) _toolBar->insertAction(_actionBefore, accountAction); auto accountSettings = new AccountSettings(s, this); - _ui->stack->insertWidget(0, accountSettings); + QString objectName = QLatin1String("accountSettings_"); + objectName += s->account()->displayName(); + accountSettings->setObjectName(objectName); + _ui->stack->insertWidget(0 , accountSettings); + _actionGroup->addAction(accountAction); _actionGroupWidgets.insert(accountAction, accountSettings); _actionForAccount.insert(s->account().data(), accountAction); @@ -339,6 +343,10 @@ public: } auto *btn = new QToolButton(parent); + QString objectName = QLatin1String("settingsdialog_toolbutton_"); + objectName += text(); + btn->setObjectName(objectName); + btn->setDefaultAction(this); btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp index 32f295ea3..3c1a87bfe 100644 --- a/src/gui/socketapi.cpp +++ b/src/gui/socketapi.cpp @@ -15,6 +15,7 @@ */ #include "socketapi.h" +#include "socketapi_p.h" #include "conflictdialog.h" #include "conflictsolver.h" @@ -53,6 +54,13 @@ #include #include #include + + +#include +#include +#include +#include + #include #include @@ -62,10 +70,30 @@ #include #endif + // This is the version that is returned when the client asks for the VERSION. // The first number should be changed if there is an incompatible change that breaks old clients. // The second number should be changed when there are new features. #define MIRALL_SOCKET_API_VERSION "1.1" +#define DEBUG qDebug() << "SocketApi: " + +namespace { +#if GUI_TESTING +QWidget *findWidget(const QString &objectName) +{ + auto widgets = QApplication::allWidgets(); + + auto foundWidget = std::find_if(widgets.constBegin(), widgets.constEnd(), [&](QWidget *widget) { + return widget->objectName() == objectName; + }); + + if (foundWidget == widgets.constEnd()) { + return nullptr; + } + + return *foundWidget; +} +#endif static inline QString removeTrailingSlash(QString path) { @@ -89,6 +117,7 @@ static QString buildMessage(const QString &verb, const QString &path, const QStr } return msg; } +} namespace OCC { @@ -96,80 +125,28 @@ Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg) Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg) -class BloomFilter -{ - // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash). - // For a client navigating in less than 100 directories, this gives us a probability less than (1-e^(-2*100/1024))^2 = 0.03147872136 false positives. - const static int NumBits = 1024; - -public: - BloomFilter() - : hashBits(NumBits) - { - } - - void storeHash(uint hash) - { - hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range - hashBits.setBit((hash >> 16) % NumBits); // NOLINT - } - bool isHashMaybeStored(uint hash) const - { - return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT - && hashBits.testBit((hash >> 16) % NumBits); // NOLINT - } - -private: - QBitArray hashBits; -}; - -class SocketListener +void SocketListener::sendMessage(const QString &message, bool doWait) const { -public: - QPointer socket; - - explicit SocketListener(QIODevice *socket) - : socket(socket) - { + if (!socket) { + qCInfo(lcSocketApi) << "Not sending message to dead socket:" << message; + return; } - void sendMessage(const QString &message, bool doWait = false) const - { - if (!socket) { - qCInfo(lcSocketApi) << "Not sending message to dead socket:" << message; - return; - } - - qCInfo(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket; - QString localMessage = message; - if (!localMessage.endsWith(QLatin1Char('\n'))) { - localMessage.append(QLatin1Char('\n')); - } - - QByteArray bytesToSend = localMessage.toUtf8(); - qint64 sent = socket->write(bytesToSend); - if (doWait) { - socket->waitForBytesWritten(1000); - } - if (sent != bytesToSend.length()) { - qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage; - } + qCInfo(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket; + QString localMessage = message; + if (!localMessage.endsWith(QLatin1Char('\n'))) { + localMessage.append(QLatin1Char('\n')); } - void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const - { - if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash)) - sendMessage(message, false); + QByteArray bytesToSend = localMessage.toUtf8(); + qint64 sent = socket->write(bytesToSend); + if (doWait) { + socket->waitForBytesWritten(1000); } - - void registerMonitoredDirectory(uint systemDirectoryHash) - { - _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash); + if (sent != bytesToSend.length()) { + qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage; } - -private: - BloomFilter _monitoredDirectoriesBloomFilter; -}; +} struct ListenerHasSocketPred { @@ -186,6 +163,9 @@ SocketApi::SocketApi(QObject *parent) { QString socketPath; + qRegisterMetaType("SocketListener*"); + qRegisterMetaType>("QSharedPointer"); + if (Utility::isWindows()) { socketPath = QLatin1String(R"(\\.\pipe\)") + QLatin1String(APPLICATION_EXECUTABLE) @@ -321,20 +301,48 @@ void SocketApi::slotReadSocket() line.chop(1); // remove the '\n' qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket; QByteArray command = line.split(":").value(0).toLatin1(); - QByteArray functionWithArguments = "command_" + command + "(QString,SocketListener*)"; + + QByteArray functionWithArguments = "command_" + command; + if (command.startsWith("ASYNC_")) { + functionWithArguments += "(QSharedPointer)"; + } else { + functionWithArguments += "(QString,SocketListener*)"; + } + int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments); QString argument = line.remove(0, command.length() + 1); - if (indexOfMethod == -1) { - // Fallback: Try upper-case command - functionWithArguments = "command_" + command.toUpper() + "(QString,SocketListener*)"; - indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments); - } + if (command.startsWith("ASYNC_")) { + + auto arguments = argument.split('|'); + if (arguments.size() != 2) { + listener->sendMessage(QLatin1String("argument count is wrong")); + return; + } + + auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object(); - if (indexOfMethod != -1) { - staticMetaObject.method(indexOfMethod).invoke(this, Q_ARG(QString, argument), Q_ARG(SocketListener *, listener)); + auto jobId = arguments[0]; + + auto socketApiJob = QSharedPointer( + new SocketApiJob(jobId, listener, json), &QObject::deleteLater); + if (indexOfMethod != -1) { + staticMetaObject.method(indexOfMethod) + .invoke(this, Qt::QueuedConnection, + Q_ARG(QSharedPointer, socketApiJob)); + } else { + qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command + << "with argument:" << argument; + socketApiJob->reject("command not found"); + } } else { - qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument; + if (indexOfMethod != -1) { + staticMetaObject.method(indexOfMethod) + .invoke(this, Qt::QueuedConnection, Q_ARG(QString, argument), + Q_ARG(SocketListener *, listener)); + } else { + qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument; + } } } } @@ -1123,6 +1131,109 @@ DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile) return nullptr; } +#if GUI_TESTING +void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer &job) +{ + QString response; + for (auto &widget : QApplication::allWidgets()) { + auto objectName = widget->objectName(); + if (!objectName.isEmpty()) { + response += objectName + ":" + widget->property("text").toString() + ", "; + } + } + job->resolve(response); +} + +void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + + auto widget = findWidget(arguments["objectName"].toString()); + if (!widget) { + job->reject(QLatin1String("widget not found")); + return; + } + + QMetaObject::invokeMethod(widget, arguments["method"].toString().toLocal8Bit().constData()); + job->resolve(); +} + +void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job) +{ + auto widget = findWidget(job->arguments()[QLatin1String("objectName")].toString()); + if (!widget) { + job->reject(QLatin1String("widget not found")); + return; + } + + auto propertyName = job->arguments()[QLatin1String("property")].toString(); + + job->resolve(widget->property(propertyName.toLocal8Bit().constData()) + .toString() + .toLocal8Bit() + .constData()); +} + +void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + auto widget = findWidget(arguments["objectName"].toString()); + if (!widget) { + job->reject(QLatin1String("widget not found")); + return; + } + widget->setProperty(arguments["property"].toString().toLocal8Bit().constData(), + arguments["value"].toString().toLocal8Bit().constData()); + job->resolve(); +} + +void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + auto widget = findWidget(arguments["objectName"].toString()); + if (!widget) { + job->reject(QLatin1String("widget not found")); + return; + } + + ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); }); + + auto signalSignature = arguments["signalSignature"].toString(); + signalSignature.prepend("2"); + auto local8bit = signalSignature.toLocal8Bit(); + auto signalSignatureFinal = local8bit.constData(); + connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection); +} + +void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + + auto objectName = arguments["objectName"].toString(); + auto widget = findWidget(objectName); + if (!widget) { + job->reject(QLatin1String("widget not found: ") + objectName); + return; + } + + auto children = widget->findChildren(); + for (auto childWidget : children) { + // foo is the popupwidget! + auto actions = childWidget->actions(); + for (auto action : actions) { + if (action->objectName() == arguments["actionName"].toString()) { + action->trigger(); + + job->resolve("action found"); + return; + } + } + } + + job->reject("Action not found"); +} +#endif + QString SocketApi::buildRegisterPathMessage(const QString &path) { QFileInfo fi(path); diff --git a/src/gui/socketapi.h b/src/gui/socketapi.h index 6ef89f3ee..d24999019 100644 --- a/src/gui/socketapi.h +++ b/src/gui/socketapi.h @@ -12,7 +12,6 @@ * for more details. */ - #ifndef SOCKETAPI_H #define SOCKETAPI_H @@ -21,6 +20,8 @@ #include "sharedialog.h" // for the ShareDialogStartPage #include "common/syncjournalfilerecord.h" +#include "config.h" + #if defined(Q_OS_MAC) #include "socketapisocket_mac.h" #else @@ -38,6 +39,7 @@ class SyncFileStatus; class Folder; class SocketListener; class DirectEditor; +class SocketApiJob; /** * @brief The SocketApi class @@ -147,6 +149,15 @@ private: Q_INVOKABLE void command_EDIT(const QString &localFile, SocketListener *listener); DirectEditor* getDirectEditorForLocalFile(const QString &localFile); +#if GUI_TESTING + Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job); +#endif + QString buildRegisterPathMessage(const QString &path); QSet _registeredAliases; @@ -154,4 +165,5 @@ private: SocketApiServer _localServer; }; } + #endif // SOCKETAPI_H diff --git a/src/gui/socketapi_p.h b/src/gui/socketapi_p.h new file mode 100644 index 000000000..da3e90b9c --- /dev/null +++ b/src/gui/socketapi_p.h @@ -0,0 +1,142 @@ +/* + * Copyright (C) by Dominik Schmidt + * Copyright (C) by Klaas Freitag + * Copyright (C) by Roeland Jago Douma + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef SOCKETAPI_P_H +#define SOCKETAPI_P_H + +#include +#include +#include + +#include +#include + +#include +#include + +namespace OCC { + +class BloomFilter +{ + // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash). + // For a client navigating in less than 100 directories, this gives us a probability less than + // (1-e^(-2*100/1024))^2 = 0.03147872136 false positives. + const static int NumBits = 1024; + +public: + BloomFilter() + : hashBits(NumBits) + { + } + + void storeHash(uint hash) + { + hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range + hashBits.setBit((hash >> 16) % NumBits); // NOLINT + } + bool isHashMaybeStored(uint hash) const + { + return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT + && hashBits.testBit((hash >> 16) % NumBits); // NOLINT + } + +private: + QBitArray hashBits; +}; + +class SocketListener +{ +public: + QPointer socket; + + explicit SocketListener(QIODevice *socket) + : socket(socket) + { + } + + void sendMessage(const QString &message, bool doWait = false) const; + + void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const + { + if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash)) + sendMessage(message, false); + } + + void registerMonitoredDirectory(uint systemDirectoryHash) + { + _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash); + } + +private: + BloomFilter _monitoredDirectoriesBloomFilter; +}; + +class ListenerClosure : public QObject +{ + Q_OBJECT +public: + using CallbackFunction = std::function; + ListenerClosure(CallbackFunction callback) + : callback_(callback) + { + } + +public slots: + void closureSlot() + { + callback_(); + deleteLater(); + } + +private: + CallbackFunction callback_; +}; + +class SocketApiJob : public QObject +{ + Q_OBJECT +public: + SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments) + : _jobId(jobId) + , _socketListener(socketListener) + , _arguments(arguments) + { + } + + void resolve(const QString &response = QString()) + { + _socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response); + } + + void resolve(const QJsonObject &response) { resolve(QJsonDocument{ response }.toJson()); } + + const QJsonObject &arguments() { return _arguments; } + + void reject(const QString &response) + { + _socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response); + } + +private: + QString _jobId; + SocketListener *_socketListener; + QJsonObject _arguments; +}; +} + +Q_DECLARE_METATYPE(OCC::SocketListener *) + +#endif // SOCKETAPI_P_H diff --git a/src/gui/wizard/owncloudwizard.cpp b/src/gui/wizard/owncloudwizard.cpp index afc88a0d9..f38e28372 100644 --- a/src/gui/wizard/owncloudwizard.cpp +++ b/src/gui/wizard/owncloudwizard.cpp @@ -59,6 +59,8 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) , _resultPage(new OwncloudWizardResultPage) , _webViewPage(new WebViewPage(this)) { + setObjectName("owncloudWizard"); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setPage(WizardCommon::Page_ServerSetup, _setupPage); setPage(WizardCommon::Page_HttpCreds, _httpCredsPage); -- 2.30.2