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