loader.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """
  2. GRASS Python testing framework test loading functionality
  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 fnmatch
  11. import unittest
  12. import collections
  13. import re
  14. def fnmatch_exclude_with_base(files, base, exclude):
  15. """Return list of files not matching any exclusion pattern
  16. :param files: list of file names
  17. :param base: directory (path) where the files are
  18. :param exclude: list of fnmatch glob patterns for exclusion
  19. """
  20. not_excluded = []
  21. patterns = []
  22. # Make all dir separators slashes and drop leading current dir
  23. # for both patterns and (later) for files.
  24. for pattern in exclude:
  25. pattern = pattern.replace(os.sep, "/")
  26. if pattern.startswith("./"):
  27. patterns.append(pattern[2:])
  28. else:
  29. patterns.append(pattern)
  30. for filename in files:
  31. full_file_path = os.path.join(base, filename)
  32. test_filename = full_file_path.replace(os.sep, "/")
  33. if full_file_path.startswith("./"):
  34. test_filename = full_file_path[2:]
  35. matches = False
  36. for pattern in patterns:
  37. if fnmatch.fnmatch(test_filename, pattern):
  38. matches = True
  39. break
  40. if not matches:
  41. not_excluded.append(filename)
  42. return not_excluded
  43. # TODO: resolve test file versus test module
  44. GrassTestPythonModule = collections.namedtuple(
  45. "GrassTestPythonModule",
  46. [
  47. "name",
  48. "module",
  49. "file_type",
  50. "tested_dir",
  51. "file_dir",
  52. "file_path",
  53. "abs_file_path",
  54. ],
  55. )
  56. # TODO: implement loading without the import
  57. def discover_modules(
  58. start_dir,
  59. skip_dirs,
  60. testsuite_dir,
  61. grass_location,
  62. all_locations_value,
  63. universal_location_value,
  64. import_modules,
  65. add_failed_imports=True,
  66. file_pattern=None,
  67. file_regexp=None,
  68. exclude=None,
  69. ):
  70. """Find all test files (modules) in a directory tree.
  71. The function is designed specifically for GRASS testing framework
  72. test layout. It expects some directories to have a "testsuite"
  73. directory where test files (test modules) are present.
  74. Additionally, it also handles loading of test files which specify
  75. in which location they can run.
  76. :param start_dir: directory to start the search
  77. :param file_pattern: pattern of files in a test suite directory
  78. (using Unix shell-style wildcards)
  79. :param skip_dirs: directories not to recurse to (e.g. ``.svn``)
  80. :param testsuite_dir: name of directory where the test files are found,
  81. the function will not recurse to this directory
  82. :param grass_location: string with an accepted location type (category, shortcut)
  83. :param all_locations_value: string used to say that all locations
  84. should be loaded (grass_location can be set to this value)
  85. :param universal_location_value: string marking a test as
  86. location-independent (same as not providing any)
  87. :param import_modules: True if files should be imported as modules,
  88. False if the files should be just searched for the needed values
  89. :returns: a list of GrassTestPythonModule objects
  90. .. todo::
  91. Implement import_modules.
  92. """
  93. modules = []
  94. for root, dirs, unused_files in os.walk(start_dir, topdown=True):
  95. dirs.sort()
  96. for dir_pattern in skip_dirs:
  97. to_skip = fnmatch.filter(dirs, dir_pattern)
  98. for skip in to_skip:
  99. dirs.remove(skip)
  100. if testsuite_dir in dirs:
  101. dirs.remove(testsuite_dir) # do not recurse to testsuite
  102. full = os.path.join(root, testsuite_dir)
  103. all_files = os.listdir(full)
  104. if file_pattern:
  105. files = fnmatch.filter(all_files, file_pattern)
  106. if file_regexp:
  107. files = [f for f in all_files if re.match(file_regexp, f)]
  108. if exclude:
  109. files = fnmatch_exclude_with_base(files, full, exclude)
  110. files = sorted(files)
  111. # get test/module name without .py
  112. # extpecting all files to end with .py
  113. # this will not work for invoking bat files but it works fine
  114. # as long as we handle only Python files (and using Python
  115. # interpreter for invoking)
  116. # TODO: warning about no tests in a testsuite
  117. # (in what way?)
  118. for file_name in files:
  119. # TODO: add also import if requested
  120. # (see older versions of this file)
  121. # TODO: check if there is some main in .py
  122. # otherwise we can have successful test just because
  123. # everything was loaded into Python
  124. # TODO: check if there is set -e or exit !0 or ?
  125. # otherwise we can have successful because nothing was reported
  126. abspath = os.path.abspath(full)
  127. abs_file_path = os.path.join(abspath, file_name)
  128. if file_name.endswith(".py"):
  129. if file_name == "__init__.py":
  130. # we always ignore __init__.py
  131. continue
  132. file_type = "py"
  133. name = file_name[:-3]
  134. elif file_name.endswith(".sh"):
  135. file_type = "sh"
  136. name = file_name[:-3]
  137. else:
  138. file_type = None # alternative would be '', now equivalent
  139. name = file_name
  140. add = False
  141. try:
  142. if grass_location == all_locations_value:
  143. add = True
  144. else:
  145. try:
  146. locations = ["nc", "stdmaps", "all"]
  147. except AttributeError:
  148. add = True # test is universal
  149. else:
  150. if universal_location_value in locations:
  151. add = True # cases when it is explicit
  152. if grass_location in locations:
  153. add = True # standard case with given location
  154. if not locations:
  155. add = True # count not specified as universal
  156. except ImportError as e:
  157. if add_failed_imports:
  158. add = True
  159. else:
  160. raise ImportError(
  161. "Cannot import module named"
  162. " %s in %s (%s)" % (name, full, e.message)
  163. )
  164. # alternative is to create TestClass which will raise
  165. # see unittest.loader
  166. if add:
  167. modules.append(
  168. GrassTestPythonModule(
  169. name=name,
  170. module=None,
  171. tested_dir=root,
  172. file_dir=full,
  173. abs_file_path=abs_file_path,
  174. file_path=os.path.join(full, file_name),
  175. file_type=file_type,
  176. )
  177. )
  178. # in else with some verbose we could tell about skipped test
  179. return modules
  180. # TODO: find out if this is useful for us in some way
  181. # we are now using only discover_modules directly
  182. class GrassTestLoader(unittest.TestLoader):
  183. """Class handles GRASS-specific loading of test modules."""
  184. skip_dirs = [".git", ".svn", "dist.*", "bin.*", "OBJ.*"]
  185. testsuite_dir = "testsuite"
  186. files_in_testsuite = "*.py"
  187. all_tests_value = "all"
  188. universal_tests_value = "universal"
  189. def __init__(self, grass_location):
  190. self.grass_location = grass_location
  191. # TODO: what is the purpose of top_level_dir, can it be useful?
  192. # probably yes, we need to know grass src or dist root
  193. # TODO: not using pattern here
  194. def discover(self, start_dir, pattern="test*.py", top_level_dir=None):
  195. """Load test modules from in GRASS testing framework way."""
  196. modules = discover_modules(
  197. start_dir=start_dir,
  198. file_pattern=self.files_in_testsuite,
  199. skip_dirs=self.skip_dirs,
  200. testsuite_dir=self.testsuite_dir,
  201. grass_location=self.grass_location,
  202. all_locations_value=self.all_tests_value,
  203. universal_location_value=self.universal_tests_value,
  204. import_modules=True,
  205. )
  206. tests = []
  207. for module in modules:
  208. tests.append(self.loadTestsFromModule(module.module))
  209. return self.suiteClass(tests)
  210. if __name__ == "__main__":
  211. GrassTestLoader().discover()