The Gatekeeper, or a project gating system
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.
 
 
 

2230 lines
91 KiB

  1. # Copyright 2015 GoodData
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. import json
  15. import os
  16. import re
  17. from testtools.matchers import MatchesRegex, Not, StartsWith
  18. import urllib
  19. import socket
  20. import time
  21. import textwrap
  22. from unittest import mock, skip
  23. import git
  24. import github3.exceptions
  25. from tests.fakegithub import FakeGithubEnterpriseClient
  26. from zuul.driver.github.githubconnection import GithubShaCache
  27. import zuul.rpcclient
  28. from tests.base import (AnsibleZuulTestCase, BaseTestCase,
  29. ZuulGithubAppTestCase, ZuulTestCase,
  30. simple_layout, random_sha1)
  31. from tests.base import ZuulWebFixture
  32. class TestGithubDriver(ZuulTestCase):
  33. config_file = 'zuul-github-driver.conf'
  34. @simple_layout('layouts/basic-github.yaml', driver='github')
  35. def test_pull_event(self):
  36. self.executor_server.hold_jobs_in_build = True
  37. body = "This is the\nPR body."
  38. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A',
  39. body=body)
  40. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  41. self.waitUntilSettled()
  42. self.executor_server.hold_jobs_in_build = False
  43. self.executor_server.release()
  44. self.waitUntilSettled()
  45. self.assertEqual('SUCCESS',
  46. self.getJobFromHistory('project-test1').result)
  47. self.assertEqual('SUCCESS',
  48. self.getJobFromHistory('project-test2').result)
  49. job = self.getJobFromHistory('project-test2')
  50. zuulvars = job.parameters['zuul']
  51. self.assertEqual(str(A.number), zuulvars['change'])
  52. self.assertEqual(str(A.head_sha), zuulvars['patchset'])
  53. self.assertEqual('master', zuulvars['branch'])
  54. self.assertEquals('https://github.com/org/project/pull/1',
  55. zuulvars['items'][0]['change_url'])
  56. self.assertEqual(zuulvars["message"], "A\n\n{}".format(body))
  57. self.assertEqual(1, len(A.comments))
  58. self.assertThat(
  59. A.comments[0],
  60. MatchesRegex(r'.*\[project-test1 \]\(.*\).*', re.DOTALL))
  61. self.assertThat(
  62. A.comments[0],
  63. MatchesRegex(r'.*\[project-test2 \]\(.*\).*', re.DOTALL))
  64. self.assertEqual(2, len(self.history))
  65. # test_pull_unmatched_branch_event(self):
  66. self.create_branch('org/project', 'unmatched_branch')
  67. B = self.fake_github.openFakePullRequest(
  68. 'org/project', 'unmatched_branch', 'B')
  69. self.fake_github.emitEvent(B.getPullRequestOpenedEvent())
  70. self.waitUntilSettled()
  71. self.assertEqual(2, len(self.history))
  72. # now emit closed event without merging
  73. self.fake_github.emitEvent(A.getPullRequestClosedEvent())
  74. self.waitUntilSettled()
  75. # nothing should have happened due to the merged requirement
  76. self.assertEqual(2, len(self.history))
  77. # now merge the PR and emit the event again
  78. A.setMerged('merged')
  79. self.fake_github.emitEvent(A.getPullRequestClosedEvent())
  80. self.waitUntilSettled()
  81. # post job must be run
  82. self.assertEqual(3, len(self.history))
  83. @simple_layout('layouts/files-github.yaml', driver='github')
  84. def test_pull_matched_file_event(self):
  85. A = self.fake_github.openFakePullRequest(
  86. 'org/project', 'master', 'A',
  87. files={'random.txt': 'test', 'build-requires': 'test'})
  88. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  89. self.waitUntilSettled()
  90. self.assertEqual(1, len(self.history))
  91. # test_pull_unmatched_file_event
  92. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B',
  93. files={'random.txt': 'test2'})
  94. self.fake_github.emitEvent(B.getPullRequestOpenedEvent())
  95. self.waitUntilSettled()
  96. self.assertEqual(1, len(self.history))
  97. @simple_layout('layouts/files-github.yaml', driver='github')
  98. def test_pull_changed_files_length_mismatch(self):
  99. files = {'{:03d}.txt'.format(n): 'test' for n in range(300)}
  100. # File 301 which is not included in the list of files of the PR,
  101. # since Github only returns max. 300 files in alphabetical order
  102. files["foobar-requires"] = "test"
  103. A = self.fake_github.openFakePullRequest(
  104. 'org/project', 'master', 'A', files=files)
  105. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  106. self.waitUntilSettled()
  107. self.assertEqual(1, len(self.history))
  108. @simple_layout('layouts/files-github.yaml', driver='github')
  109. def test_pull_changed_files_length_mismatch_reenqueue(self):
  110. # Hold jobs so we can trigger a reconfiguration while the item is in
  111. # the pipeline
  112. self.executor_server.hold_jobs_in_build = True
  113. files = {'{:03d}.txt'.format(n): 'test' for n in range(300)}
  114. # File 301 which is not included in the list of files of the PR,
  115. # since Github only returns max. 300 files in alphabetical order
  116. files["foobar-requires"] = "test"
  117. A = self.fake_github.openFakePullRequest(
  118. 'org/project', 'master', 'A', files=files)
  119. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  120. self.waitUntilSettled()
  121. # Comment on the pull request to trigger updateChange
  122. self.fake_github.emitEvent(A.getCommentAddedEvent('casual comment'))
  123. self.waitUntilSettled()
  124. # Trigger reconfig to enforce a reenqueue of the item
  125. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  126. self.waitUntilSettled()
  127. # Now we can release all jobs
  128. self.executor_server.hold_jobs_in_build = True
  129. self.executor_server.release()
  130. self.waitUntilSettled()
  131. # There must be exactly one successful job in the history. If there is
  132. # an aborted job in the history the reenqueue failed.
  133. self.assertHistory([
  134. dict(name='project-test1', result='SUCCESS',
  135. changes="%s,%s" % (A.number, A.head_sha)),
  136. ])
  137. @simple_layout('layouts/basic-github.yaml', driver='github')
  138. def test_pull_github_files_error(self):
  139. A = self.fake_github.openFakePullRequest(
  140. 'org/project', 'master', 'A')
  141. with mock.patch("tests.fakegithub.FakePull.files") as files_mock:
  142. files_mock.side_effect = github3.exceptions.ServerError(
  143. mock.MagicMock())
  144. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  145. self.waitUntilSettled()
  146. self.assertEqual(1, files_mock.call_count)
  147. self.assertEqual(2, len(self.history))
  148. @simple_layout('layouts/basic-github.yaml', driver='github')
  149. def test_comment_event(self):
  150. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  151. self.fake_github.emitEvent(A.getCommentAddedEvent('test me'))
  152. self.waitUntilSettled()
  153. self.assertEqual(2, len(self.history))
  154. # Test an unmatched comment, history should remain the same
  155. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  156. self.fake_github.emitEvent(B.getCommentAddedEvent('casual comment'))
  157. self.waitUntilSettled()
  158. self.assertEqual(2, len(self.history))
  159. # Test an unmatched comment, history should remain the same
  160. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  161. self.fake_github.emitEvent(
  162. C.getIssueCommentAddedEvent('a non-PR issue comment'))
  163. self.waitUntilSettled()
  164. self.assertEqual(2, len(self.history))
  165. @simple_layout('layouts/push-tag-github.yaml', driver='github')
  166. def test_tag_event(self):
  167. self.executor_server.hold_jobs_in_build = True
  168. self.create_branch('org/project', 'tagbranch')
  169. files = {'README.txt': 'test'}
  170. self.addCommitToRepo('org/project', 'test tag',
  171. files, branch='tagbranch', tag='newtag')
  172. path = os.path.join(self.upstream_root, 'org/project')
  173. repo = git.Repo(path)
  174. tag = repo.tags['newtag']
  175. sha = tag.commit.hexsha
  176. del repo
  177. # Notify zuul about the new branch to load the config
  178. self.fake_github.emitEvent(
  179. self.fake_github.getPushEvent(
  180. 'org/project',
  181. ref='refs/heads/%s' % 'tagbranch'))
  182. self.waitUntilSettled()
  183. # Record previous tenant reconfiguration time
  184. before = self.scheds.first.sched.tenant_last_reconfigured.get(
  185. 'tenant-one', 0)
  186. self.fake_github.emitEvent(
  187. self.fake_github.getPushEvent('org/project', 'refs/tags/newtag',
  188. new_rev=sha))
  189. self.waitUntilSettled()
  190. # Make sure the tenant hasn't been reconfigured due to the new tag
  191. after = self.scheds.first.sched.tenant_last_reconfigured.get(
  192. 'tenant-one', 0)
  193. self.assertEqual(before, after)
  194. build_params = self.builds[0].parameters
  195. self.assertEqual('refs/tags/newtag', build_params['zuul']['ref'])
  196. self.assertFalse('oldrev' in build_params['zuul'])
  197. self.assertEqual(sha, build_params['zuul']['newrev'])
  198. self.assertEqual(
  199. 'https://github.com/org/project/releases/tag/newtag',
  200. build_params['zuul']['change_url'])
  201. self.executor_server.hold_jobs_in_build = False
  202. self.executor_server.release()
  203. self.waitUntilSettled()
  204. self.assertEqual('SUCCESS',
  205. self.getJobFromHistory('project-tag').result)
  206. @simple_layout('layouts/push-tag-github.yaml', driver='github')
  207. def test_push_event(self):
  208. self.executor_server.hold_jobs_in_build = True
  209. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  210. old_sha = '0' * 40
  211. new_sha = A.head_sha
  212. A.setMerged("merging A")
  213. pevent = self.fake_github.getPushEvent(project='org/project',
  214. ref='refs/heads/master',
  215. old_rev=old_sha,
  216. new_rev=new_sha)
  217. self.fake_github.emitEvent(pevent)
  218. self.waitUntilSettled()
  219. build_params = self.builds[0].parameters
  220. self.assertEqual('refs/heads/master', build_params['zuul']['ref'])
  221. self.assertFalse('oldrev' in build_params['zuul'])
  222. self.assertEqual(new_sha, build_params['zuul']['newrev'])
  223. self.assertEqual(
  224. 'https://github.com/org/project/commit/%s' % new_sha,
  225. build_params['zuul']['change_url'])
  226. self.executor_server.hold_jobs_in_build = False
  227. self.executor_server.release()
  228. self.waitUntilSettled()
  229. self.assertEqual('SUCCESS',
  230. self.getJobFromHistory('project-post').result)
  231. self.assertEqual(1, len(self.history))
  232. # test unmatched push event
  233. old_sha = random_sha1()
  234. new_sha = random_sha1()
  235. self.fake_github.emitEvent(
  236. self.fake_github.getPushEvent('org/project',
  237. 'refs/heads/unmatched_branch',
  238. old_sha, new_sha))
  239. self.waitUntilSettled()
  240. self.assertEqual(1, len(self.history))
  241. @simple_layout('layouts/labeling-github.yaml', driver='github')
  242. def test_labels(self):
  243. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  244. self.fake_github.emitEvent(A.addLabel('test'))
  245. self.waitUntilSettled()
  246. self.assertEqual(1, len(self.history))
  247. self.assertEqual('project-labels', self.history[0].name)
  248. self.assertEqual(['tests passed'], A.labels)
  249. # test label removed
  250. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  251. B.addLabel('do not test')
  252. self.fake_github.emitEvent(B.removeLabel('do not test'))
  253. self.waitUntilSettled()
  254. self.assertEqual(2, len(self.history))
  255. self.assertEqual('project-labels', self.history[1].name)
  256. self.assertEqual(['tests passed'], B.labels)
  257. # test unmatched label
  258. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  259. self.fake_github.emitEvent(C.addLabel('other label'))
  260. self.waitUntilSettled()
  261. self.assertEqual(2, len(self.history))
  262. self.assertEqual(['other label'], C.labels)
  263. @simple_layout('layouts/reviews-github.yaml', driver='github')
  264. def test_reviews(self):
  265. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  266. self.fake_github.emitEvent(A.getReviewAddedEvent('approve'))
  267. self.waitUntilSettled()
  268. self.assertEqual(1, len(self.history))
  269. self.assertEqual('project-reviews', self.history[0].name)
  270. self.assertEqual(['tests passed'], A.labels)
  271. # test_review_unmatched_event
  272. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  273. self.fake_github.emitEvent(B.getReviewAddedEvent('comment'))
  274. self.waitUntilSettled()
  275. self.assertEqual(1, len(self.history))
  276. # test sending reviews
  277. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  278. self.fake_github.emitEvent(C.getCommentAddedEvent(
  279. "I solemnly swear that I am up to no good"))
  280. self.waitUntilSettled()
  281. self.assertEqual('project-reviews', self.history[0].name)
  282. self.assertEqual(1, len(C.reviews))
  283. self.assertEqual('APPROVE', C.reviews[0].as_dict()['state'])
  284. @simple_layout('layouts/basic-github.yaml', driver='github')
  285. def test_timer_event(self):
  286. self.executor_server.hold_jobs_in_build = True
  287. self.commitConfigUpdate('org/common-config',
  288. 'layouts/timer-github.yaml')
  289. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  290. time.sleep(2)
  291. self.waitUntilSettled()
  292. self.assertEqual(len(self.builds), 1)
  293. self.executor_server.hold_jobs_in_build = False
  294. # Stop queuing timer triggered jobs so that the assertions
  295. # below don't race against more jobs being queued.
  296. self.commitConfigUpdate('org/common-config',
  297. 'layouts/basic-github.yaml')
  298. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  299. self.waitUntilSettled()
  300. # If APScheduler is in mid-event when we remove the job, we
  301. # can end up with one more event firing, so give it an extra
  302. # second to settle.
  303. time.sleep(1)
  304. self.waitUntilSettled()
  305. self.executor_server.release()
  306. self.waitUntilSettled()
  307. self.assertHistory([
  308. dict(name='project-bitrot', result='SUCCESS',
  309. ref='refs/heads/master'),
  310. ], ordered=False)
  311. @simple_layout('layouts/dequeue-github.yaml', driver='github')
  312. def test_dequeue_pull_synchronized(self):
  313. self.executor_server.hold_jobs_in_build = True
  314. A = self.fake_github.openFakePullRequest(
  315. 'org/one-job-project', 'master', 'A')
  316. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  317. self.waitUntilSettled()
  318. # event update stamp has resolution one second, wait so the latter
  319. # one has newer timestamp
  320. time.sleep(1)
  321. # On a push to a PR Github may emit a pull_request_review event with
  322. # the old head so send that right before the synchronized event.
  323. review_event = A.getReviewAddedEvent('dismissed')
  324. A.addCommit()
  325. self.fake_github.emitEvent(review_event)
  326. self.fake_github.emitEvent(A.getPullRequestSynchronizeEvent())
  327. self.waitUntilSettled()
  328. self.executor_server.hold_jobs_in_build = False
  329. self.executor_server.release()
  330. self.waitUntilSettled()
  331. self.assertEqual(2, len(self.history))
  332. self.assertEqual(1, self.countJobResults(self.history, 'ABORTED'))
  333. @simple_layout('layouts/dequeue-github.yaml', driver='github')
  334. def test_dequeue_pull_abandoned(self):
  335. self.executor_server.hold_jobs_in_build = True
  336. A = self.fake_github.openFakePullRequest(
  337. 'org/one-job-project', 'master', 'A')
  338. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  339. self.waitUntilSettled()
  340. self.fake_github.emitEvent(A.getPullRequestClosedEvent())
  341. self.waitUntilSettled()
  342. self.executor_server.hold_jobs_in_build = False
  343. self.executor_server.release()
  344. self.waitUntilSettled()
  345. self.assertEqual(1, len(self.history))
  346. self.assertEqual(1, self.countJobResults(self.history, 'ABORTED'))
  347. @simple_layout('layouts/basic-github.yaml', driver='github')
  348. def test_git_https_url(self):
  349. """Test that git_ssh option gives git url with ssh"""
  350. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  351. _, project = tenant.getProject('org/project')
  352. url = self.fake_github.real_getGitUrl(project)
  353. self.assertEqual('https://github.com/org/project', url)
  354. @simple_layout('layouts/basic-github.yaml', driver='github')
  355. def test_git_ssh_url(self):
  356. """Test that git_ssh option gives git url with ssh"""
  357. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  358. _, project = tenant.getProject('org/project')
  359. url = self.fake_github_ssh.real_getGitUrl(project)
  360. self.assertEqual('ssh://git@github.com/org/project.git', url)
  361. @simple_layout('layouts/basic-github.yaml', driver='github')
  362. def test_git_enterprise_url(self):
  363. """Test that git_url option gives git url with proper host"""
  364. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  365. _, project = tenant.getProject('org/project')
  366. url = self.fake_github_ent.real_getGitUrl(project)
  367. self.assertEqual('ssh://git@github.enterprise.io/org/project.git', url)
  368. @simple_layout('layouts/reporting-github.yaml', driver='github')
  369. def test_reporting(self):
  370. project = 'org/project'
  371. github = self.fake_github.getGithubClient(None)
  372. # pipeline reports pull status both on start and success
  373. self.executor_server.hold_jobs_in_build = True
  374. A = self.fake_github.openFakePullRequest(project, 'master', 'A')
  375. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  376. self.waitUntilSettled()
  377. # We should have a status container for the head sha
  378. self.assertIn(
  379. A.head_sha, github.repo_from_project(project)._commits.keys())
  380. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  381. # We should only have one status for the head sha
  382. self.assertEqual(1, len(statuses))
  383. check_status = statuses[0]
  384. check_url = (
  385. 'http://zuul.example.com/t/tenant-one/status/change/%s,%s' %
  386. (A.number, A.head_sha))
  387. self.assertEqual('tenant-one/check', check_status['context'])
  388. self.assertEqual('check status: pending',
  389. check_status['description'])
  390. self.assertEqual('pending', check_status['state'])
  391. self.assertEqual(check_url, check_status['url'])
  392. self.assertEqual(0, len(A.comments))
  393. self.executor_server.hold_jobs_in_build = False
  394. self.executor_server.release()
  395. self.waitUntilSettled()
  396. # We should only have two statuses for the head sha
  397. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  398. self.assertEqual(2, len(statuses))
  399. check_status = statuses[0]
  400. check_url = 'http://zuul.example.com/t/tenant-one/buildset/'
  401. self.assertEqual('tenant-one/check', check_status['context'])
  402. self.assertEqual('check status: success',
  403. check_status['description'])
  404. self.assertEqual('success', check_status['state'])
  405. self.assertThat(check_status['url'], StartsWith(check_url))
  406. self.assertEqual(1, len(A.comments))
  407. self.assertThat(A.comments[0],
  408. MatchesRegex(r'.*Build succeeded.*', re.DOTALL))
  409. # pipeline does not report any status but does comment
  410. self.executor_server.hold_jobs_in_build = True
  411. self.fake_github.emitEvent(
  412. A.getCommentAddedEvent('reporting check'))
  413. self.waitUntilSettled()
  414. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  415. self.assertEqual(2, len(statuses))
  416. # comments increased by one for the start message
  417. self.assertEqual(2, len(A.comments))
  418. self.assertThat(A.comments[1],
  419. MatchesRegex(r'.*Starting reporting jobs.*',
  420. re.DOTALL))
  421. self.executor_server.hold_jobs_in_build = False
  422. self.executor_server.release()
  423. self.waitUntilSettled()
  424. # pipeline reports success status
  425. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  426. self.assertEqual(3, len(statuses))
  427. report_status = statuses[0]
  428. self.assertEqual('tenant-one/reporting', report_status['context'])
  429. self.assertEqual('reporting status: success',
  430. report_status['description'])
  431. self.assertEqual('success', report_status['state'])
  432. self.assertEqual(2, len(A.comments))
  433. base = 'http://zuul.example.com/t/tenant-one/buildset/'
  434. # Deconstructing the URL because we don't save the BuildSet UUID
  435. # anywhere to do a direct comparison and doing regexp matches on a full
  436. # URL is painful.
  437. # The first part of the URL matches the easy base string
  438. self.assertThat(report_status['url'], StartsWith(base))
  439. # The rest of the URL is a UUID
  440. self.assertThat(report_status['url'][len(base):],
  441. MatchesRegex(r'^[a-fA-F0-9]{32}$'))
  442. @simple_layout('layouts/reporting-github.yaml', driver='github')
  443. def test_truncated_status_description(self):
  444. project = 'org/project'
  445. # pipeline reports pull status both on start and success
  446. self.executor_server.hold_jobs_in_build = True
  447. A = self.fake_github.openFakePullRequest(project, 'master', 'A')
  448. self.fake_github.emitEvent(
  449. A.getCommentAddedEvent('long pipeline'))
  450. self.waitUntilSettled()
  451. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  452. self.assertEqual(1, len(statuses))
  453. check_status = statuses[0]
  454. # Status is truncated due to long pipeline name
  455. self.assertEqual('status: pending',
  456. check_status['description'])
  457. self.executor_server.hold_jobs_in_build = False
  458. self.executor_server.release()
  459. self.waitUntilSettled()
  460. # We should only have two statuses for the head sha
  461. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  462. self.assertEqual(2, len(statuses))
  463. check_status = statuses[0]
  464. # Status is truncated due to long pipeline name
  465. self.assertEqual('status: success',
  466. check_status['description'])
  467. @simple_layout('layouts/reporting-github.yaml', driver='github')
  468. def test_push_reporting(self):
  469. project = 'org/project2'
  470. # pipeline reports pull status both on start and success
  471. self.executor_server.hold_jobs_in_build = True
  472. A = self.fake_github.openFakePullRequest(project, 'master', 'A')
  473. old_sha = '0' * 40
  474. new_sha = A.head_sha
  475. A.setMerged("merging A")
  476. pevent = self.fake_github.getPushEvent(project=project,
  477. ref='refs/heads/master',
  478. old_rev=old_sha,
  479. new_rev=new_sha)
  480. self.fake_github.emitEvent(pevent)
  481. self.waitUntilSettled()
  482. # there should only be one report, a status
  483. self.assertEqual(1, len(self.fake_github.github_data.reports))
  484. # Verify the user/context/state of the status
  485. status = ('zuul', 'tenant-one/push-reporting', 'pending')
  486. self.assertEqual(status, self.fake_github.github_data.reports[0][-1])
  487. # free the executor, allow the build to finish
  488. self.executor_server.hold_jobs_in_build = False
  489. self.executor_server.release()
  490. self.waitUntilSettled()
  491. # Now there should be a second report, the success of the build
  492. self.assertEqual(2, len(self.fake_github.github_data.reports))
  493. # Verify the user/context/state of the status
  494. status = ('zuul', 'tenant-one/push-reporting', 'success')
  495. self.assertEqual(status, self.fake_github.github_data.reports[-1][-1])
  496. # now make a PR which should also comment
  497. self.executor_server.hold_jobs_in_build = True
  498. A = self.fake_github.openFakePullRequest(project, 'master', 'A')
  499. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  500. self.waitUntilSettled()
  501. # Now there should be a four reports, a new comment
  502. # and status
  503. self.assertEqual(4, len(self.fake_github.github_data.reports))
  504. self.executor_server.release()
  505. self.waitUntilSettled()
  506. @simple_layout("layouts/reporting-github.yaml", driver="github")
  507. def test_reporting_checks_api_unauthorized(self):
  508. # Using the checks API only works with app authentication. As all tests
  509. # within the TestGithubDriver class are executed without app
  510. # authentication, the checks API won't work here.
  511. project = "org/project3"
  512. github = self.fake_github.getGithubClient(None)
  513. # The pipeline reports pull request status both on start and success.
  514. # As we are not authenticated as app, this won't create or update any
  515. # check runs, but should post two comments (start, success) informing
  516. # the user about the missing authentication.
  517. A = self.fake_github.openFakePullRequest(project, "master", "A")
  518. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  519. self.waitUntilSettled()
  520. self.assertIn(
  521. A.head_sha, github.repo_from_project(project)._commits.keys()
  522. )
  523. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  524. self.assertEqual(0, len(check_runs))
  525. expected_warning = (
  526. "Unable to create or update check tenant-one/checks-api-reporting."
  527. " Must be authenticated as app integration."
  528. )
  529. self.assertEqual(2, len(A.comments))
  530. self.assertIn(expected_warning, A.comments[0])
  531. self.assertIn(expected_warning, A.comments[1])
  532. @simple_layout('layouts/merging-github.yaml', driver='github')
  533. def test_report_pull_merge(self):
  534. # pipeline merges the pull request on success
  535. A = self.fake_github.openFakePullRequest('org/project', 'master',
  536. 'PR title',
  537. body='I shouldnt be seen',
  538. body_text='PR body')
  539. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  540. self.waitUntilSettled()
  541. self.assertTrue(A.is_merged)
  542. self.assertThat(A.merge_message,
  543. MatchesRegex(r'.*PR title\n\nPR body.*', re.DOTALL))
  544. self.assertThat(A.merge_message,
  545. Not(MatchesRegex(
  546. r'.*I shouldnt be seen.*',
  547. re.DOTALL)))
  548. self.assertEqual(len(A.comments), 0)
  549. # pipeline merges the pull request on success after failure
  550. self.fake_github.merge_failure = True
  551. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  552. self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
  553. self.waitUntilSettled()
  554. self.assertFalse(B.is_merged)
  555. self.assertEqual(len(B.comments), 1)
  556. self.assertEqual(B.comments[0],
  557. 'Pull request merge failed: Unknown merge failure')
  558. self.fake_github.merge_failure = False
  559. # pipeline merges the pull request on second run of merge
  560. # first merge failed on 405 Method Not Allowed error
  561. self.fake_github.merge_not_allowed_count = 1
  562. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  563. self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
  564. self.waitUntilSettled()
  565. self.assertTrue(C.is_merged)
  566. # pipeline does not merge the pull request
  567. # merge failed on 405 Method Not Allowed error - twice
  568. self.fake_github.merge_not_allowed_count = 2
  569. D = self.fake_github.openFakePullRequest('org/project', 'master', 'D')
  570. self.fake_github.emitEvent(D.getCommentAddedEvent('merge me'))
  571. self.waitUntilSettled()
  572. self.assertFalse(D.is_merged)
  573. self.assertEqual(len(D.comments), 1)
  574. # Validate that the merge failure comment contains the message github
  575. # returned
  576. self.assertEqual(D.comments[0],
  577. 'Pull request merge failed: 403 Merge not allowed')
  578. @simple_layout('layouts/merging-github.yaml', driver='github')
  579. def test_report_pull_merge_message_reviewed_by(self):
  580. # pipeline merges the pull request on success
  581. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  582. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  583. self.waitUntilSettled()
  584. self.assertTrue(A.is_merged)
  585. # assert that no 'Reviewed-By' is in merge commit message
  586. self.assertThat(A.merge_message,
  587. Not(MatchesRegex(r'.*Reviewed-By.*', re.DOTALL)))
  588. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  589. B.addReview('derp', 'APPROVED')
  590. self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
  591. self.waitUntilSettled()
  592. self.assertTrue(B.is_merged)
  593. # assert that single 'Reviewed-By' is in merge commit message
  594. self.assertThat(B.merge_message,
  595. MatchesRegex(
  596. r'.*Reviewed-by: derp <derp@example.com>.*',
  597. re.DOTALL))
  598. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  599. C.addReview('derp', 'APPROVED')
  600. C.addReview('herp', 'COMMENTED')
  601. self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
  602. self.waitUntilSettled()
  603. self.assertTrue(C.is_merged)
  604. # assert that multiple 'Reviewed-By's are in merge commit message
  605. self.assertThat(C.merge_message,
  606. MatchesRegex(
  607. r'.*Reviewed-by: derp <derp@example.com>.*',
  608. re.DOTALL))
  609. self.assertThat(C.merge_message,
  610. MatchesRegex(
  611. r'.*Reviewed-by: herp <herp@example.com>.*',
  612. re.DOTALL))
  613. @simple_layout('layouts/dependent-github.yaml', driver='github')
  614. def test_draft_pr(self):
  615. # pipeline merges the pull request on success
  616. A = self.fake_github.openFakePullRequest('org/project', 'master',
  617. 'PR title', draft=True)
  618. self.fake_github.emitEvent(A.addLabel('merge'))
  619. self.waitUntilSettled()
  620. # A draft pull request must not enter the gate
  621. self.assertFalse(A.is_merged)
  622. self.assertHistory([])
  623. @simple_layout('layouts/reporting-multiple-github.yaml', driver='github')
  624. def test_reporting_multiple_github(self):
  625. project = 'org/project1'
  626. github = self.fake_github.getGithubClient(None)
  627. # pipeline reports pull status both on start and success
  628. self.executor_server.hold_jobs_in_build = True
  629. A = self.fake_github.openFakePullRequest(project, 'master', 'A')
  630. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  631. # open one on B as well, which should not effect A reporting
  632. B = self.fake_github.openFakePullRequest('org/project2', 'master',
  633. 'B')
  634. self.fake_github.emitEvent(B.getPullRequestOpenedEvent())
  635. self.waitUntilSettled()
  636. # We should have a status container for the head sha
  637. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  638. self.assertIn(
  639. A.head_sha, github.repo_from_project(project)._commits.keys())
  640. # We should only have one status for the head sha
  641. self.assertEqual(1, len(statuses))
  642. check_status = statuses[0]
  643. check_url = (
  644. 'http://zuul.example.com/t/tenant-one/status/change/%s,%s' %
  645. (A.number, A.head_sha))
  646. self.assertEqual('tenant-one/check', check_status['context'])
  647. self.assertEqual('check status: pending', check_status['description'])
  648. self.assertEqual('pending', check_status['state'])
  649. self.assertEqual(check_url, check_status['url'])
  650. self.assertEqual(0, len(A.comments))
  651. self.executor_server.hold_jobs_in_build = False
  652. self.executor_server.release()
  653. self.waitUntilSettled()
  654. # We should only have two statuses for the head sha
  655. statuses = self.fake_github.getCommitStatuses(project, A.head_sha)
  656. self.assertEqual(2, len(statuses))
  657. check_status = statuses[0]
  658. check_url = 'http://zuul.example.com/t/tenant-one/buildset/'
  659. self.assertEqual('tenant-one/check', check_status['context'])
  660. self.assertEqual('success', check_status['state'])
  661. self.assertEqual('check status: success', check_status['description'])
  662. self.assertThat(check_status['url'], StartsWith(check_url))
  663. self.assertEqual(1, len(A.comments))
  664. self.assertThat(A.comments[0],
  665. MatchesRegex(r'.*Build succeeded.*', re.DOTALL))
  666. @simple_layout('layouts/dependent-github.yaml', driver='github')
  667. def test_parallel_changes(self):
  668. "Test that changes are tested in parallel and merged in series"
  669. self.executor_server.hold_jobs_in_build = True
  670. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  671. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  672. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  673. self.fake_github.emitEvent(A.addLabel('merge'))
  674. self.fake_github.emitEvent(B.addLabel('merge'))
  675. self.fake_github.emitEvent(C.addLabel('merge'))
  676. self.waitUntilSettled()
  677. self.assertEqual(len(self.builds), 1)
  678. self.assertEqual(self.builds[0].name, 'project-merge')
  679. self.assertTrue(self.builds[0].hasChanges(A))
  680. self.executor_server.release('.*-merge')
  681. self.waitUntilSettled()
  682. self.assertEqual(len(self.builds), 3)
  683. self.assertEqual(self.builds[0].name, 'project-test1')
  684. self.assertTrue(self.builds[0].hasChanges(A))
  685. self.assertEqual(self.builds[1].name, 'project-test2')
  686. self.assertTrue(self.builds[1].hasChanges(A))
  687. self.assertEqual(self.builds[2].name, 'project-merge')
  688. self.assertTrue(self.builds[2].hasChanges(A, B))
  689. self.executor_server.release('.*-merge')
  690. self.waitUntilSettled()
  691. self.assertEqual(len(self.builds), 5)
  692. self.assertEqual(self.builds[0].name, 'project-test1')
  693. self.assertTrue(self.builds[0].hasChanges(A))
  694. self.assertEqual(self.builds[1].name, 'project-test2')
  695. self.assertTrue(self.builds[1].hasChanges(A))
  696. self.assertEqual(self.builds[2].name, 'project-test1')
  697. self.assertTrue(self.builds[2].hasChanges(A))
  698. self.assertEqual(self.builds[3].name, 'project-test2')
  699. self.assertTrue(self.builds[3].hasChanges(A, B))
  700. self.assertEqual(self.builds[4].name, 'project-merge')
  701. self.assertTrue(self.builds[4].hasChanges(A, B, C))
  702. self.executor_server.release('.*-merge')
  703. self.waitUntilSettled()
  704. self.assertEqual(len(self.builds), 6)
  705. self.assertEqual(self.builds[0].name, 'project-test1')
  706. self.assertTrue(self.builds[0].hasChanges(A))
  707. self.assertEqual(self.builds[1].name, 'project-test2')
  708. self.assertTrue(self.builds[1].hasChanges(A))
  709. self.assertEqual(self.builds[2].name, 'project-test1')
  710. self.assertTrue(self.builds[2].hasChanges(A, B))
  711. self.assertEqual(self.builds[3].name, 'project-test2')
  712. self.assertTrue(self.builds[3].hasChanges(A, B))
  713. self.assertEqual(self.builds[4].name, 'project-test1')
  714. self.assertTrue(self.builds[4].hasChanges(A, B, C))
  715. self.assertEqual(self.builds[5].name, 'project-test2')
  716. self.assertTrue(self.builds[5].hasChanges(A, B, C))
  717. all_builds = self.builds[:]
  718. self.release(all_builds[2])
  719. self.release(all_builds[3])
  720. self.waitUntilSettled()
  721. self.assertFalse(A.is_merged)
  722. self.assertFalse(B.is_merged)
  723. self.assertFalse(C.is_merged)
  724. self.release(all_builds[0])
  725. self.release(all_builds[1])
  726. self.waitUntilSettled()
  727. self.assertTrue(A.is_merged)
  728. self.assertTrue(B.is_merged)
  729. self.assertFalse(C.is_merged)
  730. self.executor_server.hold_jobs_in_build = False
  731. self.executor_server.release()
  732. self.waitUntilSettled()
  733. self.assertEqual(len(self.builds), 0)
  734. self.assertEqual(len(self.history), 9)
  735. self.assertTrue(C.is_merged)
  736. self.assertNotIn('merge', A.labels)
  737. self.assertNotIn('merge', B.labels)
  738. self.assertNotIn('merge', C.labels)
  739. @simple_layout('layouts/dependent-github.yaml', driver='github')
  740. def test_failed_changes(self):
  741. "Test that a change behind a failed change is retested"
  742. self.executor_server.hold_jobs_in_build = True
  743. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  744. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  745. self.executor_server.failJob('project-test1', A)
  746. self.fake_github.emitEvent(A.addLabel('merge'))
  747. self.fake_github.emitEvent(B.addLabel('merge'))
  748. self.waitUntilSettled()
  749. self.executor_server.release('.*-merge')
  750. self.waitUntilSettled()
  751. self.executor_server.hold_jobs_in_build = False
  752. self.executor_server.release()
  753. self.waitUntilSettled()
  754. # It's certain that the merge job for change 2 will run, but
  755. # the test1 and test2 jobs may or may not run.
  756. self.assertTrue(len(self.history) > 6)
  757. self.assertFalse(A.is_merged)
  758. self.assertTrue(B.is_merged)
  759. self.assertNotIn('merge', A.labels)
  760. self.assertNotIn('merge', B.labels)
  761. @simple_layout('layouts/dependent-github.yaml', driver='github')
  762. def test_failed_change_at_head(self):
  763. "Test that if a change at the head fails, jobs behind it are canceled"
  764. self.executor_server.hold_jobs_in_build = True
  765. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  766. B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
  767. C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
  768. self.executor_server.failJob('project-test1', A)
  769. self.fake_github.emitEvent(A.addLabel('merge'))
  770. self.fake_github.emitEvent(B.addLabel('merge'))
  771. self.fake_github.emitEvent(C.addLabel('merge'))
  772. self.waitUntilSettled()
  773. self.assertEqual(len(self.builds), 1)
  774. self.assertEqual(self.builds[0].name, 'project-merge')
  775. self.assertTrue(self.builds[0].hasChanges(A))
  776. self.executor_server.release('.*-merge')
  777. self.waitUntilSettled()
  778. self.executor_server.release('.*-merge')
  779. self.waitUntilSettled()
  780. self.executor_server.release('.*-merge')
  781. self.waitUntilSettled()
  782. self.assertEqual(len(self.builds), 6)
  783. self.assertEqual(self.builds[0].name, 'project-test1')
  784. self.assertEqual(self.builds[1].name, 'project-test2')
  785. self.assertEqual(self.builds[2].name, 'project-test1')
  786. self.assertEqual(self.builds[3].name, 'project-test2')
  787. self.assertEqual(self.builds[4].name, 'project-test1')
  788. self.assertEqual(self.builds[5].name, 'project-test2')
  789. self.release(self.builds[0])
  790. self.waitUntilSettled()
  791. # project-test2, project-merge for B
  792. self.assertEqual(len(self.builds), 2)
  793. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 4)
  794. self.executor_server.hold_jobs_in_build = False
  795. self.executor_server.release()
  796. self.waitUntilSettled()
  797. self.assertEqual(len(self.builds), 0)
  798. self.assertEqual(len(self.history), 15)
  799. self.assertFalse(A.is_merged)
  800. self.assertTrue(B.is_merged)
  801. self.assertTrue(C.is_merged)
  802. self.assertNotIn('merge', A.labels)
  803. self.assertNotIn('merge', B.labels)
  804. self.assertNotIn('merge', C.labels)
  805. def _test_push_event_reconfigure(self, project, branch,
  806. expect_reconfigure=False,
  807. old_sha=None, new_sha=None,
  808. modified_files=None,
  809. removed_files=None,
  810. expected_cat_jobs=None):
  811. pevent = self.fake_github.getPushEvent(
  812. project=project,
  813. ref='refs/heads/%s' % branch,
  814. old_rev=old_sha,
  815. new_rev=new_sha,
  816. modified_files=modified_files,
  817. removed_files=removed_files)
  818. # record previous tenant reconfiguration time, which may not be set
  819. old = self.scheds.first.sched.tenant_last_reconfigured\
  820. .get('tenant-one', 0)
  821. self.waitUntilSettled()
  822. if expected_cat_jobs is not None:
  823. # clear the gearman jobs history so we can count the cat jobs
  824. # issued during reconfiguration
  825. self.gearman_server.jobs_history.clear()
  826. self.fake_github.emitEvent(pevent)
  827. self.waitUntilSettled()
  828. new = self.scheds.first.sched.tenant_last_reconfigured\
  829. .get('tenant-one', 0)
  830. if expect_reconfigure:
  831. # New timestamp should be greater than the old timestamp
  832. self.assertLess(old, new)
  833. else:
  834. # Timestamps should be equal as no reconfiguration shall happen
  835. self.assertEqual(old, new)
  836. if expected_cat_jobs is not None:
  837. # Check the expected number of cat jobs here as the (empty) config
  838. # of org/project should be cached.
  839. cat_jobs = set([job for job in self.gearman_server.jobs_history
  840. if job.name == b'merger:cat'])
  841. self.assertEqual(expected_cat_jobs, len(cat_jobs), cat_jobs)
  842. @simple_layout('layouts/basic-github.yaml', driver='github')
  843. def test_push_event_reconfigure(self):
  844. self._test_push_event_reconfigure('org/common-config', 'master',
  845. modified_files=['zuul.yaml'],
  846. old_sha='0' * 40,
  847. expect_reconfigure=True,
  848. expected_cat_jobs=1)
  849. @simple_layout('layouts/basic-github.yaml', driver='github')
  850. def test_push_event_reconfigure_complex_branch(self):
  851. branch = 'feature/somefeature'
  852. project = 'org/common-config'
  853. # prepare an existing branch
  854. self.create_branch(project, branch)
  855. github = self.fake_github.getGithubClient()
  856. repo = github.repo_from_project(project)
  857. repo._create_branch(branch)
  858. repo._set_branch_protection(branch, False)
  859. self.fake_github.emitEvent(
  860. self.fake_github.getPushEvent(
  861. project,
  862. ref='refs/heads/%s' % branch))
  863. self.waitUntilSettled()
  864. A = self.fake_github.openFakePullRequest(project, branch, 'A')
  865. old_sha = A.head_sha
  866. A.setMerged("merging A")
  867. new_sha = random_sha1()
  868. self._test_push_event_reconfigure(project, branch,
  869. expect_reconfigure=True,
  870. old_sha=old_sha,
  871. new_sha=new_sha,
  872. modified_files=['zuul.yaml'],
  873. expected_cat_jobs=1)
  874. # there are no exclude-unprotected-branches in the test class, so a
  875. # reconfigure shall occur
  876. repo._delete_branch(branch)
  877. self._test_push_event_reconfigure(project, branch,
  878. expect_reconfigure=True,
  879. old_sha=new_sha,
  880. new_sha='0' * 40,
  881. removed_files=['zuul.yaml'])
  882. # TODO(jlk): Make this a more generic test for unknown project
  883. @skip("Skipped for rewrite of webhook handler")
  884. @simple_layout('layouts/basic-github.yaml', driver='github')
  885. def test_ping_event(self):
  886. # Test valid ping
  887. pevent = {'repository': {'full_name': 'org/project'}}
  888. resp = self.fake_github.emitEvent(('ping', pevent))
  889. self.assertEqual(resp.status_code, 200, "Ping event didn't succeed")
  890. # Test invalid ping
  891. pevent = {'repository': {'full_name': 'unknown-project'}}
  892. self.assertRaises(
  893. urllib.error.HTTPError,
  894. self.fake_github.emitEvent,
  895. ('ping', pevent),
  896. )
  897. @simple_layout('layouts/gate-github.yaml', driver='github')
  898. def test_status_checks(self):
  899. github = self.fake_github.getGithubClient()
  900. repo = github.repo_from_project('org/project')
  901. repo._set_branch_protection(
  902. 'master', contexts=['tenant-one/check', 'tenant-one/gate'])
  903. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  904. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  905. self.waitUntilSettled()
  906. # since the required status 'tenant-one/check' is not fulfilled no
  907. # job is expected
  908. self.assertEqual(0, len(self.history))
  909. # now set a failing status 'tenant-one/check'
  910. repo = github.repo_from_project('org/project')
  911. repo.create_status(A.head_sha, 'failed', 'example.com', 'description',
  912. 'tenant-one/check')
  913. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  914. self.waitUntilSettled()
  915. self.assertEqual(0, len(self.history))
  916. # now set a successful status followed by a failing status to check
  917. # that the later failed status wins
  918. repo.create_status(A.head_sha, 'success', 'example.com', 'description',
  919. 'tenant-one/check')
  920. repo.create_status(A.head_sha, 'failed', 'example.com', 'description',
  921. 'tenant-one/check')
  922. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  923. self.waitUntilSettled()
  924. self.assertEqual(0, len(self.history))
  925. # now set the required status 'tenant-one/check'
  926. repo.create_status(A.head_sha, 'success', 'example.com', 'description',
  927. 'tenant-one/check')
  928. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  929. self.waitUntilSettled()
  930. # the change should have entered the gate
  931. self.assertEqual(2, len(self.history))
  932. # This test case verifies that no reconfiguration happens if a branch was
  933. # deleted that didn't contain configuration.
  934. @simple_layout('layouts/basic-github.yaml', driver='github')
  935. def test_no_reconfigure_on_non_config_branch_delete(self):
  936. branch = 'feature/somefeature'
  937. project = 'org/common-config'
  938. # prepare an existing branch
  939. self.create_branch(project, branch)
  940. github = self.fake_github.getGithubClient()
  941. repo = github.repo_from_project(project)
  942. repo._create_branch(branch)
  943. repo._set_branch_protection(branch, False)
  944. A = self.fake_github.openFakePullRequest(project, branch, 'A')
  945. old_sha = A.head_sha
  946. A.setMerged("merging A")
  947. new_sha = random_sha1()
  948. self._test_push_event_reconfigure(project, branch,
  949. expect_reconfigure=False,
  950. old_sha=old_sha,
  951. new_sha=new_sha,
  952. modified_files=['README.md'])
  953. # Check if deleting that branch is ignored as well
  954. repo._delete_branch(branch)
  955. self._test_push_event_reconfigure(project, branch,
  956. expect_reconfigure=False,
  957. old_sha=new_sha,
  958. new_sha='0' * 40,
  959. modified_files=['README.md'])
  960. @simple_layout('layouts/basic-github.yaml', driver='github')
  961. def test_client_dequeue_change_github(self):
  962. "Test that the RPC client can dequeue a github pull request"
  963. client = zuul.rpcclient.RPCClient('127.0.0.1',
  964. self.gearman_server.port)
  965. self.addCleanup(client.shutdown)
  966. self.executor_server.hold_jobs_in_build = True
  967. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  968. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  969. self.waitUntilSettled()
  970. client.dequeue(
  971. tenant='tenant-one',
  972. pipeline='check',
  973. project='org/project',
  974. change='{},{}'.format(A.number, A.head_sha),
  975. ref=None)
  976. self.waitUntilSettled()
  977. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  978. check_pipeline = tenant.layout.pipelines['check']
  979. self.assertEqual(check_pipeline.getAllItems(), [])
  980. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 2)
  981. self.executor_server.hold_jobs_in_build = False
  982. self.executor_server.release()
  983. self.waitUntilSettled()
  984. @simple_layout('layouts/basic-github.yaml', driver='github')
  985. def test_client_enqueue_change_github(self):
  986. "Test that the RPC client can enqueue a pull request"
  987. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  988. client = zuul.rpcclient.RPCClient('127.0.0.1',
  989. self.gearman_server.port)
  990. self.addCleanup(client.shutdown)
  991. r = client.enqueue(tenant='tenant-one',
  992. pipeline='check',
  993. project='org/project',
  994. trigger='github',
  995. change='{},{}'.format(A.number, A.head_sha))
  996. self.waitUntilSettled()
  997. self.assertEqual(self.getJobFromHistory('project-test1').result,
  998. 'SUCCESS')
  999. self.assertEqual(self.getJobFromHistory('project-test2').result,
  1000. 'SUCCESS')
  1001. self.assertEqual(r, True)
  1002. # check that change_url is correct
  1003. job1_params = self.getJobFromHistory('project-test1').parameters
  1004. job2_params = self.getJobFromHistory('project-test2').parameters
  1005. self.assertEquals('https://github.com/org/project/pull/1',
  1006. job1_params['zuul']['items'][0]['change_url'])
  1007. self.assertEquals('https://github.com/org/project/pull/1',
  1008. job2_params['zuul']['items'][0]['change_url'])
  1009. @simple_layout('layouts/basic-github.yaml', driver='github')
  1010. def test_pull_commit_race(self):
  1011. """Test graceful handling of delayed availability of commits"""
  1012. github = self.fake_github.getGithubClient('org/project')
  1013. repo = github.repo_from_project('org/project')
  1014. repo.fail_not_found = 1
  1015. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  1016. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1017. self.waitUntilSettled()
  1018. self.assertEqual('SUCCESS',
  1019. self.getJobFromHistory('project-test1').result)
  1020. self.assertEqual('SUCCESS',
  1021. self.getJobFromHistory('project-test2').result)
  1022. job = self.getJobFromHistory('project-test2')
  1023. zuulvars = job.parameters['zuul']
  1024. self.assertEqual(str(A.number), zuulvars['change'])
  1025. self.assertEqual(str(A.head_sha), zuulvars['patchset'])
  1026. self.assertEqual('master', zuulvars['branch'])
  1027. self.assertEqual(1, len(A.comments))
  1028. self.assertThat(
  1029. A.comments[0],
  1030. MatchesRegex(r'.*\[project-test1 \]\(.*\).*', re.DOTALL))
  1031. self.assertThat(
  1032. A.comments[0],
  1033. MatchesRegex(r'.*\[project-test2 \]\(.*\).*', re.DOTALL))
  1034. self.assertEqual(2, len(self.history))
  1035. @simple_layout('layouts/gate-github-cherry-pick.yaml', driver='github')
  1036. def test_merge_method_cherry_pick(self):
  1037. """
  1038. Tests that the merge mode gets forwarded to the reporter and the
  1039. merge fails because cherry-pick is not supported by github.
  1040. """
  1041. github = self.fake_github.getGithubClient()
  1042. repo = github.repo_from_project('org/project')
  1043. repo._set_branch_protection(
  1044. 'master', contexts=['tenant-one/check', 'tenant-one/gate'])
  1045. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  1046. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1047. self.waitUntilSettled()
  1048. repo = github.repo_from_project('org/project')
  1049. repo.create_status(A.head_sha, 'success', 'example.com', 'description',
  1050. 'tenant-one/check')
  1051. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1052. self.waitUntilSettled()
  1053. # the change should have entered the gate
  1054. self.assertEqual(2, len(self.history))
  1055. # Merge should have failed because cherry-pick is not supported
  1056. self.assertEqual(2, len(A.comments))
  1057. self.assertFalse(A.is_merged)
  1058. self.assertEquals(A.comments[1],
  1059. 'Merge mode cherry-pick not supported by Github')
  1060. @simple_layout('layouts/gate-github-squash-merge.yaml', driver='github')
  1061. def test_merge_method_squash_merge(self):
  1062. """
  1063. Tests that the merge mode gets forwarded to the reporter and the
  1064. merge fails because cherry-pick is not supported by github.
  1065. """
  1066. github = self.fake_github.getGithubClient()
  1067. repo = github.repo_from_project('org/project')
  1068. repo._set_branch_protection(
  1069. 'master', contexts=['tenant-one/check', 'tenant-one/gate'])
  1070. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  1071. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1072. self.waitUntilSettled()
  1073. repo = github.repo_from_project('org/project')
  1074. repo.create_status(A.head_sha, 'success', 'example.com', 'description',
  1075. 'tenant-one/check')
  1076. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1077. self.waitUntilSettled()
  1078. # the change should have entered the gate
  1079. self.assertEqual(2, len(self.history))
  1080. # now check if the merge was done via rebase
  1081. merges = [report for report in self.fake_github.github_data.reports
  1082. if report[2] == 'merge']
  1083. assert(len(merges) == 1 and merges[0][3] == 'squash')
  1084. class TestGithubUnprotectedBranches(ZuulTestCase):
  1085. config_file = 'zuul-github-driver.conf'
  1086. tenant_config_file = 'config/unprotected-branches/main.yaml'
  1087. def test_unprotected_branches(self):
  1088. tenant = self.scheds.first.sched.abide.tenants\
  1089. .get('tenant-one')
  1090. project1 = tenant.untrusted_projects[0]
  1091. project2 = tenant.untrusted_projects[1]
  1092. tpc1 = tenant.project_configs[project1.canonical_name]
  1093. tpc2 = tenant.project_configs[project2.canonical_name]
  1094. # project1 should have parsed master
  1095. self.assertIn('master', tpc1.parsed_branch_config.keys())
  1096. # project2 should have no parsed branch
  1097. self.assertEqual(0, len(tpc2.parsed_branch_config.keys()))
  1098. # now enable branch protection and trigger reload
  1099. github = self.fake_github.getGithubClient()
  1100. repo = github.repo_from_project('org/project2')
  1101. repo._set_branch_protection('master', True)
  1102. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1103. self.waitUntilSettled()
  1104. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  1105. tpc1 = tenant.project_configs[project1.canonical_name]
  1106. tpc2 = tenant.project_configs[project2.canonical_name]
  1107. # project1 and project2 should have parsed master now
  1108. self.assertIn('master', tpc1.parsed_branch_config.keys())
  1109. self.assertIn('master', tpc2.parsed_branch_config.keys())
  1110. def test_filtered_branches_in_build(self):
  1111. """
  1112. Tests unprotected branches are filtered in builds if excluded
  1113. """
  1114. self.executor_server.keep_jobdir = True
  1115. # Enable branch protection on org/project2@master
  1116. github = self.fake_github.getGithubClient()
  1117. repo = github.repo_from_project('org/project2')
  1118. self.create_branch('org/project2', 'feat-x')
  1119. repo._set_branch_protection('master', True)
  1120. # Enable branch protection on org/project3@stable. We'll use a PR on
  1121. # this branch as a depends-on to validate that the stable branch
  1122. # which is not protected in org/project3 is not filtered out.
  1123. repo = github.repo_from_project('org/project3')
  1124. self.create_branch('org/project3', 'stable')
  1125. repo._set_branch_protection('stable', True)
  1126. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1127. self.waitUntilSettled()
  1128. A = self.fake_github.openFakePullRequest('org/project3', 'stable', 'A')
  1129. msg = "Depends-On: https://github.com/org/project1/pull/%s" % A.number
  1130. B = self.fake_github.openFakePullRequest('org/project2', 'master', 'B',
  1131. body=msg)
  1132. self.fake_github.emitEvent(B.getPullRequestOpenedEvent())
  1133. self.waitUntilSettled()
  1134. build = self.history[0]
  1135. path = os.path.join(
  1136. build.jobdir.src_root, 'github.com', 'org/project2')
  1137. build_repo = git.Repo(path)
  1138. branches = [x.name for x in build_repo.branches]
  1139. self.assertNotIn('feat-x', branches)
  1140. self.assertHistory([
  1141. dict(name='used-job', result='SUCCESS',
  1142. changes="%s,%s %s,%s" % (A.number, A.head_sha,
  1143. B.number, B.head_sha)),
  1144. ])
  1145. def test_unfiltered_branches_in_build(self):
  1146. """
  1147. Tests unprotected branches are not filtered in builds if not excluded
  1148. """
  1149. self.executor_server.keep_jobdir = True
  1150. # Enable branch protection on org/project1@master
  1151. github = self.fake_github.getGithubClient()
  1152. repo = github.repo_from_project('org/project1')
  1153. self.create_branch('org/project1', 'feat-x')
  1154. repo._set_branch_protection('master', True)
  1155. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1156. self.waitUntilSettled()
  1157. A = self.fake_github.openFakePullRequest('org/project1', 'master', 'A')
  1158. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1159. self.waitUntilSettled()
  1160. build = self.history[0]
  1161. path = os.path.join(
  1162. build.jobdir.src_root, 'github.com', 'org/project1')
  1163. build_repo = git.Repo(path)
  1164. branches = [x.name for x in build_repo.branches]
  1165. self.assertIn('feat-x', branches)
  1166. self.assertHistory([
  1167. dict(name='project-test', result='SUCCESS',
  1168. changes="%s,%s" % (A.number, A.head_sha)),
  1169. ])
  1170. def test_unprotected_push(self):
  1171. """Test that unprotected pushes don't cause tenant reconfigurations"""
  1172. # Prepare repo with an initial commit
  1173. A = self.fake_github.openFakePullRequest('org/project2', 'master', 'A')
  1174. A.setMerged("merging A")
  1175. # Do a push on top of A
  1176. pevent = self.fake_github.getPushEvent(project='org/project2',
  1177. old_rev=A.head_sha,
  1178. ref='refs/heads/master',
  1179. modified_files=['zuul.yaml'])
  1180. # record previous tenant reconfiguration time, which may not be set
  1181. old = self.scheds.first.sched.tenant_last_reconfigured\
  1182. .get('tenant-one', 0)
  1183. self.waitUntilSettled()
  1184. self.fake_github.emitEvent(pevent)
  1185. self.waitUntilSettled()
  1186. new = self.scheds.first.sched.tenant_last_reconfigured\
  1187. .get('tenant-one', 0)
  1188. # We don't expect a reconfiguration because the push was to an
  1189. # unprotected branch
  1190. self.assertEqual(old, new)
  1191. # now enable branch protection and trigger the push event again
  1192. github = self.fake_github.getGithubClient()
  1193. repo = github.repo_from_project('org/project2')
  1194. repo._set_branch_protection('master', True)
  1195. self.fake_github.emitEvent(pevent)
  1196. self.waitUntilSettled()
  1197. new = self.scheds.first.sched.tenant_last_reconfigured\
  1198. .get('tenant-one', 0)
  1199. # We now expect that zuul reconfigured itself
  1200. self.assertLess(old, new)
  1201. def test_protected_branch_delete(self):
  1202. """Test that protected branch deletes trigger a tenant reconfig"""
  1203. # Prepare repo with an initial commit and enable branch protection
  1204. github = self.fake_github.getGithubClient()
  1205. repo = github.repo_from_project('org/project2')
  1206. repo._set_branch_protection('master', True)
  1207. A = self.fake_github.openFakePullRequest('org/project2', 'master', 'A')
  1208. A.setMerged("merging A")
  1209. # add a spare branch so that the project is not empty after master gets
  1210. # deleted.
  1211. repo._create_branch('feat-x')
  1212. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1213. self.waitUntilSettled()
  1214. # record previous tenant reconfiguration time, which may not be set
  1215. old = self.scheds.first.sched.tenant_last_reconfigured\
  1216. .get('tenant-one', 0)
  1217. self.waitUntilSettled()
  1218. # Delete the branch
  1219. repo._delete_branch('master')
  1220. pevent = self.fake_github.getPushEvent(project='org/project2',
  1221. old_rev=A.head_sha,
  1222. new_rev='0' * 40,
  1223. ref='refs/heads/master',
  1224. modified_files=['zuul.yaml'])
  1225. self.fake_github.emitEvent(pevent)
  1226. self.waitUntilSettled()
  1227. new = self.scheds.first.sched.tenant_last_reconfigured\
  1228. .get('tenant-one', 0)
  1229. # We now expect that zuul reconfigured itself as we deleted a protected
  1230. # branch
  1231. self.assertLess(old, new)
  1232. # This test verifies that a PR is considered in case it was created for
  1233. # a branch just has been set to protected before a tenant reconfiguration
  1234. # took place.
  1235. def test_reconfigure_on_pr_to_new_protected_branch(self):
  1236. self.create_branch('org/project2', 'release')
  1237. github = self.fake_github.getGithubClient()
  1238. repo = github.repo_from_project('org/project2')
  1239. repo._set_branch_protection('master', True)
  1240. repo._create_branch('release')
  1241. repo._create_branch('feature')
  1242. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1243. self.waitUntilSettled()
  1244. repo._set_branch_protection('release', True)
  1245. self.executor_server.hold_jobs_in_build = True
  1246. A = self.fake_github.openFakePullRequest(
  1247. 'org/project2', 'release', 'A')
  1248. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1249. self.waitUntilSettled()
  1250. self.executor_server.hold_jobs_in_build = False
  1251. self.executor_server.release()
  1252. self.waitUntilSettled()
  1253. self.assertEqual('SUCCESS',
  1254. self.getJobFromHistory('used-job').result)
  1255. job = self.getJobFromHistory('used-job')
  1256. zuulvars = job.parameters['zuul']
  1257. self.assertEqual(str(A.number), zuulvars['change'])
  1258. self.assertEqual(str(A.head_sha), zuulvars['patchset'])
  1259. self.assertEqual('release', zuulvars['branch'])
  1260. self.assertEqual(1, len(self.history))
  1261. def _test_push_event_reconfigure(self, project, branch,
  1262. expect_reconfigure=False,
  1263. old_sha=None, new_sha=None,
  1264. modified_files=None,
  1265. removed_files=None,
  1266. expected_cat_jobs=None):
  1267. pevent = self.fake_github.getPushEvent(
  1268. project=project,
  1269. ref='refs/heads/%s' % branch,
  1270. old_rev=old_sha,
  1271. new_rev=new_sha,
  1272. modified_files=modified_files,
  1273. removed_files=removed_files)
  1274. # record previous tenant reconfiguration time, which may not be set
  1275. old = self.scheds.first.sched.tenant_last_reconfigured\
  1276. .get('tenant-one', 0)
  1277. self.waitUntilSettled()
  1278. if expected_cat_jobs is not None:
  1279. # clear the gearman jobs history so we can count the cat jobs
  1280. # issued during reconfiguration
  1281. self.gearman_server.jobs_history.clear()
  1282. self.fake_github.emitEvent(pevent)
  1283. self.waitUntilSettled()
  1284. new = self.scheds.first.sched.tenant_last_reconfigured\
  1285. .get('tenant-one', 0)
  1286. if expect_reconfigure:
  1287. # New timestamp should be greater than the old timestamp
  1288. self.assertLess(old, new)
  1289. else:
  1290. # Timestamps should be equal as no reconfiguration shall happen
  1291. self.assertEqual(old, new)
  1292. if expected_cat_jobs is not None:
  1293. # Check the expected number of cat jobs here as the (empty) config
  1294. # of org/project should be cached.
  1295. cat_jobs = set([job for job in self.gearman_server.jobs_history
  1296. if job.name == b'merger:cat'])
  1297. self.assertEqual(expected_cat_jobs, len(cat_jobs), cat_jobs)
  1298. def test_push_event_reconfigure_complex_branch(self):
  1299. branch = 'feature/somefeature'
  1300. project = 'org/project2'
  1301. # prepare an existing branch
  1302. self.create_branch(project, branch)
  1303. github = self.fake_github.getGithubClient()
  1304. repo = github.repo_from_project(project)
  1305. repo._create_branch(branch)
  1306. repo._set_branch_protection(branch, False)
  1307. self.fake_github.emitEvent(
  1308. self.fake_github.getPushEvent(
  1309. project,
  1310. ref='refs/heads/%s' % branch))
  1311. self.waitUntilSettled()
  1312. A = self.fake_github.openFakePullRequest(project, branch, 'A')
  1313. old_sha = A.head_sha
  1314. A.setMerged("merging A")
  1315. new_sha = random_sha1()
  1316. # branch is not protected, no reconfiguration even if config file
  1317. self._test_push_event_reconfigure(project, branch,
  1318. expect_reconfigure=False,
  1319. old_sha=old_sha,
  1320. new_sha=new_sha,
  1321. modified_files=['zuul.yaml'],
  1322. expected_cat_jobs=0)
  1323. # branch is not protected: no reconfiguration
  1324. repo._delete_branch(branch)
  1325. self._test_push_event_reconfigure(project, branch,
  1326. expect_reconfigure=False,
  1327. old_sha=new_sha,
  1328. new_sha='0' * 40,
  1329. removed_files=['zuul.yaml'])
  1330. class TestGithubWebhook(ZuulTestCase):
  1331. config_file = 'zuul-github-driver.conf'
  1332. def setUp(self):
  1333. super(TestGithubWebhook, self).setUp()
  1334. # Start the web server
  1335. self.web = self.useFixture(
  1336. ZuulWebFixture(self.gearman_server.port, self.changes, self.config,
  1337. self.additional_event_queues, self.upstream_root,
  1338. self.rpcclient, self.poller_events,
  1339. self.git_url_with_auth, self.addCleanup,
  1340. self.test_root))
  1341. host = '127.0.0.1'
  1342. # Wait until web server is started
  1343. while True:
  1344. port = self.web.port
  1345. try:
  1346. with socket.create_connection((host, port)):
  1347. break
  1348. except ConnectionRefusedError:
  1349. pass
  1350. self.fake_github.setZuulWebPort(port)
  1351. def tearDown(self):
  1352. super(TestGithubWebhook, self).tearDown()
  1353. @simple_layout('layouts/basic-github.yaml', driver='github')
  1354. def test_webhook(self):
  1355. """Test that we can get github events via zuul-web."""
  1356. self.executor_server.hold_jobs_in_build = True
  1357. A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
  1358. self.fake_github.emitEvent(A.getPullRequestOpenedEvent(),
  1359. use_zuulweb=True)
  1360. self.waitUntilSettled()
  1361. self.executor_server.hold_jobs_in_build = False
  1362. self.executor_server.release()
  1363. self.waitUntilSettled()
  1364. self.assertEqual('SUCCESS',
  1365. self.getJobFromHistory('project-test1').result)
  1366. self.assertEqual('SUCCESS',
  1367. self.getJobFromHistory('project-test2').result)
  1368. job = self.getJobFromHistory('project-test2')
  1369. zuulvars = job.parameters['zuul']
  1370. self.assertEqual(str(A.number), zuulvars['change'])
  1371. self.assertEqual(str(A.head_sha), zuulvars['patchset'])
  1372. self.assertEqual('master', zuulvars['branch'])
  1373. self.assertEqual(1, len(A.comments))
  1374. self.assertThat(
  1375. A.comments[0],
  1376. MatchesRegex(r'.*\[project-test1 \]\(.*\).*', re.DOTALL))
  1377. self.assertThat(
  1378. A.comments[0],
  1379. MatchesRegex(r'.*\[project-test2 \]\(.*\).*', re.DOTALL))
  1380. self.assertEqual(2, len(self.history))
  1381. # test_pull_unmatched_branch_event(self):
  1382. self.create_branch('org/project', 'unmatched_branch')
  1383. B = self.fake_github.openFakePullRequest(
  1384. 'org/project', 'unmatched_branch', 'B')
  1385. self.fake_github.emitEvent(B.getPullRequestOpenedEvent(),
  1386. use_zuulweb=True)
  1387. self.waitUntilSettled()
  1388. self.assertEqual(2, len(self.history))
  1389. class TestGithubShaCache(BaseTestCase):
  1390. def testInsert(self):
  1391. cache = GithubShaCache()
  1392. pr_dict = {
  1393. 'head': {
  1394. 'sha': '123456',
  1395. },
  1396. 'number': 1,
  1397. 'state': 'open',
  1398. }
  1399. cache.update('foo/bar', pr_dict)
  1400. self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
  1401. def testRemoval(self):
  1402. cache = GithubShaCache()
  1403. pr_dict = {
  1404. 'head': {
  1405. 'sha': '123456',
  1406. },
  1407. 'number': 1,
  1408. 'state': 'open',
  1409. }
  1410. cache.update('foo/bar', pr_dict)
  1411. self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
  1412. # Create 4096 entries so original falls off.
  1413. for x in range(0, 4096):
  1414. pr_dict['head']['sha'] = str(x)
  1415. cache.update('foo/bar', pr_dict)
  1416. cache.get('foo/bar', str(x))
  1417. self.assertEqual(cache.get('foo/bar', '123456'), set())
  1418. def testMultiInsert(self):
  1419. cache = GithubShaCache()
  1420. pr_dict = {
  1421. 'head': {
  1422. 'sha': '123456',
  1423. },
  1424. 'number': 1,
  1425. 'state': 'open',
  1426. }
  1427. cache.update('foo/bar', pr_dict)
  1428. self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
  1429. pr_dict['number'] = 2
  1430. cache.update('foo/bar', pr_dict)
  1431. self.assertEqual(cache.get('foo/bar', '123456'), set({1, 2}))
  1432. def testMultiProjectInsert(self):
  1433. cache = GithubShaCache()
  1434. pr_dict = {
  1435. 'head': {
  1436. 'sha': '123456',
  1437. },
  1438. 'number': 1,
  1439. 'state': 'open',
  1440. }
  1441. cache.update('foo/bar', pr_dict)
  1442. self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
  1443. cache.update('foo/baz', pr_dict)
  1444. self.assertEqual(cache.get('foo/baz', '123456'), set({1}))
  1445. def testNoMatch(self):
  1446. cache = GithubShaCache()
  1447. pr_dict = {
  1448. 'head': {
  1449. 'sha': '123456',
  1450. },
  1451. 'number': 1,
  1452. 'state': 'open',
  1453. }
  1454. cache.update('foo/bar', pr_dict)
  1455. self.assertEqual(cache.get('bar/foo', '789'), set())
  1456. self.assertEqual(cache.get('foo/bar', '789'), set())
  1457. def testClosedPRRemains(self):
  1458. cache = GithubShaCache()
  1459. pr_dict = {
  1460. 'head': {
  1461. 'sha': '123456',
  1462. },
  1463. 'number': 1,
  1464. 'state': 'closed',
  1465. }
  1466. cache.update('foo/bar', pr_dict)
  1467. self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
  1468. class TestGithubAppDriver(ZuulGithubAppTestCase):
  1469. """Inheriting from ZuulGithubAppTestCase will enable app authentication"""
  1470. config_file = 'zuul-github-driver.conf'
  1471. @simple_layout("layouts/reporting-github.yaml", driver="github")
  1472. def test_reporting_checks_api(self):
  1473. """Using the checks API only works with app authentication"""
  1474. project = "org/project3"
  1475. github = self.fake_github.getGithubClient(None)
  1476. repo = github.repo_from_project('org/project3')
  1477. repo._set_branch_protection(
  1478. 'master', contexts=['tenant-one/checks-api-reporting',
  1479. 'tenant-one/gate'])
  1480. # pipeline reports pull request status both on start and success
  1481. self.executor_server.hold_jobs_in_build = True
  1482. A = self.fake_github.openFakePullRequest(project, "master", "A")
  1483. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1484. self.waitUntilSettled()
  1485. # We should have a pending check for the head sha
  1486. self.assertIn(
  1487. A.head_sha, github.repo_from_project(project)._commits.keys())
  1488. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1489. self.assertEqual(1, len(check_runs))
  1490. check_run = check_runs[0]
  1491. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1492. self.assertEqual("in_progress", check_run["status"])
  1493. self.assertThat(
  1494. check_run["output"]["summary"],
  1495. MatchesRegex(r'.*Starting checks-api-reporting jobs.*', re.DOTALL)
  1496. )
  1497. # The external id should be a json-string containing all relevant
  1498. # information to uniquely identify this change.
  1499. self.assertEqual(
  1500. json.dumps(
  1501. {
  1502. "tenant": "tenant-one",
  1503. "pipeline": "checks-api-reporting",
  1504. "change": 1
  1505. }
  1506. ),
  1507. check_run["external_id"],
  1508. )
  1509. # A running check run should provide a custom abort action
  1510. self.assertEqual(1, len(check_run["actions"]))
  1511. self.assertEqual(
  1512. {
  1513. "identifier": "abort",
  1514. "description": "Abort this check run",
  1515. "label": "Abort",
  1516. },
  1517. check_run["actions"][0],
  1518. )
  1519. # TODO (felix): How can we test if the details_url was set correctly?
  1520. # How can the details_url be configured on the test case?
  1521. self.executor_server.hold_jobs_in_build = False
  1522. self.executor_server.release()
  1523. self.waitUntilSettled()
  1524. # We should now have an updated status for the head sha
  1525. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1526. self.assertEqual(1, len(check_runs))
  1527. check_run = check_runs[0]
  1528. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1529. self.assertEqual("completed", check_run["status"])
  1530. self.assertEqual("success", check_run["conclusion"])
  1531. self.assertThat(
  1532. check_run["output"]["summary"],
  1533. MatchesRegex(r'.*Build succeeded.*', re.DOTALL)
  1534. )
  1535. self.assertIsNotNone(check_run["completed_at"])
  1536. # A completed check run should not provide any custom actions
  1537. self.assertEqual(0, len(check_run["actions"]))
  1538. # Tell gate to merge to test checks requirements
  1539. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  1540. self.waitUntilSettled()
  1541. self.assertTrue(A.is_merged)
  1542. @simple_layout("layouts/reporting-github.yaml", driver="github")
  1543. def test_reporting_checks_api_dequeue(self):
  1544. "Test that a dequeued change will be reported back to the check run"
  1545. project = "org/project3"
  1546. github = self.fake_github.getGithubClient(None)
  1547. client = zuul.rpcclient.RPCClient(
  1548. "127.0.0.1", self.gearman_server.port
  1549. )
  1550. self.addCleanup(client.shutdown)
  1551. self.executor_server.hold_jobs_in_build = True
  1552. A = self.fake_github.openFakePullRequest(project, "master", "A")
  1553. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1554. self.waitUntilSettled()
  1555. # We should have a pending check for the head sha
  1556. self.assertIn(
  1557. A.head_sha, github.repo_from_project(project)._commits.keys())
  1558. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1559. self.assertEqual(1, len(check_runs))
  1560. check_run = check_runs[0]
  1561. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1562. self.assertEqual("in_progress", check_run["status"])
  1563. self.assertThat(
  1564. check_run["output"]["summary"],
  1565. MatchesRegex(r'.*Starting checks-api-reporting jobs.*', re.DOTALL)
  1566. )
  1567. # Use the client to dequeue the pending change
  1568. client.dequeue(
  1569. tenant="tenant-one",
  1570. pipeline="checks-api-reporting",
  1571. project="org/project3",
  1572. change="{},{}".format(A.number, A.head_sha),
  1573. ref=None,
  1574. )
  1575. self.waitUntilSettled()
  1576. # We should now have a cancelled check run for the head sha
  1577. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1578. self.assertEqual(1, len(check_runs))
  1579. check_run = check_runs[0]
  1580. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1581. self.assertEqual("completed", check_run["status"])
  1582. self.assertEqual("cancelled", check_run["conclusion"])
  1583. self.assertThat(
  1584. check_run["output"]["summary"],
  1585. MatchesRegex(r'.*Build canceled.*', re.DOTALL)
  1586. )
  1587. self.assertIsNotNone(check_run["completed_at"])
  1588. @simple_layout("layouts/reporting-github.yaml", driver="github")
  1589. def test_update_non_existing_check_run(self):
  1590. project = "org/project3"
  1591. github = self.fake_github.getGithubClient(None)
  1592. # pipeline reports pull request status both on start and success
  1593. self.executor_server.hold_jobs_in_build = True
  1594. A = self.fake_github.openFakePullRequest(project, "master", "A")
  1595. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1596. self.waitUntilSettled()
  1597. # We should have a pending check for the head sha
  1598. commit = github.repo_from_project(project)._commits.get(A.head_sha)
  1599. check_runs = commit.check_runs()
  1600. self.assertEqual(1, len(check_runs))
  1601. # Delete this check_run to simulate a failed check_run creation
  1602. commit._check_runs = []
  1603. # Now run the build and check if the update of the check_run could
  1604. # still be accomplished.
  1605. self.executor_server.hold_jobs_in_build = False
  1606. self.executor_server.release()
  1607. self.waitUntilSettled()
  1608. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1609. self.assertEqual(1, len(check_runs))
  1610. check_run = check_runs[0]
  1611. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1612. self.assertEqual("completed", check_run["status"])
  1613. self.assertEqual("success", check_run["conclusion"])
  1614. self.assertThat(
  1615. check_run["output"]["summary"],
  1616. MatchesRegex(r'.*Build succeeded.*', re.DOTALL)
  1617. )
  1618. self.assertIsNotNone(check_run["completed_at"])
  1619. # A completed check run should not provide any custom actions
  1620. self.assertEqual(0, len(check_run["actions"]))
  1621. @simple_layout("layouts/reporting-github.yaml", driver="github")
  1622. def test_update_check_run_missing_permissions(self):
  1623. project = "org/project3"
  1624. github = self.fake_github.getGithubClient(None)
  1625. repo = github.repo_from_project(project)
  1626. repo._set_permission("checks", False)
  1627. A = self.fake_github.openFakePullRequest(project, "master", "A")
  1628. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1629. self.waitUntilSettled()
  1630. # Alghough we are authenticated as github app, we are lacking the
  1631. # necessary "checks" permissions for the test repository. Thus, the
  1632. # check run creation/update should fail and we end up in two comments
  1633. # being posted to the PR with appropriate warnings.
  1634. commit = github.repo_from_project(project)._commits.get(A.head_sha)
  1635. check_runs = commit.check_runs()
  1636. self.assertEqual(0, len(check_runs))
  1637. self.assertIn(
  1638. A.head_sha, github.repo_from_project(project)._commits.keys()
  1639. )
  1640. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1641. self.assertEqual(0, len(check_runs))
  1642. expected_warning = (
  1643. "Failed to update check run tenant-one/checks-api-reporting: "
  1644. "403 Resource not accessible by integration"
  1645. )
  1646. self.assertEqual(2, len(A.comments))
  1647. self.assertIn(expected_warning, A.comments[0])
  1648. self.assertIn(expected_warning, A.comments[1])
  1649. @simple_layout("layouts/reporting-github.yaml", driver="github")
  1650. def test_abort_check_run(self):
  1651. "Test that we can dequeue a change by aborting the related check run"
  1652. project = "org/project3"
  1653. self.executor_server.hold_jobs_in_build = True
  1654. A = self.fake_github.openFakePullRequest(project, "master", "A")
  1655. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1656. self.waitUntilSettled()
  1657. # We should have a pending check for the head sha that provides an
  1658. # abort action.
  1659. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1660. self.assertEqual(1, len(check_runs))
  1661. check_run = check_runs[0]
  1662. self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
  1663. self.assertEqual("in_progress", check_run["status"])
  1664. self.assertEqual(1, len(check_run["actions"]))
  1665. self.assertEqual("abort", check_run["actions"][0]["identifier"])
  1666. self.assertEqual(
  1667. {
  1668. "tenant": "tenant-one",
  1669. "pipeline": "checks-api-reporting",
  1670. "change": 1
  1671. },
  1672. json.loads(check_run["external_id"])
  1673. )
  1674. # Simulate a click on the "Abort" button in Github by faking a webhook
  1675. # event with our custom abort action.
  1676. # Handling this event should dequeue the related change
  1677. self.fake_github.emitEvent(A.getCheckRunAbortEvent(check_run))
  1678. self.waitUntilSettled()
  1679. tenant = self.scheds.first.sched.abide.tenants.get("tenant-one")
  1680. check_pipeline = tenant.layout.pipelines["check"]
  1681. self.assertEqual(0, len(check_pipeline.getAllItems()))
  1682. self.assertEqual(1, self.countJobResults(self.history, "ABORTED"))
  1683. # The buildset was already dequeued, so there shouldn't be anything to
  1684. # release.
  1685. self.executor_server.hold_jobs_in_build = False
  1686. self.executor_server.release()
  1687. self.waitUntilSettled()
  1688. # Since the change/buildset was dequeued, the check run should be
  1689. # reported as cancelled and don't provide any further action.
  1690. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1691. self.assertEqual(1, len(check_runs))
  1692. aborted_check_run = check_runs[0]
  1693. self.assertEqual(
  1694. "tenant-one/checks-api-reporting", aborted_check_run["name"]
  1695. )
  1696. self.assertEqual("completed", aborted_check_run["status"])
  1697. self.assertEqual("cancelled", aborted_check_run["conclusion"])
  1698. self.assertEqual(0, len(aborted_check_run["actions"]))
  1699. class TestCheckRunAnnotations(ZuulGithubAppTestCase, AnsibleZuulTestCase):
  1700. """We need Github app authentication and be able to run Ansible jobs"""
  1701. config_file = 'zuul-github-driver.conf'
  1702. tenant_config_file = "config/github-file-comments/main.yaml"
  1703. def test_file_comments(self):
  1704. project = "org/project"
  1705. github = self.fake_github.getGithubClient(None)
  1706. # The README file must be part of this PR to make the comment function
  1707. # work. Thus we change it's content to provide some more text.
  1708. files_dict = {
  1709. "README": textwrap.dedent(
  1710. """
  1711. section one
  1712. ===========
  1713. here is some text
  1714. and some more text
  1715. and a last line of text
  1716. section two
  1717. ===========
  1718. here is another section
  1719. with even more text
  1720. and the end of the section
  1721. """
  1722. ),
  1723. }
  1724. A = self.fake_github.openFakePullRequest(
  1725. project, "master", "A", files=files_dict
  1726. )
  1727. self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
  1728. self.waitUntilSettled()
  1729. # We should have a pending check for the head sha
  1730. self.assertIn(
  1731. A.head_sha, github.repo_from_project(project)._commits.keys())
  1732. check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
  1733. self.assertEqual(1, len(check_runs))
  1734. check_run = check_runs[0]
  1735. self.assertEqual("tenant-one/check", check_run["name"])
  1736. self.assertEqual("completed", check_run["status"])
  1737. self.assertThat(
  1738. check_run["output"]["summary"],
  1739. MatchesRegex(r'.*Build succeeded.*', re.DOTALL)
  1740. )
  1741. annotations = check_run["output"]["annotations"]
  1742. self.assertEqual(6, len(annotations))
  1743. self.assertEqual(annotations[0], {
  1744. "path": "README",
  1745. "annotation_level": "warning",
  1746. "message": "Simple line annotation",
  1747. "start_line": 1,
  1748. "end_line": 1,
  1749. })
  1750. self.assertEqual(annotations[1], {
  1751. "path": "README",
  1752. "annotation_level": "warning",
  1753. "message": "Line annotation with level",
  1754. "start_line": 2,
  1755. "end_line": 2,
  1756. })
  1757. # As the columns are not part of the same line, they are ignored in the
  1758. # annotation. Otherwise Github will complain about the request.
  1759. self.assertEqual(annotations[2], {
  1760. "path": "README",
  1761. "annotation_level": "notice",
  1762. "message": "simple range annotation",
  1763. "start_line": 4,
  1764. "end_line": 6,
  1765. })
  1766. self.assertEqual(annotations[3], {
  1767. "path": "README",
  1768. "annotation_level": "failure",
  1769. "message": "Columns must be part of the same line",
  1770. "start_line": 7,
  1771. "end_line": 7,
  1772. "start_column": 13,
  1773. "end_column": 26,
  1774. })
  1775. # From the invalid/error file comments, only the "line out of file"
  1776. # should remain. All others are excluded as they would result in
  1777. # invalid Github requests, making the whole check run update fail.
  1778. self.assertEqual(annotations[4], {
  1779. "path": "README",
  1780. "annotation_level": "warning",
  1781. "message": "Line is not part of the file",
  1782. "end_line": 9999,
  1783. "start_line": 9999
  1784. })
  1785. self.assertEqual(annotations[5], {
  1786. "path": "README",
  1787. "annotation_level": "warning",
  1788. "message": "Invalid level will fall back to warning",
  1789. "start_line": 3,
  1790. "end_line": 3,
  1791. })
  1792. class TestGithubDriverEnterise(ZuulGithubAppTestCase):
  1793. config_file = 'zuul-github-driver-enterprise.conf'
  1794. @simple_layout('layouts/merging-github.yaml', driver='github')
  1795. def test_report_pull_merge(self):
  1796. github = self.fake_github.getGithubClient()
  1797. repo = github.repo_from_project('org/project')
  1798. repo._set_branch_protection(
  1799. 'master', require_review=True)
  1800. # pipeline merges the pull request on success
  1801. A = self.fake_github.openFakePullRequest('org/project', 'master',
  1802. 'PR title',
  1803. body='I shouldnt be seen',
  1804. body_text='PR body')
  1805. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  1806. self.waitUntilSettled()
  1807. # Since the PR was not approved it should not be merged
  1808. self.assertFalse(A.is_merged)
  1809. A.addReview('derp', 'APPROVED')
  1810. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  1811. self.waitUntilSettled()
  1812. # After approval it should be merged
  1813. self.assertTrue(A.is_merged)
  1814. self.assertThat(A.merge_message,
  1815. MatchesRegex(r'.*PR title\n\nPR body.*', re.DOTALL))
  1816. self.assertThat(A.merge_message,
  1817. Not(MatchesRegex(
  1818. r'.*I shouldnt be seen.*',
  1819. re.DOTALL)))
  1820. self.assertEqual(len(A.comments), 0)
  1821. class TestGithubDriverEnteriseLegacy(ZuulGithubAppTestCase):
  1822. config_file = 'zuul-github-driver-enterprise.conf'
  1823. def setUp(self):
  1824. self.old_version = FakeGithubEnterpriseClient.version
  1825. FakeGithubEnterpriseClient.version = '2.19.0'
  1826. super().setUp()
  1827. def tearDown(self):
  1828. super().tearDown()
  1829. FakeGithubEnterpriseClient.version = self.old_version
  1830. @simple_layout('layouts/merging-github.yaml', driver='github')
  1831. def test_report_pull_merge(self):
  1832. github = self.fake_github.getGithubClient()
  1833. repo = github.repo_from_project('org/project')
  1834. repo._set_branch_protection(
  1835. 'master', require_review=True)
  1836. # pipeline merges the pull request on success
  1837. A = self.fake_github.openFakePullRequest('org/project', 'master',
  1838. 'PR title',
  1839. body='I shouldnt be seen',
  1840. body_text='PR body')
  1841. self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
  1842. self.waitUntilSettled()
  1843. # Note: PR was not approved but old github does not support
  1844. # reviewDecision so this gets ignored and zuul merges nevertheless
  1845. self.assertTrue(A.is_merged)
  1846. self.assertThat(A.merge_message,
  1847. MatchesRegex(r'.*PR title\n\nPR body.*', re.DOTALL))
  1848. self.assertThat(A.merge_message,
  1849. Not(MatchesRegex(
  1850. r'.*I shouldnt be seen.*',
  1851. re.DOTALL)))
  1852. self.assertEqual(len(A.comments), 0)