Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/owncloud/client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJocelyn Turcotte <jturcotte@woboq.com>2016-08-04 15:59:46 +0300
committerJocelyn Turcotte <jturcotte@woboq.com>2016-08-17 16:39:30 +0300
commitb7ff4a76e8643d6a23c9f36b4f3e4e0cf7373867 (patch)
treecd0fbde246ce7d19c7ff575ae5447d7266c3846e
parent2507ba981853b76fcd5054613f0ee65ce20a4422 (diff)
Add TestSyncEngine and TestSyncFileStatusTracker auto tests
To be able to test the SyncEngine efficiently, a set of server mocking classes have been implemented on top of QNetworkAccessManager. The local disk side hasn't been mocked since this would require adding a large abstraction layer in csync. The SyncEngine is instead pointed to a different temporary dir in each test and we test by interacting with files in this directory instead. The FakeFolder object wraps the SyncEngine with those abstractions and allow controlling the local files, and the fake remote state through the FileModifier interface, using a FileInfo tree structure for the remote-side implementation as well as feeding and comparing the states on both side in tests. Tests run fast and require no setup to be run, but each server feature that we want to test on the client side needs to be implemented in this fake objects library. For example, the OC-FileId header isn't set as of this commit, and we can't test the file move logic properly without implementing it first. The TestSyncFileStatusTracker tests already contain a few QEXPECT_FAIL for what I esteem being issues that need to be fixed in order to catch up on our test coverage without making this patch too huge.
-rw-r--r--config.h.in2
-rw-r--r--csync/CMakeLists.txt2
-rw-r--r--csync/ConfigureChecks.cmake4
-rw-r--r--csync/src/csync_exclude.c2
-rw-r--r--csync/src/csync_exclude.h2
-rw-r--r--src/libsync/excludedfiles.cpp7
-rw-r--r--src/libsync/excludedfiles.h4
-rw-r--r--src/libsync/syncengine.cpp4
-rw-r--r--src/libsync/syncfilestatus.cpp4
-rw-r--r--src/libsync/syncfilestatus.h16
-rw-r--r--test/CMakeLists.txt4
-rw-r--r--test/syncenginetestutils.h693
-rw-r--r--test/testsyncengine.cpp125
-rw-r--r--test/testsyncfilestatustracker.cpp420
14 files changed, 1279 insertions, 10 deletions
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>("SyncFileItem");
qRegisterMetaType<SyncFileItem::Status>("SyncFileItem::Status");
+ qRegisterMetaType<SyncFileStatus>("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 <QMetaType>
+#include <QObject>
#include <QString>
#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 <QDir>
+#include <QNetworkReply>
+#include <QtTest>
+
+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<FileInfo> &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<QString, FileInfo> 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 &currentRemoteState() { 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<OCC::SyncJournalDb> _journalDb;
+ std::unique_ptr<OCC::SyncEngine> _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<QVariant> &args : spy) {
+ auto item = args[0].value<OCC::SyncFileItem>();
+ 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 <QtTest>
+#include "syncenginetestutils.h"
+
+using namespace OCC;
+
+bool itemDidComplete(const QSignalSpy &spy, const QString &path)
+{
+ for(const QList<QVariant> &args : spy) {
+ SyncFileItem item = args[0].value<SyncFileItem>();
+ if (item.destination() == path)
+ return true;
+ }
+ return false;
+}
+
+bool itemDidCompleteSuccessfully(const QSignalSpy &spy, const QString &path)
+{
+ for(const QList<QVariant> &args : spy) {
+ SyncFileItem item = args[0].value<SyncFileItem>();
+ 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 <QtTest>
+#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<SyncFileStatus>();
+ }
+ 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"