runtests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. #!/usr/bin/env python
  2. import os, re, sys, time, traceback
  3. # doctest is included in the same package as this module, because this testing
  4. # framework uses features only available in the Python 2.4 version of doctest,
  5. # and Django aims to work with Python 2.3+.
  6. import doctest
  7. MODEL_TESTS_DIR_NAME = 'modeltests'
  8. OTHER_TESTS_DIR = "othertests"
  9. REGRESSION_TESTS_DIR_NAME = 'regressiontests'
  10. TEST_DATABASE_NAME = 'django_test_db'
  11. error_list = []
  12. def log_error(model_name, title, description):
  13. error_list.append({
  14. 'title': "%r module: %s" % (model_name, title),
  15. 'description': description,
  16. })
  17. MODEL_TEST_DIR = os.path.join(os.path.dirname(__file__), MODEL_TESTS_DIR_NAME)
  18. REGRESSION_TEST_DIR = os.path.join(os.path.dirname(__file__), REGRESSION_TESTS_DIR_NAME)
  19. ALWAYS_INSTALLED_APPS = [
  20. 'django.contrib.contenttypes',
  21. 'django.contrib.auth',
  22. 'django.contrib.sites',
  23. 'django.contrib.flatpages',
  24. 'django.contrib.redirects',
  25. 'django.contrib.sessions',
  26. 'django.contrib.comments',
  27. 'django.contrib.admin',
  28. ]
  29. def get_test_models():
  30. return [(MODEL_TESTS_DIR_NAME, f) for f in os.listdir(MODEL_TEST_DIR) if not f.startswith('__init__') and not f.startswith('.')] +\
  31. [(REGRESSION_TESTS_DIR_NAME, f) for f in os.listdir(REGRESSION_TEST_DIR) if not f.startswith('__init__') and not f.startswith('.')]
  32. class DjangoDoctestRunner(doctest.DocTestRunner):
  33. def __init__(self, verbosity_level, *args, **kwargs):
  34. self.verbosity_level = verbosity_level
  35. doctest.DocTestRunner.__init__(self, *args, **kwargs)
  36. self._checker = DjangoDoctestOutputChecker()
  37. self.optionflags = doctest.ELLIPSIS
  38. def report_start(self, out, test, example):
  39. if self.verbosity_level > 1:
  40. out(" >>> %s\n" % example.source.strip())
  41. def report_failure(self, out, test, example, got):
  42. log_error(test.name, "API test failed",
  43. "Code: %r\nLine: %s\nExpected: %r\nGot: %r" % (example.source.strip(), example.lineno, example.want, got))
  44. def report_unexpected_exception(self, out, test, example, exc_info):
  45. from django.db import transaction
  46. tb = ''.join(traceback.format_exception(*exc_info)[1:])
  47. log_error(test.name, "API test raised an exception",
  48. "Code: %r\nLine: %s\nException: %s" % (example.source.strip(), example.lineno, tb))
  49. # Rollback, in case of database errors. Otherwise they'd have
  50. # side effects on other tests.
  51. transaction.rollback_unless_managed()
  52. normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
  53. class DjangoDoctestOutputChecker(doctest.OutputChecker):
  54. def check_output(self, want, got, optionflags):
  55. ok = doctest.OutputChecker.check_output(self, want, got, optionflags)
  56. # Doctest does an exact string comparison of output, which means long
  57. # integers aren't equal to normal integers ("22L" vs. "22"). The
  58. # following code normalizes long integers so that they equal normal
  59. # integers.
  60. if not ok:
  61. return normalize_long_ints(want) == normalize_long_ints(got)
  62. return ok
  63. class TestRunner:
  64. def __init__(self, verbosity_level=0, which_tests=None):
  65. self.verbosity_level = verbosity_level
  66. self.which_tests = which_tests
  67. def output(self, required_level, message):
  68. if self.verbosity_level > required_level - 1:
  69. print message
  70. def run_tests(self):
  71. from django.conf import settings
  72. # An empty access of the settings to force the default options to be
  73. # installed prior to assigning to them.
  74. settings.INSTALLED_APPS
  75. # Manually set INSTALLED_APPS to point to the test models.
  76. settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS + ['.'.join(a) for a in get_test_models()]
  77. # Manually set DEBUG = False.
  78. settings.DEBUG = False
  79. from django.db import connection
  80. from django.core import management
  81. import django.db.models
  82. # Determine which models we're going to test.
  83. test_models = get_test_models()
  84. if 'othertests' in self.which_tests:
  85. self.which_tests.remove('othertests')
  86. run_othertests = True
  87. if not self.which_tests:
  88. test_models = []
  89. else:
  90. run_othertests = not self.which_tests
  91. if self.which_tests:
  92. # Only run the specified tests.
  93. bad_models = [m for m in self.which_tests if (MODEL_TESTS_DIR_NAME, m) not in test_models and (REGRESSION_TESTS_DIR_NAME, m) not in test_models]
  94. if bad_models:
  95. sys.stderr.write("Models not found: %s\n" % bad_models)
  96. sys.exit(1)
  97. else:
  98. all_tests = []
  99. for test in self.which_tests:
  100. for loc in MODEL_TESTS_DIR_NAME, REGRESSION_TESTS_DIR_NAME:
  101. if (loc, test) in test_models:
  102. all_tests.append((loc, test))
  103. test_models = all_tests
  104. self.output(0, "Running tests with database %r" % settings.DATABASE_ENGINE)
  105. # If we're using SQLite, it's more convenient to test against an
  106. # in-memory database.
  107. if settings.DATABASE_ENGINE == "sqlite3":
  108. global TEST_DATABASE_NAME
  109. TEST_DATABASE_NAME = ":memory:"
  110. else:
  111. # Create the test database and connect to it. We need to autocommit
  112. # if the database supports it because PostgreSQL doesn't allow
  113. # CREATE/DROP DATABASE statements within transactions.
  114. cursor = connection.cursor()
  115. self._set_autocommit(connection)
  116. self.output(1, "Creating test database")
  117. try:
  118. cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME)
  119. except Exception, e:
  120. sys.stderr.write("Got an error creating the test database: %s\n" % e)
  121. confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_DATABASE_NAME)
  122. if confirm == 'yes':
  123. cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME)
  124. cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME)
  125. else:
  126. print "Tests cancelled."
  127. return
  128. connection.close()
  129. old_database_name = settings.DATABASE_NAME
  130. settings.DATABASE_NAME = TEST_DATABASE_NAME
  131. # Initialize the test database.
  132. cursor = connection.cursor()
  133. # Install the core always installed apps
  134. for app in ALWAYS_INSTALLED_APPS:
  135. self.output(1, "Installing contrib app %s" % app)
  136. mod = __import__(app + ".models", '', '', [''])
  137. management.install(mod)
  138. # Run the tests for each test model.
  139. self.output(1, "Running app tests")
  140. for model_dir, model_name in test_models:
  141. self.output(1, "%s model: Importing" % model_name)
  142. try:
  143. # TODO: Abstract this into a meta.get_app() replacement?
  144. mod = __import__(model_dir + '.' + model_name + '.models', '', '', [''])
  145. except Exception, e:
  146. log_error(model_name, "Error while importing", ''.join(traceback.format_exception(*sys.exc_info())[1:]))
  147. continue
  148. if not getattr(mod, 'error_log', None):
  149. # Model is not marked as an invalid model
  150. self.output(1, "%s.%s model: Installing" % (model_dir, model_name))
  151. management.install(mod)
  152. # Run the API tests.
  153. p = doctest.DocTestParser()
  154. test_namespace = dict([(m._meta.object_name, m) \
  155. for m in django.db.models.get_models(mod)])
  156. dtest = p.get_doctest(mod.API_TESTS, test_namespace, model_name, None, None)
  157. # Manually set verbose=False, because "-v" command-line parameter
  158. # has side effects on doctest TestRunner class.
  159. runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False)
  160. self.output(1, "%s.%s model: Running tests" % (model_dir, model_name))
  161. runner.run(dtest, clear_globs=True, out=sys.stdout.write)
  162. else:
  163. # Check that model known to be invalid is invalid for the right reasons.
  164. self.output(1, "%s.%s model: Validating" % (model_dir, model_name))
  165. from cStringIO import StringIO
  166. s = StringIO()
  167. count = management.get_validation_errors(s, mod)
  168. s.seek(0)
  169. error_log = s.read()
  170. actual = error_log.split('\n')
  171. expected = mod.error_log.split('\n')
  172. unexpected = [err for err in actual if err not in expected]
  173. missing = [err for err in expected if err not in actual]
  174. if unexpected or missing:
  175. unexpected_log = '\n'.join(unexpected)
  176. missing_log = '\n'.join(missing)
  177. log_error(model_name,
  178. "Validator found %d validation errors, %d expected" % (count, len(expected) - 1),
  179. "Missing errors:\n%s\n\nUnexpected errors:\n%s" % (missing_log, unexpected_log))
  180. if run_othertests:
  181. # Run the non-model tests in the other tests dir
  182. self.output(1, "Running other tests")
  183. other_tests_dir = os.path.join(os.path.dirname(__file__), OTHER_TESTS_DIR)
  184. test_modules = [f[:-3] for f in os.listdir(other_tests_dir) if f.endswith('.py') and not f.startswith('__init__')]
  185. for module in test_modules:
  186. self.output(1, "%s module: Importing" % module)
  187. try:
  188. mod = __import__("othertests." + module, '', '', [''])
  189. except Exception, e:
  190. log_error(module, "Error while importing", ''.join(traceback.format_exception(*sys.exc_info())[1:]))
  191. continue
  192. if mod.__doc__:
  193. p = doctest.DocTestParser()
  194. dtest = p.get_doctest(mod.__doc__, mod.__dict__, module, None, None)
  195. runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False)
  196. self.output(1, "%s module: running tests" % module)
  197. runner.run(dtest, clear_globs=True, out=sys.stdout.write)
  198. if hasattr(mod, "run_tests") and callable(mod.run_tests):
  199. self.output(1, "%s module: running tests" % module)
  200. try:
  201. mod.run_tests(verbosity_level)
  202. except Exception, e:
  203. log_error(module, "Exception running tests", ''.join(traceback.format_exception(*sys.exc_info())[1:]))
  204. continue
  205. # Unless we're using SQLite, remove the test database to clean up after
  206. # ourselves. Connect to the previous database (not the test database)
  207. # to do so, because it's not allowed to delete a database while being
  208. # connected to it.
  209. if settings.DATABASE_ENGINE != "sqlite3":
  210. connection.close()
  211. settings.DATABASE_NAME = old_database_name
  212. cursor = connection.cursor()
  213. self.output(1, "Deleting test database")
  214. self._set_autocommit(connection)
  215. time.sleep(1) # To avoid "database is being accessed by other users" errors.
  216. cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME)
  217. # Display output.
  218. if error_list:
  219. for d in error_list:
  220. print
  221. print d['title']
  222. print "=" * len(d['title'])
  223. print d['description']
  224. print "%s error%s:" % (len(error_list), len(error_list) != 1 and 's' or '')
  225. else:
  226. print "All tests passed."
  227. def _set_autocommit(self, connection):
  228. """
  229. Make sure a connection is in autocommit mode.
  230. """
  231. if hasattr(connection.connection, "autocommit"):
  232. connection.connection.autocommit(True)
  233. elif hasattr(connection.connection, "set_isolation_level"):
  234. connection.connection.set_isolation_level(0)
  235. if __name__ == "__main__":
  236. from optparse import OptionParser
  237. usage = "%prog [options] [model model model ...]"
  238. parser = OptionParser(usage=usage)
  239. parser.add_option('-v', help='How verbose should the output be? Choices are 0, 1 and 2, where 2 is most verbose. Default is 0.',
  240. type='choice', choices=['0', '1', '2'])
  241. parser.add_option('--settings',
  242. help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.')
  243. options, args = parser.parse_args()
  244. verbosity_level = 0
  245. if options.v:
  246. verbosity_level = int(options.v)
  247. if options.settings:
  248. os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
  249. t = TestRunner(verbosity_level, args)
  250. t.run_tests()