QGIS API Documentation 4.3.0-Master (16eddb2670e)
Loading...
Searching...
No Matches
qgstestutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstestutils.cpp
3 --------------------
4 begin : January 2018
5 copyright : (C) 2018 by Nyall Dawson
6 email : nyall.dawson@gmail.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 "qgstestutils.h"
17
18#include <atomic>
19
20#include "qgsconnectionpool.h"
21#include "qgsprovidermetadata.h"
22#include "qgsproviderregistry.h"
25#include "qgsvectorlayer.h"
26
27#include <QCryptographicHash>
28#include <QDeadlineTimer>
29#include <QString>
30#include <QThread>
31#include <QtConcurrentMap>
32
33using namespace Qt::StringLiterals;
34
37
38static void getFeaturesForProvider( const QPair< std::shared_ptr< QgsAbstractFeatureSource >, QgsFeatureRequest > &pair )
39{
40 QgsFeatureIterator it = pair.first->getFeatures( pair.second );
41 QgsFeature f;
42 while ( it.nextFeature( f ) )
43 {
44 }
45}
46
47bool QgsTestUtils::testProviderIteratorThreadSafety( QgsVectorDataProvider *provider, const QgsFeatureRequest &request )
48{
49 constexpr int JOBS_TO_RUN = 100;
50 QList< QPair< std::shared_ptr< QgsAbstractFeatureSource >, QgsFeatureRequest > > jobs;
51 jobs.reserve( JOBS_TO_RUN );
52 for ( int i = 0; i < JOBS_TO_RUN; ++i )
53 {
54 jobs.append( qMakePair( std::shared_ptr< QgsAbstractFeatureSource >( provider->featureSource() ), request ) );
55 }
56
57 //freaking hammer the provider with a ton of concurrent requests.
58 //thread unsafe providers... you better be ready!!!!
59 QtConcurrent::blockingMap( jobs, getFeaturesForProvider );
60
61 return true;
62}
63
64bool QgsTestUtils::compareDomElements( const QDomElement &element1, const QDomElement &element2 )
65{
66 QString tag1 = element1.tagName();
67 tag1.replace( QRegularExpression( ".*:" ), QString() );
68 QString tag2 = element2.tagName();
69 tag2.replace( QRegularExpression( ".*:" ), QString() );
70 if ( tag1 != tag2 )
71 {
72 qDebug( "Different tag names: %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data() );
73 return false;
74 }
75
76 if ( element1.hasAttributes() != element2.hasAttributes() )
77 {
78 qDebug( "Different hasAttributes: %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data() );
79 return false;
80 }
81
82 if ( element1.hasAttributes() )
83 {
84 const QDomNamedNodeMap attrs1 = element1.attributes();
85 const QDomNamedNodeMap attrs2 = element2.attributes();
86
87 if ( attrs1.size() != attrs2.size() )
88 {
89 qDebug( "Different attributes size: %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data() );
90 return false;
91 }
92
93 for ( int i = 0; i < attrs1.size(); ++i )
94 {
95 const QDomNode node1 = attrs1.item( i );
96 const QDomAttr attr1 = node1.toAttr();
97
98 if ( !element2.hasAttribute( attr1.name() ) )
99 {
100 qDebug( "Element2 has not attribute: %s, %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data(), attr1.name().toLatin1().data() );
101 return false;
102 }
103
104 if ( element2.attribute( attr1.name() ) != attr1.value() )
105 {
106 qDebug( "Element2 attribute has not the same value: %s, %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data(), attr1.name().toLatin1().data() );
107 return false;
108 }
109 }
110 }
111
112 if ( element1.hasChildNodes() != element2.hasChildNodes() )
113 {
114 qDebug( "Different childNodes: %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data() );
115 return false;
116 }
117
118 if ( element1.hasChildNodes() )
119 {
120 const QDomNodeList nodes1 = element1.childNodes();
121 const QDomNodeList nodes2 = element2.childNodes();
122
123 if ( nodes1.size() != nodes2.size() )
124 {
125 qDebug( "Different childNodes size: %s, %s", tag1.toLatin1().data(), tag2.toLatin1().data() );
126 return false;
127 }
128
129 for ( int i = 0; i < nodes1.size(); ++i )
130 {
131 const QDomNode node1 = nodes1.at( i );
132 const QDomNode node2 = nodes2.at( i );
133 if ( node1.isElement() && node2.isElement() )
134 {
135 QDomElement elt1 = node1.toElement();
136 QDomElement elt2 = node2.toElement();
137
138 if ( !compareDomElements( elt1, elt2 ) )
139 return false;
140 }
141 else if ( node1.isText() && node2.isText() )
142 {
143 const QDomText txt1 = node1.toText();
144 const QDomText txt2 = node2.toText();
145
146 if ( txt1.data() != txt2.data() )
147 {
148 qDebug( "Different text data: %s %s", tag1.toLatin1().data(), txt1.data().toLatin1().data() );
149 qDebug( "Different text data: %s %s", tag2.toLatin1().data(), txt2.data().toLatin1().data() );
150 return false;
151 }
152 }
153 }
154 }
155
156 if ( element1.text() != element2.text() )
157 {
158 qDebug( "Different text: %s %s", tag1.toLatin1().data(), element1.text().toLatin1().data() );
159 qDebug( "Different text: %s %s", tag2.toLatin1().data(), element2.text().toLatin1().data() );
160 return false;
161 }
162
163 return true;
164}
165
166QString QgsTestUtils::sanitizeFakeHttpEndpoint( const QString &urlString )
167{
168 QString modifiedUrlString = urlString;
169
170 // For REST API using URL subpaths, normalize the subpaths
171 const int afterEndpointStartPos = static_cast<int>( modifiedUrlString.indexOf( "fake_qgis_http_endpoint" ) + strlen( "fake_qgis_http_endpoint" ) );
172 QString afterEndpointStart = modifiedUrlString.mid( afterEndpointStartPos );
173 afterEndpointStart.replace( "/"_L1, "_"_L1 );
174 modifiedUrlString = modifiedUrlString.mid( 0, afterEndpointStartPos ) + afterEndpointStart;
175
176 const qsizetype posQuotationMark = modifiedUrlString.indexOf( '?' );
177 QString args = modifiedUrlString.mid( posQuotationMark );
178 if ( modifiedUrlString.size() > 256 )
179 {
180 args.replace( '/', '_' );
181 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
182 }
183 else
184 {
185 args.replace( '?', '_' );
186 args.replace( '&', '_' );
187 args.replace( '<', '_' );
188 args.replace( '>', '_' );
189 args.replace( '\"', '_' );
190 args.replace( '\'', '_' );
191 args.replace( ' ', '_' );
192 args.replace( ':', '_' );
193 args.replace( '/', '_' );
194 args.replace( '\n', '_' );
195 }
196 return modifiedUrlString.mid( 0, posQuotationMark ) + args;
197}
198
199// anonymous namespace to avoid potential symbol collisions
200namespace
201{
202
204 class LayerOpenWorker : public QThread
205 {
206 public:
207 LayerOpenWorker( const QString &uri, const QString &providerKey, std::atomic<bool> &stop )
208 : mUri( uri )
209 , mProviderKey( providerKey )
210 , mStop( stop )
211 {}
212
213 void run() override
214 {
215 while ( !mStop )
216 {
217 mLoops++;
218 const QgsVectorLayer layer( mUri, u"concurrent_access_worker"_s, mProviderKey );
219 if ( !layer.isValid() )
220 continue;
221 mValidOpens++;
222 QgsFeature f;
223 layer.getFeatures().nextFeature( f );
224 }
225 }
226
227 int loops() const { return mLoops; }
228 int validOpens() const { return mValidOpens; }
229
230 private:
231 QString mUri;
232 QString mProviderKey;
233 std::atomic<bool> &mStop;
234 int mLoops = 0;
235 int mValidOpens = 0;
236 };
237
242 class SublayerQueryWorker : public QThread
243 {
244 public:
245 SublayerQueryWorker( QgsProviderMetadata *metadata, const QString &uri, int expectedSublayerCount, std::atomic<bool> &stop )
246 : mMetadata( metadata )
247 , mUri( uri )
248 , mExpectedSublayerCount( expectedSublayerCount )
249 , mStop( stop )
250 {}
251
252 void run() override
253 {
254 while ( !mStop )
255 {
256 mLoops++;
257 if ( mMetadata->querySublayers( mUri ).size() == mExpectedSublayerCount )
258 mConsistentQueries++;
259 }
260 }
261
262 int loops() const { return mLoops; }
263 int consistentQueries() const { return mConsistentQueries; }
264
265 private:
266 QgsProviderMetadata *mMetadata = nullptr;
267 QString mUri;
268 int mExpectedSublayerCount = 0;
269 std::atomic<bool> &mStop;
270 int mLoops = 0;
271 int mConsistentQueries = 0;
272 };
273
274} // namespace
275
276bool QgsTestUtils::testConcurrentLayerAccess( const QString &uri, const QString &providerKey, int runTimeMs, int timeoutMs )
277{
278 constexpr int LAYER_WORKER_COUNT = 6;
279
281 if ( !metadata )
282 {
283 qDebug( "testConcurrentLayerAccess: no provider metadata found for %s", providerKey.toLatin1().constData() );
284 return false;
285 }
286
287 // single-threaded baseline results: concurrent access must give the same
288 // answers
289 const bool expectValidLayer = QgsVectorLayer( uri, u"concurrent_access_baseline"_s, providerKey ).isValid();
290 const int expectedSublayerCount = static_cast<int>( metadata->querySublayers( uri ).size() );
291
292 std::atomic<bool> stop( false );
293 std::vector<std::unique_ptr<QThread>> threads;
294
295 std::vector<LayerOpenWorker *> layerWorkers;
296 for ( int i = 0; i < LAYER_WORKER_COUNT; i++ )
297 {
298 layerWorkers.push_back( new LayerOpenWorker( uri, providerKey, stop ) );
299 threads.emplace_back( layerWorkers.back() );
300 }
301
302 SublayerQueryWorker *sublayerWorker = new SublayerQueryWorker( metadata, uri, expectedSublayerCount, stop );
303 threads.emplace_back( sublayerWorker );
304
305 for ( auto &thread : threads )
306 thread->start();
307
308 // let all threads contend for a fixed time window, then ask them to stop
309 QThread::msleep( static_cast<unsigned long>( runTimeMs ) );
310 stop = true;
311
312 // Use a single shared deadline across all threads: if a deadlock occurs, some
313 // threads will never return from wait(). A per-thread timeout would reset the
314 // clock for each thread, allowing an arbitrarily long hang to go undetected.
315 QDeadlineTimer deadline( timeoutMs );
316 bool allFinished = true;
317 for ( auto &thread : threads )
318 allFinished = thread->wait( deadline ) && allFinished;
319
320 if ( !allFinished )
321 {
322 // deliberately leak the thread objects: some threads are deadlocked and
323 // destroying a still-running QThread is a fatal error which would mask
324 // the failure report
325 for ( auto &thread : threads )
326 ( void ) thread.release();
327 qDebug(
328 "testConcurrentLayerAccess: deadlock detected, not all threads "
329 "accessing %s finished in time",
330 uri.toLatin1().constData()
331 );
332 return false;
333 }
334
335 bool ok = true;
336 for ( const LayerOpenWorker *worker : layerWorkers )
337 {
338 if ( worker->loops() == 0 )
339 {
340 qDebug(
341 "testConcurrentLayerAccess: a layer open worker did not complete "
342 "a single loop"
343 );
344 ok = false;
345 }
346 if ( expectValidLayer && worker->validOpens() != worker->loops() )
347 {
348 qDebug(
349 "testConcurrentLayerAccess: only %d of %d concurrent layer opens "
350 "gave a valid layer",
351 worker->validOpens(),
352 worker->loops()
353 );
354 ok = false;
355 }
356 }
357 if ( sublayerWorker->consistentQueries() != sublayerWorker->loops() )
358 {
359 qDebug(
360 "testConcurrentLayerAccess: only %d of %d concurrent sublayer "
361 "queries returned the expected %d sublayers",
362 sublayerWorker->consistentQueries(),
363 sublayerWorker->loops(),
364 expectedSublayerCount
365 );
366 ok = false;
367 }
368 return ok;
369}
370
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
Holds data provider key, description, and associated shared library file or function pointer informat...
virtual QList< QgsProviderSublayerDetails > querySublayers(const QString &uri, Qgis::SublayerQueryFlags flags=Qgis::SublayerQueryFlags(), QgsFeedback *feedback=nullptr) const
Queries the specified uri and returns a list of any valid sublayers found in the dataset which can be...
static QgsProviderRegistry * instance(const QString &pluginPath=QString())
Means of accessing canonical single instance.
QgsProviderMetadata * providerMetadata(const QString &providerKey) const
Returns metadata of the provider or nullptr if not found.
Base class for vector data providers.
virtual QgsAbstractFeatureSource * featureSource() const =0
Returns feature source object that can be used for querying provider's data.
Represents a vector layer which manages a vector based dataset.