def check_tests(org, repo, token, input_file, pr_number, commit_hash): """ Check the current combined status of a GitHub PR/commit in a repo once. If tests have passed for the PR/commit, return a success. If any other status besides success (such as in-progress/pending), return a failure. If an input YAML file is specified, read the PR number from the file to check. Else if both PR number -and- commit hash is specified, return a failure. Else if either PR number -or- commit hash is specified, check the tests for the specified value. """ gh_utils = GitHubAPI(org, repo, token) if pr_number and commit_hash: LOG.info("Both PR number and commit hash are specified. Only one of the two should be specified - failing.") sys.exit(1) status_success = False if input_file: input_vars = yaml.safe_load(io.open(input_file, 'r')) pr_number = input_vars['pr_number'] status_success = gh_utils.check_pull_request_test_status(pr_number) git_obj = 'PR #{}'.format(pr_number) elif pr_number: status_success = gh_utils.check_pull_request_test_status(pr_number) git_obj = 'PR #{}'.format(pr_number) elif commit_hash: status_success = gh_utils.is_commit_successful(commit_hash) git_obj = 'commit hash {}'.format(commit_hash) LOG.info("{}: Combined status of {} is {}.".format( sys.argv[0], git_obj, "success" if status_success else "failed" )) # An exit code of 0 means success and non-zero means failure. sys.exit(not status_success)
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)), 'SuCcEsS', True), ('123', list(range(10)), 'success', True), ('123', list(range(10)), 'SUCCESS', True), ('123', list(range(10)), 'pending', False), ('123', list(range(10)), 'failure', False), ('123', list(range(10)), None, False)) @ddt.unpack def test_is_commit_successful(self, sha, statuses, state, expected): mock_combined_status = Mock(spec=CommitCombinedStatus) mock_combined_status.statuses = 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 = self.api.is_commit_successful(sha) self.assertEqual(response, expected) 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) self.api.most_recent_good_commit(branch) self.assertEqual(self.api.is_commit_successful.call_count, good_commit_id) 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) 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)