Skip to content

Commit

Permalink
Initial outline of protocol error testing.
Browse files Browse the repository at this point in the history
- Adds a simple script to decode error codes.
- Adds tests for bad page sizes, and adds a configuration key for
  default page size.
- Improves request validation error messages
  • Loading branch information
jeromekelleher committed Mar 17, 2015
1 parent 182543f commit 872159f
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 17 deletions.
33 changes: 33 additions & 0 deletions convert_error_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Simple script to decode exception error codes. This translates
the error code received by clients into an exception class.
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import argparse

import ga4gh.exceptions as exceptions


def parseArgs():
parser = argparse.ArgumentParser(
description=(
"Converts an error code received by a clients to the "
"corresponding exception class."))
parser.add_argument(
"errorCode", type=int,
help="The errorCode value in a GAException object.")
args = parser.parse_args()
return args


def main():
args = parseArgs()
exceptionClass = exceptions.getExceptionClass(args.errorCode)
print(args.errorCode, exceptionClass, sep="\t")


if __name__ == '__main__':
main()
17 changes: 15 additions & 2 deletions ga4gh/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self):
self._callSetIds = []
self._requestValidation = False
self._responseValidation = False
self._defaultPageSize = 100

def getVariantSets(self):
"""
Expand Down Expand Up @@ -85,6 +86,10 @@ def runSearchRequest(
raise exceptions.InvalidJsonException(requestStr)
self.validateRequest(requestDict, requestClass)
request = requestClass.fromJsonDict(requestDict)
if request.pageSize is None:
request.pageSize = self._defaultPageSize
if request.pageSize <= 0:
raise exceptions.BadPageSizeException(request.pageSize)
pageList = []
nextPageToken = None
for obj, nextPageToken in objectGenerator(request):
Expand Down Expand Up @@ -279,7 +284,8 @@ def validateRequest(self, jsonDict, requestClass):
"""
if self._requestValidation:
if not requestClass.validate(jsonDict):
raise exceptions.RequestValidationFailureException()
raise exceptions.RequestValidationFailureException(
jsonDict, requestClass)

def validateResponse(self, jsonDict, responseClass):
"""
Expand All @@ -288,7 +294,8 @@ def validateResponse(self, jsonDict, responseClass):
"""
if self._responseValidation:
if not responseClass.validate(jsonDict):
raise exceptions.ResponseValidationFailureException()
raise exceptions.ResponseValidationFailureException(
jsonDict, responseClass)

def setRequestValidation(self, requestValidation):
"""
Expand All @@ -302,6 +309,12 @@ def setResponseValidation(self, responseValidation):
"""
self._responseValidation = responseValidation

def setDefaultPageSize(self, defaultPageSize):
"""
Sets the default page size for request to the specified value.
"""
self._defaultPageSize = defaultPageSize


class EmptyBackend(AbstractBackend):
"""
Expand Down
46 changes: 31 additions & 15 deletions ga4gh/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@
from __future__ import print_function
from __future__ import unicode_literals

import sys
import zlib
import inspect

import ga4gh.protocol as protocol


def getExceptionClass(errorCode):
"""
Converts the specified error code into the corresponding class object.
Raises a KeyError if the errorCode is not found.
"""
classMap = {}
for name, class_ in inspect.getmembers(sys.modules[__name__]):
if inspect.isclass(class_) and issubclass(class_, Exception):
classMap[class_.getErrorCode()] = class_
return classMap[errorCode]


def getServerError(exception):
"""
Converts the specified exception that is not a subclass of
Expand Down Expand Up @@ -85,7 +99,8 @@ class BadRequestException(BaseServerException):


class BadPageSizeException(BadRequestException):
message = "Request page size invalid"
def __init__(self, pageSize):
self.message = "Request page size '{}' is invalid".format(pageSize)


class BadPageTokenException(BadRequestException):
Expand All @@ -97,6 +112,16 @@ def __init__(self, jsonString):
self.message = "Cannot parse JSON: '{}'".format(jsonString)


class RequestValidationFailureException(BadRequestException):
"""
A validation of the request data failed
"""
def __init__(self, jsonDict, requestClass):
self.message = (
"Request '{}' is not a valid instance of {}".format(
jsonDict, requestClass))


class NotFoundException(BaseServerException):
"""
The superclass of all exceptions in which some resource was not
Expand Down Expand Up @@ -138,17 +163,6 @@ def __init__(self, message):
self.message = message


class RequestValidationFailureException(BaseServerException):
"""
A validation of the request data failed
"""
def getMessage(self):
message = (
"Malformed request: JSON does not conform to the GA4GH"
"protocol version {}".format(protocol.version))
return message


class CallSetNotInVariantSetException(NotFoundException):
"""
Indicates a request was made for a callSet not in the actual variantSet
Expand Down Expand Up @@ -180,6 +194,8 @@ class ResponseValidationFailureException(ServerError):
"""
A validation of the response data failed
"""
message = (
"Validation of the generated response failed. "
"Please file a bug report")
def __init__(self, jsonDict, requestClass):
self.message = (
"Response '{}' is not a valid instance of {}. "
"Please file a bug report.".format(
jsonDict, requestClass))
1 change: 1 addition & 0 deletions ga4gh/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def configure(configFile=None, baseConfig="ProductionConfig"):
theBackend = backend.FileSystemBackend(dataSource)
theBackend.setRequestValidation(app.config["REQUEST_VALIDATION"])
theBackend.setResponseValidation(app.config["RESPONSE_VALIDATION"])
theBackend.setDefaultPageSize(app.config["DEFAULT_PAGE_SIZE"])
app.backend = theBackend


Expand Down
1 change: 1 addition & 0 deletions ga4gh/serverconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class BaseConfig(object):
MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # 2MB
REQUEST_VALIDATION = False
RESPONSE_VALIDATION = False
DEFAULT_PAGE_SIZE = 100
DATA_SOURCE = "__EMPTY__"

# Options for the simulated backend.
Expand Down
104 changes: 104 additions & 0 deletions tests/unit/test_protocol_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Unit tests for frontend error conditions.
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import unittest

import ga4gh.frontend as frontend
import ga4gh.exceptions as exceptions
import ga4gh.protocol as protocol
import tests.utils as utils

_app = None


def setUp():
"""
Set up the test Flask app.
"""
global _app
frontend.configure(baseConfig="TestConfig")
_app = frontend.app.test_client()


def tearDown():
global _app
_app = None


class TestFrontendErrors(unittest.TestCase):
"""
Tests the frontend for various errors that can occur and verify
that the correct exception was raised by the error code sent
back.
"""
def setUp(self):
self.app = _app
# TODO replace this with ALL post methods once the rest of the
# end points have been implemented. This should also add an API
# to protocol.py to simplify and document the process of getting
# the correct API endpoints and classes. That is, we shouldn't
# use protocol.postMethods directly, but instead call a function.
supportedMethods = set([
protocol.GASearchCallSetsRequest,
protocol.GASearchVariantSetsRequest,
protocol.GASearchVariantsRequest,
])
self.endPointMap = {}
for endPoint, requestClass, responseClass in protocol.postMethods:
if requestClass in supportedMethods:
path = utils.applyVersion(endPoint)
self.endPointMap[path] = requestClass

def _createInstance(self, requestClass):
"""
Returns a valid instance of the specified class.
"""
return utils.InstanceGenerator().generateInstance(requestClass)

def assertRawRequestRaises(self, exceptionClass, url, requestString):
"""
Verifies that the specified request string returns a protocol
exception corresponding to the specified class when applied to
all POST endpoints.
"""
response = self.app.post(
url, headers={'Content-type': 'application/json'},
data=requestString)
self.assertEqual(response.status_code, exceptionClass.httpStatus)
error = protocol.GAException.fromJsonString(response.data)
self.assertEqual(
error.errorCode, exceptionClass.getErrorCode())
self.assertGreater(len(error.message), 0)

def assertRequestRaises(self, exceptionClass, url, request):
"""
Verifies that the specified request returns a protocol exception
corresponding to the specified exception class.
"""
self.assertRawRequestRaises(
exceptionClass, url, request.toJsonString())

def testPageSize(self):
for url, requestClass in self.endPointMap.items():
for badType in ["", "1", "None", 0.0, 1e3]:
request = self._createInstance(requestClass)
request.pageSize = badType
self.assertRequestRaises(
exceptions.RequestValidationFailureException, url, request)
for badSize in [-100, -1, 0]:
request = self._createInstance(requestClass)
request.pageSize = badSize
self.assertRequestRaises(
exceptions.BadPageSizeException, url, request)

def testPageToken(self):
for url, requestClass in self.endPointMap.items():
for badType in [0, 0.0, 1e-3, {}, [], [None]]:
request = self._createInstance(requestClass)
request.pageToken = badType
self.assertRequestRaises(
exceptions.RequestValidationFailureException, url, request)

0 comments on commit 872159f

Please sign in to comment.