Add GUI testing SocketApi extension

This commit is contained in:
Dominik Schmidt 2017-03-30 13:24:04 +02:00 committed by Kevin Ottens
parent e97784bb9d
commit 3288a36da6
No known key found for this signature in database
GPG key ID: 074BBBCB8DECC9E2
7 changed files with 357 additions and 78 deletions

View file

@ -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()

View file

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

View file

@ -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);

View file

@ -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
void SocketListener::sendMessage(const QString &message, bool doWait) const
{
// 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)
{
if (!socket) {
qCInfo(lcSocketApi) << "Not sending message to dead socket:" << message;
return;
}
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
qCInfo(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket;
QString localMessage = message;
if (!localMessage.endsWith(QLatin1Char('\n'))) {
localMessage.append(QLatin1Char('\n'));
}
private:
QBitArray hashBits;
};
class SocketListener
{
public:
QPointer<QIODevice> socket;
explicit SocketListener(QIODevice *socket)
: socket(socket)
{
QByteArray bytesToSend = localMessage.toUtf8();
qint64 sent = socket->write(bytesToSend);
if (doWait) {
socket->waitForBytesWritten(1000);
}
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;
}
if (sent != bytesToSend.length()) {
qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage;
}
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;
};
}
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_")) {
if (indexOfMethod != -1) {
staticMetaObject.method(indexOfMethod).invoke(this, Q_ARG(QString, argument), Q_ARG(SocketListener *, listener));
auto arguments = argument.split('|');
if (arguments.size() != 2) {
listener->sendMessage(QLatin1String("argument count is wrong"));
return;
}
auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object();
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);

View file

@ -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
src/gui/socketapi_p.h Normal file
View file

@ -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

View file

@ -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);