Skip to content

Commit 3b2a76f

Browse files
committed
Merge pull request #2648 from m-kuhn/processingtests
Processing algorithm test framework
2 parents 27f1637 + 8b84f3a commit 3b2a76f

File tree

123 files changed

+2134
-833
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+2134
-833
lines changed

ci/travis/linux/before_install.sh

+1
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ sudo apt-get install --force-yes --no-install-recommends --no-install-suggests \
6161
postgresql-9.1-postgis-2.1/precise # from ubuntugis-unstable, not pgdg
6262

6363
sudo -H pip install autopep8 # TODO when switching to trusty or above: replace python-pip with python-autopep8
64+
sudo -H pip install nose2 pyyaml mock

ci/travis/osx/before_install.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ mkdir -p /Users/travis/Library/Python/2.7/lib/python/site-packages
1818
echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> /Users/travis/Library/Python/2.7/lib/python/site-packages/homebrew.pth
1919

2020
# Needed for Processing
21-
pip install psycopg2 numpy
21+
pip install psycopg2 numpy nose2 pyyaml mock

python/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ ADD_SUBDIRECTORY(console)
5959
ADD_SUBDIRECTORY(PyQt)
6060
ADD_SUBDIRECTORY(pyplugin_installer)
6161
ADD_SUBDIRECTORY(ext-libs)
62+
ADD_SUBDIRECTORY(testing)
6263

6364
IF(POLICY CMP0040)
6465
CMAKE_POLICY (POP) # see PUSH above

python/plugins/processing/algs/qgis/Centroids.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,13 @@ def processAlgorithm(self, progress):
6767
inGeom = inFeat.geometry()
6868
attrs = inFeat.attributes()
6969

70-
outGeom = QgsGeometry(inGeom.centroid())
71-
if outGeom is None:
72-
raise GeoAlgorithmExecutionException(
73-
self.tr('Error calculating centroid'))
70+
if not inGeom:
71+
outGeom = QgsGeometry(None)
72+
else:
73+
outGeom = QgsGeometry(inGeom.centroid())
74+
if not outGeom:
75+
raise GeoAlgorithmExecutionException(
76+
self.tr('Error calculating centroid'))
7477

7578
outFeat.setGeometry(outGeom)
7679
outFeat.setAttributes(attrs)

python/plugins/processing/algs/qgis/DeleteHoles.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,21 @@ def processAlgorithm(self, progress):
6060
for count, f in enumerate(features):
6161

6262
geometry = f.geometry()
63-
if geometry.isMultipart():
64-
multi_polygon = geometry.asMultiPolygon()
65-
for polygon in multi_polygon:
63+
if geometry:
64+
if geometry.isMultipart():
65+
multi_polygon = geometry.asMultiPolygon()
66+
for polygon in multi_polygon:
67+
for ring in polygon[1:]:
68+
polygon.remove(ring)
69+
geometry = QgsGeometry.fromMultiPolygon(multi_polygon)
70+
71+
else:
72+
polygon = geometry.asPolygon()
6673
for ring in polygon[1:]:
6774
polygon.remove(ring)
68-
geometry = QgsGeometry.fromMultiPolygon(multi_polygon)
69-
75+
geometry = QgsGeometry.fromPolygon(polygon)
7076
else:
71-
polygon = geometry.asPolygon()
72-
for ring in polygon[1:]:
73-
polygon.remove(ring)
74-
geometry = QgsGeometry.fromPolygon(polygon)
77+
geometry = QgsGeometry(None)
7578

7679
feat.setGeometry(geometry)
7780
feat.setAttributes(f.attributes())

python/plugins/processing/algs/qgis/PolygonsToLines.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def processAlgorithm(self, progress):
7777
def extractAsLine(self, geom):
7878
multiGeom = QgsGeometry()
7979
lines = []
80-
if geom.type() == QGis.Polygon:
80+
if geom and geom.type() == QGis.Polygon:
8181
if geom.isMultipart():
8282
multiGeom = geom.asMultiPolygon()
8383
for i in multiGeom:

python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@
141141

142142
class QGISAlgorithmProvider(AlgorithmProvider):
143143

144-
_icon = QIcon(os.path.join(pluginPath, 'images', 'qgis.png'))
145-
146144
def __init__(self):
147145
AlgorithmProvider.__init__(self)
146+
self._icon = QIcon(os.path.join(pluginPath, 'images', 'qgis.png'))
147+
148148
self.alglist = [SumLines(), PointsInPolygon(),
149149
PointsInPolygonWeighted(), PointsInPolygonUnique(),
150150
BasicStatisticsStrings(), BasicStatisticsNumbers(),

python/plugins/processing/core/GeoAlgorithm.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,8 @@
4848

4949
class GeoAlgorithm:
5050

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

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

220219
def _checkParameterValuesBeforeExecuting(self):
221220
for param in self.parameters:
222-
if isinstance(param, (ParameterRaster, ParameterVector, ParameterMultipleInput)):
221+
if isinstance(param, (ParameterRaster, ParameterVector,
222+
ParameterMultipleInput)):
223223
if param.value:
224224
if isinstance(param, ParameterMultipleInput):
225225
inputlayers = param.value.split(';')

python/plugins/processing/core/ProcessingLog.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def addToLog(msgtype, msg):
6060
# It seems that this fails sometimes depending on the msg
6161
# added. To avoid it stopping the normal functioning of the
6262
# algorithm, we catch all errors, assuming that is better
63-
# to miss some log info that breaking the algorithm.
63+
# to miss some log info than breaking the algorithm.
6464
if msgtype == ProcessingLog.LOG_ALGORITHM:
6565
line = msgtype + '|' + datetime.datetime.now().strftime(
6666
ProcessingLog.DATE_FORMAT) + '|' \

python/plugins/processing/gui/AlgorithmDialog.py

-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def __init__(self, alg):
8888

8989
def runAsBatch(self):
9090
dlg = BatchAlgorithmDialog(self.alg)
91-
dlg.show()
9291
dlg.exec_()
9392

9493
def setParamValues(self):

python/plugins/processing/gui/HistoryDialog.py

-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ def createTest(self):
128128
TestTools.createTest(item.entry.text)
129129

130130
def showPopupMenu(self, point):
131-
return
132131
item = self.tree.currentItem()
133132
if isinstance(item, TreeLogEntryItem):
134133
if item.isAlg:

python/plugins/processing/gui/PostgisTableSelector.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ def okPressed(self):
8585

8686
class ConnectionItem(QtGui.QTreeWidgetItem):
8787

88-
connIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/postgis.png')
89-
schemaIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/namespace.png')
90-
9188
def __init__(self, connection):
89+
self.connIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/postgis.png')
90+
self.schemaIcon = QtGui.QIcon(os.path.dirname(__file__) + '/../images/namespace.png')
91+
9292
QtGui.QTreeWidgetItem.__init__(self)
9393
self.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.ShowIndicator)
9494
self.connection = connection

python/plugins/processing/gui/TestTools.py

+141-65
Original file line numberDiff line numberDiff line change
@@ -26,88 +26,163 @@
2626
__revision__ = '$Format:%H$'
2727

2828
import os
29+
import yaml
30+
import hashlib
31+
2932
from osgeo import gdal
3033
from osgeo.gdalconst import GA_ReadOnly
3134

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

3538
from processing.core.Processing import Processing
36-
from processing.core.outputs import OutputNumber
37-
from processing.core.outputs import OutputString
38-
from processing.core.outputs import OutputRaster
39-
from processing.core.outputs import OutputVector
40-
from processing.tools import vector, dataobjects
39+
from processing.core.outputs import (
40+
OutputNumber,
41+
OutputString,
42+
OutputRaster,
43+
OutputVector
44+
)
45+
46+
from processing.core.parameters import (
47+
ParameterRaster,
48+
ParameterVector,
49+
ParameterMultipleInput
50+
)
51+
52+
53+
def extractSchemaPath(filepath):
54+
"""
55+
Trys to find where the file is relative to the QGIS source code directory.
56+
If it is already placed in the processing or QGIS testdata directory it will
57+
return an appropriate schema and relative filepath
58+
59+
Args:
60+
filepath: The path of the file to examine
61+
62+
Returns:
63+
A tuple (schema, relative_file_path) where the schema is 'qgs' or 'proc'
64+
if we can assume that the file is in this testdata directory.
65+
"""
66+
parts = []
67+
schema = None
68+
localpath = ''
69+
path = filepath
70+
part = True
71+
72+
while part:
73+
(path, part) = os.path.split(path)
74+
if part == 'testdata' and not localpath:
75+
localparts = parts
76+
localparts.reverse()
77+
localpath = os.path.join(*localparts)
78+
79+
parts.append(part)
80+
81+
parts.reverse()
82+
83+
try:
84+
testsindex = parts.index('tests')
85+
except ValueError:
86+
return '', filepath
87+
88+
if parts[testsindex - 1] == 'processing':
89+
schema = 'proc'
90+
91+
return schema, localpath
4192

4293

4394
def createTest(text):
44-
s = ''
95+
definition = {}
96+
4597
tokens = text[len('processing.runalg('):-1].split(',')
4698
cmdname = (tokens[0])[1:-1]
47-
methodname = 'test_' + cmdname.replace(':', '')
48-
s += 'def ' + methodname + '(self):\n'
4999
alg = Processing.getAlgorithm(cmdname)
50-
execcommand = 'processing.runalg('
100+
101+
definition['name'] = 'Test ({})'.format(cmdname)
102+
definition['algorithm'] = cmdname
103+
104+
params = []
105+
results = {}
106+
51107
i = 0
52-
for token in tokens:
53-
if i < alg.getVisibleParametersCount() + 1:
54-
if os.path.exists(token[1:-1]):
55-
token = os.path.basename(token[1:-1])[:-4] + '()'
56-
execcommand += token + ','
57-
else:
58-
execcommand += 'None,'
108+
for param in alg.parameters:
109+
if param.hidden:
110+
continue
111+
59112
i += 1
60-
s += '\toutputs=' + execcommand[:-1] + ')\n'
61-
62-
i = -1 * len(alg.outputs)
63-
for out in alg.outputs:
64-
filename = (tokens[i])[1:-1]
65-
if tokens[i] == unicode(None):
66-
QMessageBox.critical(None, tr('Error'),
67-
tr('Cannot create unit test for that algorithm execution. The '
68-
'output cannot be a temporary file'))
69-
return
70-
s += "\toutput=outputs['" + out.name + "']\n"
113+
token = tokens[i]
114+
115+
if isinstance(param, ParameterVector):
116+
filename = token[1:-1]
117+
schema, filepath = extractSchemaPath(filename)
118+
p = {
119+
'type': 'vector',
120+
'name': filepath
121+
}
122+
if not schema:
123+
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
124+
125+
params.append(p)
126+
elif isinstance(param, ParameterRaster):
127+
filename = token[1:-1]
128+
schema, filepath = extractSchemaPath(filename)
129+
p = {
130+
'type': 'raster',
131+
'name': filepath
132+
}
133+
if not schema:
134+
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
135+
136+
params.append(p)
137+
elif isinstance(param, ParameterMultipleInput):
138+
multiparams = token[1:-1].split(';')
139+
newparam = []
140+
for mp in multiparams:
141+
schema, filepath = extractSchemaPath(mp)
142+
newparam.append({
143+
'type': 'vector',
144+
'name': filepath
145+
})
146+
p = {
147+
'type': 'multi',
148+
'params': newparam
149+
}
150+
if not schema:
151+
p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
152+
153+
params.append(p)
154+
else:
155+
params.append(token)
156+
157+
definition['params'] = params
158+
159+
for i, out in enumerate(alg.outputs):
160+
token = tokens[i - len(alg.outputs)]
161+
71162
if isinstance(out, (OutputNumber, OutputString)):
72-
s += 'self.assertTrue(' + unicode(out) + ', output.value)\n'
73-
if isinstance(out, OutputRaster):
163+
results[out.name] = unicode(out)
164+
elif isinstance(out, OutputRaster):
165+
filename = token[1:-1]
74166
dataset = gdal.Open(filename, GA_ReadOnly)
75-
strhash = hash(unicode(dataset.ReadAsArray(0).tolist()))
76-
s += '\tself.assertTrue(os.path.isfile(output))\n'
77-
s += '\tdataset=gdal.Open(output, GA_ReadOnly)\n'
78-
s += '\tstrhash=hash(unicode(dataset.ReadAsArray(0).tolist()))\n'
79-
s += '\tself.assertEqual(strhash,' + unicode(strhash) + ')\n'
80-
if isinstance(out, OutputVector):
81-
layer = dataobjects.getObject(filename)
82-
fields = layer.pendingFields()
83-
s += '\tlayer=dataobjects.getObjectFromUri(output, True)\n'
84-
s += '\tfields=layer.pendingFields()\n'
85-
s += '\texpectednames=[' + ','.join(["'" + unicode(f.name()) + "'"
86-
for f in fields]) + ']\n'
87-
s += '\texpectedtypes=[' + ','.join(["'" + unicode(f.typeName()) + "'"
88-
for f in fields]) + ']\n'
89-
s += '\tnames=[unicode(f.name()) for f in fields]\n'
90-
s += '\ttypes=[unicode(f.typeName()) for f in fields]\n'
91-
s += '\tself.assertEqual(expectednames, names)\n'
92-
s += '\tself.assertEqual(expectedtypes, types)\n'
93-
features = vector.features(layer)
94-
numfeat = len(features)
95-
s += '\tfeatures=processing.features(layer)\n'
96-
s += '\tself.assertEqual(' + unicode(numfeat) + ', len(features))\n'
97-
if numfeat > 0:
98-
feature = features.next()
99-
attrs = feature.attributes()
100-
s += '\tfeature=features.next()\n'
101-
s += '\tattrs=feature.attributes()\n'
102-
s += '\texpectedvalues=[' + ','.join(['"' + unicode(attr) + '"'
103-
for attr in attrs]) + ']\n'
104-
s += '\tvalues=[unicode(attr) for attr in attrs]\n'
105-
s += '\tself.assertEqual(expectedvalues, values)\n'
106-
s += "\twkt='" + unicode(feature.geometry().exportToWkt()) + "'\n"
107-
s += '\tself.assertEqual(wkt, \
108-
unicode(feature.geometry().exportToWkt()))'
109-
110-
dlg = ShowTestDialog(s)
167+
strhash = hashlib.sha224(dataset.ReadAsArray(0).data).hexdigest()
168+
169+
results[out.name] = {
170+
'type': 'rasterhash',
171+
'hash': strhash
172+
}
173+
elif isinstance(out, OutputVector):
174+
filename = token[1:-1]
175+
schema, filepath = extractSchemaPath(filename)
176+
results[out.name] = {
177+
'type': 'vector',
178+
'name': filepath
179+
}
180+
if not schema:
181+
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.]'
182+
183+
definition['results'] = results
184+
185+
dlg = ShowTestDialog(yaml.dump([definition], default_flow_style=False))
111186
dlg.exec_()
112187

113188

@@ -124,6 +199,7 @@ def __init__(self, s):
124199
self.setWindowTitle(self.tr('Unit test'))
125200
layout = QVBoxLayout()
126201
self.text = QTextEdit()
202+
self.text.setFontFamily("monospace")
127203
self.text.setEnabled(True)
128204
self.text.setText(s)
129205
layout.addWidget(self.text)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from PyQt4 import uic
2+
import logging
3+
4+
uic.properties.logger.setLevel(logging.WARNING)
5+
uic.uiparser.logger.setLevel(logging.WARNING)
6+
uic.Compiler.qobjectcreator.logger.setLevel(logging.WARNING)

0 commit comments

Comments
 (0)