test_graph.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. from django.db.migrations.exceptions import (
  2. CircularDependencyError, NodeNotFoundError,
  3. )
  4. from django.db.migrations.graph import (
  5. RECURSION_DEPTH_WARNING, DummyNode, MigrationGraph, Node,
  6. )
  7. from django.test import SimpleTestCase
  8. class GraphTests(SimpleTestCase):
  9. """
  10. Tests the digraph structure.
  11. """
  12. def test_simple_graph(self):
  13. """
  14. Tests a basic dependency graph:
  15. app_a: 0001 <-- 0002 <--- 0003 <-- 0004
  16. /
  17. app_b: 0001 <-- 0002 <-/
  18. """
  19. # Build graph
  20. graph = MigrationGraph()
  21. graph.add_node(("app_a", "0001"), None)
  22. graph.add_node(("app_a", "0002"), None)
  23. graph.add_node(("app_a", "0003"), None)
  24. graph.add_node(("app_a", "0004"), None)
  25. graph.add_node(("app_b", "0001"), None)
  26. graph.add_node(("app_b", "0002"), None)
  27. graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_a", "0003"))
  28. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002"))
  29. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  30. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002"))
  31. graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001"))
  32. # Test root migration case
  33. self.assertEqual(
  34. graph.forwards_plan(("app_a", "0001")),
  35. [('app_a', '0001')],
  36. )
  37. # Test branch B only
  38. self.assertEqual(
  39. graph.forwards_plan(("app_b", "0002")),
  40. [("app_b", "0001"), ("app_b", "0002")],
  41. )
  42. # Test whole graph
  43. self.assertEqual(
  44. graph.forwards_plan(("app_a", "0004")),
  45. [
  46. ('app_b', '0001'), ('app_b', '0002'), ('app_a', '0001'),
  47. ('app_a', '0002'), ('app_a', '0003'), ('app_a', '0004'),
  48. ],
  49. )
  50. # Test reverse to b:0002
  51. self.assertEqual(
  52. graph.backwards_plan(("app_b", "0002")),
  53. [('app_a', '0004'), ('app_a', '0003'), ('app_b', '0002')],
  54. )
  55. # Test roots and leaves
  56. self.assertEqual(
  57. graph.root_nodes(),
  58. [('app_a', '0001'), ('app_b', '0001')],
  59. )
  60. self.assertEqual(
  61. graph.leaf_nodes(),
  62. [('app_a', '0004'), ('app_b', '0002')],
  63. )
  64. def test_complex_graph(self):
  65. r"""
  66. Tests a complex dependency graph:
  67. app_a: 0001 <-- 0002 <--- 0003 <-- 0004
  68. \ \ / /
  69. app_b: 0001 <-\ 0002 <-X /
  70. \ \ /
  71. app_c: \ 0001 <-- 0002 <-
  72. """
  73. # Build graph
  74. graph = MigrationGraph()
  75. graph.add_node(("app_a", "0001"), None)
  76. graph.add_node(("app_a", "0002"), None)
  77. graph.add_node(("app_a", "0003"), None)
  78. graph.add_node(("app_a", "0004"), None)
  79. graph.add_node(("app_b", "0001"), None)
  80. graph.add_node(("app_b", "0002"), None)
  81. graph.add_node(("app_c", "0001"), None)
  82. graph.add_node(("app_c", "0002"), None)
  83. graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_a", "0003"))
  84. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002"))
  85. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  86. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002"))
  87. graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001"))
  88. graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_c", "0002"))
  89. graph.add_dependency("app_c.0002", ("app_c", "0002"), ("app_c", "0001"))
  90. graph.add_dependency("app_c.0001", ("app_c", "0001"), ("app_b", "0001"))
  91. graph.add_dependency("app_c.0002", ("app_c", "0002"), ("app_a", "0002"))
  92. # Test branch C only
  93. self.assertEqual(
  94. graph.forwards_plan(("app_c", "0002")),
  95. [('app_b', '0001'), ('app_c', '0001'), ('app_a', '0001'), ('app_a', '0002'), ('app_c', '0002')],
  96. )
  97. # Test whole graph
  98. self.assertEqual(
  99. graph.forwards_plan(("app_a", "0004")),
  100. [
  101. ('app_b', '0001'), ('app_c', '0001'), ('app_a', '0001'),
  102. ('app_a', '0002'), ('app_c', '0002'), ('app_b', '0002'),
  103. ('app_a', '0003'), ('app_a', '0004'),
  104. ],
  105. )
  106. # Test reverse to b:0001
  107. self.assertEqual(
  108. graph.backwards_plan(("app_b", "0001")),
  109. [
  110. ('app_a', '0004'), ('app_c', '0002'), ('app_c', '0001'),
  111. ('app_a', '0003'), ('app_b', '0002'), ('app_b', '0001'),
  112. ],
  113. )
  114. # Test roots and leaves
  115. self.assertEqual(
  116. graph.root_nodes(),
  117. [('app_a', '0001'), ('app_b', '0001'), ('app_c', '0001')],
  118. )
  119. self.assertEqual(
  120. graph.leaf_nodes(),
  121. [('app_a', '0004'), ('app_b', '0002'), ('app_c', '0002')],
  122. )
  123. def test_circular_graph(self):
  124. """
  125. Tests a circular dependency graph.
  126. """
  127. # Build graph
  128. graph = MigrationGraph()
  129. graph.add_node(("app_a", "0001"), None)
  130. graph.add_node(("app_a", "0002"), None)
  131. graph.add_node(("app_a", "0003"), None)
  132. graph.add_node(("app_b", "0001"), None)
  133. graph.add_node(("app_b", "0002"), None)
  134. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002"))
  135. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  136. graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002"))
  137. graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001"))
  138. graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0003"))
  139. # Test whole graph
  140. with self.assertRaises(CircularDependencyError):
  141. graph.forwards_plan(("app_a", "0003"))
  142. def test_circular_graph_2(self):
  143. graph = MigrationGraph()
  144. graph.add_node(('A', '0001'), None)
  145. graph.add_node(('C', '0001'), None)
  146. graph.add_node(('B', '0001'), None)
  147. graph.add_dependency('A.0001', ('A', '0001'), ('B', '0001'))
  148. graph.add_dependency('B.0001', ('B', '0001'), ('A', '0001'))
  149. graph.add_dependency('C.0001', ('C', '0001'), ('B', '0001'))
  150. with self.assertRaises(CircularDependencyError):
  151. graph.forwards_plan(('C', '0001'))
  152. def test_graph_recursive(self):
  153. graph = MigrationGraph()
  154. root = ("app_a", "1")
  155. graph.add_node(root, None)
  156. expected = [root]
  157. for i in range(2, 750):
  158. parent = ("app_a", str(i - 1))
  159. child = ("app_a", str(i))
  160. graph.add_node(child, None)
  161. graph.add_dependency(str(i), child, parent)
  162. expected.append(child)
  163. leaf = expected[-1]
  164. forwards_plan = graph.forwards_plan(leaf)
  165. self.assertEqual(expected, forwards_plan)
  166. backwards_plan = graph.backwards_plan(root)
  167. self.assertEqual(expected[::-1], backwards_plan)
  168. def test_graph_iterative(self):
  169. graph = MigrationGraph()
  170. root = ("app_a", "1")
  171. graph.add_node(root, None)
  172. expected = [root]
  173. for i in range(2, 1000):
  174. parent = ("app_a", str(i - 1))
  175. child = ("app_a", str(i))
  176. graph.add_node(child, None)
  177. graph.add_dependency(str(i), child, parent)
  178. expected.append(child)
  179. leaf = expected[-1]
  180. with self.assertWarnsMessage(RuntimeWarning, RECURSION_DEPTH_WARNING):
  181. forwards_plan = graph.forwards_plan(leaf)
  182. self.assertEqual(expected, forwards_plan)
  183. with self.assertWarnsMessage(RuntimeWarning, RECURSION_DEPTH_WARNING):
  184. backwards_plan = graph.backwards_plan(root)
  185. self.assertEqual(expected[::-1], backwards_plan)
  186. def test_plan_invalid_node(self):
  187. """
  188. Tests for forwards/backwards_plan of nonexistent node.
  189. """
  190. graph = MigrationGraph()
  191. message = "Node ('app_b', '0001') not a valid node"
  192. with self.assertRaisesMessage(NodeNotFoundError, message):
  193. graph.forwards_plan(("app_b", "0001"))
  194. with self.assertRaisesMessage(NodeNotFoundError, message):
  195. graph.backwards_plan(("app_b", "0001"))
  196. def test_missing_parent_nodes(self):
  197. """
  198. Tests for missing parent nodes.
  199. """
  200. # Build graph
  201. graph = MigrationGraph()
  202. graph.add_node(("app_a", "0001"), None)
  203. graph.add_node(("app_a", "0002"), None)
  204. graph.add_node(("app_a", "0003"), None)
  205. graph.add_node(("app_b", "0001"), None)
  206. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002"))
  207. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  208. msg = "Migration app_a.0001 dependencies reference nonexistent parent node ('app_b', '0002')"
  209. with self.assertRaisesMessage(NodeNotFoundError, msg):
  210. graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002"))
  211. def test_missing_child_nodes(self):
  212. """
  213. Tests for missing child nodes.
  214. """
  215. # Build graph
  216. graph = MigrationGraph()
  217. graph.add_node(("app_a", "0001"), None)
  218. msg = "Migration app_a.0002 dependencies reference nonexistent child node ('app_a', '0002')"
  219. with self.assertRaisesMessage(NodeNotFoundError, msg):
  220. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  221. def test_validate_consistency(self):
  222. """
  223. Tests for missing nodes, using `validate_consistency()` to raise the error.
  224. """
  225. # Build graph
  226. graph = MigrationGraph()
  227. graph.add_node(("app_a", "0001"), None)
  228. # Add dependency with missing parent node (skipping validation).
  229. graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002"), skip_validation=True)
  230. msg = "Migration app_a.0001 dependencies reference nonexistent parent node ('app_b', '0002')"
  231. with self.assertRaisesMessage(NodeNotFoundError, msg):
  232. graph.validate_consistency()
  233. # Add missing parent node and ensure `validate_consistency()` no longer raises error.
  234. graph.add_node(("app_b", "0002"), None)
  235. graph.validate_consistency()
  236. # Add dependency with missing child node (skipping validation).
  237. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"), skip_validation=True)
  238. msg = "Migration app_a.0002 dependencies reference nonexistent child node ('app_a', '0002')"
  239. with self.assertRaisesMessage(NodeNotFoundError, msg):
  240. graph.validate_consistency()
  241. # Add missing child node and ensure `validate_consistency()` no longer raises error.
  242. graph.add_node(("app_a", "0002"), None)
  243. graph.validate_consistency()
  244. # Rawly add dummy node.
  245. msg = "app_a.0001 (req'd by app_a.0002) is missing!"
  246. graph.add_dummy_node(
  247. key=("app_a", "0001"),
  248. origin="app_a.0002",
  249. error_message=msg
  250. )
  251. with self.assertRaisesMessage(NodeNotFoundError, msg):
  252. graph.validate_consistency()
  253. def test_remove_replaced_nodes(self):
  254. """
  255. Replaced nodes are properly removed and dependencies remapped.
  256. """
  257. # Add some dummy nodes to be replaced.
  258. graph = MigrationGraph()
  259. graph.add_dummy_node(key=("app_a", "0001"), origin="app_a.0002", error_message="BAD!")
  260. graph.add_dummy_node(key=("app_a", "0002"), origin="app_b.0001", error_message="BAD!")
  261. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"), skip_validation=True)
  262. # Add some normal parent and child nodes to test dependency remapping.
  263. graph.add_node(("app_c", "0001"), None)
  264. graph.add_node(("app_b", "0001"), None)
  265. graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_c", "0001"), skip_validation=True)
  266. graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0002"), skip_validation=True)
  267. # Try replacing before replacement node exists.
  268. msg = (
  269. "Unable to find replacement node ('app_a', '0001_squashed_0002'). It was either"
  270. " never added to the migration graph, or has been removed."
  271. )
  272. with self.assertRaisesMessage(NodeNotFoundError, msg):
  273. graph.remove_replaced_nodes(
  274. replacement=("app_a", "0001_squashed_0002"),
  275. replaced=[("app_a", "0001"), ("app_a", "0002")]
  276. )
  277. graph.add_node(("app_a", "0001_squashed_0002"), None)
  278. # Ensure `validate_consistency()` still raises an error at this stage.
  279. with self.assertRaisesMessage(NodeNotFoundError, "BAD!"):
  280. graph.validate_consistency()
  281. # Remove the dummy nodes.
  282. graph.remove_replaced_nodes(
  283. replacement=("app_a", "0001_squashed_0002"),
  284. replaced=[("app_a", "0001"), ("app_a", "0002")]
  285. )
  286. # Ensure graph is now consistent and dependencies have been remapped
  287. graph.validate_consistency()
  288. parent_node = graph.node_map[("app_c", "0001")]
  289. replacement_node = graph.node_map[("app_a", "0001_squashed_0002")]
  290. child_node = graph.node_map[("app_b", "0001")]
  291. self.assertIn(parent_node, replacement_node.parents)
  292. self.assertIn(replacement_node, parent_node.children)
  293. self.assertIn(child_node, replacement_node.children)
  294. self.assertIn(replacement_node, child_node.parents)
  295. def test_remove_replacement_node(self):
  296. """
  297. A replacement node is properly removed and child dependencies remapped.
  298. We assume parent dependencies are already correct.
  299. """
  300. # Add some dummy nodes to be replaced.
  301. graph = MigrationGraph()
  302. graph.add_node(("app_a", "0001"), None)
  303. graph.add_node(("app_a", "0002"), None)
  304. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  305. # Try removing replacement node before replacement node exists.
  306. msg = (
  307. "Unable to remove replacement node ('app_a', '0001_squashed_0002'). It was"
  308. " either never added to the migration graph, or has been removed already."
  309. )
  310. with self.assertRaisesMessage(NodeNotFoundError, msg):
  311. graph.remove_replacement_node(
  312. replacement=("app_a", "0001_squashed_0002"),
  313. replaced=[("app_a", "0001"), ("app_a", "0002")]
  314. )
  315. graph.add_node(("app_a", "0001_squashed_0002"), None)
  316. # Add a child node to test dependency remapping.
  317. graph.add_node(("app_b", "0001"), None)
  318. graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0001_squashed_0002"))
  319. # Remove the replacement node.
  320. graph.remove_replacement_node(
  321. replacement=("app_a", "0001_squashed_0002"),
  322. replaced=[("app_a", "0001"), ("app_a", "0002")]
  323. )
  324. # Ensure graph is consistent and child dependency has been remapped
  325. graph.validate_consistency()
  326. replaced_node = graph.node_map[("app_a", "0002")]
  327. child_node = graph.node_map[("app_b", "0001")]
  328. self.assertIn(child_node, replaced_node.children)
  329. self.assertIn(replaced_node, child_node.parents)
  330. # Ensure child dependency hasn't also gotten remapped to the other replaced node.
  331. other_replaced_node = graph.node_map[("app_a", "0001")]
  332. self.assertNotIn(child_node, other_replaced_node.children)
  333. self.assertNotIn(other_replaced_node, child_node.parents)
  334. def test_infinite_loop(self):
  335. """
  336. Tests a complex dependency graph:
  337. app_a: 0001 <-
  338. \
  339. app_b: 0001 <- x 0002 <-
  340. / \
  341. app_c: 0001<- <------------- x 0002
  342. And apply squashing on app_c.
  343. """
  344. graph = MigrationGraph()
  345. graph.add_node(("app_a", "0001"), None)
  346. graph.add_node(("app_b", "0001"), None)
  347. graph.add_node(("app_b", "0002"), None)
  348. graph.add_node(("app_c", "0001_squashed_0002"), None)
  349. graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_c", "0001_squashed_0002"))
  350. graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_a", "0001"))
  351. graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001"))
  352. graph.add_dependency("app_c.0001_squashed_0002", ("app_c", "0001_squashed_0002"), ("app_b", "0002"))
  353. with self.assertRaises(CircularDependencyError):
  354. graph.forwards_plan(("app_c", "0001_squashed_0002"))
  355. def test_stringify(self):
  356. graph = MigrationGraph()
  357. self.assertEqual(str(graph), "Graph: 0 nodes, 0 edges")
  358. graph.add_node(("app_a", "0001"), None)
  359. graph.add_node(("app_a", "0002"), None)
  360. graph.add_node(("app_a", "0003"), None)
  361. graph.add_node(("app_b", "0001"), None)
  362. graph.add_node(("app_b", "0002"), None)
  363. graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
  364. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002"))
  365. graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002"))
  366. self.assertEqual(str(graph), "Graph: 5 nodes, 3 edges")
  367. self.assertEqual(repr(graph), "<MigrationGraph: nodes=5, edges=3>")
  368. class NodeTests(SimpleTestCase):
  369. def test_node_repr(self):
  370. node = Node(('app_a', '0001'))
  371. self.assertEqual(repr(node), "<Node: ('app_a', '0001')>")
  372. def test_dummynode_repr(self):
  373. node = DummyNode(
  374. key=('app_a', '0001'),
  375. origin='app_a.0001',
  376. error_message='x is missing',
  377. )
  378. self.assertEqual(repr(node), "<DummyNode: ('app_a', '0001')>")
  379. def test_dummynode_promote(self):
  380. dummy = DummyNode(
  381. key=('app_a', '0001'),
  382. origin='app_a.0002',
  383. error_message="app_a.0001 (req'd by app_a.0002) is missing!",
  384. )
  385. dummy.promote()
  386. self.assertIsInstance(dummy, Node)
  387. self.assertFalse(hasattr(dummy, 'origin'))
  388. self.assertFalse(hasattr(dummy, 'error_message'))