/* * 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 "localdiscoverytracker.h" #include "lockwatcher.h" #include "syncengine.h" #include "testutils.h" #include "testutils/syncenginetestutils.h" #include using namespace std::chrono_literals; using namespace OCC; #ifdef Q_OS_WIN #include "common/utility_win.h" // pass combination of FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE Utility::Handle makeHandle(const QString &file, uint32_t shareMode) { auto handle = Utility::Handle::createHandle(FileSystem::toFilesystemPath(file), {.accessMode = GENERIC_READ | GENERIC_WRITE, .shareMode = shareMode}); if (!handle) { qWarning() << handle.errorMessage(); } return handle; } #endif class TestLockedFiles : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase_data() { QTest::addColumn("vfsMode"); QTest::addColumn("filesAreDehydrated"); QTest::newRow("Vfs::Mode::Off") << Vfs::Mode::Off << false; if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::WindowsCfApi)) { QTest::newRow("Vfs::Mode::WindowsCfApi dehydrated") << Vfs::Mode::WindowsCfApi << true; // TODO: the hydrated version will fail due to an issue in the winvfs plugin, so leave it disabled for now. // QTest::newRow("Vfs::Mode::WindowsCfApi hydrated") << Vfs::Mode::WindowsCfApi << false; } else if (Utility::isWindows()) { qWarning("Skipping Vfs::Mode::WindowsCfApi"); } } void testBasicLockFileWatcher() { auto temporaryDir = TestUtils::createTempDir(); int count = 0; QString file; LockWatcher watcher; watcher.setCheckInterval(std::chrono::milliseconds(50)); connect(&watcher, &LockWatcher::fileUnlocked, &watcher, [&](const QString &f) { ++count; file = f; }); const QString tmpFile = temporaryDir.path() + QString::fromUtf8("/alonglonglonglong/blonglonglonglong/clonglonglonglong/dlonglonglonglong/" "elonglonglonglong/flonglonglonglong/glonglonglonglong/hlonglonglonglong/ilonglonglonglong/" "jlonglonglonglong/klonglonglonglong/llonglonglonglong/mlonglonglonglong/nlonglonglonglong/" "olonglonglonglong/file🐷.txt"); { // use a long file path to ensure we handle that correctly QVERIFY(QFileInfo(tmpFile).dir().mkpath(QStringLiteral("."))); QFile tmp(tmpFile); QVERIFY(tmp.open(QFile::WriteOnly)); QVERIFY(tmp.write("QSfera")); } QVERIFY(QFile::exists(tmpFile)); QVERIFY(!FileSystem::isFileLocked(tmpFile, FileSystem::LockMode::Shared)); watcher.addFile(tmpFile, FileSystem::LockMode::Shared); QVERIFY(watcher.contains(tmpFile, FileSystem::LockMode::Shared)); QEventLoop loop; QTimer::singleShot(120ms, &loop, [&] { loop.exit(); }); loop.exec(); QCOMPARE(count, 1); QCOMPARE(file, tmpFile); QVERIFY(!watcher.contains(tmpFile, FileSystem::LockMode::Shared)); #ifdef Q_OS_WIN auto h = makeHandle(tmpFile, 0); QVERIFY(FileSystem::isFileLocked(tmpFile, FileSystem::LockMode::Shared)); watcher.addFile(tmpFile, FileSystem::LockMode::Shared); count = 0; file.clear(); QThread::msleep(120); qApp->processEvents(); QCOMPARE(count, 0); QVERIFY(file.isEmpty()); QVERIFY(watcher.contains(tmpFile, FileSystem::LockMode::Shared)); h.close(); QVERIFY(!FileSystem::isFileLocked(tmpFile, FileSystem::LockMode::Shared)); QThread::msleep(120); qApp->processEvents(); QCOMPARE(count, 1); QCOMPARE(file, tmpFile); QVERIFY(!watcher.contains(tmpFile, FileSystem::LockMode::Shared)); #endif QVERIFY(temporaryDir.remove()); } #ifdef Q_OS_WIN void testLockedFilePropagation() { QFETCH_GLOBAL(Vfs::Mode, vfsMode); QFETCH_GLOBAL(bool, filesAreDehydrated); FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12(), vfsMode, filesAreDehydrated); // This test can only work when the file is locally available... if (filesAreDehydrated) { // ... so force hydration in case of dehydrated vfs fakeFolder.localModifier().appendByte(QStringLiteral("A/a1")); QVERIFY(fakeFolder.applyLocalModificationsAndSync()); } QStringList seenLockedFiles; connect(&fakeFolder.syncEngine(), &SyncEngine::seenLockedFile, &fakeFolder.syncEngine(), [&](const QString &file) { seenLockedFiles.append(file); }); LocalDiscoveryTracker tracker; connect(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted, &tracker, &LocalDiscoveryTracker::slotItemCompleted); connect(&fakeFolder.syncEngine(), &SyncEngine::finished, &tracker, &LocalDiscoveryTracker::slotSyncFinished); auto hasLocalDiscoveryPath = [&](const QString &path) { auto &paths = tracker.localDiscoveryPaths(); return paths.find(path) != paths.end(); }; // // Local change, attempted upload, but file is locked! // fakeFolder.localModifier().appendByte(QStringLiteral("A/a1")); QVERIFY(fakeFolder.applyLocalModificationsWithoutSync()); tracker.addTouchedPath(QStringLiteral("A/a1")); auto h1 = makeHandle(fakeFolder.localPath() + QStringLiteral("A/a1"), 0); fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); tracker.startSyncPartialDiscovery(); QVERIFY(!fakeFolder.applyLocalModificationsAndSync()); QVERIFY(seenLockedFiles.contains(fakeFolder.localPath() + QStringLiteral("A/a1"))); QVERIFY(seenLockedFiles.size() == 1); QVERIFY(hasLocalDiscoveryPath(QStringLiteral("A/a1"))); h1.close(); fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); tracker.startSyncPartialDiscovery(); fakeFolder.syncJournal().wipeErrorBlacklist(); QVERIFY(fakeFolder.applyLocalModificationsAndSync()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); seenLockedFiles.clear(); QVERIFY(tracker.localDiscoveryPaths().empty()); // // Remote change, attempted download, but file is locked! // fakeFolder.remoteModifier().appendByte(QStringLiteral("A/a1")); QVERIFY(fakeFolder.applyLocalModificationsWithoutSync()); auto h2 = makeHandle(fakeFolder.localPath() + QStringLiteral("A/a1"), 0); fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); tracker.startSyncPartialDiscovery(); QVERIFY(!fakeFolder.applyLocalModificationsAndSync()); QVERIFY(seenLockedFiles.contains(fakeFolder.localPath() + QStringLiteral("A/a1"))); QVERIFY(seenLockedFiles.size() == 1); h2.close(); fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); tracker.startSyncPartialDiscovery(); fakeFolder.syncJournal().wipeErrorBlacklist(); QVERIFY(fakeFolder.applyLocalModificationsAndSync()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } #endif }; QTEST_GUILESS_MAIN(TestLockedFiles) #include "testlockedfiles.moc"