Forráskód Böngészése

Add GUI testing SocketApi extension

Dominik Schmidt 9 éve
szülő
commit
3288a36da6

+ 2 - 0
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()

+ 2 - 0
config.h.in

@@ -35,4 +35,6 @@
 #cmakedefine SHAREDIR "@SHAREDIR@"
 #cmakedefine PLUGINDIR "@PLUGINDIR@"
 
+#cmakedefine01 GUI_TESTING
+
 #endif

+ 9 - 1
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);

+ 187 - 76
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 <QMessageBox>
 #include <QInputDialog>
 #include <QFileDialog>
+
+
+#include <QAction>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QWidget>
+
 #include <QClipboard>
 #include <QDesktopServices>
 
@@ -62,10 +70,30 @@
 #include <CoreFoundation/CoreFoundation.h>
 #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<QIODevice> 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 *>("SocketListener*");
+    qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
+
     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<SocketApiJob>)";
+        } 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<SocketApiJob>(
+                new SocketApiJob(jobId, listener, json), &QObject::deleteLater);
+            if (indexOfMethod != -1) {
+                staticMetaObject.method(indexOfMethod)
+                    .invoke(this, Qt::QueuedConnection,
+                            Q_ARG(QSharedPointer<SocketApiJob>, 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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<QWidget *>();
+    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);

+ 13 - 1
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<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job);
+#endif
+
     QString buildRegisterPathMessage(const QString &path);
 
     QSet<QString> _registeredAliases;
@@ -154,4 +165,5 @@ private:
     SocketApiServer _localServer;
 };
 }
+
 #endif // SOCKETAPI_H

+ 142 - 0
src/gui/socketapi_p.h

@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * 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 <functional>
+#include <QBitArray>
+#include <QPointer>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include <memory>
+#include <QTimer>
+
+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<QIODevice> 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<void()>;
+    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

+ 2 - 0
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);