Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+12
View File
@@ -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 &currentRemoteState() { 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 &currentRemoteState() { 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;
}
};
+509
View File
@@ -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;
}
+150
View File
@@ -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);
}
}
}
}
+49
View File
@@ -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)
}