From: Hannah von Reth Date: Thu, 10 Dec 2020 15:50:59 +0000 (+0100) Subject: Speedup test build by compile the fake server just once X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~21^2~468^2~9 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=6818b8e30323e4b8efab40796ca352342a7405aa;p=nextcloud-desktop.git Speedup test build by compile the fake server just once --- diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dc4100e97..8932d2b80 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,6 +14,10 @@ include_directories(${CMAKE_SOURCE_DIR}/src include(nextcloud_add_test.cmake) +set(CMAKE_AUTOMOC TRUE) +add_library(syncenginetestutils STATIC syncenginetestutils.cpp) +target_link_libraries(syncenginetestutils PUBLIC ${APPLICATION_EXECUTABLE}sync Qt5::Test) + nextcloud_add_test(NextcloudPropagator "") IF(BUILD_UPDATER) @@ -45,31 +49,31 @@ nextcloud_add_test(ClientSideEncryption "") nextcloud_add_test(ExcludedFiles "") nextcloud_add_test(Utility "") -nextcloud_add_test(SyncEngine "syncenginetestutils.h") -nextcloud_add_test(SyncVirtualFiles "syncenginetestutils.h") -nextcloud_add_test(SyncMove "syncenginetestutils.h") -nextcloud_add_test(SyncDelete "syncenginetestutils.h") -nextcloud_add_test(SyncConflict "syncenginetestutils.h") -nextcloud_add_test(SyncFileStatusTracker "syncenginetestutils.h") -nextcloud_add_test(Download "syncenginetestutils.h") -nextcloud_add_test(ChunkingNg "syncenginetestutils.h") -nextcloud_add_test(AsyncOp "syncenginetestutils.h") -nextcloud_add_test(UploadReset "syncenginetestutils.h") -nextcloud_add_test(AllFilesDeleted "syncenginetestutils.h") -nextcloud_add_test(Blacklist "syncenginetestutils.h") -nextcloud_add_test(LocalDiscovery "syncenginetestutils.h") -nextcloud_add_test(RemoteDiscovery "syncenginetestutils.h") -nextcloud_add_test(Permissions "syncenginetestutils.h") -nextcloud_add_test(SelectiveSync "syncenginetestutils.h") -nextcloud_add_test(DatabaseError "syncenginetestutils.h") -nextcloud_add_test(LockedFiles "syncenginetestutils.h;../src/gui/lockwatcher.cpp") +nextcloud_add_test(SyncEngine "") +nextcloud_add_test(SyncVirtualFiles "") +nextcloud_add_test(SyncMove "") +nextcloud_add_test(SyncDelete "") +nextcloud_add_test(SyncConflict "") +nextcloud_add_test(SyncFileStatusTracker "") +nextcloud_add_test(Download "") +nextcloud_add_test(ChunkingNg "") +nextcloud_add_test(AsyncOp "") +nextcloud_add_test(UploadReset "") +nextcloud_add_test(AllFilesDeleted "") +nextcloud_add_test(Blacklist "") +nextcloud_add_test(LocalDiscovery "") +nextcloud_add_test(RemoteDiscovery "") +nextcloud_add_test(Permissions "") +nextcloud_add_test(SelectiveSync "") +nextcloud_add_test(DatabaseError "") +nextcloud_add_test(LockedFiles "../src/gui/lockwatcher.cpp") nextcloud_add_test(FolderWatcher "${FolderWatcher_SRC}") if( UNIX AND NOT APPLE ) nextcloud_add_test(InotifyWatcher "${FolderWatcher_SRC}") endif(UNIX AND NOT APPLE) -nextcloud_add_benchmark(LargeSync "syncenginetestutils.h") +nextcloud_add_benchmark(LargeSync "") SET(FolderMan_SRC ../src/gui/folderman.cpp) list(APPEND FolderMan_SRC ../src/gui/folder.cpp ) @@ -106,7 +110,7 @@ list(APPEND RemoteWipe_SRC ${RemoteWipe_SRC}) list(APPEND RemoteWipe_SRC stubremotewipe.cpp ) nextcloud_add_test(RemoteWipe "${RemoteWipe_SRC}") -nextcloud_add_test(OAuth "syncenginetestutils.h;../src/gui/creds/oauth.cpp") +nextcloud_add_test(OAuth "../src/gui/creds/oauth.cpp") configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY) diff --git a/test/nextcloud_add_test.cmake b/test/nextcloud_add_test.cmake index 1cfeec832..d43e4e37d 100644 --- a/test/nextcloud_add_test.cmake +++ b/test/nextcloud_add_test.cmake @@ -9,7 +9,7 @@ macro(nextcloud_add_test test_class additional_cpp) set_target_properties(${OWNCLOUD_TEST_CLASS}Test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY}) target_link_libraries(${OWNCLOUD_TEST_CLASS}Test - ${APPLICATION_EXECUTABLE}sync + ${APPLICATION_EXECUTABLE}sync syncenginetestutils Qt5::Core Qt5::Test Qt5::Xml Qt5::Network Qt5::Qml Qt5::Quick ) @@ -37,7 +37,7 @@ macro(nextcloud_add_benchmark test_class additional_cpp) set_target_properties(${OWNCLOUD_TEST_CLASS}Bench PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY}) target_link_libraries(${OWNCLOUD_TEST_CLASS}Bench - ${APPLICATION_EXECUTABLE}sync + ${APPLICATION_EXECUTABLE}sync syncenginetestutils Qt5::Core Qt5::Test Qt5::Xml Qt5::Network ) diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp new file mode 100644 index 000000000..36e81e3bc --- /dev/null +++ b/test/syncenginetestutils.cpp @@ -0,0 +1,1009 @@ +/* + * 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 "syncenginetestutils.h" + + +PathComponents::PathComponents(const char *path) + : PathComponents { QString::fromUtf8(path) } +{ +} + +PathComponents::PathComponents(const QString &path) + : QStringList { path.split(QLatin1Char('/'), QString::SkipEmptyParts) } +{ +} + +PathComponents::PathComponents(const QStringList &pathComponents) + : QStringList { pathComponents } +{ +} + +PathComponents PathComponents::parentDirComponents() const +{ + return PathComponents { mid(0, size() - 1) }; +} + +PathComponents PathComponents::subComponents() const & +{ + return PathComponents { mid(1) }; +} + +void DiskFileModifier::remove(const QString &relativePath) +{ + QFileInfo fi { _rootDir.filePath(relativePath) }; + if (fi.isFile()) + QVERIFY(_rootDir.remove(relativePath)); + else + QVERIFY(QDir { fi.filePath() }.removeRecursively()); +} + +void DiskFileModifier::insert(const QString &relativePath, qint64 size, char contentChar) +{ + QFile file { _rootDir.filePath(relativePath) }; + QVERIFY(!file.exists()); + file.open(QFile::WriteOnly); + QByteArray buf(1024, contentChar); + for (int x = 0; x < size / buf.size(); ++x) { + file.write(buf); + } + file.write(buf.data(), size % buf.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::currentDateTimeUtc().addSecs(-30))); + QCOMPARE(file.size(), size); +} + +void DiskFileModifier::setContents(const QString &relativePath, char contentChar) +{ + QFile file { _rootDir.filePath(relativePath) }; + QVERIFY(file.exists()); + qint64 size = file.size(); + file.open(QFile::WriteOnly); + file.write(QByteArray {}.fill(contentChar, size)); +} + +void DiskFileModifier::appendByte(const QString &relativePath) +{ + 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 DiskFileModifier::mkdir(const QString &relativePath) +{ + _rootDir.mkpath(relativePath); +} + +void DiskFileModifier::rename(const QString &from, const QString &to) +{ + QVERIFY(_rootDir.exists(from)); + QVERIFY(_rootDir.rename(from, to)); +} + +void DiskFileModifier::setModTime(const QString &relativePath, const QDateTime &modTime) +{ + OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime)); +} + +FileInfo 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::FileInfo(const QString &name, const std::initializer_list &children) + : name { name } +{ + for (const auto &source : children) + addChild(source); +} + +void FileInfo::addChild(const FileInfo &info) +{ + auto &dest = this->children[info.name] = info; + dest.parentPath = path(); + dest.fixupParentPathRecursively(); +} + +void FileInfo::remove(const QString &relativePath) +{ + 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 FileInfo::insert(const QString &relativePath, qint64 size, char contentChar) +{ + create(relativePath, size, contentChar); +} + +void FileInfo::setContents(const QString &relativePath, char contentChar) +{ + FileInfo *file = findInvalidatingEtags(relativePath); + Q_ASSERT(file); + file->contentChar = contentChar; +} + +void FileInfo::appendByte(const QString &relativePath) +{ + FileInfo *file = findInvalidatingEtags(relativePath); + Q_ASSERT(file); + file->size += 1; +} + +void FileInfo::mkdir(const QString &relativePath) +{ + createDir(relativePath); +} + +void FileInfo::rename(const QString &oldPath, const QString &newPath) +{ + const PathComponents newPathComponents { newPath }; + FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents()); + Q_ASSERT(dir); + Q_ASSERT(dir->isDir); + const PathComponents pathComponents { oldPath }; + FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); + Q_ASSERT(parent); + FileInfo fi = parent->children.take(pathComponents.fileName()); + fi.parentPath = dir->path(); + fi.name = newPathComponents.fileName(); + fi.fixupParentPathRecursively(); + dir->children.insert(newPathComponents.fileName(), std::move(fi)); +} + +void FileInfo::setModTime(const QString &relativePath, const QDateTime &modTime) +{ + FileInfo *file = findInvalidatingEtags(relativePath); + Q_ASSERT(file); + file->lastModified = modTime; +} + +FileInfo *FileInfo::find(PathComponents pathComponents, const bool invalidateEtags) +{ + 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(std::move(pathComponents).subComponents(), invalidateEtags); + if (file && invalidateEtags) { + // Update parents on the way back + etag = generateEtag(); + } + return file; + } + return nullptr; +} + +FileInfo *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 *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 FileInfo::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 FileInfo::path() const +{ + return (parentPath.isEmpty() ? QString() : (parentPath + QLatin1Char('/'))) + name; +} + +void FileInfo::fixupParentPathRecursively() +{ + auto p = path(); + for (auto it = children.begin(); it != children.end(); ++it) { + Q_ASSERT(it.key() == it->name); + it->parentPath = p; + it->fixupParentPathRecursively(); + } +} + +FileInfo *FileInfo::findInvalidatingEtags(PathComponents pathComponents) +{ + return find(std::move(pathComponents), true); +} + +FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isNull()); // for root, it should be empty + const FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + if (!fileInfo) { + QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection); + return; + } + QString prefix = request.url().path().left(request.url().path().size() - fileName.size()); + + // 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, QStringLiteral("d")); + xml.writeNamespace(ocUri, QStringLiteral("oc")); + xml.writeStartDocument(); + xml.writeStartElement(davUri, QStringLiteral("multistatus")); + auto writeFileResponse = [&](const FileInfo &fileInfo) { + xml.writeStartElement(davUri, QStringLiteral("response")); + + xml.writeTextElement(davUri, QStringLiteral("href"), prefix + QString::fromUtf8(QUrl::toPercentEncoding(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.toUTC(); + auto stringDate = QLocale::c().toString(gmtDate, QStringLiteral("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"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag))); + xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); + xml.writeTextElement(ocUri, QStringLiteral("id"), QString::fromUtf8(fileInfo.fileId)); + xml.writeTextElement(ocUri, QStringLiteral("checksums"), QString::fromUtf8(fileInfo.checksums)); + buffer.write(fileInfo.extraDavProperties); + xml.writeEndElement(); // prop + xml.writeTextElement(davUri, QStringLiteral("status"), QStringLiteral("HTTP/1.1 200 OK")); + xml.writeEndElement(); // propstat + xml.writeEndElement(); // response + }; + + writeFileResponse(*fileInfo); + foreach (const FileInfo &childFileInfo, fileInfo->children) + writeFileResponse(childFileInfo); + xml.writeEndElement(); // multistatus + xml.writeEndDocument(); + + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakePropfindReply::respond() +{ + setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); + setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8")); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207); + setFinished(true); + emit metaDataChanged(); + if (bytesAvailable()) + emit readyRead(); + emit finished(); +} + +void FakePropfindReply::respond404() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404); + setError(InternalServerError, QStringLiteral("Not Found")); + emit metaDataChanged(); + emit finished(); +} + +qint64 FakePropfindReply::bytesAvailable() const +{ + return payload.size() + QIODevice::bytesAvailable(); +} + +qint64 FakePropfindReply::readData(char *data, qint64 maxlen) +{ + qint64 len = std::min(qint64 { payload.size() }, maxlen); + std::copy(payload.cbegin(), payload.cbegin() + len, data); + payload.remove(0, static_cast(len)); + return len; +} + +FakePutReply::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); + fileInfo = perform(remoteRootFileInfo, request, putPayload); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +FileInfo *FakePutReply::perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload) +{ + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + if (fileInfo) { + 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)); + } + fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); + remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); + return fileInfo; +} + +void FakePutReply::respond() +{ + emit uploadProgress(fileInfo->size, fileInfo->size); + setRawHeader("OC-ETag", fileInfo->etag); + setRawHeader("ETag", fileInfo->etag); + setRawHeader("OC-FileID", fileInfo->fileId); + 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 FakePutReply::abort() +{ + setError(OperationCanceledError, QStringLiteral("abort")); + emit finished(); +} + +FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + fileInfo = remoteRootFileInfo.createDir(fileName); + + if (!fileInfo) { + abort(); + return; + } + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakeMkcolReply::respond() +{ + setRawHeader("OC-FileId", fileInfo->fileId); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); + emit metaDataChanged(); + emit finished(); +} + +FakeDeleteReply::FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + remoteRootFileInfo.remove(fileName); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakeDeleteReply::respond() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204); + emit metaDataChanged(); + emit finished(); +} + +FakeMoveReply::FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); + Q_ASSERT(!dest.isEmpty()); + remoteRootFileInfo.rename(fileName, dest); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakeMoveReply::respond() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); + emit metaDataChanged(); + emit finished(); +} + +FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + fileInfo = remoteRootFileInfo.find(fileName); + if (!fileInfo) + qWarning() << "Could not find file" << fileName << "on the remote"; + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakeGetReply::respond() +{ + if (aborted) { + setError(OperationCanceledError, QStringLiteral("Operation Canceled")); + emit metaDataChanged(); + emit finished(); + return; + } + payload = fileInfo->contentChar; + size = fileInfo->size; + setHeader(QNetworkRequest::ContentLengthHeader, size); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + setRawHeader("OC-ETag", fileInfo->etag); + setRawHeader("ETag", fileInfo->etag); + setRawHeader("OC-FileId", fileInfo->fileId); + emit metaDataChanged(); + if (bytesAvailable()) + emit readyRead(); + emit finished(); +} + +void FakeGetReply::abort() +{ + setError(OperationCanceledError, QStringLiteral("Operation Canceled")); + aborted = true; +} + +qint64 FakeGetReply::bytesAvailable() const +{ + if (aborted) + return 0; + return size + QIODevice::bytesAvailable(); +} + +qint64 FakeGetReply::readData(char *data, qint64 maxlen) +{ + qint64 len = std::min(qint64 { size }, maxlen); + std::fill_n(data, len, payload); + size -= len; + return len; +} + +FakeGetWithDataReply::FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + Q_ASSERT(!data.isEmpty()); + payload = data; + QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isEmpty()); + fileInfo = remoteRootFileInfo.find(fileName); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + + if (request.hasRawHeader("Range")) { + const QString range = QString::fromUtf8(request.rawHeader("Range")); + const QRegularExpression bytesPattern(QStringLiteral("bytes=(?\\d+)-(?\\d+)")); + const QRegularExpressionMatch match = bytesPattern.match(range); + if (match.hasMatch()) { + const int start = match.captured(QStringLiteral("start")).toInt(); + const int end = match.captured(QStringLiteral("end")).toInt(); + payload = payload.mid(start, end - start + 1); + } + } +} + +void FakeGetWithDataReply::respond() +{ + if (aborted) { + setError(OperationCanceledError, QStringLiteral("Operation Canceled")); + emit metaDataChanged(); + emit finished(); + return; + } + setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + setRawHeader("OC-ETag", fileInfo->etag); + setRawHeader("ETag", fileInfo->etag); + setRawHeader("OC-FileId", fileInfo->fileId); + emit metaDataChanged(); + if (bytesAvailable()) + emit readyRead(); + emit finished(); +} + +void FakeGetWithDataReply::abort() +{ + setError(OperationCanceledError, QStringLiteral("Operation Canceled")); + aborted = true; +} + +qint64 FakeGetWithDataReply::bytesAvailable() const +{ + if (aborted) + return 0; + return payload.size() - offset + QIODevice::bytesAvailable(); +} + +qint64 FakeGetWithDataReply::readData(char *data, qint64 maxlen) +{ + qint64 len = std::min(payload.size() - offset, quint64(maxlen)); + std::memcpy(data, payload.constData() + offset, len); + offset += len; + return len; +} + +FakeChunkMoveReply::FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply { parent } +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request); + if (!fileInfo) { + QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed); + } else { + QTimer::singleShot(0, this, &FakeChunkMoveReply::respond); + } +} + +FileInfo *FakeChunkMoveReply::perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request) +{ + QString source = getFilePathFromUrl(request.url()); + Q_ASSERT(!source.isEmpty()); + Q_ASSERT(source.endsWith(QLatin1String("/.file"))); + source = source.left(source.length() - qstrlen("/.file")); + + auto sourceFolder = uploadsFileInfo.find(source); + Q_ASSERT(sourceFolder); + Q_ASSERT(sourceFolder->isDir); + int count = 0; + qlonglong size = 0; + char payload = '\0'; + + QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); + Q_ASSERT(!fileName.isEmpty()); + + // Compute the size and content from the chunks if possible + for (auto chunkName : sourceFolder->children.keys()) { + auto &x = sourceFolder->children[chunkName]; + Q_ASSERT(!x.isDir); + Q_ASSERT(x.size > 0); // There should not be empty chunks + size += x.size; + Q_ASSERT(!payload || payload == x.contentChar); + payload = x.contentChar; + ++count; + } + Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files + + // NOTE: This does not actually assemble the file data from the chunks! + FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + if (fileInfo) { + // The client should put this header + Q_ASSERT(request.hasRawHeader("If")); + + // And it should condition on the destination file + auto start = QByteArray("<" + request.rawHeader("Destination") + ">"); + Q_ASSERT(request.rawHeader("If").startsWith(start)); + + if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag + "\"])") { + return nullptr; + } + fileInfo->size = size; + fileInfo->contentChar = payload; + } else { + Q_ASSERT(!request.hasRawHeader("If")); + // Assume that the file is filled with the same character + fileInfo = remoteRootFileInfo.create(fileName, size, payload); + } + fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); + remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); + + return fileInfo; +} + +void FakeChunkMoveReply::respond() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); + setRawHeader("OC-ETag", fileInfo->etag); + setRawHeader("ETag", fileInfo->etag); + setRawHeader("OC-FileId", fileInfo->fileId); + emit metaDataChanged(); + emit finished(); +} + +void FakeChunkMoveReply::respondPreconditionFailed() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412); + setError(InternalServerError, QStringLiteral("Precondition Failed")); + emit metaDataChanged(); + emit finished(); +} + +void FakeChunkMoveReply::abort() +{ + setError(OperationCanceledError, QStringLiteral("abort")); + emit finished(); +} + +FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent) + : QNetworkReply { parent } + , _body(body) +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + QTimer::singleShot(10, this, &FakePayloadReply::respond); +} + +void FakePayloadReply::respond() +{ + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); + emit metaDataChanged(); + emit readyRead(); + setFinished(true); + emit finished(); +} + +qint64 FakePayloadReply::readData(char *buf, qint64 max) +{ + max = qMin(max, _body.size()); + memcpy(buf, _body.constData(), max); + _body = _body.mid(max); + return max; +} + +qint64 FakePayloadReply::bytesAvailable() const +{ + return _body.size(); +} + +FakeErrorReply::FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent, int httpErrorCode, const QByteArray &body) + : QNetworkReply { parent } + , _body(body) +{ + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode); + setError(InternalServerError, QStringLiteral("Internal Server Fake Error")); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); +} + +void FakeErrorReply::respond() +{ + emit metaDataChanged(); + emit readyRead(); + // finishing can come strictly after readyRead was called + QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished); +} + +void FakeErrorReply::slotSetFinished() +{ + setFinished(true); + emit finished(); +} + +qint64 FakeErrorReply::readData(char *buf, qint64 max) +{ + max = qMin(max, _body.size()); + memcpy(buf, _body.constData(), max); + _body = _body.mid(max); + return max; +} + +qint64 FakeErrorReply::bytesAvailable() const +{ + return _body.size(); +} + +void FakeHangingReply::abort() +{ + // Follow more or less the implementation of QNetworkReplyImpl::abort + close(); + setError(OperationCanceledError, tr("Operation canceled")); + emit error(OperationCanceledError); + setFinished(true); + emit finished(); +} + +FakeQNAM::FakeQNAM(FileInfo initialRoot) + : _remoteRootFileInfo { std::move(initialRoot) } +{ + setCookieJar(new OCC::CookieJar); +} + +QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) +{ + if (_override) { + if (auto reply = _override(op, request, outgoingData)) + return reply; + } + const QString fileName = getFilePathFromUrl(request.url()); + Q_ASSERT(!fileName.isNull()); + if (_errorPaths.contains(fileName)) + return new FakeErrorReply { op, request, this, _errorPaths[fileName] }; + + bool isUpload = request.url().path().startsWith(sUploadUrl.path()); + FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo; + + auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute); + if (verb == QLatin1String("PROPFIND")) + // Ignore outgoingData always returning somethign good enough, works for now. + return new FakePropfindReply { info, op, request, this }; + else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation) + return new FakeGetReply { info, op, request, this }; + else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation) + return new FakePutReply { info, op, request, outgoingData->readAll(), this }; + else if (verb == QLatin1String("MKCOL")) + return new FakeMkcolReply { info, op, request, this }; + else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation) + return new FakeDeleteReply { info, op, request, this }; + else if (verb == QLatin1String("MOVE") && !isUpload) + return new FakeMoveReply { info, op, request, this }; + else if (verb == QLatin1String("MOVE") && isUpload) + return new FakeChunkMoveReply { info, _remoteRootFileInfo, op, request, this }; + else { + qDebug() << verb << outgoingData; + Q_UNREACHABLE(); + } +} + +FakeFolder::FakeFolder(const FileInfo &fileTemplate) + : _localModifier(_tempDir.path()) +{ + // Needs to be done once + OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0); + OCC::Logger::instance()->setLogFile(QStringLiteral("-")); + + QDir rootDir { _tempDir.path() }; + qDebug() << "FakeFolder operating on" << rootDir; + 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 }); + _account->setDavDisplayName(QStringLiteral("fakename")); + _account->setServerVersion(QStringLiteral("10.0.0")); + + _journalDb.reset(new OCC::SyncJournalDb(localPath() + QStringLiteral(".sync_test.db"))); + _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), QString(), _journalDb.get())); + // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it) + _syncEngine->excludedFiles().addManualExclude(QStringLiteral("]*.~*")); + + // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it + QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function callback) { + QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback] { + callback(false); + }); + }); + + // Ensure we have a valid VfsOff instance "running" + switchToVfs(_syncEngine->syncOptions()._vfs); + + // 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. + ENFORCE(syncOnce()); +} + +void FakeFolder::switchToVfs(QSharedPointer vfs) +{ + auto opts = _syncEngine->syncOptions(); + + opts._vfs->stop(); + QObject::disconnect(_syncEngine.get(), nullptr, opts._vfs.data(), nullptr); + + opts._vfs = vfs; + _syncEngine->setSyncOptions(opts); + + OCC::VfsSetupParams vfsParams; + vfsParams.filesystemPath = localPath(); + vfsParams.remotePath = QLatin1Char('/'); + vfsParams.account = _account; + vfsParams.journal = _journalDb.get(); + vfsParams.providerName = QStringLiteral("OC-TEST"); + vfsParams.providerVersion = QStringLiteral("0.1"); + QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() { + vfs->stop(); + vfs->unregisterFolder(); + }); + + vfs->start(vfsParams); +} + +FileInfo FakeFolder::currentLocalState() +{ + QDir rootDir { _tempDir.path() }; + FileInfo rootTemplate; + fromDisk(rootDir, rootTemplate); + rootTemplate.fixupParentPathRecursively(); + return rootTemplate; +} + +QString FakeFolder::localPath() const +{ + // SyncEngine wants a trailing slash + if (_tempDir.path().endsWith(QLatin1Char('/'))) + return _tempDir.path(); + return _tempDir.path() + QLatin1Char('/'); +} + +void FakeFolder::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 FakeFolder::execUntilBeforePropagation() +{ + QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector &))); + QVERIFY(spy.wait()); +} + +void FakeFolder::execUntilItemCompleted(const QString &relativePath) +{ + QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + 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 FakeFolder::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)); + } + } +} + +void FakeFolder::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()); + FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo { diskChild.fileName() }; + fromDisk(subDir, subFi); + } else { + QFile f { diskChild.filePath() }; + f.open(QFile::ReadOnly); + auto content = f.read(1); + if (content.size() == 0) { + qWarning() << "Empty file at:" << diskChild.filePath(); + continue; + } + char contentChar = content.at(0); + templateFi.children.insert(diskChild.fileName(), FileInfo { diskChild.fileName(), diskChild.size(), contentChar }); + } + } +} + +FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components) +{ + if (components.isEmpty()) + return base; + auto childName = components.pathRoot(); + auto it = base.children.find(childName); + if (it != base.children.end()) { + return findOrCreateDirs(*it, components.subComponents()); + } + auto &newDir = base.children[childName] = FileInfo { childName }; + newDir.parentPath = base.path(); + return findOrCreateDirs(newDir, components.subComponents()); +} + +FileInfo FakeFolder::dbState() const +{ + FileInfo result; + _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) { + auto components = PathComponents(QString::fromUtf8(record._path)); + auto &parentDir = findOrCreateDirs(result, components.parentDirComponents()); + auto name = components.fileName(); + auto &item = parentDir.children[name]; + item.name = name; + item.parentPath = parentDir.path(); + item.size = record._fileSize; + item.isDir = record._type == ItemTypeDirectory; + item.permissions = record._remotePerm; + item.etag = record._etag; + item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime); + item.fileId = record._fileId; + item.checksums = record._checksumHeader; + // item.contentChar can't be set from the db + }); + return result; +} + +OCC::SyncFileItemPtr ItemCompletedSpy::findItem(const QString &path) const +{ + for (const QList &args : *this) { + auto item = args[0].value(); + if (item->destination() == path) + return item; + } + return OCC::SyncFileItemPtr::create(); +} diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index b19f2d0f5..52c330c62 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -20,6 +20,8 @@ #include #include #include + +#include #include #include @@ -47,8 +49,8 @@ inline QString getFilePathFromUrl(const QUrl &url) { } -inline QString generateEtag() { - return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16); +inline QByteArray generateEtag() { + return QByteArray::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16); } inline QByteArray generateFileId() { return QByteArray::number(qrand(), 16); @@ -56,14 +58,12 @@ inline QByteArray generateFileId() { class PathComponents : public QStringList { public: - PathComponents(const char *path) : PathComponents{QString::fromUtf8(path)} {} - PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { } - PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { } + PathComponents(const char *path); + PathComponents(const QString &path); + PathComponents(const QStringList &pathComponents); - PathComponents parentDirComponents() const { - return PathComponents{mid(0, size() - 1)}; - } - PathComponents subComponents() const& { return PathComponents{mid(1)}; } + PathComponents parentDirComponents() const; + PathComponents subComponents() const &; PathComponents subComponents() && { removeFirst(); return std::move(*this); } QString pathRoot() const { return first(); } QString fileName() const { return last(); } @@ -87,225 +87,69 @@ 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); - QByteArray buf(1024, contentChar); - for (int x = 0; x < size/buf.size(); ++x) { - file.write(buf); - } - file.write(buf.data(), size % buf.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::currentDateTimeUtc().addSecs(-30))); - QCOMPARE(file.size(), size); - } - 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); - } - void rename(const QString &from, const QString &to) override { - QVERIFY(_rootDir.exists(from)); - QVERIFY(_rootDir.rename(from, to)); - } - void setModTime(const QString &relativePath, const QDateTime &modTime) override { - OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime)); - } + void remove(const QString &relativePath) override; + void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override; + void setContents(const QString &relativePath, char contentChar) override; + void appendByte(const QString &relativePath) override; + + void mkdir(const QString &relativePath) override; + void rename(const QString &from, const QString &to) override; + void setModTime(const QString &relativePath, const QDateTime &modTime) override; }; 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; - } + static FileInfo A12_B12_C12_S12(); 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} { - for (const auto &source : children) - addChild(source); - } + FileInfo(const QString &name, const std::initializer_list &children); - void addChild(const FileInfo &info) - { - auto &dest = this->children[info.name] = info; - dest.parentPath = path(); - dest.fixupParentPathRecursively(); - } + void addChild(const FileInfo &info); - 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 remove(const QString &relativePath) override; - void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override { - create(relativePath, size, contentChar); - } + void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override; - void setContents(const QString &relativePath, char contentChar) override { - FileInfo *file = findInvalidatingEtags(relativePath); - Q_ASSERT(file); - file->contentChar = contentChar; - } + void setContents(const QString &relativePath, char contentChar) override; - void appendByte(const QString &relativePath) override { - FileInfo *file = findInvalidatingEtags(relativePath); - Q_ASSERT(file); - file->size += 1; - } + void appendByte(const QString &relativePath) override; - void mkdir(const QString &relativePath) override { - createDir(relativePath); - } + void mkdir(const QString &relativePath) override; - void rename(const QString &oldPath, const QString &newPath) override { - const PathComponents newPathComponents{newPath}; - FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents()); - Q_ASSERT(dir); - Q_ASSERT(dir->isDir); - const PathComponents pathComponents{oldPath}; - FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); - Q_ASSERT(parent); - FileInfo fi = parent->children.take(pathComponents.fileName()); - fi.parentPath = dir->path(); - fi.name = newPathComponents.fileName(); - fi.fixupParentPathRecursively(); - dir->children.insert(newPathComponents.fileName(), std::move(fi)); - } + void rename(const QString &oldPath, const QString &newPath) override; - void setModTime(const QString &relativePath, const QDateTime &modTime) override { - FileInfo *file = findInvalidatingEtags(relativePath); - Q_ASSERT(file); - file->lastModified = modTime; - } + void setModTime(const QString &relativePath, const QDateTime &modTime) override; - FileInfo *find(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(std::move(pathComponents).subComponents(), invalidateEtags); - if (file && invalidateEtags) { - // Update parents on the way back - etag = generateEtag(); - } - return file; - } - return nullptr; - } + FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false); - 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 *createDir(const QString &relativePath); - 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; - } + FileInfo *create(const QString &relativePath, qint64 size, char contentChar); 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; - } + bool operator==(const FileInfo &other) const; bool operator!=(const FileInfo &other) const { return !operator==(other); } - QString path() const { - return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name; - } + QString path() const; - void fixupParentPathRecursively() { - auto p = path(); - for (auto it = children.begin(); it != children.end(); ++it) { - Q_ASSERT(it.key() == it->name); - it->parentPath = p; - it->fixupParentPathRecursively(); - } - } + void fixupParentPathRecursively(); QString name; bool isDir = true; bool isShared = false; OCC::RemotePermissions permissions; // When uset, defaults to everything QDateTime lastModified = QDateTime::currentDateTimeUtc().addDays(-7); - QString etag = generateEtag(); + QByteArray etag = generateEtag(); QByteArray fileId = generateFileId(); QByteArray checksums; QByteArray extraDavProperties; @@ -316,9 +160,7 @@ public: QMap children; QString parentPath; - FileInfo *findInvalidatingEtags(PathComponents pathComponents) { - return find(std::move(pathComponents), true); - } + FileInfo *findInvalidatingEtags(PathComponents pathComponents); friend inline QDebug operator<<(QDebug dbg, const FileInfo& fi) { return dbg << "{ " << fi.path() << ": " << fi.children; @@ -331,99 +173,16 @@ class FakePropfindReply : public QNetworkReply 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); - - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isNull()); // for root, it should be empty - const FileInfo *fileInfo = remoteRootFileInfo.find(fileName); - if (!fileInfo) { - QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection); - return; - } - QString prefix = request.url().path().left(request.url().path().size() - fileName.size()); - - // 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"), prefix + QUrl::toPercentEncoding(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.toUTC(); - auto stringDate = QLocale::c().toString(gmtDate, "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"), QStringLiteral("\"%1\"").arg(fileInfo.etag)); - xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() - ? QString(fileInfo.permissions.toString()) - : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); - xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId); - xml.writeTextElement(ocUri, QStringLiteral("checksums"), fileInfo.checksums); - buffer.write(fileInfo.extraDavProperties); - xml.writeEndElement(); // prop - xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK"); - xml.writeEndElement(); // propstat - xml.writeEndElement(); // response - }; - - writeFileResponse(*fileInfo); - foreach(const FileInfo &childFileInfo, fileInfo->children) - writeFileResponse(childFileInfo); - xml.writeEndElement(); // multistatus - xml.writeEndDocument(); - - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } + FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); - 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(); - } + Q_INVOKABLE void respond(); - Q_INVOKABLE void respond404() { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404); - setError(InternalServerError, "Not Found"); - emit metaDataChanged(); - emit finished(); - } + Q_INVOKABLE void respond404(); 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; - } + qint64 bytesAvailable() const override; + qint64 readData(char *data, qint64 maxlen) override; }; class FakePutReply : public QNetworkReply @@ -431,50 +190,13 @@ 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); - fileInfo = perform(remoteRootFileInfo, request, putPayload); - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } + FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent); - static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload) - { - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isEmpty()); - FileInfo *fileInfo = remoteRootFileInfo.find(fileName); - if (fileInfo) { - 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)); - } - fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); - remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true); - return fileInfo; - } + static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload); - Q_INVOKABLE virtual void respond() - { - emit uploadProgress(fileInfo->size, fileInfo->size); - setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); - setRawHeader("ETag", fileInfo->etag.toLatin1()); - setRawHeader("OC-FileID", fileInfo->fileId); - 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(); - } + Q_INVOKABLE virtual void respond(); - void abort() override - { - setError(OperationCanceledError, "abort"); - emit finished(); - } + void abort() override; qint64 readData(char *, qint64) override { return 0; } }; @@ -483,30 +205,9 @@ 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); + FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isEmpty()); - fileInfo = remoteRootFileInfo.createDir(fileName); - - if (!fileInfo) { - abort(); - return; - } - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } - - Q_INVOKABLE void respond() { - setRawHeader("OC-FileId", fileInfo->fileId); - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); - emit metaDataChanged(); - emit finished(); - } + Q_INVOKABLE void respond(); void abort() override { } qint64 readData(char *, qint64) override { return 0; } @@ -516,24 +217,9 @@ 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); - - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isEmpty()); - remoteRootFileInfo.remove(fileName); - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } + FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); - Q_INVOKABLE void respond() { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204); - emit metaDataChanged(); - emit finished(); - } + Q_INVOKABLE void respond(); void abort() override { } qint64 readData(char *, qint64) override { return 0; } @@ -543,26 +229,9 @@ class FakeMoveReply : public QNetworkReply { Q_OBJECT public: - FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) - : QNetworkReply{parent} { - setRequest(request); - setUrl(request.url()); - setOperation(op); - open(QIODevice::ReadOnly); + FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isEmpty()); - QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); - Q_ASSERT(!dest.isEmpty()); - remoteRootFileInfo.rename(fileName, dest); - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } - - Q_INVOKABLE void respond() { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); - emit metaDataChanged(); - emit finished(); - } + Q_INVOKABLE void respond(); void abort() override { } qint64 readData(char *, qint64) override { return 0; } @@ -577,61 +246,40 @@ public: int size; bool aborted = false; - FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) - : QNetworkReply{parent} { - setRequest(request); - setUrl(request.url()); - setOperation(op); - open(QIODevice::ReadOnly); + FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); - QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isEmpty()); - fileInfo = remoteRootFileInfo.find(fileName); - if (!fileInfo) - qWarning() << "Could not find file" << fileName << "on the remote"; - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } + Q_INVOKABLE void respond(); - Q_INVOKABLE void respond() { - if (aborted) { - setError(OperationCanceledError, "Operation Canceled"); - emit metaDataChanged(); - emit finished(); - return; - } - payload = fileInfo->contentChar; - size = fileInfo->size; - setHeader(QNetworkRequest::ContentLengthHeader, size); - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); - setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); - setRawHeader("ETag", fileInfo->etag.toLatin1()); - setRawHeader("OC-FileId", fileInfo->fileId); - emit metaDataChanged(); - if (bytesAvailable()) - emit readyRead(); - emit finished(); - } + void abort() override; + qint64 bytesAvailable() const override; - void abort() override { - aborted = true; - } - qint64 bytesAvailable() const override { - if (aborted) - return 0; - return size + QIODevice::bytesAvailable(); - } - - qint64 readData(char *data, qint64 maxlen) override { - qint64 len = std::min(qint64{size}, maxlen); - std::fill_n(data, len, payload); - size -= len; - return len; - } + qint64 readData(char *data, qint64 maxlen) override; // useful to be public for testing using QNetworkReply::setRawHeader; }; +class FakeGetWithDataReply : public QNetworkReply +{ + Q_OBJECT +public: + const FileInfo *fileInfo; + QByteArray payload; + quint64 offset = 0; + bool aborted = false; + + FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); + + Q_INVOKABLE void respond(); + + void abort() override; + qint64 bytesAvailable() const override; + + qint64 readData(char *data, qint64 maxlen) override; + + // useful to be public for testing + using QNetworkReply::setRawHeader; +}; class FakeChunkMoveReply : public QNetworkReply { @@ -640,101 +288,15 @@ class FakeChunkMoveReply : public QNetworkReply public: FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, - QObject *parent) - : QNetworkReply{ parent } - { - setRequest(request); - setUrl(request.url()); - setOperation(op); - open(QIODevice::ReadOnly); - fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request); - if (!fileInfo) { - QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed); - } else { - QTimer::singleShot(0, this, &FakeChunkMoveReply::respond); - } - } + QObject *parent); - static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request) - { - QString source = getFilePathFromUrl(request.url()); - Q_ASSERT(!source.isEmpty()); - Q_ASSERT(source.endsWith("/.file")); - source = source.left(source.length() - static_cast(qstrlen("/.file"))); - auto sourceFolder = uploadsFileInfo.find(source); - Q_ASSERT(sourceFolder); - Q_ASSERT(sourceFolder->isDir); - qint64 count = 0; - qint64 size = 0; - char payload = '\0'; - - do { - QString chunkName = QString::number(count).rightJustified(16, '0'); - if (!sourceFolder->children.contains(chunkName)) - break; - auto &x = sourceFolder->children[chunkName]; - Q_ASSERT(!x.isDir); - Q_ASSERT(x.size > 0); // There should not be empty chunks - size += x.size; - Q_ASSERT(!payload || payload == x.contentChar); - payload = x.contentChar; - ++count; - } while(true); - - Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking? - Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files - - QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); - Q_ASSERT(!fileName.isEmpty()); - - FileInfo *fileInfo = remoteRootFileInfo.find(fileName); - if (fileInfo) { - // The client should put this header - Q_ASSERT(request.hasRawHeader("If")); - - // And it should condition on the destination file - auto start = QByteArray("<" + request.rawHeader("Destination") + ">"); - Q_ASSERT(request.rawHeader("If").startsWith(start)); - - if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag.toLatin1() + "\"])") { - return nullptr; - } - fileInfo->size = size; - fileInfo->contentChar = payload; - } else { - Q_ASSERT(!request.hasRawHeader("If")); - // Assume that the file is filled with the same character - fileInfo = remoteRootFileInfo.create(fileName, size, payload); - } + static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request); - fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); - remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true); + Q_INVOKABLE virtual void respond(); - return fileInfo; - } + Q_INVOKABLE void respondPreconditionFailed(); - Q_INVOKABLE virtual void respond() - { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); - setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); - setRawHeader("ETag", fileInfo->etag.toLatin1()); - setRawHeader("OC-FileId", fileInfo->fileId); - emit metaDataChanged(); - emit finished(); - } - - Q_INVOKABLE void respondPreconditionFailed() { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412); - setError(InternalServerError, "Precondition Failed"); - emit metaDataChanged(); - emit finished(); - } - - void abort() override - { - setError(OperationCanceledError, "abort"); - emit finished(); - } + void abort() override; qint64 readData(char *, qint64) override { return 0; } }; @@ -745,39 +307,13 @@ class FakePayloadReply : public QNetworkReply Q_OBJECT public: FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, - const QByteArray &body, QObject *parent) - : QNetworkReply{ parent } - , _body(body) - { - setRequest(request); - setUrl(request.url()); - setOperation(op); - open(QIODevice::ReadOnly); - QTimer::singleShot(10, this, &FakePayloadReply::respond); - } + const QByteArray &body, QObject *parent); - void respond() - { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); - setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); - emit metaDataChanged(); - emit readyRead(); - setFinished(true); - emit finished(); - } + void respond(); void abort() override {} - qint64 readData(char *buf, qint64 max) override - { - max = qMin(max, _body.size()); - memcpy(buf, _body.constData(), max); - _body = _body.mid(max); - return max; - } - qint64 bytesAvailable() const override - { - return _body.size(); - } + qint64 readData(char *buf, qint64 max) override; + qint64 bytesAvailable() const override; QByteArray _body; }; @@ -787,45 +323,21 @@ class FakeErrorReply : public QNetworkReply Q_OBJECT public: FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, - QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray()) - : QNetworkReply{parent}, _body(body) { - setRequest(request); - setUrl(request.url()); - setOperation(op); - open(QIODevice::ReadOnly); - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode); - setError(InternalServerError, "Internal Server Fake Error"); - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); - } + QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray()); - Q_INVOKABLE virtual void respond() { - emit metaDataChanged(); - emit readyRead(); - // finishing can come strictly after readyRead was called - QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished); - } + Q_INVOKABLE virtual void respond(); // make public to give tests easy interface using QNetworkReply::setError; using QNetworkReply::setAttribute; public slots: - void slotSetFinished() { - setFinished(true); - emit finished(); - } + void slotSetFinished(); public: void abort() override { } - qint64 readData(char *buf, qint64 max) override { - max = qMin(max, _body.size()); - memcpy(buf, _body.constData(), max); - _body = _body.mid(max); - return max; - } - qint64 bytesAvailable() const override { - return _body.size(); - } + qint64 readData(char *buf, qint64 max) override; + qint64 bytesAvailable() const override; QByteArray _body; }; @@ -844,14 +356,7 @@ public: open(QIODevice::ReadOnly); } - void abort() override { - // Follow more or less the implementation of QNetworkReplyImpl::abort - close(); - setError(OperationCanceledError, tr("Operation canceled")); - emit error(OperationCanceledError); - setFinished(true); - emit finished(); - } + void abort() override; qint64 readData(char *, qint64) override { return 0; } }; @@ -891,11 +396,7 @@ private: Override _override; public: - FakeQNAM(FileInfo initialRoot) - : _remoteRootFileInfo{std::move(initialRoot)} - { - setCookieJar(new OCC::CookieJar); - } + FakeQNAM(FileInfo initialRoot); FileInfo ¤tRemoteState() { return _remoteRootFileInfo; } FileInfo &uploadState() { return _uploadFileInfo; } @@ -905,40 +406,7 @@ public: protected: QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, - QIODevice *outgoingData = nullptr) { - if (_override) { - if (auto reply = _override(op, request, outgoingData)) - return reply; - } - const QString fileName = getFilePathFromUrl(request.url()); - Q_ASSERT(!fileName.isNull()); - if (_errorPaths.contains(fileName)) - return new FakeErrorReply{op, request, this, _errorPaths[fileName]}; - - bool isUpload = request.url().path().startsWith(sUploadUrl.path()); - FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo; - - auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute); - if (verb == "PROPFIND") - // Ignore outgoingData always returning somethign good enough, works for now. - return new FakePropfindReply{info, op, request, this}; - else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation) - return new FakeGetReply{info, op, request, this}; - else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation) - return new FakePutReply{info, op, request, outgoingData->readAll(), this}; - else if (verb == QLatin1String("MKCOL")) - return new FakeMkcolReply{info, op, request, this}; - else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation) - return new FakeDeleteReply{info, op, request, this}; - else if (verb == QLatin1String("MOVE") && !isUpload) - return new FakeMoveReply{info, op, request, this}; - else if (verb == QLatin1String("MOVE") && isUpload) - return new FakeChunkMoveReply{ info, _remoteRootFileInfo, op, request, this }; - else { - qDebug() << verb << outgoingData; - Q_UNREACHABLE(); - } - } + QIODevice *outgoingData = nullptr) override; }; class FakeCredentials : public OCC::AbstractCredentials @@ -969,69 +437,9 @@ class FakeFolder std::unique_ptr _syncEngine; public: - FakeFolder(const FileInfo &fileTemplate) - : _localModifier(_tempDir.path()) - { - // Needs to be done once - OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0); - OCC::Logger::instance()->setLogFile("-"); - - QDir rootDir{_tempDir.path()}; - qDebug() << "FakeFolder operating on" << rootDir; - 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}); - _account->setDavDisplayName("fakename"); - _account->setServerVersion("10.0.0"); - - _journalDb = std::make_unique(localPath() + ".sync_test.db"); - _syncEngine = std::make_unique(_account, localPath(), "", _journalDb.get()); - // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it) - _syncEngine->excludedFiles().addManualExclude("]*.~*"); - - // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it - QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function callback){ - QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback]{ - callback(false); - }); - }); - - // Ensure we have a valid VfsOff instance "running" - switchToVfs(_syncEngine->syncOptions()._vfs); + FakeFolder(const FileInfo &fileTemplate); - // 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. - ENFORCE(syncOnce()); - } - - void switchToVfs(QSharedPointer vfs) - { - auto opts = _syncEngine->syncOptions(); - - opts._vfs->stop(); - QObject::disconnect(_syncEngine.get(), 0, opts._vfs.data(), 0); - - opts._vfs = vfs; - _syncEngine->setSyncOptions(opts); - - OCC::VfsSetupParams vfsParams; - vfsParams.filesystemPath = localPath(); - vfsParams.remotePath = "/"; - vfsParams.account = _account; - vfsParams.journal = _journalDb.get(); - vfsParams.providerName = "OC-TEST"; - vfsParams.providerVersion = "0.1"; - QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() { - vfs->stop(); - vfs->unregisterFolder(); - }); - - vfs->start(vfsParams); - } + void switchToVfs(QSharedPointer vfs); OCC::AccountPtr account() const { return _account; } OCC::SyncEngine &syncEngine() const { return *_syncEngine; } @@ -1039,13 +447,7 @@ public: FileModifier &localModifier() { return _localModifier; } FileInfo &remoteModifier() { return _fakeQnam->currentRemoteState(); } - FileInfo currentLocalState() { - QDir rootDir{_tempDir.path()}; - FileInfo rootTemplate; - fromDisk(rootDir, rootTemplate); - rootTemplate.fixupParentPathRecursively(); - return rootTemplate; - } + FileInfo currentLocalState(); FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); } FileInfo &uploadState() { return _fakeQnam->uploadState(); } @@ -1060,38 +462,13 @@ public: ErrorList serverErrorPaths() { return {_fakeQnam}; } void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); } - QString localPath() const { - // SyncEngine wants a trailing slash - if (_tempDir.path().endsWith('/')) - return _tempDir.path(); - return _tempDir.path() + '/'; - } + QString localPath() const; - 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 scheduleSync(); - void execUntilBeforePropagation() { - QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&))); - QVERIFY(spy.wait()); - } + void execUntilBeforePropagation(); - void execUntilItemCompleted(const QString &relativePath) { - QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); - 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 execUntilItemCompleted(const QString &relativePath); bool execUntilFinished() { QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool))); @@ -1106,80 +483,13 @@ public: } 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 toDisk(QDir &dir, const FileInfo &templateFi); - 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()); - FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo{diskChild.fileName()}; - fromDisk(subDir, subFi); - } else { - QFile f{diskChild.filePath()}; - f.open(QFile::ReadOnly); - auto content = f.read(1); - if (content.size() == 0) { - qWarning() << "Empty file at:" << diskChild.filePath(); - continue; - } - char contentChar = content.at(0); - templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar}); - } - } - } + static void fromDisk(QDir &dir, FileInfo &templateFi); }; -static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components) -{ - if (components.isEmpty()) - return base; - auto childName = components.pathRoot(); - auto it = base.children.find(childName); - if (it != base.children.end()) { - return findOrCreateDirs(*it, components.subComponents()); - } - auto &newDir = base.children[childName] = FileInfo{childName}; - newDir.parentPath = base.path(); - return findOrCreateDirs(newDir, components.subComponents()); -} +static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components); -inline FileInfo FakeFolder::dbState() const -{ - FileInfo result; - _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) { - auto components = PathComponents(QString::fromUtf8(record._path)); - auto &parentDir = findOrCreateDirs(result, components.parentDirComponents()); - auto name = components.fileName(); - auto &item = parentDir.children[name]; - item.name = name; - item.parentPath = parentDir.path(); - item.size = record._fileSize; - item.isDir = record._type == ItemTypeDirectory; - item.permissions = record._remotePerm; - item.etag = record._etag; - item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime); - item.fileId = record._fileId; - item.checksums = record._checksumHeader; - // item.contentChar can't be set from the db - }); - return result; -} /* Return the FileInfo for a conflict file for the specified relative filename */ inline const FileInfo *findConflict(FileInfo &dir, const QString &filename) @@ -1202,15 +512,7 @@ struct ItemCompletedSpy : QSignalSpy { : QSignalSpy(&folder.syncEngine(), &OCC::SyncEngine::itemCompleted) {} - OCC::SyncFileItemPtr findItem(const QString &path) const - { - for (const QList &args : *this) { - auto item = args[0].value(); - if (item->destination() == path) - return item; - } - return OCC::SyncFileItemPtr::create(); - } + OCC::SyncFileItemPtr findItem(const QString &path) const; }; // QTest::toString overloads diff --git a/test/testasyncop.cpp b/test/testasyncop.cpp index 1f90e4b31..d8de8bce1 100644 --- a/test/testasyncop.cpp +++ b/test/testasyncop.cpp @@ -113,7 +113,7 @@ private slots: auto successCallback = [](TestCase *tc, const QNetworkRequest &request) { tc->pollRequest = [](TestCase *, const QNetworkRequest &) -> QNetworkReply * { std::abort(); }; // shall no longer be called FileInfo *info = tc->perform(); - QByteArray body = "{ \"status\":\"finished\", \"ETag\":\"\\\"" + info->etag.toUtf8() + "\\\"\", \"fileId\":\"" + info->fileId + "\"}\n"; + QByteArray body = "{ \"status\":\"finished\", \"ETag\":\"\\\"" + info->etag + "\\\"\", \"fileId\":\"" + info->fileId + "\"}\n"; return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr); }; // Callback that never finishes diff --git a/test/testblacklist.cpp b/test/testblacklist.cpp index 0b399b702..dd54e6df4 100644 --- a/test/testblacklist.cpp +++ b/test/testblacklist.cpp @@ -171,7 +171,7 @@ private slots: QCOMPARE(counter, 4); if (remote) - QCOMPARE(journalRecord(fakeFolder, "A")._etag, fakeFolder.currentRemoteState().find("A")->etag.toUtf8()); + QCOMPARE(journalRecord(fakeFolder, "A")._etag, fakeFolder.currentRemoteState().find("A")->etag); } cleanup(); diff --git a/test/testchunkingng.cpp b/test/testchunkingng.cpp index 8a49bb538..602f2da5a 100644 --- a/test/testchunkingng.cpp +++ b/test/testchunkingng.cpp @@ -275,7 +275,7 @@ private slots: QCOMPARE(items[0]->_file, QLatin1String("A")); SyncJournalFileRecord record; QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record)); - QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag.toUtf8()); + QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag); }; auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated); QVERIFY(fakeFolder.syncOnce());