def message_pull_requests(org, repo, token, base_sha, base_ami_tags, base_ami_tag_app, head_sha, head_ami_tags, head_ami_tag_app, message_type, extra_text): u""" Message a range of Pull requests between the BASE and HEAD SHA specified. Message can be one of 3 types: - PR on stage - PR on prod - Release canceled Args: org (str): The github organization repo (str): The github repository token (str): The authentication token base_sha (str): The starting SHA base_ami_tags (str): An open YAML file containing the base AMI tags base_ami_tag_app (str): The app name to read the the base_ami_tags head_sha (str): The ending SHA head_ami_tags (str): Yaml file containing the head AMI tags head_ami_tag_app (str): the app name to read the head_ami_tags message_type (str): type of message to send extra_text (str): Extra text to be inserted in the PR message Returns: None """ methods = { u'stage': u'message_pr_deployed_stage', u'prod': u'message_pr_deployed_prod', u'rollback': u'message_pr_release_canceled', u'broke_vagrant': u'message_pr_broke_vagrant', } if base_sha is None and base_ami_tags and base_ami_tag_app: base_ami_tags = yaml.safe_load(base_ami_tags) tag = u'version:{}'.format(base_ami_tag_app) version = base_ami_tags[tag] _, _, base_sha = version.partition(u' ') if head_sha is None and head_ami_tags and head_ami_tag_app: head_ami_tags = yaml.safe_load(head_ami_tags) tag = u'version:{}'.format(head_ami_tag_app) version = head_ami_tags[tag] _, _, head_sha = version.partition(u' ') api = GitHubAPI(org, repo, token) for pull_request in api.get_pr_range(base_sha, head_sha): LOG.info(u"Posting message type %r to %d.", message_type, pull_request.number) getattr(api, methods[message_type])(pr_number=pull_request, extra_text=extra_text)
def message_pull_requests(org, repo, token, base_sha, base_ami_tags, ami_tag_app, head_sha, message_type): u""" Message a range of Pull requests between the BASE and HEAD SHA specified. Message can be one of 3 types: - PR on stage - PR on prod - Release canceled Args: org (str): The github organization repo (str): The github repository token (str): The authentication token base_sha (str): The starting SHA base_ami_tags (file): An open YAML file containing ami tags ami_tag_app (str): The app to read from the base_ami_tags head_sha (str): The ending SHA message_type (str): type of message to send Returns: None """ methods = { u'stage': u'message_pr_deployed_stage', u'prod': u'message_pr_deployed_prod', u'rollback': u'message_pr_release_canceled' } if base_sha is None and base_ami_tags and ami_tag_app: ami_tags = yaml.safe_load(base_ami_tags) tag = u'version:{}'.format(ami_tag_app) version = ami_tags[tag] _, _, base_sha = version.partition(u' ') api = GitHubAPI(org, repo, token) for pull_request in api.get_pr_range(base_sha, head_sha): LOG.info(u"Posting message type %r to %d.", message_type, pull_request.number) getattr(api, methods[message_type])(pull_request.number)
def pr_table(token, jira_url, delta): u""" Return an Element that renders all changes in `delta` as a table listing merged PRs.abs Arguments: token: The github token to access the github API with. jira_url: The base url of the JIRA instance to link JIRA tickets to. delta (VersionDelta): The AMIs to compare. """ version = delta.new or delta.base match = re.search(u"github.com/(?P<org>[^/]*)/(?P<repo>.*)", version.repo) api = GitHubAPI(match.group(u'org'), match.group(u'repo'), token) try: prs = api.get_pr_range(delta.base.sha, delta.new.sha) change_details = E.TABLE( E.CLASS(u"wrapped"), E.TBODY( E.TR( E.TH(u"Merged By"), E.TH(u"Author"), E.TH(u"Title"), E.TH(u"PR"), E.TH(u"JIRA"), E.TH(u"Release Notes?"), ), *[ E.TR( E.TD( E.A( pull_request.merged_by.login, href=pull_request.merged_by.html_url, )), E.TD( E.A( pull_request.user.login, href=pull_request.user.html_url, )), E.TD(pull_request.title), E.TD( E.A( str(pull_request.number), href=pull_request.html_url, )), E.TD( format_jira_references(jira_url, pull_request.body)), E.TD(u""), ) for pull_request in sorted( prs, key=lambda pr: pr.merged_by.login) ])) except Exception: # pylint: disable=broad-except LOGGER.exception(u'Unable to get PRs for %r', delta) change_details = E.P("Unable to list changes") return SECTION( E.H3(u"Changes for {} (".format(delta.app), E.A(GITHUB_PREFIX.sub(u'', version.repo), href=version.repo), ")"), E.P(E.STRONG(u"Before: "), E.A(delta.base.sha, href=format_commit_url(delta.base))), E.P(E.STRONG(u"After: "), E.A(delta.new.sha, href=format_commit_url(delta.new))), change_details, )
class GitHubApiTestCase(TestCase): """ Tests the requests creation/response handling for the Github API All Network calls should be mocked out. """ def setUp(self): with patch.object(Github, 'get_organization', return_value=Mock(spec=Organization)) as org_mock: with patch.object(Github, 'get_repo', return_value=Mock(spec=Repository)) as repo_mock: self.org_mock = org_mock.return_value = Mock(spec=Organization) self.repo_mock = repo_mock.return_value = Mock(spec=Repository) self.api = GitHubAPI('test-org', 'test-repo', token='abc123') super(GitHubApiTestCase, self).setUp() @patch('github.Github.get_user') def test_user(self, mock_user_method): # setup the mock mock_user_method.return_value = Mock(spec=NamedUser) self.assertIsInstance(self.api.user(), NamedUser) mock_user_method.assert_called() @ddt.data(('abc', 'success'), ('123', 'failure'), (Mock(spec=GitCommit, **{'sha': '123'}), 'success'), (Mock(spec=GitCommit, **{'sha': '123'}), 'failure')) @ddt.unpack def test_get_commit_combined_statuses(self, sha, state): combined_status = Mock(spec=CommitCombinedStatus, state=state) attrs = {'get_combined_status.return_value': combined_status} commit_mock = Mock(spec=Commit, **attrs) self.repo_mock.get_commit = lambda sha: commit_mock status = self.api.get_commit_combined_statuses(sha) self.assertEqual(status.state, state) def test_get_commit_combined_statuses_passing_commit_obj(self): combined_status = Mock(spec=CommitCombinedStatus, **{'state': 'success'}) attrs = {'get_combined_status.return_value': combined_status} commit_mock = Mock(spec=Commit, **attrs) self.repo_mock.get_commit = lambda sha: commit_mock status = self.api.get_commit_combined_statuses(commit_mock) self.assertEqual(status.state, 'success') def test_get_commit_combined_statuses_bad_object(self): self.assertRaises(UnknownObjectException, self.api.get_commit_combined_statuses, object()) def test_get_commits_by_branch(self): self.repo_mock.get_branch.return_value = Mock(spec=Branch, **{'commit.sha': '123'}) self.repo_mock.get_commits.return_value = [ Mock(spec=Commit, sha=i) for i in range(10) ] commits = self.api.get_commits_by_branch('test') self.repo_mock.get_branch.assert_called_with('test') self.repo_mock.get_commits.assert_called_with('123') self.assertEqual(len(commits), 10) def test_get_diff_url(self): def _check_url(org, repo, base_sha, head_sha): """ private method to do the comparison of the expected URL and the one we get back """ url = self.api.get_diff_url(org, repo, base_sha, head_sha) expected = 'https://github.com/{}/{}/compare/{}...{}'.format( org, repo, base_sha, head_sha) self.assertEqual(url, expected) _check_url('org', 'repo', 'base-sha', 'head-sha') with self.assertRaises(InvalidUrlException): _check_url('org', 'repo', 'abc def', 'head-sha') def test_get_commits_by_branch_branch_not_found(self): self.repo_mock.get_branch.side_effect = GithubException( 404, { 'documentation_url': 'https://developer.github.com/v3/repos/#get-branch', 'message': 'Branch not found' }) self.assertRaises(GithubException, self.api.get_commits_by_branch, 'blah') def test_delete_branch(self): ref_mock = Mock(spec=GitRef) get_git_ref_mock = Mock(return_value=ref_mock) self.repo_mock.get_git_ref = get_git_ref_mock self.api.delete_branch('blah') get_git_ref_mock.assert_called_with(ref='heads/blah') ref_mock.delete.assert_called() @ddt.data(('blah-candidate', 'falafel'), ('meow', 'schwarma ')) @ddt.unpack def test_create_branch(self, branch_name, sha): create_git_ref_mock = Mock() self.repo_mock.create_git_ref = create_git_ref_mock self.api.create_branch(branch_name, sha) create_git_ref_mock.assert_called_with( ref='refs/heads/{}'.format(branch_name), sha=sha) @ddt.data( ('blah-candidate', 'release', 'test', 'test_pr'), ('catnip', 'release', 'My meowsome PR', 'this PR has lots of catnip inside, go crazy!'), ) @ddt.unpack def test_create_pull_request(self, head, base, title, body): self.api.create_pull_request(head=head, base=base, title=title, body=body) self.repo_mock.create_pull.assert_called_with(head=head, base=base, title=title, body=body) @ddt.data('*****@*****.**', None) def test_create_tag(self, user_email): mock_user = Mock(spec=NamedUser) mock_user.email = user_email mock_user.name = 'test_name' with patch.object(Github, 'get_user', return_value=mock_user): create_tag_mock = Mock() create_ref_mock = Mock() self.repo_mock.create_git_tag = create_tag_mock self.repo_mock.create_git_ref = create_ref_mock test_tag = 'test_tag' test_sha = 'abc' self.api.create_tag(test_sha, test_tag) _, kwargs = create_tag_mock.call_args # pylint: disable=unpacking-non-sequence self.assertEqual(kwargs['tag'], test_tag) self.assertEqual(kwargs['message'], '') self.assertEqual(kwargs['type'], 'commit') self.assertEqual(kwargs['object'], test_sha) create_ref_mock.assert_called_with( ref='refs/tags/{}'.format(test_tag), sha=test_sha) def _setup_create_tag_mocks(self, status_code, msg, return_sha): """ Setup the mocks for the create_tag calls below. """ mock_user = Mock(NamedUser, email='*****@*****.**') mock_user.name = 'test_name' self.repo_mock.create_git_tag = Mock() self.repo_mock.create_git_ref = Mock( side_effect=GithubException(status_code, {'message': msg})) self.repo_mock.get_git_ref = get_tag_mock = Mock() get_tag_mock.return_value = Mock(object=Mock(sha=return_sha)) return mock_user def test_create_tag_which_already_exists_but_matches_sha(self): test_sha = 'abc' mock_user = self._setup_create_tag_mocks(422, 'Reference already exists', test_sha) with patch.object(Github, 'get_user', return_value=mock_user): # No exception. self.api.create_tag(test_sha, 'test_tag') def test_create_tag_which_already_exists_and_no_sha_match(self): mock_user = self._setup_create_tag_mocks(422, 'Reference already exists', 'def') with patch.object(Github, 'get_user', return_value=mock_user): with self.assertRaises(GitTagMismatchError): self.api.create_tag('abc', 'test_tag') def test_create_tag_which_already_exists_and_unknown_exception(self): mock_user = self._setup_create_tag_mocks(421, 'Not sure what this is!', 'def') with patch.object(Github, 'get_user', return_value=mock_user): with self.assertRaises(GithubException): self.api.create_tag('abc', 'test_tag') @ddt.data(('diverged', True), ('divergent', False), ('ahead', False)) @ddt.unpack def test_have_branches_diverged(self, status, expected): self.repo_mock.compare.return_value = Mock(spec=Comparison, status=status) self.assertEqual(self.api.have_branches_diverged('base', 'head'), expected) @ddt.data(('123', list(range(10)), 10, 'SuCcEsS', True), ('123', list(range(10)), 10, 'success', True), ('123', list(range(10)), 10, 'SUCCESS', True), ('123', list(range(10)), 10, 'pending', False), ('123', list(range(10)), 10, 'failure', False), ('123', list(range(10)), 0, None, False)) @ddt.unpack def test_check_combined_status_commit(self, sha, statuses, statuses_returned, state, expected): mock_combined_status = Mock(spec=CommitCombinedStatus) mock_combined_status.statuses = [ Mock(spec=CommitStatus, id=i) for i in statuses ] mock_combined_status.state = state commit_mock = Mock(spec=Commit) commit_mock.get_combined_status.return_value = mock_combined_status self.repo_mock.get_commit.return_value = commit_mock response, statuses = self.api.check_combined_status_commit(sha) self.assertEqual(response, expected) self.assertIsInstance(statuses, dict) self.assertEqual(len(statuses), statuses_returned) commit_mock.get_combined_status.assert_called() self.repo_mock.get_commit.assert_called_with(sha) @ddt.data( ('release-candidate', 4), ('meow-candidate', 6), ('should-have-gone-to-law-school', 1), ) @ddt.unpack def test_most_recent_good_commit(self, branch, good_commit_id): commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)] self.api.get_commits_by_branch = Mock(return_value=commits) def _side_effect(sha): """ side effect returns True when the commit ID matches the current iteration """ return (sha == good_commit_id, {}) self.api._is_commit_successful = Mock(side_effect=_side_effect) # pylint: disable=protected-access self.api.most_recent_good_commit(branch) self.assertEqual(self.api._is_commit_successful.call_count, good_commit_id) # pylint: disable=protected-access def test_most_recent_good_commit_no_commit(self): commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)] self.api.get_commits_by_branch = Mock(return_value=commits) self.api._is_commit_successful = Mock(return_value=(False, {})) # pylint: disable=protected-access self.assertRaises(NoValidCommitsError, self.api.most_recent_good_commit, 'release-candidate') @ddt.data( # 1 unique SHA should result in 1 search query and 1 PR. (SHAS[:1], 1, 1), # 18 unique SHAs should result in 1 search query and 18 PRs. (SHAS[:18], 1, 18), # 36 unique SHAs should result in 2 search queries and 36 PRs. (SHAS[:36], 2, 36), # 37 unique SHAs should result in 3 search queries and 37 PRs. (SHAS[:37], 3, 37), # 20 unique SHAs, each appearing twice, should result in 3 search queries and 20 PRs. (SHAS[:20] * 2, 3, 20), ) @ddt.unpack @patch('github.Github.search_issues') def test_get_pr_range(self, shas, expected_search_count, expected_pull_count, mock_search_issues): commits = [Mock(spec=Commit, sha=sha) for sha in shas] self.repo_mock.compare.return_value = Mock(spec=Comparison, commits=commits) def search_issues_side_effect(shas, **kwargs): # pylint: disable=unused-argument """ Stub implementation of GitHub issue search. """ return [ Mock(spec=Issue, number=TRIMMED_SHA_MAP[sha]) for sha in shas.split() ] mock_search_issues.side_effect = search_issues_side_effect self.repo_mock.get_pull = lambda number: Mock(spec=PullRequest, number=number) start_sha, end_sha = 'abc', '123' pulls = self.api.get_pr_range(start_sha, end_sha) self.repo_mock.compare.assert_called_with(start_sha, end_sha) self.assertEqual(mock_search_issues.call_count, expected_search_count) for call_args in mock_search_issues.call_args_list: # Verify that the batched SHAs have been trimmed. self.assertLess(len(call_args[0]), 200) self.assertEqual(len(pulls), expected_pull_count) for pull in pulls: self.assertIsInstance(pull, PullRequest) @ddt.data( ('Deployed to PROD', [':+1:', ':+1:', ':ship: :it:' ], False, IssueComment), ('Deployed to stage', ['wahoo', 'want BLT', 'Deployed, to PROD, JK' ], False, IssueComment), ('Deployed to PROD', [ ':+1:', 'law school man', '@macdiesel Deployed to PROD' ], False, None), ('Deployed to stage', [':+1:', ':+1:', '@macdiesel dEpLoYeD tO stage' ], False, None), ('Deployed to stage', ['@macdiesel dEpLoYeD tO stage', ':+1:', ':+1:' ], False, None), ('Deployed to PROD', [':+1:', ':+1:', '@macdiesel Deployed to PROD' ], True, IssueComment), ) @ddt.unpack def test_message_pull_request(self, new_message, existing_messages, force_message, expected_result): comments = [ Mock(spec=IssueComment, body=message) for message in existing_messages ] self.repo_mock.get_pull.return_value = \ Mock(spec=PullRequest, get_issue_comments=Mock(return_value=comments), create_issue_comment=lambda message: Mock(spec=IssueComment, body=message)) result = self.api.message_pull_request(1, new_message, new_message, force_message) self.repo_mock.get_pull.assert_called() if expected_result: self.assertIsInstance(result, IssueComment) self.assertEqual(result.body, new_message) else: self.assertEqual(result, expected_result) def test_message_pr_does_not_exist(self): with patch.object(self.repo_mock, 'get_pull', side_effect=UnknownObjectException(404, '')): self.assertRaises(InvalidPullRequestError, self.api.message_pull_request, 3, 'test', 'test') def test_message_pr_deployed_stage(self): with patch.object(self.api, 'message_pull_request') as mock: self.api.message_pr_deployed_stage(1, deploy_date=datetime( 2017, 1, 10)) mock.assert_called_with( 1, (github_api.PR_ON_STAGE_BASE_MESSAGE + github_api.PR_ON_STAGE_DATE_MESSAGE).format( date=datetime(2017, 1, 10)), github_api.PR_ON_STAGE_BASE_MESSAGE, False) @ddt.data( (datetime(2017, 1, 9, 11), date(2017, 1, 10)), (datetime(2017, 1, 13, 11), date(2017, 1, 16)), ) @ddt.unpack def test_message_pr_deployed_stage_weekend(self, message_date, deploy_date): with patch.object(self.api, 'message_pull_request') as mock: with patch.object(github_api, 'datetime', Mock(wraps=datetime)) as mock_datetime: mock_datetime.now.return_value = message_date self.api.message_pr_deployed_stage(1) mock.assert_called_with( 1, (github_api.PR_ON_STAGE_BASE_MESSAGE + github_api.PR_ON_STAGE_DATE_MESSAGE).format( date=deploy_date), github_api.PR_ON_STAGE_BASE_MESSAGE, False) def test_message_pr_deployed_prod(self): with patch.object(self.api, 'message_pull_request') as mock: self.api.message_pr_deployed_prod(1) mock.assert_called_with(1, github_api.PR_ON_PROD_MESSAGE, github_api.PR_ON_PROD_MESSAGE, False) def test_message_pr_release_canceled(self): with patch.object(self.api, 'message_pull_request') as mock: self.api.message_pr_release_canceled(1) mock.assert_called_with(1, github_api.PR_RELEASE_CANCELED_MESSAGE, github_api.PR_RELEASE_CANCELED_MESSAGE, False)
class GitHubApiTestCase(TestCase): """ Tests the requests creation/response handling for the Github API All Network calls should be mocked out. """ def setUp(self): with patch.object(Github, 'get_organization', return_value=Mock(spec=Organization)) as org_mock: with patch.object(Github, 'get_repo', return_value=Mock(spec=Repository)) as repo_mock: self.org_mock = org_mock.return_value = Mock(spec=Organization) self.repo_mock = repo_mock.return_value = Mock(spec=Repository) self.api = GitHubAPI('test-org', 'test-repo', token='abc123') self.api.log_rate_limit = Mock(return_value=None) super(GitHubApiTestCase, self).setUp() @patch('github.Github.get_user') def test_user(self, mock_user_method): # setup the mock mock_user_method.return_value = Mock(spec=NamedUser) self.assertIsInstance(self.api.user(), NamedUser) mock_user_method.assert_called() @ddt.data(('abc', 'success'), ('123', 'failure'), (Mock(spec=GitCommit, **{'sha': '123'}), 'success'), (Mock(spec=GitCommit, **{'sha': '123'}), 'failure')) @ddt.unpack def test_get_commit_combined_statuses(self, sha, state): combined_status = Mock(spec=CommitCombinedStatus, state=state) attrs = {'get_combined_status.return_value': combined_status} commit_mock = Mock(spec=Commit, **attrs) self.repo_mock.get_commit = lambda sha: commit_mock status = self.api.get_commit_combined_statuses(sha) self.assertEqual(status.state, state) def test_get_commit_combined_statuses_passing_commit_obj(self): combined_status = Mock(spec=CommitCombinedStatus, **{'state': 'success'}) attrs = {'get_combined_status.return_value': combined_status} commit_mock = Mock(spec=Commit, **attrs) self.repo_mock.get_commit = lambda sha: commit_mock status = self.api.get_commit_combined_statuses(commit_mock) self.assertEqual(status.state, 'success') def test_get_commit_combined_statuses_bad_object(self): self.assertRaises(UnknownObjectException, self.api.get_commit_combined_statuses, object()) def test_get_commits_by_branch(self): self.repo_mock.get_branch.return_value = Mock(spec=Branch, **{'commit.sha': '123'}) self.repo_mock.get_commits.return_value = [ Mock(spec=Commit, sha=i) for i in range(10) ] commits = self.api.get_commits_by_branch('test') self.repo_mock.get_branch.assert_called_with('test') self.repo_mock.get_commits.assert_called_with('123') self.assertEqual(len(commits), 10) def test_get_diff_url(self): def _check_url(org, repo, base_sha, head_sha): """ private method to do the comparison of the expected URL and the one we get back """ url = self.api.get_diff_url(org, repo, base_sha, head_sha) expected = 'https://github.com/{}/{}/compare/{}...{}'.format( org, repo, base_sha, head_sha) self.assertEqual(url, expected) _check_url('org', 'repo', 'base-sha', 'head-sha') with self.assertRaises(InvalidUrlException): _check_url('org', 'repo', 'abc def', 'head-sha') def test_get_commits_by_branch_branch_not_found(self): self.repo_mock.get_branch.side_effect = GithubException( 404, { 'documentation_url': 'https://developer.github.com/v3/repos/#get-branch', 'message': 'Branch not found' }) self.assertRaises(GithubException, self.api.get_commits_by_branch, 'blah') def test_delete_branch(self): ref_mock = Mock(spec=GitRef) get_git_ref_mock = Mock(return_value=ref_mock) self.repo_mock.get_git_ref = get_git_ref_mock self.api.delete_branch('blah') get_git_ref_mock.assert_called_with(ref='heads/blah') ref_mock.delete.assert_called() @ddt.data(('blah-candidate', 'falafel'), ('meow', 'schwarma ')) @ddt.unpack def test_create_branch(self, branch_name, sha): create_git_ref_mock = Mock() self.repo_mock.create_git_ref = create_git_ref_mock self.api.create_branch(branch_name, sha) create_git_ref_mock.assert_called_with( ref='refs/heads/{}'.format(branch_name), sha=sha) @ddt.data( ('blah-candidate', 'release', 'test', 'test_pr'), ('catnip', 'release', 'My meowsome PR', 'this PR has lots of catnip inside, go crazy!'), ) @ddt.unpack def test_create_pull_request(self, head, base, title, body): self.api.create_pull_request(head=head, base=base, title=title, body=body) self.repo_mock.create_pull.assert_called_with(head=head, base=base, title=title, body=body) @ddt.data('*****@*****.**', None) def test_create_tag(self, user_email): mock_user = Mock(spec=NamedUser) mock_user.email = user_email mock_user.name = 'test_name' with patch.object(Github, 'get_user', return_value=mock_user): create_tag_mock = Mock() create_ref_mock = Mock() self.repo_mock.create_git_tag = create_tag_mock self.repo_mock.create_git_ref = create_ref_mock test_tag = 'test_tag' test_sha = 'abc' self.api.create_tag(test_sha, test_tag) _, kwargs = create_tag_mock.call_args # pylint: disable=unpacking-non-sequence self.assertEqual(kwargs['tag'], test_tag) self.assertEqual(kwargs['message'], '') self.assertEqual(kwargs['type'], 'commit') self.assertEqual(kwargs['object'], test_sha) create_ref_mock.assert_called_with( ref='refs/tags/{}'.format(test_tag), sha=test_sha) def _setup_create_tag_mocks(self, status_code, msg, return_sha): """ Setup the mocks for the create_tag calls below. """ mock_user = Mock(NamedUser, email='*****@*****.**') mock_user.name = 'test_name' self.repo_mock.create_git_tag = Mock() self.repo_mock.create_git_ref = Mock( side_effect=GithubException(status_code, {'message': msg})) self.repo_mock.get_git_ref = get_tag_mock = Mock() get_tag_mock.return_value = Mock(object=Mock(sha=return_sha)) return mock_user def test_create_tag_which_already_exists_but_matches_sha(self): test_sha = 'abc' mock_user = self._setup_create_tag_mocks(422, 'Reference already exists', test_sha) with patch.object(Github, 'get_user', return_value=mock_user): # No exception. self.api.create_tag(test_sha, 'test_tag') def test_create_tag_which_already_exists_and_no_sha_match(self): mock_user = self._setup_create_tag_mocks(422, 'Reference already exists', 'def') with patch.object(Github, 'get_user', return_value=mock_user): with self.assertRaises(GitTagMismatchError): self.api.create_tag('abc', 'test_tag') def test_create_tag_which_already_exists_and_unknown_exception(self): mock_user = self._setup_create_tag_mocks(421, 'Not sure what this is!', 'def') with patch.object(Github, 'get_user', return_value=mock_user): with self.assertRaises(GithubException): self.api.create_tag('abc', 'test_tag') @ddt.data(('diverged', True), ('divergent', False), ('ahead', False)) @ddt.unpack def test_have_branches_diverged(self, status, expected): self.repo_mock.compare.return_value = Mock(spec=Comparison, status=status) self.assertEqual(self.api.have_branches_diverged('base', 'head'), expected) @ddt.data(('123', list(range(10)), 10, 'SuCcEsS', True, True), ('123', list(range(10)), 10, 'success', True, True), ('123', list(range(10)), 10, 'SUCCESS', True, True), ('123', list(range(10)), 10, 'pending', False, True), ('123', list(range(10)), 10, 'failure', False, True), ('123', [], 0, None, False, True), ('123', list(range(10)), 10, 'SuCcEsS', True, False), ('123', list(range(10)), 10, 'success', True, False), ('123', list(range(10)), 10, 'SUCCESS', True, False), ('123', list(range(10)), 10, 'pending', False, False), ('123', list(range(10)), 10, 'failure', False, False), ('123', [], 0, None, False, False)) @ddt.unpack def test_check_combined_status_commit(self, sha, statuses, statuses_returned, state, success_expected, use_statuses): if use_statuses: mock_combined_status = Mock(spec=CommitCombinedStatus) mock_combined_status.statuses = [ Mock(spec=CommitStatus, id=i, state=state) for i in statuses ] mock_combined_status.state = state commit_mock = Mock(spec=Commit, url="some.fake.repo/") commit_mock.get_combined_status.return_value = mock_combined_status self.repo_mock.get_commit.return_value = commit_mock commit_mock._requester = Mock() # pylint: disable=protected-access # pylint: disable=protected-access commit_mock._requester.requestJsonAndCheck.return_value = ({}, { 'check_suites': [] }) else: mock_combined_status = Mock(spec=CommitCombinedStatus) mock_combined_status.statuses = [] mock_combined_status.state = None mock_combined_status.url = None commit_mock = Mock(spec=Commit, url="some.fake.repo/") commit_mock.get_combined_status.return_value = mock_combined_status self.repo_mock.get_commit.return_value = commit_mock commit_mock._requester = Mock() # pylint: disable=protected-access # pylint: disable=protected-access commit_mock._requester.requestJsonAndCheck.return_value = ({}, { 'check_suites': [{ 'app': { 'name': 'App {}'.format(i) }, 'conclusion': state, 'url': 'some.fake.repo' } for i in statuses] }) successful, statuses = self.api.check_combined_status_commit(sha) assert successful == success_expected assert isinstance(statuses, dict) assert len(statuses) == statuses_returned commit_mock.get_combined_status.assert_called() self.repo_mock.get_commit.assert_called_with(sha) @ddt.data(('passed', True), ('failed', False)) @ddt.unpack def test_poll_commit(self, end_status, successful): url_dict = {'TravisCI': 'some url'} with patch.object(self.api, '_is_commit_successful', side_effect=[ (False, url_dict, 'pending'), (successful, url_dict, end_status), ]): result = self.api._poll_commit('some sha') # pylint: disable=protected-access assert self.api._is_commit_successful.call_count == 2 # pylint: disable=protected-access assert result[0] == end_status assert result[1] == url_dict @ddt.data( (None, None, [ '{}-{}'.format(state, valtype) for state in ['passed', 'pending', None, 'failed'] for valtype in ['status', 'check'] ]), ('status', None, ['passed-check', 'pending-check', 'None-check', 'failed-check']), ('check', None, ['passed-status', 'pending-status', 'None-status', 'failed-status']), ('check', 'passed', [ 'passed-status', 'passed-check', 'pending-status', 'None-status', 'failed-status' ]), ('.*', 'passed', ['passed-status', 'passed-check']), ) @ddt.unpack def test_filter_validation(self, exclude_contexts, include_contexts, expected_contexts): filterable_states = ['passed', 'pending', None, 'failed'] with patch.object(Github, 'get_organization', return_value=Mock(name='org-mock', spec=Organization)): with patch.object(Github, 'get_repo', return_value=Mock(name='repo-mock', spec=Repository)) as repo_mock: api = GitHubAPI('test-org', 'test-repo', token='abc123', exclude_contexts=exclude_contexts, include_contexts=include_contexts) api.log_rate_limit = Mock(return_value=None) mock_combined_status = Mock(name='combined-status', spec=CommitCombinedStatus) mock_combined_status.statuses = [ Mock(name='{}-status'.format(state), spec=CommitStatus, context='{}-status'.format(state), state=state) for state in filterable_states ] mock_combined_status.state = None mock_combined_status.url = None commit_mock = Mock(name='commit', spec=Commit, url="some.fake.repo/") commit_mock.get_combined_status.return_value = mock_combined_status repo_mock.return_value.get_commit.return_value = commit_mock commit_mock._requester = Mock(name='_requester') # pylint: disable=protected-access # pylint: disable=protected-access commit_mock._requester.requestJsonAndCheck.return_value = ({}, { 'check_suites': [{ 'app': { 'name': '{}-check'.format(state) }, 'conclusion': state, 'url': 'some.fake.repo' } for state in filterable_states] }) filtered_results = api.filter_validation_results( api.get_validation_results('deadbeef')) assert set(expected_contexts) == set(filtered_results.keys()) @ddt.data( ('release-candidate', 4), ('meow-candidate', 6), ('should-have-gone-to-law-school', 1), ) @ddt.unpack def test_most_recent_good_commit(self, branch, good_commit_id): commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)] self.api.get_commits_by_branch = Mock(return_value=commits) def _side_effect(sha): """ side effect returns True when the commit ID matches the current iteration """ return (sha == good_commit_id, {}) self.api._is_commit_successful = Mock(side_effect=_side_effect) # pylint: disable=protected-access self.api.most_recent_good_commit(branch) self.assertEqual(self.api._is_commit_successful.call_count, good_commit_id) # pylint: disable=protected-access def test_most_recent_good_commit_no_commit(self): commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)] self.api.get_commits_by_branch = Mock(return_value=commits) self.api._is_commit_successful = Mock(return_value=(False, {})) # pylint: disable=protected-access self.assertRaises(NoValidCommitsError, self.api.most_recent_good_commit, 'release-candidate') @ddt.data( # 1 unique SHA should result in 1 search query and 1 PR. (SHAS[:1], 1, 1), # 18 unique SHAs should result in 1 search query and 18 PRs. (SHAS[:18], 1, 18), # 36 unique SHAs should result in 2 search queries and 36 PRs. (SHAS[:36], 2, 36), # 37 unique SHAs should result in 3 search queries and 37 PRs. (SHAS[:37], 3, 37), # 20 unique SHAs, each appearing twice, should result in 3 search queries and 20 PRs. (SHAS[:20] * 2, 3, 20), ) @ddt.unpack @patch('github.Github.search_issues') def test_get_pr_range(self, shas, expected_search_count, expected_pull_count, mock_search_issues): commits = [Mock(spec=Commit, sha=sha) for sha in shas] self.repo_mock.compare.return_value = Mock(spec=Comparison, commits=commits) def search_issues_side_effect(query, **kwargs): # pylint: disable=unused-argument """ Stub implementation of GitHub issue search. """ return [ Mock( spec=Issue, number=TRIMMED_SHA_MAP[query_item], repository=self.repo_mock, ) for query_item in query.split() if query_item in TRIMMED_SHA_MAP ] # The query is all the shas + params to narry the query to PRs and repo. # This shouldn't break the intent of the test because we are still pulling # in all the params that are relevant to this test which are the passed in # shas. And it's ignoring other parameters that search_issues might add # to the test. mock_search_issues.side_effect = search_issues_side_effect self.repo_mock.get_pull = lambda number: Mock(spec=PullRequest, number=number) start_sha, end_sha = 'abc', '123' pulls = self.api.get_pr_range(start_sha, end_sha) self.repo_mock.compare.assert_called_with(start_sha, end_sha) self.assertEqual(mock_search_issues.call_count, expected_search_count) for call_args in mock_search_issues.call_args_list: # Verify that the batched SHAs have been trimmed. self.assertLess(len(call_args[0]), 200) self.assertEqual(len(pulls), expected_pull_count) for pull in pulls: self.assertIsInstance(pull, PullRequest) @ddt.data( ('Deployed to PROD', [':+1:', ':+1:', ':ship: :it:' ], True, IssueComment), ('Deployed to stage', ['wahoo', 'want BLT', 'Deployed, to PROD' ], False, IssueComment), ('Deployed to PROD', [ ':+1:', 'law school man', '@macdiesel Deployed to PROD' ], True, IssueComment), ('Deployed to stage', [':+1:', ':+1:', '@macdiesel dEpLoYeD tO stage' ], False, None), ('Deployed to stage', ['@macdiesel dEpLoYeD tO stage', ':+1:', ':+1:' ], False, IssueComment), ('Deployed to PROD', [':+1:', ':+1:', '@macdiesel Deployed to PROD' ], False, None), ) @ddt.unpack def test_message_pull_request(self, new_message, existing_messages, force_message, expected_result): comments = [ Mock(spec=IssueComment, body=message) for message in existing_messages ] self.repo_mock.get_pull.return_value = \ Mock(spec=PullRequest, get_issue_comments=Mock(return_value=comments), create_issue_comment=lambda message: Mock(spec=IssueComment, body=message)) result = self.api.message_pull_request(1, new_message, new_message, force_message) self.repo_mock.get_pull.assert_called() if expected_result: self.assertIsInstance(result, IssueComment) self.assertEqual(result.body, new_message) else: self.assertEqual(result, expected_result) def test_message_pr_does_not_exist(self): with patch.object(self.repo_mock, 'get_pull', side_effect=UnknownObjectException(404, '')): self.assertRaises(InvalidPullRequestError, self.api.message_pull_request, 3, 'test', 'test') def test_message_pr_deployed_stage(self): deploy_date = github_api.default_expected_release_date() with patch.object(self.api, 'message_pull_request') as mock: self.api.message_pr_with_type(1, github_api.MessageType.stage, deploy_date=deploy_date) mock.assert_called_with( 1, github_api.PR_MESSAGE_FORMAT.format( prefix=github_api.PR_PREFIX, message=github_api.MessageType.stage.value, extra_text=github_api.PR_ON_STAGE_DATE_EXTRA.format( date=deploy_date, extra_text='')), github_api.PR_MESSAGE_FILTER.format( prefix=github_api.PR_PREFIX, message=github_api.MessageType.stage.value), False) @ddt.data( (datetime(2017, 1, 9, 11), date(2017, 1, 10)), (datetime(2017, 1, 13, 11), date(2017, 1, 16)), ) @ddt.unpack def test_message_pr_deployed_stage_weekend(self, message_date, deploy_date): with patch.object(self.api, 'message_pull_request') as mock: with patch.object(github_api, 'datetime', Mock(wraps=datetime)) as mock_datetime: mock_datetime.now.return_value = message_date self.api.message_pr_with_type(1, github_api.MessageType.stage, deploy_date=deploy_date) mock.assert_called_with( 1, github_api.PR_MESSAGE_FORMAT.format( prefix=github_api.PR_PREFIX, message=github_api.MessageType.stage.value, extra_text=github_api.PR_ON_STAGE_DATE_EXTRA.format( date=deploy_date, extra_text='')), github_api.PR_MESSAGE_FILTER.format( prefix=github_api.PR_PREFIX, message=github_api.MessageType.stage.value), False) @ddt.data( (1, github_api.MessageType.prod, '', False), (1337, github_api.MessageType.prod, 'some extra words', False), (867, github_api.MessageType.prod_rollback, '', True), (5, github_api.MessageType.prod_rollback, 'Elmo does not approve', False), ) @ddt.unpack def test_message_pr_methods(self, pr_number, message_type, extra_text, force_message): with patch.object(self.api, 'message_pull_request') as mock: self.api.message_pr_with_type(pr_number, message_type, extra_text=extra_text, force_message=force_message) mock.assert_called_with( pr_number, github_api.PR_MESSAGE_FORMAT.format( prefix=github_api.PR_PREFIX, message=message_type.value, extra_text=extra_text), github_api.PR_MESSAGE_FILTER.format( prefix=github_api.PR_PREFIX, message=message_type.value), force_message)