1 /***************************************************************************
2 testqgsofflineediting.cpp
3
4 ---------------------
5 begin : 3.7.2018
6 copyright : (C) 2018 by david signer
7 email : david at opengis dot ch
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16 #include <QObject>
17
18 #include <QString>
19 #include <QStringList>
20 #include <QApplication>
21 #include <QFileInfo>
22 #include <QDir>
23
24 #include "qgsunittypes.h"
25 #include "qgsofflineediting.h"
26 #include "qgstest.h"
27 #include "qgsvectorlayerref.h"
28 #include "qgslayertree.h"
29 #include "qgsmaplayerstylemanager.h"
30 #include "qgsjsonutils.h"
31
32 /**
33 * \ingroup UnitTests
34 */
35 class TestQgsOfflineEditing : public QObject
36 {
37 Q_OBJECT
38
39 private:
40 QgsOfflineEditing *mOfflineEditing = nullptr;
41 QgsVectorLayer *mpLayer = nullptr;
42 QgsVectorLayer *gpkgLayer = nullptr;
43 QString offlineDataPath;
44 QString offlineDbFile;
45 QStringList layerIds;
46 long numberOfFeatures;
47 int numberOfFields;
48 QTemporaryDir tempDir;
49
50 private slots:
51 void initTestCase();// will be called before the first testfunction is executed.
52 void cleanupTestCase();// will be called after the last testfunction was executed.
53 void init(); // will be called before each testfunction is executed.
54 void cleanup(); // will be called after every testfunction.
55
56 void createSpatialiteAndSynchronizeBack_data();
57 void createGeopackageAndSynchronizeBack_data();
58
59 void createSpatialiteAndSynchronizeBack();
60 void createGeopackageAndSynchronizeBack();
61 void removeConstraintsOnDefaultValues();
62 };
63
initTestCase()64 void TestQgsOfflineEditing::initTestCase()
65 {
66 //
67 // Runs once before any tests are run
68 //
69 // init QGIS's paths - true means that all path will be inited from prefix
70 QgsApplication::init();
71 QgsApplication::initQgis();
72 QgsApplication::showSettings();
73
74 //OfflineEditing
75 mOfflineEditing = new QgsOfflineEditing();
76 offlineDataPath = ".";
77 }
78
cleanupTestCase()79 void TestQgsOfflineEditing::cleanupTestCase()
80 {
81 delete mOfflineEditing;
82 QgsApplication::exitQgis();
83 }
84
init()85 void TestQgsOfflineEditing::init()
86 {
87 const QString myFileName( TEST_DATA_DIR ); //defined in CmakeLists.txt
88 const QString myTempDirName = tempDir.path();
89 QFile::copy( myFileName + "/points.geojson", myTempDirName + "/points.geojson" );
90 const QString myTempFileName = myTempDirName + "/points.geojson";
91 const QFileInfo myMapFileInfo( myTempFileName );
92 mpLayer = new QgsVectorLayer( myMapFileInfo.filePath(),
93 myMapFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );
94 QgsProject::instance()->addMapLayer( mpLayer );
95
96 numberOfFeatures = mpLayer->featureCount();
97 numberOfFields = mpLayer->fields().size();
98
99 layerIds.append( mpLayer->id() );
100
101 //same with gpkg
102 QFile::copy( myFileName + "/points_gpkg.gpkg", myTempDirName + "/points_gpkg.gpkg" );
103 const QString myTempFileNameGpgk = myTempDirName + "/points_gpkg.gpkg";
104 const QFileInfo myMapFileInfoGpkg( myTempFileNameGpgk );
105 gpkgLayer = new QgsVectorLayer( myMapFileInfoGpkg.filePath() + "|layername=points_gpkg", "points_gpkg", QStringLiteral( "ogr" ) );
106
107 QgsProject::instance()->addMapLayer( gpkgLayer );
108 layerIds.append( gpkgLayer->id() );
109 }
110
cleanup()111 void TestQgsOfflineEditing::cleanup()
112 {
113 QgsProject::instance()->removeAllMapLayers();
114 layerIds.clear();
115 QDir dir( offlineDataPath );
116 dir.remove( offlineDbFile );
117 }
118
createSpatialiteAndSynchronizeBack_data()119 void TestQgsOfflineEditing::createSpatialiteAndSynchronizeBack_data()
120 {
121 QTest::addColumn<QString>( "suffix_input" );
122 QTest::addColumn<QString>( "suffix_result" );
123
124 QTest::newRow( "no suffix" ) << QString( "no suffix" ) << QStringLiteral( " (offline)" ); //default value expected
125 QTest::newRow( "null suffix" ) << QString() << QString();
126 QTest::newRow( "empty suffix" ) << QStringLiteral( "" ) << QStringLiteral( "" );
127 QTest::newRow( "part of name suffix" ) << QStringLiteral( "point" ) << QStringLiteral( "point" );
128 QTest::newRow( "another suffix" ) << QStringLiteral( "another suffix" ) << QStringLiteral( "another suffix" );
129 }
130
createGeopackageAndSynchronizeBack_data()131 void TestQgsOfflineEditing::createGeopackageAndSynchronizeBack_data()
132 {
133 QTest::addColumn<QString>( "suffix_input" );
134 QTest::addColumn<QString>( "suffix_result" );
135
136 QTest::newRow( "no suffix" ) << QStringLiteral( "no suffix" ) << QStringLiteral( " (offline)" ); //default value expected
137 QTest::newRow( "null suffix" ) << QString() << QString();
138 QTest::newRow( "empty suffix" ) << QStringLiteral( "" ) << QStringLiteral( "" );
139 QTest::newRow( "part of name suffix" ) << QStringLiteral( "point" ) << QStringLiteral( "point" );
140 QTest::newRow( "another suffix" ) << QStringLiteral( "another suffix" ) << QStringLiteral( "another suffix" );
141 }
142
createSpatialiteAndSynchronizeBack()143 void TestQgsOfflineEditing::createSpatialiteAndSynchronizeBack()
144 {
145
146 QFETCH( QString, suffix_input );
147 QFETCH( QString, suffix_result );
148
149 offlineDbFile = "TestQgsOfflineEditing.sqlite";
150 QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
151 QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
152 QCOMPARE( mpLayer->fields().size(), numberOfFields );
153
154 //set on LayerTreeNode showFeatureCount property
155 QgsLayerTreeLayer *layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
156 layerTreelayer->setCustomProperty( QStringLiteral( "showFeatureCount" ), 1 );
157
158 //convert
159 if ( suffix_input.compare( QLatin1String( "no suffix" ) ) == 0 )
160 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::SpatiaLite );
161 else
162 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::SpatiaLite, suffix_input );
163
164 const QString layerName = QStringLiteral( "points%1" ).arg( suffix_result );
165
166 mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( layerName ).first() );
167 QCOMPARE( mpLayer->name(), layerName );
168 QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
169 //check LayerTreeNode showFeatureCount property
170 layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
171 QCOMPARE( layerTreelayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt(), 1 );
172 //unset on LayerTreeNode showFeatureCount property
173 layerTreelayer->setCustomProperty( QStringLiteral( "showFeatureCount" ), 0 );
174
175 //synchronize back
176 mOfflineEditing->synchronize();
177
178 mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( QStringLiteral( "points" ) ).first() );
179 QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
180 QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
181 QCOMPARE( mpLayer->fields().size(), numberOfFields );
182
183 //check LayerTreeNode showFeatureCount property
184 layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
185 QCOMPARE( layerTreelayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt(), 0 );
186 }
187
createGeopackageAndSynchronizeBack()188 void TestQgsOfflineEditing::createGeopackageAndSynchronizeBack()
189 {
190 QFETCH( QString, suffix_input );
191 QFETCH( QString, suffix_result );
192
193 offlineDbFile = "TestQgsOfflineEditing.gpkg";
194 QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
195 QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
196 QCOMPARE( mpLayer->fields().size(), numberOfFields );
197 QgsFeature firstFeatureBeforeAction;
198 QgsFeatureIterator it = mpLayer->getFeatures();
199 it.nextFeature( firstFeatureBeforeAction );
200
201 connect( mOfflineEditing, &QgsOfflineEditing::warning, this, []( const QString & title, const QString & message ) { qDebug() << title << message; } );
202
203 //set on LayerTreeNode showFeatureCount property
204 QgsLayerTreeLayer *layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
205 layerTreelayer->setCustomProperty( QStringLiteral( "showFeatureCount" ), 1 );
206 layerTreelayer->setItemVisibilityChecked( false );
207 QgsMapLayerStyle style;
208 style.readFromLayer( mpLayer );
209
210 mpLayer->styleManager()->addStyle( QStringLiteral( "testStyle" ), style );
211
212 //convert
213 if ( suffix_input.compare( QLatin1String( "no suffix" ) ) == 0 )
214 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG );
215 else
216 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG, suffix_input );
217
218 const QString layerName = QStringLiteral( "points%1" ).arg( suffix_result );
219 mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( layerName ).first() );
220 QCOMPARE( mpLayer->name(), layerName );
221 QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
222 //comparing with the number +1 because GPKG created an fid
223 QCOMPARE( mpLayer->fields().size(), numberOfFields + 1 );
224 //check LayerTreeNode showFeatureCount property
225 layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
226 QCOMPARE( layerTreelayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt(), 1 );
227 QCOMPARE( layerTreelayer->isVisible(), false );
228 QVERIFY( mpLayer->styleManager()->styles().contains( QStringLiteral( "testStyle" ) ) );
229
230 QgsFeature firstFeatureInAction;
231 it = mpLayer->getFeatures();
232 it.nextFeature( firstFeatureInAction );
233
234 //compare some values
235 QCOMPARE( firstFeatureInAction.attribute( QStringLiteral( "Class" ) ).toString(), firstFeatureBeforeAction.attribute( QStringLiteral( "Class" ) ).toString() );
236 QCOMPARE( firstFeatureInAction.attribute( QStringLiteral( "Heading" ) ).toString(), firstFeatureBeforeAction.attribute( QStringLiteral( "Heading" ) ).toString() );
237 QCOMPARE( firstFeatureInAction.attribute( QStringLiteral( "Cabin Crew" ) ).toString(), firstFeatureBeforeAction.attribute( QStringLiteral( "Cabin Crew" ) ).toString() );
238
239 //check converted lists values
240 QCOMPARE( firstFeatureInAction.attribute( QStringLiteral( "StaffNames" ) ), QVariantList() << QStringLiteral( "Bob" ) << QStringLiteral( "Alice" ) );
241 QCOMPARE( firstFeatureInAction.attribute( QStringLiteral( "StaffAges" ) ), QVariantList() << 22 << 33 );
242
243 QgsFeature newFeature( mpLayer->dataProvider()->fields() );
244 newFeature.setAttribute( QStringLiteral( "Class" ), QStringLiteral( "Superjet" ) );
245 newFeature.setAttribute( QStringLiteral( "StaffNames" ), QgsJsonUtils::parseArray( QStringLiteral( "[ \"Sebastien\", \"Naomi\", \"And, many, more\" ]" ) ) );
246 newFeature.setAttribute( QStringLiteral( "StaffAges" ), QgsJsonUtils::parseArray( QStringLiteral( "[ 0, 2 ]" ) ) );
247 mpLayer->startEditing();
248 mpLayer->addFeature( newFeature );
249 mpLayer->commitChanges();
250 QCOMPARE( mpLayer->featureCount(), numberOfFeatures + 1 );
251
252 //unset on LayerTreeNode showFeatureCount property
253 layerTreelayer->setCustomProperty( QStringLiteral( "showFeatureCount" ), 0 );
254
255 //synchronize back
256 mOfflineEditing->synchronize();
257
258 mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( QStringLiteral( "points" ) ).first() );
259 QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
260 QCOMPARE( mpLayer->dataProvider()->featureCount(), numberOfFeatures + 1 );
261 QCOMPARE( mpLayer->fields().size(), numberOfFields );
262 //check LayerTreeNode showFeatureCount property
263 layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
264 QCOMPARE( layerTreelayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt(), 0 );
265
266 //get last feature
267 const QgsFeature f = mpLayer->getFeature( mpLayer->dataProvider()->featureCount() - 1 );
268 qDebug() << "FID:" << f.id() << "Class:" << f.attribute( "Class" ).toString();
269 QCOMPARE( f.attribute( QStringLiteral( "Class" ) ).toString(), QStringLiteral( "Superjet" ) );
270 QCOMPARE( f.attribute( QStringLiteral( "StaffNames" ) ).toStringList(), QStringList() << QStringLiteral( "Sebastien" ) << QStringLiteral( "Naomi" ) << QStringLiteral( "And, many, more" ) );
271 QCOMPARE( f.attribute( QStringLiteral( "StaffAges" ) ).toList(), QList<QVariant>() << 0 << 2 );
272
273 QgsFeature firstFeatureAfterAction;
274 it = mpLayer->getFeatures();
275 it.nextFeature( firstFeatureAfterAction );
276
277 QCOMPARE( firstFeatureAfterAction, firstFeatureBeforeAction );
278
279 //and delete the feature again
280 QgsFeatureIds idsToClean;
281 idsToClean << f.id();
282 mpLayer->dataProvider()->deleteFeatures( idsToClean );
283 QCOMPARE( mpLayer->dataProvider()->featureCount(), numberOfFeatures );
284 }
285
removeConstraintsOnDefaultValues()286 void TestQgsOfflineEditing::removeConstraintsOnDefaultValues()
287 {
288 offlineDbFile = "TestQgsOfflineEditing.gpkg";
289 QCOMPARE( gpkgLayer->name(), QStringLiteral( "points_gpkg" ) );
290
291 //check constraints (not null and unique)
292 QgsFieldConstraints constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
293 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull );
294 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
295
296 //convert
297 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG );
298
299 QCOMPARE( gpkgLayer->name(), QStringLiteral( "points_gpkg (offline)" ) );
300
301 //check constraints (not not null)
302 constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
303 QVERIFY( !( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull ) );
304 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
305
306 //synchronize back
307 mOfflineEditing->synchronize();
308
309 //check constraints (not null and unique)
310 constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
311 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull );
312 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
313 }
314
315
316 QGSTEST_MAIN( TestQgsOfflineEditing )
317 #include "testqgsofflineediting.moc"
318