runner.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. from importlib import import_module
  2. import os
  3. from optparse import make_option
  4. import unittest
  5. from unittest import TestSuite, defaultTestLoader
  6. from django.conf import settings
  7. from django.core.exceptions import ImproperlyConfigured
  8. from django.test import SimpleTestCase, TestCase
  9. from django.test.utils import setup_test_environment, teardown_test_environment
  10. class DiscoverRunner(object):
  11. """
  12. A Django test runner that uses unittest2 test discovery.
  13. """
  14. test_suite = TestSuite
  15. test_runner = unittest.TextTestRunner
  16. test_loader = defaultTestLoader
  17. reorder_by = (TestCase, SimpleTestCase)
  18. option_list = (
  19. make_option('-t', '--top-level-directory',
  20. action='store', dest='top_level', default=None,
  21. help='Top level of project for unittest discovery.'),
  22. make_option('-p', '--pattern', action='store', dest='pattern',
  23. default="test*.py",
  24. help='The test matching pattern. Defaults to test*.py.'),
  25. )
  26. def __init__(self, pattern=None, top_level=None,
  27. verbosity=1, interactive=True, failfast=False,
  28. **kwargs):
  29. self.pattern = pattern
  30. self.top_level = top_level
  31. self.verbosity = verbosity
  32. self.interactive = interactive
  33. self.failfast = failfast
  34. def setup_test_environment(self, **kwargs):
  35. setup_test_environment()
  36. settings.DEBUG = False
  37. unittest.installHandler()
  38. def build_suite(self, test_labels=None, extra_tests=None, **kwargs):
  39. suite = self.test_suite()
  40. test_labels = test_labels or ['.']
  41. extra_tests = extra_tests or []
  42. discover_kwargs = {}
  43. if self.pattern is not None:
  44. discover_kwargs['pattern'] = self.pattern
  45. if self.top_level is not None:
  46. discover_kwargs['top_level_dir'] = self.top_level
  47. for label in test_labels:
  48. kwargs = discover_kwargs.copy()
  49. tests = None
  50. label_as_path = os.path.abspath(label)
  51. # if a module, or "module.ClassName[.method_name]", just run those
  52. if not os.path.exists(label_as_path):
  53. tests = self.test_loader.loadTestsFromName(label)
  54. elif os.path.isdir(label_as_path) and not self.top_level:
  55. # Try to be a bit smarter than unittest about finding the
  56. # default top-level for a given directory path, to avoid
  57. # breaking relative imports. (Unittest's default is to set
  58. # top-level equal to the path, which means relative imports
  59. # will result in "Attempted relative import in non-package.").
  60. # We'd be happy to skip this and require dotted module paths
  61. # (which don't cause this problem) instead of file paths (which
  62. # do), but in the case of a directory in the cwd, which would
  63. # be equally valid if considered as a top-level module or as a
  64. # directory path, unittest unfortunately prefers the latter.
  65. top_level = label_as_path
  66. while True:
  67. init_py = os.path.join(top_level, '__init__.py')
  68. if os.path.exists(init_py):
  69. try_next = os.path.dirname(top_level)
  70. if try_next == top_level:
  71. # __init__.py all the way down? give up.
  72. break
  73. top_level = try_next
  74. continue
  75. break
  76. kwargs['top_level_dir'] = top_level
  77. if not (tests and tests.countTestCases()) and is_discoverable(label):
  78. # Try discovery if path is a package or directory
  79. tests = self.test_loader.discover(start_dir=label, **kwargs)
  80. # Make unittest forget the top-level dir it calculated from this
  81. # run, to support running tests from two different top-levels.
  82. self.test_loader._top_level_dir = None
  83. suite.addTests(tests)
  84. for test in extra_tests:
  85. suite.addTest(test)
  86. return reorder_suite(suite, self.reorder_by)
  87. def setup_databases(self, **kwargs):
  88. return setup_databases(self.verbosity, self.interactive, **kwargs)
  89. def run_suite(self, suite, **kwargs):
  90. return self.test_runner(
  91. verbosity=self.verbosity,
  92. failfast=self.failfast,
  93. ).run(suite)
  94. def teardown_databases(self, old_config, **kwargs):
  95. """
  96. Destroys all the non-mirror databases.
  97. """
  98. old_names, mirrors = old_config
  99. for connection, old_name, destroy in old_names:
  100. if destroy:
  101. connection.creation.destroy_test_db(old_name, self.verbosity)
  102. def teardown_test_environment(self, **kwargs):
  103. unittest.removeHandler()
  104. teardown_test_environment()
  105. def suite_result(self, suite, result, **kwargs):
  106. return len(result.failures) + len(result.errors)
  107. def run_tests(self, test_labels, extra_tests=None, **kwargs):
  108. """
  109. Run the unit tests for all the test labels in the provided list.
  110. Test labels should be dotted Python paths to test modules, test
  111. classes, or test methods.
  112. A list of 'extra' tests may also be provided; these tests
  113. will be added to the test suite.
  114. Returns the number of tests that failed.
  115. """
  116. self.setup_test_environment()
  117. suite = self.build_suite(test_labels, extra_tests)
  118. old_config = self.setup_databases()
  119. result = self.run_suite(suite)
  120. self.teardown_databases(old_config)
  121. self.teardown_test_environment()
  122. return self.suite_result(suite, result)
  123. def is_discoverable(label):
  124. """
  125. Check if a test label points to a python package or file directory.
  126. Relative labels like "." and ".." are seen as directories.
  127. """
  128. try:
  129. mod = import_module(label)
  130. except (ImportError, TypeError):
  131. pass
  132. else:
  133. return hasattr(mod, '__path__')
  134. return os.path.isdir(os.path.abspath(label))
  135. def dependency_ordered(test_databases, dependencies):
  136. """
  137. Reorder test_databases into an order that honors the dependencies
  138. described in TEST[DEPENDENCIES].
  139. """
  140. ordered_test_databases = []
  141. resolved_databases = set()
  142. # Maps db signature to dependencies of all it's aliases
  143. dependencies_map = {}
  144. # sanity check - no DB can depend on its own alias
  145. for sig, (_, aliases) in test_databases:
  146. all_deps = set()
  147. for alias in aliases:
  148. all_deps.update(dependencies.get(alias, []))
  149. if not all_deps.isdisjoint(aliases):
  150. raise ImproperlyConfigured(
  151. "Circular dependency: databases %r depend on each other, "
  152. "but are aliases." % aliases)
  153. dependencies_map[sig] = all_deps
  154. while test_databases:
  155. changed = False
  156. deferred = []
  157. # Try to find a DB that has all it's dependencies met
  158. for signature, (db_name, aliases) in test_databases:
  159. if dependencies_map[signature].issubset(resolved_databases):
  160. resolved_databases.update(aliases)
  161. ordered_test_databases.append((signature, (db_name, aliases)))
  162. changed = True
  163. else:
  164. deferred.append((signature, (db_name, aliases)))
  165. if not changed:
  166. raise ImproperlyConfigured(
  167. "Circular dependency in TEST[DEPENDENCIES]")
  168. test_databases = deferred
  169. return ordered_test_databases
  170. def reorder_suite(suite, classes):
  171. """
  172. Reorders a test suite by test type.
  173. `classes` is a sequence of types
  174. All tests of type classes[0] are placed first, then tests of type
  175. classes[1], etc. Tests with no match in classes are placed last.
  176. """
  177. class_count = len(classes)
  178. suite_class = type(suite)
  179. bins = [suite_class() for i in range(class_count + 1)]
  180. partition_suite(suite, classes, bins)
  181. for i in range(class_count):
  182. bins[0].addTests(bins[i + 1])
  183. return bins[0]
  184. def partition_suite(suite, classes, bins):
  185. """
  186. Partitions a test suite by test type.
  187. classes is a sequence of types
  188. bins is a sequence of TestSuites, one more than classes
  189. Tests of type classes[i] are added to bins[i],
  190. tests with no match found in classes are place in bins[-1]
  191. """
  192. suite_class = type(suite)
  193. for test in suite:
  194. if isinstance(test, suite_class):
  195. partition_suite(test, classes, bins)
  196. else:
  197. for i in range(len(classes)):
  198. if isinstance(test, classes[i]):
  199. bins[i].addTest(test)
  200. break
  201. else:
  202. bins[-1].addTest(test)
  203. def setup_databases(verbosity, interactive, **kwargs):
  204. from django.db import connections, DEFAULT_DB_ALIAS
  205. # First pass -- work out which databases actually need to be created,
  206. # and which ones are test mirrors or duplicate entries in DATABASES
  207. mirrored_aliases = {}
  208. test_databases = {}
  209. dependencies = {}
  210. default_sig = connections[DEFAULT_DB_ALIAS].creation.test_db_signature()
  211. for alias in connections:
  212. connection = connections[alias]
  213. test_settings = connection.settings_dict['TEST']
  214. if test_settings['MIRROR']:
  215. # If the database is marked as a test mirror, save
  216. # the alias.
  217. mirrored_aliases[alias] = test_settings['MIRROR']
  218. else:
  219. # Store a tuple with DB parameters that uniquely identify it.
  220. # If we have two aliases with the same values for that tuple,
  221. # we only need to create the test database once.
  222. item = test_databases.setdefault(
  223. connection.creation.test_db_signature(),
  224. (connection.settings_dict['NAME'], set())
  225. )
  226. item[1].add(alias)
  227. if 'DEPENDENCIES' in test_settings:
  228. dependencies[alias] = test_settings['DEPENDENCIES']
  229. else:
  230. if alias != DEFAULT_DB_ALIAS and connection.creation.test_db_signature() != default_sig:
  231. dependencies[alias] = test_settings.get('DEPENDENCIES', [DEFAULT_DB_ALIAS])
  232. # Second pass -- actually create the databases.
  233. old_names = []
  234. mirrors = []
  235. for signature, (db_name, aliases) in dependency_ordered(
  236. test_databases.items(), dependencies):
  237. test_db_name = None
  238. # Actually create the database for the first connection
  239. for alias in aliases:
  240. connection = connections[alias]
  241. if test_db_name is None:
  242. test_db_name = connection.creation.create_test_db(
  243. verbosity,
  244. autoclobber=not interactive,
  245. serialize=connection.settings_dict.get("TEST_SERIALIZE", True),
  246. )
  247. destroy = True
  248. else:
  249. connection.settings_dict['NAME'] = test_db_name
  250. destroy = False
  251. old_names.append((connection, db_name, destroy))
  252. for alias, mirror_alias in mirrored_aliases.items():
  253. mirrors.append((alias, connections[alias].settings_dict['NAME']))
  254. connections[alias].settings_dict['NAME'] = (
  255. connections[mirror_alias].settings_dict['NAME'])
  256. return old_names, mirrors