Added offline editing WFS tests and debug info

This commit is contained in:
Alessandro Pasotti 2016-06-30 09:29:41 +02:00
parent 1316d9c72c
commit a058c36bce
6 changed files with 357 additions and 14 deletions

View File

@ -205,7 +205,9 @@ void QgsOfflineEditing::synchronize()
// open logging db
sqlite3* db = openLoggingDb();
if ( !db )
{
return;
}
emit progressStarted();
@ -221,6 +223,7 @@ void QgsOfflineEditing::synchronize()
}
}
QgsDebugMsgLevel( QString( "Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
for ( int l = 0; l < offlineLayers.count(); l++ )
{
QgsMapLayer* layer = offlineLayers[l];
@ -265,8 +268,10 @@ void QgsOfflineEditing::synchronize()
// TODO: only get commitNos of this layer?
int commitNo = getCommitNo( db );
QgsDebugMsgLevel( QString( "Found %1 commits" ).arg( commitNo ), 4 );
for ( int i = 0; i < commitNo; i++ )
{
QgsDebugMsgLevel( "Apply commits chronologically", 4 );
// apply commits chronologically
applyAttributesAdded( remoteLayer, db, layerId, i );
applyAttributeValueChanges( offlineLayer, remoteLayer, db, layerId, i );
@ -302,6 +307,10 @@ void QgsOfflineEditing::synchronize()
showWarning( remoteLayer->commitErrors().join( "\n" ) );
}
}
else
{
QgsDebugMsg( "Could not find the layer id in the edit logs!" );
}
// Invalidate the connection to force a reload if the project is put offline
// again with the same path
offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceURI( offlineLayer->source() ).database() );
@ -317,6 +326,10 @@ void QgsOfflineEditing::synchronize()
QgsProject::instance()->removeEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH );
remoteLayer->reload(); //update with other changes
}
else
{
QgsDebugMsg( "Remote layer is not valid!" );
}
}
emit progressStopped();
@ -477,7 +490,7 @@ QgsVectorLayer* QgsOfflineEditing::copyVectorLayer( QgsVectorLayer* layer, sqlit
return nullptr;
QString tableName = layer->id();
QgsDebugMsg( QString( "Creating offline table %1 ..." ).arg( tableName ) );
QgsDebugMsgLevel( QString( "Creating offline table %1 ..." ).arg( tableName ), 4 );
// create table
QString sql = QString( "CREATE TABLE '%1' (" ).arg( tableName );
@ -817,7 +830,7 @@ void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer* offlineLayer
for ( int i = 0; i < values.size(); i++ )
{
QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
QgsDebugMsgLevel( QString( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
emit progressUpdated( i + 1 );
@ -932,11 +945,16 @@ sqlite3* QgsOfflineEditing::openLoggingDb()
int rc = sqlite3_open( dbPath.toUtf8().constData(), &db );
if ( rc != SQLITE_OK )
{
QgsDebugMsg( "Could not open the spatialite logging database" );
showWarning( tr( "Could not open the spatialite logging database" ) );
sqlite3_close( db );
db = nullptr;
}
}
else
{
QgsDebugMsg( "dbPath is empty!" );
}
return db;
}

View File

@ -132,4 +132,5 @@ IF (WITH_SERVER)
ADD_PYTHON_TEST(PyQgsServer test_qgsserver.py)
ADD_PYTHON_TEST(PyQgsServerAccessControl test_qgsserver_accesscontrol.py)
ADD_PYTHON_TEST(PyQgsServerWFST test_qgsserver_wfst.py)
ADD_PYTHON_TEST(PyQgsOfflineEditingWFS test_offline_editing_wfs.py)
ENDIF (WITH_SERVER)

View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
"""QGIS Unit test utils for offline editing tests.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
from __future__ import print_function
from builtins import str
from builtins import object
__author__ = 'Alessandro Pasotti'
__date__ = '2016-06-30'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
from time import sleep
from qgis.core import (
QgsFeature,
QgsGeometry,
QgsPoint,
QgsFeatureRequest,
QgsExpression,
QgsMapLayerRegistry,
QgsOfflineEditing,
)
# Tet features, fields: [id, name, geometry]
# "id" is used as a pk to retriev features by attribute
TEST_FEATURES = [
(1, 'name 1', QgsPoint(9, 45)),
(2, 'name 2', QgsPoint(9.5, 45.5)),
(3, 'name 3', QgsPoint(9.5, 46)),
(4, 'name 4', QgsPoint(10, 46.5)),
]
class OfflineTestBase(object):
"""Generic test methods for all online providers"""
def _setUp(self):
"""Called by setUp: run before each test."""
# Setup: create some features for the test layer
features = []
layer = self._getLayer('test_point')
for id, name, geom in TEST_FEATURES:
f = QgsFeature(layer.pendingFields())
f['id'] = id
f['name'] = name
f.setGeometry(QgsGeometry.fromPoint(geom))
features.append(f)
layer.dataProvider().addFeatures(features)
# Add the remote layer
self.registry = QgsMapLayerRegistry.instance()
self.registry.removeAllMapLayers()
assert self.registry.addMapLayer(self._getOnlineLayer('test_point')) is not None
def _tearDown(self):
"""Called by tearDown: run after each test."""
# Clear test layers
self._clearLayer('test_point')
@classmethod
def _compareFeature(cls, layer, attributes):
"""Compare id, name and geometry"""
f = cls._getFeatureByAttribute(layer, 'id', attributes[0])
return f['name'] == attributes[1] and f.geometry().asPoint().toString() == attributes[2].toString()
@classmethod
def _clearLayer(cls, layer_name):
"""
Delete all features from the backend layer
"""
layer = cls._getLayer(layer_name)
layer.startEditing()
layer.deleteFeatures([f.id() for f in layer.getFeatures()])
layer.commitChanges()
assert layer.featureCount() == 0
@classmethod
def _getLayer(cls, layer_name):
"""
Layer factory (return the backend layer), provider specific
"""
raise NotImplementedError
@classmethod
def _getOnlineLayer(cls, type_name, layer_name=None):
"""
Layer factory (return the online layer), provider specific
"""
raise NotImplementedError
@classmethod
def _getFeatureByAttribute(cls, layer, attr_name, attr_value):
"""
Find the feature and return it, raise exception if not found
"""
request = QgsFeatureRequest(QgsExpression("%s=%s" % (attr_name,
attr_value)))
try:
return next(layer.dataProvider().getFeatures(request))
except StopIteration:
raise Exception("Wrong attributes in WFS layer %s" %
layer.name())
def test_offlineConversion(self):
# goes offline
ol = QgsOfflineEditing()
online_layer = list(self.registry.mapLayers().values())[0]
self.assertTrue(online_layer.hasGeometryType())
# Check we have 3 features
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES))
self.assertTrue(ol.convertToOfflineProject(self.temp_path, 'offlineDbFile.sqlite', [online_layer.id()]))
offline_layer = list(self.registry.mapLayers().values())[0]
self.assertTrue(offline_layer.hasGeometryType())
self.assertTrue(offline_layer.isValid())
self.assertTrue(offline_layer.name().find('(offline)') > -1)
self.assertEqual(len([f for f in offline_layer.getFeatures()]), len(TEST_FEATURES))
# Edit feature 2
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2'")
self.assertTrue(offline_layer.startEditing())
self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fieldNameIndex('name'), 'name 2 edited'))
self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPoint(QgsPoint(33.0, 60.0))))
self.assertTrue(offline_layer.commitChanges())
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'")
self.assertTrue(ol.isOfflineProject())
# Sync
ol.synchronize()
# Does anybody know why the sleep is needed? Is that a threaded WFS consequence?
sleep(1)
online_layer = list(self.registry.mapLayers().values())[0]
self.assertTrue(online_layer.isValid())
self.assertFalse(online_layer.name().find('(offline)') > -1)
self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES))
# Check that data have changed in the backend (raise exception if not found)
feat2 = self._getFeatureByAttribute(self._getLayer('test_point'), 'name', "'name 2 edited'")
feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2 edited'")
self.assertEqual(feat2.geometry().asPoint().toString(), QgsPoint(33.0, 60.0).toString())
# Check that all other features have not changed
layer = self._getLayer('test_point')
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1]))
# Test for regression on double sync (it was a SEGFAULT)
# goes offline
ol = QgsOfflineEditing()
offline_layer = list(self.registry.mapLayers().values())[0]
# Edit feature 2
feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'")
self.assertTrue(offline_layer.startEditing())
self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fieldNameIndex('name'), 'name 2'))
self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPoint(TEST_FEATURES[1][2])))
# Edit feat 4
feat4 = self._getFeatureByAttribute(offline_layer, 'name', "'name 4'")
self.assertTrue(offline_layer.changeAttributeValue(feat4.id(), offline_layer.fieldNameIndex('name'), 'name 4 edited'))
self.assertTrue(offline_layer.commitChanges())
# Sync
ol.synchronize()
# Does anybody knows why the sleep is needed? Is that a threaded WFS consequence?
sleep(1)
online_layer = list(self.registry.mapLayers().values())[0]
layer = self._getLayer('test_point')
# Check that data have changed in the backend (raise exception if not found)
feat4 = self._getFeatureByAttribute(layer, 'name', "'name 4 edited'")
feat4 = self._getFeatureByAttribute(online_layer, 'name', "'name 4 edited'")
feat2 = self._getFeatureByAttribute(layer, 'name', "'name 2'")
feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2'")
# Check that all other features have not changed
layer = self._getLayer('test_point')
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1]))
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1]))
self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))

View File

@ -10,6 +10,9 @@ it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
__author__ = 'Alessandro Pasotti'
__date__ = '05/15/2016'
@ -19,8 +22,8 @@ __revision__ = '$Format:%H$'
import os
import urlparse
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from qgis.server import QgsServer
try:
@ -32,19 +35,19 @@ except KeyError:
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_path = urlparse.urlparse(self.path)
parsed_path = urllib.parse.urlparse(self.path)
s = QgsServer()
headers, body = s.handleRequest(parsed_path.query)
self.send_response(200)
for k, v in [h.split(':') for h in headers.split('\n') if h]:
for k, v in [h.split(':') for h in headers.decode().split('\n') if h]:
self.send_header(k, v)
self.end_headers()
self.wfile.write(body)
return
def do_POST(self):
content_len = int(self.headers.getheader('content-length', 0))
post_body = self.rfile.read(content_len)
content_len = int(self.headers.get('content-length', 0))
post_body = self.rfile.read(content_len).decode()
request = post_body[1:post_body.find(' ')]
self.path = self.path + '&REQUEST_BODY=' + \
post_body.replace('&amp;', '') + '&REQUEST=' + request
@ -53,6 +56,6 @@ class Handler(BaseHTTPRequestHandler):
if __name__ == '__main__':
server = HTTPServer(('localhost', QGIS_SERVER_DEFAULT_PORT), Handler)
print 'Starting server on localhost:%s, use <Ctrl-C> to stop' % \
QGIS_SERVER_DEFAULT_PORT
print('Starting server on localhost:%s, use <Ctrl-C> to stop' %
QGIS_SERVER_DEFAULT_PORT)
server.serve_forever()

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
"""
Offline editing Tests.
WFS-T tests need using QGIS Server through
qgis_wrapped_server.py.
This is an integration test for QGIS Desktop WFS-T provider and QGIS Server
WFS-T that check if QGIS offline editing works with a WFS-T endpoint.
The test uses testdata/wfs_transactional/wfs_transactional.qgs and three
initially empty shapefiles layers with points, lines and polygons.
The point layer is used in the test
From build dir, run: ctest -R PyQgsOfflineEditingWFS -V
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
from builtins import str
__author__ = 'Alessandro Pasotti'
__date__ = '05/15/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import os
import sys
import subprocess
from shutil import copytree, rmtree
import tempfile
from time import sleep
from utilities import unitTestDataPath
from qgis.core import (
QgsVectorLayer,
QgsProject,
)
from qgis.testing import (
start_app,
unittest,
)
from offlineditingtestbase import OfflineTestBase
from qgis.PyQt.QtCore import QFileInfo
try:
QGIS_SERVER_WFST_DEFAULT_PORT = os.environ['QGIS_SERVER_WFST_DEFAULT_PORT']
except:
QGIS_SERVER_WFST_DEFAULT_PORT = 8081
qgis_app = start_app()
class TestWFST(unittest.TestCase, OfflineTestBase):
@classmethod
def setUpClass(cls):
"""Run before all tests"""
cls.port = QGIS_SERVER_WFST_DEFAULT_PORT
# Create tmp folder
cls.temp_path = tempfile.mkdtemp()
cls.testdata_path = cls.temp_path + '/' + 'wfs_transactional' + '/'
copytree(unitTestDataPath('wfs_transactional') + '/',
cls.temp_path + '/' + 'wfs_transactional')
cls.project_path = cls.temp_path + '/' + 'wfs_transactional' + '/' + \
'wfs_transactional.qgs'
assert os.path.exists(cls.project_path), "Project not found: %s" % \
cls.project_path
# Clean env just to be sure
env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE']
for ev in env_vars:
try:
del os.environ[ev]
except KeyError:
pass
# Clear all test layers
cls._clearLayer('test_point')
os.environ['QGIS_SERVER_DEFAULT_PORT'] = str(cls.port)
server_path = os.path.dirname(os.path.realpath(__file__)) + \
'/qgis_wrapped_server.py'
cls.server = subprocess.Popen([sys.executable, server_path],
env=os.environ)
sleep(2)
@classmethod
def tearDownClass(cls):
"""Run after all tests"""
cls.server.terminate()
del cls.server
# Clear test layer
cls._clearLayer('test_point')
rmtree(cls.temp_path)
def setUp(self):
"""Run before each test."""
self._setUp()
def tearDown(self):
"""Run after each test."""
self._tearDown()
@classmethod
def _getOnlineLayer(cls, type_name, layer_name=None):
"""
Layer factory (return the online layer), provider specific
"""
if layer_name is None:
layer_name = 'wfs_' + type_name
parms = {
'srsname': 'EPSG:4326',
'typename': type_name,
'url': 'http://127.0.0.1:%s/?map=%s' % (cls.port,
cls.project_path),
'version': 'auto',
'table': '',
#'sql': '',
}
uri = ' '.join([("%s='%s'" % (k, v)) for k, v in parms.items()])
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
assert wfs_layer.isValid()
return wfs_layer
@classmethod
def _getLayer(cls, layer_name):
"""
Layer factory (return the backend layer), provider specific
"""
path = cls.testdata_path + layer_name + '.shp'
layer = QgsVectorLayer(path, layer_name, "ogr")
assert layer.isValid()
return layer
if __name__ == '__main__':
unittest.main()

View File

@ -35,6 +35,7 @@ __revision__ = '$Format:%H$'
import os
import sys
import subprocess
from shutil import copytree, rmtree
import tempfile
@ -69,7 +70,6 @@ class TestWFST(unittest.TestCase):
def setUpClass(cls):
"""Run before all tests"""
cls.port = QGIS_SERVER_WFST_DEFAULT_PORT
cls.testdata_path = unitTestDataPath('wfs_transactional') + '/'
# Create tmp folder
cls.temp_path = tempfile.mkdtemp()
cls.testdata_path = cls.temp_path + '/' + 'wfs_transactional' + '/'
@ -92,7 +92,7 @@ class TestWFST(unittest.TestCase):
os.environ['QGIS_SERVER_DEFAULT_PORT'] = str(cls.port)
server_path = os.path.dirname(os.path.realpath(__file__)) + \
'/qgis_wrapped_server.py'
cls.server = subprocess.Popen(['python', server_path],
cls.server = subprocess.Popen([sys.executable, server_path],
env=os.environ)
sleep(2)
@ -152,7 +152,7 @@ class TestWFST(unittest.TestCase):
'table': '',
#'sql': '',
}
uri = ' '.join([("%s='%s'" % (k, v)) for k, v in parms.iteritems()])
uri = ' '.join([("%s='%s'" % (k, v)) for k, v in parms.items()])
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
assert wfs_layer.isValid()
return wfs_layer
@ -165,7 +165,7 @@ class TestWFST(unittest.TestCase):
request = QgsFeatureRequest(QgsExpression("%s=%s" % (attr_name,
attr_value)))
try:
return layer.dataProvider().getFeatures(request).next()
return next(layer.dataProvider().getFeatures(request))
except StopIteration:
raise Exception("Wrong attributes in WFS layer %s" %
layer.name())