Skip to content

Commit a058c36

Browse files
committed
Added offline editing WFS tests and debug info
1 parent 1316d9c commit a058c36

File tree

6 files changed

+357
-14
lines changed

6 files changed

+357
-14
lines changed

src/core/qgsofflineediting.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ void QgsOfflineEditing::synchronize()
205205
// open logging db
206206
sqlite3* db = openLoggingDb();
207207
if ( !db )
208+
{
208209
return;
210+
}
209211

210212
emit progressStarted();
211213

@@ -221,6 +223,7 @@ void QgsOfflineEditing::synchronize()
221223
}
222224
}
223225

226+
QgsDebugMsgLevel( QString( "Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
224227
for ( int l = 0; l < offlineLayers.count(); l++ )
225228
{
226229
QgsMapLayer* layer = offlineLayers[l];
@@ -265,8 +268,10 @@ void QgsOfflineEditing::synchronize()
265268

266269
// TODO: only get commitNos of this layer?
267270
int commitNo = getCommitNo( db );
271+
QgsDebugMsgLevel( QString( "Found %1 commits" ).arg( commitNo ), 4 );
268272
for ( int i = 0; i < commitNo; i++ )
269273
{
274+
QgsDebugMsgLevel( "Apply commits chronologically", 4 );
270275
// apply commits chronologically
271276
applyAttributesAdded( remoteLayer, db, layerId, i );
272277
applyAttributeValueChanges( offlineLayer, remoteLayer, db, layerId, i );
@@ -302,6 +307,10 @@ void QgsOfflineEditing::synchronize()
302307
showWarning( remoteLayer->commitErrors().join( "\n" ) );
303308
}
304309
}
310+
else
311+
{
312+
QgsDebugMsg( "Could not find the layer id in the edit logs!" );
313+
}
305314
// Invalidate the connection to force a reload if the project is put offline
306315
// again with the same path
307316
offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceURI( offlineLayer->source() ).database() );
@@ -317,6 +326,10 @@ void QgsOfflineEditing::synchronize()
317326
QgsProject::instance()->removeEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH );
318327
remoteLayer->reload(); //update with other changes
319328
}
329+
else
330+
{
331+
QgsDebugMsg( "Remote layer is not valid!" );
332+
}
320333
}
321334

322335
emit progressStopped();
@@ -477,7 +490,7 @@ QgsVectorLayer* QgsOfflineEditing::copyVectorLayer( QgsVectorLayer* layer, sqlit
477490
return nullptr;
478491

479492
QString tableName = layer->id();
480-
QgsDebugMsg( QString( "Creating offline table %1 ..." ).arg( tableName ) );
493+
QgsDebugMsgLevel( QString( "Creating offline table %1 ..." ).arg( tableName ), 4 );
481494

482495
// create table
483496
QString sql = QString( "CREATE TABLE '%1' (" ).arg( tableName );
@@ -817,7 +830,7 @@ void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer* offlineLayer
817830
for ( int i = 0; i < values.size(); i++ )
818831
{
819832
QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
820-
833+
QgsDebugMsgLevel( QString( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
821834
remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
822835

823836
emit progressUpdated( i + 1 );
@@ -932,11 +945,16 @@ sqlite3* QgsOfflineEditing::openLoggingDb()
932945
int rc = sqlite3_open( dbPath.toUtf8().constData(), &db );
933946
if ( rc != SQLITE_OK )
934947
{
948+
QgsDebugMsg( "Could not open the spatialite logging database" );
935949
showWarning( tr( "Could not open the spatialite logging database" ) );
936950
sqlite3_close( db );
937951
db = nullptr;
938952
}
939953
}
954+
else
955+
{
956+
QgsDebugMsg( "dbPath is empty!" );
957+
}
940958
return db;
941959
}
942960

tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,5 @@ IF (WITH_SERVER)
132132
ADD_PYTHON_TEST(PyQgsServer test_qgsserver.py)
133133
ADD_PYTHON_TEST(PyQgsServerAccessControl test_qgsserver_accesscontrol.py)
134134
ADD_PYTHON_TEST(PyQgsServerWFST test_qgsserver_wfst.py)
135+
ADD_PYTHON_TEST(PyQgsOfflineEditingWFS test_offline_editing_wfs.py)
135136
ENDIF (WITH_SERVER)
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit test utils for offline editing tests.
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
from __future__ import print_function
10+
from builtins import str
11+
from builtins import object
12+
__author__ = 'Alessandro Pasotti'
13+
__date__ = '2016-06-30'
14+
__copyright__ = 'Copyright 2016, The QGIS Project'
15+
# This will get replaced with a git SHA1 when you do a git archive
16+
__revision__ = '$Format:%H$'
17+
18+
from time import sleep
19+
20+
from qgis.core import (
21+
QgsFeature,
22+
QgsGeometry,
23+
QgsPoint,
24+
QgsFeatureRequest,
25+
QgsExpression,
26+
QgsMapLayerRegistry,
27+
QgsOfflineEditing,
28+
)
29+
30+
31+
# Tet features, fields: [id, name, geometry]
32+
# "id" is used as a pk to retriev features by attribute
33+
TEST_FEATURES = [
34+
(1, 'name 1', QgsPoint(9, 45)),
35+
(2, 'name 2', QgsPoint(9.5, 45.5)),
36+
(3, 'name 3', QgsPoint(9.5, 46)),
37+
(4, 'name 4', QgsPoint(10, 46.5)),
38+
]
39+
40+
41+
class OfflineTestBase(object):
42+
43+
"""Generic test methods for all online providers"""
44+
45+
def _setUp(self):
46+
"""Called by setUp: run before each test."""
47+
# Setup: create some features for the test layer
48+
features = []
49+
layer = self._getLayer('test_point')
50+
for id, name, geom in TEST_FEATURES:
51+
f = QgsFeature(layer.pendingFields())
52+
f['id'] = id
53+
f['name'] = name
54+
f.setGeometry(QgsGeometry.fromPoint(geom))
55+
features.append(f)
56+
layer.dataProvider().addFeatures(features)
57+
# Add the remote layer
58+
self.registry = QgsMapLayerRegistry.instance()
59+
self.registry.removeAllMapLayers()
60+
assert self.registry.addMapLayer(self._getOnlineLayer('test_point')) is not None
61+
62+
def _tearDown(self):
63+
"""Called by tearDown: run after each test."""
64+
# Clear test layers
65+
self._clearLayer('test_point')
66+
67+
@classmethod
68+
def _compareFeature(cls, layer, attributes):
69+
"""Compare id, name and geometry"""
70+
f = cls._getFeatureByAttribute(layer, 'id', attributes[0])
71+
return f['name'] == attributes[1] and f.geometry().asPoint().toString() == attributes[2].toString()
72+
73+
@classmethod
74+
def _clearLayer(cls, layer_name):
75+
"""
76+
Delete all features from the backend layer
77+
"""
78+
layer = cls._getLayer(layer_name)
79+
layer.startEditing()
80+
layer.deleteFeatures([f.id() for f in layer.getFeatures()])
81+
layer.commitChanges()
82+
assert layer.featureCount() == 0
83+
84+
@classmethod
85+
def _getLayer(cls, layer_name):
86+
"""
87+
Layer factory (return the backend layer), provider specific
88+
"""
89+
raise NotImplementedError
90+
91+
@classmethod
92+
def _getOnlineLayer(cls, type_name, layer_name=None):
93+
"""
94+
Layer factory (return the online layer), provider specific
95+
"""
96+
raise NotImplementedError
97+
98+
@classmethod
99+
def _getFeatureByAttribute(cls, layer, attr_name, attr_value):
100+
"""
101+
Find the feature and return it, raise exception if not found
102+
"""
103+
request = QgsFeatureRequest(QgsExpression("%s=%s" % (attr_name,
104+
attr_value)))
105+
try:
106+
return next(layer.dataProvider().getFeatures(request))
107+
except StopIteration:
108+
raise Exception("Wrong attributes in WFS layer %s" %
109+
layer.name())
110+
111+
def test_offlineConversion(self):
112+
# goes offline
113+
ol = QgsOfflineEditing()
114+
online_layer = list(self.registry.mapLayers().values())[0]
115+
self.assertTrue(online_layer.hasGeometryType())
116+
# Check we have 3 features
117+
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES))
118+
self.assertTrue(ol.convertToOfflineProject(self.temp_path, 'offlineDbFile.sqlite', [online_layer.id()]))
119+
offline_layer = list(self.registry.mapLayers().values())[0]
120+
self.assertTrue(offline_layer.hasGeometryType())
121+
self.assertTrue(offline_layer.isValid())
122+
self.assertTrue(offline_layer.name().find('(offline)') > -1)
123+
self.assertEqual(len([f for f in offline_layer.getFeatures()]), len(TEST_FEATURES))
124+
# Edit feature 2
125+
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2'")
126+
self.assertTrue(offline_layer.startEditing())
127+
self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fieldNameIndex('name'), 'name 2 edited'))
128+
self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPoint(QgsPoint(33.0, 60.0))))
129+
self.assertTrue(offline_layer.commitChanges())
130+
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'")
131+
self.assertTrue(ol.isOfflineProject())
132+
# Sync
133+
ol.synchronize()
134+
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
135+
sleep(1)
136+
online_layer = list(self.registry.mapLayers().values())[0]
137+
self.assertTrue(online_layer.isValid())
138+
self.assertFalse(online_layer.name().find('(offline)') > -1)
139+
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES))
140+
# Check that data have changed in the backend (raise exception if not found)
141+
feat2 = self._getFeatureByAttribute(self._getLayer('test_point'), 'name', "'name 2 edited'")
142+
feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2 edited'")
143+
self.assertEqual(feat2.geometry().asPoint().toString(), QgsPoint(33.0, 60.0).toString())
144+
# Check that all other features have not changed
145+
layer = self._getLayer('test_point')
146+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
147+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))
148+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1]))
149+
150+
# Test for regression on double sync (it was a SEGFAULT)
151+
# goes offline
152+
ol = QgsOfflineEditing()
153+
offline_layer = list(self.registry.mapLayers().values())[0]
154+
# Edit feature 2
155+
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'")
156+
self.assertTrue(offline_layer.startEditing())
157+
self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fieldNameIndex('name'), 'name 2'))
158+
self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPoint(TEST_FEATURES[1][2])))
159+
# Edit feat 4
160+
feat4 = self._getFeatureByAttribute(offline_layer, 'name', "'name 4'")
161+
self.assertTrue(offline_layer.changeAttributeValue(feat4.id(), offline_layer.fieldNameIndex('name'), 'name 4 edited'))
162+
self.assertTrue(offline_layer.commitChanges())
163+
# Sync
164+
ol.synchronize()
165+
# Does anybody knows why the sleep is needed? Is that a threaded WFS consequence?
166+
sleep(1)
167+
online_layer = list(self.registry.mapLayers().values())[0]
168+
layer = self._getLayer('test_point')
169+
# Check that data have changed in the backend (raise exception if not found)
170+
feat4 = self._getFeatureByAttribute(layer, 'name', "'name 4 edited'")
171+
feat4 = self._getFeatureByAttribute(online_layer, 'name', "'name 4 edited'")
172+
feat2 = self._getFeatureByAttribute(layer, 'name', "'name 2'")
173+
feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2'")
174+
# Check that all other features have not changed
175+
layer = self._getLayer('test_point')
176+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
177+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
178+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))

tests/src/python/qgis_wrapped_server.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
the Free Software Foundation; either version 2 of the License, or
1111
(at your option) any later version.
1212
"""
13+
from __future__ import print_function
14+
from future import standard_library
15+
standard_library.install_aliases()
1316

1417
__author__ = 'Alessandro Pasotti'
1518
__date__ = '05/15/2016'
@@ -19,8 +22,8 @@
1922

2023

2124
import os
22-
import urlparse
23-
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
25+
import urllib.parse
26+
from http.server import BaseHTTPRequestHandler, HTTPServer
2427
from qgis.server import QgsServer
2528

2629
try:
@@ -32,19 +35,19 @@
3235
class Handler(BaseHTTPRequestHandler):
3336

3437
def do_GET(self):
35-
parsed_path = urlparse.urlparse(self.path)
38+
parsed_path = urllib.parse.urlparse(self.path)
3639
s = QgsServer()
3740
headers, body = s.handleRequest(parsed_path.query)
3841
self.send_response(200)
39-
for k, v in [h.split(':') for h in headers.split('\n') if h]:
42+
for k, v in [h.split(':') for h in headers.decode().split('\n') if h]:
4043
self.send_header(k, v)
4144
self.end_headers()
4245
self.wfile.write(body)
4346
return
4447

4548
def do_POST(self):
46-
content_len = int(self.headers.getheader('content-length', 0))
47-
post_body = self.rfile.read(content_len)
49+
content_len = int(self.headers.get('content-length', 0))
50+
post_body = self.rfile.read(content_len).decode()
4851
request = post_body[1:post_body.find(' ')]
4952
self.path = self.path + '&REQUEST_BODY=' + \
5053
post_body.replace('&amp;', '') + '&REQUEST=' + request
@@ -53,6 +56,6 @@ def do_POST(self):
5356

5457
if __name__ == '__main__':
5558
server = HTTPServer(('localhost', QGIS_SERVER_DEFAULT_PORT), Handler)
56-
print 'Starting server on localhost:%s, use <Ctrl-C> to stop' % \
57-
QGIS_SERVER_DEFAULT_PORT
59+
print('Starting server on localhost:%s, use <Ctrl-C> to stop' %
60+
QGIS_SERVER_DEFAULT_PORT)
5861
server.serve_forever()

0 commit comments

Comments
 (0)