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
31 /**
32 * \ingroup UnitTests
33 */
34 class TestQgsOfflineEditing : public QObject
35 {
36 Q_OBJECT
37
38 private:
39 QgsOfflineEditing *mOfflineEditing = nullptr;
40 QgsVectorLayer *mpLayer = nullptr;
41 QgsVectorLayer *gpkgLayer = nullptr;
42 QString offlineDataPath;
43 QString offlineDbFile;
44 QStringList layerIds;
45 long numberOfFeatures;
46 int numberOfFields;
47 QTemporaryDir tempDir;
48
49 private slots:
50 void initTestCase();// will be called before the first testfunction is executed.
51 void cleanupTestCase();// will be called after the last testfunction was executed.
52 void init(); // will be called before each testfunction is executed.
53 void cleanup(); // will be called after every testfunction.
54
55 void createSpatialiteAndSynchronizeBack_data();
56 void createGeopackageAndSynchronizeBack_data();
57
58 void createSpatialiteAndSynchronizeBack();
59 void createGeopackageAndSynchronizeBack();
60 void removeConstraintsOnDefaultValues();
61 };
62
initTestCase()63 void TestQgsOfflineEditing::initTestCase()
64 {
65 //
66 // Runs once before any tests are run
67 //
68 // init QGIS's paths - true means that all path will be inited from prefix
69 QgsApplication::init();
70 QgsApplication::initQgis();
71 QgsApplication::showSettings();
72
73 //OfflineEditing
74 mOfflineEditing = new QgsOfflineEditing();
75 offlineDataPath = ".";
76 }
77
cleanupTestCase()78 void TestQgsOfflineEditing::cleanupTestCase()
79 {
80 QgsApplication::exitQgis();
81 }
82
init()83 void TestQgsOfflineEditing::init()
84 {
85 QString myFileName( TEST_DATA_DIR ); //defined in CmakeLists.txt
86 QString myTempDirName = tempDir.path();
87 QFile::copy( myFileName + "/points.shp", myTempDirName + "/points.shp" );
88 QFile::copy( myFileName + "/points.shx", myTempDirName + "/points.shx" );
89 QFile::copy( myFileName + "/points.dbf", myTempDirName + "/points.dbf" );
90 QString myTempFileName = myTempDirName + "/points.shp";
91 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 QString myTempFileNameGpgk = myTempDirName + "/points_gpkg.gpkg";
104 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( QStringLiteral( "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 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( QStringLiteral( "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 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 QgsFeature newFeature( mpLayer->dataProvider()->fields() );
240 newFeature.setAttribute( QStringLiteral( "Class" ), QStringLiteral( "Superjet" ) );
241 mpLayer->startEditing();
242 mpLayer->addFeature( newFeature );
243 mpLayer->commitChanges();
244 QCOMPARE( mpLayer->featureCount(), numberOfFeatures + 1 );
245
246 //unset on LayerTreeNode showFeatureCount property
247 layerTreelayer->setCustomProperty( QStringLiteral( "showFeatureCount" ), 0 );
248
249 //synchronize back
250 mOfflineEditing->synchronize();
251
252 mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( QStringLiteral( "points" ) ).first() );
253 QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
254 QCOMPARE( mpLayer->dataProvider()->featureCount(), numberOfFeatures + 1 );
255 QCOMPARE( mpLayer->fields().size(), numberOfFields );
256 //check LayerTreeNode showFeatureCount property
257 layerTreelayer = QgsProject::instance()->layerTreeRoot()->findLayer( mpLayer->id() );
258 QCOMPARE( layerTreelayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt(), 0 );
259
260 //get last feature
261 QgsFeature f = mpLayer->getFeature( mpLayer->dataProvider()->featureCount() - 1 );
262 qDebug() << "FID:" << f.id() << "Class:" << f.attribute( "Class" ).toString();
263 QCOMPARE( f.attribute( QStringLiteral( "Class" ) ).toString(), QStringLiteral( "Superjet" ) );
264
265 QgsFeature firstFeatureAfterAction;
266 it = mpLayer->getFeatures();
267 it.nextFeature( firstFeatureAfterAction );
268
269 QCOMPARE( firstFeatureAfterAction, firstFeatureBeforeAction );
270
271 //and delete the feature again
272 QgsFeatureIds idsToClean;
273 idsToClean << f.id();
274 mpLayer->dataProvider()->deleteFeatures( idsToClean );
275 QCOMPARE( mpLayer->dataProvider()->featureCount(), numberOfFeatures );
276 }
277
removeConstraintsOnDefaultValues()278 void TestQgsOfflineEditing::removeConstraintsOnDefaultValues()
279 {
280 offlineDbFile = "TestQgsOfflineEditing.gpkg";
281 QCOMPARE( gpkgLayer->name(), QStringLiteral( "points_gpkg" ) );
282 QString name = gpkgLayer->name();
283
284 //check constraints (not null and unique)
285 QgsFieldConstraints constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
286 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull );
287 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
288
289 //convert
290 mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG );
291
292 gpkgLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( QStringLiteral( "points_gpkg (offline)" ) ).first() );
293 QCOMPARE( gpkgLayer->name(), QStringLiteral( "points_gpkg (offline)" ) );
294
295 name = gpkgLayer->name();
296 //check constraints (unique but not not null)
297 constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
298 QVERIFY( !( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull ) );
299 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
300
301 //synchronize back
302 mOfflineEditing->synchronize();
303
304 gpkgLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayersByName( QStringLiteral( "points_gpkg" ) ).first() );
305
306 name = gpkgLayer->name();
307 //check constraints (not null and unique)
308 constraintsOfFidField = gpkgLayer->fields().at( gpkgLayer->fields().indexOf( QLatin1String( "fid" ) ) ).constraints();
309 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintNotNull );
310 QVERIFY( constraintsOfFidField.constraints() & QgsFieldConstraints::ConstraintUnique );
311 }
312
313
314 QGSTEST_MAIN( TestQgsOfflineEditing )
315 #include "testqgsofflineediting.moc"
316