Port rpc and wsgi to Python 3

* rpc: allow also exceptions from the builtins module. On Python 3,
  builtin exceptions are part of the builtins module. The exceptions
  module was removed in Python 3.
* Fix usage of reraise(): translate_exception() returns an instance
  which is the second parameter of the reraise() function, not the
  first parameter. The first parameter is the exception type.
* test_rpc: add json_dump_as_bytes() helper to serialize JSON as
  bytes.
* JSONRequestDeserializer: don't compare None to int, it raises a
  TypeError on Python 3.
* Fix Unicode versus bytes issues: HTTP body type is bytes. Encode
  JSON to UTF-8 for example. Replace StringIO with BytesIO.
* Replace filter() with a list-comprehension to get a list on
  Python 3.
* test_client, test_image_cache_client: use bytes for HTTP body.
* tox.ini: add glance.tests.unit.common.test_rpc to Python 3.

Change-Id: I9960d6a810375474f89c1788b7016a8fbb0770e0
This commit is contained in:
Victor Stinner 2015-10-05 15:38:25 +02:00
parent 27ca72587e
commit 3ba4a591ee
7 changed files with 46 additions and 29 deletions

View File

@ -42,6 +42,7 @@ rpc_opts = [
# from oslo rpc.
cfg.ListOpt('allowed_rpc_exception_modules',
default=['glance.common.exception',
'builtins',
'exceptions',
],
help='Modules of exceptions that are permitted to be recreated'

View File

@ -774,7 +774,7 @@ class JSONRequestDeserializer(object):
is_valid_encoding = request_encoding in self.valid_transfer_encoding
if is_valid_encoding and request.is_body_readable:
return True
elif request.content_length > 0:
elif request.content_length is not None and request.content_length > 0:
return True
return False
@ -813,7 +813,10 @@ class JSONResponseSerializer(object):
def default(self, response, result):
response.content_type = 'application/json'
response.body = self.to_json(result)
body = self.to_json(result)
if isinstance(body, six.text_type):
body = body.encode('utf-8')
response.body = body
def translate_exception(req, e):
@ -879,7 +882,8 @@ class Resource(object):
request, **action_args)
except webob.exc.WSGIHTTPException as e:
exc_info = sys.exc_info()
six.reraise(translate_exception(request, e), None, exc_info[2])
e = translate_exception(request, e)
six.reraise(type(e), e, exc_info[2])
except Exception as e:
LOG.exception(_LE("Caught error: %s"), six.text_type(e))
response = webob.exc.HTTPInternalServerError()

View File

@ -62,7 +62,7 @@ class TestClient(testtools.TestCase):
# Lets fake the response
# returned by http_client
fake = utils.FakeHTTPResponse(data="Ok")
fake = utils.FakeHTTPResponse(data=b"Ok")
http_client.HTTPConnection.getresponse().AndReturn(fake)
self.mock.ReplayAll()
@ -80,7 +80,7 @@ class TestClient(testtools.TestCase):
# Lets fake the response
# returned by http_client
fake = utils.FakeHTTPResponse(data="Ok")
fake = utils.FakeHTTPResponse(data=b"Ok")
http_client.HTTPConnection.getresponse().AndReturn(fake)
self.mock.ReplayAll()

View File

@ -19,6 +19,7 @@ import datetime
from oslo_config import cfg
from oslo_serialization import jsonutils
import routes
import six
import webob
from glance.common import exception
@ -30,6 +31,13 @@ from glance.tests import utils as test_utils
CONF = cfg.CONF
def json_dump_as_bytes(obj):
body = jsonutils.dumps(obj)
if isinstance(body, six.text_type):
body = body.encode('utf-8')
return body
class FakeResource(object):
"""
Fake resource defining some methods that
@ -118,7 +126,7 @@ class TestRPCController(base.IsolatedUnitTest):
api = create_api()
req = webob.Request.blank('/rpc')
req.method = 'POST'
req.body = jsonutils.dumps([
req.body = json_dump_as_bytes([
{
"command": "get_images",
"kwargs": {"keyword": 1}
@ -133,7 +141,7 @@ class TestRPCController(base.IsolatedUnitTest):
api = create_api()
req = webob.Request.blank('/rpc')
req.method = 'POST'
req.body = jsonutils.dumps([
req.body = json_dump_as_bytes([
{
"command": "get_all_images",
"kwargs": {"keyword": 1}
@ -153,27 +161,27 @@ class TestRPCController(base.IsolatedUnitTest):
req.content_type = 'application/json'
# Body is not a list, it should fail
req.body = jsonutils.dumps({})
req.body = json_dump_as_bytes({})
res = req.get_response(api)
self.assertEqual(400, res.status_int)
# cmd is not dict, it should fail.
req.body = jsonutils.dumps([None])
req.body = json_dump_as_bytes([None])
res = req.get_response(api)
self.assertEqual(400, res.status_int)
# No command key, it should fail.
req.body = jsonutils.dumps([{}])
req.body = json_dump_as_bytes([{}])
res = req.get_response(api)
self.assertEqual(400, res.status_int)
# kwargs not dict, it should fail.
req.body = jsonutils.dumps([{"command": "test", "kwargs": 200}])
req.body = json_dump_as_bytes([{"command": "test", "kwargs": 200}])
res = req.get_response(api)
self.assertEqual(400, res.status_int)
# Command does not exist, it should fail.
req.body = jsonutils.dumps([{"command": "test"}])
req.body = json_dump_as_bytes([{"command": "test"}])
res = req.get_response(api)
self.assertEqual(404, res.status_int)
@ -183,14 +191,15 @@ class TestRPCController(base.IsolatedUnitTest):
req.method = 'POST'
req.content_type = 'application/json'
req.body = jsonutils.dumps([{"command": "raise_value_error"}])
req.body = json_dump_as_bytes([{"command": "raise_value_error"}])
res = req.get_response(api)
self.assertEqual(200, res.status_int)
returned = jsonutils.loads(res.body)[0]
self.assertEqual('exceptions.ValueError', returned['_error']['cls'])
err_cls = 'builtins.ValueError' if six.PY3 else 'exceptions.ValueError'
self.assertEqual(err_cls, returned['_error']['cls'])
req.body = jsonutils.dumps([{"command": "raise_weird_error"}])
req.body = json_dump_as_bytes([{"command": "raise_weird_error"}])
res = req.get_response(api)
self.assertEqual(200, res.status_int)
@ -209,6 +218,8 @@ class TestRPCClient(base.IsolatedUnitTest):
def fake_request(self, method, url, body, headers):
req = webob.Request.blank(url.path)
if isinstance(body, six.text_type):
body = body.encode('utf-8')
req.body = body
req.method = method
@ -281,11 +292,11 @@ class TestRPCJSONSerializer(test_utils.BaseTestCase):
response = webob.Response()
rpc.RPCJSONSerializer().default(response, fixture)
self.assertEqual(200, response.status_int)
content_types = filter(lambda h: h[0] == 'Content-Type',
response.headerlist)
content_types = [h for h in response.headerlist
if h[0] == 'Content-Type']
self.assertEqual(1, len(content_types))
self.assertEqual('application/json', response.content_type)
self.assertEqual('{"key": "value"}', response.body)
self.assertEqual(b'{"key": "value"}', response.body)
class TestRPCJSONDeserializer(test_utils.BaseTestCase):
@ -293,21 +304,21 @@ class TestRPCJSONDeserializer(test_utils.BaseTestCase):
def test_has_body_no_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
request.body = b'asdf'
request.headers.pop('Content-Length')
self.assertFalse(rpc.RPCJSONDeserializer().has_body(request))
def test_has_body_zero_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
request.body = b'asdf'
request.headers['Content-Length'] = 0
self.assertFalse(rpc.RPCJSONDeserializer().has_body(request))
def test_has_body_has_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
request.body = b'asdf'
self.assertIn('Content-Length', request.headers)
self.assertTrue(rpc.RPCJSONDeserializer().has_body(request))
@ -335,7 +346,7 @@ class TestRPCJSONDeserializer(test_utils.BaseTestCase):
def test_default_with_body(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = '{"key": "value"}'
request.body = b'{"key": "value"}'
actual = rpc.RPCJSONDeserializer().default(request)
expected = {"body": {"key": "value"}}
self.assertEqual(expected, actual)
@ -343,7 +354,7 @@ class TestRPCJSONDeserializer(test_utils.BaseTestCase):
def test_has_body_has_transfer_encoding(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'fake_body'
request.body = b'fake_body'
request.headers['transfer-encoding'] = ''
self.assertIn('transfer-encoding', request.headers)
self.assertTrue(rpc.RPCJSONDeserializer().has_body(request))

View File

@ -34,21 +34,21 @@ class CacheClientTestCase(utils.BaseTestCase):
"/cached_images/test_id")
def test_get_cached_images(self):
expected_data = '{"cached_images": "some_images"}'
expected_data = b'{"cached_images": "some_images"}'
self.client.do_request.return_value = utils.FakeHTTPResponse(
data=expected_data)
self.assertEqual("some_images", self.client.get_cached_images())
self.client.do_request.assert_called_with("GET", "/cached_images")
def test_get_queued_images(self):
expected_data = '{"queued_images": "some_images"}'
expected_data = b'{"queued_images": "some_images"}'
self.client.do_request.return_value = utils.FakeHTTPResponse(
data=expected_data)
self.assertEqual("some_images", self.client.get_queued_images())
self.client.do_request.assert_called_with("GET", "/queued_images")
def test_delete_all_cached_images(self):
expected_data = '{"num_deleted": 4}'
expected_data = b'{"num_deleted": 4}'
self.client.do_request.return_value = utils.FakeHTTPResponse(
data=expected_data)
self.assertEqual(4, self.client.delete_all_cached_images())
@ -67,7 +67,7 @@ class CacheClientTestCase(utils.BaseTestCase):
"/queued_images/test_id")
def test_delete_all_queued_images(self):
expected_data = '{"num_deleted": 4}'
expected_data = b'{"num_deleted": 4}'
self.client.do_request.return_value = utils.FakeHTTPResponse(
data=expected_data)
self.assertEqual(4, self.client.delete_all_queued_images())

View File

@ -570,8 +570,8 @@ class FakeAuthMiddleware(wsgi.Middleware):
class FakeHTTPResponse(object):
def __init__(self, status=200, headers=None, data=None, *args, **kwargs):
data = data or 'I am a teapot, short and stout\n'
self.data = six.StringIO(data)
data = data or b'I am a teapot, short and stout\n'
self.data = six.BytesIO(data)
self.read = self.data.read
self.status = status
self.headers = headers or {'content-length': len(data)}

View File

@ -23,6 +23,7 @@ commands =
glance.tests.unit.common.test_config \
glance.tests.unit.common.test_exception \
glance.tests.unit.common.test_property_utils \
glance.tests.unit.common.test_rpc \
glance.tests.unit.common.test_scripts \
glance.tests.unit.common.test_semver \
glance.tests.unit.common.test_swift_store_utils \