Skip to content

Commit b5c0bc9

Browse files
committedJul 12, 2016
[offline editing] Added insert and update tests
1 parent 2ccc7e1 commit b5c0bc9

File tree

3 files changed

+168
-26
lines changed

3 files changed

+168
-26
lines changed
 

‎tests/src/python/offlineditingtestbase.py

Lines changed: 133 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
# -*- coding: utf-8 -*-
22
"""QGIS Unit test utils for offline editing tests.
33
4+
There are three layers referenced through the code:
5+
6+
- the "online_layer" is the layer being edited online (WFS or PostGIS) layer inside
7+
QGIS client
8+
- the "offline_layer" (SQLite)
9+
- the "layer", is the shapefile layer that is served by QGIS Server WFS, in case of
10+
PostGIS, this will be the same layer referenced by online_layer
11+
12+
Each test simulates one working session.
13+
14+
When testing on PostGIS, the first two layers will be exactly the same object.
15+
416
.. note:: This program is free software; you can redistribute it and/or modify
517
it under the terms of the GNU General Public License as published by
618
the Free Software Foundation; either version 2 of the License, or
@@ -16,6 +28,7 @@
1628
__revision__ = '$Format:%H$'
1729

1830
from time import sleep
31+
import os
1932

2033
from qgis.core import (
2134
QgsFeature,
@@ -37,6 +50,12 @@
3750
(4, 'name 4', QgsPoint(10, 46.5)),
3851
]
3952

53+
# Additional features for insert test
54+
TEST_FEATURES_INSERT = [
55+
(5, 'name 5', QgsPoint(9.7, 45.7)),
56+
(6, 'name 6', QgsPoint(10.6, 46.6)),
57+
]
58+
4059

4160
class OfflineTestBase(object):
4261

@@ -47,22 +66,25 @@ def _setUp(self):
4766
# Setup: create some features for the test layer
4867
features = []
4968
layer = self._getLayer('test_point')
69+
assert layer.startEditing()
5070
for id, name, geom in TEST_FEATURES:
5171
f = QgsFeature(layer.pendingFields())
5272
f['id'] = id
5373
f['name'] = name
5474
f.setGeometry(QgsGeometry.fromPoint(geom))
5575
features.append(f)
56-
layer.dataProvider().addFeatures(features)
57-
# Add the remote layer
76+
layer.addFeatures(features)
77+
assert layer.commitChanges()
78+
# Add the online layer
5879
self.registry = QgsMapLayerRegistry.instance()
5980
self.registry.removeAllMapLayers()
6081
assert self.registry.addMapLayer(self._getOnlineLayer('test_point')) is not None
6182

6283
def _tearDown(self):
6384
"""Called by tearDown: run after each test."""
64-
# Clear test layers
65-
self._clearLayer('test_point')
85+
# Delete the sqlite db
86+
#os.unlink(os.path.join(self.temp_path, 'offlineDbFile.sqlite'))
87+
pass
6688

6789
@classmethod
6890
def _compareFeature(cls, layer, attributes):
@@ -71,11 +93,10 @@ def _compareFeature(cls, layer, attributes):
7193
return f['name'] == attributes[1] and f.geometry().asPoint().toString() == attributes[2].toString()
7294

7395
@classmethod
74-
def _clearLayer(cls, layer_name):
96+
def _clearLayer(cls, layer):
7597
"""
76-
Delete all features from the backend layer
98+
Delete all features from the given layer
7799
"""
78-
layer = cls._getLayer(layer_name)
79100
layer.startEditing()
80101
layer.deleteFeatures([f.id() for f in layer.getFeatures()])
81102
layer.commitChanges()
@@ -108,19 +129,26 @@ def _getFeatureByAttribute(cls, layer, attr_name, attr_value):
108129
raise Exception("Wrong attributes in WFS layer %s" %
109130
layer.name())
110131

111-
def test_offlineConversion(self):
132+
def _testInit(self):
133+
"""
134+
Preliminary checks for each test
135+
"""
112136
# goes offline
113137
ol = QgsOfflineEditing()
114138
online_layer = list(self.registry.mapLayers().values())[0]
115139
self.assertTrue(online_layer.hasGeometryType())
116-
# Check we have 3 features
140+
# Check we have features
117141
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES))
118142
self.assertTrue(ol.convertToOfflineProject(self.temp_path, 'offlineDbFile.sqlite', [online_layer.id()]))
119143
offline_layer = list(self.registry.mapLayers().values())[0]
120144
self.assertTrue(offline_layer.hasGeometryType())
121145
self.assertTrue(offline_layer.isValid())
122146
self.assertTrue(offline_layer.name().find('(offline)') > -1)
123147
self.assertEqual(len([f for f in offline_layer.getFeatures()]), len(TEST_FEATURES))
148+
return ol, offline_layer
149+
150+
def test_updateFeatures(self):
151+
ol, offline_layer = self._testInit()
124152
# Edit feature 2
125153
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2'")
126154
self.assertTrue(offline_layer.startEditing())
@@ -131,8 +159,8 @@ def test_offlineConversion(self):
131159
self.assertTrue(ol.isOfflineProject())
132160
# Sync
133161
ol.synchronize()
162+
sleep(2)
134163
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
135-
sleep(1)
136164
online_layer = list(self.registry.mapLayers().values())[0]
137165
self.assertTrue(online_layer.isValid())
138166
self.assertFalse(online_layer.name().find('(offline)') > -1)
@@ -176,3 +204,98 @@ def test_offlineConversion(self):
176204
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
177205
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
178206
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))
207+
208+
def test_deleteOneFeature(self):
209+
"""
210+
Delete a single feature
211+
"""
212+
ol, offline_layer = self._testInit()
213+
# Delete feature 3
214+
feat3 = self._getFeatureByAttribute(offline_layer, 'name', "'name 3'")
215+
self.assertTrue(offline_layer.startEditing())
216+
self.assertTrue(offline_layer.deleteFeatures([feat3.id()]))
217+
self.assertTrue(offline_layer.commitChanges())
218+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(offline_layer, 'name', "'name 3'"))
219+
self.assertTrue(ol.isOfflineProject())
220+
# Sync
221+
ol.synchronize()
222+
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
223+
sleep(1)
224+
online_layer = list(self.registry.mapLayers().values())[0]
225+
self.assertTrue(online_layer.isValid())
226+
self.assertFalse(online_layer.name().find('(offline)') > -1)
227+
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES) - 1)
228+
# Check that data have changed in the backend (raise exception if not found)
229+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(online_layer, 'name', "'name 3'"))
230+
# Check that all other features have not changed
231+
layer = self._getLayer('test_point')
232+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
233+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
234+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1]))
235+
236+
def test_deleteMultipleFeatures(self):
237+
"""
238+
Delete a multiple features
239+
"""
240+
ol, offline_layer = self._testInit()
241+
# Delete feature 1 and 3
242+
feat1 = self._getFeatureByAttribute(offline_layer, 'name', "'name 1'")
243+
feat3 = self._getFeatureByAttribute(offline_layer, 'name', "'name 3'")
244+
self.assertTrue(offline_layer.startEditing())
245+
self.assertTrue(offline_layer.deleteFeatures([feat3.id(), feat1.id()]))
246+
self.assertTrue(offline_layer.commitChanges())
247+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(offline_layer, 'name', "'name 3'"))
248+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(offline_layer, 'name', "'name 1'"))
249+
self.assertTrue(ol.isOfflineProject())
250+
# Sync
251+
ol.synchronize()
252+
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
253+
sleep(1)
254+
online_layer = list(self.registry.mapLayers().values())[0]
255+
self.assertTrue(online_layer.isValid())
256+
self.assertFalse(online_layer.name().find('(offline)') > -1)
257+
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES) - 2)
258+
# Check that data have changed in the backend (raise exception if not found)
259+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(online_layer, 'name', "'name 3'"))
260+
self.assertRaises(Exception, lambda: self._getFeatureByAttribute(online_layer, 'name', "'name 1'"))
261+
# Check that all other features have not changed
262+
layer = self._getLayer('test_point')
263+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
264+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1]))
265+
266+
def test_InsertFeatures(self):
267+
"""
268+
Insert multiple features
269+
"""
270+
ol, offline_layer = self._testInit()
271+
# Insert feature 5 and 6
272+
self.assertTrue(offline_layer.startEditing())
273+
features = []
274+
for id, name, geom in TEST_FEATURES_INSERT:
275+
f = QgsFeature(offline_layer.pendingFields())
276+
f['id'] = id
277+
f['name'] = name
278+
f.setGeometry(QgsGeometry.fromPoint(geom))
279+
features.append(f)
280+
offline_layer.addFeatures(features)
281+
self.assertTrue(offline_layer.commitChanges())
282+
self._getFeatureByAttribute(offline_layer, 'name', "'name 5'")
283+
self._getFeatureByAttribute(offline_layer, 'name', "'name 6'")
284+
self.assertTrue(ol.isOfflineProject())
285+
# Sync
286+
ol.synchronize()
287+
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
288+
sleep(1)
289+
online_layer = list(self.registry.mapLayers().values())[0]
290+
self.assertTrue(online_layer.isValid())
291+
self.assertFalse(online_layer.name().find('(offline)') > -1)
292+
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES) + 2)
293+
# Check that data have changed in the backend (raise exception if not found)
294+
self._getFeatureByAttribute(online_layer, 'name', "'name 5'")
295+
self._getFeatureByAttribute(online_layer, 'name', "'name 6'")
296+
# Check that all other features have not changed
297+
layer = self._getLayer('test_point')
298+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
299+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
300+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))
301+
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1]))

‎tests/src/python/qgis_wrapped_server.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,26 @@
3131
except KeyError:
3232
QGIS_SERVER_DEFAULT_PORT = 8081
3333

34+
qgs_server = QgsServer()
35+
3436

3537
class Handler(BaseHTTPRequestHandler):
3638

3739
def do_GET(self):
40+
# CGI vars:
41+
for k, v in self.headers.items():
42+
qgs_server.putenv('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v)
43+
qgs_server.putenv('SERVER_PORT', str(self.server.server_port))
44+
qgs_server.putenv('SERVER_NAME', self.server.server_name)
45+
qgs_server.putenv('REQUEST_URI', self.path)
3846
parsed_path = urllib.parse.urlparse(self.path)
39-
s = QgsServer()
40-
headers, body = s.handleRequest(parsed_path.query)
41-
self.send_response(200)
42-
for k, v in [h.split(':') for h in headers.decode().split('\n') if h]:
47+
headers, body = qgs_server.handleRequest(parsed_path.query)
48+
headers_dict = dict(h.split(': ', 1) for h in headers.decode().split('\n') if h)
49+
try:
50+
self.send_response(int(headers_dict['Status'].split(' ')[0]))
51+
except:
52+
self.send_response(200)
53+
for k, v in headers_dict.items():
4354
self.send_header(k, v)
4455
self.end_headers()
4556
self.wfile.write(body)

‎tests/src/python/test_offline_editing_wfs.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161

6262
class TestWFST(unittest.TestCase, OfflineTestBase):
6363

64+
# To fake the WFS cache!
65+
counter = 0
66+
6467
@classmethod
6568
def setUpClass(cls):
6669
"""Run before all tests"""
@@ -82,47 +85,52 @@ def setUpClass(cls):
8285
except KeyError:
8386
pass
8487
# Clear all test layers
85-
cls._clearLayer('test_point')
88+
cls._clearLayer(cls._getLayer('test_point'))
8689
os.environ['QGIS_SERVER_DEFAULT_PORT'] = str(cls.port)
87-
server_path = os.path.dirname(os.path.realpath(__file__)) + \
90+
cls.server_path = os.path.dirname(os.path.realpath(__file__)) + \
8891
'/qgis_wrapped_server.py'
89-
cls.server = subprocess.Popen([sys.executable, server_path],
90-
env=os.environ)
91-
sleep(2)
9292

9393
@classmethod
9494
def tearDownClass(cls):
9595
"""Run after all tests"""
96-
cls.server.terminate()
97-
del cls.server
98-
# Clear test layer
99-
cls._clearLayer('test_point')
10096
rmtree(cls.temp_path)
10197

10298
def setUp(self):
10399
"""Run before each test."""
100+
self.server = subprocess.Popen([sys.executable, self.server_path],
101+
env=os.environ)
102+
sleep(2)
104103
self._setUp()
105104

106105
def tearDown(self):
107106
"""Run after each test."""
107+
# Clear test layer
108+
self._clearLayer(self._getOnlineLayer('test_point'))
109+
# Kill the server
110+
self.server.terminate()
111+
del self.server
112+
# Delete the sqlite db
113+
os.unlink(os.path.join(self.temp_path, 'offlineDbFile.sqlite'))
108114
self._tearDown()
109115

110116
@classmethod
111117
def _getOnlineLayer(cls, type_name, layer_name=None):
112118
"""
113-
Layer factory (return the online layer), provider specific
119+
Return a new WFS layer, overriding the WFS cache
114120
"""
115121
if layer_name is None:
116122
layer_name = 'wfs_' + type_name
117123
parms = {
118124
'srsname': 'EPSG:4326',
119125
'typename': type_name,
120-
'url': 'http://127.0.0.1:%s/?map=%s' % (cls.port,
121-
cls.project_path),
126+
'url': 'http://127.0.0.1:%s/%s/?map=%s' % (cls.port,
127+
cls.counter,
128+
cls.project_path),
122129
'version': 'auto',
123130
'table': '',
124131
#'sql': '',
125132
}
133+
cls.counter += 1
126134
uri = ' '.join([("%s='%s'" % (k, v)) for k, v in parms.items()])
127135
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
128136
assert wfs_layer.isValid()

0 commit comments

Comments
 (0)
Please sign in to comment.