From 626609f4cf23785b41960b800c1a9f5bc877b343 Mon Sep 17 00:00:00 2001 From: kgriffs Date: Fri, 31 Jan 2014 13:02:50 -0600 Subject: [PATCH] test(functional): Use direct WSGI requests in lieu of a wsgiref server The wsgiref server is very flaky when running in the gate, so let's just pass WSGI requests directly to the app for now, at look into adding a devstack job to the gate that will use a standalone server. Change-Id: Ifb6cd0c9080ded7ab93cced6e0d40a776eb2cd50 --- marconi/queues/transport/wsgi/app.py | 2 +- marconi/tests/functional/base.py | 37 +++++--- marconi/tests/functional/http.py | 126 +++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 13 deletions(-) diff --git a/marconi/queues/transport/wsgi/app.py b/marconi/queues/transport/wsgi/app.py index e2d952b95..ce37e6a21 100644 --- a/marconi/queues/transport/wsgi/app.py +++ b/marconi/queues/transport/wsgi/app.py @@ -18,7 +18,7 @@ This app should be used by external WSGI containers. For example: - $ gunicorn marconi.queues.transport.wsgi.public.app:app + $ gunicorn marconi.queues.transport.wsgi.app:app NOTE: As for external containers, it is necessary to put config files in the standard paths. There's diff --git a/marconi/tests/functional/base.py b/marconi/tests/functional/base.py index 9683412d1..a0b792586 100644 --- a/marconi/tests/functional/base.py +++ b/marconi/tests/functional/base.py @@ -17,20 +17,28 @@ import abc import jsonschema import multiprocessing - +import os from marconi.openstack.common import timeutils from marconi.queues import bootstrap -# NOTE(flaper87): This is necessary to register, +# TODO(flaper87): This is necessary to register, # wsgi configs and won't be permanent. It'll be # refactored as part of the work for this blueprint from marconi.queues.transport import validation from marconi.queues.transport import wsgi # noqa +from marconi.queues.transport.wsgi import app from marconi import tests as testing from marconi.tests.functional import config from marconi.tests.functional import helpers from marconi.tests.functional import http +# TODO(kgriffs): Run functional tests to a devstack gate job and +# set this using an environment variable or something. +# +# TODO(kgriffs): Find a more general way to do this; we seem to be +# using this environ flag pattern over and over againg. +_TEST_INTEGRATION = os.environ.get('MARCONI_TEST_INTEGRATION') is not None + class FunctionalTestBase(testing.TestBase): @@ -51,19 +59,24 @@ class FunctionalTestBase(testing.TestBase): self.mconf = self.load_conf(self.cfg.marconi.config) - # NOTE(flaper87): Use running instances. - if self.cfg.marconi.run_server: - if not (self.server and self.server.is_alive()): - # pylint: disable=not-callable - self.server = self.server_class() - self.server.start(self.mconf) - validator = validation.Validator(self.mconf) self.limits = validator._limits_conf - # NOTE(flaper87): Create client - # for this test unit. - self.client = http.Client() + if _TEST_INTEGRATION: + # TODO(kgriffs): This code should be replaced to use + # an external wsgi server instance. + + # NOTE(flaper87): Use running instances. + if self.cfg.marconi.run_server: + if not (self.server and self.server.is_alive()): + # pylint: disable=not-callable + self.server = self.server_class() + self.server.start(self.mconf) + + self.client = http.Client() + else: + self.client = http.WSGIClient(app.app) + self.headers = helpers.create_marconi_headers(self.cfg) if self.cfg.auth.auth_on: diff --git a/marconi/tests/functional/http.py b/marconi/tests/functional/http.py index a779c293c..dff6626f9 100755 --- a/marconi/tests/functional/http.py +++ b/marconi/tests/functional/http.py @@ -16,7 +16,9 @@ import functools import json +from falcon import testing as ftest import requests +import six def _build_url(method): @@ -38,6 +40,7 @@ def _build_url(method): class Client(object): def __init__(self): + # NOTE(kgriffs): used by @_build_url self.base_url = None self.session = requests.session() @@ -86,3 +89,126 @@ class Client(object): if "data" in kwargs: kwargs['data'] = json.dumps(kwargs["data"]) return self.session.patch(url, **kwargs) + + +class ResponseMock(object): + """Mocks part of the Requests library's Response object.""" + + def __init__(self, srmock, wsgi_result): + self.status_code = int(srmock.status.partition(' ')[0]) + self._body = wsgi_result[0] if wsgi_result else '' + self.headers = srmock.headers_dict + + def json(self): + return json.loads(self._body, encoding='utf-8') + + +class WSGIClient(object): + """Same inteface as Client, but speaks directly to a WSGI callable.""" + + def __init__(self, app): + # NOTE(kgriffs): used by @_build_url + self.base_url = None + + self.app = app + self.headers = {} + + @staticmethod + def _sanitize_headers(headers): + # NOTE(kgriffs): Workaround for a little create_environ bug + return dict([(key, '' if value is None else value) + for key, value in headers.items()]) + + def _simulate_request(self, url, method='GET', data=None, + headers=None, params=None): + """Simulate a request. + + Simulates a WSGI request to the API for testing. + + :param url: Request path for the desired resource + :param method: (Default 'GET') The HTTP method to send + :param data: (Default None) A dict that will be serialized + to JSON and submitted as the body of the request. May + also be a pre-serialized string. + :param headers: (Default None) A dict containing + extra HTTP headers to send. + :param params: (Default None) A dict of parameters + to use in the query string for the request. + + :returns: a requests response instance + """ + + if headers is None: + headers = self.headers + + headers = self._sanitize_headers(headers) + + if data is None: + body = '' + elif isinstance(data, str) or isinstance(data, six.text_type): + body = data + else: + body = json.dumps(data, ensure_ascii=False) + + parsed_url = six.moves.urllib_parse.urlparse(url) + + query = parsed_url.query + + if params is not None: + extra = '&'.join([key + '=' + str(value) + for key, value in params.items()]) + + query += '&' + extra + + environ = ftest.create_environ(method=method, + path=parsed_url.path, + query_string=query, + headers=headers, + body=body) + + srmock = ftest.StartResponseMock() + wsgi_result = self.app(environ, srmock) + + return ResponseMock(srmock, wsgi_result) + + def set_base_url(self, base_url): + self.base_url = base_url + + def set_headers(self, headers): + self.headers.update(headers) + + @_build_url + def get(self, url=None, **kwargs): + """Simulate a GET request.""" + kwargs['method'] = 'GET' + return self._simulate_request(url=url, **kwargs) + + @_build_url + def head(self, url=None, **kwargs): + """Simulate a HEAD request.""" + kwargs['method'] = 'HEAD' + return self._simulate_request(url=url, **kwargs) + + @_build_url + def post(self, url=None, **kwargs): + """Simulate a POST request.""" + kwargs['method'] = 'POST' + return self._simulate_request(url=url, **kwargs) + + @_build_url + def put(self, url=None, **kwargs): + """Simulate a PUT request.""" + kwargs['method'] = 'PUT' + return self._simulate_request(url=url, **kwargs) + + @_build_url + def delete(self, url=None, **kwargs): + """Simulate a DELETE request.""" + kwargs['method'] = 'DELETE' + return self._simulate_request(url=url, **kwargs) + + @_build_url + def patch(self, url=None, **kwargs): + """Simulate a PATCH request.""" + kwargs['method'] = 'PATCH' + return self._simulate_request(url=url, **kwargs)