Skip to content
Permalink
Browse files

Merge pull request #2648 from m-kuhn/processingtests

Processing algorithm test framework
  • Loading branch information
m-kuhn committed Feb 4, 2016
2 parents 27f1637 + 8b84f3a commit 3b2a76f4c4466e9b052213f46fe5071c6f194dbd
Showing with 2,134 additions and 833 deletions.
  1. +1 −0 ci/travis/linux/before_install.sh
  2. +1 −1 ci/travis/osx/before_install.sh
  3. +1 −0 python/CMakeLists.txt
  4. +7 −4 python/plugins/processing/algs/qgis/Centroids.py
  5. +12 −9 python/plugins/processing/algs/qgis/DeleteHoles.py
  6. +1 −1 python/plugins/processing/algs/qgis/PolygonsToLines.py
  7. +2 −2 python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
  8. +3 −3 python/plugins/processing/core/GeoAlgorithm.py
  9. +1 −1 python/plugins/processing/core/ProcessingLog.py
  10. +0 −1 python/plugins/processing/gui/AlgorithmDialog.py
  11. +0 −1 python/plugins/processing/gui/HistoryDialog.py
  12. +3 −3 python/plugins/processing/gui/PostgisTableSelector.py
  13. +141 −65 python/plugins/processing/gui/TestTools.py
  14. +6 −0 python/plugins/processing/gui/__init__.py
  15. +3 −3 python/plugins/processing/modeler/ModelerGraphicItem.py
  16. +2 −2 python/plugins/processing/script/ScriptAlgorithm.py
  17. +203 −0 python/plugins/processing/tests/AlgorithmsTest.py
  18. +1 −0 python/plugins/processing/tests/CMakeLists.txt
  19. +4 −4 python/plugins/processing/tests/ParametersTest.py
  20. +118 −0 python/plugins/processing/tests/README.md
  21. +65 −0 python/plugins/processing/tests/testdata/algorithm_tests.yaml
  22. +3 −0 python/plugins/processing/tests/testdata/expected/frequency.csv
  23. BIN python/plugins/processing/tests/testdata/expected/intersection_collection_fallback.dbf
  24. +1 −0 python/plugins/processing/tests/testdata/expected/intersection_collection_fallback.prj
  25. +1 −0 python/plugins/processing/tests/testdata/expected/intersection_collection_fallback.qpj
  26. BIN python/plugins/processing/tests/testdata/expected/intersection_collection_fallback.shp
  27. BIN python/plugins/processing/tests/testdata/expected/intersection_collection_fallback.shx
  28. +42 −0 python/plugins/processing/tests/testdata/expected/multipolys_densify.gml
  29. +43 −0 python/plugins/processing/tests/testdata/expected/multipolys_densify.xsd
  30. +50 −0 python/plugins/processing/tests/testdata/expected/polys_centroid.gml
  31. +43 −0 python/plugins/processing/tests/testdata/expected/polys_centroid.xsd
  32. +50 −0 python/plugins/processing/tests/testdata/expected/polys_deleteholes.gml
  33. +43 −0 python/plugins/processing/tests/testdata/expected/polys_deleteholes.xsd
  34. +40 −0 python/plugins/processing/tests/testdata/expected/polys_to_lines.gml
  35. +43 −0 python/plugins/processing/tests/testdata/expected/polys_to_lines.xsd
  36. +16 −0 python/plugins/processing/tests/testdata/lines.gfs
  37. +44 −0 python/plugins/processing/tests/testdata/lines.gml
  38. +32 −0 python/plugins/processing/tests/testdata/multipolys.gfs
  39. +42 −0 python/plugins/processing/tests/testdata/multipolys.gml
  40. +16 −0 python/plugins/processing/tests/testdata/points.gfs
  41. +59 −0 python/plugins/processing/tests/testdata/points.gml
  42. +32 −0 python/plugins/processing/tests/testdata/polys.gfs
  43. +50 −0 python/plugins/processing/tests/testdata/polys.gml
  44. +0 −89 python/plugins/processing/tests/utilities_test.py
  45. +24 −0 python/testing/CMakeLists.txt
  46. +195 −0 python/testing/__init__.py
  47. +67 −0 python/testing/mocked.py
  48. +3 −0 src/core/geometry/qgsgeometry.cpp
  49. +17 −5 src/core/qgsvectorfilewriter.cpp
  50. +27 −12 tests/src/python/test_provider_memory.py
  51. +46 −26 tests/src/python/test_provider_mssql.py
  52. +6 −8 tests/src/python/test_provider_postgres.py
  53. +7 −7 tests/src/python/test_provider_shapefile.py
  54. +6 −8 tests/src/python/test_provider_spatialite.py
  55. +7 −7 tests/src/python/test_provider_tabfile.py
  56. +7 −9 tests/src/python/test_provider_virtual.py
  57. +7 −7 tests/src/python/test_qgis_local_server.py
  58. +7 −16 tests/src/python/test_qgsanalysis.py
  59. +2 −2 tests/src/python/test_qgsapplication.py
  60. +2 −1 tests/src/python/test_qgsappstartup.py
  61. +3 −3 tests/src/python/test_qgsatlascomposition.py
  62. +6 −7 tests/src/python/test_qgsattributetablemodel.py
  63. +4 −5 tests/src/python/test_qgsauthsystem.py
  64. +12 −9 tests/src/python/test_qgsblendmodes.py
  65. +5 −6 tests/src/python/test_qgscategorizedsymbolrendererv2.py
  66. +2 −2 tests/src/python/test_qgscolorscheme.py
  67. +2 −2 tests/src/python/test_qgscolorschemeregistry.py
  68. +6 −7 tests/src/python/test_qgscomposereffects.py
  69. +16 −9 tests/src/python/test_qgscomposerhtml.py
  70. +3 −3 tests/src/python/test_qgscomposerlabel.py
  71. +7 −9 tests/src/python/test_qgscomposermap.py
  72. +6 −7 tests/src/python/test_qgscomposermapgrid.py
  73. +6 −7 tests/src/python/test_qgscomposerpicture.py
  74. +6 −7 tests/src/python/test_qgscomposershapes.py
  75. +12 −12 tests/src/python/test_qgscomposition.py
  76. +13 −7 tests/src/python/test_qgsconditionalstyle.py
  77. +4 −8 tests/src/python/test_qgscoordinatetransform.py
  78. +30 −30 tests/src/python/test_qgsdelimitedtextprovider.py
  79. +4 −5 tests/src/python/test_qgsdistancearea.py
  80. +3 −6 tests/src/python/test_qgsdoccoverage.py
  81. +8 −7 tests/src/python/test_qgseditwidgets.py
  82. +2 −2 tests/src/python/test_qgsexpression.py
  83. +4 −8 tests/src/python/test_qgsfeature.py
  84. +6 −5 tests/src/python/test_qgsfeatureiterator.py
  85. +6 −8 tests/src/python/test_qgsfield.py
  86. +6 −7 tests/src/python/test_qgsfontutils.py
  87. +24 −20 tests/src/python/test_qgsgeometry.py
  88. +3 −3 tests/src/python/test_qgsgeometry_avoid_intersections.py
  89. +26 −20 tests/src/python/test_qgsgeometrygeneratorsymbollayerv2.py
  90. +5 −6 tests/src/python/test_qgsgraduatedsymbolrendererv2.py
  91. +5 −6 tests/src/python/test_qgsissue7244.py
  92. +7 −3 tests/src/python/test_qgslayerdefinition.py
  93. +4 −4 tests/src/python/test_qgslogger.py
  94. +1 −1 tests/src/python/test_qgsmaplayerregistry.py
  95. +2 −2 tests/src/python/test_qgsmapunitscale.py
  96. +3 −2 tests/src/python/test_qgsnetworkcontentfetcher.py
  97. +12 −8 tests/src/python/test_qgspallabeling_base.py
  98. +1 −1 tests/src/python/test_qgspallabeling_placement.py
  99. +3 −3 tests/src/python/test_qgspoint.py
  100. +3 −3 tests/src/python/test_qgsproject.py
  101. +6 −7 tests/src/python/test_qgsrangewidgets.py
  102. +10 −12 tests/src/python/test_qgsrasterfilewriter.py
  103. +6 −6 tests/src/python/test_qgsrasterlayer.py
  104. +11 −9 tests/src/python/test_qgsrectangle.py
  105. +5 −6 tests/src/python/test_qgsrelation.py
  106. +8 −10 tests/src/python/test_qgsrelationeditwidget.py
  107. +11 −8 tests/src/python/test_qgsrulebasedrenderer.py
  108. +1 −1 tests/src/python/test_qgsserver.py
  109. +3 −3 tests/src/python/test_qgsserver_accesscontrol.py
  110. +12 −8 tests/src/python/test_qgssinglesymbolrenderer.py
  111. +3 −6 tests/src/python/test_qgssipcoverage.py
  112. +4 −3 tests/src/python/test_qgsspatialindex.py
  113. +27 −23 tests/src/python/test_qgssymbolexpressionvariables.py
  114. +41 −43 tests/src/python/test_qgssymbollayerv2.py
  115. +9 −8 tests/src/python/test_qgssymbolv2.py
  116. +7 −11 tests/src/python/test_qgsunittypes.py
  117. +2 −2 tests/src/python/test_qgsvectorcolorramp.py
  118. +9 −7 tests/src/python/test_qgsvectorfilewriter.py
  119. +6 −7 tests/src/python/test_qgsvectorlayer.py
  120. +2 −2 tests/src/python/test_qgsvirtuallayerdefinition.py
  121. +8 −7 tests/src/python/test_qgszonalstatistics.py
  122. +5 −6 tests/src/python/test_syntactic_sugar.py
  123. +2 −71 tests/src/python/utilities.py
@@ -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
@@ -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

This comment has been minimized.

Copy link
@nyalldawson

nyalldawson Feb 4, 2016

Collaborator

@jef-n any chance we could get these additional dependencies added to the nightly builds?

pip install psycopg2 numpy
pip install psycopg2 numpy nose2 pyyaml mock
@@ -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
@@ -67,10 +67,13 @@ def processAlgorithm(self, progress):
inGeom = inFeat.geometry()
attrs = inFeat.attributes()

outGeom = QgsGeometry(inGeom.centroid())
if outGeom is None:
raise GeoAlgorithmExecutionException(
self.tr('Error calculating centroid'))
if not inGeom:
outGeom = QgsGeometry(None)
else:
outGeom = QgsGeometry(inGeom.centroid())
if not outGeom:
raise GeoAlgorithmExecutionException(
self.tr('Error calculating centroid'))

outFeat.setGeometry(outGeom)
outFeat.setAttributes(attrs)
@@ -60,18 +60,21 @@ def processAlgorithm(self, progress):
for count, f in enumerate(features):

geometry = f.geometry()
if geometry.isMultipart():
multi_polygon = geometry.asMultiPolygon()
for polygon in multi_polygon:
if geometry:
if geometry.isMultipart():
multi_polygon = geometry.asMultiPolygon()
for polygon in multi_polygon:
for ring in polygon[1:]:
polygon.remove(ring)
geometry = QgsGeometry.fromMultiPolygon(multi_polygon)

else:
polygon = geometry.asPolygon()
for ring in polygon[1:]:
polygon.remove(ring)
geometry = QgsGeometry.fromMultiPolygon(multi_polygon)

geometry = QgsGeometry.fromPolygon(polygon)
else:
polygon = geometry.asPolygon()
for ring in polygon[1:]:
polygon.remove(ring)
geometry = QgsGeometry.fromPolygon(polygon)
geometry = QgsGeometry(None)

feat.setGeometry(geometry)
feat.setAttributes(f.attributes())
@@ -77,7 +77,7 @@ def processAlgorithm(self, progress):
def extractAsLine(self, geom):
multiGeom = QgsGeometry()
lines = []
if geom.type() == QGis.Polygon:
if geom and geom.type() == QGis.Polygon:
if geom.isMultipart():
multiGeom = geom.asMultiPolygon()
for i in multiGeom:
@@ -141,10 +141,10 @@

class QGISAlgorithmProvider(AlgorithmProvider):

_icon = QIcon(os.path.join(pluginPath, 'images', 'qgis.png'))

def __init__(self):
AlgorithmProvider.__init__(self)
self._icon = QIcon(os.path.join(pluginPath, 'images', 'qgis.png'))

self.alglist = [SumLines(), PointsInPolygon(),
PointsInPolygonWeighted(), PointsInPolygonUnique(),
BasicStatisticsStrings(), BasicStatisticsNumbers(),
@@ -48,9 +48,8 @@

class GeoAlgorithm:

_icon = QIcon(os.path.dirname(__file__) + '/../images/alg.png')

def __init__(self):
self._icon = QIcon(os.path.dirname(__file__) + '/../images/alg.png')
# Parameters needed by the algorithm
self.parameters = list()

@@ -219,7 +218,8 @@ def execute(self, progress=None, model=None):

def _checkParameterValuesBeforeExecuting(self):
for param in self.parameters:
if isinstance(param, (ParameterRaster, ParameterVector, ParameterMultipleInput)):
if isinstance(param, (ParameterRaster, ParameterVector,
ParameterMultipleInput)):
if param.value:
if isinstance(param, ParameterMultipleInput):
inputlayers = param.value.split(';')
@@ -60,7 +60,7 @@ def addToLog(msgtype, msg):
# It seems that this fails sometimes depending on the msg
# added. To avoid it stopping the normal functioning of the
# algorithm, we catch all errors, assuming that is better
# to miss some log info that breaking the algorithm.
# to miss some log info than breaking the algorithm.
if msgtype == ProcessingLog.LOG_ALGORITHM:
line = msgtype + '|' + datetime.datetime.now().strftime(
ProcessingLog.DATE_FORMAT) + '|' \
@@ -88,7 +88,6 @@ def __init__(self, alg):

def runAsBatch(self):
dlg = BatchAlgorithmDialog(self.alg)
dlg.show()
dlg.exec_()

def setParamValues(self):
@@ -128,7 +128,6 @@ def createTest(self):
TestTools.createTest(item.entry.text)

def showPopupMenu(self, point):
return
item = self.tree.currentItem()
if isinstance(item, TreeLogEntryItem):
if item.isAlg:
@@ -85,10 +85,10 @@ def okPressed(self):

class ConnectionItem(QtGui.QTreeWidgetItem):

connIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/postgis.png')
schemaIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/namespace.png')

def __init__(self, connection):
self.connIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/postgis.png')
self.schemaIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/namespace.png')

QtGui.QTreeWidgetItem.__init__(self)
self.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.ShowIndicator)
self.connection = connection
@@ -26,88 +26,163 @@
__revision__ = '$Format:%H$'

import os
import yaml
import hashlib

from osgeo import gdal
from osgeo.gdalconst import GA_ReadOnly

from PyQt4.QtCore import QCoreApplication, QMetaObject
from PyQt4.QtGui import QMessageBox, QDialog, QVBoxLayout, QTextEdit

from processing.core.Processing import Processing
from processing.core.outputs import OutputNumber
from processing.core.outputs import OutputString
from processing.core.outputs import OutputRaster
from processing.core.outputs import OutputVector
from processing.tools import vector, dataobjects
from processing.core.outputs import (
OutputNumber,
OutputString,
OutputRaster,
OutputVector
)

from processing.core.parameters import (
ParameterRaster,
ParameterVector,
ParameterMultipleInput
)


def extractSchemaPath(filepath):
"""
Trys to find where the file is relative to the QGIS source code directory.
If it is already placed in the processing or QGIS testdata directory it will
return an appropriate schema and relative filepath
Args:
filepath: The path of the file to examine
Returns:
A tuple (schema, relative_file_path) where the schema is 'qgs' or 'proc'
if we can assume that the file is in this testdata directory.
"""
parts = []
schema = None
localpath = ''
path = filepath
part = True

while part:
(path, part) = os.path.split(path)
if part == 'testdata' and not localpath:
localparts = parts
localparts.reverse()
localpath = os.path.join(*localparts)

parts.append(part)

parts.reverse()

try:
testsindex = parts.index('tests')
except ValueError:
return '', filepath

if parts[testsindex - 1] == 'processing':
schema = 'proc'

return schema, localpath


def createTest(text):
s = ''
definition = {}

tokens = text[len('processing.runalg('):-1].split(',')
cmdname = (tokens[0])[1:-1]
methodname = 'test_' + cmdname.replace(':', '')
s += 'def ' + methodname + '(self):\n'
alg = Processing.getAlgorithm(cmdname)
execcommand = 'processing.runalg('

definition['name'] = 'Test ({})'.format(cmdname)
definition['algorithm'] = cmdname

params = []
results = {}

i = 0
for token in tokens:
if i < alg.getVisibleParametersCount() + 1:
if os.path.exists(token[1:-1]):
token = os.path.basename(token[1:-1])[:-4] + '()'
execcommand += token + ','
else:
execcommand += 'None,'
for param in alg.parameters:
if param.hidden:
continue

i += 1
s += '\toutputs=' + execcommand[:-1] + ')\n'

i = -1 * len(alg.outputs)
for out in alg.outputs:
filename = (tokens[i])[1:-1]
if tokens[i] == unicode(None):
QMessageBox.critical(None, tr('Error'),
tr('Cannot create unit test for that algorithm execution. The '
'output cannot be a temporary file'))
return
s += "\toutput=outputs['" + out.name + "']\n"
token = tokens[i]

if isinstance(param, ParameterVector):
filename = token[1:-1]
schema, filepath = extractSchemaPath(filename)
p = {
'type': 'vector',
'name': filepath
}
if not schema:
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'

params.append(p)
elif isinstance(param, ParameterRaster):
filename = token[1:-1]
schema, filepath = extractSchemaPath(filename)
p = {
'type': 'raster',
'name': filepath
}
if not schema:
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'

params.append(p)
elif isinstance(param, ParameterMultipleInput):
multiparams = token[1:-1].split(';')
newparam = []
for mp in multiparams:
schema, filepath = extractSchemaPath(mp)
newparam.append({
'type': 'vector',
'name': filepath
})
p = {
'type': 'multi',
'params': newparam
}
if not schema:
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'

params.append(p)
else:
params.append(token)

definition['params'] = params

for i, out in enumerate(alg.outputs):
token = tokens[i - len(alg.outputs)]

if isinstance(out, (OutputNumber, OutputString)):
s += 'self.assertTrue(' + unicode(out) + ', output.value)\n'
if isinstance(out, OutputRaster):
results[out.name] = unicode(out)
elif isinstance(out, OutputRaster):
filename = token[1:-1]
dataset = gdal.Open(filename, GA_ReadOnly)
strhash = hash(unicode(dataset.ReadAsArray(0).tolist()))
s += '\tself.assertTrue(os.path.isfile(output))\n'
s += '\tdataset=gdal.Open(output, GA_ReadOnly)\n'
s += '\tstrhash=hash(unicode(dataset.ReadAsArray(0).tolist()))\n'
s += '\tself.assertEqual(strhash,' + unicode(strhash) + ')\n'
if isinstance(out, OutputVector):
layer = dataobjects.getObject(filename)
fields = layer.pendingFields()
s += '\tlayer=dataobjects.getObjectFromUri(output, True)\n'
s += '\tfields=layer.pendingFields()\n'
s += '\texpectednames=[' + ','.join(["'" + unicode(f.name()) + "'"
for f in fields]) + ']\n'
s += '\texpectedtypes=[' + ','.join(["'" + unicode(f.typeName()) + "'"
for f in fields]) + ']\n'
s += '\tnames=[unicode(f.name()) for f in fields]\n'
s += '\ttypes=[unicode(f.typeName()) for f in fields]\n'
s += '\tself.assertEqual(expectednames, names)\n'
s += '\tself.assertEqual(expectedtypes, types)\n'
features = vector.features(layer)
numfeat = len(features)
s += '\tfeatures=processing.features(layer)\n'
s += '\tself.assertEqual(' + unicode(numfeat) + ', len(features))\n'
if numfeat > 0:
feature = features.next()
attrs = feature.attributes()
s += '\tfeature=features.next()\n'
s += '\tattrs=feature.attributes()\n'
s += '\texpectedvalues=[' + ','.join(['"' + unicode(attr) + '"'
for attr in attrs]) + ']\n'
s += '\tvalues=[unicode(attr) for attr in attrs]\n'
s += '\tself.assertEqual(expectedvalues, values)\n'
s += "\twkt='" + unicode(feature.geometry().exportToWkt()) + "'\n"
s += '\tself.assertEqual(wkt, \
unicode(feature.geometry().exportToWkt()))'

dlg = ShowTestDialog(s)
strhash = hashlib.sha224(dataset.ReadAsArray(0).data).hexdigest()

results[out.name] = {
'type': 'rasterhash',
'hash': strhash
}
elif isinstance(out, OutputVector):
filename = token[1:-1]
schema, filepath = extractSchemaPath(filename)
results[out.name] = {
'type': 'vector',
'name': filepath
}
if not schema:
results[out.name]['location'] = '[The expected result data is not in the testdata directory. Please write it to processing/tests/testdata/expected. Prefer gml files.]'

definition['results'] = results

dlg = ShowTestDialog(yaml.dump([definition], default_flow_style=False))
dlg.exec_()


@@ -124,6 +199,7 @@ def __init__(self, s):
self.setWindowTitle(self.tr('Unit test'))
layout = QVBoxLayout()
self.text = QTextEdit()
self.text.setFontFamily("monospace")
self.text.setEnabled(True)
self.text.setText(s)
layout.addWidget(self.text)
@@ -0,0 +1,6 @@
from PyQt4 import uic
import logging

uic.properties.logger.setLevel(logging.WARNING)
uic.uiparser.logger.setLevel(logging.WARNING)
uic.Compiler.qobjectcreator.logger.setLevel(logging.WARNING)

0 comments on commit 3b2a76f

Please sign in to comment.
You can’t perform that action at this time.