TestTools.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. """
  2. ***************************************************************************
  3. TestTools.py
  4. ---------------------
  5. Date : February 2013
  6. Copyright : (C) 2013 by Victor Olaya
  7. Email : volayaf at gmail dot com
  8. ***************************************************************************
  9. * *
  10. * This program is free software; you can redistribute it and/or modify *
  11. * it under the terms of the GNU General Public License as published by *
  12. * the Free Software Foundation; either version 2 of the License, or *
  13. * (at your option) any later version. *
  14. * *
  15. ***************************************************************************
  16. """
  17. __author__ = 'Victor Olaya'
  18. __date__ = 'February 2013'
  19. __copyright__ = '(C) 2013, Victor Olaya'
  20. import os
  21. import posixpath
  22. import re
  23. import yaml
  24. import hashlib
  25. import ast
  26. from osgeo import gdal
  27. from osgeo.gdalconst import GA_ReadOnly
  28. from numpy import nan_to_num
  29. from qgis.core import (QgsApplication,
  30. QgsProcessing,
  31. QgsProcessingParameterDefinition,
  32. QgsProcessingParameterBoolean,
  33. QgsProcessingParameterNumber,
  34. QgsProcessingParameterDistance,
  35. QgsProcessingParameterDuration,
  36. QgsProcessingParameterFile,
  37. QgsProcessingParameterBand,
  38. QgsProcessingParameterString,
  39. QgsProcessingParameterVectorLayer,
  40. QgsProcessingParameterFeatureSource,
  41. QgsProcessingParameterRasterLayer,
  42. QgsProcessingParameterMultipleLayers,
  43. QgsProcessingParameterRasterDestination,
  44. QgsProcessingParameterFeatureSink,
  45. QgsProcessingParameterVectorDestination,
  46. QgsProcessingParameterFileDestination,
  47. QgsProcessingParameterEnum)
  48. from qgis.PyQt.QtCore import QCoreApplication, QMetaObject
  49. from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QMessageBox
  50. def extractSchemaPath(filepath):
  51. """
  52. Tries to find where the file is relative to the QGIS source code directory.
  53. If it is already placed in the processing or QGIS testdata directory it will
  54. return an appropriate schema and relative filepath
  55. Args:
  56. filepath: The path of the file to examine
  57. Returns:
  58. A tuple (schema, relative_file_path) where the schema is 'qgs' or 'proc'
  59. if we can assume that the file is in this testdata directory.
  60. """
  61. parts = []
  62. schema = None
  63. localpath = ''
  64. path = filepath
  65. part = True
  66. while part and filepath:
  67. (path, part) = os.path.split(path)
  68. if part == 'testdata' and not localpath:
  69. localparts = parts
  70. localparts.reverse()
  71. # we always want posix style paths here
  72. localpath = posixpath.join(*localparts)
  73. parts.append(part)
  74. parts.reverse()
  75. try:
  76. testsindex = parts.index('tests')
  77. except ValueError:
  78. return '', filepath
  79. if parts[testsindex - 1] == 'processing':
  80. schema = 'proc'
  81. return schema, localpath
  82. def parseParameters(command):
  83. """
  84. Parse alg string to grab parameters value.
  85. Can handle quotes and comma.
  86. """
  87. pos = 0
  88. exp = re.compile(r"""(['"]?)(.*?)\1(,|$)""")
  89. while True:
  90. m = exp.search(command, pos)
  91. result = m.group(2)
  92. separator = m.group(3)
  93. # Handle special values:
  94. if result == 'None':
  95. result = None
  96. elif result.lower() == str(True).lower():
  97. result = True
  98. elif result.lower() == str(False).lower():
  99. result = False
  100. yield result
  101. if not separator:
  102. break
  103. pos = m.end(0)
  104. def splitAlgIdAndParameters(command):
  105. """
  106. Extracts the algorithm ID and input parameter list from a processing runalg command
  107. """
  108. exp = re.compile(r"""['"](.*?)['"]\s*,\s*(.*)""")
  109. m = exp.search(command[len('processing.run('):-1])
  110. alg_id = m.group(1)
  111. params = m.group(2)
  112. # replace QgsCoordinateReferenceSystem('EPSG:4325') with just string value
  113. exp = re.compile(r"""QgsCoordinateReferenceSystem\((['"].*?['"])\)""")
  114. params = exp.sub('\\1', params)
  115. return alg_id, ast.literal_eval(params)
  116. def createTest(text):
  117. definition = {}
  118. alg_id, parameters = splitAlgIdAndParameters(text)
  119. alg = QgsApplication.processingRegistry().createAlgorithmById(alg_id)
  120. definition['name'] = f'Test ({alg_id})'
  121. definition['algorithm'] = alg_id
  122. params = {}
  123. results = {}
  124. i = 0
  125. for param in alg.parameterDefinitions():
  126. if param.flags() & QgsProcessingParameterDefinition.FlagHidden or param.isDestination():
  127. continue
  128. if not param.name() in parameters:
  129. continue
  130. i += 1
  131. token = parameters[param.name()]
  132. # Handle empty parameters that are optionals
  133. if param.flags() & QgsProcessingParameterDefinition.FlagOptional and token is None:
  134. continue
  135. if isinstance(param, (QgsProcessingParameterVectorLayer, QgsProcessingParameterFeatureSource)):
  136. schema, filepath = extractSchemaPath(token)
  137. p = {
  138. 'type': 'vector',
  139. 'name': filepath
  140. }
  141. if not schema:
  142. p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
  143. params[param.name()] = p
  144. elif isinstance(param, QgsProcessingParameterRasterLayer):
  145. schema, filepath = extractSchemaPath(token)
  146. p = {
  147. 'type': 'raster',
  148. 'name': filepath
  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. params[param.name()] = p
  153. elif isinstance(param, QgsProcessingParameterMultipleLayers):
  154. multiparams = token
  155. newparam = []
  156. # Handle datatype detection
  157. dataType = param.layerType()
  158. if dataType in [QgsProcessing.TypeVectorAnyGeometry, QgsProcessing.TypeVectorPoint, QgsProcessing.TypeVectorLine, QgsProcessing.TypeVectorPolygon, QgsProcessing.TypeVector]:
  159. dataType = 'vector'
  160. else:
  161. dataType = 'raster'
  162. schema = None
  163. for mp in multiparams:
  164. schema, filepath = extractSchemaPath(mp)
  165. newparam.append({
  166. 'type': dataType,
  167. 'name': filepath
  168. })
  169. p = {
  170. 'type': 'multi',
  171. 'params': newparam
  172. }
  173. if not schema:
  174. p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
  175. params[param.name()] = p
  176. elif isinstance(param, QgsProcessingParameterFile):
  177. schema, filepath = extractSchemaPath(token)
  178. p = {
  179. 'type': 'file',
  180. 'name': filepath
  181. }
  182. if not schema:
  183. p['location'] = '[The source data is not in the testdata directory. Please use data in the processing/tests/testdata folder.]'
  184. params[param.name()] = p
  185. elif isinstance(param, QgsProcessingParameterString):
  186. params[param.name()] = token
  187. elif isinstance(param, QgsProcessingParameterBoolean):
  188. params[param.name()] = token
  189. elif isinstance(param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance)):
  190. if param.dataType() == QgsProcessingParameterNumber.Integer:
  191. params[param.name()] = int(token)
  192. else:
  193. params[param.name()] = float(token)
  194. elif isinstance(param, QgsProcessingParameterEnum):
  195. if isinstance(token, list):
  196. params[param.name()] = [int(t) for t in token]
  197. else:
  198. params[param.name()] = int(token)
  199. elif isinstance(param, QgsProcessingParameterBand):
  200. params[param.name()] = int(token)
  201. elif token:
  202. if token[0] == '"':
  203. token = token[1:]
  204. if token[-1] == '"':
  205. token = token[:-1]
  206. params[param.name()] = token
  207. definition['params'] = params
  208. for i, out in enumerate([out for out in alg.destinationParameterDefinitions() if not out.flags() & QgsProcessingParameterDefinition.FlagHidden]):
  209. if not out.name() in parameters:
  210. continue
  211. token = parameters[out.name()]
  212. if isinstance(out, QgsProcessingParameterRasterDestination):
  213. if token is None:
  214. QMessageBox.warning(None,
  215. tr('Error'),
  216. tr('Seems some outputs are temporary '
  217. 'files. To create test you need to '
  218. 'redirect all algorithm outputs to '
  219. 'files'))
  220. return
  221. dataset = gdal.Open(token, GA_ReadOnly)
  222. if dataset is None:
  223. QMessageBox.warning(None,
  224. tr('Error'),
  225. tr('Seems some outputs are temporary '
  226. 'files. To create test you need to '
  227. 'redirect all algorithm outputs to '
  228. 'files'))
  229. return
  230. dataArray = nan_to_num(dataset.ReadAsArray(0))
  231. strhash = hashlib.sha224(dataArray.data).hexdigest()
  232. results[out.name()] = {
  233. 'type': 'rasterhash',
  234. 'hash': strhash
  235. }
  236. elif isinstance(out, (QgsProcessingParameterVectorDestination, QgsProcessingParameterFeatureSink)):
  237. schema, filepath = extractSchemaPath(token)
  238. results[out.name()] = {
  239. 'type': 'vector',
  240. 'name': filepath
  241. }
  242. if not schema:
  243. 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.]'
  244. elif isinstance(out, QgsProcessingParameterFileDestination):
  245. schema, filepath = extractSchemaPath(token)
  246. results[out.name()] = {
  247. 'type': 'file',
  248. 'name': filepath
  249. }
  250. if not schema:
  251. results[out.name()]['location'] = '[The expected result file is not in the testdata directory. Please redirect the output to processing/tests/testdata/expected.]'
  252. definition['results'] = results
  253. dlg = ShowTestDialog(yaml.dump([definition], default_flow_style=False))
  254. dlg.exec_()
  255. def tr(string):
  256. return QCoreApplication.translate('TestTools', string)
  257. class ShowTestDialog(QDialog):
  258. def __init__(self, s):
  259. QDialog.__init__(self)
  260. self.setModal(True)
  261. self.resize(600, 400)
  262. self.setWindowTitle(self.tr('Unit Test'))
  263. layout = QVBoxLayout()
  264. self.text = QTextEdit()
  265. self.text.setFontFamily("monospace")
  266. self.text.setEnabled(True)
  267. # Add two spaces in front of each text for faster copy/paste
  268. self.text.setText(' {}'.format(s.replace('\n', '\n ')))
  269. layout.addWidget(self.text)
  270. self.setLayout(layout)
  271. QMetaObject.connectSlotsByName(self)