Add GUI testing SocketApi extension

This commit is contained in:
Dominik Schmidt 2017-03-30 13:24:04 +02:00 committed by Christian Kamm
parent 50c8b7516c
commit 54d1bb95a0
8 changed files with 359 additions and 73 deletions

View File

@ -146,6 +146,8 @@ if(APPLE)
endif()
if(BUILD_CLIENT)
OPTION(GUI_TESTING "Build with gui introspection features of socket api" OFF)
if(APPLE)
find_package(Sparkle)
endif(APPLE)

View File

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

View File

@ -195,6 +195,7 @@ void AccountSettings::createAccountToolbox()
{
QMenu *menu = new QMenu();
_addAccountAction = new QAction(tr("Add new"), this);
_addAccountAction->setObjectName("addAccountAction");
menu->addAction(_addAccountAction);
connect(_addAccountAction, &QAction::triggered, this, &AccountSettings::slotOpenAccountWizard);

View File

@ -231,7 +231,11 @@ void SettingsDialog::accountAdded(AccountState *s)
}
_toolBar->insertAction(_toolBar->actions().at(0), 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);
@ -363,6 +367,10 @@ public:
}
QToolButton *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 "config.h"
#include "configfile.h"
@ -50,6 +51,13 @@
#include <QStringBuilder>
#include <QMessageBox>
#include <QFileDialog>
#include <QAction>
#include <QJsonDocument>
#include <QJsonObject>
#include <QWidget>
#include <QClipboard>
#include <QStandardPaths>
@ -58,10 +66,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)
{
@ -85,86 +113,35 @@ static QString buildMessage(const QString &verb, const QString &path, const QStr
}
return msg;
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcSocketApi, "gui.socketapi", QtInfoMsg)
Q_LOGGING_CATEGORY(lcPublicLink, "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);
hashBits.setBit((hash >> 16) % NumBits);
}
bool isHashMaybeStored(uint hash) const
{
return hashBits.testBit((hash & 0xFFFF) % NumBits)
&& hashBits.testBit((hash >> 16) % NumBits);
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
{
@ -181,6 +158,9 @@ SocketApi::SocketApi(QObject *parent)
{
QString socketPath;
qRegisterMetaType<SocketListener *>("SocketListener*");
qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
if (Utility::isWindows()) {
socketPath = QLatin1String("\\\\.\\pipe\\")
+ QLatin1String(APPLICATION_SHORTNAME)
@ -316,14 +296,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) {
staticMetaObject.method(indexOfMethod).invoke(this, Q_ARG(QString, argument), Q_ARG(SocketListener *, listener));
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();
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;
}
}
}
}
@ -1045,6 +1059,109 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
listener->sendMessage(QString("GET_MENU_ITEMS:END"));
}
#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
@ -37,6 +38,7 @@ namespace OCC {
class SyncFileStatus;
class Folder;
class SocketListener;
class SocketApiJob;
/**
* @brief The SocketApi class
@ -135,6 +137,15 @@ private:
*/
Q_INVOKABLE void command_GET_MENU_ITEMS(const QString &argument, SocketListener *listener);
#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;
@ -142,4 +153,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);
hashBits.setBit((hash >> 16) % NumBits);
}
bool isHashMaybeStored(uint hash) const
{
return hashBits.testBit((hash & 0xFFFF) % NumBits)
&& hashBits.testBit((hash >> 16) % NumBits);
}
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

@ -50,6 +50,8 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
, _credentialsPage(0)
, _setupLog()
{
setObjectName("owncloudWizard");
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setPage(WizardCommon::Page_ServerSetup, _setupPage);
setPage(WizardCommon::Page_HttpCreds, _httpCredsPage);