test_loader.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. from django.db import connection, connections
  2. from django.db.migrations.exceptions import (
  3. AmbiguityError, InconsistentMigrationHistory, NodeNotFoundError,
  4. )
  5. from django.db.migrations.loader import MigrationLoader
  6. from django.db.migrations.recorder import MigrationRecorder
  7. from django.test import TestCase, modify_settings, override_settings
  8. class RecorderTests(TestCase):
  9. """
  10. Tests recording migrations as applied or not.
  11. """
  12. def test_apply(self):
  13. """
  14. Tests marking migrations as applied/unapplied.
  15. """
  16. recorder = MigrationRecorder(connection)
  17. self.assertEqual(
  18. set((x, y) for (x, y) in recorder.applied_migrations() if x == "myapp"),
  19. set(),
  20. )
  21. recorder.record_applied("myapp", "0432_ponies")
  22. self.assertEqual(
  23. set((x, y) for (x, y) in recorder.applied_migrations() if x == "myapp"),
  24. {("myapp", "0432_ponies")},
  25. )
  26. # That should not affect records of another database
  27. recorder_other = MigrationRecorder(connections['other'])
  28. self.assertEqual(
  29. set((x, y) for (x, y) in recorder_other.applied_migrations() if x == "myapp"),
  30. set(),
  31. )
  32. recorder.record_unapplied("myapp", "0432_ponies")
  33. self.assertEqual(
  34. set((x, y) for (x, y) in recorder.applied_migrations() if x == "myapp"),
  35. set(),
  36. )
  37. class LoaderTests(TestCase):
  38. """
  39. Tests the disk and database loader, and running through migrations
  40. in memory.
  41. """
  42. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
  43. @modify_settings(INSTALLED_APPS={'append': 'basic'})
  44. def test_load(self):
  45. """
  46. Makes sure the loader can load the migrations for the test apps,
  47. and then render them out to a new Apps.
  48. """
  49. # Load and test the plan
  50. migration_loader = MigrationLoader(connection)
  51. self.assertEqual(
  52. migration_loader.graph.forwards_plan(("migrations", "0002_second")),
  53. [
  54. ("migrations", "0001_initial"),
  55. ("migrations", "0002_second"),
  56. ],
  57. )
  58. # Now render it out!
  59. project_state = migration_loader.project_state(("migrations", "0002_second"))
  60. self.assertEqual(len(project_state.models), 2)
  61. author_state = project_state.models["migrations", "author"]
  62. self.assertEqual(
  63. [x for x, y in author_state.fields],
  64. ["id", "name", "slug", "age", "rating"]
  65. )
  66. book_state = project_state.models["migrations", "book"]
  67. self.assertEqual(
  68. [x for x, y in book_state.fields],
  69. ["id", "author"]
  70. )
  71. # Ensure we've included unmigrated apps in there too
  72. self.assertIn("basic", project_state.real_apps)
  73. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_unmigdep"})
  74. def test_load_unmigrated_dependency(self):
  75. """
  76. Makes sure the loader can load migrations with a dependency on an unmigrated app.
  77. """
  78. # Load and test the plan
  79. migration_loader = MigrationLoader(connection)
  80. self.assertEqual(
  81. migration_loader.graph.forwards_plan(("migrations", "0001_initial")),
  82. [
  83. ('contenttypes', '0001_initial'),
  84. ('auth', '0001_initial'),
  85. ("migrations", "0001_initial"),
  86. ],
  87. )
  88. # Now render it out!
  89. project_state = migration_loader.project_state(("migrations", "0001_initial"))
  90. self.assertEqual(len([m for a, m in project_state.models if a == "migrations"]), 1)
  91. book_state = project_state.models["migrations", "book"]
  92. self.assertEqual(
  93. [x for x, y in book_state.fields],
  94. ["id", "user"]
  95. )
  96. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_run_before"})
  97. def test_run_before(self):
  98. """
  99. Makes sure the loader uses Migration.run_before.
  100. """
  101. # Load and test the plan
  102. migration_loader = MigrationLoader(connection)
  103. self.assertEqual(
  104. migration_loader.graph.forwards_plan(("migrations", "0002_second")),
  105. [
  106. ("migrations", "0001_initial"),
  107. ("migrations", "0003_third"),
  108. ("migrations", "0002_second"),
  109. ],
  110. )
  111. @override_settings(MIGRATION_MODULES={
  112. "migrations": "migrations.test_migrations_first",
  113. "migrations2": "migrations2.test_migrations_2_first",
  114. })
  115. @modify_settings(INSTALLED_APPS={'append': 'migrations2'})
  116. def test_first(self):
  117. """
  118. Makes sure the '__first__' migrations build correctly.
  119. """
  120. migration_loader = MigrationLoader(connection)
  121. self.assertEqual(
  122. migration_loader.graph.forwards_plan(("migrations", "second")),
  123. [
  124. ("migrations", "thefirst"),
  125. ("migrations2", "0001_initial"),
  126. ("migrations2", "0002_second"),
  127. ("migrations", "second"),
  128. ],
  129. )
  130. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
  131. def test_name_match(self):
  132. "Tests prefix name matching"
  133. migration_loader = MigrationLoader(connection)
  134. self.assertEqual(
  135. migration_loader.get_migration_by_prefix("migrations", "0001").name,
  136. "0001_initial",
  137. )
  138. with self.assertRaises(AmbiguityError):
  139. migration_loader.get_migration_by_prefix("migrations", "0")
  140. with self.assertRaises(KeyError):
  141. migration_loader.get_migration_by_prefix("migrations", "blarg")
  142. def test_load_import_error(self):
  143. with override_settings(MIGRATION_MODULES={"migrations": "import_error_package"}):
  144. with self.assertRaises(ImportError):
  145. MigrationLoader(connection)
  146. def test_load_module_file(self):
  147. with override_settings(MIGRATION_MODULES={"migrations": "migrations.faulty_migrations.file"}):
  148. loader = MigrationLoader(connection)
  149. self.assertIn(
  150. "migrations", loader.unmigrated_apps,
  151. "App with migrations module file not in unmigrated apps."
  152. )
  153. def test_load_empty_dir(self):
  154. with override_settings(MIGRATION_MODULES={"migrations": "migrations.faulty_migrations.namespace"}):
  155. loader = MigrationLoader(connection)
  156. self.assertIn(
  157. "migrations", loader.unmigrated_apps,
  158. "App missing __init__.py in migrations module not in unmigrated apps."
  159. )
  160. @override_settings(
  161. INSTALLED_APPS=['migrations.migrations_test_apps.migrated_app'],
  162. )
  163. def test_marked_as_migrated(self):
  164. """
  165. Undefined MIGRATION_MODULES implies default migration module.
  166. """
  167. migration_loader = MigrationLoader(connection)
  168. self.assertEqual(migration_loader.migrated_apps, {'migrated_app'})
  169. self.assertEqual(migration_loader.unmigrated_apps, set())
  170. @override_settings(
  171. INSTALLED_APPS=['migrations.migrations_test_apps.migrated_app'],
  172. MIGRATION_MODULES={"migrated_app": None},
  173. )
  174. def test_marked_as_unmigrated(self):
  175. """
  176. MIGRATION_MODULES allows disabling of migrations for a particular app.
  177. """
  178. migration_loader = MigrationLoader(connection)
  179. self.assertEqual(migration_loader.migrated_apps, set())
  180. self.assertEqual(migration_loader.unmigrated_apps, {'migrated_app'})
  181. @override_settings(
  182. INSTALLED_APPS=['migrations.migrations_test_apps.migrated_app'],
  183. MIGRATION_MODULES={'migrated_app': 'missing-module'},
  184. )
  185. def test_explicit_missing_module(self):
  186. """
  187. If a MIGRATION_MODULES override points to a missing module, the error
  188. raised during the importation attempt should be propagated unless
  189. `ignore_no_migrations=True`.
  190. """
  191. with self.assertRaisesMessage(ImportError, 'missing-module'):
  192. migration_loader = MigrationLoader(connection)
  193. migration_loader = MigrationLoader(connection, ignore_no_migrations=True)
  194. self.assertEqual(migration_loader.migrated_apps, set())
  195. self.assertEqual(migration_loader.unmigrated_apps, {'migrated_app'})
  196. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"})
  197. def test_loading_squashed(self):
  198. "Tests loading a squashed migration"
  199. migration_loader = MigrationLoader(connection)
  200. recorder = MigrationRecorder(connection)
  201. self.addCleanup(recorder.flush)
  202. # Loading with nothing applied should just give us the one node
  203. self.assertEqual(
  204. len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]),
  205. 1,
  206. )
  207. # However, fake-apply one migration and it should now use the old two
  208. recorder.record_applied("migrations", "0001_initial")
  209. migration_loader.build_graph()
  210. self.assertEqual(
  211. len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]),
  212. 2,
  213. )
  214. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed_complex"})
  215. def test_loading_squashed_complex(self):
  216. "Tests loading a complex set of squashed migrations"
  217. loader = MigrationLoader(connection)
  218. recorder = MigrationRecorder(connection)
  219. self.addCleanup(recorder.flush)
  220. def num_nodes():
  221. plan = set(loader.graph.forwards_plan(('migrations', '7_auto')))
  222. return len(plan - loader.applied_migrations)
  223. # Empty database: use squashed migration
  224. loader.build_graph()
  225. self.assertEqual(num_nodes(), 5)
  226. # Starting at 1 or 2 should use the squashed migration too
  227. recorder.record_applied("migrations", "1_auto")
  228. loader.build_graph()
  229. self.assertEqual(num_nodes(), 4)
  230. recorder.record_applied("migrations", "2_auto")
  231. loader.build_graph()
  232. self.assertEqual(num_nodes(), 3)
  233. # However, starting at 3 to 5 cannot use the squashed migration
  234. recorder.record_applied("migrations", "3_auto")
  235. loader.build_graph()
  236. self.assertEqual(num_nodes(), 4)
  237. recorder.record_applied("migrations", "4_auto")
  238. loader.build_graph()
  239. self.assertEqual(num_nodes(), 3)
  240. # Starting at 5 to 7 we are passed the squashed migrations
  241. recorder.record_applied("migrations", "5_auto")
  242. loader.build_graph()
  243. self.assertEqual(num_nodes(), 2)
  244. recorder.record_applied("migrations", "6_auto")
  245. loader.build_graph()
  246. self.assertEqual(num_nodes(), 1)
  247. recorder.record_applied("migrations", "7_auto")
  248. loader.build_graph()
  249. self.assertEqual(num_nodes(), 0)
  250. @override_settings(MIGRATION_MODULES={
  251. "app1": "migrations.test_migrations_squashed_complex_multi_apps.app1",
  252. "app2": "migrations.test_migrations_squashed_complex_multi_apps.app2",
  253. })
  254. @modify_settings(INSTALLED_APPS={'append': [
  255. "migrations.test_migrations_squashed_complex_multi_apps.app1",
  256. "migrations.test_migrations_squashed_complex_multi_apps.app2",
  257. ]})
  258. def test_loading_squashed_complex_multi_apps(self):
  259. loader = MigrationLoader(connection)
  260. loader.build_graph()
  261. plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
  262. expected_plan = {
  263. ('app1', '1_auto'),
  264. ('app2', '1_squashed_2'),
  265. ('app1', '2_squashed_3'),
  266. ('app1', '4_auto'),
  267. }
  268. self.assertEqual(plan, expected_plan)
  269. @override_settings(MIGRATION_MODULES={
  270. "app1": "migrations.test_migrations_squashed_complex_multi_apps.app1",
  271. "app2": "migrations.test_migrations_squashed_complex_multi_apps.app2",
  272. })
  273. @modify_settings(INSTALLED_APPS={'append': [
  274. "migrations.test_migrations_squashed_complex_multi_apps.app1",
  275. "migrations.test_migrations_squashed_complex_multi_apps.app2",
  276. ]})
  277. def test_loading_squashed_complex_multi_apps_partially_applied(self):
  278. loader = MigrationLoader(connection)
  279. recorder = MigrationRecorder(connection)
  280. recorder.record_applied('app1', '1_auto')
  281. recorder.record_applied('app1', '2_auto')
  282. loader.build_graph()
  283. plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
  284. plan = plan - loader.applied_migrations
  285. expected_plan = {
  286. ('app2', '1_squashed_2'),
  287. ('app1', '3_auto'),
  288. ('app1', '4_auto'),
  289. }
  290. self.assertEqual(plan, expected_plan)
  291. @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed_erroneous"})
  292. def test_loading_squashed_erroneous(self):
  293. "Tests loading a complex but erroneous set of squashed migrations"
  294. loader = MigrationLoader(connection)
  295. recorder = MigrationRecorder(connection)
  296. self.addCleanup(recorder.flush)
  297. def num_nodes():
  298. plan = set(loader.graph.forwards_plan(('migrations', '7_auto')))
  299. return len(plan - loader.applied_migrations)
  300. # Empty database: use squashed migration
  301. loader.build_graph()
  302. self.assertEqual(num_nodes(), 5)
  303. # Starting at 1 or 2 should use the squashed migration too
  304. recorder.record_applied("migrations", "1_auto")
  305. loader.build_graph()
  306. self.assertEqual(num_nodes(), 4)
  307. recorder.record_applied("migrations", "2_auto")
  308. loader.build_graph()
  309. self.assertEqual(num_nodes(), 3)
  310. # However, starting at 3 or 4, nonexistent migrations would be needed.
  311. msg = ("Migration migrations.6_auto depends on nonexistent node ('migrations', '5_auto'). "
  312. "Django tried to replace migration migrations.5_auto with any of "
  313. "[migrations.3_squashed_5] but wasn't able to because some of the replaced "
  314. "migrations are already applied.")
  315. recorder.record_applied("migrations", "3_auto")
  316. with self.assertRaisesMessage(NodeNotFoundError, msg):
  317. loader.build_graph()
  318. recorder.record_applied("migrations", "4_auto")
  319. with self.assertRaisesMessage(NodeNotFoundError, msg):
  320. loader.build_graph()
  321. # Starting at 5 to 7 we are passed the squashed migrations
  322. recorder.record_applied("migrations", "5_auto")
  323. loader.build_graph()
  324. self.assertEqual(num_nodes(), 2)
  325. recorder.record_applied("migrations", "6_auto")
  326. loader.build_graph()
  327. self.assertEqual(num_nodes(), 1)
  328. recorder.record_applied("migrations", "7_auto")
  329. loader.build_graph()
  330. self.assertEqual(num_nodes(), 0)
  331. @override_settings(
  332. MIGRATION_MODULES={'migrations': 'migrations.test_migrations'},
  333. INSTALLED_APPS=['migrations'],
  334. )
  335. def test_check_consistent_history(self):
  336. loader = MigrationLoader(connection=None)
  337. loader.check_consistent_history(connection)
  338. recorder = MigrationRecorder(connection)
  339. recorder.record_applied('migrations', '0002_second')
  340. msg = (
  341. "Migration migrations.0002_second is applied before its dependency "
  342. "migrations.0001_initial on database 'default'."
  343. )
  344. with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
  345. loader.check_consistent_history(connection)
  346. @override_settings(
  347. MIGRATION_MODULES={'migrations': 'migrations.test_migrations_squashed_extra'},
  348. INSTALLED_APPS=['migrations'],
  349. )
  350. def test_check_consistent_history_squashed(self):
  351. """
  352. MigrationLoader.check_consistent_history() should ignore unapplied
  353. squashed migrations that have all of their `replaces` applied.
  354. """
  355. loader = MigrationLoader(connection=None)
  356. recorder = MigrationRecorder(connection)
  357. recorder.record_applied('migrations', '0001_initial')
  358. recorder.record_applied('migrations', '0002_second')
  359. loader.check_consistent_history(connection)
  360. recorder.record_applied('migrations', '0003_third')
  361. loader.check_consistent_history(connection)
  362. @override_settings(MIGRATION_MODULES={
  363. "app1": "migrations.test_migrations_squashed_ref_squashed.app1",
  364. "app2": "migrations.test_migrations_squashed_ref_squashed.app2",
  365. })
  366. @modify_settings(INSTALLED_APPS={'append': [
  367. "migrations.test_migrations_squashed_ref_squashed.app1",
  368. "migrations.test_migrations_squashed_ref_squashed.app2",
  369. ]})
  370. def test_loading_squashed_ref_squashed(self):
  371. "Tests loading a squashed migration with a new migration referencing it"
  372. r"""
  373. The sample migrations are structured like this:
  374. app_1 1 --> 2 ---------------------*--> 3 *--> 4
  375. \ / /
  376. *-------------------*----/--> 2_sq_3 --*
  377. \ / /
  378. =============== \ ============= / == / ======================
  379. app_2 *--> 1_sq_2 --* /
  380. \ /
  381. *--> 1 --> 2 --*
  382. Where 2_sq_3 is a replacing migration for 2 and 3 in app_1,
  383. as 1_sq_2 is a replacing migration for 1 and 2 in app_2.
  384. """
  385. loader = MigrationLoader(connection)
  386. recorder = MigrationRecorder(connection)
  387. self.addCleanup(recorder.flush)
  388. # Load with nothing applied: both migrations squashed.
  389. loader.build_graph()
  390. plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
  391. plan = plan - loader.applied_migrations
  392. expected_plan = {
  393. ('app1', '1_auto'),
  394. ('app2', '1_squashed_2'),
  395. ('app1', '2_squashed_3'),
  396. ('app1', '4_auto'),
  397. }
  398. self.assertEqual(plan, expected_plan)
  399. # Fake-apply a few from app1: unsquashes migration in app1.
  400. recorder.record_applied('app1', '1_auto')
  401. recorder.record_applied('app1', '2_auto')
  402. loader.build_graph()
  403. plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
  404. plan = plan - loader.applied_migrations
  405. expected_plan = {
  406. ('app2', '1_squashed_2'),
  407. ('app1', '3_auto'),
  408. ('app1', '4_auto'),
  409. }
  410. self.assertEqual(plan, expected_plan)
  411. # Fake-apply one from app2: unsquashes migration in app2 too.
  412. recorder.record_applied('app2', '1_auto')
  413. loader.build_graph()
  414. plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
  415. plan = plan - loader.applied_migrations
  416. expected_plan = {
  417. ('app2', '2_auto'),
  418. ('app1', '3_auto'),
  419. ('app1', '4_auto'),
  420. }
  421. self.assertEqual(plan, expected_plan)