invoker.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # -*- coding: utf-8 -*-
  2. """!@package grass.gunittest.invoker
  3. @brief GRASS Python testing framework test files invoker (runner)
  4. Copyright (C) 2014 by the GRASS Development Team
  5. This program is free software under the GNU General Public
  6. License (>=v2). Read the file COPYING that comes with GRASS GIS
  7. for details.
  8. @author Vaclav Petras
  9. """
  10. import os
  11. import sys
  12. import shutil
  13. import string
  14. import subprocess
  15. from .checkers import text_to_keyvalue
  16. from .loader import GrassTestLoader, discover_modules
  17. from .reporters import (GrassTestFilesMultiReporter,
  18. GrassTestFilesTextReporter, GrassTestFilesHtmlReporter,
  19. TestsuiteDirReporter, GrassTestFilesKeyValueReporter,
  20. get_svn_path_authors,
  21. NoopFileAnonymizer, keyvalue_to_text)
  22. from .utils import silent_rmtree, ensure_dir
  23. # needed for write_gisrc
  24. # TODO: it would be good to find some way of writing rc without the need to
  25. # have GRASS proprly set (anything from grass.script requires translations to
  26. # be set, i.e. the GRASS environment properly set)
  27. import grass.script.setup as gsetup
  28. import collections
  29. # TODO: this might be more extend then update
  30. def update_keyval_file(filename, module, returncode):
  31. if os.path.exists(filename):
  32. with open(filename, 'r') as keyval_file:
  33. keyval = text_to_keyvalue(keyval_file.read(), sep='=')
  34. else:
  35. keyval = {}
  36. # this is for one file
  37. test_file_authors = get_svn_path_authors(module.abs_file_path)
  38. # in case that SVN is not available use empty authors
  39. if test_file_authors is None:
  40. test_file_authors = ''
  41. # always owerwrite name and status
  42. keyval['name'] = module.name
  43. keyval['tested_dir'] = module.tested_dir
  44. if 'status' not in keyval.keys():
  45. keyval['status'] = 'failed' if returncode else 'passed'
  46. keyval['returncode'] = returncode
  47. keyval['test_file_authors'] = test_file_authors
  48. with open(filename, 'w') as keyval_file:
  49. keyval_file.write(keyvalue_to_text(keyval))
  50. return keyval
  51. class GrassTestFilesInvoker(object):
  52. """A class used to invoke test files and create the main report"""
  53. # TODO: it is not clear what clean_outputs mean, if should be split
  54. # std stream, random outputs, saved results, profiling
  55. # not stdout and stderr if they contain test results
  56. # we can also save only failed tests, or generate only if assert fails
  57. def __init__(self, start_dir,
  58. clean_mapsets=True, clean_outputs=True, clean_before=True,
  59. testsuite_dir='testsuite', file_anonymizer=None):
  60. """
  61. :param bool clean_mapsets: if the mapsets should be removed
  62. :param bool clean_outputs: meaning is unclear: random tests outputs,
  63. saved images from maps, profiling?
  64. :param bool clean_before: if mapsets, outputs, and results
  65. should be removed before the tests start
  66. (advantageous when the previous run left everything behind)
  67. """
  68. self.start_dir = start_dir
  69. self.clean_mapsets = clean_mapsets
  70. self.clean_outputs = clean_outputs
  71. self.clean_before = clean_before
  72. self.testsuite_dir = testsuite_dir # TODO: solve distribution of this constant
  73. # reporter is created for each call of run_in_location()
  74. self.reporter = None
  75. self.testsuite_dirs = None
  76. if file_anonymizer is None:
  77. self._file_anonymizer = NoopFileAnonymizer()
  78. else:
  79. self._file_anonymizer = file_anonymizer
  80. def _create_mapset(self, gisdbase, location, module):
  81. """Create mapset according to informations in module.
  82. :param loader.GrassTestPythonModule module:
  83. """
  84. # using path.sep but also / and \ for cases when it is confused
  85. # (namely the case of Unix path on MS Windows)
  86. # replace . to get rid of unclean path
  87. # TODO: clean paths
  88. # note that backslash cannot be at the end of raw string
  89. dir_as_name = module.tested_dir.translate(string.maketrans(r'/\.', '___'))
  90. mapset = dir_as_name + '_' + module.name
  91. # TODO: use grass module to do this? but we are not in the right gisdbase
  92. mapset_dir = os.path.join(gisdbase, location, mapset)
  93. if self.clean_before:
  94. silent_rmtree(mapset_dir)
  95. os.mkdir(mapset_dir)
  96. # TODO: default region in mapset will be what?
  97. # copy WIND file from PERMANENT
  98. # TODO: this should be a function in grass.script (used also in gis_set.py, PyGRASS also has its way with Mapset)
  99. # TODO: are premisions an issue here?
  100. shutil.copy(os.path.join(gisdbase, location, 'PERMANENT', 'WIND'),
  101. os.path.join(mapset_dir))
  102. return mapset, mapset_dir
  103. def _run_test_module(self, module, results_dir, gisdbase, location):
  104. """Run one test file."""
  105. self.testsuite_dirs[module.tested_dir].append(module.name)
  106. cwd = os.path.join(results_dir, module.tested_dir, module.name)
  107. data_dir = os.path.join(module.file_dir, 'data')
  108. if os.path.exists(data_dir):
  109. # TODO: link dir instead of copy tree and remove link afterwads
  110. # (removing is good because of testsuite dir in samplecode)
  111. # TODO: use different dir name in samplecode and test if it works
  112. shutil.copytree(data_dir, os.path.join(cwd, 'data'),
  113. ignore=shutil.ignore_patterns('*.svn*'))
  114. ensure_dir(os.path.abspath(cwd))
  115. # TODO: put this to constructor and copy here again
  116. env = os.environ.copy()
  117. mapset, mapset_dir = self._create_mapset(gisdbase, location, module)
  118. gisrc = gsetup.write_gisrc(gisdbase, location, mapset)
  119. env['GISRC'] = gisrc
  120. stdout_path = os.path.join(cwd, 'stdout.txt')
  121. stderr_path = os.path.join(cwd, 'stderr.txt')
  122. stdout = open(stdout_path, 'w')
  123. stderr = open(stderr_path, 'w')
  124. self.reporter.start_file_test(module)
  125. # TODO: we might clean the directory here before test if non-empty
  126. if module.file_type == 'py':
  127. # ignoring shebang line to use current Python
  128. # and also pass parameters to it
  129. # add also '-Qwarn'?
  130. p = subprocess.Popen([sys.executable, '-tt', '-3',
  131. module.abs_file_path],
  132. cwd=cwd, env=env,
  133. stdout=stdout, stderr=stderr)
  134. elif module.file_type == 'sh':
  135. # ignoring shebang line to pass parameters to shell
  136. # expecting system to have sh or something compatible
  137. # TODO: add some special checks for MS Windows
  138. # using -x to see commands in stderr
  139. # using -e to terminate fast
  140. # from dash manual:
  141. # -e errexit If not interactive, exit immediately if any
  142. # untested command fails. The exit status of a com‐
  143. # mand is considered to be explicitly tested if the
  144. # command is used to control an if, elif, while, or
  145. # until; or if the command is the left hand operand
  146. # of an '&&' or '||' operator.
  147. p = subprocess.Popen(['sh', '-e', '-x', module.abs_file_path],
  148. cwd=cwd, env=env,
  149. stdout=stdout, stderr=stderr)
  150. else:
  151. p = subprocess.Popen([module.abs_file_path],
  152. cwd=cwd, env=env,
  153. stdout=stdout, stderr=stderr)
  154. returncode = p.wait()
  155. stdout.close()
  156. stderr.close()
  157. self._file_anonymizer.anonymize([stdout_path, stderr_path])
  158. test_summary = update_keyval_file(
  159. os.path.join(cwd, 'test_keyvalue_result.txt'),
  160. module=module, returncode=returncode)
  161. self.reporter.end_file_test(module=module, cwd=cwd,
  162. returncode=returncode,
  163. stdout=stdout_path, stderr=stderr_path,
  164. test_summary=test_summary)
  165. # TODO: add some try-except or with for better error handling
  166. os.remove(gisrc)
  167. # TODO: only if clean up
  168. if self.clean_mapsets:
  169. shutil.rmtree(mapset_dir)
  170. def run_in_location(self, gisdbase, location, location_type,
  171. results_dir):
  172. """Run tests in a given location"""
  173. if os.path.abspath(results_dir) == os.path.abspath(self.start_dir):
  174. raise RuntimeError("Results root directory should not be the same"
  175. " as discovery start directory")
  176. self.reporter = GrassTestFilesMultiReporter(
  177. reporters=[
  178. GrassTestFilesTextReporter(stream=sys.stderr),
  179. GrassTestFilesHtmlReporter(
  180. file_anonymizer=self._file_anonymizer,
  181. main_page_name='testfiles.html'),
  182. GrassTestFilesKeyValueReporter(
  183. info=dict(location=location, location_type=location_type))
  184. ])
  185. self.testsuite_dirs = collections.defaultdict(list) # reset list of dirs each time
  186. # TODO: move constants out of loader class or even module
  187. modules = discover_modules(start_dir=self.start_dir,
  188. grass_location=location_type,
  189. file_regexp=r'.*\.(py|sh)$',
  190. skip_dirs=GrassTestLoader.skip_dirs,
  191. testsuite_dir=GrassTestLoader.testsuite_dir,
  192. all_locations_value=GrassTestLoader.all_tests_value,
  193. universal_location_value=GrassTestLoader.universal_tests_value,
  194. import_modules=False)
  195. self.reporter.start(results_dir)
  196. for module in modules:
  197. self._run_test_module(module=module, results_dir=results_dir,
  198. gisdbase=gisdbase, location=location)
  199. self.reporter.finish()
  200. # TODO: move this to some (new?) reporter
  201. # TODO: add basic summary of linked files so that the page is not empty
  202. with open(os.path.join(results_dir, 'index.html'), 'w') as main_index:
  203. main_index.write(
  204. '<html><body>'
  205. '<h1>Tests for &lt;{location}&gt;'
  206. ' using &lt;{type}&gt; type tests</h1>'
  207. '<ul>'
  208. '<li><a href="testsuites.html">Results by testsuites</a>'
  209. ' (testsuite directories)</li>'
  210. '<li><a href="testfiles.html">Results by test files</a></li>'
  211. '<ul>'
  212. '</body></html>'
  213. .format(location=location, type=location_type))
  214. testsuite_dir_reporter = TestsuiteDirReporter(
  215. main_page_name='testsuites.html', testsuite_page_name='index.html',
  216. top_level_testsuite_page_name='testsuite_index.html')
  217. testsuite_dir_reporter.report_for_dirs(root=results_dir,
  218. directories=self.testsuite_dirs)