Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

creation.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import sys
  2. from io import StringIO
  3. from django.apps import apps
  4. from django.conf import settings
  5. from django.core import serializers
  6. from django.db import router
  7. # The prefix to put on the default database name when creating
  8. # the test database.
  9. TEST_DATABASE_PREFIX = 'test_'
  10. class BaseDatabaseCreation:
  11. """
  12. Encapsulate backend-specific differences pertaining to creation and
  13. destruction of the test database.
  14. """
  15. def __init__(self, connection):
  16. self.connection = connection
  17. @property
  18. def _nodb_connection(self):
  19. """
  20. Used to be defined here, now moved to DatabaseWrapper.
  21. """
  22. return self.connection._nodb_connection
  23. def create_test_db(self, verbosity=1, autoclobber=False, serialize=True, keepdb=False):
  24. """
  25. Create a test database, prompting the user for confirmation if the
  26. database already exists. Return the name of the test database created.
  27. """
  28. # Don't import django.core.management if it isn't needed.
  29. from django.core.management import call_command
  30. test_database_name = self._get_test_db_name()
  31. if verbosity >= 1:
  32. action = 'Creating'
  33. if keepdb:
  34. action = "Using existing"
  35. print("%s test database for alias %s..." % (
  36. action,
  37. self._get_database_display_str(verbosity, test_database_name),
  38. ))
  39. # We could skip this call if keepdb is True, but we instead
  40. # give it the keepdb param. This is to handle the case
  41. # where the test DB doesn't exist, in which case we need to
  42. # create it, then just not destroy it. If we instead skip
  43. # this, we will get an exception.
  44. self._create_test_db(verbosity, autoclobber, keepdb)
  45. self.connection.close()
  46. settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
  47. self.connection.settings_dict["NAME"] = test_database_name
  48. # We report migrate messages at one level lower than that requested.
  49. # This ensures we don't get flooded with messages during testing
  50. # (unless you really ask to be flooded).
  51. call_command(
  52. 'migrate',
  53. verbosity=max(verbosity - 1, 0),
  54. interactive=False,
  55. database=self.connection.alias,
  56. run_syncdb=True,
  57. )
  58. # We then serialize the current state of the database into a string
  59. # and store it on the connection. This slightly horrific process is so people
  60. # who are testing on databases without transactions or who are using
  61. # a TransactionTestCase still get a clean database on every test run.
  62. if serialize:
  63. self.connection._test_serialized_contents = self.serialize_db_to_string()
  64. call_command('createcachetable', database=self.connection.alias)
  65. # Ensure a connection for the side effect of initializing the test database.
  66. self.connection.ensure_connection()
  67. return test_database_name
  68. def set_as_test_mirror(self, primary_settings_dict):
  69. """
  70. Set this database up to be used in testing as a mirror of a primary
  71. database whose settings are given.
  72. """
  73. self.connection.settings_dict['NAME'] = primary_settings_dict['NAME']
  74. def serialize_db_to_string(self):
  75. """
  76. Serialize all data in the database into a JSON string.
  77. Designed only for test runner usage; will not handle large
  78. amounts of data.
  79. """
  80. # Build list of all apps to serialize
  81. from django.db.migrations.loader import MigrationLoader
  82. loader = MigrationLoader(self.connection)
  83. app_list = []
  84. for app_config in apps.get_app_configs():
  85. if (
  86. app_config.models_module is not None and
  87. app_config.label in loader.migrated_apps and
  88. app_config.name not in settings.TEST_NON_SERIALIZED_APPS
  89. ):
  90. app_list.append((app_config, None))
  91. # Make a function to iteratively return every object
  92. def get_objects():
  93. for model in serializers.sort_dependencies(app_list):
  94. if (model._meta.can_migrate(self.connection) and
  95. router.allow_migrate_model(self.connection.alias, model)):
  96. queryset = model._default_manager.using(self.connection.alias).order_by(model._meta.pk.name)
  97. yield from queryset.iterator()
  98. # Serialize to a string
  99. out = StringIO()
  100. serializers.serialize("json", get_objects(), indent=None, stream=out)
  101. return out.getvalue()
  102. def deserialize_db_from_string(self, data):
  103. """
  104. Reload the database with data from a string generated by
  105. the serialize_db_to_string() method.
  106. """
  107. data = StringIO(data)
  108. for obj in serializers.deserialize("json", data, using=self.connection.alias):
  109. obj.save()
  110. def _get_database_display_str(self, verbosity, database_name):
  111. """
  112. Return display string for a database for use in various actions.
  113. """
  114. return "'%s'%s" % (
  115. self.connection.alias,
  116. (" ('%s')" % database_name) if verbosity >= 2 else '',
  117. )
  118. def _get_test_db_name(self):
  119. """
  120. Internal implementation - return the name of the test DB that will be
  121. created. Only useful when called from create_test_db() and
  122. _create_test_db() and when no external munging is done with the 'NAME'
  123. settings.
  124. """
  125. if self.connection.settings_dict['TEST']['NAME']:
  126. return self.connection.settings_dict['TEST']['NAME']
  127. return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME']
  128. def _execute_create_test_db(self, cursor, parameters, keepdb=False):
  129. cursor.execute('CREATE DATABASE %(dbname)s %(suffix)s' % parameters)
  130. def _create_test_db(self, verbosity, autoclobber, keepdb=False):
  131. """
  132. Internal implementation - create the test db tables.
  133. """
  134. test_database_name = self._get_test_db_name()
  135. test_db_params = {
  136. 'dbname': self.connection.ops.quote_name(test_database_name),
  137. 'suffix': self.sql_table_creation_suffix(),
  138. }
  139. # Create the test database and connect to it.
  140. with self._nodb_connection.cursor() as cursor:
  141. try:
  142. self._execute_create_test_db(cursor, test_db_params, keepdb)
  143. except Exception as e:
  144. # if we want to keep the db, then no need to do any of the below,
  145. # just return and skip it all.
  146. if keepdb:
  147. return test_database_name
  148. sys.stderr.write(
  149. "Got an error creating the test database: %s\n" % e)
  150. if not autoclobber:
  151. confirm = input(
  152. "Type 'yes' if you would like to try deleting the test "
  153. "database '%s', or 'no' to cancel: " % test_database_name)
  154. if autoclobber or confirm == 'yes':
  155. try:
  156. if verbosity >= 1:
  157. print("Destroying old test database for alias %s..." % (
  158. self._get_database_display_str(verbosity, test_database_name),
  159. ))
  160. cursor.execute('DROP DATABASE %(dbname)s' % test_db_params)
  161. self._execute_create_test_db(cursor, test_db_params, keepdb)
  162. except Exception as e:
  163. sys.stderr.write(
  164. "Got an error recreating the test database: %s\n" % e)
  165. sys.exit(2)
  166. else:
  167. print("Tests cancelled.")
  168. sys.exit(1)
  169. return test_database_name
  170. def clone_test_db(self, suffix, verbosity=1, autoclobber=False, keepdb=False):
  171. """
  172. Clone a test database.
  173. """
  174. source_database_name = self.connection.settings_dict['NAME']
  175. if verbosity >= 1:
  176. action = 'Cloning test database'
  177. if keepdb:
  178. action = 'Using existing clone'
  179. print("%s for alias %s..." % (
  180. action,
  181. self._get_database_display_str(verbosity, source_database_name),
  182. ))
  183. # We could skip this call if keepdb is True, but we instead
  184. # give it the keepdb param. See create_test_db for details.
  185. self._clone_test_db(suffix, verbosity, keepdb)
  186. def get_test_db_clone_settings(self, suffix):
  187. """
  188. Return a modified connection settings dict for the n-th clone of a DB.
  189. """
  190. # When this function is called, the test database has been created
  191. # already and its name has been copied to settings_dict['NAME'] so
  192. # we don't need to call _get_test_db_name.
  193. orig_settings_dict = self.connection.settings_dict
  194. return {**orig_settings_dict, 'NAME': '{}_{}'.format(orig_settings_dict['NAME'], suffix)}
  195. def _clone_test_db(self, suffix, verbosity, keepdb=False):
  196. """
  197. Internal implementation - duplicate the test db tables.
  198. """
  199. raise NotImplementedError(
  200. "The database backend doesn't support cloning databases. "
  201. "Disable the option to run tests in parallel processes.")
  202. def destroy_test_db(self, old_database_name=None, verbosity=1, keepdb=False, suffix=None):
  203. """
  204. Destroy a test database, prompting the user for confirmation if the
  205. database already exists.
  206. """
  207. self.connection.close()
  208. if suffix is None:
  209. test_database_name = self.connection.settings_dict['NAME']
  210. else:
  211. test_database_name = self.get_test_db_clone_settings(suffix)['NAME']
  212. if verbosity >= 1:
  213. action = 'Destroying'
  214. if keepdb:
  215. action = 'Preserving'
  216. print("%s test database for alias %s..." % (
  217. action,
  218. self._get_database_display_str(verbosity, test_database_name),
  219. ))
  220. # if we want to preserve the database
  221. # skip the actual destroying piece.
  222. if not keepdb:
  223. self._destroy_test_db(test_database_name, verbosity)
  224. # Restore the original database name
  225. if old_database_name is not None:
  226. settings.DATABASES[self.connection.alias]["NAME"] = old_database_name
  227. self.connection.settings_dict["NAME"] = old_database_name
  228. def _destroy_test_db(self, test_database_name, verbosity):
  229. """
  230. Internal implementation - remove the test db tables.
  231. """
  232. # Remove the test database to clean up after
  233. # ourselves. Connect to the previous database (not the test database)
  234. # to do so, because it's not allowed to delete a database while being
  235. # connected to it.
  236. with self.connection._nodb_connection.cursor() as cursor:
  237. cursor.execute("DROP DATABASE %s"
  238. % self.connection.ops.quote_name(test_database_name))
  239. def sql_table_creation_suffix(self):
  240. """
  241. SQL to append to the end of the test table creation statements.
  242. """
  243. return ''
  244. def test_db_signature(self):
  245. """
  246. Return a tuple with elements of self.connection.settings_dict (a
  247. DATABASES setting value) that uniquely identify a database
  248. accordingly to the RDBMS particularities.
  249. """
  250. settings_dict = self.connection.settings_dict
  251. return (
  252. settings_dict['HOST'],
  253. settings_dict['PORT'],
  254. settings_dict['ENGINE'],
  255. self._get_test_db_name(),
  256. )