invoker.py 11 KB

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