test_loader.py 21 KB

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