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:
authorErik Verbruggen <erik@verbruggen.consulting>2022-01-13 15:41:26 +0300
committerErik Verbruggen <erik@verbruggen.consulting>2022-01-27 15:40:27 +0300
commit946878ea79c2676d83625afc41cc90a5284dc946 (patch)
tree30085587f87f47643c4622c4b290de507a3c07a7
parentcb130ab1eb5c732d1f93de322296bdca1a8cb600 (diff)
Draft: Tests: use external helper executable to do fs changeswork/winvfs-tests-with-helper
Otherwise the cfsync api will either ignore local changes, or not deliver the correct callbacks.
-rw-r--r--src/libsync/discovery.cpp2
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/testsyncmove.cpp212
-rw-r--r--test/testutils/CMakeLists.txt4
-rw-r--r--test/testutils/syncenginetestutils.cpp198
-rw-r--r--test/testutils/syncenginetestutils.h18
-rw-r--r--test/testutils/test_helper.cpp420
7 files changed, 727 insertions, 128 deletions
diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
index 4dd5bc695..3a9e2bb0d 100644
--- a/src/libsync/discovery.cpp
+++ b/src/libsync/discovery.cpp
@@ -719,7 +719,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
if (dbEntry.isValid()) {
bool typeChange = localEntry.isDirectory != dbEntry.isDirectory();
- if (!typeChange && localEntry.isVirtualFile) {
+ if (!typeChange && localEntry.isVirtualFile && dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) {
if (noServerEntry) {
item->_instruction = CSYNC_INSTRUCTION_REMOVE;
item->_direction = SyncFileItem::Down;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index d706c9d63..cdf98ecf4 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -25,6 +25,7 @@ owncloud_add_test(Utility)
owncloud_add_test(SyncEngine)
owncloud_add_test(SyncVirtualFiles)
owncloud_add_test(SyncMove)
+add_dependencies(SyncMoveTest test_helper)
owncloud_add_test(SyncDelete)
owncloud_add_test(SyncConflict)
owncloud_add_test(SyncFileStatusTracker)
diff --git a/test/testsyncmove.cpp b/test/testsyncmove.cpp
index bfd774a1b..ebed2eedd 100644
--- a/test/testsyncmove.cpp
+++ b/test/testsyncmove.cpp
@@ -117,14 +117,14 @@ private slots:
// Edit a file in a moved directory.
fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
- fakeFolder.syncOnce();
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto oldState = fakeFolder.currentLocalState();
QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
QVERIFY(!oldState.find("folder/folderA/file.txt"));
// This sync should not remove the file
- fakeFolder.syncOnce();
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(fakeFolder.currentLocalState(), oldState);
}
@@ -145,7 +145,7 @@ private slots:
{ "parentFolder/subFolderA/" });
fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
- fakeFolder.syncOnce();
+ QVERIFY(fakeFolder.applyLocalModifications());
{
// Nothing changed on the server
@@ -159,7 +159,7 @@ private slots:
// Rename parentFolder on the server
fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
expectedServerState = fakeFolder.currentRemoteState();
- fakeFolder.syncOnce();
+ QVERIFY(fakeFolder.applyLocalModifications());
{
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
@@ -173,7 +173,7 @@ private slots:
// Rename it again, locally this time.
fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
- fakeFolder.syncOnce();
+ QVERIFY(fakeFolder.applyLocalModifications());
{
auto remoteState = fakeFolder.currentRemoteState();
@@ -185,7 +185,7 @@ private slots:
expectedServerState = fakeFolder.currentRemoteState();
ItemCompletedSpy completeSpy(fakeFolder);
- fakeFolder.syncOnce(); // This sync should do nothing
+ QVERIFY(fakeFolder.applyLocalModifications()); // This sync should do nothing
QCOMPARE(completeSpy.count(), 0);
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
@@ -193,21 +193,25 @@ private slots:
}
}
- void testLocalMoveDetection()
+ void testLocalMoveDetection() // HERE!
{
QFETCH_GLOBAL(Vfs::Mode, vfsMode);
QFETCH_GLOBAL(bool, filesAreDehydrated);
+ if (vfsMode == Vfs::Off) return;
+
FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12(), vfsMode, filesAreDehydrated);
+
OperationCounter counter;
fakeFolder.setServerOverride(counter.functor());
// For directly editing the remote checksum
FileInfo &remoteInfo = fakeFolder.remoteModifier();
-
// Simple move causing a remote rename
fakeFolder.localModifier().rename("A/a1", "A/a1m");
- QVERIFY(fakeFolder.syncOnce());
+// QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
+ qDebug()<<"counters:"<<counter;
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
QCOMPARE(counter.nGET, 0);
@@ -218,22 +222,24 @@ private slots:
// Move-and-change, mtime+size, causing a upload and delete
QVERIFY(fakeFolder.currentLocalState().find("A/a2")->isDehydratedPlaceholder == filesAreDehydrated); // no-one touched it, so the hydration state should be the same as the initial state
+ auto mt = fakeFolder.currentLocalState().find("A/a2")->lastModified();
+ QVERIFY(mt.toSecsSinceEpoch() + 1 < QDateTime::currentDateTime().toSecsSinceEpoch());
fakeFolder.localModifier().rename("A/a2", "A/a2m");
fakeFolder.localModifier().setContents("A/a2m", 'x', fakeFolder.remoteModifier().contentSize + 1);
- QVERIFY(fakeFolder.syncOnce());
+ fakeFolder.localModifier().setModTime("A/a2m", mt.addSecs(1));
+ QVERIFY(fakeFolder.applyLocalModifications());
QVERIFY(!fakeFolder.currentLocalState().find("A/a2m")->isDehydratedPlaceholder); // We overwrote all data in the file, so whatever the state was before, it is no longer dehydrated
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
- QCOMPARE(counter.nGET, 0); // we never *read* the file (i.e. we didn't *append* data to it), so even in the dehydrated case the data should never be downloaded
- QCOMPARE(counter.nMOVE, 0); // we cannot detect moves (and we didn't implement it yet in winvfs)...
- QCOMPARE(counter.nDELETE, 1); // ... so the file just disappears ...
- QCOMPARE(counter.nPUT, 1); // ... and another file (with just 1 byte difference) appears somewhere else. Coincidence.
+ QCOMPARE(counter.nGET, filesAreDehydrated ? 1 : 0); // on winvfs, with a dehydrated file, the os will try to hydrate the file before we write to it. When the file is hydrated, it doesn't need to be fetched.
+ QCOMPARE(counter.nMOVE, 0); // we cannot detect moves (and we didn't implement it yet in winvfs), so ...
+ QCOMPARE(counter.nDELETE, 1); // ... the file just disappears, and ...
+ QCOMPARE(counter.nPUT, 1); // ... another file (with just 1 byte difference) appears somewhere else. Coincidence.
counter.reset();
-
// Move-and-change, mtime+content only
fakeFolder.localModifier().rename("B/b1", "B/b1m");
fakeFolder.localModifier().setContents("B/b1m", 'C');
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
QCOMPARE(counter.nPUT, 1);
@@ -245,31 +251,44 @@ private slots:
fakeFolder.localModifier().rename("B/b2", "B/b2m");
fakeFolder.localModifier().appendByte("B/b2m");
fakeFolder.localModifier().setModTime("B/b2m", mtime);
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
+ QCOMPARE(counter.nGET, 1);
QCOMPARE(counter.nPUT, 1);
+ QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nDELETE, 1);
counter.reset();
- // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
- // NOTE: This is an expected failure.
- mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified();
- fakeFolder.localModifier().rename("C/c1", "C/c1m");
- fakeFolder.localModifier().setContents("C/c1m", 'C');
- fakeFolder.localModifier().setModTime("C/c1m", mtime);
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(counter.nPUT, 0);
- QCOMPARE(counter.nDELETE, 0);
- QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
- counter.reset();
+ // WinVFS handles this just fine.
+ if (vfsMode == Vfs::Off) {
+ // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
+ // NOTE: This is an expected failure.
+ mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified();
+ fakeFolder.localModifier().rename("C/c1", "C/c1m");
+ fakeFolder.localModifier().setContents("C/c1m", 'C');
+ fakeFolder.localModifier().setModTime("C/c1m", mtime);
+ QVERIFY(fakeFolder.applyLocalModifications());
+ QCOMPARE(counter.nPUT, 0);
+ QCOMPARE(counter.nDELETE, 0);
+ QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
+ counter.reset();
+ }
// cleanup, and upload a file that will have a checksum in the db
- fakeFolder.localModifier().remove("C/c1m");
- fakeFolder.localModifier().insert("C/c3");
- QVERIFY(fakeFolder.syncOnce());
+ if (vfsMode == Vfs::Off) {
+ // rename happened in the previous test
+ fakeFolder.localModifier().remove("C/c1m");
+ } else {
+ // no rename happened, remove the "original"
+ fakeFolder.localModifier().remove("C/c1");
+ }
+ fakeFolder.localModifier().insert("C/c3", 13, 'E');
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
+ QCOMPARE(counter.nGET, 0);
+ QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nPUT, 1);
QCOMPARE(counter.nDELETE, 1);
counter.reset();
@@ -279,7 +298,9 @@ private slots:
fakeFolder.localModifier().rename("C/c3", "C/c3m");
fakeFolder.localModifier().setContents("C/c3m", 'C');
fakeFolder.localModifier().setModTime("C/c3m", mtime);
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
+ QCOMPARE(counter.nGET, 0);
+ QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nPUT, 1);
QCOMPARE(counter.nDELETE, 1);
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
@@ -326,7 +347,7 @@ private slots:
// This already checks that the rename detection doesn't get
// horribly confused if we add new files that have the same
// fileid as existing ones
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
OperationCounter counter;
@@ -335,28 +356,28 @@ private slots:
// Try a remote file move
remote.rename("A/a1", "A/W/a1m");
remote.rename(prefix + "/A/a1", prefix + "/A/W/a1m");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 0);
// And a remote directory move
remote.rename("A/W", "A/Q/W");
remote.rename(prefix + "/A/W", prefix + "/A/Q/W");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 0);
// Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
remote.rename(prefix + "/A/a2", prefix + "/a2");
remote.remove("A/a2");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 0);
// Local change plus remote move at the same time
fakeFolder.localModifier().appendByte(prefix + "/a2");
remote.rename(prefix + "/a2", prefix + "/a3");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 1);
counter.reset();
@@ -365,7 +386,7 @@ private slots:
fakeFolder.localModifier().remove("A/Q/W/a1m");
remote.rename("A/Q/W/a1m", "A/Q/W/a1p");
remote.rename(prefix + "/A/Q/W/a1m", prefix + "/A/Q/W/a1p");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 1);
counter.reset();
@@ -376,10 +397,6 @@ private slots:
QFETCH_GLOBAL(Vfs::Mode, vfsMode);
QFETCH_GLOBAL(bool, filesAreDehydrated);
- if (filesAreDehydrated) {
- QSKIP("This test expects to be able to modify local files on disk, which does not work with dehydrated files.");
- }
-
FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12(), vfsMode, filesAreDehydrated);
auto &local = fakeFolder.localModifier();
auto &remote = fakeFolder.remoteModifier();
@@ -388,14 +405,14 @@ private slots:
fakeFolder.setServerOverride(counter.functor());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ counter.reset();
// Move
{
- counter.reset();
local.rename("A/a1", "A/a1m");
remote.rename("B/b1", "B/b1m");
ItemCompletedSpy completeSpy(fakeFolder);
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(counter.nGET, 0);
QCOMPARE(counter.nPUT, 0);
@@ -407,15 +424,15 @@ private slots:
QCOMPARE(completeSpy.findItem("A/a1m")->_renameTarget, QStringLiteral("A/a1m"));
QCOMPARE(completeSpy.findItem("B/b1m")->_file, QStringLiteral("B/b1"));
QCOMPARE(completeSpy.findItem("B/b1m")->_renameTarget, QStringLiteral("B/b1m"));
+ counter.reset();
}
// Touch+Move on same side
- counter.reset();
local.rename("A/a2", "A/a2m");
local.setContents("A/a2m", 'A');
remote.rename("B/b2", "B/b2m");
remote.setContents("B/b2m", 'A');
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QCOMPARE(counter.nGET, 1);
@@ -424,32 +441,50 @@ private slots:
QCOMPARE(counter.nDELETE, 1);
QCOMPARE(remote.find("A/a2m")->contentChar, 'A');
QCOMPARE(remote.find("B/b2m")->contentChar, 'A');
+ counter.reset();
// Touch+Move on opposite sides
- counter.reset();
local.rename("A/a1m", "A/a1m2");
remote.setContents("A/a1m", 'B');
remote.rename("B/b1m", "B/b1m2");
local.setContents("B/b1m", 'B');
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
+ qDebug()<<" local:"<<fakeFolder.currentLocalState();
+ qDebug()<<"remote:"<<fakeFolder.currentRemoteState();
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
- QCOMPARE(counter.nGET, 2);
- QCOMPARE(counter.nPUT, 2);
- QCOMPARE(counter.nMOVE, 0);
- QCOMPARE(counter.nDELETE, 0);
+ if (vfsMode == Vfs::Off) {
+ QCOMPARE(counter.nGET, 2);
+ QCOMPARE(counter.nPUT, 2);
+ QCOMPARE(counter.nMOVE, 0);
+ QCOMPARE(counter.nDELETE, 0);
+ } else {
+ QCOMPARE(counter.nGET, 0);
+ QCOMPARE(counter.nPUT, 1); // the setContents for the "new" file b1m
+ QCOMPARE(counter.nMOVE, 1); // the rename of a1m to a1m2
+ QCOMPARE(counter.nDELETE, 0);
+ }
+
+ if (vfsMode != Vfs::Off) {
+ QSKIP("Behaviour for any VFS is different at this point compared to no-VFS");
+ }
+
// All these files existing afterwards is debatable. Should we propagate
// the rename in one direction and grab the new contents in the other?
// Currently there's no propagation job that would do that, and this does
// at least not lose data.
+ QVERIFY(remote.find("A/a1m")->contentChar == 'B');
+ QVERIFY(remote.find("B/b1m")->contentChar== 'B');
+ QVERIFY(remote.find("A/a1m2")->contentChar== 'W');
+ QVERIFY(remote.find("B/b1m2")->contentChar== 'W');
QCOMPARE(remote.find("A/a1m")->contentChar, 'B');
QCOMPARE(remote.find("B/b1m")->contentChar, 'B');
QCOMPARE(remote.find("A/a1m2")->contentChar, 'W');
QCOMPARE(remote.find("B/b1m2")->contentChar, 'W');
+ counter.reset();
// Touch+create on one side, move on the other
{
- counter.reset();
local.appendByte("A/a1m");
local.insert("A/a1mt");
remote.rename("A/a1m", "A/a1mt");
@@ -457,49 +492,59 @@ private slots:
remote.insert("B/b1mt");
local.rename("B/b1m", "B/b1mt");
ItemCompletedSpy completeSpy(fakeFolder);
- QVERIFY(fakeFolder.syncOnce());
- QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
- QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
- QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
+ QVERIFY(fakeFolder.applyLocalModifications());
+ // First check the counters:
QCOMPARE(counter.nGET, 3);
QCOMPARE(counter.nPUT, 1);
QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nDELETE, 0);
+ // Ok, now we can remove the conflicting files. This needs disk access, so it might trigger server interaction. (Hence checking the counters before we do this.)
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
+ QVERIFY(fakeFolder.applyLocalModifications());
+ // Now we can compare the clean-up states:
+ qDebug()<<" local:"<<fakeFolder.currentLocalState();
+ qDebug()<<"remote:"<<fakeFolder.currentRemoteState();
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QVERIFY(itemSuccessful(completeSpy, "A/a1m", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemSuccessful(completeSpy, "B/b1m", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemConflict(completeSpy, "A/a1mt"));
QVERIFY(itemConflict(completeSpy, "B/b1mt"));
+ counter.reset();
}
// Create new on one side, move to new on the other
{
- counter.reset();
local.insert("A/a1N", 13);
remote.rename("A/a1mt", "A/a1N");
remote.insert("B/b1N", 13);
local.rename("B/b1mt", "B/b1N");
ItemCompletedSpy completeSpy(fakeFolder);
- QVERIFY(fakeFolder.syncOnce());
- QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
- QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
- QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
+ QVERIFY(fakeFolder.applyLocalModifications());
+ // First check the counters:
QCOMPARE(counter.nGET, 2);
QCOMPARE(counter.nPUT, 0);
QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nDELETE, 1);
+ // Ok, now we can remove the conflicting files. This needs disk access, so it might trigger server interaction. (Hence checking the counters before we do this.)
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
+ QVERIFY(fakeFolder.applyLocalModifications());
+ // Now we can compare the clean-up states:
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QVERIFY(itemSuccessful(completeSpy, "A/a1mt", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemSuccessful(completeSpy, "B/b1mt", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemConflict(completeSpy, "A/a1N"));
QVERIFY(itemConflict(completeSpy, "B/b1N"));
+ counter.reset();
}
// Local move, remote move
- counter.reset();
local.rename("C/c1", "C/c1mL");
remote.rename("C/c1", "C/c1mR");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
// end up with both files
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
@@ -512,7 +557,7 @@ private slots:
counter.reset();
remote.rename("C", "CMR");
local.rename("C", "CML");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
// End up with both folders
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
@@ -520,14 +565,14 @@ private slots:
QCOMPARE(counter.nPUT, 3);
QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nDELETE, 0);
+ counter.reset();
// Folder move
{
- counter.reset();
local.rename("A", "AM");
remote.rename("B", "BM");
ItemCompletedSpy completeSpy(fakeFolder);
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QCOMPARE(counter.nGET, 0);
@@ -540,11 +585,11 @@ private slots:
QCOMPARE(completeSpy.findItem("AM")->_renameTarget, QStringLiteral("AM"));
QCOMPARE(completeSpy.findItem("BM")->_file, QStringLiteral("B"));
QCOMPARE(completeSpy.findItem("BM")->_renameTarget, QStringLiteral("BM"));
+ counter.reset();
}
// Folder move with contents touched on the same side
{
- counter.reset();
local.setContents("AM/a2m", 'C');
// We must change the modtime for it is likely that it did not change between sync.
// (Previous version of the client (<=2.5) would not need this because it was always doing
@@ -555,7 +600,7 @@ private slots:
remote.setContents("BM/b2m", 'C');
remote.rename("BM", "B2");
ItemCompletedSpy completeSpy(fakeFolder);
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QCOMPARE(counter.nGET, 1);
@@ -566,10 +611,10 @@ private slots:
QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
+ counter.reset();
}
// Folder rename with contents touched on the other tree
- counter.reset();
remote.setContents("A2/a2m", 'D');
// setContents alone may not produce updated mtime if the test is fast
// and since we don't use checksums here, that matters.
@@ -578,7 +623,7 @@ private slots:
local.setContents("B2/b2m", 'D');
local.appendByte("B2/b2m");
remote.rename("B2", "B3");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QCOMPARE(counter.nGET, 1);
@@ -587,9 +632,9 @@ private slots:
QCOMPARE(counter.nDELETE, 0);
QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
+ counter.reset();
// Folder rename with contents touched on both ends
- counter.reset();
remote.setContents("A3/a2m", 'R');
remote.appendByte("A3/a2m");
local.setContents("A3/a2m", 'L');
@@ -602,7 +647,13 @@ private slots:
local.appendByte("B3/b2m");
local.appendByte("B3/b2m");
remote.rename("B3", "B4");
- QVERIFY(fakeFolder.syncOnce());
+ QThread::sleep(1); // This test is timing-sensitive. No idea why, it's probably the modtime on the client side.
+ QVERIFY(fakeFolder.applyLocalModifications());
+ qDebug() << counter;
+ QCOMPARE(counter.nGET, 2);
+ QCOMPARE(counter.nPUT, 0);
+ QCOMPARE(counter.nMOVE, 1);
+ QCOMPARE(counter.nDELETE, 0);
auto currentLocal = fakeFolder.currentLocalState();
auto conflicts = findConflicts(currentLocal.children["A4"]);
QCOMPARE(conflicts.size(), 1);
@@ -616,22 +667,19 @@ private slots:
QCOMPARE(currentLocal.find(c)->contentChar, 'L');
local.remove(c);
}
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
- QCOMPARE(counter.nGET, 2);
- QCOMPARE(counter.nPUT, 0);
- QCOMPARE(counter.nMOVE, 1);
- QCOMPARE(counter.nDELETE, 0);
QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
+ counter.reset();
// Rename a folder and rename the contents at the same time
- counter.reset();
local.rename("A4/a2m", "A4/a2m2");
local.rename("A4", "A5");
remote.rename("B4/b2m", "B4/b2m2");
remote.rename("B4", "B5");
- QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.applyLocalModifications());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
QCOMPARE(counter.nGET, 0);
@@ -803,7 +851,7 @@ private slots:
QFETCH(bool, local);
FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12(), vfsMode, filesAreDehydrated);
- auto &modifier = local ? fakeFolder.localModifier() : fakeFolder.remoteModifier();
+ FileModifier &modifier = local ? fakeFolder.localModifier() : fakeFolder.remoteModifier();
modifier.mkdir("FolA");
modifier.mkdir("FolA/FolB");
diff --git a/test/testutils/CMakeLists.txt b/test/testutils/CMakeLists.txt
index a27fa720b..e58c67bd5 100644
--- a/test/testutils/CMakeLists.txt
+++ b/test/testutils/CMakeLists.txt
@@ -1,5 +1,9 @@
+add_executable(test_helper test_helper.cpp)
+target_link_libraries(test_helper PUBLIC Qt5::Core libsync)
+
add_library(syncenginetestutils STATIC syncenginetestutils.cpp testutils.cpp)
target_link_libraries(syncenginetestutils PUBLIC owncloudCore Qt5::Test)
+target_compile_definitions(syncenginetestutils PRIVATE TEST_HELPER_EXE="$<TARGET_FILE:test_helper>")
# testutilsloader.cpp uses Q_COREAPP_STARTUP_FUNCTION which can't used reliably in a static lib
# therefore we compile it in the tests
diff --git a/test/testutils/syncenginetestutils.cpp b/test/testutils/syncenginetestutils.cpp
index 5ee5f74a4..3a0a55219 100644
--- a/test/testutils/syncenginetestutils.cpp
+++ b/test/testutils/syncenginetestutils.cpp
@@ -38,53 +38,64 @@ PathComponents PathComponents::subComponents() const &
void DiskFileModifier::remove(const QString &relativePath)
{
- QFileInfo fi { _rootDir.filePath(relativePath) };
- if (fi.isFile())
- QVERIFY(_rootDir.remove(relativePath));
- else
- QVERIFY(QDir { fi.filePath() }.removeRecursively());
+// QFileInfo fi { _rootDir.filePath(relativePath) };
+// if (fi.isFile())
+// QVERIFY(_rootDir.remove(relativePath));
+// else
+// QVERIFY(QDir { fi.filePath() }.removeRecursively());
+
+ _processArguments.append({ "remove", relativePath });
}
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();
+// 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();
+
+ _processArguments.append({ "insert", relativePath, QString::number(size), QChar::fromLatin1(contentChar) });
// 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);
+ setModTime(relativePath, QDateTime::currentDateTimeUtc().addSecs(-30));
}
void DiskFileModifier::setContents(const QString &relativePath, char contentChar, int newSize)
{
- QFile file { _rootDir.filePath(relativePath) };
- QVERIFY(file.exists());
- qint64 size = file.size();
- if (newSize != -1) {
- size = newSize;
- }
- QVERIFY(file.open(QFile::WriteOnly));
- QCOMPARE(file.write(QByteArray {}.fill(contentChar, size)), size);
+ _processArguments.append({ "contents", relativePath, QString::number(newSize), QChar::fromLatin1(contentChar) });
+
+// QFile file { _rootDir.filePath(relativePath) };
+// OC_ENFORCE(file.exists());
+// qint64 size = file.size();
+// if (newSize != -1) {
+// size = newSize;
+// }
+// QVERIFY(file.open(QFile::WriteOnly));
+// OC_ENFORCE(file.write(QByteArray {}.fill(contentChar, size)) == size);
}
void DiskFileModifier::appendByte(const QString &relativePath, char contentChar)
{
- QFile file { _rootDir.filePath(relativePath) };
- QVERIFY(file.exists());
- file.open(QFile::ReadWrite);
- QByteArray contents;
- if (contentChar)
- contents += contentChar;
- else
- contents = file.read(1);
- file.seek(file.size());
- file.write(contents);
+// QFile file { _rootDir.filePath(relativePath) };
+// OC_ENSURE(file.exists());
+// file.open(QFile::ReadWrite);
+// QByteArray contents;
+// if (contentChar)
+// contents += contentChar;
+// else
+// contents = file.read(1);
+// OC_ENSURE(file.seek(file.size()));
+// OC_ENSURE(file.write(contents) == contents.size());
+
+ if (contentChar == '\0') {
+ contentChar = 'X';
+ }
+
+ _processArguments.append({ "appendbyte", relativePath, QChar::fromLatin1(contentChar) });
}
void DiskFileModifier::modifyByte(const QString &relativePath, quint64 offset, char contentChar)
@@ -104,13 +115,17 @@ void DiskFileModifier::mkdir(const QString &relativePath)
void DiskFileModifier::rename(const QString &from, const QString &to)
{
- QVERIFY(_rootDir.exists(from));
- QVERIFY(_rootDir.rename(from, to));
+// QVERIFY(_rootDir.exists(from));
+// QVERIFY(_rootDir.rename(from, to));
+
+ _processArguments.append({"rename", from, to });
}
void DiskFileModifier::setModTime(const QString &relativePath, const QDateTime &modTime)
{
- OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
+// OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
+
+ _processArguments.append({"mtime", relativePath, QString::number(modTime.toSecsSinceEpoch()) });
}
void DiskFileModifier::incModTime(const QString &relativePath, int secondsToAdd)
@@ -120,6 +135,88 @@ void DiskFileModifier::incModTime(const QString &relativePath, int secondsToAdd)
setModTime(relativePath, newMTime);
}
+class HelperProcess : public QProcess
+{
+public:
+ HelperProcess(const QDir &rootDir, QStringList processArguments) // copy is on purpose
+ {
+ if (processArguments.isEmpty()) { // fast-path:
+ finished = true;
+ succeeded = true;
+ return;
+ }
+
+ setProcessChannelMode(QProcess::MergedChannels);
+ processArguments.prepend(rootDir.absolutePath());
+ QObject::connect(this, &QProcess::readyRead, [this]() {
+ while (canReadLine()) {
+ qDebug() << "helper output:" << readLine();
+ }
+ });
+ QObject::connect(this, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
+ [this](int exitCode, QProcess::ExitStatus exitStatus) {
+ qDebug() << "helper finished:" << exitCode << exitStatus;
+ this->finished = true;
+ this->succeeded = exitStatus == QProcess::NormalExit && exitCode == 0;
+ });
+
+ qDebug() << "Starting helper:" << TEST_HELPER_EXE << processArguments;
+ start(TEST_HELPER_EXE, processArguments);
+ }
+
+public:
+ bool finished = false;
+ bool succeeded = true;
+};
+
+bool DiskFileModifier::applyModifications(FakeFolder &ff, OCC::Vfs::Mode mode)
+{
+ if (mode == OCC::Vfs::Off) { // Classic mode:
+ HelperProcess helper(_rootDir, _processArguments);
+ _processArguments.clear(); // the helper has a copy, we clean it now for the next run
+ while (!helper.finished) {
+ QThread::currentThread()->eventDispatcher()->processEvents(QEventLoop::AllEvents);
+ helper.waitForFinished();
+ }
+ if (!helper.succeeded) {
+ return false;
+ }
+ return ff.syncOnce();
+ }
+
+ // Non-classic mode:
+
+ // apply any remote modifications
+ ff.syncOnce();
+
+ if (_processArguments.isEmpty()) {
+ // nothing to do
+ return true;
+ }
+
+ HelperProcess p(_rootDir, _processArguments);
+
+ ;
+ if (!(p.waitForStarted() && p.succeeded)) {
+ return false;
+ }
+
+ do {
+ // process any pending events
+ QThread::currentThread()->eventDispatcher()->processEvents(QEventLoop::AllEvents);
+ // wait a bit to get the helper started with it's work
+ QThread::msleep(50);
+ // process any output from the helper
+ QThread::currentThread()->eventDispatcher()->processEvents(QEventLoop::AllEvents);
+ // now we might need to sync
+ ff.syncOnce();
+ QThread::currentThread()->eventDispatcher()->processEvents(QEventLoop::AllEvents);
+ } while (!p.finished);
+
+ _processArguments.clear();
+ return p.succeeded;
+}
+
FileInfo FileInfo::A12_B12_C12_S12()
{
FileInfo fi { QString {}, {
@@ -282,35 +379,42 @@ bool FileInfo::equals(const FileInfo &other, CompareWhat compareWhat) const
// Only check the content and contentSize if both files are hydrated:
if (!isDehydratedPlaceholder && !other.isDehydratedPlaceholder) {
if (contentSize != other.contentSize || contentChar != other.contentChar) {
+ qDebug() << "1" << name << "!=" << other.name;
return false;
}
}
// We need to check this before we use isDir in the next if-statement:
if (isDir != other.isDir) {
+ qDebug() << "2" << name << "!=" << other.name;
return false;
}
if (compareWhat == CompareLastModified) {
// Don't check directory mtime: it might change when (unsynced) files get created.
if (!isDir && _lastModifiedInSecondsUTC != other._lastModifiedInSecondsUTC) {
+ qDebug() << "3" << name << "!=" << other.name;
return false;
}
}
if (name != other.name || fileSize != other.fileSize) {
+ qDebug() << "4" << name << "!=" << other.name;
return false;
}
if (children.size() != other.children.size()) {
+ qDebug() << "5" << name << "!=" << other.name;
return false;
}
for (auto it = children.constBegin(), eit = children.constEnd(); it != eit; ++it) {
auto oit = other.children.constFind(it.key());
if (oit == other.children.constEnd()) {
+ qDebug() << "6" << name << "!=" << other.name;
return false;
} else if (!it.value().equals(oit.value(), compareWhat)) {
+ qDebug() << "7" << name << "!=" << other.name;
return false;
}
}
@@ -581,7 +685,7 @@ FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::
Q_ASSERT(!fileName.isEmpty());
fileInfo = remoteRootFileInfo.find(fileName);
if (!fileInfo) {
- qDebug() << "meh;";
+ qDebug() << "File does not exist:" << fileName;
}
Q_ASSERT_X(fileInfo, Q_FUNC_INFO, "Could not find file on the remote");
QMetaObject::invokeMethod(this, &FakeGetReply::respond, Qt::QueuedConnection);
@@ -952,7 +1056,7 @@ FakeFolder::FakeFolder(const FileInfo &fileTemplate, OCC::Vfs::Mode vfsMode, boo
QDir rootDir { _tempDir.path() };
qDebug() << "FakeFolder operating on" << rootDir;
- toDisk(rootDir, filesAreDehydrated ? FileInfo() : fileTemplate);
+ toDisk(rootDir, FileInfo());//filesAreDehydrated ? FileInfo() : fileTemplate);
_fakeQnam = new FakeQNAM(fileTemplate);
_account = OCC::Account::create();
@@ -983,7 +1087,9 @@ FakeFolder::FakeFolder(const FileInfo &fileTemplate, OCC::Vfs::Mode vfsMode, boo
switchToVfs(vfs);
if (vfsMode != OCC::Vfs::Off) {
- syncJournal().internalPinStates().setForPath("", filesAreDehydrated ? OCC::PinState::OnlineOnly : OCC::PinState::AlwaysLocal);
+ const auto pinState = filesAreDehydrated ? OCC::PinState::OnlineOnly : OCC::PinState::AlwaysLocal;
+ syncJournal().internalPinStates().setForPath("", pinState);
+ vfs->setPinState("", pinState);
}
// A new folder will update the local file state database on first sync.
@@ -1010,10 +1116,13 @@ void FakeFolder::switchToVfs(QSharedPointer<OCC::Vfs> vfs)
vfsParams.providerName = QStringLiteral("OC-TEST");
vfsParams.providerDisplayName = QStringLiteral("OC-TEST");
vfsParams.providerVersion = QStringLiteral("0.1");
+ vfsParams.multipleAccountsRegistered = false;
QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
vfs->stop();
vfs->unregisterFolder();
});
+ QObject::connect(&_syncEngine->syncFileStatusTracker(), &OCC::SyncFileStatusTracker::fileStatusChanged,
+ vfs.data(), &OCC::Vfs::fileStatusChanged);
QObject::connect(vfs.get(), &OCC::Vfs::error, vfs.get(), [](const QString &error) {
QFAIL(qUtf8Printable(error));
@@ -1075,7 +1184,12 @@ void FakeFolder::execUntilItemCompleted(const QString &relativePath)
bool FakeFolder::isDehydratedPlaceholder(const QString &filePath)
{
- return _syncEngine->syncOptions()._vfs->isDehydratedPlaceholder(filePath);
+ return vfs()->isDehydratedPlaceholder(filePath);
+}
+
+QSharedPointer<OCC::Vfs> FakeFolder::vfs() const
+{
+ return _syncEngine->syncOptions()._vfs;
}
void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
@@ -1117,7 +1231,7 @@ void FakeFolder::fromDisk(QDir &dir, FileInfo &templateFi)
fi.contentSize = 0;
} else {
QFile f { diskChild.filePath() };
- f.open(QFile::ReadOnly);
+ OC_ENFORCE(f.open(QFile::ReadOnly));
auto content = f.read(1);
if (content.size() == 0) {
qWarning() << "Empty file at:" << diskChild.filePath();
diff --git a/test/testutils/syncenginetestutils.h b/test/testutils/syncenginetestutils.h
index 6ae99a963..c847a9042 100644
--- a/test/testutils/syncenginetestutils.h
+++ b/test/testutils/syncenginetestutils.h
@@ -82,7 +82,7 @@ public:
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, int newSize = -1) = 0;
- virtual void appendByte(const QString &relativePath, char contentChar = 0) = 0;
+ virtual void appendByte(const QString &relativePath, char contentChar = 'X') = 0;
virtual void modifyByte(const QString &relativePath, quint64 offset, char contentChar) = 0;
virtual void mkdir(const QString &relativePath) = 0;
virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
@@ -90,9 +90,12 @@ public:
virtual void incModTime(const QString &relativePath, int secondsToAdd) = 0;
};
+class FakeFolder;
+
class DiskFileModifier : public FileModifier
{
QDir _rootDir;
+ QStringList _processArguments;
public:
DiskFileModifier(const QString &rootDirPath)
@@ -102,13 +105,15 @@ public:
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, int newSize = -1) override;
- void appendByte(const QString &relativePath, char contentChar) override;
+ void appendByte(const QString &relativePath, char contentChar = 'X') override;
void modifyByte(const QString &relativePath, quint64 offset, char contentChar) 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;
void incModTime(const QString &relativePath, int secondsToAdd) override;
+
+ bool applyModifications(FakeFolder &ff, OCC::Vfs::Mode mode) Q_REQUIRED_RESULT;
};
static inline qint64 defaultLastModified()
@@ -154,7 +159,7 @@ public:
void setContents(const QString &relativePath, char contentChar, int newSize = -1) override;
- void appendByte(const QString &relativePath, char contentChar = 0) override;
+ void appendByte(const QString &relativePath, char contentChar = 'X') override;
void modifyByte(const QString &relativePath, quint64 offset, char contentChar) override;
@@ -580,7 +585,14 @@ public:
return execUntilFinished();
}
+ bool applyLocalModifications() Q_REQUIRED_RESULT
+ {
+ auto mode = _syncEngine->syncOptions()._vfs->mode();
+ return _localModifier.applyModifications(*this, mode);
+ }
+
bool isDehydratedPlaceholder(const QString &filePath);
+ QSharedPointer<OCC::Vfs> vfs() const;
private:
static void toDisk(QDir &dir, const FileInfo &templateFi);
diff --git a/test/testutils/test_helper.cpp b/test/testutils/test_helper.cpp
new file mode 100644
index 000000000..b931112f5
--- /dev/null
+++ b/test/testutils/test_helper.cpp
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+
+#include <iostream>
+#include <vector>
+
+#include <libsync/filesystem.h>
+
+using namespace std;
+
+class Command
+{
+public:
+ Command(const QString &fileName): _fileName(fileName) {}
+ virtual ~Command() = 0;
+
+ virtual bool execute(QDir &rootDir) const = 0;
+
+protected:
+ static QString parseFileName(QStringListIterator &it)
+ {
+ return it.next();
+ }
+
+ const QString _fileName;
+};
+
+Command::~Command() {}
+
+class SetMtimeCommand: public Command
+{
+public:
+ static const QString name;
+
+ SetMtimeCommand(const QString &fileName, qlonglong secs): Command(fileName), _secs(secs){}
+ ~SetMtimeCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ return OCC::FileSystem::setModTime(rootDir.filePath(_fileName), _secs);
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ QString secsStr = it.next();
+ bool ok = false;
+ auto secs = secsStr.toLongLong(&ok);
+ if (!ok) {
+ cerr << "Error: '" << qPrintable(secsStr) << "' is not a valid number (" << qPrintable(name) << ")" << endl;
+ return {};
+ }
+
+ return new SetMtimeCommand(fileName, secs);
+ }
+
+private:
+ const qlonglong _secs;
+};
+
+const QString SetMtimeCommand::name = QStringLiteral("mtime");
+
+class SetContentsCommand: public Command
+{
+public:
+ static const QString name;
+
+ SetContentsCommand(const QString &fileName, unsigned count, char ch): Command(fileName), _count(count), _ch(ch) {}
+ ~SetContentsCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ QFile f(rootDir.filePath(_fileName));
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ cerr << "Error: cannot open file '" << qPrintable(_fileName) << "' for " << qPrintable(name) << " command: "
+ << qPrintable(f.errorString()) << endl;
+ return false;
+ }
+ int count = _count == -1 ? 32 : _count;
+ auto written = f.write(QByteArray(count, _ch));
+ if (written != count) {
+ cerr << "Error: wrote " << written << " bytes to '" << qPrintable(_fileName) << "' instead of requested " << _count << " bytes" << endl;
+ return false;
+ }
+ f.close();
+ return true;
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ QString countStr = it.next();
+ bool ok = false;
+ auto count = countStr.toInt(&ok);
+ if (!ok) {
+ cerr << "Error: '" << qPrintable(countStr) << "' is not a valid number (" << qPrintable(name) << ")" << endl;
+ return {};
+ }
+
+ QString charStr = it.next();
+ if (charStr.size() != 1) {
+ cerr << "Error: content for " << qPrintable(name) << " command should be 1 character in size" << endl;
+ }
+
+ return new SetContentsCommand(fileName, count, charStr.at(0).toLatin1());
+ }
+
+private:
+ const int _count;
+ const char _ch;
+};
+
+const QString SetContentsCommand::name = QStringLiteral("contents");
+
+class RenameCommand: public Command
+{
+public:
+ static const QString name;
+
+ RenameCommand(const QString &fileName, const QString &newName): Command(fileName), _newName(newName) {}
+ ~RenameCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ if (!rootDir.exists(_fileName)) {
+ cerr << "File does not exist: " << qPrintable(rootDir.absoluteFilePath(_fileName)) << endl;
+ return false;
+ }
+
+ bool success = rootDir.rename(_fileName, _newName);
+ if (!success) {
+ cerr << "Rename of " << qPrintable(_fileName) << " failed" << endl;
+ }
+ return success;
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ QString newName = it.next();
+ if (newName.isEmpty()) {
+ cerr << "Error: invalid new name for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ return new RenameCommand(fileName, newName);
+ }
+
+private:
+ const QString _newName;
+};
+
+const QString RenameCommand::name = QStringLiteral("rename");
+
+class AppendByteCommand: public Command
+{
+public:
+ static const QString name;
+
+ AppendByteCommand(const QString &fileName, char ch): Command(fileName), _ch(ch) {}
+ ~AppendByteCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ QFile f(rootDir.filePath(_fileName));
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) {
+ cerr << "Error: cannot open file '" << qPrintable(_fileName) << "' for " << qPrintable(name) << " command: "
+ << qPrintable(f.errorString()) << endl;
+ return false;
+ }
+ char ch = _ch;
+ if (ch == '\0') {
+ ch = f.readAll().at(0);
+ }
+ if (!f.seek(f.size())) {
+ cerr << "Error: cannot seek to EOF in '" << qPrintable(_fileName) << "' for " << qPrintable(name) << " command";
+ return false;
+ }
+ auto written = f.write(QByteArray(1, ch));
+ if (written != 1) {
+ cerr << "Error: wrote " << written << " bytes to '" << qPrintable(_fileName) << "' instead of requested 1 bytes" << endl;
+ return false;
+ }
+ f.close();
+ return true;
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ QString charStr = it.next();
+ if (charStr.size() != 1) {
+ cerr << "Error: content for " << qPrintable(name) << " command should be 1 character in size" << endl;
+ }
+
+ return new AppendByteCommand(fileName, charStr.at(0).toLatin1());
+ }
+
+private:
+ const char _ch;
+};
+
+const QString AppendByteCommand::name = QStringLiteral("appendbyte");
+
+class InsertCommand: public Command
+{
+public:
+ static const QString name;
+
+ InsertCommand(const QString &fileName, int count, char ch): Command(fileName), _count(count), _ch(ch) {}
+ ~InsertCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ QFile f(rootDir.filePath(_fileName));
+ if (f.exists()) {
+ cerr << "Error: file '" << qPrintable(_fileName) << "' for " << qPrintable(name) << " command already exists" << endl;
+ return false;
+ }
+ if (!f.open(QIODevice::WriteOnly)) {
+ cerr << "Error: cannot open file '" << qPrintable(_fileName) << "' for " << qPrintable(name) << " command: "
+ << qPrintable(f.errorString()) << endl;
+ return false;
+ }
+ auto written = f.write(QByteArray(_count, _ch));
+ if (written != _count) {
+ cerr << "Error: wrote " << written << " bytes to '" << qPrintable(_fileName) << "' instead of requested " << _count << " bytes" << endl;
+ return false;
+ }
+ f.close();
+ return true;
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ QString countStr = it.next();
+ bool ok = false;
+ auto count = countStr.toInt(&ok);
+ if (!ok) {
+ cerr << "Error: '" << qPrintable(countStr) << "' is not a valid number (" << qPrintable(name) << ")" << endl;
+ return {};
+ }
+
+ QString charStr = it.next();
+ if (charStr.size() != 1) {
+ cerr << "Error: content for " << qPrintable(name) << " command should be 1 character in size" << endl;
+ }
+
+ return new InsertCommand(fileName, count, charStr.at(0).toLatin1());
+ }
+
+private:
+ const int _count;
+ const char _ch;
+};
+
+const QString InsertCommand::name = QStringLiteral("insert");
+
+class RemoveCommand: public Command
+{
+public:
+ static const QString name;
+
+ RemoveCommand(const QString &fileName): Command(fileName) {}
+ ~RemoveCommand() override {}
+
+ bool execute(QDir &rootDir) const override
+ {
+ cerr << qPrintable(name) << endl;
+ QFileInfo fi(rootDir.filePath(_fileName));
+ if (fi.isFile()) {
+ return rootDir.remove(_fileName);
+ } else {
+ return QDir(fi.filePath()).removeRecursively();
+ }
+ }
+
+ static Command *parse(QStringListIterator &it)
+ {
+ QString fileName = parseFileName(it);
+ if (fileName.isEmpty()) {
+ cerr << "Error: invalid filename for " << qPrintable(name) << " command" << endl;
+ return {};
+ }
+
+ return new RemoveCommand(fileName);
+ }
+};
+
+const QString RemoveCommand::name = QStringLiteral("remove");
+
+vector<Command *> parseArguments(QStringListIterator &it)
+{
+ vector<Command *> commands;
+
+ while (it.hasNext()) {
+ const QString option = it.next();
+
+ if (option == SetMtimeCommand::name) {
+ if (auto cmd = SetMtimeCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else if (option == SetContentsCommand::name) {
+ if (auto cmd = SetContentsCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else if (option == RenameCommand::name) {
+ if (auto cmd = RenameCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else if (option == AppendByteCommand::name) {
+ if (auto cmd = AppendByteCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else if (option == InsertCommand::name) {
+ if (auto cmd = InsertCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else if (option == RemoveCommand::name) {
+ if (auto cmd = RemoveCommand::parse(it)) {
+ commands.emplace_back(cmd);
+ } else {
+ return {};
+ }
+ } else {
+ cerr << "Error: unknown command '" << qPrintable(option) << "'" << endl;
+ }
+ }
+
+ return commands;
+}
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication app(argc, argv);
+ QStringListIterator it(app.arguments());
+ if (it.hasNext()) {
+ // skip program name
+ it.next();
+ }
+
+ QDir rootDir(it.next());
+
+ const auto commands = parseArguments(it);
+ if (commands.empty()) {
+ return -1;
+ }
+
+ cerr << "Starting executing commands...:" << endl;
+
+ for (const auto &cmd : commands) {
+ cerr << ".. Executing command: ";
+ if (!cmd->execute(rootDir)) {
+ return -2;
+ }
+ cerr << ".. command done." << endl;
+ }
+
+ cerr << "Successfully executed all commands." << endl;
+
+ qDeleteAll(commands);
+
+ return 0;
+}