Skip to content

Commit

Permalink
Added offline editing WFS tests and debug info
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso committed Jul 4, 2016
1 parent 1316d9c commit a058c36
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 14 deletions.
22 changes: 20 additions & 2 deletions src/core/qgsofflineediting.cpp
Expand Up @@ -205,7 +205,9 @@ void QgsOfflineEditing::synchronize()
// open logging db
sqlite3* db = openLoggingDb();
if ( !db )
{
return;
}

emit progressStarted();

Expand All @@ -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];
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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() );
Expand All @@ -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();
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -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)
178 changes: 178 additions & 0 deletions tests/src/python/offlineditingtestbase.py
@@ -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]))
19 changes: 11 additions & 8 deletions tests/src/python/qgis_wrapped_server.py
Expand Up @@ -10,6 +10,9 @@
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'
Expand All @@ -19,8 +22,8 @@


import os
import urlparse
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from qgis.server import QgsServer

try:
Expand All @@ -32,19 +35,19 @@
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
Expand All @@ -53,6 +56,6 @@ def do_POST(self):

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()

0 comments on commit a058c36

Please sign in to comment.