Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
436 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
""" | ||
*************************************************************************** | ||
test_algorithms.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 qgis | ||
import os | ||
import shutil | ||
import yaml | ||
import nose2 | ||
import gdal | ||
import hashlib | ||
import tempfile | ||
|
||
from osgeo.gdalconst import GA_ReadOnly | ||
|
||
import processing | ||
|
||
from processing.gui import AlgorithmExecutor | ||
|
||
from qgis.core import ( | ||
QgsVectorLayer, | ||
QgsRasterLayer, | ||
QgsMapLayerRegistry | ||
) | ||
|
||
from qgis.testing import ( | ||
start_app, | ||
unittest | ||
) | ||
|
||
from utilities import ( | ||
unitTestDataPath | ||
) | ||
|
||
|
||
def processingTestDataPath(): | ||
return os.path.join(os.path.dirname(__file__), 'testdata') | ||
|
||
|
||
class TestAlgorithms(unittest.TestCase): | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
start_app() | ||
from processing.core.Processing import Processing | ||
Processing.initialize() | ||
cls.cleanup_paths = [] | ||
|
||
@classmethod | ||
def tearDownClass(cls): | ||
for path in cls.cleanup_paths: | ||
shutil.rmtree(path) | ||
|
||
def test_algorithms(self): | ||
""" | ||
This is the main test function. All others will be executed based on the definitions in testdata/algorithm_tests.yaml | ||
""" | ||
with open(os.path.join(processingTestDataPath(), 'algorithm_tests.yaml'), 'r') as stream: | ||
algorithm_tests = yaml.load(stream) | ||
|
||
for algtest in algorithm_tests['tests']: | ||
yield self.check_algorithm, algtest['name'], algtest | ||
|
||
def check_algorithm(self, name, defs): | ||
""" | ||
Will run an algorithm definition and check if it generates the expected result | ||
:param name: The identifier name used in the test output heading | ||
:param defs: A python dict containing a test algorithm definition | ||
""" | ||
params = self.load_params(defs['params']) | ||
|
||
alg = processing.Processing.getAlgorithm(defs['algorithm']).getCopy() | ||
|
||
if isinstance(params, list): | ||
for param in zip(alg.parameters, params): | ||
param[0].setValue(param[1]) | ||
else: | ||
for k, p in params.iteritems(): | ||
alg.setParameterValue(k, p) | ||
|
||
for r, p in defs['results'].iteritems(): | ||
alg.setOutputValue(r, self.load_result_param(p)) | ||
|
||
self.assertTrue(AlgorithmExecutor.runalg(alg)) | ||
print(alg.getAsCommand()) | ||
self.check_results(alg.getOutputValuesAsDictionary(), defs['results']) | ||
|
||
def load_params(self, params): | ||
""" | ||
Loads an array of parameters | ||
""" | ||
if type(params) == list: | ||
return [self.load_param(p) for p in params] | ||
elif type(params) == dict: | ||
return {key: self.load_param(p) for key, p in params.iteritems()} | ||
else: | ||
return params | ||
|
||
def load_param(self, param): | ||
""" | ||
Loads a parameter. If it's not a map, the parameter will be returned as-is. If it is a map, it will process the | ||
parameter based on its key `type` and return the appropriate parameter to pass to the algorithm. | ||
""" | ||
try: | ||
if param['type'] == 'vector' or param['type'] == 'raster': | ||
return self.load_layer(param) | ||
if param['type'] == 'multi': | ||
return [self.load_param(p) for p in param['params']] | ||
except TypeError: | ||
# No type specified, use whatever is there | ||
return param | ||
|
||
raise KeyError("Unknown type '{}' specified for parameter '{}'".format(param['type'], param['name'])) | ||
|
||
def load_result_param(self, param): | ||
""" | ||
Lodas a result parameter. Creates a temporary destination where the result should go to and returns this location | ||
so it can be sent to the algorithm as parameter. | ||
""" | ||
if param['type'] == 'vector': | ||
outdir = tempfile.mkdtemp() | ||
self.cleanup_paths.append(outdir) | ||
basename = os.path.basename(param['name']) | ||
filepath = os.path.join(outdir, basename) | ||
return filepath | ||
|
||
raise KeyError("Unknown type '{}' specified for parameter '{}'".format(param['type'], param['name'])) | ||
|
||
def load_layer(self, param): | ||
""" | ||
Loads a layer which was specified as parameter. | ||
""" | ||
prefix = processingTestDataPath() | ||
try: | ||
if param['location'] == 'qgs': | ||
prefix = unitTestDataPath() | ||
except KeyError: | ||
pass | ||
|
||
filepath = os.path.join(prefix, param['name']) | ||
|
||
if param['type'] == 'vector': | ||
lyr = QgsVectorLayer(filepath, param['name'], 'ogr') | ||
elif param['type'] == 'raster': | ||
lyr = QgsRasterLayer(filepath, param['name'], 'ogr') | ||
|
||
self.assertTrue(lyr.isValid(), 'Could not load layer "{}"'.format(filepath)) | ||
QgsMapLayerRegistry.instance().addMapLayer(lyr) | ||
return lyr | ||
|
||
def check_results(self, results, expected): | ||
""" | ||
Checks if result produced by an algorithm matches with the expected specification. | ||
""" | ||
for id, expected_result in expected.iteritems(): | ||
if 'vector' == expected_result['type']: | ||
expected_lyr = self.load_layer(expected_result) | ||
try: | ||
results[id] | ||
except KeyError as e: | ||
raise KeyError('Expected result {} does not exist in {}'.format(e.message, results.keys())) | ||
|
||
result_lyr = QgsVectorLayer(results[id], id, 'ogr') | ||
|
||
try: | ||
compare = expected_result['compare'] | ||
except KeyError: | ||
compare = {} | ||
|
||
self.assertLayersEqual(expected_lyr, result_lyr, compare=compare) | ||
|
||
elif 'rasterhash' == expected_result['type']: | ||
dataset = gdal.Open(results[id], GA_ReadOnly) | ||
strhash = hashlib.sha224(dataset.ReadAsArray(0).data).hexdigest() | ||
|
||
self.assertEqual(strhash, expected_result['hash']) | ||
|
||
|
||
if __name__ == '__main__': | ||
nose2.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
Algorithm tests | ||
=============== | ||
|
||
To test algorithms you can add entries into `testdata/algorithm_tests.yaml`. | ||
|
||
This file is structured with [yaml syntax](http://www.yaml.org/start.html). | ||
|
||
A basic test appears under the toplevel key `tests` and looks like this: | ||
|
||
- name: centroid | ||
algorithm: qgis:polygoncentroids | ||
params: | ||
- type: vector | ||
location: qgs | ||
name: polys.shp | ||
results: | ||
- id: OUTPUT_LAYER | ||
type: vector | ||
location: proc | ||
name: polys_centroid.geojson | ||
|
||
How To | ||
------ | ||
|
||
To add a new test you can follow these steps: | ||
|
||
Run the algorithm you want to test in QGIS from the processing toolbox. If the | ||
result is a vector layer prefer geojson as output for its support of mixed | ||
geometry types and good readability. Redirect output to | ||
`python/plugins/processing/tests/testdata/expected` | ||
|
||
When you have run the algorithm, go to "Processing" > "History" and find the | ||
algorithm which you have just run. This looks like | ||
|
||
processing.runalg("qgis:densifygeometries","/home/mku/dev/cpp/qgis/QGIS/tests/testdata/polys.shp",2,"/home/mku/dev/cpp/qgis/QGIS/python/plugins/processing/tests/testdata/polys_densify.geojson") | ||
|
||
Open the file `python/plugins/processing/tests/testdata/algorithm_tests.yaml`, | ||
copy an existing test block and adjust it to your needs based on the | ||
information found in the history. | ||
|
||
The first string from the command goes to the key `algorithm`, the subsequent | ||
ones to params and the last one(s) to results. | ||
|
||
The above translates to | ||
|
||
- name: densify | ||
algorithm: qgis:densifygeometriesgivenaninterval | ||
params: | ||
- type: vector | ||
location: qgs | ||
name: polys.shp | ||
- 2 # Interval | ||
results: | ||
- id: OUTPUT | ||
type: vector | ||
location: proc | ||
name: expected/polys_densify.geojson | ||
|
||
Params and results | ||
------------------ | ||
|
||
Trivial type parameters | ||
....................... | ||
|
||
Params and results are specified as lists: | ||
|
||
params: | ||
- 2 | ||
- string | ||
- another param | ||
|
||
As in the example above they can be plain variables. | ||
|
||
Layer type parameters | ||
..................... | ||
|
||
To specify layers you will have to specify | ||
|
||
* the type | ||
* `vector` or `raster` | ||
* a location to allow using files from the shared qgis test data | ||
* `qgs` will look for the file in the src/tests/testdata | ||
* `proc` will look for the file in python/plugins/processing/tests/testdata | ||
you should use this location for expected data. | ||
* a name | ||
* relative path like `expected/polys_centroid.geojson` | ||
|
||
params: | ||
- 2 | ||
- string | ||
- type: vector | ||
location: qgs | ||
name: polys.shp | ||
- another param | ||
|
||
Results | ||
....... | ||
|
||
Results have a special key `id` which is required because an algorithm can | ||
produce multiple results. If you don't know the `id`, just start with `OUTPUT` | ||
and run the test. You will be told if it was wrong and about the possible | ||
values. | ||
|
||
To deal with a certain tolerance for output values you can specify a | ||
`compare` property for an output. | ||
|
||
For a vector layer this means | ||
|
||
OUTPUT: | ||
type: vector | ||
name: expected/abcd.geojson | ||
compare: | ||
fields: | ||
__all__: | ||
precision: 5 # compare to a precision of .00001 on all fields | ||
A: skip # skip field A | ||
geometry: | ||
precision: 5 # compare coordinates with a precision of 5 digits |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
python/plugins/processing/tests/testdata/expected/frequency.csv
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Name,FREQ | ||
Lake,6 | ||
Dam,4 |
Oops, something went wrong.