From 05d2cc014df20ded59e18727c72cbe9ad5110bd0 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 20 Dec 2018 17:59:11 +0800 Subject: [PATCH] enhance report: display all 30X redirect request and response --- httprunner/client.py | 167 +++++++++++++--------- httprunner/report.py | 12 +- httprunner/runner.py | 2 + httprunner/templates/report_template.html | 34 +++-- tests/httpbin/api/302_redirect.yml | 10 ++ tests/test_api.py | 18 ++- 6 files changed, 151 insertions(+), 92 deletions(-) create mode 100644 tests/httpbin/api/302_redirect.yml diff --git a/httprunner/client.py b/httprunner/client.py index 8a420550..de060ff3 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -44,23 +44,84 @@ class HttpSession(requests.Session): """ self.meta_data = { "name": "", - "request": { - "url": "N/A", - "method": "N/A", - "headers": {}, - "start_timestamp": None - }, - "response": { - "status_code": "N/A", - "headers": {}, + "data": [ + { + "request": { + "url": "N/A", + "method": "N/A", + "headers": {} + }, + "response": { + "status_code": "N/A", + "headers": {}, + "encoding": None, + "content_type": "" + } + } + ], + "stat": { "content_size": "N/A", "response_time_ms": "N/A", "elapsed_ms": "N/A", - "encoding": None, - "content_type": "" } } + def get_req_resp_record(self, resp_obj): + """ get request and response info from Response() object. + """ + def log_print(req_resp_dict, r_type): + msg = "\n================== {} details ==================\n".format(r_type) + for key, value in req_resp_dict[r_type].items(): + msg += "{:<16} : {}\n".format(key, repr(value)) + logger.log_debug(msg) + + req_resp_dict = { + "request": {}, + "response": {} + } + + # record actual request info + req_resp_dict["request"]["url"] = resp_obj.request.url + req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers) + + request_body = resp_obj.request.body + if request_body: + req_resp_dict["request"]["body"] = omit_long_data(request_body) + + # log request details in debug mode + log_print(req_resp_dict, "request") + + # record response info + req_resp_dict["response"]["ok"] = resp_obj.ok + req_resp_dict["response"]["url"] = resp_obj.url + req_resp_dict["response"]["status_code"] = resp_obj.status_code + req_resp_dict["response"]["reason"] = resp_obj.reason + req_resp_dict["response"]["cookies"] = resp_obj.cookies or {} + req_resp_dict["response"]["encoding"] = resp_obj.encoding + resp_headers = dict(resp_obj.headers) + req_resp_dict["response"]["headers"] = resp_headers + + lower_resp_headers = lower_dict_keys(resp_headers) + content_type = lower_resp_headers.get("content-type", "") + req_resp_dict["response"]["content_type"] = content_type + + if "image" in content_type: + # response is image type, record bytes content only + req_resp_dict["response"]["content"] = resp_obj.content + else: + try: + # try to record json data + req_resp_dict["response"]["json"] = resp_obj.json() + except ValueError: + # only record at most 512 text charactors + resp_text = resp_obj.text + req_resp_dict["response"]["text"] = omit_long_data(resp_text) + + # log response details in debug mode + log_print(req_resp_dict, "response") + + return req_resp_dict + def request(self, method, url, name=None, **kwargs): """ Constructs and sends a :py:class:`requests.Request`. @@ -100,78 +161,42 @@ class HttpSession(requests.Session): :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. """ - def log_print(request_response): - msg = "\n================== {} details ==================\n".format(request_response) - for key, value in self.meta_data[request_response].items(): - msg += "{:<16} : {}\n".format(key, repr(value)) - logger.log_debug(msg) - # record test name self.meta_data["name"] = name # record original request info - self.meta_data["request"]["method"] = method - self.meta_data["request"]["url"] = url - self.meta_data["request"].update(kwargs) - self.meta_data["request"]["start_timestamp"] = time.time() - - request_data = self.meta_data["request"].get("data") - if request_data: - self.meta_data["request"]["data"] = omit_long_data(request_data) + self.meta_data["data"][0]["request"]["method"] = method + self.meta_data["data"][0]["request"]["url"] = url + kwargs.setdefault("timeout", 120) + self.meta_data["data"][0]["request"].update(kwargs) # prepend url with hostname unless it's already an absolute URL url = build_url(self.base_url, url) - kwargs.setdefault("timeout", 120) + start_timestamp = time.time() response = self._send_request_safe_mode(method, url, **kwargs) - - # record the consumed time - self.meta_data["response"]["response_time_ms"] = \ - round((time.time() - self.meta_data["request"]["start_timestamp"]) * 1000, 2) - self.meta_data["response"]["elapsed_ms"] = response.elapsed.microseconds / 1000.0 - - # record actual request info - self.meta_data["request"]["url"] = (response.history and response.history[0] or response).request.url - self.meta_data["request"]["headers"] = dict(response.request.headers) - - # log request details in debug mode - log_print("request") - - # record response info - self.meta_data["response"]["ok"] = response.ok - self.meta_data["response"]["url"] = response.url - self.meta_data["response"]["status_code"] = response.status_code - self.meta_data["response"]["reason"] = response.reason - self.meta_data["response"]["cookies"] = response.cookies or {} - self.meta_data["response"]["encoding"] = response.encoding - resp_headers = dict(response.headers) - self.meta_data["response"]["headers"] = resp_headers - - lower_resp_headers = lower_dict_keys(resp_headers) - content_type = lower_resp_headers.get("content-type", "") - self.meta_data["response"]["content_type"] = content_type - - if "image" in content_type: - # response is image type, record bytes content only - self.meta_data["response"]["content"] = response.content - else: - try: - # try to record json data - self.meta_data["response"]["json"] = response.json() - except ValueError: - # only record at most 512 text charactors - resp_text = response.text - self.meta_data["response"]["text"] = omit_long_data(resp_text) + response_time_ms = round((time.time() - start_timestamp) * 1000, 2) # 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): - self.meta_data["response"]["content_size"] = int(self.meta_data["response"]["headers"].get("content-length") or 0) + content_size = int(dict(response.headers).get("content-length") or 0) else: - self.meta_data["response"]["content_size"] = len(response.content or "") + content_size = len(response.content or "") - # log response details in debug mode - log_print("response") + # record the consumed time + self.meta_data["stat"] = { + "response_time_ms": response_time_ms, + "elapsed_ms": response.elapsed.microseconds / 1000.0, + "content_size": content_size + } + + # record request and response histories, include 30X redirection + response_list = response.history + [response] + self.meta_data["data"] = [ + self.get_req_resp_record(resp_obj) + for resp_obj in response_list + ] try: response.raise_for_status() @@ -180,9 +205,9 @@ class HttpSession(requests.Session): else: logger.log_info( """status_code: {}, response_time(ms): {} ms, response_length: {} bytes\n""".format( - self.meta_data["response"]["status_code"], - self.meta_data["response"]["response_time_ms"], - self.meta_data["response"]["content_size"] + response.status_code, + response_time_ms, + content_size ) ) diff --git a/httprunner/report.py b/httprunner/report.py index 8c12f8ad..bf448e5a 100644 --- a/httprunner/report.py +++ b/httprunner/report.py @@ -117,7 +117,6 @@ def __stringify_request(request_data): "Content-Type": "application/json", "Content-Length": "52" }, - "start_timestamp": 1543299567.6505039, "json": { "sign": "cb9d60acd09080ea66c8e63a1c78c6459ea00168" }, @@ -161,9 +160,6 @@ def __stringify_response(response_data): "Server": "Werkzeug/0.14.1 Python/3.7.0", "Date": "Tue, 27 Nov 2018 06:19:27 GMT" }, - "content_size": 30, - "response_time_ms": 3.63, - "elapsed_ms": 2.197, "encoding": "None", "content_type": "application/json", "ok": false, @@ -245,7 +241,7 @@ def __get_total_response_time(meta_datas_expanded): try: response_time = 0 for meta_data in meta_datas_expanded: - response_time += meta_data["response"]["response_time_ms"] + response_time += meta_data["stat"]["response_time_ms"] return "{:.2f}".format(response_time) @@ -260,8 +256,10 @@ def __stringify_meta_datas(meta_datas): for _meta_data in meta_datas: __stringify_meta_datas(_meta_data) elif isinstance(meta_datas, dict): - __stringify_request(meta_datas["request"]) - __stringify_response(meta_datas["response"]) + data_list = meta_datas["data"] + for data in data_list: + __stringify_request(data["request"]) + __stringify_response(data["response"]) def render_html_report(summary, report_template=None, report_dir=None): diff --git a/httprunner/runner.py b/httprunner/runner.py index c815efda..3b5ca787 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -256,6 +256,8 @@ class Runner(object): # log request err_msg += "====== request details ======\n" + err_msg += "url: {}\n".format(url) + err_msg += "method: {}\n".format(method) err_msg += "headers: {}\n".format(parsed_test_request.pop("headers", {})) for k, v in parsed_test_request.items(): v = utils.omit_long_data(v) diff --git a/httprunner/templates/report_template.html b/httprunner/templates/report_template.html index 6cffaa49..8e4414d1 100644 --- a/httprunner/templates/report_template.html +++ b/httprunner/templates/report_template.html @@ -132,6 +132,9 @@ overflow: auto; text-align: left; } + .popup .separator { + color:royalblue + } @media screen and (max-width: 700px) { .box { @@ -224,15 +227,22 @@

Name: {{ meta_data.name }}

+ + {% for req_resp in meta_data.data %} + + {% if loop.index > 1 %} +
==================================== redirect to ====================================
+ {% endif %} +

Request:

- {% for key, value in meta_data.request.items() %} + {% for key, value in req_resp.request.items() %}
{{key}} {% if key == "headers" %} - {% for header_key, header_value in meta_data.request.headers.items() %} + {% for header_key, header_value in req_resp.request.headers.items() %}
{{ header_key }}: {{ header_value }}
@@ -249,27 +259,24 @@

Response:

- {% for key, value in meta_data.response.items() %} - {% if key in ["elapsed_ms", "response_time_ms", "content_size", "content_type"] %} - {% continue %} - {% endif %} + {% for key, value in req_resp.response.items() %}
{{key}} {% if key == "headers" %} - {% for header_key, header_value in meta_data.response.headers.items() %} + {% for header_key, header_value in req_resp.response.headers.items() %}
{{ header_key }}: {{ header_value }}
{% endfor %} {% elif key == "content" %} - {% if "image" in meta_data.response.content_type %} - + {% if "image" in req_resp.response.content_type %} + {% else %} {{ value }} {% endif %} {% elif key == "text" %} -
{{ meta_data.response.text | e }}
+
{{ req_resp.response.text | e }}
{% else %} {{ value }} {% endif %} @@ -278,6 +285,7 @@ {% endfor %}
+ {% endfor %}

Validators:

@@ -312,15 +320,15 @@ - + - + - +
content_size(bytes){{ meta_data.response.content_size }}{{ meta_data.stat.content_size }}
response_time(ms){{ meta_data.response.response_time_ms }}{{ meta_data.stat.response_time_ms }}
elapsed(ms){{ meta_data.response.elapsed_ms }}{{ meta_data.stat.elapsed_ms }}
diff --git a/tests/httpbin/api/302_redirect.yml b/tests/httpbin/api/302_redirect.yml new file mode 100644 index 00000000..e61eb3b3 --- /dev/null +++ b/tests/httpbin/api/302_redirect.yml @@ -0,0 +1,10 @@ + +name: 302 redirect +request: + url: https://httpbin.org/redirect-to?url=https%3A%2F%2Fdebugtalk.com&status_code=302 + # params: + # url: https%3A%2F%2Fdebugtalk.com + # status_code: 302 + method: GET +validate: + - eq: ["status_code", 200] diff --git a/tests/test_api.py b/tests/test_api.py index 4de36228..c5726baa 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -143,7 +143,10 @@ class TestHttpRunner(ApiServerUnittest): summary = self.runner.summary self.assertTrue(summary["success"]) self.assertEqual(summary["stat"]["testsRun"], 1) - self.assertEqual(summary["details"][0]["records"][0]["meta_datas"]["response"]["json"]["data"], "abc") + self.assertEqual( + summary["details"][0]["records"][0]["meta_datas"]["data"][0]["response"]["json"]["data"], + "abc" + ) def test_html_report_repsonse_image(self): report_save_dir = os.path.join(os.getcwd(), 'reports', "demo") @@ -286,6 +289,19 @@ class TestHttpRunner(ApiServerUnittest): self.assertEqual(summary["stat"]["testsRun"], 1) self.assertEqual(summary["stat"]["successes"], 1) + def test_request_302_logs(self): + path = "tests/httpbin/api/302_redirect.yml" + self.runner.run(path) + summary = self.runner.summary + self.assertTrue(summary["success"]) + self.assertEqual(summary["stat"]["testsRun"], 1) + self.assertEqual(summary["stat"]["successes"], 1) + + req_resp_data = summary["details"][0]["records"][0]["meta_datas"]["data"] + self.assertEqual(len(req_resp_data), 2) + self.assertEqual(req_resp_data[0]["response"]["status_code"], 302) + self.assertEqual(req_resp_data[1]["response"]["status_code"], 200) + def test_run_testcase_hardcode(self): for testcase_file_path in self.testcase_file_path_list: self.runner.run(testcase_file_path)