Skip to content

Commit

Permalink
Retry failed download requests implementation (#3673)
Browse files Browse the repository at this point in the history
  • Loading branch information
VitorVieiraZ authored Jan 20, 2025
1 parent df0d199 commit f671c96
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 39 deletions.
142 changes: 141 additions & 1 deletion app/test/testmerginapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2687,7 +2687,7 @@ void TestMerginApi::deleteRemoteProjectNow( MerginApi *api, const QString &proje
QUrl url( api->mApiRoot + QStringLiteral( "/v2/projects/%1" ).arg( projectId ) );
request.setUrl( url );
qDebug() << "Trying to delete project " << projectName << ", id: " << projectId << " (" << url << ")";
QNetworkReply *r = api->mManager.deleteResource( request );
QNetworkReply *r = api->mManager->deleteResource( request );
QSignalSpy spy( r, &QNetworkReply::finished );
spy.wait( TestUtils::SHORT_REPLY );

Expand Down Expand Up @@ -2942,3 +2942,143 @@ void TestMerginApi::testParseVersion()
QCOMPARE( major, 2024 );
QCOMPARE( minor, 4 );
}

void TestMerginApi::testDownloadWithNetworkError()
{
// Store original manager
QNetworkAccessManager *originalManager = mApi->networkManager();

QString projectName = "testDownloadRetry";
createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" );

// Errors to test
QList<QNetworkReply::NetworkError> errorsToTest =
{
QNetworkReply::TimeoutError,
QNetworkReply::NetworkSessionFailedError
};

foreach ( QNetworkReply::NetworkError networkError, errorsToTest )
{
// Create mock manager - initially not failing
MockNetworkManager *failingManager = new MockNetworkManager( this );
mApi->setNetworkManager( failingManager );

// Create signal spies
QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted );
QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried );
QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished );

// Trigger the current network error when download starts
connect( mApi, &MerginApi::pullFilesStarted, this, [this, failingManager, networkError]()
{
failingManager->setShouldFail( true, networkError );
} );

mApi->pullProject( mWorkspaceName, projectName );

// Verify a transaction was created
QCOMPARE( mApi->transactions().count(), 1 );

// Wait for download to start and then fail
QVERIFY( startSpy.wait( TestUtils::LONG_REPLY ) );
QVERIFY( finishSpy.wait( TestUtils::LONG_REPLY ) );

// Verify signals were emitted
QVERIFY( startSpy.count() > 0 );
QVERIFY( retrySpy.count() > 0 );
QCOMPARE( finishSpy.count(), 1 );

// Verify that MAX_RETRY_COUNT retry attempts were made
int maxRetries = TransactionStatus::MAX_RETRY_COUNT;
QCOMPARE( retrySpy.count(), maxRetries );

// Verify sync failed
QList<QVariant> arguments = finishSpy.takeFirst();
QVERIFY( !arguments.at( 1 ).toBool() );

// Verify no local project was created
LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName );
QVERIFY( !localProject.isValid() );

// Disconnect all signals
disconnect( mApi, &MerginApi::pullFilesStarted, this, nullptr );

// Clean up
mApi->setNetworkManager( originalManager );
delete failingManager;
}
}

void TestMerginApi::testDownloadWithNetworkErrorRecovery()
{
// Store original manager
QNetworkAccessManager *originalManager = mApi->networkManager();

QString projectName = "testDownloadRetryRecovery";
createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" );

// Create mock manager - initially not failing
MockNetworkManager *failingManager = new MockNetworkManager( this );
mApi->setNetworkManager( failingManager );

// Create signal spies
QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted );
QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried );
QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished );

// Counter to track retry attempts
int retryCount = 0;
QNetworkReply::NetworkError networkError = QNetworkReply::TimeoutError;

// Reset network after two retries
connect( mApi, &MerginApi::downloadItemRetried, this, [&retryCount, failingManager, this]()
{
retryCount++;
if ( retryCount == 2 )
{
failingManager->setShouldFail( false );
disconnect( mApi, &MerginApi::pullFilesStarted, nullptr, nullptr );
disconnect( mApi, &MerginApi::downloadItemRetried, nullptr, nullptr );
}
} );

// Trigger network error when download starts
connect( mApi, &MerginApi::pullFilesStarted, this, [failingManager, networkError]()
{
failingManager->setShouldFail( true, networkError );
} );

mApi->pullProject( mWorkspaceName, projectName );

// Verify a transaction was created
QCOMPARE( mApi->transactions().count(), 1 );

// Wait for download to start, retry twice, and then complete successfully
QVERIFY( startSpy.wait( TestUtils::LONG_REPLY ) );
QVERIFY( finishSpy.wait( TestUtils::LONG_REPLY ) );

// Verify signals were emitted
QVERIFY( startSpy.count() > 0 );
QCOMPARE( retrySpy.count(), 2 ); // Should have exactly 2 retries
QCOMPARE( finishSpy.count(), 1 );

// Verify sync succeeded
QList<QVariant> arguments = finishSpy.takeFirst();
QVERIFY( arguments.at( 1 ).toBool() );

// Verify local project was created successfully
LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName );
QVERIFY( localProject.isValid() );

// Verify project files were downloaded correctly
QString projectDir = mApi->projectsPath() + "/" + projectName;
QStringList projectFiles = QDir( projectDir ).entryList( QDir::Files );
QVERIFY( projectFiles.count() > 0 );
QVERIFY( projectFiles.contains( "project.qgs" ) );

// Clean up
mApi->setNetworkManager( originalManager );
delete failingManager;
}

69 changes: 69 additions & 0 deletions app/test/testmerginapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,73 @@

#include <qgsapplication.h>

class MockReply : public QNetworkReply
{
public:
explicit MockReply( const QNetworkRequest &request, QNetworkAccessManager::Operation operation,
QObject *parent = nullptr, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError )
: QNetworkReply( parent )
{
setRequest( request );
setOperation( operation );
setUrl( request.url() );

if ( errorCode != QNetworkReply::NoError )
{
setError( errorCode, "Mock network failure" );
QMetaObject::invokeMethod( this, "errorOccurred", Qt::QueuedConnection, Q_ARG( QNetworkReply::NetworkError, errorCode ) );
}

QMetaObject::invokeMethod( this, "finished", Qt::QueuedConnection );
open( QIODevice::ReadOnly );
}

void abort() override {}

qint64 readData( char *data, qint64 maxlen ) override
{
Q_UNUSED( data );
Q_UNUSED( maxlen );
return -1;
}

qint64 bytesAvailable() const override
{
return 0;
}
};

class MockNetworkManager : public QNetworkAccessManager
{
public:
explicit MockNetworkManager( QObject *parent = nullptr )
: QNetworkAccessManager( parent )
, mShouldFail( false )
, mErrorCode( QNetworkReply::NoError )
{}

void setShouldFail( bool shouldFail, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError )
{
mShouldFail = shouldFail;
mErrorCode = errorCode;
}

protected:
QNetworkReply *createRequest( Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr ) override
{
if ( mShouldFail )
{
auto *reply = new MockReply( request, op, this, mErrorCode );
return reply;
}
return QNetworkAccessManager::createRequest( op, request, outgoingData );
}

private:
bool mShouldFail;
QNetworkReply::NetworkError mErrorCode;
};

class TestMerginApi: public QObject
{
Q_OBJECT
Expand All @@ -40,6 +107,8 @@ class TestMerginApi: public QObject
void testListProject();
void testListProjectsByName();
void testDownloadProject();
void testDownloadWithNetworkError();
void testDownloadWithNetworkErrorRecovery();
void testDownloadProjectSpecChars();
void testCancelDownloadProject();
void testCreateProjectTwice();
Expand Down
Loading

1 comment on commit f671c96

@inputapp-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS - version 25.1.699311 just submitted!

Please sign in to comment.