diff --git a/python/plugins/otbprovider/CMakeLists.txt b/python/plugins/otbprovider/CMakeLists.txt
index 2122113fa47a..facadb6e69e9 100644
--- a/python/plugins/otbprovider/CMakeLists.txt
+++ b/python/plugins/otbprovider/CMakeLists.txt
@@ -1,4 +1,6 @@
file(GLOB PY_FILES *.py)
file(GLOB OTHER_FILES metadata.txt)
+add_subdirectory(tests)
+
PLUGIN_INSTALL(otbprovider . ${PY_FILES} ${OTHER_FILES})
diff --git a/python/plugins/otbprovider/OtbChoiceWidget.py b/python/plugins/otbprovider/OtbChoiceWidget.py
index 8d4a88908312..06872550cdae 100644
--- a/python/plugins/otbprovider/OtbChoiceWidget.py
+++ b/python/plugins/otbprovider/OtbChoiceWidget.py
@@ -152,7 +152,7 @@ def __init__(self, name='', description='', options=[], default=None, isSource=F
self.setMetadata({
'widget_wrapper': {
- 'class': 'processing.algs.otb.OtbChoiceWidget.OtbChoiceWidgetWrapper'}})
+ 'class': 'otbprovider.OtbChoiceWidget.OtbChoiceWidgetWrapper'}})
self.options = options
if default is not None:
diff --git a/python/plugins/otbprovider/tests/AlgorithmsTestBase.py b/python/plugins/otbprovider/tests/AlgorithmsTestBase.py
new file mode 100644
index 000000000000..e862e3e31901
--- /dev/null
+++ b/python/plugins/otbprovider/tests/AlgorithmsTestBase.py
@@ -0,0 +1,429 @@
+# -*- coding: utf-8 -*-
+
+"""
+***************************************************************************
+ AlgorithmsTest.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'
+
+import qgis # NOQA switch sip api
+
+import os
+import yaml
+import nose2
+import shutil
+import glob
+import hashlib
+import tempfile
+import re
+
+from osgeo import gdal
+from osgeo.gdalconst import GA_ReadOnly
+from numpy import nan_to_num
+from copy import deepcopy
+
+from qgis.core import (QgsVectorLayer,
+ QgsRasterLayer,
+ QgsCoordinateReferenceSystem,
+ QgsFeatureRequest,
+ QgsMapLayer,
+ QgsProject,
+ QgsApplication,
+ QgsProcessingContext,
+ QgsProcessingUtils,
+ QgsProcessingFeedback)
+from qgis.analysis import (QgsNativeAlgorithms)
+from qgis.testing import (_UnexpectedSuccess,
+ start_app,
+ unittest)
+from utilities import unitTestDataPath
+
+import processing
+
+
+def processingTestDataPath():
+ return os.path.join(os.path.dirname(__file__), 'testdata')
+
+
+class AlgorithmsTest(object):
+
+ 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(), self.test_definition_file()), 'r') as stream:
+ algorithm_tests = yaml.load(stream, Loader=yaml.SafeLoader)
+
+ if 'tests' in algorithm_tests and algorithm_tests['tests'] is not None:
+ for idx, algtest in enumerate(algorithm_tests['tests']):
+ print('About to start {} of {}: "{}"'.format(idx, len(algorithm_tests['tests']), algtest['name']))
+ 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
+ """
+ self.vector_layer_params = {}
+ QgsProject.instance().clear()
+
+ if 'project' in defs:
+ full_project_path = os.path.join(processingTestDataPath(), defs['project'])
+ project_read_success = QgsProject.instance().read(full_project_path)
+ self.assertTrue(project_read_success, 'Failed to load project file: ' + defs['project'])
+
+ if 'project_crs' in defs:
+ QgsProject.instance().setCrs(QgsCoordinateReferenceSystem(defs['project_crs']))
+ else:
+ QgsProject.instance().setCrs(QgsCoordinateReferenceSystem())
+
+ if 'ellipsoid' in defs:
+ QgsProject.instance().setEllipsoid(defs['ellipsoid'])
+ else:
+ QgsProject.instance().setEllipsoid('')
+
+ params = self.load_params(defs['params'])
+
+ print('Running alg: "{}"'.format(defs['algorithm']))
+ alg = QgsApplication.processingRegistry().createAlgorithmById(defs['algorithm'])
+
+ parameters = {}
+ if isinstance(params, list):
+ for param in zip(alg.parameterDefinitions(), params):
+ parameters[param[0].name()] = param[1]
+ else:
+ for k, p in params.items():
+ parameters[k] = p
+
+ for r, p in list(defs['results'].items()):
+ if 'in_place_result' not in p or not p['in_place_result']:
+ parameters[r] = self.load_result_param(p)
+
+ expectFailure = False
+ if 'expectedFailure' in defs:
+ exec(('\n'.join(defs['expectedFailure'][:-1])), globals(), locals())
+ expectFailure = eval(defs['expectedFailure'][-1])
+
+ if 'expectedException' in defs:
+ expectFailure = True
+
+ # ignore user setting for invalid geometry handling
+ context = QgsProcessingContext()
+ context.setProject(QgsProject.instance())
+
+ if 'skipInvalid' in defs and defs['skipInvalid']:
+ context.setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)
+
+ feedback = QgsProcessingFeedback()
+
+ print('Algorithm parameters are {}'.format(parameters))
+
+ # first check that algorithm accepts the parameters we pass...
+ ok, msg = alg.checkParameterValues(parameters, context)
+ self.assertTrue(ok, 'Algorithm failed checkParameterValues with result {}'.format(msg))
+
+ if expectFailure:
+ try:
+ results, ok = alg.run(parameters, context, feedback)
+ self.check_results(results, context, parameters, defs['results'])
+ if ok:
+ raise _UnexpectedSuccess
+ except Exception:
+ pass
+ else:
+ results, ok = alg.run(parameters, context, feedback)
+ self.assertTrue(ok, 'params: {}, results: {}'.format(parameters, results))
+ self.check_results(results, context, parameters, defs['results'])
+
+ def load_params(self, params):
+ """
+ Loads an array of parameters
+ """
+ if isinstance(params, list):
+ return [self.load_param(p) for p in params]
+ elif isinstance(params, dict):
+ return {key: self.load_param(p, key) for key, p in params.items()}
+ else:
+ return params
+
+ def load_param(self, param, id=None):
+ """
+ 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'] in ('vector', 'raster', 'table'):
+ return self.load_layer(id, param).id()
+ elif param['type'] == 'vrtlayers':
+ vals = []
+ for p in param['params']:
+ p['layer'] = self.load_layer(None, {'type': 'vector', 'name': p['layer']})
+ vals.append(p)
+ return vals
+ elif param['type'] == 'multi':
+ return [self.load_param(p) for p in param['params']]
+ elif param['type'] == 'file':
+ return self.filepath_from_param(param)
+ elif param['type'] == 'interpolation':
+ prefix = processingTestDataPath()
+ tmp = ''
+ for r in param['name'].split('::|::'):
+ v = r.split('::~::')
+ tmp += '{}::~::{}::~::{}::~::{};'.format(os.path.join(prefix, v[0]),
+ v[1], v[2], v[3])
+ return tmp[:-1]
+ except TypeError:
+ # No type specified, use whatever is there
+ return param
+
+ raise KeyError("Unknown type '{}' specified for parameter".format(param['type']))
+
+ def load_result_param(self, param):
+ """
+ Loads 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'] in ['vector', 'file', 'table', 'regex']:
+ outdir = tempfile.mkdtemp()
+ self.cleanup_paths.append(outdir)
+ if isinstance(param['name'], str):
+ basename = os.path.basename(param['name'])
+ else:
+ basename = os.path.basename(param['name'][0])
+
+ filepath = self.uri_path_join(outdir, basename)
+ return filepath
+ elif param['type'] == 'rasterhash':
+ outdir = tempfile.mkdtemp()
+ self.cleanup_paths.append(outdir)
+ if self.test_definition_file().lower().startswith('saga'):
+ basename = 'raster.sdat'
+ else:
+ basename = 'raster.tif'
+ filepath = os.path.join(outdir, basename)
+ return filepath
+ elif param['type'] == 'directory':
+ outdir = tempfile.mkdtemp()
+ return outdir
+
+ raise KeyError("Unknown type '{}' specified for parameter".format(param['type']))
+
+ def load_layers(self, id, param):
+ layers = []
+ if param['type'] in ('vector', 'table'):
+ if isinstance(param['name'], str) or 'uri' in param:
+ layers.append(self.load_layer(id, param))
+ else:
+ for n in param['name']:
+ layer_param = deepcopy(param)
+ layer_param['name'] = n
+ layers.append(self.load_layer(id, layer_param))
+ else:
+ layers.append(self.load_layer(id, param))
+ return layers
+
+ def load_layer(self, id, param):
+ """
+ Loads a layer which was specified as parameter.
+ """
+
+ filepath = self.filepath_from_param(param)
+
+ if 'in_place' in param and param['in_place']:
+ # check if alg modifies layer in place
+ tmpdir = tempfile.mkdtemp()
+ self.cleanup_paths.append(tmpdir)
+ path, file_name = os.path.split(filepath)
+ base, ext = os.path.splitext(file_name)
+ for file in glob.glob(os.path.join(path, '{}.*'.format(base))):
+ shutil.copy(os.path.join(path, file), tmpdir)
+ filepath = os.path.join(tmpdir, file_name)
+ self.in_place_layers[id] = filepath
+
+ if param['type'] in ('vector', 'table'):
+ gmlrex = r'\.gml\b'
+ if re.search(gmlrex, filepath, re.IGNORECASE):
+ # ewwwww - we have to force SRS detection for GML files, otherwise they'll be loaded
+ # with no srs
+ filepath += '|option:FORCE_SRS_DETECTION=YES'
+
+ if filepath in self.vector_layer_params:
+ return self.vector_layer_params[filepath]
+
+ options = QgsVectorLayer.LayerOptions()
+ options.loadDefaultStyle = False
+ lyr = QgsVectorLayer(filepath, param['name'], 'ogr', options)
+ self.vector_layer_params[filepath] = lyr
+ elif param['type'] == 'raster':
+ options = QgsRasterLayer.LayerOptions()
+ options.loadDefaultStyle = False
+ lyr = QgsRasterLayer(filepath, param['name'], 'gdal', options)
+
+ self.assertTrue(lyr.isValid(), 'Could not load layer "{}" from param {}'.format(filepath, param))
+ QgsProject.instance().addMapLayer(lyr)
+ return lyr
+
+ def filepath_from_param(self, param):
+ """
+ Creates a filepath from a param
+ """
+ prefix = processingTestDataPath()
+ if 'location' in param and param['location'] == 'qgs':
+ prefix = unitTestDataPath()
+
+ if 'uri' in param:
+ path = param['uri']
+ else:
+ path = param['name']
+
+ return self.uri_path_join(prefix, path)
+
+ def uri_path_join(self, prefix, filepath):
+ if filepath.startswith('ogr:'):
+ if not prefix[-1] == os.path.sep:
+ prefix += os.path.sep
+ filepath = re.sub(r"dbname='", "dbname='{}".format(prefix), filepath)
+ else:
+ filepath = os.path.join(prefix, filepath)
+
+ return filepath
+
+ def check_results(self, results, context, params, expected):
+ """
+ Checks if result produced by an algorithm matches with the expected specification.
+ """
+ for id, expected_result in expected.items():
+ if expected_result['type'] in ('vector', 'table'):
+ if 'compare' in expected_result and not expected_result['compare']:
+ # skipping the comparison, so just make sure output is valid
+ if isinstance(results[id], QgsMapLayer):
+ result_lyr = results[id]
+ else:
+ result_lyr = QgsProcessingUtils.mapLayerFromString(results[id], context)
+ self.assertTrue(result_lyr.isValid())
+ continue
+
+ expected_lyrs = self.load_layers(id, expected_result)
+ if 'in_place_result' in expected_result:
+ result_lyr = QgsProcessingUtils.mapLayerFromString(self.in_place_layers[id], context)
+ self.assertTrue(result_lyr.isValid(), self.in_place_layers[id])
+ else:
+ try:
+ results[id]
+ except KeyError as e:
+ raise KeyError('Expected result {} does not exist in {}'.format(str(e), list(results.keys())))
+
+ if isinstance(results[id], QgsMapLayer):
+ result_lyr = results[id]
+ else:
+ string = results[id]
+
+ gmlrex = r'\.gml\b'
+ if re.search(gmlrex, string, re.IGNORECASE):
+ # ewwwww - we have to force SRS detection for GML files, otherwise they'll be loaded
+ # with no srs
+ string += '|option:FORCE_SRS_DETECTION=YES'
+
+ result_lyr = QgsProcessingUtils.mapLayerFromString(string, context)
+ self.assertTrue(result_lyr, results[id])
+
+ compare = expected_result.get('compare', {})
+ pk = expected_result.get('pk', None)
+
+ if len(expected_lyrs) == 1:
+ self.assertLayersEqual(expected_lyrs[0], result_lyr, compare=compare, pk=pk)
+ else:
+ res = False
+ for l in expected_lyrs:
+ if self.checkLayersEqual(l, result_lyr, compare=compare, pk=pk):
+ res = True
+ break
+ self.assertTrue(res, 'Could not find matching layer in expected results')
+
+ elif 'rasterhash' == expected_result['type']:
+ print("id:{} result:{}".format(id, results[id]))
+ self.assertTrue(os.path.exists(results[id]), 'File does not exist: {}, {}'.format(results[id], params))
+ dataset = gdal.Open(results[id], GA_ReadOnly)
+ dataArray = nan_to_num(dataset.ReadAsArray(0))
+ strhash = hashlib.sha224(dataArray.data).hexdigest()
+
+ if not isinstance(expected_result['hash'], str):
+ self.assertIn(strhash, expected_result['hash'])
+ else:
+ self.assertEqual(strhash, expected_result['hash'])
+ elif 'file' == expected_result['type']:
+ result_filepath = results[id]
+ if isinstance(expected_result.get('name'), list):
+ # test to see if any match expected
+ for path in expected_result['name']:
+ expected_filepath = self.filepath_from_param({'name': path})
+ if self.checkFilesEqual(expected_filepath, result_filepath):
+ break
+ else:
+ expected_filepath = self.filepath_from_param({'name': expected_result['name'][0]})
+ else:
+ expected_filepath = self.filepath_from_param(expected_result)
+
+ self.assertFilesEqual(expected_filepath, result_filepath)
+ elif 'directory' == expected_result['type']:
+ expected_dirpath = self.filepath_from_param(expected_result)
+ result_dirpath = results[id]
+
+ self.assertDirectoriesEqual(expected_dirpath, result_dirpath)
+ elif 'regex' == expected_result['type']:
+ with open(results[id], 'r') as file:
+ data = file.read()
+
+ for rule in expected_result.get('rules', []):
+ self.assertRegex(data, rule)
+
+
+class GenericAlgorithmsTest(unittest.TestCase):
+ """
+ General (non-provider specific) algorithm tests
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ start_app()
+ from processing.core.Processing import Processing
+ Processing.initialize()
+ cls.cleanup_paths = []
+
+ @classmethod
+ def tearDownClass(cls):
+ from processing.core.Processing import Processing
+ Processing.deinitialize()
+ for path in cls.cleanup_paths:
+ shutil.rmtree(path)
+
+ def testAlgorithmCompliance(self):
+ for p in QgsApplication.processingRegistry().providers():
+ print('testing provider {}'.format(p.id()))
+ for a in p.algorithms():
+ print('testing algorithm {}'.format(a.id()))
+ self.check_algorithm(a)
+
+ def check_algorithm(self, alg):
+ # check that calling helpUrl() works without error
+ alg.helpUrl()
+
+
+if __name__ == '__main__':
+ nose2.main()
diff --git a/python/plugins/otbprovider/tests/CMakeLists.txt b/python/plugins/otbprovider/tests/CMakeLists.txt
new file mode 100644
index 000000000000..8a67dd5c3fb8
--- /dev/null
+++ b/python/plugins/otbprovider/tests/CMakeLists.txt
@@ -0,0 +1,8 @@
+file(GLOB PY_FILES *.py)
+
+PLUGIN_INSTALL(otbprovider tests ${PY_FILES})
+
+if(ENABLE_TESTS)
+ include(UsePythonTest)
+ ADD_PYTHON_TEST(ProcessingOtbAlgorithmsTest OtbAlgorithmsTest.py)
+endif()
diff --git a/python/plugins/processing/tests/OtbAlgorithmsTest.py b/python/plugins/otbprovider/tests/OtbAlgorithmsTest.py
similarity index 96%
rename from python/plugins/processing/tests/OtbAlgorithmsTest.py
rename to python/plugins/otbprovider/tests/OtbAlgorithmsTest.py
index ceb8a38b265d..317418d31e2a 100644
--- a/python/plugins/processing/tests/OtbAlgorithmsTest.py
+++ b/python/plugins/otbprovider/tests/OtbAlgorithmsTest.py
@@ -45,10 +45,10 @@
from processing.gui.BatchAlgorithmDialog import BatchAlgorithmDialog
from processing.gui.wrappers import WidgetWrapperFactory
from processing.modeler.ModelerParametersDialog import ModelerParametersDialog
-from processing.algs.otb.OtbAlgorithm import OtbAlgorithm
-from processing.algs.otb.OtbAlgorithmProvider import OtbAlgorithmProvider
-from processing.algs.otb.OtbUtils import OtbUtils
-from processing.algs.otb.OtbChoiceWidget import OtbParameterChoice, OtbChoiceWidgetWrapper
+from otbprovider.OtbAlgorithm import OtbAlgorithm
+from otbprovider.OtbAlgorithmProvider import OtbAlgorithmProvider
+from otbprovider.OtbUtils import OtbUtils
+from otbprovider.OtbChoiceWidget import OtbParameterChoice, OtbChoiceWidgetWrapper
import AlgorithmsTestBase
import processing
@@ -226,7 +226,8 @@ def setUpClass(cls):
start_app()
from processing.core.Processing import Processing
Processing.initialize()
- ProcessingConfig.setSettingValue("OTB_ACTIVATE", True)
+ cls.provider = OtbAlgorithmProvider()
+ QgsApplication.processingRegistry().addProvider(cls.provider)
ProcessingConfig.setSettingValue(OtbUtils.FOLDER, OTB_INSTALL_DIR)
ProcessingConfig.setSettingValue(OtbUtils.APP_FOLDER, os.path.join(OTB_INSTALL_DIR, 'lib', 'otb', 'applications'))
ProcessingConfig.readSettings()
@@ -241,6 +242,7 @@ def setUpClass(cls):
def tearDownClass(cls):
from processing.core.Processing import Processing
Processing.deinitialize()
+ QgsApplication.processingRegistry().removeProvider(cls.provider)
for path in cls.cleanup_paths:
shutil.rmtree(path)
diff --git a/python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml b/python/plugins/otbprovider/tests/testdata/otb_algorithm_tests.yaml
similarity index 100%
rename from python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml
rename to python/plugins/otbprovider/tests/testdata/otb_algorithm_tests.yaml
diff --git a/python/plugins/otbprovider/tests/testdata/polys.gfs b/python/plugins/otbprovider/tests/testdata/polys.gfs
new file mode 100644
index 000000000000..8b7950032e25
--- /dev/null
+++ b/python/plugins/otbprovider/tests/testdata/polys.gfs
@@ -0,0 +1,31 @@
+
+
+ polys2
+ polys2
+ 3
+ EPSG:4326
+
+ 6
+ -1.00000
+ 10.00000
+ -3.00000
+ 6.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/otbprovider/tests/testdata/polys.gml b/python/plugins/otbprovider/tests/testdata/polys.gml
new file mode 100644
index 000000000000..346f73c4e107
--- /dev/null
+++ b/python/plugins/otbprovider/tests/testdata/polys.gml
@@ -0,0 +1,58 @@
+
+
+
+
+ -1-3
+ 106
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+
+
+
+
+ 5,5 6,4 4,4 5,5
+ Aaaaa
+ -33
+ 0
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,5
+ bbaaa
+ 0.123
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+
+
+
+
+ 120
+ -100291.43213
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+
+
+
diff --git a/python/plugins/otbprovider/tests/testdata/raster with spaces.tif b/python/plugins/otbprovider/tests/testdata/raster with spaces.tif
new file mode 100644
index 000000000000..14ad3684b279
Binary files /dev/null and b/python/plugins/otbprovider/tests/testdata/raster with spaces.tif differ
diff --git a/python/plugins/otbprovider/tests/testdata/raster with spaces.tif.aux.xml b/python/plugins/otbprovider/tests/testdata/raster with spaces.tif.aux.xml
new file mode 100644
index 000000000000..b80b3ef02d6c
--- /dev/null
+++ b/python/plugins/otbprovider/tests/testdata/raster with spaces.tif.aux.xml
@@ -0,0 +1,11 @@
+
+
+
+ 899
+ 865.86666666667
+ 826
+ 17.808206597584
+ 53.57
+
+
+
diff --git a/python/plugins/otbprovider/tests/testdata/raster.tif b/python/plugins/otbprovider/tests/testdata/raster.tif
new file mode 100644
index 000000000000..14ad3684b279
Binary files /dev/null and b/python/plugins/otbprovider/tests/testdata/raster.tif differ
diff --git a/python/plugins/otbprovider/tests/testdata/raster.tif.aux.xml b/python/plugins/otbprovider/tests/testdata/raster.tif.aux.xml
new file mode 100644
index 000000000000..b80b3ef02d6c
--- /dev/null
+++ b/python/plugins/otbprovider/tests/testdata/raster.tif.aux.xml
@@ -0,0 +1,11 @@
+
+
+
+ 899
+ 865.86666666667
+ 826
+ 17.808206597584
+ 53.57
+
+
+
diff --git a/python/plugins/processing/tests/CMakeLists.txt b/python/plugins/processing/tests/CMakeLists.txt
index 415f8d858d0a..90ef16c2ba41 100644
--- a/python/plugins/processing/tests/CMakeLists.txt
+++ b/python/plugins/processing/tests/CMakeLists.txt
@@ -31,7 +31,6 @@ if(ENABLE_TESTS)
ADD_PYTHON_TEST(ProcessingGrass7AlgorithmsRasterTestPt1 Grass7AlgorithmsRasterTestPt1.py)
ADD_PYTHON_TEST(ProcessingGrass7AlgorithmsRasterTestPt2 Grass7AlgorithmsRasterTestPt2.py)
ADD_PYTHON_TEST(ProcessingGrass7AlgorithmsVectorTest Grass7AlgorithmsVectorTest.py)
- ADD_PYTHON_TEST(ProcessingOtbAlgorithmsTest OtbAlgorithmsTest.py)
ADD_PYTHON_TEST(ProcessingCheckValidityAlgorithmTest CheckValidityAlgorithm.py)
ADD_PYTHON_TEST(ProcessingScriptUtilsTest ScriptUtilsTest.py)
if(ENABLE_SAGA_TESTS)