commit-index: Mountebank tests for testing getting commit data and raw diff from hg server (
bug 1349673) r?mars
- Update iterations.py to use mercurial.py to access data from hg server
- mountebank stubs tests getting commit data as well as raw diffs from fake hg server
MozReview-Commit-ID: Cxnu0o0MvqP
--- a/commitindex/commitindex/api/iterations.py
+++ b/commitindex/commitindex/api/iterations.py
@@ -1,20 +1,27 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from connexion import problem, request
+from flask import current_app
from commitindex.reviews.triggers import get_bugzilla_client, trigger_review
+from commitindex.commits.mercurial import Mercurial
def search():
pass
+def get_mercurial_client():
+ """Return Mercurial client instance."""
+ return Mercurial(rest_url=current_app.config['HG_SERVER_URL'])
+
+
def get(id):
# TODO: Attempt to find a persisted iteration matching the request.
# We could not find a matching iteration.
return problem(
404,
'Iteration not found',
'The requested iteration does not exist',
@@ -40,20 +47,60 @@ def post(data):
401, 'Invalid Bugzilla header values',
'The Bugzilla API headers in the request were not valid'
)
topic = data.get('topic', 1)
# TODO: Topic lookup and validation
+ # Validate the commits with the mercurial server
+ data['commits'] = fetch_commit_data(data['commits'])
+
# Trigger review creation for this iteration.
trigger_review(data['commits'], request.header['X-Bugzilla-API-Key'])
return {
'data': {
'id': 1,
'topic': topic,
'commits': [{
'id': commit
} for commit in data['commits']],
},
}, 200
+
+
+def fetch_commit_data(commits, repo):
+ """
+ Take a list of commits ids, and fetch data for each from the
+ Mercurial server.
+
+ TODO:
+ 1. host variable should go away and be pulled in from some
+ configuration file or environment variable. It is added here
+ to facilitate mountebank testing.
+ """
+
+ mercurial = get_mercurial_client()
+
+ commit_data = []
+
+ for commit in commits:
+ data = mercurial.get_commit_data(repo, commit)
+ commit_data.append(data)
+
+ return commit_data
+
+
+def fetch_commit_diff(commit, repo):
+ """
+ Fetch the full diff for a specified commit.
+
+ TODO:
+ 1. host variable should go away and be pulled in from some
+ configuration file or environment variable. It is added here
+ to facilitate mountebank testing.
+ """
+
+ mercurial = get_mercurial_client()
+
+ return mercurial.get_commit_diff(repo, commit)
--- a/commitindex/commitindex/commits/mercurial.py
+++ b/commitindex/commitindex/commits/mercurial.py
@@ -9,24 +9,21 @@ import logging
import requests
logger = logging.getLogger(__name__)
class Mercurial(object):
"""
Interface to a Mercurial system.
-
- TODO:
- 1. Load API URL from system wide config
"""
def __init__(self, rest_url):
self.rest_url = rest_url
- self.session = requests.Session()
+ self.session = requests.Session()
def call(self, method, path, data=None, raw=False):
"""Perform API call and decode JSON.
Generic call function that performs an API call to the
Mercurial system and turns the JSON data returned into a
Python data object.
@@ -56,29 +53,29 @@ class Mercurial(object):
self.rest_url + path, params=data, headers=headers
)
if method == 'POST':
response = self.session.post(
self.rest_url + path, json=data, headers=headers
)
+ if not response.ok:
+ raise MercurialError(response.reason, response.status_code)
+
if not raw and \
'Content-Type' in response.headers and \
response.headers['Content-Type'] == 'application/json':
try:
data = json.loads(response.content.decode())
except:
raise MercurialError("Error decoding JSON data", 400)
else:
data = str(response.content.decode())
- if isinstance(data, dict) and 'error' in data:
- raise MercurialError(data['message'], data['code'])
-
return data
def get_commit_data(self, repo, commit):
"""Get meta data about the commit from Merurial.
Params:
repo: Path for the repo
commit: Commit node for the data to retrieve.
@@ -86,57 +83,40 @@ class Mercurial(object):
Returns:
Python dict containing meta data about commit.
Raises:
MercurialError: General error where the fault code and string will
pertain to the specific error code generated by Mercurial.
"""
- try:
- commit_data = self.call(
- 'GET',
- '/' + quote(str(repo)) + '/json-rev/' + quote(str(commit))
- )
- except MercurialError as error:
- raise MercurialError(
- 'Resource not found or Mercurial server not available.',
- error.fault_code
- )
-
- return commit_data
+ return self.call(
+ 'GET', '/' + quote(str(repo)) + '/json-rev/' + quote(str(commit))
+ )
def get_commit_diff(self, repo, commit):
"""Get raw diff belonging the commit from Merurial.
Params:
repo: Path for the repo
commit: Commit node for the data to retrieve.
Returns:
Raw diff (text)
Raises:
MercurialError: General error where the fault code and string will
pertain to the specific error code generated by Mercurial.
"""
- try:
- diff = self.call(
- 'GET',
- '/' + quote(str(repo)) + '/raw-rev/' + quote(str(commit)),
- raw=True
- )
- except MercurialError as error:
- raise MercurialError(
- 'Resource not found or Mercurial server not available.',
- error.fault_code
- )
-
- return diff
+ return self.call(
+ 'GET',
+ '/' + quote(str(repo)) + '/raw-rev/' + quote(str(commit)),
+ raw=True
+ )
class MercurialError(Exception):
"""Generic Merurial Exception"""
def __init__(self, msg, code=None):
super(MercurialError, self).__init__(msg)
self.fault_code = int(code)
--- a/commitindex/commitindex/reviews/bugzilla.py
+++ b/commitindex/commitindex/reviews/bugzilla.py
@@ -120,17 +120,20 @@ class Bugzilla(object):
BugzillaError: General error where the fault code and string will
pertain to the specific error code generated by Bugzilla.
"""
params = {'login': quote(username)}
try:
self.call(
- 'GET', '/rest/valid_login', data=params, api_key=quote(api_key)
+ 'GET',
+ '/rest/valid_login',
+ data=params,
+ api_key=quote(api_key)
)
except BugzillaError as error:
if error.fault_code == 306:
return False
raise
return True
new file mode 100644
--- /dev/null
+++ b/commitindex/tests/test_hg_server.py
@@ -0,0 +1,281 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+Mountebank test cases for commit-index
+"""
+
+from commitindex.commitindex import app
+from commitindex.api.iterations import (
+ get_mercurial_client, fetch_commit_data, fetch_commit_diff
+)
+from commitindex.commits.mercurial import MercurialError
+from testing import MountebankClient
+from flask import current_app
+import pytest
+
+COMMIT_DIFF = """diff --git a/dirs/source.py b/dirs/source.py
+--- a/dirs/source.py
++++ b/dirs/source.py
+@@ -1,8 +1,17 @@
+
++from commitindex.reviews.bugzilla import Bugzilla
+"""
+
+
+class FakeMercurial:
+ """Setups up the imposter test double emulating Mercurial"""
+
+ def __init__(self, mountebank_client):
+ self.mountebank = mountebank_client
+
+ @property
+ def url(self):
+ """Return fully qualified url for server"""
+
+ # Copied from the project docker-compose.yml file.
+ return 'http://mountebank:' + str(self.mountebank.imposter_port)
+
+ def create_commit_stubs(self, commit_data):
+ """Get detail information about specific revids from Mercurial."""
+
+ data_path = '/automation/conduit/json-rev/'
+ diff_path = '/automation/conduit/raw-rev/'
+
+ create_stubs = []
+ for commit_node in commit_data.keys():
+ create_stubs.append(
+ {
+ "predicates": [
+ {
+ "equals": {
+ "method": "GET",
+ "path": data_path + commit_node
+ }
+ }
+ ],
+ "responses": [
+ {
+ "is": {
+ "statusCode": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "node": commit_node,
+ "date": [1490110970.0, 14400],
+ "desc": "commit description",
+ "backedoutby": "",
+ "branch": "default",
+ "bookmarks": [],
+ "tags": [],
+ "user": "Joe Developer <joe@mexample.com>",
+ "parents": [commit_data[commit_node]],
+ "phase": "public",
+ "pushid": 75,
+ "pushdate": [1490146788, 0],
+ "pushuser": "joe@example.com"
+ }
+ }
+ }
+ ]
+ }
+ )
+ create_stubs.append(
+ {
+ "predicates": [
+ {
+ "equals": {
+ "method": "GET",
+ "path": diff_path + commit_node
+ }
+ }
+ ],
+ "responses": [
+ {
+ "is": {
+ "statusCode": 200,
+ "headers": {
+ "Content-Type": "text/plain"
+ },
+ "body": COMMIT_DIFF
+ }
+ }
+ ]
+ }
+ )
+
+ predicates_not_found = []
+ for commit_node in commit_data.keys():
+ predicates_not_found.append(
+ {
+ "not": {
+ "equals": {
+ "path": data_path + commit_node
+ }
+ }
+ }
+ )
+ predicates_not_found.append(
+ {
+ "not": {
+ "equals": {
+ "path": diff_path + commit_node
+ }
+ }
+ }
+ )
+
+ create_stubs.append(
+ {
+ "predicates": predicates_not_found,
+ "responses": [{
+ "is": {
+ "statusCode": 404
+ }
+ }]
+ }
+ )
+ self.mountebank.create_stub(create_stubs)
+
+
+@pytest.fixture(scope='session')
+def mountebank():
+ """Returns configured Mounteback client instance"""
+
+ # The docker-compose internal DNS entry for the mountebank container
+ mountebank_host = "mountebank"
+ # Lifted from the docker-compose file
+ mountebank_admin_port = 2525
+ mountebank_imposter_port = 4000
+
+ return MountebankClient(
+ mountebank_host, mountebank_admin_port, mountebank_imposter_port
+ )
+
+
+@pytest.fixture
+def mercurial(request, mountebank):
+ """Returns emulated Mercurial service methods"""
+
+ # NOTE: comment out the line below if you want mountebank to save your
+ # requests and responses for inspection after the test suite completes.
+ # You can manually clean up the imposters afterwards by sending HTTP
+ # DELETE to the exposed mountebank admin port, documented in
+ # docker-compose.yml, or by restarting the mountebank container. See
+ # http://www.mbtest.org/docs/api/stubs for details.
+ request.addfinalizer(mountebank.reset_imposters)
+ return FakeMercurial(mountebank)
+
+
+@pytest.fixture
+def fake_commit_data(mercurial):
+ """Setup stubs for testing."""
+
+ # commit_node : parent_node
+ commit_data = {
+ 'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc':
+ '797fef1ce31a759dcea06e8cd269405bcbd14f96',
+ '48982f7f928b8c8a77433bf7b1fa986fda4b239d':
+ 'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc'
+ }
+
+ mercurial.create_commit_stubs(commit_data)
+
+ return commit_data
+
+
+def test_mercurial_client_properly_created():
+ """Tests that a Mercurial client is properly created with URL"""
+
+ hg_server_url = 'http://blah/'
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = hg_server_url
+
+ client = get_mercurial_client()
+ assert client.rest_url == hg_server_url
+
+
+def test_get_commit_data_success(fake_commit_data, mercurial):
+ """
+ Tests for successfully getting extended commit data from Mercurial.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ results = fetch_commit_data(
+ [x for x in fake_commit_data.keys()],
+ 'automation/conduit',
+ )
+
+ for result in results:
+ assert result['node'] in fake_commit_data.keys()
+
+
+def test_get_commit_data_bad_commit_raises_error(fake_commit_data, mercurial):
+ """
+ Tests for proper failure from Mercurial when using bad commit node.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ with pytest.raises(MercurialError):
+ fetch_commit_data(['12345'], 'automation/conduit')
+
+
+def test_get_commit_data_bad_repo_raises_error(fake_commit_data, mercurial):
+ """
+ Tests for proper failure from Mercurial when using bad repo name.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ with pytest.raises(MercurialError):
+ fetch_commit_data(
+ ['f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc'],
+ 'automation/bugzilla'
+ )
+
+
+def test_get_commit_diff_success(fake_commit_data, mercurial):
+ """
+ Tests for successfully getting raw diff from Mercurial.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ result = fetch_commit_diff(
+ 'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc', 'automation/conduit'
+ )
+ assert result == COMMIT_DIFF
+
+
+def test_get_commit_diff_bad_commit_raises_error(fake_commit_data, mercurial):
+ """
+ Tests for proper failure Mercurial when using bad commit node.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ with pytest.raises(MercurialError):
+ fetch_commit_diff('12345', 'automation/conduit')
+
+
+def test_get_commit_diff_bad_repo_raises_error(fake_commit_data, mercurial):
+ """
+ Tests for proper failure from Mercurial when using bad repo.
+ """
+
+ with app.app.app_context():
+ current_app.config['HG_SERVER_URL'] = mercurial.url
+
+ with pytest.raises(MercurialError):
+ fetch_commit_diff(
+ 'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc',
+ 'automation/bugzilla'
+ )