Initial QSfera import
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
add_executable(test_helper test_helper.cpp)
|
||||
target_link_libraries(test_helper PUBLIC Qt::Core libsync)
|
||||
|
||||
add_library(syncenginetestutils STATIC syncenginetestutils.cpp testutils.cpp)
|
||||
target_link_libraries(syncenginetestutils PUBLIC QSferaGui Qt::Test)
|
||||
target_compile_definitions(syncenginetestutils PRIVATE TEST_HELPER_EXE="$<TARGET_FILE:test_helper>")
|
||||
set_source_files_properties(testutils.cpp PROPERTIES COMPILE_DEFINITIONS SOURCEDIR="${PROJECT_SOURCE_DIR}")
|
||||
|
||||
# testutilsloader.cpp uses Q_COREAPP_STARTUP_FUNCTION which can't used reliably in a static lib
|
||||
# therefore we compile it in the tests
|
||||
add_library(testutilsloader OBJECT testutilsloader.cpp)
|
||||
target_link_libraries(testutilsloader PUBLIC QSferaGui QSferaResources)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
* This software is in the public domain, furnished "as is", without technical
|
||||
* support, and with no warranty, express or implied, as to its usefulness for
|
||||
* any purpose.
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "account.h"
|
||||
#include "common/filesystembase.h"
|
||||
#include "common/syncjournaldb.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "folder.h"
|
||||
#include "libsync/vfs/vfs.h"
|
||||
#include "syncengine.h"
|
||||
#include "testutils.h"
|
||||
|
||||
|
||||
#include <QDir>
|
||||
#include <QMap>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace OCC::FileSystem::SizeLiterals;
|
||||
/*
|
||||
* TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this
|
||||
* only works when directly called from a QTest :-(
|
||||
*/
|
||||
|
||||
QString getFilePathFromUrl(const QUrl &url);
|
||||
|
||||
|
||||
inline QString generateEtag()
|
||||
{
|
||||
return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QString::number(QRandomGenerator::global()->generate(), 16);
|
||||
}
|
||||
|
||||
inline QByteArray generateFileId()
|
||||
{
|
||||
return QByteArray::number(QRandomGenerator::global()->generate(), 16);
|
||||
}
|
||||
|
||||
class PathComponents : public QStringList
|
||||
{
|
||||
public:
|
||||
PathComponents(const QString &path);
|
||||
PathComponents(const QStringList &pathComponents);
|
||||
|
||||
PathComponents parentDirComponents() const;
|
||||
PathComponents subComponents() const &;
|
||||
PathComponents subComponents() &&
|
||||
{
|
||||
removeFirst();
|
||||
return std::move(*this);
|
||||
}
|
||||
QString pathRoot() const { return first(); }
|
||||
QString fileName() const { return last(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The FileModifier class defines the interface for both the local on-disk modifier and the
|
||||
* remote in-memory modifier.
|
||||
*/
|
||||
class FileModifier
|
||||
{
|
||||
public:
|
||||
static constexpr auto DefaultFileSize = 64_B;
|
||||
static constexpr char DefaultContentChar = 'X';
|
||||
|
||||
virtual ~FileModifier() { }
|
||||
virtual void remove(const QString &relativePath) = 0;
|
||||
virtual void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) = 0;
|
||||
|
||||
virtual void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) = 0;
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete;
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete;
|
||||
|
||||
virtual void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) = 0;
|
||||
virtual void mkdir(const QString &relativePath) = 0;
|
||||
virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
|
||||
virtual void setModTime(const QString &relativePath, const QDateTime &modTime) = 0;
|
||||
};
|
||||
|
||||
class FakeFolder;
|
||||
|
||||
/**
|
||||
* @brief The DiskFileModifier class is a FileModifier for files on the local disk.
|
||||
*
|
||||
* It uses a helper program (`test_helper`) to do the actual modifications. When running without a
|
||||
* VFS, or when running with suffix-VFS, the changes are done before doing a sync. When running
|
||||
* with a VFS, the OS will do callbacks to the test program. So in this case the modifications are
|
||||
* done while the event loop is running, in order to process those OS callbacks.
|
||||
*
|
||||
* Note: these actions do NOT read content from files on disk: if the file would be dehydrated,
|
||||
* reading will cause a re-hydration, which means network requests, and some tests explicitly check
|
||||
* for the amount of requests.
|
||||
*/
|
||||
class DiskFileModifier : public FileModifier
|
||||
{
|
||||
QDir _rootDir;
|
||||
QStringList _processArguments;
|
||||
|
||||
public:
|
||||
DiskFileModifier(const QString &rootDirPath)
|
||||
: _rootDir(rootDirPath)
|
||||
{
|
||||
}
|
||||
void remove(const QString &relativePath) override;
|
||||
void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) override;
|
||||
void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) override;
|
||||
void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) override;
|
||||
|
||||
void mkdir(const QString &relativePath) override;
|
||||
void rename(const QString &from, const QString &to) override;
|
||||
void setModTime(const QString &relativePath, const QDateTime &modTime) override;
|
||||
|
||||
Q_REQUIRED_RESULT bool applyModifications();
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete;
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete;
|
||||
};
|
||||
|
||||
static inline qint64 defaultLastModified()
|
||||
{
|
||||
auto precise = QDateTime::currentDateTimeUtc().addDays(-7);
|
||||
time_t timeInSeconds = OCC::Utility::qDateTimeToTime_t(precise);
|
||||
return timeInSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The FileInfo class represents the remotely stored (on-server) content. To be able to
|
||||
* modify the content, this class is also the remote \c FileModifier.
|
||||
*/
|
||||
class FileInfo : public FileModifier
|
||||
{
|
||||
/// FIXME: we should make it explicit in the construtor if we're talking about a hydrated or a dehydrated file!
|
||||
/// FIXME: this class is both a remote folder, as the root folder which in turn is the remote FileModifier.
|
||||
/// This is a mess: unifying remote files/folders is ok, but because it's also the remote root (which
|
||||
/// should implement the FileModifier), this means that each single remote file/folder also implements
|
||||
/// this interface.
|
||||
public:
|
||||
static FileInfo A12_B12_C12_S12();
|
||||
|
||||
FileInfo() = default;
|
||||
FileInfo(const QString &name)
|
||||
: name { name }
|
||||
{
|
||||
}
|
||||
FileInfo(const QString &name, quint64 size)
|
||||
: name { name }
|
||||
, isDir { false }
|
||||
, fileSize(size)
|
||||
, contentSize { size }
|
||||
{
|
||||
}
|
||||
FileInfo(const QString &name, quint64 size, char contentChar)
|
||||
: name { name }
|
||||
, isDir { false }
|
||||
, fileSize(size)
|
||||
, contentSize { size }
|
||||
, contentChar { contentChar }
|
||||
{
|
||||
}
|
||||
FileInfo(const QString &name, const std::initializer_list<FileInfo> &children);
|
||||
|
||||
FileInfo &addChild(const FileInfo &info);
|
||||
|
||||
void remove(const QString &relativePath) override;
|
||||
|
||||
void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) override;
|
||||
|
||||
void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) override;
|
||||
|
||||
void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) override;
|
||||
|
||||
void mkdir(const QString &relativePath) override;
|
||||
|
||||
void rename(const QString &oldPath, const QString &newPath) override;
|
||||
|
||||
void setModTime(const QString &relativePath, const QDateTime &modTime) override;
|
||||
|
||||
/// Return a pointer to the FileInfo, or a nullptr if it doesn't exist
|
||||
FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false);
|
||||
FileInfo *find(const QString &pathComponents, const bool invalidateEtags = false) { return find(PathComponents{pathComponents}, invalidateEtags); }
|
||||
|
||||
FileInfo *createDir(const QString &relativePath);
|
||||
|
||||
FileInfo *create(const QString &relativePath, quint64 size, char contentChar);
|
||||
|
||||
bool operator<(const FileInfo &other) const
|
||||
{
|
||||
return name < other.name;
|
||||
}
|
||||
|
||||
enum CompareWhat {
|
||||
CompareLastModified,
|
||||
IgnoreLastModified,
|
||||
};
|
||||
|
||||
bool operator==(const FileInfo &other) const { return equals(other, CompareLastModified); }
|
||||
|
||||
bool equals(const FileInfo &other, CompareWhat compareWhat) const;
|
||||
|
||||
bool operator!=(const FileInfo &other) const
|
||||
{
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
QDateTime lastModified() const { return QDateTime::fromSecsSinceEpoch(_lastModifiedInSecondsUTC, QTimeZone::LocalTime); }
|
||||
|
||||
QDateTime lastModifiedInUtc() const { return QDateTime::fromSecsSinceEpoch(_lastModifiedInSecondsUTC, QTimeZone::UTC); }
|
||||
|
||||
void setLastModified(const QDateTime &t)
|
||||
{
|
||||
_lastModifiedInSecondsUTC = t.toSecsSinceEpoch();
|
||||
}
|
||||
|
||||
qint64 lastModifiedInSecondsUTC() const
|
||||
{
|
||||
return _lastModifiedInSecondsUTC;
|
||||
}
|
||||
|
||||
void setLastModifiedFromSecondsUTC(qint64 utcSecs)
|
||||
{
|
||||
_lastModifiedInSecondsUTC = utcSecs;
|
||||
}
|
||||
|
||||
QString path() const;
|
||||
QString absolutePath() const;
|
||||
|
||||
void fixupParentPathRecursively();
|
||||
|
||||
QString name;
|
||||
bool isDir = true;
|
||||
bool isShared = false;
|
||||
OCC::RemotePermissions permissions; // When uset, defaults to everything
|
||||
qint64 _lastModifiedInSecondsUTC = defaultLastModified();
|
||||
QString etag = generateEtag();
|
||||
QByteArray fileId = generateFileId();
|
||||
QByteArray checksums = OCC::CheckSums::toString(OCC::CheckSums::Algorithm::DUMMY_FOR_TESTS).data() + QByteArrayLiteral(":0x1");
|
||||
QByteArray extraDavProperties;
|
||||
quint64 fileSize = 0;
|
||||
quint64 contentSize = 0;
|
||||
char contentChar = 'W';
|
||||
bool isDehydratedPlaceholder = false;
|
||||
|
||||
// Sorted by name to be able to compare trees
|
||||
QMap<QString, FileInfo> children;
|
||||
QString parentPath;
|
||||
|
||||
FileInfo *findInvalidatingEtags(PathComponents pathComponents);
|
||||
|
||||
friend inline QDebug operator<<(QDebug dbg, const FileInfo &fi)
|
||||
{
|
||||
return dbg.nospace().noquote()
|
||||
<< "{ '" << fi.path() << "': "
|
||||
<< ", isDir:" << fi.isDir
|
||||
<< QStringLiteral(", lastModified: %1 (%2)").arg(QString::number(fi._lastModifiedInSecondsUTC), fi.lastModifiedInUtc().toString())
|
||||
<< ", fileSize:" << fi.fileSize
|
||||
<< ", contentSize:" << fi.contentSize
|
||||
<< QStringLiteral(", contentChar: 0x%1").arg(QString::number(int(fi.contentChar), 16))
|
||||
<< ", isDehydratedPlaceholder:" << fi.isDehydratedPlaceholder
|
||||
<< ", children:" << fi.children
|
||||
<< " }";
|
||||
}
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete;
|
||||
|
||||
// prevent implicit cast to quint64
|
||||
template <typename T>
|
||||
void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete;
|
||||
};
|
||||
|
||||
class FakeReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FakeReply(QObject *parent);
|
||||
virtual ~FakeReply() override;
|
||||
|
||||
// useful to be public for testing
|
||||
using QNetworkReply::setAttribute;
|
||||
using QNetworkReply::setRawHeader;
|
||||
|
||||
void checkedFinished();
|
||||
|
||||
virtual void abort() override;
|
||||
};
|
||||
|
||||
class FakePropfindReply : public FakeReply
|
||||
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QByteArray payload;
|
||||
|
||||
FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
Q_INVOKABLE void respond();
|
||||
|
||||
Q_INVOKABLE void respond404();
|
||||
|
||||
qint64 bytesAvailable() const override;
|
||||
qint64 readData(char *data, qint64 maxlen) override;
|
||||
};
|
||||
|
||||
class FakePutReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent);
|
||||
|
||||
static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload);
|
||||
|
||||
Q_INVOKABLE virtual void respond();
|
||||
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
|
||||
private:
|
||||
FileInfo *fileInfo;
|
||||
};
|
||||
|
||||
class FakeMkcolReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
Q_INVOKABLE void respond();
|
||||
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
|
||||
private:
|
||||
FileInfo *fileInfo;
|
||||
};
|
||||
|
||||
class FakeDeleteReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
Q_INVOKABLE void respond();
|
||||
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
class FakeMoveReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
Q_INVOKABLE void respond();
|
||||
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
class FakeGetReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class State {
|
||||
Ok,
|
||||
Aborted,
|
||||
FileNotFound,
|
||||
};
|
||||
Q_ENUM(State);
|
||||
|
||||
const FileInfo *fileInfo;
|
||||
char payload;
|
||||
qint64 size;
|
||||
State state = State::Ok;
|
||||
const std::pair<qint64, qint64> _range;
|
||||
|
||||
FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
virtual void respond();
|
||||
|
||||
void abort() override;
|
||||
virtual qint64 bytesAvailable() const override;
|
||||
|
||||
virtual qint64 readData(char *data, qint64 maxlen) override;
|
||||
|
||||
static std::pair<qint64, qint64> parseRange(const QNetworkRequest &request);
|
||||
};
|
||||
|
||||
class FakePayloadReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
|
||||
const QByteArray &body, QObject *parent);
|
||||
|
||||
virtual void respond();
|
||||
|
||||
qint64 readData(char *buf, qint64 max) override;
|
||||
qint64 bytesAvailable() const override;
|
||||
QByteArray _body;
|
||||
};
|
||||
|
||||
|
||||
class FakeErrorReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
|
||||
QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray());
|
||||
|
||||
Q_INVOKABLE virtual void respond();
|
||||
|
||||
// make public to give tests easy interface
|
||||
using QNetworkReply::setAttribute;
|
||||
using QNetworkReply::setError;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotSetFinished();
|
||||
|
||||
public:
|
||||
qint64 readData(char *buf, qint64 max) override;
|
||||
qint64 bytesAvailable() const override;
|
||||
|
||||
QByteArray _body;
|
||||
};
|
||||
|
||||
// A reply that never responds
|
||||
class FakeHangingReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
|
||||
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
// A delayed reply
|
||||
template <class OriginalReply>
|
||||
class DelayedReply : public OriginalReply
|
||||
{
|
||||
public:
|
||||
template <typename... Args>
|
||||
explicit DelayedReply(const std::chrono::milliseconds delayMS, Args &&...args)
|
||||
: OriginalReply(std::forward<Args>(args)...)
|
||||
, _delayMs(delayMS)
|
||||
{
|
||||
}
|
||||
std::chrono::milliseconds _delayMs;
|
||||
|
||||
void respond() override
|
||||
{
|
||||
QTimer::singleShot(_delayMs, static_cast<OriginalReply *>(this), [this] {
|
||||
// Explicit call to bases's respond();
|
||||
this->OriginalReply::respond();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class FakeAM : public OCC::AccessManager
|
||||
{
|
||||
public:
|
||||
using Override = std::function<QNetworkReply *(Operation, const QNetworkRequest &, QIODevice *)>;
|
||||
|
||||
private:
|
||||
FileInfo _remoteRootFileInfo;
|
||||
// maps a path to an HTTP error
|
||||
QHash<QString, int> _errorPaths;
|
||||
// monitor requests and optionally provide custom replies
|
||||
Override _override;
|
||||
|
||||
public:
|
||||
FakeAM(FileInfo initialRoot, QObject *parent);
|
||||
FileInfo ¤tRemoteState() { return _remoteRootFileInfo; }
|
||||
|
||||
QHash<QString, int> &errorPaths() { return _errorPaths; }
|
||||
|
||||
void setOverride(const Override &override) { _override = override; }
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
|
||||
QIODevice *outgoingData = nullptr) override;
|
||||
};
|
||||
|
||||
class FakeCredentials : public OCC::AbstractCredentials
|
||||
{
|
||||
public:
|
||||
FakeCredentials(OCC::AccessManager *am)
|
||||
: _am { am }
|
||||
{
|
||||
}
|
||||
|
||||
OCC::AccessManager *createAM() const override { return _am; }
|
||||
bool ready() const override { return true; }
|
||||
void fetchFromKeychain() override { }
|
||||
void restartOauth() override { }
|
||||
void persist() override { }
|
||||
void invalidateToken() override { }
|
||||
void forgetSensitiveData() override { }
|
||||
|
||||
private:
|
||||
OCC::AccessManager *_am;
|
||||
};
|
||||
|
||||
class FakeFolder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
const QTemporaryDir _tempDir = OCC::TestUtils::createTempDir();
|
||||
DiskFileModifier _localModifier;
|
||||
FakeAM *_fakeAm;
|
||||
OCC::TestUtils::TestUtilsPrivate::AccountStateRaii _accountState =
|
||||
OCC::TestUtils::TestUtilsPrivate::AccountStateRaii{nullptr, &OCC::TestUtils::TestUtilsPrivate::accountStateDeleter};
|
||||
std::unique_ptr<OCC::SyncJournalDb> _journalDb;
|
||||
std::unique_ptr<OCC::SyncEngine> _syncEngine;
|
||||
|
||||
public:
|
||||
FakeFolder(const FileInfo &fileTemplate, OCC::Vfs::Mode vfsMode = OCC::Vfs::Mode::Off, bool filesAreDehydrated = false);
|
||||
~FakeFolder();
|
||||
|
||||
void switchToVfs(QSharedPointer<OCC::Vfs> vfs);
|
||||
|
||||
OCC::AccountPtr account() const { return _accountState->account(); }
|
||||
OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
|
||||
OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
|
||||
|
||||
FileModifier &localModifier() { return _localModifier; }
|
||||
FileInfo &remoteModifier() { return _fakeAm->currentRemoteState(); }
|
||||
FileInfo currentLocalState();
|
||||
|
||||
FileInfo ¤tRemoteState() { return _fakeAm->currentRemoteState(); }
|
||||
FileInfo dbState() const;
|
||||
|
||||
struct ErrorList
|
||||
{
|
||||
FakeAM *_qnam;
|
||||
void append(const QString &path, int error = 500)
|
||||
{
|
||||
_qnam->errorPaths().insert(path, error);
|
||||
}
|
||||
void clear() { _qnam->errorPaths().clear(); }
|
||||
};
|
||||
ErrorList serverErrorPaths() { return { _fakeAm }; }
|
||||
void setServerOverride(const FakeAM::Override &override) { _fakeAm->setOverride(override); }
|
||||
|
||||
QString localPath() const;
|
||||
|
||||
void scheduleSync();
|
||||
|
||||
void execUntilBeforePropagation();
|
||||
|
||||
void execUntilItemCompleted(const QString &relativePath);
|
||||
|
||||
bool execUntilFinished();
|
||||
|
||||
bool syncOnce();
|
||||
|
||||
Q_REQUIRED_RESULT bool applyLocalModificationsAndSync()
|
||||
{
|
||||
if (!_localModifier.applyModifications()) {
|
||||
return false;
|
||||
}
|
||||
return syncOnce();
|
||||
}
|
||||
|
||||
Q_REQUIRED_RESULT bool applyLocalModificationsWithoutSync()
|
||||
{
|
||||
return _localModifier.applyModifications();
|
||||
}
|
||||
|
||||
bool isDehydratedPlaceholder(const QString &filePath);
|
||||
QSharedPointer<OCC::Vfs> vfs() const;
|
||||
|
||||
private:
|
||||
static void toDisk(QDir &dir, const FileInfo &templateFi);
|
||||
|
||||
void fromDisk(QDir &dir, FileInfo &templateFi);
|
||||
};
|
||||
|
||||
|
||||
/* Return the FileInfo for a conflict file for the specified relative filename */
|
||||
inline const FileInfo *findConflict(FileInfo &dir, const QString &filename)
|
||||
{
|
||||
QFileInfo info(filename);
|
||||
const FileInfo *parentDir = dir.find(info.path());
|
||||
if (!parentDir)
|
||||
return nullptr;
|
||||
QString start = info.baseName() + QStringLiteral(" (conflicted copy");
|
||||
for (const auto &item : parentDir->children) {
|
||||
if (item.name.startsWith(start)) {
|
||||
return &item;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct ItemCompletedSpy : QSignalSpy
|
||||
{
|
||||
explicit ItemCompletedSpy(FakeFolder &folder)
|
||||
: QSignalSpy(&folder.syncEngine(), &OCC::SyncEngine::itemCompleted)
|
||||
{
|
||||
}
|
||||
|
||||
OCC::SyncFileItemPtr findItem(const QString &path) const;
|
||||
};
|
||||
|
||||
// QTest::toString overloads
|
||||
namespace OCC {
|
||||
char *toString(const SyncFileStatus &s);
|
||||
}
|
||||
|
||||
void addFiles(QStringList &dest, const FileInfo &fi);
|
||||
|
||||
QString toStringNoElide(const FileInfo &fi);
|
||||
|
||||
inline char *toString(const FileInfo &fi)
|
||||
{
|
||||
return QTest::toString(toStringNoElide(fi));
|
||||
}
|
||||
|
||||
void addFilesDbData(QStringList &dest, const FileInfo &fi);
|
||||
|
||||
char *printDbData(const FileInfo &fi);
|
||||
|
||||
struct OperationCounter
|
||||
{
|
||||
int nGET = 0;
|
||||
int nPUT = 0;
|
||||
int nMOVE = 0;
|
||||
int nDELETE = 0;
|
||||
|
||||
OperationCounter() {};
|
||||
OperationCounter(const OperationCounter &) = delete;
|
||||
OperationCounter(OperationCounter &&) = delete;
|
||||
void operator=(OperationCounter const &) = delete;
|
||||
void operator=(OperationCounter &&) = delete;
|
||||
|
||||
// for debugging, a list of requests by operator
|
||||
QMap<QByteArray, QList<QUrl>> requests;
|
||||
|
||||
void reset()
|
||||
{
|
||||
nGET = 0;
|
||||
nPUT = 0;
|
||||
nMOVE = 0;
|
||||
nDELETE = 0;
|
||||
requests.clear();
|
||||
}
|
||||
|
||||
QNetworkReply *serverOverride(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *)
|
||||
{
|
||||
const auto customVerb = req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
|
||||
requests[customVerb].append(req.url());
|
||||
if (op == QNetworkAccessManager::GetOperation) {
|
||||
++nGET;
|
||||
} else if (op == QNetworkAccessManager::PutOperation) {
|
||||
++nPUT;
|
||||
} else if (op == QNetworkAccessManager::DeleteOperation) {
|
||||
++nDELETE;
|
||||
} else if (customVerb == QByteArrayLiteral("MOVE")) {
|
||||
++nMOVE;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
auto functor()
|
||||
{
|
||||
return [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { return serverOverride(op, req, device); };
|
||||
}
|
||||
|
||||
OperationCounter(FakeFolder &fakeFolder)
|
||||
{
|
||||
fakeFolder.setServerOverride(functor());
|
||||
}
|
||||
|
||||
friend inline QDebug operator<<(QDebug dbg, const OperationCounter &oc)
|
||||
{
|
||||
return dbg << "nGET:" << oc.nGET << " nPUT:" << oc.nPUT << " nMOVE:" << oc.nMOVE << " nDELETE:" << oc.nDELETE;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,509 @@
|
||||
/*
|
||||
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "common/utility.h"
|
||||
#include "libsync/filesystem.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "common/utility_win.h"
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* IMPORTANT: the commands below do NOT read any data from files when modifying them!
|
||||
*
|
||||
* When a file is dehydrated, reading will cause a re-hydration (download), and many tests check
|
||||
* for the number of network requests.
|
||||
*/
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
bool writeToFile(std::string_view command, const QString &fileName, QIODevice::OpenMode mode, const QByteArray &data)
|
||||
{
|
||||
#ifndef Q_OS_WIN
|
||||
QFile f(fileName);
|
||||
if (!f.open(mode)) {
|
||||
cerr << "Error: cannot open file '" << qPrintable(fileName) << "' for " << command << " command: "
|
||||
<< qPrintable(f.errorString()) << endl;
|
||||
return false;
|
||||
}
|
||||
const auto written = f.write(data);
|
||||
|
||||
if (mode & QFile::Append) {
|
||||
if (!f.seek(f.size())) {
|
||||
cerr << "Error: cannot seek to EOF in '" << qPrintable(fileName) << "' for " << command << " command" << endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (written != data.size()) {
|
||||
cerr << "Error: wrote " << written << " bytes to '" << qPrintable(fileName) << "' instead of requested " << data.size() << " bytes" << endl;
|
||||
return false;
|
||||
}
|
||||
f.close();
|
||||
#else
|
||||
// Qt bug: [INSERT BUG HERE]
|
||||
// When opening a cloud file results in a cfapi error, Qt does not report an error.
|
||||
// Writes will succeed but not be committed to the file.
|
||||
// Reads will always return 0
|
||||
|
||||
const QFileInfo info(fileName);
|
||||
DWORD creation = OPEN_ALWAYS;
|
||||
if (mode & QIODevice::Truncate) {
|
||||
creation = TRUNCATE_EXISTING;
|
||||
if (!info.exists()) {
|
||||
cerr << "Error: truncating a non existing file '" << qPrintable(fileName) << "' for " << command << " command" << endl;
|
||||
return false;
|
||||
}
|
||||
} else if (mode & QIODevice::Append) {
|
||||
creation = OPEN_EXISTING;
|
||||
if (!info.exists()) {
|
||||
cerr << "Error: appending to non existing file '" << qPrintable(fileName) << "' for " << command << " command" << endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto handle = OCC::Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(fileName), {.accessMode = GENERIC_WRITE, .creationFlags = creation});
|
||||
|
||||
if (!handle) {
|
||||
cerr << "Error: cannot open file '" << qPrintable(fileName) << "' for " << command << " command: " << qPrintable(handle.errorMessage()) << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode & QFile::Append) {
|
||||
LARGE_INTEGER pos;
|
||||
pos.QuadPart = info.size();
|
||||
if (SetFilePointer(handle, pos.LowPart, &pos.HighPart, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {
|
||||
cerr << "Error: cannot seek to EOF in '" << qPrintable(fileName) << "' for " << command << " command" << endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
DWORD bytesWritten;
|
||||
if (!WriteFile(handle, data.constData(), data.size(), &bytesWritten, nullptr) || bytesWritten != data.size()) {
|
||||
const auto error = OCC::Utility::formatWinError(GetLastError());
|
||||
cerr << "Error: wrote " << bytesWritten << " bytes to '" << qPrintable(fileName) << "' instead of requested " << data.size() << " bytes. Error: " << qPrintable(error) << endl;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The abstract Command class. You know, from the pattern.
|
||||
*/
|
||||
class Command
|
||||
{
|
||||
public:
|
||||
Command(const QString &fileName)
|
||||
: _fileName(fileName)
|
||||
{
|
||||
}
|
||||
virtual ~Command() = 0;
|
||||
|
||||
virtual bool execute(QDir &rootDir) const = 0;
|
||||
|
||||
protected:
|
||||
static QString parseFileName(QStringListIterator &it)
|
||||
{
|
||||
return it.next();
|
||||
}
|
||||
|
||||
const QString _fileName;
|
||||
};
|
||||
|
||||
Command::~Command() { }
|
||||
|
||||
class SetMtimeCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "mtime";
|
||||
|
||||
SetMtimeCommand(const QString &fileName, qlonglong secs)
|
||||
: Command(fileName)
|
||||
, _secs(secs)
|
||||
{
|
||||
}
|
||||
~SetMtimeCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
auto modTime = QDateTime::fromSecsSinceEpoch(_secs);
|
||||
cerr << name << ", file: " << qPrintable(_fileName) << ", secs: " << _secs << endl;
|
||||
return OCC::FileSystem::setModTime(rootDir.filePath(_fileName), OCC::Utility::qDateTimeToTime_t(modTime));
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString secsStr = it.next();
|
||||
bool ok = false;
|
||||
auto secs = secsStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
cerr << "Error: '" << qPrintable(secsStr) << "' is not a valid number (" << name << ")" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
return new SetMtimeCommand(fileName, secs);
|
||||
}
|
||||
|
||||
private:
|
||||
const qlonglong _secs;
|
||||
};
|
||||
|
||||
class SetContentsCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "contents";
|
||||
|
||||
SetContentsCommand(const QString &fileName, qlonglong count, char ch)
|
||||
: Command(fileName)
|
||||
, _count(count)
|
||||
, _ch(ch)
|
||||
{
|
||||
}
|
||||
~SetContentsCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << endl;
|
||||
int count = _count == -1 ? 32 : _count;
|
||||
return writeToFile(name, rootDir.filePath(_fileName), QIODevice::WriteOnly | QIODevice::Truncate, QByteArray(count, _ch));
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString countStr = it.next();
|
||||
bool ok = false;
|
||||
auto count = countStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
cerr << "Error: '" << qPrintable(countStr) << "' is not a valid number (" << name << ")" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString charStr = it.next();
|
||||
if (charStr.size() != 1) {
|
||||
cerr << "Error: content for " << name << " command should be 1 character in size" << endl;
|
||||
}
|
||||
|
||||
return new SetContentsCommand(fileName, count, charStr.at(0).toLatin1());
|
||||
}
|
||||
|
||||
private:
|
||||
const qlonglong _count;
|
||||
const char _ch;
|
||||
};
|
||||
|
||||
class RenameCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "rename";
|
||||
|
||||
RenameCommand(const QString &fileName, const QString &newName)
|
||||
: Command(fileName)
|
||||
, _newName(newName)
|
||||
{
|
||||
}
|
||||
~RenameCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << endl;
|
||||
if (!rootDir.exists(_fileName)) {
|
||||
cerr << "File does not exist: " << qPrintable(rootDir.absoluteFilePath(_fileName)) << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = rootDir.rename(_fileName, _newName);
|
||||
if (!success) {
|
||||
cerr << "Rename of " << qPrintable(_fileName) << " failed" << endl;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString newName = it.next();
|
||||
if (newName.isEmpty()) {
|
||||
cerr << "Error: invalid new name for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
return new RenameCommand(fileName, newName);
|
||||
}
|
||||
|
||||
private:
|
||||
const QString _newName;
|
||||
};
|
||||
|
||||
class AppendByteCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "appendbyte";
|
||||
|
||||
AppendByteCommand(const QString &fileName, char ch)
|
||||
: Command(fileName)
|
||||
, _ch(ch)
|
||||
{
|
||||
}
|
||||
~AppendByteCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << endl;
|
||||
|
||||
if (_ch == '\0') {
|
||||
cerr << "Error: appending a NUL byte is probably a failure somewhere else." << endl;
|
||||
return false;
|
||||
}
|
||||
cerr << ".... file: " << qPrintable(_fileName) << ", byte: " << _ch << endl;
|
||||
return writeToFile(name, rootDir.filePath(_fileName), QIODevice::WriteOnly | QIODevice::Append, QByteArray(1, _ch));
|
||||
;
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString charStr = it.next();
|
||||
if (charStr.size() != 1) {
|
||||
cerr << "Error: content for " << name << " command should be 1 character in size" << endl;
|
||||
}
|
||||
|
||||
return new AppendByteCommand(fileName, charStr.at(0).toLatin1());
|
||||
}
|
||||
|
||||
private:
|
||||
const char _ch;
|
||||
};
|
||||
|
||||
class InsertCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "insert";
|
||||
|
||||
InsertCommand(const QString &fileName, qlonglong count, char ch)
|
||||
: Command(fileName)
|
||||
, _count(count)
|
||||
, _ch(ch)
|
||||
{
|
||||
}
|
||||
~InsertCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << " '" << qPrintable(_fileName) << "' with "
|
||||
<< _count << " " << _ch << " characters" << endl;
|
||||
if (QFileInfo::exists(rootDir.filePath(_fileName))) {
|
||||
cerr << "Error: file '" << qPrintable(_fileName) << "' for " << name << " command already exists" << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return writeToFile(name, rootDir.filePath(_fileName), QIODevice::WriteOnly, QByteArray(_count, _ch));
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString countStr = it.next();
|
||||
bool ok = false;
|
||||
auto count = countStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
cerr << "Error: '" << qPrintable(countStr) << "' is not a valid number (" << name << ")" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString charStr = it.next();
|
||||
if (charStr.size() != 1) {
|
||||
cerr << "Error: content for " << name << " command should be 1 character in size" << endl;
|
||||
}
|
||||
|
||||
return new InsertCommand(fileName, count, charStr.at(0).toLatin1());
|
||||
}
|
||||
|
||||
private:
|
||||
const qlonglong _count;
|
||||
const char _ch;
|
||||
};
|
||||
|
||||
class RemoveCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "remove";
|
||||
|
||||
RemoveCommand(const QString &fileName)
|
||||
: Command(fileName)
|
||||
{
|
||||
}
|
||||
~RemoveCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << endl;
|
||||
QFileInfo fi(rootDir.filePath(_fileName));
|
||||
if (fi.isFile()) {
|
||||
return rootDir.remove(_fileName);
|
||||
} else {
|
||||
return QDir(fi.filePath()).removeRecursively();
|
||||
}
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid filename for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
return new RemoveCommand(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
class MkdirCommand : public Command
|
||||
{
|
||||
public:
|
||||
static constexpr string_view name = "mkdir";
|
||||
|
||||
MkdirCommand(const QString &fileName)
|
||||
: Command(fileName)
|
||||
{
|
||||
}
|
||||
~MkdirCommand() override { }
|
||||
|
||||
bool execute(QDir &rootDir) const override
|
||||
{
|
||||
cerr << name << " " << qPrintable(_fileName) << endl;
|
||||
return rootDir.mkdir(_fileName);
|
||||
}
|
||||
|
||||
static Command *parse(QStringListIterator &it)
|
||||
{
|
||||
QString fileName = parseFileName(it);
|
||||
if (fileName.isEmpty()) {
|
||||
cerr << "Error: invalid directory name for " << name << " command" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
return new MkdirCommand(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief parseArguments Creates a list of commands to be executed.
|
||||
*
|
||||
* Each command's `parse` method reads the required arguments from the command-line by using the
|
||||
* iterator.
|
||||
*
|
||||
* @param it The iterator over the argument list
|
||||
* @return a list of commands to execute
|
||||
*/
|
||||
vector<Command *> parseArguments(QStringListIterator &it)
|
||||
{
|
||||
map<string_view, function<Command *(QStringListIterator &)>> parserFunctions = {
|
||||
{ SetMtimeCommand::name, SetMtimeCommand::parse },
|
||||
{ SetContentsCommand::name, SetContentsCommand::parse },
|
||||
{ RenameCommand::name, RenameCommand::parse },
|
||||
{ AppendByteCommand::name, AppendByteCommand::parse },
|
||||
{ InsertCommand::name, InsertCommand::parse },
|
||||
{ RemoveCommand::name, RemoveCommand::parse },
|
||||
{ MkdirCommand::name, MkdirCommand::parse },
|
||||
};
|
||||
|
||||
vector<Command *> commands;
|
||||
|
||||
while (it.hasNext()) {
|
||||
const QString option = it.next();
|
||||
auto pf = parserFunctions.find(option.toStdString());
|
||||
if (pf == parserFunctions.end()) {
|
||||
cerr << "Error: unknown command '" << qPrintable(option) << "'" << endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (auto cmd = (pf->second)(it)) {
|
||||
commands.emplace_back(cmd);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QStringListIterator it(app.arguments());
|
||||
if (it.hasNext()) {
|
||||
// skip program name
|
||||
it.next();
|
||||
}
|
||||
|
||||
QDir rootDir(it.next());
|
||||
|
||||
const auto commands = parseArguments(it);
|
||||
if (commands.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cerr << "Starting executing commands in '" << qPrintable(rootDir.absolutePath()) << "' ...:" << endl;
|
||||
|
||||
for (const auto &cmd : commands) {
|
||||
cerr << ".. Executing command: ";
|
||||
if (!cmd->execute(rootDir)) {
|
||||
return -2;
|
||||
}
|
||||
cerr << ".. command done." << endl;
|
||||
}
|
||||
|
||||
cerr << "Successfully executed all commands." << endl;
|
||||
|
||||
qDeleteAll(commands);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "testutils.h"
|
||||
|
||||
#include "common/checksums.h"
|
||||
#include "gui/accountmanager.h"
|
||||
#include "libsync/creds/httpcredentials.h"
|
||||
#include "resources/template.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QRandomGenerator>
|
||||
#include <QTest>
|
||||
|
||||
namespace {
|
||||
class HttpCredentialsTest : public OCC::HttpCredentials
|
||||
{
|
||||
public:
|
||||
HttpCredentialsTest(const QString &password)
|
||||
: HttpCredentials(password)
|
||||
{
|
||||
}
|
||||
|
||||
void restartOauth() override { }
|
||||
};
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace TestUtils {
|
||||
TestUtilsPrivate::AccountStateRaii createDummyAccount()
|
||||
{
|
||||
// ensure we have an instance of folder man
|
||||
std::ignore = folderMan();
|
||||
// don't use the account manager to create the account, it would try to use widgets
|
||||
auto acc = Account::create(QUuid::createUuid());
|
||||
HttpCredentialsTest *cred = new HttpCredentialsTest(QStringLiteral("secret"));
|
||||
acc->setCredentials(cred);
|
||||
acc->setUrl(QUrl(QStringLiteral("http://localhost/")));
|
||||
acc->setDavDisplayName(QStringLiteral("fakename") + acc->uuid().toString(QUuid::WithoutBraces));
|
||||
acc->setCapabilities({acc->url(), OCC::TestUtils::testCapabilities()});
|
||||
return {OCC::AccountManager::instance()->addAccount(acc).get(), &TestUtilsPrivate::accountStateDeleter};
|
||||
}
|
||||
|
||||
FolderDefinition createDummyFolderDefinition(const AccountPtr &acc, const QString &path)
|
||||
{
|
||||
auto d = OCC::FolderDefinition(acc->uuid(), Utility::concatUrlPath(dummyDavUrl(), path), {}, QStringLiteral("Dummy Folder"));
|
||||
d.setLocalPath(path);
|
||||
return d;
|
||||
}
|
||||
|
||||
QTemporaryDir createTempDir()
|
||||
{
|
||||
return QTemporaryDir{QStringLiteral("%1/QSfera-unit-test-%2-XXXXXX").arg(QDir::tempPath(), qApp->applicationName())};
|
||||
}
|
||||
|
||||
FolderMan *folderMan()
|
||||
{
|
||||
static QPointer<FolderMan> man;
|
||||
if (!man) {
|
||||
man = FolderMan::createInstance().release();
|
||||
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, man, &FolderMan::deleteLater);
|
||||
};
|
||||
return man;
|
||||
}
|
||||
|
||||
|
||||
bool writeRandomFile(const QString &fname, int size)
|
||||
{
|
||||
auto rg = QRandomGenerator::global();
|
||||
|
||||
const int maxSize = 10 * 10 * 1024;
|
||||
if (size == -1) {
|
||||
size = static_cast<int>(rg->generate() % maxSize);
|
||||
}
|
||||
|
||||
QString randString;
|
||||
for (int i = 0; i < size; i++) {
|
||||
int r = static_cast<int>(rg->generate() % 128);
|
||||
randString.append(QChar(r));
|
||||
}
|
||||
|
||||
QFile file(fname);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream out(&file);
|
||||
out << randString;
|
||||
// optional, as QFile destructor will already do it:
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QVariantMap testCapabilities(CheckSums::Algorithm algo)
|
||||
{
|
||||
static const auto algorithmNames = [] {
|
||||
QVariantList out;
|
||||
for (const auto &a : CheckSums::All) {
|
||||
out.append(Utility::enumToString(a.first));
|
||||
}
|
||||
return out;
|
||||
}();
|
||||
return {{QStringLiteral("core"),
|
||||
QVariantMap{{QStringLiteral("status"),
|
||||
QVariantMap{{QStringLiteral("installed"), QStringLiteral("1")}, {QStringLiteral("maintenance"), QStringLiteral("0")},
|
||||
{QStringLiteral("needsDbUpgrade"), QStringLiteral("0")}, {QStringLiteral("version"), QStringLiteral("10.11.0.0")},
|
||||
{QStringLiteral("versionstring"), QStringLiteral("10.11.0")}, {QStringLiteral("edition"), QStringLiteral("Community")},
|
||||
{QStringLiteral("productname"), QStringLiteral("QSfera")}, {QStringLiteral("product"), QStringLiteral("QSfera")},
|
||||
{QStringLiteral("productversion"), QStringLiteral("2.0.0-beta1+7c2e3201b")}}}}},
|
||||
{QStringLiteral("files"), QVariantList{}}, {QStringLiteral("dav"), QVariantMap{{QStringLiteral("chunking"), QStringLiteral("1.0")}}},
|
||||
{QStringLiteral("checksums"),
|
||||
QVariantMap{{QStringLiteral("preferredUploadType"), Utility::enumToString(algo)}, {QStringLiteral("supportedTypes"), algorithmNames}}}};
|
||||
}
|
||||
|
||||
QByteArray getPayload(QAnyStringView payloadName)
|
||||
{
|
||||
static QFileInfo info(QString::fromUtf8(QTest::currentAppName()));
|
||||
QFile f(QStringLiteral(SOURCEDIR "/test/%1/%2").arg(info.baseName(), payloadName.toString()));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
qFatal() << u"Failed to open file: " << f.fileName();
|
||||
}
|
||||
return f.readAll();
|
||||
}
|
||||
|
||||
QByteArray getPayloadTemplated(QAnyStringView payloadName, const Values &values)
|
||||
{
|
||||
return Resources::Template::renderTemplate(QString::fromUtf8(getPayload(payloadName)), values).toUtf8();
|
||||
}
|
||||
|
||||
SyncFileItem dummyItem(const QString &name)
|
||||
{
|
||||
SyncFileItem item(name);
|
||||
item._type = ItemTypeFile;
|
||||
item._fileId = "id";
|
||||
item._inode = 1;
|
||||
item._modtime = Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc());
|
||||
item._remotePerm = RemotePermissions::fromDbValue(" ");
|
||||
return item;
|
||||
}
|
||||
|
||||
QUrl dummyDavUrl()
|
||||
{
|
||||
return QUrl(QStringLiteral("http://localhost/dav/spaces/0e443965-2ebb-4673-9464-b2c1d388e666$cb867555-fdf7-48ce-8f1c-d64570812f21"));
|
||||
}
|
||||
|
||||
void TestUtilsPrivate::accountStateDeleter(OCC::AccountState *acc)
|
||||
{
|
||||
if (acc) {
|
||||
OCC::AccountManager::instance()->deleteAccount(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "account.h"
|
||||
#include "common/checksumalgorithms.h"
|
||||
#include "folder.h"
|
||||
#include "folderman.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace TestUtils {
|
||||
namespace TestUtilsPrivate {
|
||||
void accountStateDeleter(OCC::AccountState *acc);
|
||||
|
||||
using AccountStateRaii = std::unique_ptr<AccountState, decltype(&TestUtilsPrivate::accountStateDeleter)>;
|
||||
}
|
||||
|
||||
FolderMan *folderMan();
|
||||
FolderDefinition createDummyFolderDefinition(const AccountPtr &acc, const QString &path);
|
||||
TestUtilsPrivate::AccountStateRaii createDummyAccount();
|
||||
bool writeRandomFile(const QString &fname, int size = -1);
|
||||
|
||||
/***
|
||||
* Create a QTemporaryDir with a test specific name pattern
|
||||
* QSfera-unit-test-{TestName}-XXXXXX
|
||||
* This allows to clean up after failed tests
|
||||
*/
|
||||
QTemporaryDir createTempDir();
|
||||
|
||||
const QVariantMap testCapabilities(CheckSums::Algorithm algo = CheckSums::Algorithm::DUMMY_FOR_TESTS);
|
||||
|
||||
|
||||
QByteArray getPayload(QAnyStringView payloadName);
|
||||
|
||||
// we can't use QMap direclty with QFETCH
|
||||
using Values = QMap<QAnyStringView, QAnyStringView>;
|
||||
QByteArray getPayloadTemplated(QAnyStringView payloadName, const Values &values);
|
||||
|
||||
// creates a SyncFileItem that fulfills the minimal criteria to not trigger an assert
|
||||
SyncFileItem dummyItem(const QString &name);
|
||||
|
||||
QUrl dummyDavUrl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "configfile.h"
|
||||
#include "gui/networkinformation.h"
|
||||
#include "libsync/platform.h"
|
||||
#include "logger.h"
|
||||
#include "resources/loadresources.h"
|
||||
#include "testutils.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace {
|
||||
// create a platform object before qApp
|
||||
const auto platform = OCC::Platform::create(OCC::Platform::Type::Terminal);
|
||||
|
||||
void setUpTests()
|
||||
{
|
||||
Q_ASSERT(platform);
|
||||
// load the resources
|
||||
static const OCC::ResourcesLoader resources;
|
||||
|
||||
static auto dir = OCC::TestUtils::createTempDir();
|
||||
OCC::ConfigFile::setConfDir(QStringLiteral("%1/config").arg(dir.path())); // we don't want to pollute the user's config file
|
||||
|
||||
OCC::Logger::instance()->setLogFile(QStringLiteral("-"));
|
||||
OCC::Logger::instance()->addLogRule({ QStringLiteral("sync.httplogger=true") });
|
||||
OCC::Logger::instance()->setLogDebug(true);
|
||||
|
||||
OCC::Account::setCommonCacheDirectory(QStringLiteral("%1/cache").arg(dir.path()));
|
||||
|
||||
// ensure we have an instance of NetworkInformation
|
||||
OCC::NetworkInformation::instance();
|
||||
}
|
||||
|
||||
Q_COREAPP_STARTUP_FUNCTION(setUpTests)
|
||||
}
|
||||
Reference in New Issue
Block a user