From 055b2d5ad057692828fbfed6b5cffb1f7ff9ea04 Mon Sep 17 00:00:00 2001 From: httprunner Date: Tue, 27 Nov 2018 17:55:49 +0800 Subject: [PATCH] fix html report for HttpRunner 2.0 --- httprunner/api.py | 2 +- httprunner/client.py | 1 - httprunner/report.py | 208 +++++++++++++++++----- httprunner/runner.py | 18 +- httprunner/templates/report_template.html | 34 ++-- tests/test_api.py | 2 +- 6 files changed, 197 insertions(+), 68 deletions(-) diff --git a/httprunner/api.py b/httprunner/api.py index daef4269..a9048750 100644 --- a/httprunner/api.py +++ b/httprunner/api.py @@ -50,7 +50,7 @@ class HttpRunner(object): except exceptions.MyBaseFailure as ex: self.fail(str(ex)) finally: - self.meta_data = test_runner.get_test_data() + self.meta_datas = test_runner.meta_datas if "config" in test_dict: # run nested testcase diff --git a/httprunner/client.py b/httprunner/client.py index e1798db9..d540fcb1 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -125,7 +125,6 @@ class HttpSession(requests.Session): # 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) - self.meta_data["request"]["body"] = response.request.body # log request details in debug mode log_print("request") diff --git a/httprunner/report.py b/httprunner/report.py index 15b4d12c..65dbd183 100644 --- a/httprunner/report.py +++ b/httprunner/report.py @@ -91,9 +91,169 @@ def stringify_summary(summary): suite_summary["name"] = "test suite {}".format(index) for record in suite_summary.get("records"): - meta_data = record['meta_data'] - stringify_data(meta_data, 'request') - stringify_data(meta_data, 'response') + meta_datas = record['meta_datas'] + __stringify_meta_datas(meta_datas) + meta_datas_expanded = [] + __expand_meta_datas(meta_datas, meta_datas_expanded) + record["meta_datas_expanded"] = meta_datas_expanded + record["response_time"] = __get_total_response_time(meta_datas_expanded) + + +def __stringify_request(request_data): + """ stringfy HTTP request data + + Args: + request_data (dict): HTTP request data in dict. + + { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.20.0", + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + "Connection": "keep-alive", + "user_agent": "iOS/10.3", + "device_sn": "TESTCASE_CREATE_XXX", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json", + "Content-Length": "52" + }, + "start_timestamp": 1543299567.6505039, + "json": { + "sign": "cb9d60acd09080ea66c8e63a1c78c6459ea00168" + }, + "verify": false + } + + """ + for key, value in request_data.items(): + + if isinstance(value, list): + value = json.dumps(value, indent=2, ensure_ascii=False) + + elif isinstance(value, bytes): + try: + encoding = "utf-8" + value = escape(value.decode(encoding)) + except UnicodeDecodeError: + pass + + elif not isinstance(value, (basestring, numeric_types, Iterable)): + # class instance, e.g. MultipartEncoder() + value = repr(value) + + request_data[key] = value + + +def __stringify_response(response_data): + """ stringfy HTTP response data + + Args: + response_data (dict): + + { + "status_code": 404, + "headers": { + "Content-Type": "application/json", + "Content-Length": "30", + "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, + "url": "http://127.0.0.1:5000/api/users/9001", + "reason": "NOT FOUND", + "cookies": {}, + "json": { + "success": false, + "data": {} + } + } + + """ + for key, value in response_data.items(): + + if isinstance(value, list): + value = json.dumps(value, indent=2, ensure_ascii=False) + + elif isinstance(value, bytes): + try: + encoding = response_data.get("encoding") + if not encoding or encoding == "None": + encoding = "utf-8" + + if key == "content" and "image" in response_data["content_type"]: + # display image + value = "data:{};base64,{}".format( + response_data["content_type"], + b64encode(value).decode(encoding) + ) + else: + value = escape(value.decode(encoding)) + except UnicodeDecodeError: + pass + + elif not isinstance(value, (basestring, numeric_types, Iterable)): + # class instance, e.g. MultipartEncoder() + value = repr(value) + + response_data[key] = value + + +def __expand_meta_datas(meta_datas, meta_datas_expanded): + """ expand meta_datas to one level + + Args: + meta_datas (dict/list): maybe in nested format + + Returns: + list: expanded list in one level + + Examples: + >>> meta_datas = [ + [ + dict1, + dict2 + ], + dict3 + ] + >>> meta_datas_expanded = [] + >>> __expand_meta_datas(meta_datas, meta_datas_expanded) + >>> print(meta_datas_expanded) + [dict1, dict2, dict3] + + """ + if isinstance(meta_datas, dict): + meta_datas_expanded.append(meta_datas) + elif isinstance(meta_datas, list): + for meta_data in meta_datas: + __expand_meta_datas(meta_data, meta_datas_expanded) + + +def __get_total_response_time(meta_datas_expanded): + """ caculate total response time of all meta_datas + """ + response_time = 0 + for meta_data in meta_datas_expanded: + response_time += meta_data["response"]["response_time_ms"] + + return "{:.2f}".format(response_time) + + +def __stringify_meta_datas(meta_datas): + + if isinstance(meta_datas, list): + 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"]) def render_html_report(summary, report_template=None, report_dir=None): @@ -136,46 +296,6 @@ def render_html_report(summary, report_template=None, report_dir=None): return report_path -def stringify_data(meta_data, request_or_response): - """ - meta_data = { - "request": {}, - "response": {} - } - """ - headers = meta_data[request_or_response]["headers"] - request_or_response_dict = meta_data[request_or_response] - - for key, value in request_or_response_dict.items(): - - if isinstance(value, list): - value = json.dumps(value, indent=2, ensure_ascii=False) - - elif isinstance(value, bytes): - try: - encoding = meta_data["response"].get("encoding") - if not encoding or encoding == "None": - encoding = "utf-8" - - if request_or_response == "response" and key == "content" \ - and "image" in meta_data["response"]["content_type"]: - # display image - value = "data:{};base64,{}".format( - meta_data["response"]["content_type"], - b64encode(value).decode(encoding) - ) - else: - value = escape(value.decode(encoding)) - except UnicodeDecodeError: - pass - - elif not isinstance(value, (basestring, numeric_types, Iterable)): - # class instance, e.g. MultipartEncoder() - value = repr(value) - - meta_data[request_or_response][key] = value - - class HtmlTestResult(unittest.TextTestResult): """ A html result class that can generate formatted html results. Used by TextTestRunner. @@ -189,7 +309,7 @@ class HtmlTestResult(unittest.TextTestResult): 'name': test.shortDescription(), 'status': status, 'attachment': attachment, - "meta_data": test.meta_data + "meta_datas": test.meta_datas } self.records.append(data) diff --git a/httprunner/runner.py b/httprunner/runner.py index 6ea969c3..9fef5302 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -68,7 +68,7 @@ class Runner(object): if self.testcase_teardown_hooks: self.do_hook_actions(self.testcase_teardown_hooks, "teardown") - def clear_test_data(self): + def __clear_test_data(self): """ clear request and response data """ if not isinstance(self.http_client_session, HttpSession): @@ -77,11 +77,11 @@ class Runner(object): self.evaluated_validators = [] self.http_client_session.init_meta_data() - def get_test_data(self): + def __get_test_data(self): """ get request/response data and validate results """ if not isinstance(self.http_client_session, HttpSession): - raise exceptions.FunctionNotFound("get_test_data is only valid in HttpSession!") + return meta_data = self.http_client_session.meta_data meta_data["validators"] = self.evaluated_validators @@ -167,7 +167,7 @@ class Runner(object): "authorization": "$authorization", "random": "$random" }, - "body": '{"name": "user", "password": "123456"}' + "json": {"name": "user", "password": "123456"} }, "extract": [], # optional "validate": [], # optional @@ -182,7 +182,7 @@ class Runner(object): """ # clear meta data first to ensure independence for each test - self.clear_test_data() + self.__clear_test_data() # check skip self._handle_skip_feature(test_dict) @@ -266,14 +266,18 @@ class Runner(object): def _run_testcase(self, testcase_dict): """ run single testcase. """ + meta_data_list = [] config = testcase_dict.get("config", {}) test_runner = Runner(config, self.functions, self.http_client_session) tests = testcase_dict.get("tests", []) for index, test_dict in enumerate(tests): test_runner.run_test(test_dict) + meta_datas = test_runner.meta_datas + meta_data_list.append(meta_datas) self.session_context.update_seesion_variables(test_runner.extract_sessions()) + return meta_data_list def run_test(self, test_dict): """ run single teststep of testcase. @@ -308,12 +312,14 @@ class Runner(object): } """ + self.meta_datas = None if "config" in test_dict: # nested testcase - self._run_testcase(test_dict) + self.meta_datas = self._run_testcase(test_dict) else: # api self._run_test(test_dict) + self.meta_datas = self.__get_test_data() def extract_sessions(self): """ diff --git a/httprunner/templates/report_template.html b/httprunner/templates/report_template.html index 8b68a425..9b53e5fd 100644 --- a/httprunner/templates/report_template.html +++ b/httprunner/templates/report_template.html @@ -233,28 +233,31 @@ {% for record in test_suite_summary.records %} {% set record_index = "{}_{}".format(suite_index, loop.index) %} + {% set record_meta_datas = record.meta_datas_expanded %} {{record.status}} {{record.name}} - {{ record.meta_data.response.response_time_ms }} ms + {{ record.response_time }} ms - log -