Skip to content

Commit a676bde

Browse files
committedNov 10, 2016
[bugfix][backport] File downloader for identify dialog hyperlinks
fixes #14703 Include C++ and Python tests ( cherry-picked from commit bdc2e24 ) Try to convince Travis to behave like a normal mechanical being Travis won: ported all test cases to Python and disabled C++ companion test (still useful locally and for debugging) For the curious: QTemporaryFile is not working as expected ( cherry-picked from 57aa7fd )
1 parent afb4739 commit a676bde

File tree

12 files changed

+845
-2
lines changed

12 files changed

+845
-2
lines changed
 

‎ci/travis/linux/qt4/script.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then
2424
chmod -R ugo-w ~/.ccache
2525
fi
2626

27-
xvfb-run ctest -V -E 'qgis_openstreetmaptest|qgis_wcsprovidertest|qgis_ziplayertest' -S ./qgis-test-travis.ctest --output-on-failure
27+
xvfb-run ctest -V -E 'qgis_filedownloader|qgis_openstreetmaptest|qgis_wcsprovidertest|qgis_ziplayertest' -S ./qgis-test-travis.ctest --output-on-failure

‎python/gui/gui.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
%Include qgsfieldvalidator.sip
7575
%Include qgsfiledropedit.sip
7676
%Include qgsfilewidget.sip
77+
%Include qgsfiledownloader.sip
7778
%Include qgsfilterlineedit.sip
7879
%Include qgsformannotationitem.sip
7980
%Include qgsgenericprojectionselector.sip

‎python/gui/qgsfiledownloader.sip

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/***************************************************************************
2+
qgsfiledownloader.sip
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : elpaso at itopen dot it
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
/** \ingroup gui
17+
* QgsFileDownloader is a utility class for downloading files.
18+
*
19+
* To use this class, it is necessary to pass the URL and an output file name as
20+
* arguments to the constructor, the download will start immediately.
21+
* The download is asynchronous and depending on the guiNotificationsEnabled
22+
* parameter accepted by the constructor (default = true) the class will
23+
* show a progress dialog and report all errors in a QMessageBox::warning dialog.
24+
* If the guiNotificationsEnabled parameter is set to false, the class can still
25+
* be used through the signals and slots mechanism.
26+
* The object will destroy itself when the request completes, errors or is canceled.
27+
*
28+
* @note added in QGIS 2.18.1
29+
*/
30+
class QgsFileDownloader : public QObject
31+
{
32+
%TypeHeaderCode
33+
#include <qgsfiledownloader.h>
34+
%End
35+
public:
36+
/**
37+
* QgsFileDownloader
38+
* @param url the download url
39+
* @param outputFileName file name where the downloaded content will be stored
40+
* @param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
41+
*/
42+
QgsFileDownloader(QUrl url, QString outputFileName, bool guiNotificationsEnabled = true);
43+
44+
signals:
45+
/** Emitted when the download has completed successfully */
46+
void downloadCompleted();
47+
/** Emitted always when the downloader exits */
48+
void downloadExited();
49+
/** Emitted when the download was canceled by the user */
50+
void downloadCanceled();
51+
/** Emitted when an error makes the download fail */
52+
void downloadError( QStringList errorMessages );
53+
/** Emitted when data ready to be processed */
54+
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
55+
56+
public slots:
57+
/**
58+
* Called when a download is canceled by the user
59+
* this slot aborts the download and deletes the object
60+
*/
61+
void onDownloadCanceled();
62+
63+
private:
64+
~QgsFileDownloader();
65+
66+
};

‎src/app/qgsidentifyresultsdialog.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "qgsvectorlayer.h"
3636
#include "qgswebview.h"
3737
#include "qgswebframe.h"
38+
#include "qgsfiledownloader.h"
3839

3940
#include <QCloseEvent>
4041
#include <QLabel>
@@ -52,6 +53,11 @@
5253
#include <QDesktopServices>
5354
#include <QMessageBox>
5455
#include <QComboBox>
56+
#include <QNetworkRequest>
57+
#include <QNetworkReply>
58+
#include <QFileDialog>
59+
#include <QFileInfo>
60+
#include <QRegExp>
5561

5662
//graph
5763
#include <qwt_plot.h>
@@ -66,13 +72,59 @@ QgsIdentifyResultsWebView::QgsIdentifyResultsWebView( QWidget *parent ) : QgsWeb
6672
setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Minimum );
6773
page()->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
6874
// page()->setLinkDelegationPolicy( QWebPage::DelegateAllLinks );
75+
page()->setForwardUnsupportedContent( true );
6976
page()->setLinkDelegationPolicy( QWebPage::DontDelegateLinks );
7077
settings()->setAttribute( QWebSettings::LocalContentCanAccessRemoteUrls, true );
7178
settings()->setAttribute( QWebSettings::JavascriptCanOpenWindows, true );
7279
settings()->setAttribute( QWebSettings::PluginsEnabled, true );
7380
#ifdef QGISDEBUG
7481
settings()->setAttribute( QWebSettings::DeveloperExtrasEnabled, true );
7582
#endif
83+
connect( page(), SIGNAL( downloadRequested( QNetworkRequest ) ), this, SLOT( downloadRequested( QNetworkRequest ) ) );
84+
connect( page(), SIGNAL( unsupportedContent( QNetworkReply* ) ), this, SLOT( unsupportedContent( QNetworkReply* ) ) );
85+
}
86+
87+
88+
void QgsIdentifyResultsWebView::downloadRequested( const QNetworkRequest &request )
89+
{
90+
handleDownload( request.url() );
91+
}
92+
93+
void QgsIdentifyResultsWebView::unsupportedContent( QNetworkReply * reply )
94+
{
95+
handleDownload( reply->url() );
96+
}
97+
98+
void QgsIdentifyResultsWebView::handleDownload( QUrl url )
99+
{
100+
if ( ! url.isValid() )
101+
{
102+
QMessageBox::warning( this, tr( "Invalid URL" ), tr( "The download URL is not valid: %1" ).arg( url.toString( ) ) );
103+
}
104+
else
105+
{
106+
const QString DOWNLOADER_LAST_DIR_KEY( "Qgis/fileDownloaderLastDir" );
107+
QSettings settings;
108+
// Try to get some information from the URL
109+
QFileInfo info( url.toString( ) );
110+
QString savePath = settings.value( DOWNLOADER_LAST_DIR_KEY ).toString( );
111+
QString fileName = info.fileName().replace( QRegExp( "[^A-z0-9\\-_\\.]" ), "_" );
112+
if ( ! savePath.isEmpty() && ! fileName.isEmpty( ) )
113+
{
114+
savePath = QDir::cleanPath( savePath + QDir::separator() + fileName );
115+
}
116+
QString targetFile = QFileDialog::getSaveFileName( this,
117+
tr( "Save as" ),
118+
savePath,
119+
info.suffix( ).isEmpty() ? QString( ) : "*." + info.suffix( )
120+
);
121+
if ( ! targetFile.isEmpty() )
122+
{
123+
settings.setValue( DOWNLOADER_LAST_DIR_KEY, QFileInfo( targetFile ).dir().absolutePath( ) );
124+
// Start the download
125+
new QgsFileDownloader( url, targetFile );
126+
}
127+
}
76128
}
77129

78130
void QgsIdentifyResultsWebView::print( void )

‎src/app/qgsidentifyresultsdialog.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
#include <QWidget>
3333
#include <QList>
34+
#include <QNetworkRequest>
35+
#include <QNetworkReply>
36+
#include <QUrl>
3437

3538
class QCloseEvent;
3639
class QTreeWidgetItem;
@@ -57,9 +60,13 @@ class APP_EXPORT QgsIdentifyResultsWebView : public QgsWebView
5760
QSize sizeHint() const override;
5861
public slots:
5962
void print( void );
63+
void downloadRequested( const QNetworkRequest &request );
64+
void unsupportedContent( QNetworkReply *reply );
6065
protected:
6166
void contextMenuEvent( QContextMenuEvent* ) override;
6267
QgsWebView *createWindow( QWebPage::WebWindowType type ) override;
68+
private:
69+
void handleDownload( QUrl url );
6370
};
6471

6572
class APP_EXPORT QgsIdentifyResultsFeatureItem: public QTreeWidgetItem

‎src/gui/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ SET(QGIS_GUI_SRCS
272272
qgsuserinputdockwidget.cpp
273273
qgsvariableeditorwidget.cpp
274274
qgsvertexmarker.cpp
275+
qgsfiledownloader.cpp
275276
)
276277

277278
IF (WITH_QTWEBKIT)
@@ -403,6 +404,7 @@ SET(QGIS_GUI_MOC_HDRS
403404
qgsunitselectionwidget.h
404405
qgsuserinputdockwidget.h
405406
qgsvariableeditorwidget.h
407+
qgsfiledownloader.h
406408

407409
raster/qgsmultibandcolorrendererwidget.h
408410
raster/qgspalettedrendererwidget.h
@@ -579,6 +581,7 @@ SET(QGIS_GUI_HDRS
579581
qgsuserinputdockwidget.h
580582
qgsvectorlayertools.h
581583
qgsvertexmarker.h
584+
qgsfiledownloader.h
582585

583586
attributetable/qgsfeaturemodel.h
584587

‎src/gui/qgsfiledownloader.cpp

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/***************************************************************************
2+
qgsfiledownloader.cpp
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgsfiledownloader.h"
17+
#include "qgsnetworkaccessmanager.h"
18+
19+
#include <QNetworkAccessManager>
20+
#include <QNetworkRequest>
21+
#include <QNetworkReply>
22+
#include <QMessageBox>
23+
#ifndef QT_NO_OPENSSL
24+
#include <QSslError>
25+
#endif
26+
27+
QgsFileDownloader::QgsFileDownloader( QUrl url, QString outputFileName, bool enableGuiNotifications )
28+
: mUrl( url )
29+
, mReply( nullptr )
30+
, mProgressDialog( nullptr )
31+
, mDownloadCanceled( false )
32+
, mErrors()
33+
, mGuiNotificationsEnabled( enableGuiNotifications )
34+
{
35+
mFile.setFileName( outputFileName );
36+
startDownload();
37+
}
38+
39+
40+
QgsFileDownloader::~QgsFileDownloader()
41+
{
42+
if ( mReply )
43+
{
44+
mReply->abort();
45+
mReply->deleteLater();
46+
}
47+
if ( mProgressDialog )
48+
{
49+
mProgressDialog->deleteLater();
50+
}
51+
}
52+
53+
54+
void QgsFileDownloader::startDownload()
55+
{
56+
QgsNetworkAccessManager* nam = QgsNetworkAccessManager::instance();
57+
58+
QNetworkRequest request( mUrl );
59+
60+
mReply = nam->get( request );
61+
62+
connect( mReply, SIGNAL( readyRead() ), this, SLOT( onReadyRead() ) );
63+
connect( mReply, SIGNAL( error( QNetworkReply::NetworkError ) ), this, SLOT( onNetworkError( QNetworkReply::NetworkError ) ) );
64+
connect( mReply, SIGNAL( finished() ), this, SLOT( onFinished() ) );
65+
connect( mReply, SIGNAL( downloadProgress( qint64, qint64 ) ), this, SLOT( onDownloadProgress( qint64, qint64 ) ) );
66+
connect( nam, SIGNAL( requestTimedOut( QNetworkReply* ) ), this, SLOT( onRequestTimedOut() ) );
67+
#ifndef QT_NO_OPENSSL
68+
connect( nam, SIGNAL( sslErrors( QNetworkReply*, QList<QSslError> ) ), this, SLOT( onSslErrors( QNetworkReply*, QList<QSslError> ) ) );
69+
#endif
70+
if ( mGuiNotificationsEnabled )
71+
{
72+
mProgressDialog = new QProgressDialog();
73+
mProgressDialog->setWindowTitle( tr( "Download" ) );
74+
mProgressDialog->setLabelText( tr( "Downloading %1." ).arg( mFile.fileName() ) );
75+
mProgressDialog->show();
76+
connect( mProgressDialog, SIGNAL( canceled() ), this, SLOT( onDownloadCanceled() ) );
77+
}
78+
}
79+
80+
void QgsFileDownloader::onDownloadCanceled()
81+
{
82+
mDownloadCanceled = true;
83+
emit downloadCanceled();
84+
onFinished();
85+
}
86+
87+
void QgsFileDownloader::onRequestTimedOut()
88+
{
89+
error( tr( "Network request %1 timed out" ).arg( mUrl.toString() ) );
90+
}
91+
92+
#ifndef QT_NO_OPENSSL
93+
void QgsFileDownloader::onSslErrors( QNetworkReply *reply, const QList<QSslError> &errors )
94+
{
95+
Q_UNUSED( reply );
96+
QStringList errorMessages;
97+
errorMessages << "SSL Errors: ";
98+
for ( auto end = errors.size(), i = 0; i != end; ++i )
99+
{
100+
errorMessages << errors[i].errorString();
101+
}
102+
error( errorMessages );
103+
}
104+
#endif
105+
106+
107+
void QgsFileDownloader::error( QStringList errorMessages )
108+
{
109+
for ( auto end = errorMessages.size(), i = 0; i != end; ++i )
110+
{
111+
mErrors.append( errorMessages[i] );
112+
}
113+
// Show error
114+
if ( mGuiNotificationsEnabled )
115+
{
116+
QMessageBox::warning( nullptr, tr( "Download failed" ), mErrors.join( "<br>" ) );
117+
}
118+
emit downloadError( mErrors );
119+
}
120+
121+
void QgsFileDownloader::error( QString errorMessage )
122+
{
123+
error( QStringList() << errorMessage );
124+
}
125+
126+
void QgsFileDownloader::onReadyRead()
127+
{
128+
Q_ASSERT( mReply );
129+
if ( ! mFile.isOpen() && ! mFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
130+
{
131+
error( tr( "Cannot open output file: %1" ).arg( mFile.fileName() ) );
132+
onFinished();
133+
}
134+
else
135+
{
136+
QByteArray data = mReply->readAll();
137+
mFile.write( data );
138+
}
139+
}
140+
141+
void QgsFileDownloader::onFinished()
142+
{
143+
// when canceled
144+
if ( ! mErrors.isEmpty() || mDownloadCanceled )
145+
{
146+
mFile.close();
147+
mFile.remove();
148+
if ( mGuiNotificationsEnabled )
149+
mProgressDialog->hide();
150+
}
151+
else
152+
{
153+
// download finished normally
154+
if ( mGuiNotificationsEnabled )
155+
mProgressDialog->hide();
156+
mFile.flush();
157+
mFile.close();
158+
159+
// get redirection url
160+
QVariant redirectionTarget = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
161+
if ( mReply->error() )
162+
{
163+
mFile.remove();
164+
error( tr( "Download failed: %1." ).arg( mReply->errorString() ) );
165+
}
166+
else if ( !redirectionTarget.isNull() )
167+
{
168+
QUrl newUrl = mUrl.resolved( redirectionTarget.toUrl() );
169+
mUrl = newUrl;
170+
mReply->deleteLater();
171+
mFile.open( QIODevice::WriteOnly );
172+
mFile.resize( 0 );
173+
mFile.close();
174+
startDownload();
175+
return;
176+
}
177+
// All done
178+
emit downloadCompleted();
179+
}
180+
emit downloadExited();
181+
this->deleteLater();
182+
}
183+
184+
void QgsFileDownloader::onNetworkError( QNetworkReply::NetworkError err )
185+
{
186+
Q_ASSERT( mReply );
187+
error( QString( "Network error %1: %2" ).arg( err ).arg( mReply->errorString() ) );
188+
}
189+
190+
void QgsFileDownloader::onDownloadProgress( qint64 bytesReceived, qint64 bytesTotal )
191+
{
192+
if ( mDownloadCanceled )
193+
{
194+
return;
195+
}
196+
if ( mGuiNotificationsEnabled )
197+
{
198+
mProgressDialog->setMaximum( bytesTotal );
199+
mProgressDialog->setValue( bytesReceived );
200+
}
201+
emit downloadProgress( bytesReceived, bytesTotal );
202+
}
203+

‎src/gui/qgsfiledownloader.h

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/***************************************************************************
2+
qgsfiledownloader.h
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSFILEDOWNLOADER_H
17+
#define QGSFILEDOWNLOADER_H
18+
19+
#include <QObject>
20+
#include <QFile>
21+
#include <QNetworkReply>
22+
#include <QProgressDialog>
23+
#ifndef QT_NO_OPENSSL
24+
#include <QSslError>
25+
#endif
26+
27+
/** \ingroup gui
28+
* QgsFileDownloader is a utility class for downloading files.
29+
*
30+
* To use this class, it is necessary to pass the URL and an output file name as
31+
* arguments to the constructor, the download will start immediately.
32+
* The download is asynchronous and depending on the guiNotificationsEnabled
33+
* parameter accepted by the constructor (default = true) the class will
34+
* show a progress dialog and report all errors in a QMessageBox::warning dialog.
35+
* If the guiNotificationsEnabled parameter is set to false, the class can still
36+
* be used through the signals and slots mechanism.
37+
* The object will destroy itself when the request completes, errors or is canceled.
38+
*
39+
* @note added in QGIS 2.18.1
40+
*/
41+
class GUI_EXPORT QgsFileDownloader : public QObject
42+
{
43+
Q_OBJECT
44+
public:
45+
/**
46+
* QgsFileDownloader
47+
* @param url the download url
48+
* @param outputFileName file name where the downloaded content will be stored
49+
* @param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
50+
*/
51+
QgsFileDownloader( QUrl url, QString outputFileName, bool guiNotificationsEnabled = true );
52+
53+
signals:
54+
/** Emitted when the download has completed successfully */
55+
void downloadCompleted();
56+
/** Emitted always when the downloader exits */
57+
void downloadExited();
58+
/** Emitted when the download was canceled by the user */
59+
void downloadCanceled();
60+
/** Emitted when an error makes the download fail */
61+
void downloadError( QStringList errorMessages );
62+
/** Emitted when data ready to be processed */
63+
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
64+
65+
public slots:
66+
/**
67+
* Called when a download is canceled by the user
68+
* this slot aborts the download and deletes
69+
* the object
70+
*/
71+
void onDownloadCanceled();
72+
73+
private slots:
74+
/** Called when the network reply data are ready */
75+
void onReadyRead();
76+
/** Called when the network reply has finished */
77+
void onFinished();
78+
/** Called on Network Error */
79+
void onNetworkError( QNetworkReply::NetworkError err );
80+
/** Called on data ready to be processed */
81+
void onDownloadProgress( qint64 bytesReceived, qint64 bytesTotal );
82+
/** Called when a network request times out */
83+
void onRequestTimedOut();
84+
/** Called to start the download */
85+
void startDownload();
86+
#ifndef QT_NO_OPENSSL
87+
/**
88+
* Called on SSL network Errors
89+
* @param reply
90+
* @param errors
91+
*/
92+
void onSslErrors( QNetworkReply *reply, const QList<QSslError> &errors );
93+
#endif
94+
95+
private:
96+
~QgsFileDownloader();
97+
/**
98+
* Abort current request and show an error if the instance has GUI
99+
* notifications enabled.
100+
*/
101+
void error( QStringList errorMessages );
102+
void error( QString errorMessage );
103+
QUrl mUrl;
104+
QNetworkReply* mReply;
105+
QFile mFile;
106+
QProgressDialog* mProgressDialog;
107+
bool mDownloadCanceled;
108+
QStringList mErrors;
109+
bool mGuiNotificationsEnabled;
110+
};
111+
112+
#endif // QGSFILEDOWNLOADER_H

‎tests/src/gui/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ ADD_QGIS_TEST(qgsguitest testqgsgui.cpp)
134134
ADD_QGIS_TEST(rubberbandtest testqgsrubberband.cpp)
135135
ADD_QGIS_TEST(scalecombobox testqgsscalecombobox.cpp)
136136
ADD_QGIS_TEST(spinbox testqgsspinbox.cpp)
137-
137+
ADD_QGIS_TEST(filedownloader testqgsfiledownloader.cpp)
138138

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/***************************************************************************
2+
testqgsfilefiledownloader.cpp
3+
--------------------------------------
4+
Date : 11.8.2016
5+
Copyright : (C) 2016 Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
17+
#include <QtTest/QtTest>
18+
#include <QObject>
19+
#include <QTemporaryFile>
20+
#include <QUrl>
21+
#include <QEventLoop>
22+
#include <QTimer>
23+
24+
#include <qgsapplication.h>
25+
#include <qgsfiledownloader.h>
26+
27+
class TestQgsFileDownloader: public QObject
28+
{
29+
Q_OBJECT
30+
public:
31+
TestQgsFileDownloader()
32+
: mTempFile( nullptr )
33+
, mErrorMessage()
34+
, mCanceled( false )
35+
, mProgress( false )
36+
, mError( false )
37+
, mCompleted( false )
38+
, mExited( false )
39+
, mFileDownloader( nullptr )
40+
{}
41+
42+
public slots:
43+
/** Called when the download has completed successfully */
44+
void downloadCompleted()
45+
{
46+
mCompleted = true;
47+
}
48+
/** Called when the download exits */
49+
void downloadExited()
50+
{
51+
mExited = true;
52+
}
53+
/** Called when the download was canceled by the user */
54+
void downloadCanceled()
55+
{
56+
mCanceled = true;
57+
}
58+
/** Called when an error makes the download fail */
59+
void downloadError( QStringList errorMessages )
60+
{
61+
mError = true;
62+
errorMessages.sort();
63+
mErrorMessage = errorMessages.join( ";" );
64+
}
65+
/** Called when data ready to be processed */
66+
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
67+
{
68+
Q_UNUSED( bytesReceived );
69+
Q_UNUSED( bytesTotal );
70+
mProgress = true;
71+
}
72+
73+
private slots:
74+
void initTestCase(); // will be called before the first testfunction is executed.
75+
void cleanupTestCase(); // will be called after the last testfunction was executed.
76+
void init(); // will be called before each testfunction is executed.
77+
void cleanup(); // will be called after every testfunction.
78+
79+
void testValidDownload();
80+
void testInValidDownload();
81+
void testCanceledDownload();
82+
void testInvalidFile();
83+
void testInvalidUrl();
84+
void testBlankUrl();
85+
#ifndef QT_NO_OPENSSL
86+
void testSslError_data();
87+
void testSslError();
88+
#endif
89+
90+
private:
91+
void makeCall( QUrl url , QString fileName, bool cancel = false );
92+
QTemporaryFile *mTempFile;
93+
QString mErrorMessage;
94+
bool mCanceled;
95+
bool mProgress;
96+
bool mError;
97+
bool mCompleted;
98+
bool mExited;
99+
QgsFileDownloader *mFileDownloader;
100+
};
101+
102+
void TestQgsFileDownloader::makeCall( QUrl url, QString fileName, bool cancel )
103+
{
104+
QEventLoop loop;
105+
106+
mFileDownloader = new QgsFileDownloader( url, fileName, false );
107+
connect( mFileDownloader, SIGNAL( downloadCompleted() ), this, SLOT( downloadCompleted() ) );
108+
connect( mFileDownloader, SIGNAL( downloadCanceled() ), this, SLOT( downloadCanceled() ) );
109+
connect( mFileDownloader, SIGNAL( downloadExited() ), this, SLOT( downloadExited() ) );
110+
connect( mFileDownloader, SIGNAL( downloadError( QStringList ) ), this, SLOT( downloadError( QStringList ) ) );
111+
connect( mFileDownloader, SIGNAL( downloadProgress( qint64, qint64 ) ), this, SLOT( downloadProgress( qint64, qint64 ) ) );
112+
113+
connect( mFileDownloader, SIGNAL( downloadExited() ), &loop, SLOT( quit() ) );
114+
115+
if ( cancel )
116+
QTimer::singleShot( 1000, mFileDownloader, SLOT( onDownloadCanceled() ) );
117+
118+
loop.exec();
119+
120+
}
121+
122+
void TestQgsFileDownloader::initTestCase()
123+
{
124+
QgsApplication::init();
125+
QgsApplication::initQgis();
126+
127+
}
128+
129+
void TestQgsFileDownloader::cleanupTestCase()
130+
{
131+
QgsApplication::exitQgis();
132+
}
133+
134+
void TestQgsFileDownloader::init()
135+
{
136+
mErrorMessage.clear();
137+
mCanceled = false;
138+
mProgress = false;
139+
mError = false;
140+
mCompleted = false;
141+
mExited = false;
142+
mTempFile = new QTemporaryFile( );
143+
Q_ASSERT( mTempFile->open() );
144+
mTempFile->close();
145+
}
146+
147+
148+
149+
void TestQgsFileDownloader::cleanup()
150+
{
151+
delete mTempFile;
152+
}
153+
154+
void TestQgsFileDownloader::testValidDownload()
155+
{
156+
QVERIFY( ! mTempFile->fileName().isEmpty() );
157+
makeCall( QUrl( "http://www.qgis.org" ), mTempFile->fileName() );
158+
QVERIFY( mExited );
159+
QVERIFY( mCompleted );
160+
QVERIFY( mProgress );
161+
QVERIFY( !mError );
162+
QVERIFY( !mCanceled );
163+
QVERIFY( mTempFile->size() > 0 );
164+
}
165+
166+
void TestQgsFileDownloader::testInValidDownload()
167+
{
168+
QVERIFY( ! mTempFile->fileName().isEmpty() );
169+
makeCall( QUrl( "http://www.doesnotexistofthatimsure.qgis" ), mTempFile->fileName() );
170+
QVERIFY( mExited );
171+
QVERIFY( !mCompleted );
172+
QVERIFY( mError );
173+
QVERIFY( !mCanceled );
174+
QVERIFY( mTempFile->size() == 0 );
175+
QCOMPARE( mErrorMessage, QString( "Network error 3: Host www.doesnotexistofthatimsure.qgis not found" ) );
176+
}
177+
178+
void TestQgsFileDownloader::testCanceledDownload()
179+
{
180+
QVERIFY( ! mTempFile->fileName().isEmpty() );
181+
makeCall( QUrl( "https://github.com/qgis/QGIS/archive/master.zip" ), mTempFile->fileName(), true );
182+
QVERIFY( mExited );
183+
QVERIFY( !mCompleted );
184+
QVERIFY( !mError );
185+
QVERIFY( mProgress );
186+
QVERIFY( mCanceled );
187+
QVERIFY( mTempFile->size() == 0 );
188+
}
189+
190+
void TestQgsFileDownloader::testInvalidFile()
191+
{
192+
makeCall( QUrl( "https://github.com/qgis/QGIS/archive/master.zip" ), QString() );
193+
QVERIFY( mExited );
194+
QVERIFY( !mCompleted );
195+
QVERIFY( mError );
196+
QVERIFY( !mCanceled );
197+
QCOMPARE( mErrorMessage, QString( "Cannot open output file: " ) );
198+
}
199+
200+
void TestQgsFileDownloader::testInvalidUrl()
201+
{
202+
QVERIFY( ! mTempFile->fileName().isEmpty() );
203+
makeCall( QUrl( "xyz://www" ), mTempFile->fileName() );
204+
QVERIFY( mExited );
205+
QVERIFY( !mCompleted );
206+
QVERIFY( mError );
207+
QVERIFY( !mCanceled );
208+
QCOMPARE( mErrorMessage, QString( "Network error 301: Protocol \"xyz\" is unknown" ) );
209+
}
210+
211+
void TestQgsFileDownloader::testBlankUrl()
212+
{
213+
QVERIFY( ! mTempFile->fileName().isEmpty() );
214+
makeCall( QUrl( "" ), mTempFile->fileName() );
215+
QVERIFY( mExited );
216+
QVERIFY( !mCompleted );
217+
QVERIFY( mError );
218+
QVERIFY( !mCanceled );
219+
QCOMPARE( mErrorMessage, QString( "Network error 301: Protocol \"\" is unknown" ) );
220+
}
221+
222+
#ifndef QT_NO_OPENSSL
223+
void TestQgsFileDownloader::testSslError_data()
224+
{
225+
QTest::addColumn<QString>( "url" );
226+
QTest::addColumn<QString>( "result" );
227+
228+
QTest::newRow( "expired" ) << "https://expired.badssl.com/" << "Network error 6: SSL handshake failed;SSL Errors: ;The certificate has expired";
229+
QTest::newRow( "self-signed" ) << "https://self-signed.badssl.com/" << "Network error 6: SSL handshake failed;SSL Errors: ;The certificate is self-signed, and untrusted";
230+
QTest::newRow( "untrusted-root" ) << "https://untrusted-root.badssl.com/" << "Network error 6: SSL handshake failed;No certificates could be verified;SSL Errors: ;The issuer certificate of a locally looked up certificate could not be found;The root CA certificate is not trusted for this purpose";
231+
}
232+
233+
void TestQgsFileDownloader::testSslError()
234+
{
235+
QFETCH( QString, url );
236+
QFETCH( QString, result );
237+
QVERIFY( ! mTempFile->fileName().isEmpty() );
238+
makeCall( QUrl( url ), mTempFile->fileName() );
239+
QCOMPARE( mErrorMessage, result );
240+
QVERIFY( !mCompleted );
241+
QVERIFY( mError );
242+
QVERIFY( !mCanceled );
243+
}
244+
245+
#endif
246+
247+
248+
QTEST_MAIN( TestQgsFileDownloader )
249+
#include "testqgsfiledownloader.moc"
250+
251+

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ ADD_PYTHON_TEST(PyQgsMapLayerRegistry test_qgsmaplayerregistry.py)
7979
ADD_PYTHON_TEST(PyQgsVirtualLayerProvider test_provider_virtual.py)
8080
ADD_PYTHON_TEST(PyQgsVirtualLayerDefinition test_qgsvirtuallayerdefinition.py)
8181
ADD_PYTHON_TEST(PyQgsLayerDefinition test_qgslayerdefinition.py)
82+
ADD_PYTHON_TEST(PyQgsFileDownloader test_qgsfiledownloader.py)
8283

8384
IF (NOT WIN32)
8485
ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Test the QgsFileDownloader class
4+
5+
.. note:: This program is free software; you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation; either version 2 of the License, or
8+
(at your option) any later version.
9+
"""
10+
from __future__ import print_function
11+
from future import standard_library
12+
import os
13+
import tempfile
14+
from functools import partial
15+
from qgis.PyQt.QtCore import QEventLoop, QUrl, QTimer
16+
from qgis.gui import (QgsFileDownloader,)
17+
from qgis.testing import start_app, unittest
18+
19+
standard_library.install_aliases()
20+
21+
__author__ = 'Alessandro Pasotti'
22+
__date__ = '08/11/2016'
23+
__copyright__ = 'Copyright 2016, The QGIS Project'
24+
# This will get replaced with a git SHA1 when you do a git archive
25+
__revision__ = '$Format:%H$'
26+
27+
28+
start_app()
29+
30+
31+
class TestQgsFileDownloader(unittest.TestCase):
32+
33+
"""
34+
This class tests the QgsFileDownloader class
35+
"""
36+
37+
def _make_download(self, url, destination, cancel=False):
38+
self.completed_was_called = False
39+
self.error_was_called = False
40+
self.canceled_was_called = False
41+
self.progress_was_called = False
42+
self.exited_was_called = False
43+
44+
loop = QEventLoop()
45+
46+
downloader = QgsFileDownloader(QUrl(url), destination, False)
47+
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
48+
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
49+
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
50+
downloader.downloadError.connect(partial(self._set_slot, 'error'))
51+
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))
52+
53+
downloader.downloadExited.connect(loop.quit)
54+
55+
if cancel:
56+
QTimer.singleShot(1000, downloader.onDownloadCanceled)
57+
58+
loop.exec_()
59+
60+
def test_validDownload(self):
61+
"""Tests a valid download"""
62+
destination = tempfile.mktemp()
63+
self._make_download('http://www.qgis.org', destination)
64+
self.assertTrue(self.exited_was_called)
65+
self.assertTrue(self.completed_was_called)
66+
self.assertTrue(self.progress_was_called)
67+
self.assertFalse(self.canceled_was_called)
68+
self.assertFalse(self.error_was_called)
69+
self.assertTrue(os.path.isfile(destination))
70+
self.assertGreater(os.path.getsize(destination), 0)
71+
72+
def test_inValidDownload(self):
73+
"""Tests an invalid download"""
74+
destination = tempfile.mktemp()
75+
self._make_download('http://www.doesnotexistofthatimsure.qgis', destination)
76+
self.assertTrue(self.exited_was_called)
77+
self.assertFalse(self.completed_was_called)
78+
self.assertTrue(self.progress_was_called)
79+
self.assertFalse(self.canceled_was_called)
80+
self.assertTrue(self.error_was_called)
81+
self.assertEqual(self.error_args[1], [u'Network error 3: Host www.doesnotexistofthatimsure.qgis not found'])
82+
self.assertFalse(os.path.isfile(destination))
83+
84+
def test_dowloadCanceled(self):
85+
"""Tests user canceled download"""
86+
destination = tempfile.mktemp()
87+
self._make_download('https://github.com/qgis/QGIS/archive/master.zip', destination, True)
88+
self.assertTrue(self.exited_was_called)
89+
self.assertFalse(self.completed_was_called)
90+
self.assertTrue(self.canceled_was_called)
91+
self.assertFalse(self.error_was_called)
92+
self.assertFalse(os.path.isfile(destination))
93+
94+
def test_InvalidUrl(self):
95+
destination = tempfile.mktemp()
96+
self._make_download('xyz://www', destination)
97+
self.assertTrue(self.exited_was_called)
98+
self.assertFalse(self.completed_was_called)
99+
self.assertFalse(self.canceled_was_called)
100+
self.assertTrue(self.error_was_called)
101+
self.assertFalse(os.path.isfile(destination))
102+
self.assertEqual(self.error_args[1], [u"Network error 301: Protocol \"xyz\" is unknown"])
103+
104+
def test_InvalidFile(self):
105+
self._make_download('https://github.com/qgis/QGIS/archive/master.zip', "")
106+
self.assertTrue(self.exited_was_called)
107+
self.assertFalse(self.completed_was_called)
108+
self.assertFalse(self.canceled_was_called)
109+
self.assertTrue(self.error_was_called)
110+
self.assertEqual(self.error_args[1], [u"Cannot open output file: "])
111+
112+
def test_BlankUrl(self):
113+
destination = tempfile.mktemp()
114+
self._make_download('', destination)
115+
self.assertTrue(self.exited_was_called)
116+
self.assertFalse(self.completed_was_called)
117+
self.assertFalse(self.canceled_was_called)
118+
self.assertTrue(self.error_was_called)
119+
self.assertFalse(os.path.isfile(destination))
120+
self.assertEqual(self.error_args[1], [u"Network error 301: Protocol \"\" is unknown"])
121+
122+
def ssl_compare(self, name, url, error):
123+
destination = tempfile.mktemp()
124+
self._make_download(url, destination)
125+
msg = "Failed in %s: %s" % (name, url)
126+
self.assertTrue(self.exited_was_called)
127+
self.assertFalse(self.completed_was_called, msg)
128+
self.assertFalse(self.canceled_was_called, msg)
129+
self.assertTrue(self.error_was_called, msg)
130+
self.assertFalse(os.path.isfile(destination), msg)
131+
result = sorted(self.error_args[1])
132+
result = ';'.join(result)
133+
self.assertEqual(result, error, msg + "expected:\n%s\nactual:\n%s\n" % (result, error))
134+
135+
def test_sslExpired(self):
136+
self.ssl_compare("expired", "https://expired.badssl.com/", "Network error 6: SSL handshake failed;SSL Errors: ;The certificate has expired")
137+
self.ssl_compare("self-signed", "https://self-signed.badssl.com/", "Network error 6: SSL handshake failed;SSL Errors: ;The certificate is self-signed, and untrusted")
138+
self.ssl_compare("untrusted-root", "https://untrusted-root.badssl.com/", "Network error 6: SSL handshake failed;No certificates could be verified;SSL Errors: ;The issuer certificate of a locally looked up certificate could not be found;The root CA certificate is not trusted for this purpose")
139+
140+
def _set_slot(self, *args, **kwargs):
141+
#print('_set_slot(%s) called' % args[0])
142+
setattr(self, args[0] + '_was_called', True)
143+
setattr(self, args[0] + '_args', args)
144+
145+
146+
if __name__ == '__main__':
147+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.