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