From acf443a6edd65c80ca357dde4ef4444ff9504a1b Mon Sep 17 00:00:00 2001 From: Sean Hudgston Date: Tue, 25 Feb 2020 16:36:52 -0500 Subject: [PATCH 1/2] Make GH API URLs configurable (#41) * Add a github.api_base config value to set the API base URL. Other unplanned improvements: * Remove the cirrus package from unit tests to alleviate import confusion and errors when running tests, and to layout the tests directory in a more standard fashion. * Add GitHubContextTest tests * Remove data=json.dumps(x) calls to requests, replace with json=x in github_tools.py * Upgrade requests to 2.22.0. Previously was set to 2.3.0, but 2.22.0 is the latest version. The version we end up with does not recognize `json` as a keyword argument to requests.put/post. * Change test runner to pytest GitHub: https://github.ibm.com/cloudant/service_engineering/issues/426 --- .travis.yml | 10 +- cirrus.conf | 3 + requirements.txt | 6 +- src/cirrus/cirrus_setup.py | 10 +- src/cirrus/configuration.py | 21 +- src/cirrus/github_tools.py | 142 ++++--- src/cirrus/plusone.py | 12 +- tests/__init__.py | 0 tests/unit/{cirrus => }/build_tests.py | 0 tests/unit/cirrus/__init__.py | 0 tests/unit/cirrus/github_tools_test.py | 119 ------ tests/unit/{cirrus => }/configuration_test.py | 20 +- tests/unit/{cirrus => }/creds_plugin_tests.py | 0 .../unit/{cirrus => }/default_creds_tests.py | 0 tests/unit/{cirrus => }/docker_test.py | 0 .../{cirrus => }/documentation_utils_test.py | 0 tests/unit/{cirrus => }/environment_test.py | 0 tests/unit/{cirrus => }/feature_test.py | 0 tests/unit/{cirrus => }/git_tools_test.py | 0 tests/unit/github_tools_test.py | 396 ++++++++++++++++++ tests/unit/{cirrus => }/harnesses.py | 0 .../unit/{cirrus => }/keyring_creds_tests.py | 0 tests/unit/{cirrus => }/package_tests.py | 0 .../{cirrus => }/publisher_plugins_tests.py | 0 tests/unit/{cirrus => }/pylint_tools_test.py | 0 tests/unit/{cirrus => }/pypirc_tests.py | 0 .../unit/{cirrus => }/quality_control_test.py | 0 tests/unit/{cirrus => }/release_test.py | 0 tests/unit/{cirrus => }/test_test.py | 0 29 files changed, 537 insertions(+), 202 deletions(-) delete mode 100644 tests/__init__.py rename tests/unit/{cirrus => }/build_tests.py (100%) delete mode 100644 tests/unit/cirrus/__init__.py delete mode 100644 tests/unit/cirrus/github_tools_test.py rename tests/unit/{cirrus => }/configuration_test.py (88%) rename tests/unit/{cirrus => }/creds_plugin_tests.py (100%) rename tests/unit/{cirrus => }/default_creds_tests.py (100%) rename tests/unit/{cirrus => }/docker_test.py (100%) rename tests/unit/{cirrus => }/documentation_utils_test.py (100%) rename tests/unit/{cirrus => }/environment_test.py (100%) rename tests/unit/{cirrus => }/feature_test.py (100%) rename tests/unit/{cirrus => }/git_tools_test.py (100%) create mode 100644 tests/unit/github_tools_test.py rename tests/unit/{cirrus => }/harnesses.py (100%) rename tests/unit/{cirrus => }/keyring_creds_tests.py (100%) rename tests/unit/{cirrus => }/package_tests.py (100%) rename tests/unit/{cirrus => }/publisher_plugins_tests.py (100%) rename tests/unit/{cirrus => }/pylint_tools_test.py (100%) rename tests/unit/{cirrus => }/pypirc_tests.py (100%) rename tests/unit/{cirrus => }/quality_control_test.py (100%) rename tests/unit/{cirrus => }/release_test.py (100%) rename tests/unit/{cirrus => }/test_test.py (100%) diff --git a/.travis.yml b/.travis.yml index 3d4cee4..bf924bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: python python: - - "3.5" - -install: "pip install -r requirements.txt" + - 3.5 +install: + - pip install -e . # command to run tests -script: nosetests -w ./tests/unit - +script: + - pytest notifications: email: false diff --git a/cirrus.conf b/cirrus.conf index 0738291..55e36af 100644 --- a/cirrus.conf +++ b/cirrus.conf @@ -12,6 +12,9 @@ master_branch = new-master release_branch_prefix = release/ feature_branch_prefix = feature/ +[github] +api_base = https://api.github.com + [commands] cirrus = cirrus.delegate:main hello = cirrus.hello:main diff --git a/requirements.txt b/requirements.txt index 9e620ca..317a323 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ argparse==1.2.1 arrow==0.4.2 Fabric==2.4.0 -GitPython==2.1.9 +GitPython==3.1.0 pep8==1.5.7 pylint==1.3.0 pytest -requests==2.3.0 +requests==2.22.0 PyChef==0.2.3 keyring==8.5.1 virtualenv @@ -13,4 +13,4 @@ pluggage dockerstache>=0.0.9 requests-toolbelt==0.6.2 tox -wheel \ No newline at end of file +wheel diff --git a/src/cirrus/cirrus_setup.py b/src/cirrus/cirrus_setup.py index 84da0be..1e2e239 100644 --- a/src/cirrus/cirrus_setup.py +++ b/src/cirrus/cirrus_setup.py @@ -7,14 +7,14 @@ import json import getpass import requests +from urllib.parse import urljoin from argparse import ArgumentParser -from cirrus.configuration import load_setup_configuration, get_creds_plugin +from cirrus.configuration import load_setup_configuration, get_github_api_base from cirrus.logger import get_logger LOGGER = get_logger() -GITHUB_AUTH_URL = "https://api.github.com/authorizations" def ask_question(question, default=None, valid=None): @@ -67,13 +67,15 @@ def create_github_token(): requests for things like pull requests """ + authz_url = urljoin(get_github_api_base(), '/authorizations') + oauth_note = "cirrus script" user = ask_question( 'what is your github username?', default=os.environ['USER'] ) passwd = getpass.getpass('what is your github password?') - resp = requests.get(GITHUB_AUTH_URL, auth=(user, passwd)) + resp = requests.get(authz_url, auth=(user, passwd)) resp.raise_for_status() apps = resp.json() matched_app = None @@ -87,7 +89,7 @@ def create_github_token(): # need to create a new token LOGGER.info("Creating a new Token for github access...") resp = requests.post( - GITHUB_AUTH_URL, + authz_url, auth=(user, passwd), data=json.dumps( {"scopes": ["gist", "repo"], "note": oauth_note} diff --git a/src/cirrus/configuration.py b/src/cirrus/configuration.py index ed47a13..41b24d6 100644 --- a/src/cirrus/configuration.py +++ b/src/cirrus/configuration.py @@ -14,9 +14,12 @@ import os from cirrus.gitconfig import load_gitconfig from cirrus.environment import repo_directory -import subprocess +from cirrus.logger import get_logger import configparser import pluggage.registry +import subprocess + +LOGGER = get_logger() def get_creds_plugin(plugin_name): @@ -337,3 +340,19 @@ def get_chef_auth(): 'chef_client_user': chef['chef-client-user'], 'chef_client_keyfile': chef['chef-client-keyfile'] } + + +def get_github_api_base(): + """ + Return the github.api_base URL, or https://api.github.ibm.com if not + configured. + """ + try: + url = load_configuration()['github']['api_base'].rstrip('/') + except KeyError: + url = 'https://api.github.ibm.com' + LOGGER.info( + "Failed to load api_base from github config section. " + "Using default: {}".format(url) + ) + return url diff --git a/src/cirrus/github_tools.py b/src/cirrus/github_tools.py index 88491c6..ba69241 100644 --- a/src/cirrus/github_tools.py +++ b/src/cirrus/github_tools.py @@ -1,24 +1,19 @@ -''' +""" Contains class for handling the creation of pull requests -''' -import os -import git -import json +""" import time + import arrow +import git import requests -import itertools - -from cirrus.configuration import get_github_auth, load_configuration -from cirrus.git_tools import get_active_branch -from cirrus.git_tools import push +from cirrus.configuration import get_github_auth, load_configuration, get_github_api_base +from cirrus.git_tools import get_active_branch, push from cirrus.logger import get_logger - LOGGER = get_logger() -class GitHubContext(object): +class GitHubContext: """ _GitHubContext_ @@ -35,12 +30,22 @@ def __init__(self, repo_dir, package_dir=None): 'Authorization': 'token {0}'.format(self.token), 'Content-Type': 'application/json' } + self.api_base = get_github_api_base() @property def active_branch_name(self): """return the current branch name""" return self.repo.active_branch.name + @property + def repository_api_base(self): + url = "{api_base}/repos/{org}/{repo}".format( + api_base=self.api_base, + org=self.config.organisation_name(), + repo=self.config.package_name(), + ) + return url + def __enter__(self): """start context, establish session""" self.session = requests.Session() @@ -63,9 +68,9 @@ def branch_state(self, branch=None): """ if branch is None: branch = self.active_branch_name - url = "https://api.github.com/repos/{org}/{repo}/commits/{branch}/status".format( - org=self.config.organisation_name(), - repo=self.config.package_name(), + + url = "{repo_base}/commits/{branch}/status".format( + repo_base=self.repository_api_base, branch=branch ) resp = self.session.get(url) @@ -79,9 +84,8 @@ def branch_status_list(self, branch): given branch """ - url = "https://api.github.com/repos/{org}/{repo}/commits/{branch}/statuses".format( - org=self.config.organisation_name(), - repo=self.config.package_name(), + url = "{repo_base}/commits/{branch}/statuses".format( + repo_base=self.repository_api_base, branch=branch ) resp = self.session.get(url) @@ -141,19 +145,17 @@ def set_branch_state(self, state, context, branch=None): if "rejected" not in str(ex): raise - url = "https://api.github.com/repos/{org}/{repo}/statuses/{sha}".format( - org=self.config.organisation_name(), - repo=self.config.package_name(), + url = "{repo_base}/statuses/{sha}".format( + repo_base=self.repository_api_base, sha=sha ) - data = json.dumps( - { - "state": state, - "description": "State after cirrus check.", - "context": context - } - ) - resp = self.session.post(url, data=data) + + data = { + "state": state, + "description": "State after cirrus check.", + "context": context + } + resp = self.session.post(url, json=data) resp.raise_for_status() def wait_on_gh_status(self, branch_name=None, timeout=600, interval=2): @@ -177,7 +179,7 @@ def wait_on_gh_status(self, branch_name=None, timeout=600, interval=2): if time_spent > timeout: LOGGER.error("Exceeded timeout for branch status {}".format(branch_name)) break - status = branch_status(branch_name) + status = self.branch_state(branch_name) time.sleep(interval) time_spent += interval @@ -221,6 +223,10 @@ def push_branch_with_retry(self, branch_name=None, attempts=300, cooloff=2): Work around intermittent push failures with a dumb exception retry loop """ + + if branch_name is None: + branch_name = self.active_branch_name + count = 0 error_flag = None while count < attempts: @@ -236,7 +242,7 @@ def push_branch_with_retry(self, branch_name=None, attempts=300, cooloff=2): time.sleep(cooloff) if error_flag is not None: msg = "Unable to push branch {} due to repeated failures: {}".format( - self.active_branch_name, str(ex) + self.active_branch_name, str(error_flag) ) raise RuntimeError(msg) @@ -292,9 +298,8 @@ def iter_github_branches(self): for repos with lots of branches """ - url = "https://api.github.com/repos/{org}/{repo}/branches".format( - org=self.config.organisation_name(), - repo=self.config.package_name() + url = "{repo_base}/branches".format( + repo_base=self.repository_api_base, ) params = {'per_page': 100} resp = self.session.get(url, params=params) @@ -352,9 +357,8 @@ def pull_requests(self, user=None): :returns: yields json structures for each matched PR """ - url = "https://api.github.com/repos/{org}/{repo}/pulls".format( - org=self.config.organisation_name(), - repo=self.config.package_name() + url = "{repo_base}/pulls".format( + repo_base=self.repository_api_base ) params = { 'state': 'open', @@ -380,9 +384,8 @@ def pull_request_details(self, pr): :returns: json structure (see GH API) """ - url = "https://api.github.com/repos/{org}/{repo}/pulls/{number}".format( - org=self.config.organisation_name(), - repo=self.config.package_name(), + url = "{repo_base}/pulls/{number}".format( + repo_base=self.repository_api_base, number=pr ) @@ -410,7 +413,7 @@ def plus_one_pull_request(self, pr_id=None, pr_data=None, context='+1'): 'description': 'Reviewed by {0}'.format(self.gh_user), 'context': context, } - resp = self.session.post(pr_status_url, data=json.dumps(status)) + resp = self.session.post(pr_status_url, json=status) resp.raise_for_status() def review_pull_request( @@ -432,7 +435,7 @@ def review_pull_request( comment_data = { "body": comment, } - resp = self.session.post(comment_url, data=json.dumps(comment_data)) + resp = self.session.post(comment_url, json=comment_data) resp.raise_for_status() if plusone: self.plus_one_pull_request(pr_data=pr_data, context=plusonecontext) @@ -451,7 +454,8 @@ def branch_status(branch_name): """ config = load_configuration() token = get_github_auth()[1] - url = "https://api.github.com/repos/{org}/{repo}/commits/{branch}/status".format( + url = "{api_base}/repos/{org}/{repo}/commits/{branch}/status".format( + api_base=get_github_api_base(), org=config.organisation_name(), repo=config.package_name(), branch=branch_name @@ -492,7 +496,8 @@ def current_branch_mark_status(repo_dir, state): if "rejected" not in str(ex): raise - url = "https://api.github.com/repos/{org}/{repo}/statuses/{sha}".format( + url = "{api_base}/repos/{org}/{repo}/statuses/{sha}".format( + api_base=get_github_api_base(), org=config.organisation_name(), repo=config.package_name(), sha=sha @@ -503,18 +508,17 @@ def current_branch_mark_status(repo_dir, state): 'Content-Type': 'application/json' } - data = json.dumps( - { - "state": state, - "description": "State after cirrus check.", - # @HACK: use the travis context, which is technically - # true, because we wait for Travis tests to pass before - # cutting a release. In the future, we need to setup a - # "cirrus" context, for clarity. - "context": "continuous-integration/travis-ci" - } - ) - resp = requests.post(url, headers=headers, data=data) + data = { + "state": state, + "description": "State after cirrus check.", + # @HACK: use the travis context, which is technically + # true, because we wait for Travis tests to pass before + # cutting a release. In the future, we need to setup a + # "cirrus" context, for clarity. + "context": "continuous-integration/travis-ci" + } + + resp = requests.post(url, headers=headers, json=data) resp.raise_for_status() @@ -538,9 +542,11 @@ def create_pull_request( raise RuntimeError('body is None') config = load_configuration() - url = 'https://api.github.com/repos/{0}/{1}/pulls'.format( - config.organisation_name(), - config.package_name()) + url = '{api_base}/repos/{org}/{repo}/pulls'.format( + api_base=get_github_api_base(), + org=config.organisation_name(), + repo=config.package_name() + ) if token is None: token = get_github_auth()[1] @@ -553,9 +559,10 @@ def create_pull_request( 'title': pr_info['title'], 'head': get_active_branch(repo_dir).name, 'base': config.gitflow_branch_name(), - 'body': pr_info['body']} + 'body': pr_info['body'] + } - resp = requests.post(url, data=json.dumps(data), headers=headers) + resp = requests.post(url, json=data, headers=headers) if resp.status_code == 422: LOGGER.error( ( @@ -574,8 +581,11 @@ def comment_on_sha(owner, repo, comment, sha, path, token=None): """ add a comment to the commit/sha provided """ - url = "https://api.github.com/repos/{owner}/{repo}/commits/{sha}/comments".format( - owner=owner, repo=repo, sha=sha + url = "{api_base}/repos/{owner}/{repo}/commits/{sha}/comments".format( + api_base=get_github_api_base(), + owner=owner, + repo=repo, + sha=sha ) if token is None: token = get_github_auth()[1] @@ -595,8 +605,10 @@ def comment_on_sha(owner, repo, comment, sha, path, token=None): def get_releases(owner, repo, token=None): - url = "https://api.github.com/repos/{owner}/{repo}/releases".format( - owner=owner, repo=repo + url = "{api_base}/repos/{owner}/{repo}/releases".format( + api_base=get_github_api_base(), + owner=owner, + repo=repo ) if token is None: token = get_github_auth()[1] diff --git a/src/cirrus/plusone.py b/src/cirrus/plusone.py index befb033..ffaca10 100644 --- a/src/cirrus/plusone.py +++ b/src/cirrus/plusone.py @@ -11,7 +11,7 @@ import json import argparse import requests -from .configuration import get_github_auth +from .configuration import get_github_auth, get_github_api_base class GitHubHelper(object): @@ -30,6 +30,7 @@ def __init__(self): } self.session = requests.Session() self.session.headers.update(self.auth_headers) + self.api_base = get_github_api_base() def get_pr(self, org, repo, pr_id): """ @@ -37,7 +38,8 @@ def get_pr(self, org, repo, pr_id): grab the PR details """ - url = "https://api.github.com/repos/{org}/{repo}/pulls/{id}".format( + url = "{api_base}/repos/{org}/{repo}/pulls/{id}".format( + api_base=self.api_base, org=org, repo=repo, id=pr_id @@ -72,7 +74,8 @@ def set_branch_state(self, org, repo, context, repo_dir=None, branch=None): git_repo.remotes.origin.pull(ref) sha = git_repo.head.commit.hexsha - url = "https://api.github.com/repos/{org}/{repo}/statuses/{sha}".format( + url = "{api_base}/repos/{org}/{repo}/statuses/{sha}".format( + api_base=self.api_base, org=org, repo=repo, sha=sha @@ -94,7 +97,8 @@ def plus_one(self, org, repo, sha, context, issue_url): Set the status for the given context to success on the provided sha """ - url = "https://api.github.com/repos/{org}/{repo}/statuses/{sha}".format( + url = "{api_base}/repos/{org}/{repo}/statuses/{sha}".format( + api_base=self.api_base, org=org, repo=repo, sha=sha diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/cirrus/build_tests.py b/tests/unit/build_tests.py similarity index 100% rename from tests/unit/cirrus/build_tests.py rename to tests/unit/build_tests.py diff --git a/tests/unit/cirrus/__init__.py b/tests/unit/cirrus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/cirrus/github_tools_test.py b/tests/unit/cirrus/github_tools_test.py deleted file mode 100644 index fbf1014..0000000 --- a/tests/unit/cirrus/github_tools_test.py +++ /dev/null @@ -1,119 +0,0 @@ -''' -tests for github_tools -''' -import json -from unittest import TestCase, mock -import subprocess - -from cirrus.github_tools import create_pull_request -from cirrus.github_tools import current_branch_mark_status -from cirrus.github_tools import get_releases -from .harnesses import _repo_directory - - -class GithubToolsTest(TestCase): - """ - _GithubToolsTest_ - """ - def setUp(self): - """ - setup mocks - """ - self.owner = 'testorg' - self.repo = 'testrepo' - self.release = '0.0.0' - self.commit_info = [ - { - 'committer': 'bob', - 'message': 'I made a commit!', - 'date': '2014-08-28'}, - { - 'committer': 'tom', - 'message': 'toms commit', - 'date': '2014-08-27'}] - - self.patch_get = mock.patch('cirrus.github_tools.requests.get') - self.mock_get = self.patch_get.start() - - def tearDown(self): - """ - teardown mocks - """ - self.patch_get.stop() - - def test_create_pull_request(self): - """ - _test_create_pull_request_ - """ - resp_json = {'html_url': 'https://github.com/{0}/{1}/pull/1'.format(self.owner, self.repo)} - with mock.patch( - 'cirrus.github_tools.load_configuration') as mock_config_load: - - mock_config_load.organisation_name.return_value = self.owner - mock_config_load.package_name.return_value = self.repo - with mock.patch( - 'cirrus.github_tools.get_active_branch') as mock_get_branch: - - with mock.patch( - 'cirrus.github_tools.requests.post') as mock_post: - mock_resp = mock.Mock() - mock_resp.raise_for_status.return_value = False - mock_resp.json.return_value = resp_json - mock_post.return_value = mock_resp - - with mock.patch( - 'cirrus.github_tools.json.dumps') as mock_dumps: - result = create_pull_request( - self.repo, - {'title': 'Test', 'body': 'This is a test'}, - 'token') - self.assertTrue(mock_config_load.called) - self.assertTrue(mock_get_branch.called) - self.assertTrue(mock_post.called) - self.assertTrue(mock_dumps.called) - self.assertEqual(result, resp_json['html_url']) - - def test_get_releases(self): - """ - _test_get_releases_ - """ - resp_json = [ - { - 'tag_name': self.release - } - ] - mock_req = mock.Mock() - mock_req.raise_for_status.return_value = False - mock_req.json.return_value = resp_json - self.mock_get.return_value = mock_req - result = get_releases(self.owner, self.repo, 'token') - self.assertTrue(self.mock_get.called) - self.assertIn('tag_name', result[0]) - - @mock.patch('cirrus.github_tools.load_configuration') - @mock.patch("cirrus.github_tools.requests.post") - @mock.patch("cirrus.github_tools.push") - def test_current_branch_mark_status(self, mock_push, mock_post, mock_config_load): - """ - _test_current_branch_mark_status_ - - """ - def check_post(url, headers, data): - self.assertTrue(url.startswith("https://api.github.com/repos/")) - data = json.loads(data) - self.assertEqual(data.get("state"), "success") - self.assertTrue(data.get("description")) - self.assertTrue(data.get("context")) - return mock.Mock() - - mock_post.side_effect = check_post - - mock_config_load.organisation_name.return_value = self.owner - mock_config_load.package_name.return_value = self.repo - - current_branch_mark_status(_repo_directory(), "success") - - self.assertTrue(mock_post.called) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/cirrus/configuration_test.py b/tests/unit/configuration_test.py similarity index 88% rename from tests/unit/cirrus/configuration_test.py rename to tests/unit/configuration_test.py index c72673b..e563e41 100644 --- a/tests/unit/cirrus/configuration_test.py +++ b/tests/unit/configuration_test.py @@ -9,7 +9,11 @@ from unittest import mock from cirrus.plugins.creds.default import Default -from cirrus.configuration import load_configuration +from cirrus.configuration import ( + get_github_api_base, + load_configuration, + Configuration +) class ConfigurationTests(unittest.TestCase): @@ -121,6 +125,20 @@ def test_configuration_map(self): mapping['cirrus']['configuration']['package']['name'], 'cirrus_tests' ) + @mock.patch('cirrus.configuration.load_configuration') + def test_get_github_api_base(self, load_config): + """The github.api_base value from cirrus.conf is returned""" + c = Configuration('config_file') + config_content = { + 'github': { + 'api_base': 'https://API-BASE' + } + } + c.update(config_content) + load_config.return_value = c + api = get_github_api_base() + self.assertEqual('https://API-BASE', api) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/cirrus/creds_plugin_tests.py b/tests/unit/creds_plugin_tests.py similarity index 100% rename from tests/unit/cirrus/creds_plugin_tests.py rename to tests/unit/creds_plugin_tests.py diff --git a/tests/unit/cirrus/default_creds_tests.py b/tests/unit/default_creds_tests.py similarity index 100% rename from tests/unit/cirrus/default_creds_tests.py rename to tests/unit/default_creds_tests.py diff --git a/tests/unit/cirrus/docker_test.py b/tests/unit/docker_test.py similarity index 100% rename from tests/unit/cirrus/docker_test.py rename to tests/unit/docker_test.py diff --git a/tests/unit/cirrus/documentation_utils_test.py b/tests/unit/documentation_utils_test.py similarity index 100% rename from tests/unit/cirrus/documentation_utils_test.py rename to tests/unit/documentation_utils_test.py diff --git a/tests/unit/cirrus/environment_test.py b/tests/unit/environment_test.py similarity index 100% rename from tests/unit/cirrus/environment_test.py rename to tests/unit/environment_test.py diff --git a/tests/unit/cirrus/feature_test.py b/tests/unit/feature_test.py similarity index 100% rename from tests/unit/cirrus/feature_test.py rename to tests/unit/feature_test.py diff --git a/tests/unit/cirrus/git_tools_test.py b/tests/unit/git_tools_test.py similarity index 100% rename from tests/unit/cirrus/git_tools_test.py rename to tests/unit/git_tools_test.py diff --git a/tests/unit/github_tools_test.py b/tests/unit/github_tools_test.py new file mode 100644 index 0000000..89303d2 --- /dev/null +++ b/tests/unit/github_tools_test.py @@ -0,0 +1,396 @@ +""" +tests for github_tools +""" + +from unittest import TestCase, mock + +from cirrus import configuration +from cirrus import github_tools + +from .harnesses import _repo_directory + + +def mock_load_configuration(): + """ + Return a Configuration object populated with the basics, without reading + the config file from disk. + """ + config_content = { + 'github': { + 'api_base': 'https://API-BASE' + }, + 'package': { + 'description': 'cirrus development and build git extensions', + 'name': 'testrepo', + 'organization': 'testorg', + 'python_versions': '3', + 'version': '3.1.2', + 'version_file': 'src/cirrus/__init__.py' + } + } + c = configuration.Configuration('config_file') + c.update(config_content) + return c + + +class GithubToolsTest(TestCase): + """ + _GithubToolsTest_ + """ + def setUp(self): + """ + setup mocks + """ + config = mock_load_configuration() + self.owner = config.organisation_name() + self.repo = config.package_name() + self.release = '0.0.0' + self.commit_info = [ + { + 'committer': 'bob', + 'message': 'I made a commit!', + 'date': '2014-08-28'}, + { + 'committer': 'tom', + 'message': 'toms commit', + 'date': '2014-08-27'}] + + self.patch_get = mock.patch('cirrus.github_tools.requests.get') + self.mock_get = self.patch_get.start() + + self.mock_load_configuration = mock.patch( + 'cirrus.github_tools.load_configuration' + ).start() + self.mock_load_configuration.return_value = config + + self.mock_get_github_api_base = mock.patch( + 'cirrus.github_tools.get_github_api_base' + ).start() + self.mock_get_github_api_base.return_value = 'https://API-BASE' + + self.mock_get_github_auth = mock.patch( + 'cirrus.github_tools.get_github_auth' + ).start() + self.mock_get_github_auth.return_value = ('user', 'token') + + def tearDown(self): + """ + teardown mocks + """ + mock.patch.stopall() + + @mock.patch('cirrus.github_tools.get_active_branch') + @mock.patch('cirrus.github_tools.requests.post') + def test_create_pull_request(self, mock_post, mock_get_branch): + """ + _test_create_pull_request_ + """ + resp_json = { + 'html_url': 'https://github.com/{org}/{repo}/pull/1'.format( + org=self.owner, + repo=self.repo + ) + } + + mock_resp = mock.Mock() + mock_resp.raise_for_status.return_value = False + mock_resp.json.return_value = resp_json + mock_post.return_value = mock_resp + result = github_tools.create_pull_request( + self.repo, + {'title': 'Test', 'body': 'This is a test'}, + 'token') + self.assertTrue(self.mock_load_configuration.called) + self.assertTrue(mock_get_branch.called) + self.assertTrue(mock_post.called) + self.assertEqual(result, resp_json['html_url']) + + def test_get_releases(self): + """ + _test_get_releases_ + """ + resp_json = [ + { + 'tag_name': self.release + } + ] + mock_req = mock.Mock() + mock_req.raise_for_status.return_value = False + mock_req.json.return_value = resp_json + self.mock_get.return_value = mock_req + result = github_tools.get_releases(self.owner, self.repo, 'token') + self.assertTrue(self.mock_get.called) + self.assertIn('tag_name', result[0]) + + @mock.patch('cirrus.github_tools.load_configuration') + @mock.patch("cirrus.github_tools.requests.post") + @mock.patch("cirrus.github_tools.push") + def test_current_branch_mark_status(self, mock_push, mock_post, mock_config_load): + """ + _test_current_branch_mark_status_ + + """ + def check_post(url, headers, json=None): + self.assertTrue( + url.startswith( + "https://API-BASE/repos/testorg/testrepo/statuses/" + ) + ) + self.assertEqual(json.get("state"), "success") + self.assertTrue(json.get("description")) + self.assertTrue(json.get("context")) + return mock.Mock() + + mock_post.side_effect = check_post + mock_config_load.return_value = mock_load_configuration() + + github_tools.current_branch_mark_status(_repo_directory(), "success") + + self.assertTrue(mock_post.called) + self.assertTrue(mock_push.called) + + +class GitHubContextTest(TestCase): + """Tests for GitHubContext methods using HTTP calls.""" + + def setUp(self): + self.mock_git = mock.patch('cirrus.github_tools.git').start() + repo = mock.Mock() + repo.active_branch.name = 'TEST_BRANCH' + self.mock_git.Repo.return_value = repo + + self.mock_load_configuration = mock.patch( + 'cirrus.github_tools.load_configuration' + ).start() + self.mock_load_configuration.return_value = mock_load_configuration() + + self.mock_get_github_api_base = mock.patch( + 'cirrus.github_tools.get_github_api_base' + ).start() + self.mock_get_github_api_base.return_value = 'https://API-BASE' + + self.mock_get_github_auth = mock.patch( + 'cirrus.github_tools.get_github_auth' + ).start() + self.mock_get_github_auth.return_value = ('user', 'token') + + session_patcher = mock.patch('cirrus.github_tools.requests.Session') + + self.session = mock.Mock(name='Session instance') + + mock_Session = session_patcher.start() + mock_Session.return_value = self.session + + def tearDown(self): + mock.patch.stopall() + + def test_constructor(self): + with github_tools.GitHubContext('.') as gh: + self.assertEqual('https://API-BASE', gh.api_base) + self.assertEqual( + 'https://API-BASE/repos/testorg/testrepo', + gh.repository_api_base + ) + + def test_branch_state(self): + mock_resp = mock.Mock() + mock_resp.json.return_value = {'state': 'ok'} + + with github_tools.GitHubContext('.') as gh: + gh.session.get.return_value = mock_resp + gh.branch_state('BRANCH') + gh.session.get.assert_called_with( + 'https://API-BASE/repos/testorg/testrepo/commits/BRANCH/status' + ) + + def test_branch_status_list(self): + mock_resp = mock.Mock() + mock_resp.json.return_value = ['some status'] + + with github_tools.GitHubContext('.') as gh: + gh.session.get.return_value = mock_resp + # its a generator + next(gh.branch_status_list('BRANCH')) + gh.session.get.assert_called_with( + 'https://API-BASE/repos/testorg/testrepo/commits/BRANCH/statuses' + ) + + @mock.patch('cirrus.github_tools.push') + def test_set_branch_state(self, m_push): + mock_resp = mock.Mock() + mock_resp.json.return_value = {'state': 'ok'} + mock_repo = mock.Mock() + mock_repo.head.commit.hexsha = 'SHA' + + with github_tools.GitHubContext('.') as gh: + gh.repo = mock_repo + gh.session.post.return_value = mock_resp + gh.set_branch_state( + 'success', + 'continuous-integration/travis-ci', + branch='BRANCH' + ) + gh.session.post.assert_called_with( + 'https://API-BASE/repos/testorg/testrepo/statuses/SHA', + json={ + 'state': 'success', + 'description': 'State after cirrus check.', + 'context': 'continuous-integration/travis-ci' + } + ) + + m_push.assert_called_with('.') + + @mock.patch('cirrus.github_tools.time') + def test_wait_on_gh_status_success(self, m_time): + # returns a state which is one of 'failure', 'pending', 'success' + + with github_tools.GitHubContext('.') as gh: + gh.branch_state = mock.Mock(return_value='success') + gh.wait_on_gh_status(branch_name='BRANCH') + + m_time.sleep.assert_not_called() + + @mock.patch('cirrus.github_tools.time') + def test_wait_on_gh_status_failure(self, m_time): + with github_tools.GitHubContext('.') as gh: + gh.branch_state = mock.Mock(return_value='failure') + with self.assertRaises(RuntimeError): + gh.wait_on_gh_status(branch_name='BRANCH') + + m_time.sleep.assert_not_called() + + @mock.patch('cirrus.github_tools.time') + def test_wait_on_gh_status_pending(self, m_time): + # 'success' status breaks the wait loop + with github_tools.GitHubContext('.') as gh: + gh.branch_state = mock.Mock( + side_effect=['pending', 'pending', 'success'] + ) + gh.wait_on_gh_status(branch_name='BRANCH', timeout=30, interval=2) + m_time.sleep.assert_has_calls([mock.call(2), mock.call(2)]) + m_time.reset_mock() + + # The timeout limit breaks the loop when the state is stuck in 'pending' + def _always_be_pending(*args): + return 'pending' + + with github_tools.GitHubContext('.') as gh: + gh.branch_state = mock.Mock(side_effect=_always_be_pending) + + with self.assertRaises(RuntimeError): + gh.wait_on_gh_status( + branch_name='BRANCH', + timeout=10, + interval=2 + ) + + # sleep should ben called 6 times to break the timeout threshold. + # 5 iterations of 2s to reach timeout=10, one iteration to fail the + # time_spent > timeout check. + m_time.sleep.assert_has_calls([mock.call(2)] * 6) + m_time.reset_mock() + + # 'failure' raises RuntimeError immediately + with github_tools.GitHubContext('.') as gh: + gh.branch_state = mock.Mock(side_effect='failure') + + with self.assertRaises(RuntimeError): + gh.wait_on_gh_status( + branch_name='BRANCH', + timeout=10, + interval=2 + ) + + m_time.assert_not_called() + + def test_pull_requests(self): + mock_resp = mock.Mock() + mock_resp.json.return_value = [ + {'user': {'login': 'hodor'}, 'body': 'PR body'} + ] + + with github_tools.GitHubContext('.') as gh: + gh.session.get.return_value = mock_resp + # Its a generator + row = next(gh.pull_requests(user='hodor')) + + gh.session.get.assert_called_with( + 'https://API-BASE/repos/testorg/testrepo/pulls', + params={'state': 'open'} + ) + self.assertEqual({'user': {'login': 'hodor'}, 'body': 'PR body'}, row) + + def test_pull_request_details(self): + mock_resp = mock.Mock() + mock_resp.json.return_value = {'url': 'URL', 'id': 'ID'} + + with github_tools.GitHubContext('.') as gh: + gh.session.get.return_value = mock_resp + pr = gh.pull_request_details(123) + + gh.session.get.assert_called_with( + 'https://API-BASE/repos/testorg/testrepo/pulls/123' + ) + self.assertEqual({'url': 'URL', 'id': 'ID'}, pr) + + def test_plus_one_pull_request(self): + mock_resp = mock.Mock() + + # lifted from GH API docs + pr_statuses_url = ( + 'https://api.github.ibm.com/repos/testrepo/Hello-World/statuses/' + '6dcb09b5b57875f334f61aebed695e2e4193db5e' + ) + pr_data = { + 'statuses_url': pr_statuses_url, + 'user': { + 'login': 'homer' + } + } + + with github_tools.GitHubContext('.') as gh: + gh.gh_user = 'smithers' + gh.session.post.return_value = mock_resp + gh.plus_one_pull_request(pr_id=123, pr_data=pr_data, context='+1') + + gh.session.post.assert_called_with( + pr_statuses_url, + json={ + 'state': 'success', + 'description': 'Reviewed by smithers', + 'context': '+1', + } + ) + + def test_review_pull_request(self): + mock_resp = mock.Mock() + # lifted from GH API docs + pr_statuses_url = ( + 'https://api.github.ibm.com/repos/testrepo/Hello-World/statuses/' + '6dcb09b5b57875f334f61aebed695e2e4193db5e' + ) + pr_issue_url = 'https://api.github.com/repos/testrepo/Hello-World/issues/1' + pr_data = { + 'statuses_url': pr_statuses_url, + 'issue_url': pr_issue_url + } + + with github_tools.GitHubContext('.') as gh: + gh.session.post.return_value = mock_resp + gh.plus_one_pull_request = mock.Mock() + gh.pull_request_details = mock.Mock(return_value=pr_data) + gh.review_pull_request(123, 'Comment', plusone=True) + + gh.pull_request_details.assert_called_with(123) + gh.plus_one_pull_request.assert_called_with( + context='+1', + pr_data={ + 'statuses_url': pr_statuses_url, + 'issue_url': pr_issue_url + } + ) + gh.session.post.assert_called_with( + 'https://api.github.com/repos/testrepo/Hello-World/issues/1/comments', + json={'body': 'Comment'} + ) diff --git a/tests/unit/cirrus/harnesses.py b/tests/unit/harnesses.py similarity index 100% rename from tests/unit/cirrus/harnesses.py rename to tests/unit/harnesses.py diff --git a/tests/unit/cirrus/keyring_creds_tests.py b/tests/unit/keyring_creds_tests.py similarity index 100% rename from tests/unit/cirrus/keyring_creds_tests.py rename to tests/unit/keyring_creds_tests.py diff --git a/tests/unit/cirrus/package_tests.py b/tests/unit/package_tests.py similarity index 100% rename from tests/unit/cirrus/package_tests.py rename to tests/unit/package_tests.py diff --git a/tests/unit/cirrus/publisher_plugins_tests.py b/tests/unit/publisher_plugins_tests.py similarity index 100% rename from tests/unit/cirrus/publisher_plugins_tests.py rename to tests/unit/publisher_plugins_tests.py diff --git a/tests/unit/cirrus/pylint_tools_test.py b/tests/unit/pylint_tools_test.py similarity index 100% rename from tests/unit/cirrus/pylint_tools_test.py rename to tests/unit/pylint_tools_test.py diff --git a/tests/unit/cirrus/pypirc_tests.py b/tests/unit/pypirc_tests.py similarity index 100% rename from tests/unit/cirrus/pypirc_tests.py rename to tests/unit/pypirc_tests.py diff --git a/tests/unit/cirrus/quality_control_test.py b/tests/unit/quality_control_test.py similarity index 100% rename from tests/unit/cirrus/quality_control_test.py rename to tests/unit/quality_control_test.py diff --git a/tests/unit/cirrus/release_test.py b/tests/unit/release_test.py similarity index 100% rename from tests/unit/cirrus/release_test.py rename to tests/unit/release_test.py diff --git a/tests/unit/cirrus/test_test.py b/tests/unit/test_test.py similarity index 100% rename from tests/unit/cirrus/test_test.py rename to tests/unit/test_test.py From 0d64a0b05e3a2067fb6cd783708d5e2c027d29c9 Mon Sep 17 00:00:00 2001 From: Sean Hudgston Date: Tue, 25 Feb 2020 16:45:19 -0500 Subject: [PATCH 2/2] cirrus release: new release created for release/3.2.0 --- cirrus.conf | 2 +- src/cirrus/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cirrus.conf b/cirrus.conf index 55e36af..3e02a65 100644 --- a/cirrus.conf +++ b/cirrus.conf @@ -1,6 +1,6 @@ [package] name = cirrus-cli -version = 3.1.2 +version = 3.2.0 description = cirrus development and build git extensions organization = cloudant version_file = src/cirrus/__init__.py diff --git a/src/cirrus/__init__.py b/src/cirrus/__init__.py index f4a5960..c484f1b 100644 --- a/src/cirrus/__init__.py +++ b/src/cirrus/__init__.py @@ -18,5 +18,5 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__="3.1.2" +__version__="3.2.0"