test.py 7.9 KB

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