Test that the sync behave well if there are errors while reading the database
authorOlivier Goffart <ogoffart@woboq.com>
Thu, 26 Nov 2020 14:27:03 +0000 (15:27 +0100)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Tue, 15 Dec 2020 09:58:17 +0000 (10:58 +0100)
src/common/syncjournaldb.cpp
src/common/syncjournaldb.h
test/CMakeLists.txt
test/syncenginetestutils.h
test/testdatabaseerror.cpp [new file with mode: 0644]

index ed28a5e870afc408e0ca912dbc04e89a7f60a0ad..f904508595aafd45eb90f7eee53e9468722c0409 100644 (file)
@@ -279,6 +279,13 @@ bool SyncJournalDb::sqlFail(const QString &log, const SqlQuery &query)
 
 bool SyncJournalDb::checkConnect()
 {
+    if (autotestFailCounter >= 0) {
+        if (!autotestFailCounter--) {
+            qCInfo(lcDb) << "Error Simulated";
+            return false;
+        }
+    }
+
     if (_db.isOpen()) {
         // Unfortunately the sqlite isOpen check can return true even when the underlying storage
         // has become unavailable - and then some operations may cause crashes. See #6049
@@ -634,7 +641,7 @@ bool SyncJournalDb::updateMetadataTableStructure()
     bool re = true;
 
     // check if the file_id column is there and create it if not
-    if (!checkConnect()) {
+    if (columns.isEmpty()) {
         return false;
     }
 
@@ -751,7 +758,10 @@ bool SyncJournalDb::updateMetadataTableStructure()
         commitInternal("update database structure: add isE2eEncrypted col");
     }
 
-    if (!tableColumns("uploadinfo").contains("contentChecksum")) {
+    auto uploadInfoColumns = tableColumns("uploadinfo");
+    if (uploadInfoColumns.isEmpty())
+        return false;
+    if (!uploadInfoColumns.contains("contentChecksum")) {
         SqlQuery query(_db);
         query.prepare("ALTER TABLE uploadinfo ADD COLUMN contentChecksum TEXT;");
         if (!query.exec()) {
@@ -761,7 +771,10 @@ bool SyncJournalDb::updateMetadataTableStructure()
         commitInternal("update database structure: add contentChecksum col for uploadinfo");
     }
 
-    if (!tableColumns("conflicts").contains("basePath")) {
+    auto conflictsColumns = tableColumns("conflicts");
+    if (conflictsColumns.isEmpty())
+        return false;
+    if (!conflictsColumns.contains("basePath")) {
         SqlQuery query(_db);
         query.prepare("ALTER TABLE conflicts ADD COLUMN basePath TEXT;");
         if (!query.exec()) {
@@ -788,8 +801,7 @@ bool SyncJournalDb::updateErrorBlacklistTableStructure()
     auto columns = tableColumns("blacklist");
     bool re = true;
 
-    // check if the file_id column is there and create it if not
-    if (!checkConnect()) {
+    if (columns.isEmpty()) {
         return false;
     }
 
index 8898eefe2ae1f29d7eb7cbe830f2010818484e64..417f2b7e52479be117f883f859922447af00cd78 100644 (file)
@@ -245,6 +245,13 @@ public:
      */
     void markVirtualFileForDownloadRecursively(const QByteArray &path);
 
+    /**
+     * Only used for auto-test:
+     * when positive, will decrease the counter for every database operation.
+     * reaching 0 makes the operation fails
+     */
+    int autotestFailCounter = -1;
+
 private:
     int getFileRecordCount();
     bool updateDatabaseStructure();
index 8c719097e0efffeabd280b3778fb9ce673926c26..031333721c0f00d26c7f77f6594686bd14bf84fa 100644 (file)
@@ -58,6 +58,7 @@ nextcloud_add_test(LocalDiscovery "syncenginetestutils.h")
 nextcloud_add_test(RemoteDiscovery "syncenginetestutils.h")
 nextcloud_add_test(Permissions "syncenginetestutils.h")
 nextcloud_add_test(SelectiveSync "syncenginetestutils.h")
+nextcloud_add_test(DatabaseError "syncenginetestutils.h")
 nextcloud_add_test(LockedFiles "syncenginetestutils.h;../src/gui/lockwatcher.cpp")
 nextcloud_add_test(FolderWatcher "${FolderWatcher_SRC}")
 
index efa581d17b097e0c8687e0ccccce0ad8b23a0465..ecdcb48c433d4d40bf5ebbfd1fdbfb0195b5e8c1 100644 (file)
@@ -12,6 +12,7 @@
 #include "filesystem.h"
 #include "syncengine.h"
 #include "common/syncjournaldb.h"
+#include "csync_exclude.h"
 
 #include <QDir>
 #include <QNetworkReply>
@@ -920,6 +921,8 @@ public:
 
         _journalDb = std::make_unique<OCC::SyncJournalDb>(localPath() + "._sync_test.db");
         _syncEngine = std::make_unique<OCC::SyncEngine>(_account, localPath(), "", _journalDb.get());
+        // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
+        _syncEngine->excludedFiles().addManualExclude("]*.~*");
 
         // A new folder will update the local file state database on first sync.
         // To have a state matching what users will encounter, we have to a sync
diff --git a/test/testdatabaseerror.cpp b/test/testdatabaseerror.cpp
new file mode 100644 (file)
index 0000000..4c2ad5d
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ *    This software is in the public domain, furnished "as is", without technical
+ *    support, and with no warranty, express or implied, as to its usefulness for
+ *    any purpose.
+ *
+ */
+
+#include <QtTest>
+#include "syncenginetestutils.h"
+#include <syncengine.h>
+
+using namespace OCC;
+
+
+class TestDatabaseError : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void testDatabaseError() {
+        /* This test will make many iteration, at each iteration, the iᵗʰ database access will fail.
+         * The test ensure that if there is a failure, the next sync recovers. And if there was
+         * no error, then everything was sync'ed properly.
+         */
+
+        FileInfo finalState;
+        for (int count = 0; true; ++count) {
+            qInfo() << "Starting Iteration" << count;
+
+            FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
+
+            // Do a couple of changes
+            fakeFolder.remoteModifier().insert("A/a0");
+            fakeFolder.remoteModifier().appendByte("A/a1");
+            fakeFolder.remoteModifier().remove("A/a2");
+            fakeFolder.remoteModifier().rename("S/s1", "S/s1_renamed");
+            fakeFolder.remoteModifier().mkdir("D");
+            fakeFolder.remoteModifier().mkdir("D/subdir");
+            fakeFolder.remoteModifier().insert("D/subdir/file");
+            fakeFolder.localModifier().insert("B/b0");
+            fakeFolder.localModifier().appendByte("B/b1");
+            fakeFolder.remoteModifier().remove("B/b2");
+            fakeFolder.localModifier().mkdir("NewDir");
+            fakeFolder.localModifier().rename("C", "NewDir/C");
+
+            // Set the counter
+            fakeFolder.syncJournal().autotestFailCounter = count;
+
+            // run the sync
+            bool result = fakeFolder.syncOnce();
+
+            qInfo() << "Result of iteration" << count << "was" << result;
+
+            if (fakeFolder.syncJournal().autotestFailCounter >= 0) {
+                // No error was thrown, we are finished
+                QVERIFY(result);
+                QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+                QCOMPARE(fakeFolder.currentRemoteState(), finalState);
+                return;
+            }
+
+            if (!result) {
+                fakeFolder.syncJournal().autotestFailCounter = -1;
+                // Try again
+                QVERIFY(fakeFolder.syncOnce());
+            }
+
+            QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+            if (count == 0) {
+                finalState = fakeFolder.currentRemoteState();
+            } else {
+                // the final state should be the same for every iteration
+                QCOMPARE(fakeFolder.currentRemoteState(), finalState);
+            }
+        }
+    }
+};
+
+QTEST_GUILESS_MAIN(TestDatabaseError)
+#include "testdatabaseerror.moc"