|
| 1 | +/* |
| 2 | + * This software is in the public domain, furnished "as is", without technical |
| 3 | + * support, and with no warranty, express or implied, as to its usefulness for |
| 4 | + * any purpose. |
| 5 | + * |
| 6 | + */ |
| 7 | +#include <syncengine.h> |
| 8 | + |
| 9 | +#include "testutils/syncenginetestutils.h" |
| 10 | +#include "testutils/testutils.h" |
| 11 | + |
| 12 | +#include <QtTest> |
| 13 | + |
| 14 | +using namespace OCC; |
| 15 | + |
| 16 | +// Integration coverage for the self-heal hardening: a *transient* connectivity error on a |
| 17 | +// single file must not abort the whole sync run. Pre-fix, classifyError mapped the network |
| 18 | +// error to FatalError, which returns up to propagator()->abort() and stops the entire run, |
| 19 | +// so every file queued after the failing one was silently never uploaded. Now it is a |
| 20 | +// per-file NormalError + another-pass: the healthy files still sync (self-heal), the failing |
| 21 | +// one is retried/blacklisted. |
| 22 | +class TestSelfHeal : public QObject |
| 23 | +{ |
| 24 | + Q_OBJECT |
| 25 | + |
| 26 | +private Q_SLOTS: |
| 27 | + void testTransientUploadErrorDoesNotAbortRun() |
| 28 | + { |
| 29 | + FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12()); |
| 30 | + |
| 31 | + // Serial uploads so the failing file (a unique size) is processed before the |
| 32 | + // healthy ones -> a whole-run abort (the pre-fix behaviour) would leave the |
| 33 | + // healthy files un-synced, which is exactly what this test detects. |
| 34 | + auto opts = fakeFolder.syncEngine().syncOptions(); |
| 35 | + opts._parallelNetworkJobs = [] { return 0; }; |
| 36 | + fakeFolder.syncEngine().setSyncOptions(opts); |
| 37 | + |
| 38 | + const int failSize = 137; |
| 39 | + int nFail = 0; |
| 40 | + QObject parent; |
| 41 | + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { |
| 42 | + const QString path = request.url().path(); |
| 43 | + if (op == QNetworkAccessManager::PutOperation && path.contains(QLatin1String("a_fatal"))) { |
| 44 | + ++nFail; |
| 45 | + // A transient network-level error (not an HTTP code) on this one file. |
| 46 | + auto *reply = new FakeErrorReply(op, request, &parent, 0); |
| 47 | + reply->setError(QNetworkReply::TimeoutError, QStringLiteral("fake transient timeout")); |
| 48 | + return reply; |
| 49 | + } |
| 50 | + return nullptr; // everything else: normal server behaviour |
| 51 | + }); |
| 52 | + |
| 53 | + // "Z/" so these come after the template dirs; within Z, "a_fatal" sorts first. |
| 54 | + // The local dir must exist before inserting files into it. |
| 55 | + fakeFolder.localModifier().mkdir(QStringLiteral("Z")); |
| 56 | + fakeFolder.localModifier().insert(QStringLiteral("Z/a_fatal"), static_cast<quint64>(failSize)); |
| 57 | + fakeFolder.localModifier().insert(QStringLiteral("Z/b_ok"), quint64(100)); |
| 58 | + fakeFolder.localModifier().insert(QStringLiteral("Z/c_ok"), quint64(100)); |
| 59 | + |
| 60 | + // The failing file is retried per-file and eventually blacklisted; the healthy |
| 61 | + // files converge. A couple of passes to let any retry settle. |
| 62 | + // The overall result is false (a_fatal errors), which is fine — the discriminator |
| 63 | + // is whether the *healthy* files still made it to the server. |
| 64 | + [[maybe_unused]] const bool pass1 = fakeFolder.applyLocalModificationsAndSync(); |
| 65 | + [[maybe_unused]] const bool pass2 = fakeFolder.applyLocalModificationsAndSync(); |
| 66 | + |
| 67 | + QVERIFY2(nFail > 0, "the transient-error injection never fired"); |
| 68 | + // Self-heal: the healthy files synced despite the transient failure on a_fatal. |
| 69 | + QVERIFY2(fakeFolder.currentRemoteState().find(QStringLiteral("Z/b_ok")) != nullptr, |
| 70 | + "Z/b_ok was not uploaded -> a transient error on another file aborted the whole run"); |
| 71 | + QVERIFY2(fakeFolder.currentRemoteState().find(QStringLiteral("Z/c_ok")) != nullptr, |
| 72 | + "Z/c_ok was not uploaded -> a transient error on another file aborted the whole run"); |
| 73 | + } |
| 74 | +}; |
| 75 | + |
| 76 | +QTEST_GUILESS_MAIN(TestSelfHeal) |
| 77 | +#include "testselfheal.moc" |
0 commit comments