check_dependent_pr_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. """Tests for check_dependent_pr.py."""
  2. __copyright__ = """
  3. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  4. Exceptions. See /LICENSE for license information.
  5. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  6. """
  7. import json
  8. import unittest
  9. from unittest import mock
  10. from typing import Any
  11. import check_dependent_pr
  12. import github_helpers
  13. _OID1 = "1" * 40
  14. _OID2 = "2" * 40
  15. _OID3 = "3" * 40
  16. _OID4 = "4" * 40
  17. _OID9 = "9" * 40
  18. class TestCheckDependentPR(unittest.TestCase):
  19. def setUp(self) -> None:
  20. self.mock_client = mock.MagicMock(spec=github_helpers.Client)
  21. # Mock requests.post to avoid network calls and track status updates.
  22. self.requests_post_patcher = mock.patch("requests.post")
  23. self.mock_post = self.requests_post_patcher.start()
  24. def tearDown(self) -> None:
  25. self.requests_post_patcher.stop()
  26. def _assert_status(self, sha: str, state: str, description: str) -> None:
  27. """Validates that requests.post was called to set the commit status."""
  28. self.mock_post.assert_called_once()
  29. args, kwargs = self.mock_post.call_args
  30. self.assertIn(f"statuses/{sha}", args[0])
  31. self.assertEqual(kwargs["json"]["state"], state)
  32. self.assertEqual(kwargs["json"]["context"], "PR dependencies check")
  33. self.assertEqual(kwargs["json"]["description"], description)
  34. def _make_comment(
  35. self,
  36. open_deps: list[int],
  37. merged_deps: list[int] = None,
  38. first_commit: str = None,
  39. comment_id: str = "comment_id",
  40. ) -> dict[str, str]:
  41. """Builds a boilerplate PR comment."""
  42. state: dict[str, Any] = {
  43. "open": open_deps,
  44. "merged": merged_deps if merged_deps else [],
  45. }
  46. if first_commit:
  47. state["first_commit"] = first_commit
  48. return {
  49. "id": comment_id,
  50. "body": f"<!-- check_dependent_pr {json.dumps(state)} -->",
  51. }
  52. def _make_pr_response(
  53. self,
  54. pr_id: str,
  55. head_ref_oid: str,
  56. commits: list[str],
  57. comments: list[dict[str, str]] = None,
  58. has_dependent_label: bool = False,
  59. ) -> dict[str, Any]:
  60. """Builds a boilerplate GitHub response for a PR."""
  61. labels = (
  62. [{"name": "dependent", "id": "label_dependent"}]
  63. if has_dependent_label
  64. else []
  65. )
  66. return {
  67. "repository": {
  68. "pullRequest": {
  69. "id": pr_id,
  70. "headRefOid": head_ref_oid,
  71. "labels": {"nodes": labels},
  72. "commits": {
  73. "nodes": [{"commit": {"oid": oid}} for oid in commits]
  74. },
  75. "comments": {"nodes": comments if comments else []},
  76. }
  77. }
  78. }
  79. def test_process_pr_no_overlap(self) -> None:
  80. self.mock_client.execute.return_value = self._make_pr_response(
  81. pr_id="pr_1",
  82. head_ref_oid=_OID1,
  83. commits=[_OID1],
  84. )
  85. check_dependent_pr._process_pr(
  86. self.mock_client,
  87. pr_number=1,
  88. pr_to_commits={1: [_OID1]},
  89. open_pr_numbers={1},
  90. label_id="label_id",
  91. token="test_token",
  92. )
  93. self.assertEqual(self.mock_client.execute.call_count, 1)
  94. self._assert_status(
  95. _OID1, "success", "This PR has no open dependencies"
  96. )
  97. def test_process_pr_with_overlap(self) -> None:
  98. self.mock_client.execute.return_value = self._make_pr_response(
  99. pr_id="pr_2",
  100. head_ref_oid=_OID2,
  101. commits=[_OID1, _OID2],
  102. )
  103. check_dependent_pr._process_pr(
  104. self.mock_client,
  105. pr_number=2,
  106. pr_to_commits={1: [_OID1], 2: [_OID1, _OID2]},
  107. open_pr_numbers={1, 2},
  108. label_id="label_dependent",
  109. token="test_token",
  110. )
  111. self.assertEqual(self.mock_client.execute.call_count, 3)
  112. calls = self.mock_client.execute.call_args_list
  113. self.assertIn("addLabelsToLabelable", calls[1][0][0])
  114. self.assertIn("addComment", calls[2][0][0])
  115. self._assert_status(
  116. _OID2, "pending", "This PR has open dependencies: #1"
  117. )
  118. def test_process_pr_dependencies_merged(self) -> None:
  119. self.mock_client.execute.return_value = self._make_pr_response(
  120. pr_id="pr_3",
  121. head_ref_oid=_OID2,
  122. commits=[_OID1, _OID2],
  123. comments=[self._make_comment(open_deps=[1])],
  124. has_dependent_label=True,
  125. )
  126. check_dependent_pr._process_pr(
  127. self.mock_client,
  128. pr_number=3,
  129. pr_to_commits={3: [_OID1, _OID2]},
  130. open_pr_numbers={3},
  131. label_id="label_dependent",
  132. token="test_token",
  133. )
  134. calls = self.mock_client.execute.call_args_list
  135. self.assertIn("removeLabelsFromLabelable", calls[1][0][0])
  136. self.assertIn("updateIssueComment", calls[2][0][0])
  137. self._assert_status(
  138. _OID2, "success", "This PR has no open dependencies"
  139. )
  140. def test_process_pr_dependency_got_new_commits(self) -> None:
  141. self.mock_client.execute.return_value = self._make_pr_response(
  142. pr_id="pr_3",
  143. head_ref_oid=_OID2,
  144. commits=[_OID1, _OID2],
  145. comments=[self._make_comment(open_deps=[1, 2])],
  146. has_dependent_label=True,
  147. )
  148. check_dependent_pr._process_pr(
  149. self.mock_client,
  150. pr_number=3,
  151. pr_to_commits={1: [_OID1, _OID4], 3: [_OID1, _OID2]},
  152. open_pr_numbers={1, 3},
  153. label_id="label_dependent",
  154. token="test_token",
  155. )
  156. calls = self.mock_client.execute.call_args_list
  157. update_mutation = calls[1][0][0]
  158. self.assertIn("updateIssueComment", update_mutation)
  159. variable_values = calls[1][1]["variable_values"]
  160. self.assertIn('"open": [1]', variable_values["body"])
  161. self.assertIn('"merged": [2]', variable_values["body"])
  162. self._assert_status(
  163. _OID2, "pending", "This PR has open dependencies: #1"
  164. )
  165. def test_process_pr_non_coherent_prefix(self) -> None:
  166. self.mock_client.execute.return_value = self._make_pr_response(
  167. pr_id="pr_10",
  168. head_ref_oid=_OID2,
  169. commits=[_OID1, _OID2],
  170. )
  171. check_dependent_pr._process_pr(
  172. self.mock_client,
  173. pr_number=10,
  174. pr_to_commits={10: [_OID1, _OID2], 11: [_OID1, _OID3]},
  175. open_pr_numbers={10, 11},
  176. label_id="label_dependent",
  177. token="test_token",
  178. )
  179. self.assertEqual(self.mock_client.execute.call_count, 1)
  180. self._assert_status(
  181. _OID2, "success", "This PR has no open dependencies"
  182. )
  183. def test_process_pr_overlap_only_on_head_ref(self) -> None:
  184. self.mock_client.execute.return_value = self._make_pr_response(
  185. pr_id="pr_9",
  186. head_ref_oid=_OID2,
  187. commits=[_OID1, _OID2],
  188. )
  189. check_dependent_pr._process_pr(
  190. self.mock_client,
  191. pr_number=9,
  192. pr_to_commits={1: [_OID2], 9: [_OID1, _OID2]},
  193. open_pr_numbers={1, 9},
  194. label_id="label_dependent",
  195. token="test_token",
  196. )
  197. self.assertEqual(self.mock_client.execute.call_count, 3)
  198. calls = self.mock_client.execute.call_args_list
  199. self.assertIn("addLabelsToLabelable", calls[1][0][0])
  200. self.assertIn("addComment", calls[2][0][0])
  201. self._assert_status(
  202. _OID2, "pending", "This PR has open dependencies: #1"
  203. )
  204. def test_process_pr_scanning_no_add(self) -> None:
  205. self.mock_client.execute.return_value = self._make_pr_response(
  206. pr_id="pr_7",
  207. head_ref_oid=_OID2,
  208. commits=[_OID1, _OID2],
  209. )
  210. check_dependent_pr._process_pr(
  211. self.mock_client,
  212. pr_number=7,
  213. pr_to_commits={1: [_OID1], 7: [_OID1, _OID2]},
  214. open_pr_numbers={1, 7},
  215. label_id="label_dependent",
  216. token="test_token",
  217. scanning=True,
  218. )
  219. self.assertEqual(self.mock_client.execute.call_count, 1)
  220. self._assert_status(
  221. _OID2, "pending", "This PR has open dependencies: #1"
  222. )
  223. def test_process_pr_no_changes_needed(self) -> None:
  224. self.mock_client.execute.return_value = self._make_pr_response(
  225. pr_id="pr_6",
  226. head_ref_oid=_OID2,
  227. commits=[_OID1, _OID2],
  228. comments=[self._make_comment(open_deps=[1], first_commit=_OID2)],
  229. has_dependent_label=True,
  230. )
  231. check_dependent_pr._process_pr(
  232. self.mock_client,
  233. pr_number=6,
  234. pr_to_commits={1: [_OID1], 6: [_OID1, _OID2]},
  235. open_pr_numbers={1, 6},
  236. label_id="label_dependent",
  237. token="test_token",
  238. )
  239. self.assertEqual(self.mock_client.execute.call_count, 1)
  240. self._assert_status(
  241. _OID2, "pending", "This PR has open dependencies: #1"
  242. )
  243. def test_process_pr_invalid_marker(self) -> None:
  244. self.mock_client.execute.return_value = self._make_pr_response(
  245. pr_id="pr_5",
  246. head_ref_oid=_OID1,
  247. commits=[_OID1],
  248. comments=[
  249. {
  250. "id": "comment_id",
  251. "body": "<!-- check_dependent_pr {invalid_json} -->",
  252. }
  253. ],
  254. )
  255. import json
  256. self.assertRaises(
  257. json.decoder.JSONDecodeError,
  258. check_dependent_pr._process_pr,
  259. self.mock_client,
  260. pr_number=5,
  261. pr_to_commits={5: [_OID1]},
  262. open_pr_numbers={5},
  263. label_id="label_dependent",
  264. token="test_token",
  265. )
  266. def test_process_pr_hidden_comment(self) -> None:
  267. self.mock_client.execute.return_value = self._make_pr_response(
  268. pr_id="pr_14",
  269. head_ref_oid=_OID2,
  270. commits=[_OID1, _OID2],
  271. comments=[
  272. {
  273. "id": "hidden_comment_id",
  274. "body": '<!-- check_dependent_pr {"open": [1]} -->',
  275. "isMinimized": True,
  276. }
  277. ],
  278. has_dependent_label=True,
  279. )
  280. check_dependent_pr._process_pr(
  281. self.mock_client,
  282. pr_number=14,
  283. pr_to_commits={1: [_OID1], 14: [_OID1, _OID2]},
  284. open_pr_numbers={1, 14},
  285. label_id="label_dependent",
  286. token="test_token",
  287. )
  288. calls = self.mock_client.execute.call_args_list
  289. self.assertEqual(self.mock_client.execute.call_count, 2)
  290. self.assertIn("addComment", calls[1][0][0])
  291. self._assert_status(
  292. _OID2, "pending", "This PR has open dependencies: #1"
  293. )
  294. def test_process_pr_sticky_first_commit(self) -> None:
  295. self.mock_client.execute.return_value = self._make_pr_response(
  296. pr_id="pr_11",
  297. head_ref_oid=_OID3,
  298. commits=[_OID1, _OID2, _OID3],
  299. comments=[self._make_comment(open_deps=[1, 2], first_commit=_OID2)],
  300. has_dependent_label=True,
  301. )
  302. check_dependent_pr._process_pr(
  303. self.mock_client,
  304. pr_number=11,
  305. pr_to_commits={1: [_OID1], 11: [_OID1, _OID2, _OID3]},
  306. open_pr_numbers={1, 11},
  307. label_id="label_dependent",
  308. token="test_token",
  309. )
  310. calls = self.mock_client.execute.call_args_list
  311. variable_values = calls[1][1]["variable_values"]
  312. self.assertIn(_OID2[:8], variable_values["body"])
  313. self.assertNotIn(_OID1[:8], variable_values["body"])
  314. self._assert_status(
  315. _OID3, "pending", "This PR has open dependencies: #1"
  316. )
  317. def test_process_pr_rebase_first_commit(self) -> None:
  318. self.mock_client.execute.return_value = self._make_pr_response(
  319. pr_id="pr_12",
  320. head_ref_oid=_OID2,
  321. commits=[_OID1, _OID2],
  322. comments=[self._make_comment(open_deps=[1, 2])],
  323. has_dependent_label=True,
  324. )
  325. check_dependent_pr._process_pr(
  326. self.mock_client,
  327. pr_number=12,
  328. pr_to_commits={1: [_OID9], 12: [_OID1, _OID2]},
  329. open_pr_numbers={1, 12},
  330. label_id="label_dependent",
  331. token="test_token",
  332. )
  333. calls = self.mock_client.execute.call_args_list
  334. variable_values = calls[1][1]["variable_values"]
  335. self.assertIn(_OID1[:8], variable_values["body"])
  336. self._assert_status(
  337. _OID2, "pending", "This PR has open dependencies: #1"
  338. )
  339. def test_process_pr_fallback_no_independent_commit(self) -> None:
  340. self.mock_client.execute.return_value = self._make_pr_response(
  341. pr_id="pr_13",
  342. head_ref_oid=_OID2,
  343. commits=[_OID1, _OID2],
  344. comments=[self._make_comment(open_deps=[1, 2])],
  345. has_dependent_label=True,
  346. )
  347. check_dependent_pr._process_pr(
  348. self.mock_client,
  349. pr_number=13,
  350. pr_to_commits={1: [_OID1, _OID2], 13: [_OID1, _OID2]},
  351. open_pr_numbers={1, 13},
  352. label_id="label_dependent",
  353. token="test_token",
  354. )
  355. calls = self.mock_client.execute.call_args_list
  356. variable_values = calls[1][1]["variable_values"]
  357. self.assertIn(
  358. "unable to identify starting review commit", variable_values["body"]
  359. )
  360. self._assert_status(
  361. _OID2, "pending", "This PR has open dependencies: #1"
  362. )
  363. def test_process_pr_sequence_failure(self) -> None:
  364. self.mock_client.execute.return_value = self._make_pr_response(
  365. pr_id="pr_1",
  366. head_ref_oid=_OID1,
  367. commits=[_OID1],
  368. )
  369. check_dependent_pr._process_pr(
  370. self.mock_client,
  371. pr_number=1,
  372. pr_to_commits={1: [_OID1], 2: [_OID1, _OID2]},
  373. open_pr_numbers={1, 2},
  374. label_id="label_dependent",
  375. token="test_token",
  376. )
  377. self.assertEqual(self.mock_client.execute.call_count, 1)
  378. self._assert_status(
  379. _OID1, "success", "This PR has no open dependencies"
  380. )
  381. def test_process_pr_no_overlap_different_commits(self) -> None:
  382. self.mock_client.execute.return_value = self._make_pr_response(
  383. pr_id="pr_2",
  384. head_ref_oid=_OID2,
  385. commits=[_OID2],
  386. )
  387. check_dependent_pr._process_pr(
  388. self.mock_client,
  389. pr_number=2,
  390. pr_to_commits={1: [_OID1], 2: [_OID2]},
  391. open_pr_numbers={1, 2},
  392. label_id="label_dependent",
  393. token="test_token",
  394. )
  395. self.assertEqual(self.mock_client.execute.call_count, 1)
  396. self._assert_status(
  397. _OID2, "success", "This PR has no open dependencies"
  398. )
  399. def test_process_pr_no_unique_commit(self) -> None:
  400. self.mock_client.execute.return_value = self._make_pr_response(
  401. pr_id="pr_2",
  402. head_ref_oid=_OID2,
  403. commits=[_OID1, _OID2],
  404. )
  405. check_dependent_pr._process_pr(
  406. self.mock_client,
  407. pr_number=2,
  408. pr_to_commits={1: [_OID1, _OID2, _OID3], 2: [_OID1, _OID2]},
  409. open_pr_numbers={1, 2},
  410. label_id="label_dependent",
  411. token="test_token",
  412. )
  413. self.assertEqual(self.mock_client.execute.call_count, 1)
  414. self._assert_status(
  415. _OID2, "success", "This PR has no open dependencies"
  416. )
  417. def test_process_pr_multiple_non_overlapping_commits(self) -> None:
  418. self.mock_client.execute.return_value = self._make_pr_response(
  419. pr_id="pr_2",
  420. head_ref_oid=_OID4,
  421. commits=[_OID1, _OID3, _OID4],
  422. )
  423. check_dependent_pr._process_pr(
  424. self.mock_client,
  425. pr_number=2,
  426. pr_to_commits={1: [_OID1, _OID2], 2: [_OID1, _OID3, _OID4]},
  427. open_pr_numbers={1, 2},
  428. label_id="label_dependent",
  429. token="test_token",
  430. )
  431. self.assertEqual(self.mock_client.execute.call_count, 3)
  432. calls = self.mock_client.execute.call_args_list
  433. self.assertIn("addLabelsToLabelable", calls[1][0][0])
  434. self._assert_status(
  435. _OID4, "pending", "This PR has open dependencies: #1"
  436. )
  437. def test_always_sets_status_check_success(self) -> None:
  438. self.mock_client.execute.return_value = self._make_pr_response(
  439. pr_id="pr_1",
  440. head_ref_oid=_OID1,
  441. commits=[_OID1],
  442. )
  443. check_dependent_pr._process_pr(
  444. self.mock_client,
  445. pr_number=1,
  446. pr_to_commits={1: [_OID1]},
  447. open_pr_numbers={1},
  448. label_id="label_id",
  449. token="test_token",
  450. )
  451. self._assert_status(
  452. _OID1, "success", "This PR has no open dependencies"
  453. )
  454. def test_always_sets_status_check_pending(self) -> None:
  455. self.mock_client.execute.return_value = self._make_pr_response(
  456. pr_id="pr_2",
  457. head_ref_oid=_OID2,
  458. commits=[_OID1, _OID2],
  459. )
  460. check_dependent_pr._process_pr(
  461. self.mock_client,
  462. pr_number=2,
  463. pr_to_commits={1: [_OID1], 2: [_OID1, _OID2]},
  464. open_pr_numbers={1, 2},
  465. label_id="label_dependent",
  466. token="test_token",
  467. )
  468. self._assert_status(
  469. _OID2, "pending", "This PR has open dependencies: #1"
  470. )
  471. def test_query_max_merged_pr_explicit_orderBy_and_first_one(self) -> None:
  472. self.assertIn(
  473. "orderBy: {field: CREATED_AT, direction: DESC}",
  474. check_dependent_pr._QUERY_MAX_MERGED_PR,
  475. )
  476. self.assertIn(
  477. "first: 1",
  478. check_dependent_pr._QUERY_MAX_MERGED_PR,
  479. )
  480. if __name__ == "__main__":
  481. unittest.main()