diff options
author | Erik Verbruggen <erik@verbruggen.consulting> | 2022-01-13 15:41:26 +0300 |
---|---|---|
committer | Erik Verbruggen <erik@verbruggen.consulting> | 2022-01-27 15:40:27 +0300 |
commit | 946878ea79c2676d83625afc41cc90a5284dc946 (patch) | |
tree | 30085587f87f47643c4622c4b290de507a3c07a7 | |
parent | cb130ab1eb5c732d1f93de322296bdca1a8cb600 (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.cpp | 2 | ||||
-rw-r--r-- | test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/testsyncmove.cpp | 212 | ||||
-rw-r--r-- | test/testutils/CMakeLists.txt | 4 | ||||
-rw-r--r-- | test/testutils/syncenginetestutils.cpp | 198 | ||||
-rw-r--r-- | test/testutils/syncenginetestutils.h | 18 | ||||
-rw-r--r-- | test/testutils/test_helper.cpp | 420 |
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; +} |