From ba64e8294797ada17027fda1329d38eee5c36899 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 3 Jul 2017 12:07:40 +0800 Subject: [PATCH] create HttpSession as wrapper of requests.Session, in order to log more information of request and response --- ate/client.py | 149 ++++++++++++++++++++++++++++++++++++++++++++ ate/context.py | 3 +- ate/runner.py | 5 +- test/test_client.py | 34 ++++++++++ 4 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 ate/client.py create mode 100644 test/test_client.py diff --git a/ate/client.py b/ate/client.py new file mode 100644 index 00000000..8004e57c --- /dev/null +++ b/ate/client.py @@ -0,0 +1,149 @@ +import logging +import re +import time + +import requests +from requests import Request, Response +from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema, + RequestException) + +from ate.exception import ParamsError + +log_level = getattr(logging, "INFO") +logging.basicConfig(level=log_level) +absolute_http_url_regexp = re.compile(r"^https?://", re.I) + + +class ApiResponse(Response): + + def raise_for_status(self): + if hasattr(self, 'error') and self.error: + raise self.error + Response.raise_for_status(self) + + +class HttpSession(requests.Session): + """ + Class for performing HTTP requests and holding (session-) cookies between requests (in order + to be able to log in and out of websites). Each request is logged so that ApiTestEngine can + display statistics. + + This is a slightly extended version of `python-request `_'s + :py:class:`requests.Session` class and mostly this class works exactly the same. However + the methods for making requests (get, post, delete, put, head, options, patch, request) + can now take a *url* argument that's only the path part of the URL, in which case the host + part of the URL will be prepended with the HttpSession.base_url which is normally inherited + from a ApiTestEngine class' host property. + """ + def __init__(self, base_url=None, *args, **kwargs): + super(HttpSession, self).__init__(*args, **kwargs) + self.base_url = base_url if base_url else "" + + def _build_url(self, path): + """ prepend url with hostname unless it's already an absolute URL """ + if absolute_http_url_regexp.match(path): + return path + elif self.base_url: + return "%s%s" % (self.base_url, path) + else: + raise ParamsError("base url missed!") + + def request(self, method, url, **kwargs): + """ + Constructs and sends a :py:class:`requests.Request`. + Returns :py:class:`requests.Response` object. + + :param method: + method for the new :class:`Request` object. + :param url: + URL for the new :class:`Request` object. + :param params: (optional) + Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param data: (optional) + Dictionary or bytes to send in the body of the :class:`Request`. + :param headers: (optional) + Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) + Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) + Dictionary of ``'filename': file-like-objects`` for multipart encoding upload. + :param auth: (optional) + Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) + How long to wait for the server to send data before giving up, as a float, or \ + a (`connect timeout, read timeout `_) tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) + Set to True by default. + :type allow_redirects: bool + :param proxies: (optional) + Dictionary mapping protocol to the URL of the proxy. + :param stream: (optional) + whether to immediately download the response content. Defaults to ``False``. + :param verify: (optional) + if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. + :param cert: (optional) + if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + """ + + # prepend url with hostname unless it's already an absolute URL + url = self._build_url(url) + logging.info(" Start to {method} {url}".format(method=method, url=url)) + logging.debug(" kwargs: {kwargs}".format(kwargs=kwargs)) + # store meta data that is used when reporting the request to locust's statistics + request_meta = {} + + # set up pre_request hook for attaching meta data to the request object + request_meta["method"] = method + request_meta["start_time"] = time.time() + + response = self._send_request_safe_mode(method, url, **kwargs) + request_meta["url"] = (response.history and response.history[0] or response)\ + .request.path_url + + # record the consumed time + request_meta["response_time"] = int((time.time() - request_meta["start_time"]) * 1000) + + # get the length of the content, but if the argument stream is set to True, we take + # the size from the content-length header, in order to not trigger fetching of the body + if kwargs.get("stream", False): + request_meta["content_size"] = int(response.headers.get("content-length") or 0) + else: + request_meta["content_size"] = len(response.content or "") + + request_meta["request_headers"] = response.request.headers + request_meta["request_body"] = response.request.body + request_meta["status_code"] = response.status_code + request_meta["response_headers"] = response.headers + request_meta["response_content"] = response.content + + logging.debug(" response: {response}".format(response=request_meta)) + + try: + response.raise_for_status() + except RequestException as e: + logging.error(" Failed to {method} {url}! exception msg: {exception}".format( + method=method, url=url, exception=str(e))) + else: + logging.info( + """ status_code: {}! response_time: {} ms, response_length: {} bytes"""\ + .format(request_meta["status_code"], request_meta["response_time"], \ + request_meta["content_size"])) + + return response + + def _send_request_safe_mode(self, method, url, **kwargs): + """ + Send a HTTP request, and catch any exception that might occur due to connection problems. + Safe mode has been removed from requests 1.x. + """ + try: + return requests.Session.request(self, method, url, **kwargs) + except (MissingSchema, InvalidSchema, InvalidURL): + raise + except RequestException as ex: + resp = ApiResponse() + resp.error = ex + resp.status_code = 0 # with this status_code, content returns None + resp.request = Request(method, url).prepare() + return resp diff --git a/ate/context.py b/ate/context.py index 977af7a0..042ad094 100644 --- a/ate/context.py +++ b/ate/context.py @@ -160,7 +160,8 @@ class Context(object): if "func" in data: # this is a function, e.g. {"func": "gen_random_string", "args": [5]} # function marker: "func" key in dict - # the function will be called, and its return value will be binded to the variable. + # the function will be called, and its return value will be binded + # to the testcase context variable. func_name = data['func'] args = self.get_eval_value(data.get('args', [])) kargs = self.get_eval_value(data.get('kargs', {})) diff --git a/ate/runner.py b/ate/runner.py index 08d6a003..9275c7d3 100644 --- a/ate/runner.py +++ b/ate/runner.py @@ -1,13 +1,12 @@ -import requests - from ate import exception, response +from ate.client import HttpSession from ate.context import Context class TestRunner(object): def __init__(self): - self.client = requests.Session() + self.client = HttpSession() self.context = Context() def init_context(self, config_dict, level): diff --git a/test/test_client.py b/test/test_client.py new file mode 100644 index 00000000..777351ab --- /dev/null +++ b/test/test_client.py @@ -0,0 +1,34 @@ +from ate.client import HttpSession +from test.base import ApiServerUnittest + +class TestHttpClient(ApiServerUnittest): + def setUp(self): + super(TestHttpClient, self).setUp() + self.host = "http://127.0.0.1:5000" + self.api_client = HttpSession(self.host) + self.clear_users() + + def tearDown(self): + super(TestHttpClient, self).tearDown() + + def clear_users(self): + url = "%s/api/users" % self.host + return self.api_client.delete(url) + + def create_user(self, uid, name, password): + url = "%s/api/users/%d" % (self.host, uid) + data = { + 'name': name, + 'password': password + } + return self.api_client.post(url, json=data) + + def test_create_user_not_existed(self): + resp = self.create_user(1000, 'user1', '123456') + self.assertEqual(201, resp.status_code) + self.assertEqual(True, resp.json()['success']) + + def test_create_user_existed(self): + resp = self.create_user(1000, 'user1', '123456') + resp = self.create_user(1000, 'user1', '123456') + self.assertEqual(500, resp.status_code)