diff --git a/config.h.in b/config.h.in index 06a0a62c0..1e8f8ecdc 100644 --- a/config.h.in +++ b/config.h.in @@ -23,4 +23,6 @@ #cmakedefine SYSCONFDIR "@SYSCONFDIR@" #cmakedefine SHAREDIR "@SHAREDIR@" +#cmakedefine WITH_UNIT_TESTING 1 + #endif diff --git a/csync/CMakeLists.txt b/csync/CMakeLists.txt index 114f6f030..d34926c59 100644 --- a/csync/CMakeLists.txt +++ b/csync/CMakeLists.txt @@ -41,6 +41,8 @@ endif (MEM_NULL_TESTS) add_subdirectory(src) if (UNIT_TESTING) + set(WITH_UNIT_TESTING ON) + find_package(CMocka) if (CMOCKA_FOUND) include(AddCMockaTest) diff --git a/csync/ConfigureChecks.cmake b/csync/ConfigureChecks.cmake index 94f612999..6d64d5a63 100644 --- a/csync/ConfigureChecks.cmake +++ b/csync/ConfigureChecks.cmake @@ -62,8 +62,4 @@ if (WIN32) check_function_exists(__mingw_asprintf HAVE___MINGW_ASPRINTF) endif(WIN32) -if (UNIT_TESTING) - set(WITH_UNIT_TESTING ON) -endif (UNIT_TESTING) - set(CSYNC_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} CACHE INTERNAL "csync required system libraries") diff --git a/csync/src/csync_exclude.c b/csync/src/csync_exclude.c index fbe881c43..4f106b66f 100644 --- a/csync/src/csync_exclude.c +++ b/csync/src/csync_exclude.c @@ -40,7 +40,7 @@ #define CSYNC_LOG_CATEGORY_NAME "csync.exclude" #include "csync_log.h" -#ifndef NDEBUG +#ifndef WITH_UNIT_TESTING static #endif int _csync_exclude_add(c_strlist_t **inList, const char *string) { diff --git a/csync/src/csync_exclude.h b/csync/src/csync_exclude.h index ba2b1d321..1fe970cdd 100644 --- a/csync/src/csync_exclude.h +++ b/csync/src/csync_exclude.h @@ -34,7 +34,7 @@ enum csync_exclude_type_e { }; typedef enum csync_exclude_type_e CSYNC_EXCLUDE_TYPE; -#ifdef NDEBUG +#ifdef WITH_UNIT_TESTING int _csync_exclude_add(c_strlist_t **inList, const char *string); #endif diff --git a/src/libsync/excludedfiles.cpp b/src/libsync/excludedfiles.cpp index b2839d05d..6e1ee326e 100644 --- a/src/libsync/excludedfiles.cpp +++ b/src/libsync/excludedfiles.cpp @@ -45,6 +45,13 @@ void ExcludedFiles::addExcludeFilePath(const QString& path) _excludeFiles.insert(path); } +#ifdef WITH_UNIT_TESTING +void ExcludedFiles::addExcludeExpr(const QString &expr) +{ + _csync_exclude_add(_excludesPtr, expr.toLatin1().constData()); +} +#endif + bool ExcludedFiles::reloadExcludes() { c_strlist_destroy(*_excludesPtr); diff --git a/src/libsync/excludedfiles.h b/src/libsync/excludedfiles.h index 75895a396..7a3fa5d5d 100644 --- a/src/libsync/excludedfiles.h +++ b/src/libsync/excludedfiles.h @@ -57,6 +57,10 @@ public: const QString& basePath, bool excludeHidden) const; +#ifdef WITH_UNIT_TESTING + void addExcludeExpr(const QString &expr); +#endif + public slots: /** * Reloads the exclude patterns from the registered paths. diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 40df952c6..b0aa783eb 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -78,6 +78,10 @@ SyncEngine::SyncEngine(AccountPtr account, const QString& localPath, { qRegisterMetaType("SyncFileItem"); qRegisterMetaType("SyncFileItem::Status"); + qRegisterMetaType("SyncFileStatus"); + + // Everything in the SyncEngine expects a trailing slash for the localPath. + Q_ASSERT(localPath.endsWith(QLatin1Char('/'))); // We need to reconstruct the url because the path needs to be fully decoded, as csync will re-encode the path: // Remember that csync will just append the filename to the path and pass it to the vio plugin. diff --git a/src/libsync/syncfilestatus.cpp b/src/libsync/syncfilestatus.cpp index c2961f7f6..c81a64745 100644 --- a/src/libsync/syncfilestatus.cpp +++ b/src/libsync/syncfilestatus.cpp @@ -32,7 +32,7 @@ void SyncFileStatus::set(SyncFileStatusTag tag) _tag = tag; } -SyncFileStatus::SyncFileStatusTag SyncFileStatus::tag() +SyncFileStatus::SyncFileStatusTag SyncFileStatus::tag() const { return _tag; } @@ -42,7 +42,7 @@ void SyncFileStatus::setSharedWithMe(bool isShared) _sharedWithMe = isShared; } -bool SyncFileStatus::sharedWithMe() +bool SyncFileStatus::sharedWithMe() const { return _sharedWithMe; } diff --git a/src/libsync/syncfilestatus.h b/src/libsync/syncfilestatus.h index fddcf1af0..83a658ce3 100644 --- a/src/libsync/syncfilestatus.h +++ b/src/libsync/syncfilestatus.h @@ -14,6 +14,8 @@ #ifndef SYNCFILESTATUS_H #define SYNCFILESTATUS_H +#include +#include #include #include "owncloudlib.h" @@ -39,10 +41,10 @@ public: SyncFileStatus(SyncFileStatusTag); void set(SyncFileStatusTag tag); - SyncFileStatusTag tag(); + SyncFileStatusTag tag() const; void setSharedWithMe( bool isShared ); - bool sharedWithMe(); + bool sharedWithMe() const; QString toSocketAPIString() const; private: @@ -50,6 +52,16 @@ private: bool _sharedWithMe; }; + +inline bool operator==(const SyncFileStatus &a, const SyncFileStatus &b) { + return a.tag() == b.tag() && a.sharedWithMe() == b.sharedWithMe(); } +inline bool operator!=(const SyncFileStatus &a, const SyncFileStatus &b) { + return !(a == b); +} +} + +Q_DECLARE_METATYPE(OCC::SyncFileStatus) + #endif // SYNCFILESTATUS_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f701700aa..40c964991 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,6 +42,10 @@ owncloud_add_test(FileSystem "") owncloud_add_test(ChecksumValidator "") owncloud_add_test(ExcludedFiles "") +if(NOT BUILD_WITH_QT4) + owncloud_add_test(SyncEngine "syncenginetestutils.h") + owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h") +endif(NOT BUILD_WITH_QT4) SET(FolderMan_SRC ../src/gui/folderman.cpp) list(APPEND FolderMan_SRC ../src/gui/folder.cpp ) diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h new file mode 100644 index 000000000..b6929db91 --- /dev/null +++ b/test/syncenginetestutils.h @@ -0,0 +1,693 @@ +/* + * 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 "creds/abstractcredentials.h" +#include "filesystem.h" +#include "syncengine.h" +#include "syncjournaldb.h" + +#include +#include +#include + +static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/"); + +namespace { +QString generateEtag() { + return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16); +} + +class PathComponents : public QStringList { +public: + PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { } + PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { } + + PathComponents parentDirComponents() const { + return PathComponents{mid(0, size() - 1)}; + } + PathComponents subComponents() const { return PathComponents{mid(1)}; } + QString pathRoot() const { return first(); } + QString fileName() const { return last(); } +}; +} + +class FileModifier +{ +public: + virtual ~FileModifier() { } + virtual void remove(const QString &relativePath) = 0; + virtual void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') = 0; + virtual void setContents(const QString &relativePath, char contentChar) = 0; + virtual void appendByte(const QString &relativePath) = 0; + virtual void mkdir(const QString &relativePath) = 0; +}; + +class DiskFileModifier : public FileModifier +{ + QDir _rootDir; +public: + DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { } + void remove(const QString &relativePath) override { + QFileInfo fi{_rootDir.filePath(relativePath)}; + if (fi.isFile()) + QVERIFY(_rootDir.remove(relativePath)); + else + QVERIFY(QDir{fi.filePath()}.removeRecursively()); + } + void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override { + QFile file{_rootDir.filePath(relativePath)}; + QVERIFY(!file.exists()); + file.open(QFile::WriteOnly); + file.write(QByteArray{}.fill(contentChar, size)); + file.close(); + // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs. + OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTime().addSecs(-30))); + } + void setContents(const QString &relativePath, char contentChar) override { + QFile file{_rootDir.filePath(relativePath)}; + QVERIFY(file.exists()); + qint64 size = file.size(); + file.open(QFile::WriteOnly); + file.write(QByteArray{}.fill(contentChar, size)); + } + void appendByte(const QString &relativePath) override { + QFile file{_rootDir.filePath(relativePath)}; + QVERIFY(file.exists()); + file.open(QFile::ReadWrite); + QByteArray contents = file.read(1); + file.seek(file.size()); + file.write(contents); + } + void mkdir(const QString &relativePath) override { + _rootDir.mkpath(relativePath); + } +}; + +class FileInfo : public FileModifier +{ +public: + static FileInfo A12_B12_C12_S12() { + FileInfo fi{QString{}, { + {QStringLiteral("A"), { + {QStringLiteral("a1"), 4}, + {QStringLiteral("a2"), 4} + }}, + {QStringLiteral("B"), { + {QStringLiteral("b1"), 16}, + {QStringLiteral("b2"), 16} + }}, + {QStringLiteral("C"), { + {QStringLiteral("c1"), 24}, + {QStringLiteral("c2"), 24} + }}, + }}; + FileInfo sharedFolder{QStringLiteral("S"), { + {QStringLiteral("s1"), 32}, + {QStringLiteral("s2"), 32} + }}; + sharedFolder.isShared = true; + sharedFolder.children[QStringLiteral("s1")].isShared = true; + sharedFolder.children[QStringLiteral("s2")].isShared = true; + fi.children.insert(sharedFolder.name, std::move(sharedFolder)); + return fi; + } + + FileInfo() = default; + FileInfo(const QString &name) : name{name} { } + FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { } + FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { } + FileInfo(const QString &name, const std::initializer_list &children) : name{name} { + QString p = path(); + for (const auto &source : children) { + auto &dest = this->children[source.name] = source; + dest.parentPath = p; + } + } + + void remove(const QString &relativePath) override { + const PathComponents pathComponents{relativePath}; + FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); + Q_ASSERT(parent); + parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(), + [&pathComponents](const FileInfo &fi){ return fi.name == pathComponents.fileName(); })); + } + + void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override { + create(relativePath, size, contentChar); + } + + void setContents(const QString &relativePath, char contentChar) override { + FileInfo *file = findInvalidatingEtags(relativePath); + Q_ASSERT(file); + file->contentChar = contentChar; + } + + void appendByte(const QString &relativePath) override { + FileInfo *file = findInvalidatingEtags(relativePath); + Q_ASSERT(file); + file->size += 1; + } + + void mkdir(const QString &relativePath) override { + createDir(relativePath); + } + + FileInfo *find(const PathComponents &pathComponents, const bool invalidateEtags = false) { + if (pathComponents.isEmpty()) { + if (invalidateEtags) + etag = generateEtag(); + return this; + } + QString childName = pathComponents.pathRoot(); + auto it = children.find(childName); + if (it != children.end()) { + auto file = it->find(pathComponents.subComponents(), invalidateEtags); + if (file && invalidateEtags) + // Update parents on the way back + etag = file->etag; + return file; + } + return nullptr; + } + + FileInfo *createDir(const QString &relativePath) { + const PathComponents pathComponents{relativePath}; + FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); + Q_ASSERT(parent); + FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName()}; + child.parentPath = parent->path(); + child.etag = generateEtag(); + return &child; + } + + FileInfo *create(const QString &relativePath, qint64 size, char contentChar) { + const PathComponents pathComponents{relativePath}; + FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); + Q_ASSERT(parent); + FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName(), size}; + child.parentPath = parent->path(); + child.contentChar = contentChar; + child.etag = generateEtag(); + return &child; + } + + bool operator<(const FileInfo &other) const { + return name < other.name; + } + + bool operator==(const FileInfo &other) const { + // Consider files to be equal between local<->remote as a user would. + return name == other.name + && isDir == other.isDir + && size == other.size + && contentChar == other.contentChar + && children == other.children; + } + + QString path() const { + return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name; + } + + QString name; + bool isDir = true; + bool isShared = false; + QDateTime lastModified = QDateTime::currentDateTime().addDays(-7); + QString etag = generateEtag(); + qint64 size = 0; + char contentChar = 'W'; + + // Sorted by name to be able to compare trees + QMap children; + QString parentPath; + +private: + FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) { + return find(pathComponents, true); + } +}; + +class FakePropfindReply : public QNetworkReply +{ + Q_OBJECT +public: + QByteArray payload; + + FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + // Don't care about the request and just return a full propfind + const QString davUri{QStringLiteral("DAV:")}; + const QString ocUri{QStringLiteral("http://owncloud.org/ns")}; + QBuffer buffer{&payload}; + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter xml( &buffer ); + xml.writeNamespace(davUri, "d"); + xml.writeNamespace(ocUri, "oc"); + xml.writeStartDocument(); + xml.writeStartElement(davUri, QStringLiteral("multistatus")); + auto writeFileResponse = [&](const FileInfo &fileInfo) { + xml.writeStartElement(davUri, QStringLiteral("response")); + + xml.writeTextElement(davUri, QStringLiteral("href"), "/owncloud/remote.php/webdav/" + fileInfo.path()); + xml.writeStartElement(davUri, QStringLiteral("propstat")); + xml.writeStartElement(davUri, QStringLiteral("prop")); + + if (fileInfo.isDir) { + xml.writeStartElement(davUri, QStringLiteral("resourcetype")); + xml.writeEmptyElement(davUri, QStringLiteral("collection")); + xml.writeEndElement(); // resourcetype + } else + xml.writeEmptyElement(davUri, QStringLiteral("resourcetype")); + + auto gmtDate = fileInfo.lastModified.toTimeZone(QTimeZone("GMT")); + auto stringDate = gmtDate.toString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"); + xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate); + xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size)); + xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag); + xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); + xml.writeEndElement(); // prop + xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK"); + xml.writeEndElement(); // propstat + xml.writeEndElement(); // response + }; + + Q_ASSERT(request.url().path().startsWith(sRootUrl.path())); + QString fileName = request.url().path().mid(sRootUrl.path().length()); + const FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + + writeFileResponse(*fileInfo); + foreach(const FileInfo &childFileInfo, fileInfo->children) + writeFileResponse(childFileInfo); + xml.writeEndElement(); // multistatus + xml.writeEndDocument(); + + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); + setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8"); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207); + setFinished(true); + emit metaDataChanged(); + if (bytesAvailable()) + emit readyRead(); + emit finished(); + } + + void abort() override { } + + qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); } + qint64 readData(char *data, qint64 maxlen) override { + qint64 len = std::min(qint64{payload.size()}, maxlen); + strncpy(data, payload.constData(), len); + payload.remove(0, len); + return len; + } +}; + +class FakePutReply : public QNetworkReply +{ + Q_OBJECT + FileInfo *fileInfo; +public: + FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + Q_ASSERT(request.url().path().startsWith(sRootUrl.path())); + QString fileName = request.url().path().mid(sRootUrl.path().length()); + if ((fileInfo = remoteRootFileInfo.find(fileName))) { + fileInfo->size = putPayload.size(); + fileInfo->contentChar = putPayload.at(0); + } else { + // Assume that the file is filled with the same character + fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0)); + } + + if (!fileInfo) { + abort(); + return; + } + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); + setRawHeader("ETag", fileInfo->etag.toLatin1()); + setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case. + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + emit metaDataChanged(); + emit finished(); + } + + void abort() override { } + qint64 readData(char *, qint64) override { return 0; } +}; + +class FakeMkcolReply : public QNetworkReply +{ + Q_OBJECT + FileInfo *fileInfo; +public: + FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + Q_ASSERT(request.url().path().startsWith(sRootUrl.path())); + QString fileName = request.url().path().mid(sRootUrl.path().length()); + fileInfo = remoteRootFileInfo.createDir(fileName); + + if (!fileInfo) { + abort(); + return; + } + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + // FIXME: setRawHeader("OC-FileId", fileInfo->???); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); + emit metaDataChanged(); + emit finished(); + } + + void abort() override { } + qint64 readData(char *, qint64) override { return 0; } +}; + +class FakeDeleteReply : public QNetworkReply +{ + Q_OBJECT +public: + FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + Q_ASSERT(request.url().path().startsWith(sRootUrl.path())); + QString fileName = request.url().path().mid(sRootUrl.path().length()); + remoteRootFileInfo.remove(fileName); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204); + emit metaDataChanged(); + emit finished(); + } + + void abort() override { } + qint64 readData(char *, qint64) override { return 0; } +}; + +class FakeGetReply : public QNetworkReply +{ + Q_OBJECT +public: + const FileInfo *fileInfo; + QByteArray payload; + + FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + Q_ASSERT(request.url().path().startsWith(sRootUrl.path())); + QString fileName = request.url().path().mid(sRootUrl.path().length()); + fileInfo = remoteRootFileInfo.find(fileName); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + payload.fill(fileInfo->contentChar, fileInfo->size); + setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); + setRawHeader("ETag", fileInfo->etag.toLatin1()); + emit metaDataChanged(); + if (bytesAvailable()) + emit readyRead(); + emit finished(); + } + + void abort() override { } + qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); } + + qint64 readData(char *data, qint64 maxlen) override { + qint64 len = std::min(qint64{payload.size()}, maxlen); + strncpy(data, payload.constData(), len); + payload.remove(0, len); + return len; + } +}; + +class FakeErrorReply : public QNetworkReply +{ + Q_OBJECT +public: + FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{parent} { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 500); + emit metaDataChanged(); + emit finished(); + } + + void abort() override { } + qint64 readData(char *, qint64) override { return 0; } +}; + +class FakeQNAM : public QNetworkAccessManager +{ + FileInfo _remoteRootFileInfo; + QStringList _errorPaths; +public: + FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo{std::move(initialRoot)} { } + FileInfo ¤tRemoteState() { return _remoteRootFileInfo; } + QStringList &errorPaths() { return _errorPaths; } + +protected: + QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, + QIODevice *outgoingData = 0) { + const QString fileName = request.url().path().mid(sRootUrl.path().length()); + if (_errorPaths.contains(fileName)) + return new FakeErrorReply{op, request, this}; + + auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute); + if (verb == QLatin1String("PROPFIND")) + // Ignore outgoingData always returning somethign good enough, works for now. + return new FakePropfindReply{_remoteRootFileInfo, op, request, this}; + else if (verb == QLatin1String("GET")) + return new FakeGetReply{_remoteRootFileInfo, op, request, this}; + else if (verb == QLatin1String("PUT")) + return new FakePutReply{_remoteRootFileInfo, op, request, outgoingData->readAll(), this}; + else if (verb == QLatin1String("MKCOL")) + return new FakeMkcolReply{_remoteRootFileInfo, op, request, this}; + else if (verb == QLatin1String("DELETE")) + return new FakeDeleteReply{_remoteRootFileInfo, op, request, this}; + else { + qDebug() << verb << outgoingData; + Q_UNREACHABLE(); + } + } +}; + +class FakeCredentials : public OCC::AbstractCredentials +{ + QNetworkAccessManager *_qnam; +public: + FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { } + virtual bool changed(AbstractCredentials *) const { return false; } + virtual QString authType() const { return "test"; } + virtual QString user() const { return "admin"; } + virtual QNetworkAccessManager* getQNAM() const { return _qnam; } + virtual bool ready() const { return true; } + virtual void fetchFromKeychain() { } + virtual void askFromUser() { } + virtual bool stillValid(QNetworkReply *) { return true; } + virtual void persist() { } + virtual void invalidateToken() { } + virtual void forgetSensitiveData() { } +}; + +class FakeFolder +{ + QTemporaryDir _tempDir; + DiskFileModifier _localModifier; + // FIXME: Clarify ownership, double delete + FakeQNAM *_fakeQnam; + OCC::AccountPtr _account; + std::unique_ptr _journalDb; + std::unique_ptr _syncEngine; + +public: + FakeFolder(const FileInfo &fileTemplate) + : _localModifier(_tempDir.path()) + { + // Needs to be done once + OCC::SyncEngine::minimumFileAgeForUpload = 0; + csync_set_log_level(11); + + QDir rootDir{_tempDir.path()}; + toDisk(rootDir, fileTemplate); + + _fakeQnam = new FakeQNAM(fileTemplate); + _account = OCC::Account::create(); + _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud"))); + _account->setCredentials(new FakeCredentials{_fakeQnam}); + + _journalDb.reset(new OCC::SyncJournalDb(localPath())); + _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), sRootUrl, "", _journalDb.get())); + + // A new folder will update the local file state database on first sync. + // To have a state matching what users will encounter, we have to a sync + // using an identical local/remote file tree first. + syncOnce(); + } + + OCC::SyncEngine &syncEngine() const { return *_syncEngine; } + + FileModifier &localModifier() { return _localModifier; } + FileModifier &remoteModifier() { return _fakeQnam->currentRemoteState(); } + FileInfo currentLocalState() { + QDir rootDir{_tempDir.path()}; + FileInfo rootTemplate; + fromDisk(rootDir, rootTemplate); + return rootTemplate; + } + + FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); } + + QStringList &serverErrorPaths() { return _fakeQnam->errorPaths(); } + + QString localPath() const { + // SyncEngine wants a trailing slash + if (_tempDir.path().endsWith('/')) + return _tempDir.path(); + return _tempDir.path() + '/'; + } + + void scheduleSync() { + // Have to be done async, else, an error before exec() does not terminate the event loop. + QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection); + } + + void execUntilBeforePropagation() { + QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&))); + QVERIFY(spy.wait()); + } + + void execUntilItemCompleted(const QString &relativePath) { + QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + QElapsedTimer t; + t.start(); + while (t.elapsed() < 5000) { + spy.clear(); + QVERIFY(spy.wait()); + for(const QList &args : spy) { + auto item = args[0].value(); + if (item.destination() == relativePath) + return; + } + } + QVERIFY(false); + } + + void execUntilFinished() { + QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool))); + QVERIFY(spy.wait()); + } + + void syncOnce() { + scheduleSync(); + execUntilFinished(); + } + +private: + static void toDisk(QDir &dir, const FileInfo &templateFi) { + foreach (const FileInfo &child, templateFi.children) { + if (child.isDir) { + QDir subDir(dir); + dir.mkdir(child.name); + subDir.cd(child.name); + toDisk(subDir, child); + } else { + QFile file{dir.filePath(child.name)}; + file.open(QFile::WriteOnly); + file.write(QByteArray{}.fill(child.contentChar, child.size)); + file.close(); + OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified)); + } + } + } + + static void fromDisk(QDir &dir, FileInfo &templateFi) { + foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { + if (diskChild.isDir()) { + QDir subDir = dir; + subDir.cd(diskChild.fileName()); + templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName()}); + fromDisk(subDir, templateFi.children.last()); + } else { + QFile f{diskChild.filePath()}; + f.open(QFile::ReadOnly); + char contentChar = f.read(1).at(0); + templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar}); + } + } + } +}; + +// QTest::toString overloads +namespace OCC { + inline char *toString(const SyncFileStatus &s) { + return QTest::toString(QString("SyncFileStatus(" + s.toSocketAPIString() + ")")); + } +} + +inline void addFiles(QStringList &dest, const FileInfo &fi) +{ + if (fi.isDir) { + dest += QString("%1 - dir").arg(fi.name); + foreach (const FileInfo &fi, fi.children) + addFiles(dest, fi); + } else { + dest += QString("%1 - %2 %3-bytes").arg(fi.name).arg(fi.size).arg(fi.contentChar); + } +} + +inline char *toString(const FileInfo &fi) +{ + QStringList files; + foreach (const FileInfo &fi, fi.children) + addFiles(files, fi); + return QTest::toString(QString("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(", "))); +} diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp new file mode 100644 index 000000000..55d2ce614 --- /dev/null +++ b/test/testsyncengine.cpp @@ -0,0 +1,125 @@ +/* + * 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. + * + */ + +#include +#include "syncenginetestutils.h" + +using namespace OCC; + +bool itemDidComplete(const QSignalSpy &spy, const QString &path) +{ + for(const QList &args : spy) { + SyncFileItem item = args[0].value(); + if (item.destination() == path) + return true; + } + return false; +} + +bool itemDidCompleteSuccessfully(const QSignalSpy &spy, const QString &path) +{ + for(const QList &args : spy) { + SyncFileItem item = args[0].value(); + if (item.destination() == path) + return item._status == SyncFileItem::Success; + } + return false; +} + +class TestSyncEngine : public QObject +{ + Q_OBJECT + +private slots: + void testFileDownload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.remoteModifier().insert("A/a0"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testFileUpload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.localModifier().insert("A/a0"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testDirDownload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.remoteModifier().mkdir("Y"); + fakeFolder.remoteModifier().mkdir("Z"); + fakeFolder.remoteModifier().insert("Z/d0"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testDirUpload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.localModifier().mkdir("Y"); + fakeFolder.localModifier().mkdir("Z"); + fakeFolder.localModifier().insert("Z/d0"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testLocalDelete() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.remoteModifier().remove("A/a1"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testRemoteDelete() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + fakeFolder.localModifier().remove("A/a1"); + fakeFolder.syncOnce(); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testEmlLocalChecksum() { + FakeFolder fakeFolder{FileInfo{}}; + fakeFolder.localModifier().insert("a1.eml", 64, 'A'); + fakeFolder.localModifier().insert("a2.eml", 64, 'A'); + fakeFolder.localModifier().insert("a3.eml", 64, 'A'); + // Upload and calculate the checksums + // fakeFolder.syncOnce(); + fakeFolder.syncOnce(); + + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &))); + // Touch the file without changing the content, shouldn't upload + fakeFolder.localModifier().setContents("a1.eml", 'A'); + // Change the content/size + fakeFolder.localModifier().setContents("a2.eml", 'B'); + fakeFolder.localModifier().appendByte("a3.eml"); + fakeFolder.syncOnce(); + + QVERIFY(!itemDidComplete(completeSpy, "a1.eml")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a2.eml")); + QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } +}; + +QTEST_GUILESS_MAIN(TestSyncEngine) +#include "testsyncengine.moc" diff --git a/test/testsyncfilestatustracker.cpp b/test/testsyncfilestatustracker.cpp new file mode 100644 index 000000000..35fdbbf20 --- /dev/null +++ b/test/testsyncfilestatustracker.cpp @@ -0,0 +1,420 @@ +/* + * 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. + * + */ + +#include +#include "syncenginetestutils.h" + +using namespace OCC; + +class StatusPushSpy : public QSignalSpy +{ + SyncEngine &_syncEngine; +public: + StatusPushSpy(SyncEngine &syncEngine) + : QSignalSpy(&syncEngine.syncFileStatusTracker(), SIGNAL(fileStatusChanged(const QString&, SyncFileStatus))) + , _syncEngine(syncEngine) + { } + + SyncFileStatus statusOf(const QString &relativePath) const { + QFileInfo file(_syncEngine.localPath(), relativePath); + // Start from the end to get the latest status + for (int i = size() - 1; i >= 0; --i) { + if (QFileInfo(at(i)[0].toString()) == file) + return at(i)[1].value(); + } + return SyncFileStatus(); + } +}; + +class TestSyncFileStatusTracker : public QObject +{ + Q_OBJECT + + void verifyThatPushMatchesPull(const FakeFolder &fakeFolder, const StatusPushSpy &statusSpy) { + QString root = fakeFolder.localPath(); + QDirIterator it(root, QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString filePath = it.next().mid(root.size()); + SyncFileStatus pushedStatus = statusSpy.statusOf(filePath); + if (pushedStatus != SyncFileStatus()) + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus(filePath), pushedStatus); + } + } + +private slots: + void parentsGetSyncStatusUploadDownload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.localModifier().appendByte("B/b1"); + fakeFolder.remoteModifier().appendByte("C/c1"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("C/c1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("B/b2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("C/c2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + statusSpy.clear(); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("C/c1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetSyncStatusNewFileUploadDownload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.localModifier().insert("B/b0"); + fakeFolder.remoteModifier().insert("C/c0"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("C/c0"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("B/b1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("C/c1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + statusSpy.clear(); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("C/c0"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetSyncStatusNewDirDownload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.remoteModifier().mkdir("D"); + fakeFolder.remoteModifier().insert("D/d0"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusSync)); + + fakeFolder.execUntilItemCompleted("D"); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "The mkdir completion shouldn't mark it as OK as long as its children aren't done syncing (https://github.com/owncloud/client/issues/4797).", Continue); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusSync)); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetSyncStatusNewDirUpload() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.localModifier().mkdir("D"); + fakeFolder.localModifier().insert("D/d0"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusSync)); + + fakeFolder.execUntilItemCompleted("D"); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "The mkdir completion shouldn't mark it as OK as long as its children aren't done syncing (https://github.com/owncloud/client/issues/4797).", Continue); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusSync)); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("D"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("D/d0"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetSyncStatusDeleteUpDown() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.remoteModifier().remove("B/b1"); + fakeFolder.localModifier().remove("C/c1"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + // Discovered as remotely removed, pending for local removal. + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "C/c1 was removed locally and the parent should ideally reflect this until it's deleted on the server.", Continue); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("B/b2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("C/c2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + statusSpy.clear(); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QEXPECT_FAIL("", "Probably same cause as the missing SyncFileStatus::StatusSync for C above.", Continue); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void warningStatusForExcludedFile() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.syncEngine().excludedFiles().addExcludeExpr("A/a1"); + fakeFolder.syncEngine().excludedFiles().addExcludeExpr("B"); + fakeFolder.localModifier().appendByte("A/a1"); + fakeFolder.localModifier().appendByte("B/b1"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + // FIXME: Uncomment, fails since A/a1 gets pushed an OK for some reason, but pulls an IGNORE + // verifyThatPushMatchesPull(fakeFolder, statusSpy); + QEXPECT_FAIL("", "We should have received a warning status but didn't yet.", Continue); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QEXPECT_FAIL("", "We should have received a warning status but didn't yet.", Continue); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QEXPECT_FAIL("", "csync will stop at ignored directories without traversing children, so we don't currently push the status for newly ignored children of an ignored directory.", Continue); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusWarning)); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QEXPECT_FAIL("", "csync will stop at ignored directories without traversing children, so we don't currently push the status for newly ignored children of an ignored directory.", Continue); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QEXPECT_FAIL("", "csync will stop at ignored directories without traversing children, so we don't currently push the status for newly ignored children of an ignored directory.", Continue); + QCOMPARE(statusSpy.statusOf("B/b2"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + statusSpy.clear(); + + // Clears the exclude expr above + fakeFolder.syncEngine().excludedFiles().reloadExcludes(); + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusSync)); + statusSpy.clear(); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B/b1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetWarningStatusForError() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.serverErrorPaths().append("A/a1"); + fakeFolder.serverErrorPaths().append("B/b0"); + fakeFolder.localModifier().appendByte("A/a1"); + fakeFolder.localModifier().insert("B/b0"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusSync)); + statusSpy.clear(); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusError)); + statusSpy.clear(); + + // Remove the error and start a second sync, the blacklist should kick in + fakeFolder.serverErrorPaths().clear(); + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + // A/a1 and B/b0 should be on the black list for the next few seconds + QEXPECT_FAIL("", "Only one blacklist item, we shouldn't be showing SYNC.", Continue); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusWarning)); + QEXPECT_FAIL("", "Only one blacklist item, we shouldn't be showing SYNC.", Continue); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QEXPECT_FAIL("", "Only one blacklist item, we shouldn't be showing SYNC.", Continue); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusError)); + statusSpy.clear(); + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusError)); + statusSpy.clear(); + + // Start a third sync, this time together with a real file to sync + fakeFolder.localModifier().appendByte("C/c1"); + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + // The root should show SYNC even though there is an error underneath, + // since C/c1 is syncing and the SYNC status has priority. + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "Only one blacklist item underneath, we shouldn't be showing SYNC.", Continue); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QEXPECT_FAIL("", "Only one blacklist item underneath, we shouldn't be showing SYNC.", Continue); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("C/c1"), SyncFileStatus(SyncFileStatus::StatusSync)); + statusSpy.clear(); + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a2"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(statusSpy.statusOf("C"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("C/c1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + statusSpy.clear(); + + // Another sync after clearing the blacklist entry, everything should return to order. + fakeFolder.syncEngine().journal()->wipeErrorBlacklistEntry("A/a1"); + fakeFolder.syncEngine().journal()->wipeErrorBlacklistEntry("B/b0"); + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "Weird, should be SYNC", Continue); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QEXPECT_FAIL("", "Weird, should be SYNC", Continue); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusSync)); + statusSpy.clear(); + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QEXPECT_FAIL("", "Probably since it didn't get SYNC above.", Continue); + QCOMPARE(statusSpy.statusOf("A"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("A/a1"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QEXPECT_FAIL("", "Probably since it didn't get SYNC above.", Continue); + QCOMPARE(statusSpy.statusOf("B"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("B/b0"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void parentsGetWarningStatusForError_SibblingStartsWithPath() { + // A is a parent of A/a1, but A/a is not even if it's a substring of A/a1 + FakeFolder fakeFolder{{QString{},{ + {QStringLiteral("A"), { + {QStringLiteral("a"), 4}, + {QStringLiteral("a1"), 4} + }}}}}; + fakeFolder.serverErrorPaths().append("A/a1"); + fakeFolder.localModifier().appendByte("A/a1"); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + // The SyncFileStatusTraker won't push any status for all of them, test with a pull. + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus(""), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a1"), SyncFileStatus(SyncFileStatus::StatusSync)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + + fakeFolder.execUntilFinished(); + // We use string matching for paths in the implementation, + // an error should affect only parents and not every path that starts with the problem path. + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus(""), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A"), SyncFileStatus(SyncFileStatus::StatusWarning)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a1"), SyncFileStatus(SyncFileStatus::StatusError)); + QCOMPARE(fakeFolder.syncEngine().syncFileStatusTracker().fileStatus("A/a"), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + } + + void sharedStatus() { + SyncFileStatus sharedUpToDateStatus(SyncFileStatus::StatusUpToDate); + sharedUpToDateStatus.setSharedWithMe(true); + + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.remoteModifier().insert("S/s0"); + fakeFolder.remoteModifier().appendByte("S/s1"); + StatusPushSpy statusSpy(fakeFolder.syncEngine()); + + fakeFolder.scheduleSync(); + fakeFolder.execUntilBeforePropagation(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusSync)); + // We don't care about the shared flag for the sync status, + // Mac and Windows won't show it and we can't know it for new files. + QCOMPARE(statusSpy.statusOf("S").tag(), SyncFileStatus::StatusSync); + QCOMPARE(statusSpy.statusOf("S/s0").tag(), SyncFileStatus::StatusSync); + QCOMPARE(statusSpy.statusOf("S/s1").tag(), SyncFileStatus::StatusSync); + + fakeFolder.execUntilFinished(); + verifyThatPushMatchesPull(fakeFolder, statusSpy); + QCOMPARE(statusSpy.statusOf(""), SyncFileStatus(SyncFileStatus::StatusUpToDate)); + QCOMPARE(statusSpy.statusOf("S"), sharedUpToDateStatus); + QEXPECT_FAIL("", "We currently only know if a new file is shared on the second sync, after a PROPFIND.", Continue); + QCOMPARE(statusSpy.statusOf("S/s0"), sharedUpToDateStatus); + QCOMPARE(statusSpy.statusOf("S/s1"), sharedUpToDateStatus); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } +}; + +QTEST_GUILESS_MAIN(TestSyncFileStatusTracker) +#include "testsyncfilestatustracker.moc"