mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-09 22:44:05 +08:00
enhance report: display all 30X redirect request and response
This commit is contained in:
@@ -44,23 +44,84 @@ class HttpSession(requests.Session):
|
|||||||
"""
|
"""
|
||||||
self.meta_data = {
|
self.meta_data = {
|
||||||
"name": "",
|
"name": "",
|
||||||
"request": {
|
"data": [
|
||||||
"url": "N/A",
|
{
|
||||||
"method": "N/A",
|
"request": {
|
||||||
"headers": {},
|
"url": "N/A",
|
||||||
"start_timestamp": None
|
"method": "N/A",
|
||||||
},
|
"headers": {}
|
||||||
"response": {
|
},
|
||||||
"status_code": "N/A",
|
"response": {
|
||||||
"headers": {},
|
"status_code": "N/A",
|
||||||
|
"headers": {},
|
||||||
|
"encoding": None,
|
||||||
|
"content_type": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stat": {
|
||||||
"content_size": "N/A",
|
"content_size": "N/A",
|
||||||
"response_time_ms": "N/A",
|
"response_time_ms": "N/A",
|
||||||
"elapsed_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):
|
def request(self, method, url, name=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Constructs and sends a :py:class:`requests.Request`.
|
Constructs and sends a :py:class:`requests.Request`.
|
||||||
@@ -100,78 +161,42 @@ class HttpSession(requests.Session):
|
|||||||
:param cert: (optional)
|
:param cert: (optional)
|
||||||
if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
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
|
# record test name
|
||||||
self.meta_data["name"] = name
|
self.meta_data["name"] = name
|
||||||
|
|
||||||
# record original request info
|
# record original request info
|
||||||
self.meta_data["request"]["method"] = method
|
self.meta_data["data"][0]["request"]["method"] = method
|
||||||
self.meta_data["request"]["url"] = url
|
self.meta_data["data"][0]["request"]["url"] = url
|
||||||
self.meta_data["request"].update(kwargs)
|
kwargs.setdefault("timeout", 120)
|
||||||
self.meta_data["request"]["start_timestamp"] = time.time()
|
self.meta_data["data"][0]["request"].update(kwargs)
|
||||||
|
|
||||||
request_data = self.meta_data["request"].get("data")
|
|
||||||
if request_data:
|
|
||||||
self.meta_data["request"]["data"] = omit_long_data(request_data)
|
|
||||||
|
|
||||||
# prepend url with hostname unless it's already an absolute URL
|
# prepend url with hostname unless it's already an absolute URL
|
||||||
url = build_url(self.base_url, 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)
|
response = self._send_request_safe_mode(method, url, **kwargs)
|
||||||
|
response_time_ms = round((time.time() - start_timestamp) * 1000, 2)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# get the length of the content, but if the argument stream is set to True, we take
|
# 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
|
# the size from the content-length header, in order to not trigger fetching of the body
|
||||||
if kwargs.get("stream", False):
|
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:
|
else:
|
||||||
self.meta_data["response"]["content_size"] = len(response.content or "")
|
content_size = len(response.content or "")
|
||||||
|
|
||||||
# log response details in debug mode
|
# record the consumed time
|
||||||
log_print("response")
|
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:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -180,9 +205,9 @@ class HttpSession(requests.Session):
|
|||||||
else:
|
else:
|
||||||
logger.log_info(
|
logger.log_info(
|
||||||
"""status_code: {}, response_time(ms): {} ms, response_length: {} bytes\n""".format(
|
"""status_code: {}, response_time(ms): {} ms, response_length: {} bytes\n""".format(
|
||||||
self.meta_data["response"]["status_code"],
|
response.status_code,
|
||||||
self.meta_data["response"]["response_time_ms"],
|
response_time_ms,
|
||||||
self.meta_data["response"]["content_size"]
|
content_size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ def __stringify_request(request_data):
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Content-Length": "52"
|
"Content-Length": "52"
|
||||||
},
|
},
|
||||||
"start_timestamp": 1543299567.6505039,
|
|
||||||
"json": {
|
"json": {
|
||||||
"sign": "cb9d60acd09080ea66c8e63a1c78c6459ea00168"
|
"sign": "cb9d60acd09080ea66c8e63a1c78c6459ea00168"
|
||||||
},
|
},
|
||||||
@@ -161,9 +160,6 @@ def __stringify_response(response_data):
|
|||||||
"Server": "Werkzeug/0.14.1 Python/3.7.0",
|
"Server": "Werkzeug/0.14.1 Python/3.7.0",
|
||||||
"Date": "Tue, 27 Nov 2018 06:19:27 GMT"
|
"Date": "Tue, 27 Nov 2018 06:19:27 GMT"
|
||||||
},
|
},
|
||||||
"content_size": 30,
|
|
||||||
"response_time_ms": 3.63,
|
|
||||||
"elapsed_ms": 2.197,
|
|
||||||
"encoding": "None",
|
"encoding": "None",
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
@@ -245,7 +241,7 @@ def __get_total_response_time(meta_datas_expanded):
|
|||||||
try:
|
try:
|
||||||
response_time = 0
|
response_time = 0
|
||||||
for meta_data in meta_datas_expanded:
|
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)
|
return "{:.2f}".format(response_time)
|
||||||
|
|
||||||
@@ -260,8 +256,10 @@ def __stringify_meta_datas(meta_datas):
|
|||||||
for _meta_data in meta_datas:
|
for _meta_data in meta_datas:
|
||||||
__stringify_meta_datas(_meta_data)
|
__stringify_meta_datas(_meta_data)
|
||||||
elif isinstance(meta_datas, dict):
|
elif isinstance(meta_datas, dict):
|
||||||
__stringify_request(meta_datas["request"])
|
data_list = meta_datas["data"]
|
||||||
__stringify_response(meta_datas["response"])
|
for data in data_list:
|
||||||
|
__stringify_request(data["request"])
|
||||||
|
__stringify_response(data["response"])
|
||||||
|
|
||||||
|
|
||||||
def render_html_report(summary, report_template=None, report_dir=None):
|
def render_html_report(summary, report_template=None, report_dir=None):
|
||||||
|
|||||||
@@ -256,6 +256,8 @@ class Runner(object):
|
|||||||
|
|
||||||
# log request
|
# log request
|
||||||
err_msg += "====== request details ======\n"
|
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", {}))
|
err_msg += "headers: {}\n".format(parsed_test_request.pop("headers", {}))
|
||||||
for k, v in parsed_test_request.items():
|
for k, v in parsed_test_request.items():
|
||||||
v = utils.omit_long_data(v)
|
v = utils.omit_long_data(v)
|
||||||
|
|||||||
@@ -132,6 +132,9 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.popup .separator {
|
||||||
|
color:royalblue
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 700px) {
|
@media screen and (max-width: 700px) {
|
||||||
.box {
|
.box {
|
||||||
@@ -224,15 +227,22 @@
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h3>Name: {{ meta_data.name }}</h3>
|
<h3>Name: {{ meta_data.name }}</h3>
|
||||||
|
|
||||||
|
{% for req_resp in meta_data.data %}
|
||||||
|
|
||||||
|
{% if loop.index > 1 %}
|
||||||
|
<div class="separator">==================================== redirect to ====================================</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3>Request:</h3>
|
<h3>Request:</h3>
|
||||||
<div style="overflow: auto">
|
<div style="overflow: auto">
|
||||||
<table>
|
<table>
|
||||||
{% for key, value in meta_data.request.items() %}
|
{% for key, value in req_resp.request.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{key}}</th>
|
<th>{{key}}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if key == "headers" %}
|
{% 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() %}
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ header_key }}</strong>: {{ header_value }}
|
<strong>{{ header_key }}</strong>: {{ header_value }}
|
||||||
</div>
|
</div>
|
||||||
@@ -249,27 +259,24 @@
|
|||||||
<h3>Response:</h3>
|
<h3>Response:</h3>
|
||||||
<div style="overflow: auto">
|
<div style="overflow: auto">
|
||||||
<table>
|
<table>
|
||||||
{% for key, value in meta_data.response.items() %}
|
{% for key, value in req_resp.response.items() %}
|
||||||
{% if key in ["elapsed_ms", "response_time_ms", "content_size", "content_type"] %}
|
|
||||||
{% continue %}
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{key}}</th>
|
<th>{{key}}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if key == "headers" %}
|
{% 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() %}
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ header_key }}</strong>: {{ header_value }}
|
<strong>{{ header_key }}</strong>: {{ header_value }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif key == "content" %}
|
{% elif key == "content" %}
|
||||||
{% if "image" in meta_data.response.content_type %}
|
{% if "image" in req_resp.response.content_type %}
|
||||||
<img src="{{ meta_data.response.content }}" />
|
<img src="{{ req_resp.response.content }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif key == "text" %}
|
{% elif key == "text" %}
|
||||||
<pre>{{ meta_data.response.text | e }}</pre>
|
<pre>{{ req_resp.response.text | e }}</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -278,6 +285,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<h3>Validators:</h3>
|
<h3>Validators:</h3>
|
||||||
<div style="overflow: auto">
|
<div style="overflow: auto">
|
||||||
@@ -312,15 +320,15 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>content_size(bytes)</th>
|
<th>content_size(bytes)</th>
|
||||||
<td>{{ meta_data.response.content_size }}</td>
|
<td>{{ meta_data.stat.content_size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>response_time(ms)</th>
|
<th>response_time(ms)</th>
|
||||||
<td>{{ meta_data.response.response_time_ms }}</td>
|
<td>{{ meta_data.stat.response_time_ms }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>elapsed(ms)</th>
|
<th>elapsed(ms)</th>
|
||||||
<td>{{ meta_data.response.elapsed_ms }}</td>
|
<td>{{ meta_data.stat.elapsed_ms }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
10
tests/httpbin/api/302_redirect.yml
Normal file
10
tests/httpbin/api/302_redirect.yml
Normal file
@@ -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]
|
||||||
@@ -143,7 +143,10 @@ class TestHttpRunner(ApiServerUnittest):
|
|||||||
summary = self.runner.summary
|
summary = self.runner.summary
|
||||||
self.assertTrue(summary["success"])
|
self.assertTrue(summary["success"])
|
||||||
self.assertEqual(summary["stat"]["testsRun"], 1)
|
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):
|
def test_html_report_repsonse_image(self):
|
||||||
report_save_dir = os.path.join(os.getcwd(), 'reports', "demo")
|
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"]["testsRun"], 1)
|
||||||
self.assertEqual(summary["stat"]["successes"], 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):
|
def test_run_testcase_hardcode(self):
|
||||||
for testcase_file_path in self.testcase_file_path_list:
|
for testcase_file_path in self.testcase_file_path_list:
|
||||||
self.runner.run(testcase_file_path)
|
self.runner.run(testcase_file_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user