invoker.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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 unittest.main import TestProgram, USAGE_AS_MAIN
  16. TestProgram.USAGE = USAGE_AS_MAIN
  17. from .checkers import text_to_keyvalue
  18. from .loader import GrassTestLoader, discover_modules
  19. from .reporters import (GrassTestFilesMultiReporter,
  20. GrassTestFilesTextReporter,
  21. GrassTestFilesHtmlReporter)
  22. from .utils import silent_rmtree, ensure_dir
  23. import grass.script.setup as gsetup
  24. import collections
  25. import types
  26. # TODO: change text_to_keyvalue to same sep as here
  27. def keyvalue_to_text(keyvalue, sep='=', vsep='\n', isep=',',
  28. last_vertical=None):
  29. if not last_vertical:
  30. last_vertical = vsep == '\n'
  31. items = []
  32. for key, value in keyvalue.iteritems():
  33. # TODO: use isep for iterables other than strings
  34. if (not isinstance(value, types.StringTypes)
  35. and isinstance(value, collections.Iterable)):
  36. # TODO: this does not work for list of non-strings
  37. value = isep.join(value)
  38. items.append('{key}{sep}{value}'.format(
  39. key=key, sep=sep, value=value))
  40. text = vsep.join(items)
  41. if last_vertical:
  42. text = text + vsep
  43. return text
  44. def update_keyval_file(filename, module, returncode):
  45. if os.path.exists(filename):
  46. with open(filename, 'r') as keyval_file:
  47. keyval = text_to_keyvalue(keyval_file.read(), sep='=')
  48. else:
  49. keyval = {}
  50. # always owerwrite name and ok
  51. keyval['name'] = module.name
  52. keyval['tested_dir'] = module.tested_dir
  53. if 'status' not in keyval.keys():
  54. keyval['status'] = 'failed' if returncode else 'passed'
  55. keyval['returncode'] = returncode
  56. with open(filename, 'w') as keyval_file:
  57. keyval_file.write(keyvalue_to_text(keyval))
  58. return keyval
  59. class GrassTestFilesInvoker(object):
  60. """A class used to invoke test files and create the main report"""
  61. # TODO: it is not clear what clean_outputs mean, if should be split
  62. # std stream, random outputs, saved results, profiling
  63. # not stdout and stderr if they contain test results
  64. # we can also save only failed tests, or generate only if assert fails
  65. def __init__(self, start_dir,
  66. clean_mapsets=True, clean_outputs=True, clean_before=True,
  67. testsuite_dir='testsuite'):
  68. """
  69. :param bool clean_mapsets: if the mapsets should be removed
  70. :param bool clean_outputs: meaning is unclear: random tests outputs,
  71. saved images from maps, profiling?
  72. :param bool clean_before: if mapsets, outputs, and results
  73. should be removed before the tests start
  74. (advantageous when the previous run left everything behind)
  75. """
  76. self.start_dir = start_dir
  77. self.clean_mapsets = clean_mapsets
  78. self.clean_outputs = clean_outputs
  79. self.clean_before = clean_before
  80. self.testsuite_dir = testsuite_dir
  81. # reporter is created for each call of run_in_location()
  82. self.reporter = None
  83. def _create_mapset(self, gisdbase, location, module):
  84. """Create mapset according to informations in module.
  85. :param loader.GrassTestPythonModule module:
  86. """
  87. # using path.sep but also / and \ for cases when it is confused
  88. # (namely the case of Unix path on MS Windows)
  89. # replace . to get rid of unclean path
  90. # TODO: clean paths
  91. # note that backslash cannot be at the end of raw string
  92. dir_as_name = module.tested_dir.translate(string.maketrans(r'/\.', '___'))
  93. mapset = dir_as_name + '_' + module.name
  94. # TODO: use grass module to do this? but we are not in the right gisdbase
  95. mapset_dir = os.path.join(gisdbase, location, mapset)
  96. if self.clean_before:
  97. silent_rmtree(mapset_dir)
  98. os.mkdir(mapset_dir)
  99. # TODO: default region in mapset will be what?
  100. # copy WIND file from PERMANENT
  101. # TODO: this should be a function in grass.script (used also in gis_set.py, PyGRASS also has its way with Mapset)
  102. # TODO: are premisions an issue here?
  103. shutil.copy(os.path.join(gisdbase, location, 'PERMANENT', 'WIND'),
  104. os.path.join(mapset_dir))
  105. return mapset, mapset_dir
  106. def _run_test_module(self, module, results_dir, gisdbase, location):
  107. """Run one test file."""
  108. cwd = os.path.join(results_dir, module.tested_dir, module.name)
  109. data_dir = os.path.join(module.file_dir, 'data')
  110. if os.path.exists(data_dir):
  111. # TODO: link dir intead of copy tree
  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. # TODO: use some grass function to run?
  127. # add also '-Qwarn'?
  128. p = subprocess.Popen([sys.executable, '-tt', '-3',
  129. module.abs_file_path],
  130. cwd=cwd, env=env,
  131. stdout=stdout, stderr=stderr)
  132. returncode = p.wait()
  133. stdout.close()
  134. stderr.close()
  135. test_summary = update_keyval_file(
  136. os.path.join(cwd, 'test_keyvalue_result.txt'),
  137. module=module, returncode=returncode)
  138. self.reporter.end_file_test(module=module, cwd=cwd,
  139. returncode=returncode,
  140. stdout=stdout_path, stderr=stderr_path,
  141. test_summary=test_summary)
  142. # TODO: add some try-except or with for better error handling
  143. os.remove(gisrc)
  144. # TODO: only if clean up
  145. if self.clean_mapsets:
  146. shutil.rmtree(mapset_dir)
  147. def run_in_location(self, gisdbase, location, location_shortcut,
  148. results_dir):
  149. """Run tests in a given location"""
  150. if os.path.abspath(results_dir) == os.path.abspath(self.start_dir):
  151. raise RuntimeError("Results root directory should not be the same"
  152. " as discovery start directory")
  153. self.reporter = GrassTestFilesMultiReporter(
  154. reporters=[
  155. GrassTestFilesTextReporter(stream=sys.stderr),
  156. GrassTestFilesHtmlReporter(),
  157. ])
  158. # TODO: move constants out of loader class or even module
  159. modules = discover_modules(start_dir=self.start_dir,
  160. grass_location=location_shortcut,
  161. file_pattern=GrassTestLoader.files_in_testsuite,
  162. skip_dirs=GrassTestLoader.skip_dirs,
  163. testsuite_dir=GrassTestLoader.testsuite_dir,
  164. all_locations_value=GrassTestLoader.all_tests_value,
  165. universal_location_value=GrassTestLoader.universal_tests_value,
  166. import_modules=False)
  167. self.reporter.start(results_dir)
  168. for module in modules:
  169. self._run_test_module(module=module, results_dir=results_dir,
  170. gisdbase=gisdbase, location=location)
  171. self.reporter.finish()