test.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import os
  2. import operator
  3. import sys
  4. import contextlib
  5. import itertools
  6. from distutils.errors import DistutilsError, DistutilsOptionError
  7. from distutils import log
  8. from unittest import TestLoader
  9. import six
  10. from six.moves import map, filter
  11. from pkg_resources import (resource_listdir, resource_exists, normalize_path,
  12. working_set, _namespace_packages,
  13. add_activation_listener, require, EntryPoint)
  14. from setuptools import Command
  15. from setuptools.py31compat import unittest_main
  16. class ScanningLoader(TestLoader):
  17. def loadTestsFromModule(self, module, pattern=None):
  18. """Return a suite of all tests cases contained in the given module
  19. If the module is a package, load tests from all the modules in it.
  20. If the module has an ``additional_tests`` function, call it and add
  21. the return value to the tests.
  22. """
  23. tests = []
  24. tests.append(TestLoader.loadTestsFromModule(self, module))
  25. if hasattr(module, "additional_tests"):
  26. tests.append(module.additional_tests())
  27. if hasattr(module, '__path__'):
  28. for file in resource_listdir(module.__name__, ''):
  29. if file.endswith('.py') and file != '__init__.py':
  30. submodule = module.__name__ + '.' + file[:-3]
  31. else:
  32. if resource_exists(module.__name__, file + '/__init__.py'):
  33. submodule = module.__name__ + '.' + file
  34. else:
  35. continue
  36. tests.append(self.loadTestsFromName(submodule))
  37. if len(tests) != 1:
  38. return self.suiteClass(tests)
  39. else:
  40. return tests[0] # don't create a nested suite for only one return
  41. # adapted from jaraco.classes.properties:NonDataProperty
  42. class NonDataProperty(object):
  43. def __init__(self, fget):
  44. self.fget = fget
  45. def __get__(self, obj, objtype=None):
  46. if obj is None:
  47. return self
  48. return self.fget(obj)
  49. class test(Command):
  50. """Command to run unit tests after in-place build"""
  51. description = "run unit tests after in-place build"
  52. user_options = [
  53. ('test-module=', 'm', "Run 'test_suite' in specified module"),
  54. ('test-suite=', 's',
  55. "Test suite to run (e.g. 'some_module.test_suite')"),
  56. ('test-runner=', 'r', "Test runner to use"),
  57. ]
  58. def initialize_options(self):
  59. self.test_suite = None
  60. self.test_module = None
  61. self.test_loader = None
  62. self.test_runner = None
  63. def finalize_options(self):
  64. if self.test_suite and self.test_module:
  65. msg = "You may specify a module or a suite, but not both"
  66. raise DistutilsOptionError(msg)
  67. if self.test_suite is None:
  68. if self.test_module is None:
  69. self.test_suite = self.distribution.test_suite
  70. else:
  71. self.test_suite = self.test_module + ".test_suite"
  72. if self.test_loader is None:
  73. self.test_loader = getattr(self.distribution, 'test_loader', None)
  74. if self.test_loader is None:
  75. self.test_loader = "setuptools.command.test:ScanningLoader"
  76. if self.test_runner is None:
  77. self.test_runner = getattr(self.distribution, 'test_runner', None)
  78. @NonDataProperty
  79. def test_args(self):
  80. return list(self._test_args())
  81. def _test_args(self):
  82. if self.verbose:
  83. yield '--verbose'
  84. if self.test_suite:
  85. yield self.test_suite
  86. def with_project_on_sys_path(self, func):
  87. """
  88. Backward compatibility for project_on_sys_path context.
  89. """
  90. with self.project_on_sys_path():
  91. func()
  92. @contextlib.contextmanager
  93. def project_on_sys_path(self, include_dists=[]):
  94. with_2to3 = six.PY3 and getattr(self.distribution, 'use_2to3', False)
  95. if with_2to3:
  96. # If we run 2to3 we can not do this inplace:
  97. # Ensure metadata is up-to-date
  98. self.reinitialize_command('build_py', inplace=0)
  99. self.run_command('build_py')
  100. bpy_cmd = self.get_finalized_command("build_py")
  101. build_path = normalize_path(bpy_cmd.build_lib)
  102. # Build extensions
  103. self.reinitialize_command('egg_info', egg_base=build_path)
  104. self.run_command('egg_info')
  105. self.reinitialize_command('build_ext', inplace=0)
  106. self.run_command('build_ext')
  107. else:
  108. # Without 2to3 inplace works fine:
  109. self.run_command('egg_info')
  110. # Build extensions in-place
  111. self.reinitialize_command('build_ext', inplace=1)
  112. self.run_command('build_ext')
  113. ei_cmd = self.get_finalized_command("egg_info")
  114. old_path = sys.path[:]
  115. old_modules = sys.modules.copy()
  116. try:
  117. project_path = normalize_path(ei_cmd.egg_base)
  118. sys.path.insert(0, project_path)
  119. working_set.__init__()
  120. add_activation_listener(lambda dist: dist.activate())
  121. require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version))
  122. with self.paths_on_pythonpath([project_path]):
  123. yield
  124. finally:
  125. sys.path[:] = old_path
  126. sys.modules.clear()
  127. sys.modules.update(old_modules)
  128. working_set.__init__()
  129. @staticmethod
  130. @contextlib.contextmanager
  131. def paths_on_pythonpath(paths):
  132. """
  133. Add the indicated paths to the head of the PYTHONPATH environment
  134. variable so that subprocesses will also see the packages at
  135. these paths.
  136. Do this in a context that restores the value on exit.
  137. """
  138. nothing = object()
  139. orig_pythonpath = os.environ.get('PYTHONPATH', nothing)
  140. current_pythonpath = os.environ.get('PYTHONPATH', '')
  141. try:
  142. prefix = os.pathsep.join(paths)
  143. to_join = filter(None, [prefix, current_pythonpath])
  144. new_path = os.pathsep.join(to_join)
  145. if new_path:
  146. os.environ['PYTHONPATH'] = new_path
  147. yield
  148. finally:
  149. if orig_pythonpath is nothing:
  150. os.environ.pop('PYTHONPATH', None)
  151. else:
  152. os.environ['PYTHONPATH'] = orig_pythonpath
  153. @staticmethod
  154. def install_dists(dist):
  155. """
  156. Install the requirements indicated by self.distribution and
  157. return an iterable of the dists that were built.
  158. """
  159. ir_d = dist.fetch_build_eggs(dist.install_requires or [])
  160. tr_d = dist.fetch_build_eggs(dist.tests_require or [])
  161. return itertools.chain(ir_d, tr_d)
  162. def run(self):
  163. installed_dists = self.install_dists(self.distribution)
  164. cmd = ' '.join(self._argv)
  165. if self.dry_run:
  166. self.announce('skipping "%s" (dry run)' % cmd)
  167. return
  168. self.announce('running "%s"' % cmd)
  169. paths = map(operator.attrgetter('location'), installed_dists)
  170. with self.paths_on_pythonpath(paths):
  171. with self.project_on_sys_path():
  172. self.run_tests()
  173. def run_tests(self):
  174. # Purge modules under test from sys.modules. The test loader will
  175. # re-import them from the build location. Required when 2to3 is used
  176. # with namespace packages.
  177. if six.PY3 and getattr(self.distribution, 'use_2to3', False):
  178. module = self.test_suite.split('.')[0]
  179. if module in _namespace_packages:
  180. del_modules = []
  181. if module in sys.modules:
  182. del_modules.append(module)
  183. module += '.'
  184. for name in sys.modules:
  185. if name.startswith(module):
  186. del_modules.append(name)
  187. list(map(sys.modules.__delitem__, del_modules))
  188. exit_kwarg = {} if sys.version_info < (2, 7) else {"exit": False}
  189. test = unittest_main(
  190. None, None, self._argv,
  191. testLoader=self._resolve_as_ep(self.test_loader),
  192. testRunner=self._resolve_as_ep(self.test_runner),
  193. **exit_kwarg
  194. )
  195. if not test.result.wasSuccessful():
  196. msg = 'Test failed: %s' % test.result
  197. self.announce(msg, log.ERROR)
  198. raise DistutilsError(msg)
  199. @property
  200. def _argv(self):
  201. return ['unittest'] + self.test_args
  202. @staticmethod
  203. def _resolve_as_ep(val):
  204. """
  205. Load the indicated attribute value, called, as a as if it were
  206. specified as an entry point.
  207. """
  208. if val is None:
  209. return
  210. parsed = EntryPoint.parse("x=" + val)
  211. return parsed.resolve()()