Skip to content

Commit

Permalink
Add qgis.testing module for generic qgis test helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
m-kuhn committed Feb 4, 2016
1 parent a3d3ffd commit c21889f
Show file tree
Hide file tree
Showing 79 changed files with 871 additions and 620 deletions.
1 change: 1 addition & 0 deletions ci/travis/linux/before_install.sh
Expand Up @@ -61,3 +61,4 @@ sudo apt-get install --force-yes --no-install-recommends --no-install-suggests \
postgresql-9.1-postgis-2.1/precise # from ubuntugis-unstable, not pgdg

sudo -H pip install autopep8 # TODO when switching to trusty or above: replace python-pip with python-autopep8
sudo -H pip install nose2 pyyaml mock
2 changes: 1 addition & 1 deletion ci/travis/osx/before_install.sh
Expand Up @@ -18,4 +18,4 @@ mkdir -p /Users/travis/Library/Python/2.7/lib/python/site-packages
echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> /Users/travis/Library/Python/2.7/lib/python/site-packages/homebrew.pth

# Needed for Processing
pip install psycopg2 numpy
pip install psycopg2 numpy nose2 pyyaml mock
1 change: 1 addition & 0 deletions python/CMakeLists.txt
Expand Up @@ -59,6 +59,7 @@ ADD_SUBDIRECTORY(console)
ADD_SUBDIRECTORY(PyQt)
ADD_SUBDIRECTORY(pyplugin_installer)
ADD_SUBDIRECTORY(ext-libs)
ADD_SUBDIRECTORY(testing)

IF(POLICY CMP0040)
CMAKE_POLICY (POP) # see PUSH above
Expand Down
24 changes: 24 additions & 0 deletions python/testing/CMakeLists.txt
@@ -0,0 +1,24 @@
# See ../CMakeLists.txt for info on staged-plugins* and clean-staged-plugins targets

SET (QGIS_PYTHON_DIR ${QGIS_DATA_DIR}/python)
SET (PYTHON_OUTPUT_DIRECTORY ${QGIS_OUTPUT_DIRECTORY}/python)

SET(PY_FILES
__init__.py
mocked.py
)

FILE (MAKE_DIRECTORY ${QGIS_PYTHON_OUTPUT_DIRECTORY}/testing)
INSTALL(FILES ${PY_FILES} DESTINATION "${QGIS_PYTHON_DIR}/testing")

ADD_CUSTOM_TARGET(pytesting ALL)
# stage to output to make available when QGIS is run from build directory
FOREACH(pyfile ${PY_FILES})
ADD_CUSTOM_COMMAND(TARGET pytesting
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy ${pyfile} "${QGIS_PYTHON_OUTPUT_DIRECTORY}/testing"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${pyfile}
)
PY_COMPILE(pyutils "${QGIS_PYTHON_OUTPUT_DIRECTORY}/testing/${pyfile}")
ENDFOREACH(pyfile)
197 changes: 197 additions & 0 deletions python/testing/__init__.py
@@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
__init__.py
---------------------
Date : January 2016
Copyright : (C) 2016 by Matthias Kuhn
Email : matthias@opengis.ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************
"""

__author__ = 'Matthias Kuhn'
__date__ = 'January 2016'
__copyright__ = '(C) 2016, Matthias Kuhn'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = ':%H$'

import os
import sys

from PyQt4.QtCore import QVariant
from qgis.core import QgsApplication, QgsFeatureRequest, QgsVectorLayer
from nose2.compat import unittest

# Get a backup, we will patch this one later
_TestCase = unittest.TestCase


class TestCase(_TestCase):

def assertLayersEqual(self, layer1, layer2, **kwargs):
"""
:param layer1: The first layer to compare
:param layer2: The second layer to compare
:param request: Optional, A feature request. This can be used to specify
an order by clause to make sure features are compared in
a given sequence if they don't match by default.
"""

try:
request = kwargs['request']
except KeyError:
request = QgsFeatureRequest()

try:
compare = kwargs['compare']
except KeyError:
compare = {}

# Compare fields
_TestCase.assertEqual(self, layer1.fields().count(), layer2.fields().count())
for fieldnum in range(layer1.fields().count()):
field1 = layer1.fields().at(fieldnum)
field2 = layer2.fields().at(fieldnum)
_TestCase.assertEqual(self, field1.name(), field2.name())
# _TestCase.assertEqual(self, field1.type(), field2.type(), 'Field "{}" is not equal: {}({}) != {}({})'.format(field1.name(), field1.typeName(), field1.type(), field2.typeName(), field2.type()))

# Compare CRS
_TestCase.assertEqual(self, layer1.dataProvider().crs().authid(), layer2.dataProvider().crs().authid())

# Compare features
_TestCase.assertEqual(self, layer1.featureCount(), layer2.featureCount())

try:
precision = compare['geometry']['precision']
except KeyError:
precision = 17

for feats in zip(layer1.getFeatures(request), layer2.getFeatures(request)):
if feats[0].geometry() is not None:
geom0 = feats[0].geometry().geometry().asWkt(precision)
else:
geom0 = None
if feats[1].geometry() is not None:
geom1 = feats[1].geometry().geometry().asWkt(precision)
else:
geom1 = None
_TestCase.assertEqual(
self,
geom0,
geom1,
'Features {}/{} differ in geometry: \n\n {}\n\n vs \n\n {}'.format(
feats[0].id(),
feats[1].id(),
geom0,
geom1
)
)

for attr0, attr1, field1, field2 in zip(feats[0].attributes(), feats[1].attributes(), layer1.fields().toList(), layer2.fields().toList()):
try:
cmp = compare['fields'][field1.name()]
except KeyError:
try:
cmp = compare['fields']['__all__']
except KeyError:
cmp = {}

# Skip field
if 'skip' in cmp:
continue

# Cast field to a given type
if 'cast' in cmp:
if cmp['cast'] == 'int':
attr0 = int(attr0) if attr0 else None
attr1 = int(attr1) if attr0 else None
if cmp['cast'] == 'float':
attr0 = float(attr0) if attr0 else None
attr1 = float(attr1) if attr0 else None
if cmp['cast'] == 'str':
attr0 = str(attr0)
attr1 = str(attr1)

# Round field (only numeric so it works with __all__)
if 'precision' in cmp and field1.type() in [QVariant.Int, QVariant.Double, QVariant.LongLong]:
attr0 = round(attr0, cmp['precision'])
attr1 = round(attr1, cmp['precision'])

_TestCase.assertEqual(
self,
attr0,
attr1,
'Features {}/{} differ in attributes\n\n * Field1: {} ({})\n * Field2: {} ({})\n\n * {} != {}'.format(feats[0].id(),
feats[1].id(),
field1.name(),
field1.typeName(),
field2.name(),
field2.typeName(),
repr(attr0),
repr(attr1)
)
)

# Patch unittest
unittest.TestCase = TestCase


def start_app():
"""
Will start a QgsApplication and call all initialization code like
registering the providers and other infrastructure. It will not load
any plugins.
You can always get the reference to a running app by calling `QgsApplication.instance()`.
The initialization will only happen once, so it is safe to call this method repeatedly.
Returns
-------
QgsApplication
A QgsApplication singleton
"""
global QGISAPP

try:
QGISAPP
except NameError:
myGuiFlag = True # All test will run qgis in gui mode

# In python3 we need to convert to a bytes object (or should
# QgsApplication accept a QString instead of const char* ?)
try:
argvb = list(map(os.fsencode, sys.argv))
except AttributeError:
argvb = sys.argv

# Note: QGIS_PREFIX_PATH is evaluated in QgsApplication -
# no need to mess with it here.
QGISAPP = QgsApplication(argvb, myGuiFlag)

QGISAPP.initQgis()
s = QGISAPP.showSettings()
print(s)

return QGISAPP


def stop_app():
"""
Cleans up and exits QGIS
"""
global QGISAPP

QGISAPP.exitQgis()
del QGISAPP
67 changes: 67 additions & 0 deletions python/testing/mocked.py
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
mocked
---------------------
Date : January 2016
Copyright : (C) 2016 by Matthias Kuhn
Email : matthias@opengis.ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************
"""

__author__ = 'Matthias Kuhn'
__date__ = 'January 2016'
__copyright__ = '(C) 2016, Matthias Kuhn'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = ':%H$'

import os
import sys
import mock

from qgis.gui import QgisInterface, QgsMapCanvas
from qgis.core import QgsApplication

from PyQt4.QtGui import QMainWindow
from PyQt4.QtCore import QSize

from qgis.testing import start_app


def get_iface():
"""
Will return a mock QgisInterface object with some methods implemented in a generic way.
You can further control its behavior
by using the mock infrastructure. Refer to https://docs.python.org/3/library/unittest.mock.html
for more details.
Returns
-------
QgisInterface
A mock QgisInterface
"""

start_app()

my_iface = mock.Mock(spec=QgisInterface)

my_iface.mainWindow.return_value = QMainWindow()

canvas = QgsMapCanvas(my_iface.mainWindow())
canvas.resize(QSize(400, 400))

my_iface.mapCanvas.return_value = canvas

return my_iface
39 changes: 27 additions & 12 deletions tests/src/python/test_provider_memory.py
Expand Up @@ -17,23 +17,37 @@
import shutil
import glob

from qgis.core import QGis, QgsField, QgsPoint, QgsMapLayer, QgsVectorLayer, QgsFeatureRequest, QgsFeature, QgsProviderRegistry, \
QgsGeometry, NULL
from PyQt4.QtCore import QSettings
from utilities import (unitTestDataPath,
getQgisTestApp,
unittest,
TestCase,
compareWkt
)
from qgis.core import (
QGis,
QgsField,
QgsPoint,
QgsMapLayer,
QgsVectorLayer,
QgsFeatureRequest,
QgsFeature,
QgsProviderRegistry,
QgsGeometry,
NULL
)

from qgis.testing import (
start_app,
unittest
)

from utilities import (
unitTestDataPath,
compareWkt
)

from providertestbase import ProviderTestCase
from PyQt4.QtCore import QVariant

QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
start_app()
TEST_DATA_DIR = unitTestDataPath()


class TestPyQgsMemoryProvider(TestCase, ProviderTestCase):
class TestPyQgsMemoryProvider(unittest.TestCase, ProviderTestCase):

@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -241,7 +255,8 @@ def testSaveFields(self):
assert f == importedFields.field(f.name())


class TestPyQgsMemoryProviderIndexed(TestCase, ProviderTestCase):
class TestPyQgsMemoryProviderIndexed(unittest.TestCase, ProviderTestCase):

"""Runs the provider test suite against an indexed memory layer"""

@classmethod
Expand Down

0 comments on commit c21889f

Please sign in to comment.