Skip to content

Commit f671c96

Browse files
authored
Retry failed download requests implementation (#3673)
1 parent df0d199 commit f671c96

File tree

4 files changed

+312
-39
lines changed

4 files changed

+312
-39
lines changed

app/test/testmerginapi.cpp

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2687,7 +2687,7 @@ void TestMerginApi::deleteRemoteProjectNow( MerginApi *api, const QString &proje
26872687
QUrl url( api->mApiRoot + QStringLiteral( "/v2/projects/%1" ).arg( projectId ) );
26882688
request.setUrl( url );
26892689
qDebug() << "Trying to delete project " << projectName << ", id: " << projectId << " (" << url << ")";
2690-
QNetworkReply *r = api->mManager.deleteResource( request );
2690+
QNetworkReply *r = api->mManager->deleteResource( request );
26912691
QSignalSpy spy( r, &QNetworkReply::finished );
26922692
spy.wait( TestUtils::SHORT_REPLY );
26932693

@@ -2942,3 +2942,143 @@ void TestMerginApi::testParseVersion()
29422942
QCOMPARE( major, 2024 );
29432943
QCOMPARE( minor, 4 );
29442944
}
2945+
2946+
void TestMerginApi::testDownloadWithNetworkError()
2947+
{
2948+
// Store original manager
2949+
QNetworkAccessManager *originalManager = mApi->networkManager();
2950+
2951+
QString projectName = "testDownloadRetry";
2952+
createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" );
2953+
2954+
// Errors to test
2955+
QList<QNetworkReply::NetworkError> errorsToTest =
2956+
{
2957+
QNetworkReply::TimeoutError,
2958+
QNetworkReply::NetworkSessionFailedError
2959+
};
2960+
2961+
foreach ( QNetworkReply::NetworkError networkError, errorsToTest )
2962+
{
2963+
// Create mock manager - initially not failing
2964+
MockNetworkManager *failingManager = new MockNetworkManager( this );
2965+
mApi->setNetworkManager( failingManager );
2966+
2967+
// Create signal spies
2968+
QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted );
2969+
QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried );
2970+
QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished );
2971+
2972+
// Trigger the current network error when download starts
2973+
connect( mApi, &MerginApi::pullFilesStarted, this, [this, failingManager, networkError]()
2974+
{
2975+
failingManager->setShouldFail( true, networkError );
2976+
} );
2977+
2978+
mApi->pullProject( mWorkspaceName, projectName );
2979+
2980+
// Verify a transaction was created
2981+
QCOMPARE( mApi->transactions().count(), 1 );
2982+
2983+
// Wait for download to start and then fail
2984+
QVERIFY( startSpy.wait( TestUtils::LONG_REPLY ) );
2985+
QVERIFY( finishSpy.wait( TestUtils::LONG_REPLY ) );
2986+
2987+
// Verify signals were emitted
2988+
QVERIFY( startSpy.count() > 0 );
2989+
QVERIFY( retrySpy.count() > 0 );
2990+
QCOMPARE( finishSpy.count(), 1 );
2991+
2992+
// Verify that MAX_RETRY_COUNT retry attempts were made
2993+
int maxRetries = TransactionStatus::MAX_RETRY_COUNT;
2994+
QCOMPARE( retrySpy.count(), maxRetries );
2995+
2996+
// Verify sync failed
2997+
QList<QVariant> arguments = finishSpy.takeFirst();
2998+
QVERIFY( !arguments.at( 1 ).toBool() );
2999+
3000+
// Verify no local project was created
3001+
LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName );
3002+
QVERIFY( !localProject.isValid() );
3003+
3004+
// Disconnect all signals
3005+
disconnect( mApi, &MerginApi::pullFilesStarted, this, nullptr );
3006+
3007+
// Clean up
3008+
mApi->setNetworkManager( originalManager );
3009+
delete failingManager;
3010+
}
3011+
}
3012+
3013+
void TestMerginApi::testDownloadWithNetworkErrorRecovery()
3014+
{
3015+
// Store original manager
3016+
QNetworkAccessManager *originalManager = mApi->networkManager();
3017+
3018+
QString projectName = "testDownloadRetryRecovery";
3019+
createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" );
3020+
3021+
// Create mock manager - initially not failing
3022+
MockNetworkManager *failingManager = new MockNetworkManager( this );
3023+
mApi->setNetworkManager( failingManager );
3024+
3025+
// Create signal spies
3026+
QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted );
3027+
QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried );
3028+
QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished );
3029+
3030+
// Counter to track retry attempts
3031+
int retryCount = 0;
3032+
QNetworkReply::NetworkError networkError = QNetworkReply::TimeoutError;
3033+
3034+
// Reset network after two retries
3035+
connect( mApi, &MerginApi::downloadItemRetried, this, [&retryCount, failingManager, this]()
3036+
{
3037+
retryCount++;
3038+
if ( retryCount == 2 )
3039+
{
3040+
failingManager->setShouldFail( false );
3041+
disconnect( mApi, &MerginApi::pullFilesStarted, nullptr, nullptr );
3042+
disconnect( mApi, &MerginApi::downloadItemRetried, nullptr, nullptr );
3043+
}
3044+
} );
3045+
3046+
// Trigger network error when download starts
3047+
connect( mApi, &MerginApi::pullFilesStarted, this, [failingManager, networkError]()
3048+
{
3049+
failingManager->setShouldFail( true, networkError );
3050+
} );
3051+
3052+
mApi->pullProject( mWorkspaceName, projectName );
3053+
3054+
// Verify a transaction was created
3055+
QCOMPARE( mApi->transactions().count(), 1 );
3056+
3057+
// Wait for download to start, retry twice, and then complete successfully
3058+
QVERIFY( startSpy.wait( TestUtils::LONG_REPLY ) );
3059+
QVERIFY( finishSpy.wait( TestUtils::LONG_REPLY ) );
3060+
3061+
// Verify signals were emitted
3062+
QVERIFY( startSpy.count() > 0 );
3063+
QCOMPARE( retrySpy.count(), 2 ); // Should have exactly 2 retries
3064+
QCOMPARE( finishSpy.count(), 1 );
3065+
3066+
// Verify sync succeeded
3067+
QList<QVariant> arguments = finishSpy.takeFirst();
3068+
QVERIFY( arguments.at( 1 ).toBool() );
3069+
3070+
// Verify local project was created successfully
3071+
LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName );
3072+
QVERIFY( localProject.isValid() );
3073+
3074+
// Verify project files were downloaded correctly
3075+
QString projectDir = mApi->projectsPath() + "/" + projectName;
3076+
QStringList projectFiles = QDir( projectDir ).entryList( QDir::Files );
3077+
QVERIFY( projectFiles.count() > 0 );
3078+
QVERIFY( projectFiles.contains( "project.qgs" ) );
3079+
3080+
// Clean up
3081+
mApi->setNetworkManager( originalManager );
3082+
delete failingManager;
3083+
}
3084+

app/test/testmerginapi.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,73 @@
2121

2222
#include <qgsapplication.h>
2323

24+
class MockReply : public QNetworkReply
25+
{
26+
public:
27+
explicit MockReply( const QNetworkRequest &request, QNetworkAccessManager::Operation operation,
28+
QObject *parent = nullptr, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError )
29+
: QNetworkReply( parent )
30+
{
31+
setRequest( request );
32+
setOperation( operation );
33+
setUrl( request.url() );
34+
35+
if ( errorCode != QNetworkReply::NoError )
36+
{
37+
setError( errorCode, "Mock network failure" );
38+
QMetaObject::invokeMethod( this, "errorOccurred", Qt::QueuedConnection, Q_ARG( QNetworkReply::NetworkError, errorCode ) );
39+
}
40+
41+
QMetaObject::invokeMethod( this, "finished", Qt::QueuedConnection );
42+
open( QIODevice::ReadOnly );
43+
}
44+
45+
void abort() override {}
46+
47+
qint64 readData( char *data, qint64 maxlen ) override
48+
{
49+
Q_UNUSED( data );
50+
Q_UNUSED( maxlen );
51+
return -1;
52+
}
53+
54+
qint64 bytesAvailable() const override
55+
{
56+
return 0;
57+
}
58+
};
59+
60+
class MockNetworkManager : public QNetworkAccessManager
61+
{
62+
public:
63+
explicit MockNetworkManager( QObject *parent = nullptr )
64+
: QNetworkAccessManager( parent )
65+
, mShouldFail( false )
66+
, mErrorCode( QNetworkReply::NoError )
67+
{}
68+
69+
void setShouldFail( bool shouldFail, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError )
70+
{
71+
mShouldFail = shouldFail;
72+
mErrorCode = errorCode;
73+
}
74+
75+
protected:
76+
QNetworkReply *createRequest( Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr ) override
77+
{
78+
if ( mShouldFail )
79+
{
80+
auto *reply = new MockReply( request, op, this, mErrorCode );
81+
return reply;
82+
}
83+
return QNetworkAccessManager::createRequest( op, request, outgoingData );
84+
}
85+
86+
private:
87+
bool mShouldFail;
88+
QNetworkReply::NetworkError mErrorCode;
89+
};
90+
2491
class TestMerginApi: public QObject
2592
{
2693
Q_OBJECT
@@ -40,6 +107,8 @@ class TestMerginApi: public QObject
40107
void testListProject();
41108
void testListProjectsByName();
42109
void testDownloadProject();
110+
void testDownloadWithNetworkError();
111+
void testDownloadWithNetworkErrorRecovery();
43112
void testDownloadProjectSpecChars();
44113
void testCancelDownloadProject();
45114
void testCreateProjectTwice();

0 commit comments

Comments
 (0)