Skip to content

Commit

Permalink
Merge pull request #2648 from m-kuhn/processingtests
Browse files Browse the repository at this point in the history
Processing algorithm test framework
  • Loading branch information
m-kuhn committed Feb 4, 2016
2 parents 27f1637 + 8b84f3a commit 3b2a76f
Show file tree
Hide file tree
Showing 123 changed files with 2,134 additions and 833 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

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
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
11 changes: 7 additions & 4 deletions python/plugins/processing/algs/qgis/Centroids.py
Expand Up @@ -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)
Expand Down
21 changes: 12 additions & 9 deletions python/plugins/processing/algs/qgis/DeleteHoles.py
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/algs/qgis/PolygonsToLines.py
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions python/plugins/processing/core/GeoAlgorithm.py
Expand Up @@ -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()

Expand Down Expand Up @@ -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(';')
Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/core/ProcessingLog.py
Expand Up @@ -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) + '|' \
Expand Down
1 change: 0 additions & 1 deletion python/plugins/processing/gui/AlgorithmDialog.py
Expand Up @@ -88,7 +88,6 @@ def __init__(self, alg):

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

def setParamValues(self):
Expand Down
1 change: 0 additions & 1 deletion python/plugins/processing/gui/HistoryDialog.py
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions python/plugins/processing/gui/PostgisTableSelector.py
Expand Up @@ -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
Expand Down
206 changes: 141 additions & 65 deletions python/plugins/processing/gui/TestTools.py
Expand Up @@ -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_()


Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions python/plugins/processing/gui/__init__.py
@@ -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.