Skip to content

Commit

Permalink
Merge pull request #122
Browse files Browse the repository at this point in the history
feat(httprunner): return extractors to report
  • Loading branch information
lihuacai168 authored Sep 23, 2023
2 parents f1b044d + eccc992 commit 932d628
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 59 deletions.
5 changes: 3 additions & 2 deletions httprunner/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ def test(self):
finally:
if hasattr(test_runner.http_client_session, "meta_data"):
self.meta_data = test_runner.http_client_session.meta_data
self.meta_data["validators"] = test_runner.evaluated_validators
self.meta_data["logs"] = test_runner.context.logs
self.meta_data["validators"]: list[dict] = test_runner.evaluated_validators
self.meta_data["logs"]: list[str] = test_runner.context.logs
self.meta_data["extractors"]: list[dict] = test_runner.context.extractors
# 赋值完之后,需要重新输出化http_client_session的meta数据,否则下次就会共享
test_runner.http_client_session.init_meta_data()

Expand Down
5 changes: 4 additions & 1 deletion httprunner/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ def init_meta_data(self):
"encoding": None,
"content": None,
"content_type": ""
}
},
"validators": [],
"logs": [],
"extractors": [],
}

def request(self, method, url, name=None, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion httprunner/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def __init__(self, variables=None, functions=None):
self.evaluated_validators = []
self.init_context_variables(level="testcase")

self.logs = []
self.logs: list[str] = []
self.extractors: list[dict] = []

def init_context_variables(self, level="testcase"):
""" initialize testcase/teststep context
Expand Down
134 changes: 98 additions & 36 deletions httprunner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from httprunner.compat import OrderedDict
from httprunner.context import Context

logger = logging.getLogger('httprunner')
logger = logging.getLogger("httprunner")


class ListHandler(logging.Handler):
def __init__(self, log_list: list):
Expand All @@ -26,13 +27,39 @@ def emit(self, record):
log_entry = self.format(record)
self.log_list.append(log_entry)


def _transform_to_list_of_dict(extractors: list[dict], extracted_variables_mapping: dict) -> list[dict]:
"""transform extractors to list of dict.
Args:
extractors (list): list of extractors
extracted_variables_mapping (dict): mapping between variable name and variable value
Returns:
list: list of dict
"""
if not extractors:
return []

result = []
for extractor in extractors:
for key, value in extractor.items():
extract_expr = value
actual_value = extracted_variables_mapping[key]
result.append({
'output_variable_name': key,
'extract_expr': extract_expr,
'actual_value': actual_value
})
return result


class Runner(object):
# 每个线程对应Runner类的实例
instances = {}

def __init__(self, config_dict=None, http_client_session=None):
"""
"""
self.http_client_session = http_client_session
config_dict = config_dict or {}
self.evaluated_validators = []
Expand Down Expand Up @@ -61,7 +88,7 @@ def __del__(self):
self.do_hook_actions(self.testcase_teardown_hooks)

def init_test(self, test_dict, level):
""" create/update context variables binds
"""create/update context variables binds
Args:
test_dict (dict):
Expand Down Expand Up @@ -100,11 +127,12 @@ def init_test(self, test_dict, level):
test_dict = utils.lower_test_dict_keys(test_dict)

self.context.init_context_variables(level)
variables = test_dict.get('variables') \
or test_dict.get('variable_binds', OrderedDict())
variables = test_dict.get("variables") or test_dict.get(
"variable_binds", OrderedDict()
)
self.context.update_context_variables(variables, level)

request_config = test_dict.get('request', {})
request_config = test_dict.get("request", {})
parsed_request = self.context.get_parsed_request(request_config, level)

base_url = parsed_request.pop("base_url", None)
Expand All @@ -113,7 +141,7 @@ def init_test(self, test_dict, level):
return parsed_request

def _handle_skip_feature(self, teststep_dict):
""" handle skip feature for teststep
"""handle skip feature for teststep
- skip: skip current test unconditionally
- skipIf: skip current test if condition is true
- skipUnless: skip current test unless condition is true
Expand Down Expand Up @@ -146,12 +174,12 @@ def _handle_skip_feature(self, teststep_dict):

def do_hook_actions(self, actions):
for action in actions:
logger.info("call hook: %s",action)
logger.info("call hook: %s", action)
# TODO: check hook function if valid
self.context.eval_content(action)

def run_test(self, teststep_dict):
""" run single teststep.
"""run single teststep.
Args:
teststep_dict (dict): teststep info
Expand Down Expand Up @@ -185,16 +213,21 @@ def run_test(self, teststep_dict):
all_logs: list[str] = []
list_handler = ListHandler(all_logs)
self.context.logs = []
self.context.extractors = []
try:
# 临时添加自定义处理器
# 临时添加自定义处理器
logger.addHandler(list_handler)

# check skip
self._handle_skip_feature(teststep_dict)

# prepare
extractors = teststep_dict.get("extract", []) or teststep_dict.get("extractors", [])
validators = teststep_dict.get("validate", []) or teststep_dict.get("validators", [])
extractors = teststep_dict.get("extract", []) or teststep_dict.get(
"extractors", []
)
validators = teststep_dict.get("validate", []) or teststep_dict.get(
"validators", []
)
parsed_request = self.init_test(teststep_dict, level="teststep")
self.context.update_teststep_variables_mapping("request", parsed_request)

Expand All @@ -207,39 +240,40 @@ def run_test(self, teststep_dict):
logger.info("execute setup hooks end")
# 计算前置setup_hooks消耗的时间
setup_hooks_duration = 0
self.http_client_session.meta_data['request']['setup_hooks_start'] = setup_hooks_start
self.http_client_session.meta_data["request"][
"setup_hooks_start"
] = setup_hooks_start
if len(setup_hooks) > 1:
setup_hooks_duration = time.time() - setup_hooks_start
self.http_client_session.meta_data['request']['setup_hooks_duration'] = setup_hooks_duration
self.http_client_session.meta_data["request"][
"setup_hooks_duration"
] = setup_hooks_duration

try:
url = parsed_request.pop('url')
method = parsed_request.pop('method')
url = parsed_request.pop("url")
method = parsed_request.pop("method")
group_name = parsed_request.pop("group", None)
except KeyError:
raise exceptions.ParamsError("URL or METHOD missed!")

# TODO: move method validation to json schema
valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
if method.upper() not in valid_methods:
err_msg = u"Invalid HTTP method! => {}\n".format(method)
err_msg = "Invalid HTTP method! => {}\n".format(method)
err_msg += "Available HTTP methods: {}".format("/".join(valid_methods))
logger.error(err_msg)
raise exceptions.ParamsError(err_msg)

logger.info("{method} {url}".format(method=method, url=url))
logger.debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request))

user_timeout: str = str(pydash.get(parsed_request, 'headers.timeout'))
user_timeout: str = str(pydash.get(parsed_request, "headers.timeout"))
if user_timeout and user_timeout.isdigit():
parsed_request['timeout'] = int(user_timeout)
parsed_request["timeout"] = int(user_timeout)

# request
resp = self.http_client_session.request(
method,
url,
name=group_name,
**parsed_request
method, url, name=group_name, **parsed_request
)
resp_obj = response.ResponseObject(resp)

Expand All @@ -250,22 +284,48 @@ def run_test(self, teststep_dict):
teardown_hooks_start = time.time()
if teardown_hooks:
logger.info("start to run teardown hooks")
logger.info("update_teststep_variables_mapping, response: %s", resp_obj.resp_obj.text)
logger.info(
"update_teststep_variables_mapping, response: %s",
resp_obj.resp_obj.text,
)
self.context.update_teststep_variables_mapping("response", resp_obj)
self.do_hook_actions(teardown_hooks)
teardown_hooks_duration = time.time() - teardown_hooks_start
logger.info("run teardown hooks end, duration: %s", teardown_hooks_duration)
self.http_client_session.meta_data['response']['teardown_hooks_start'] = teardown_hooks_start
self.http_client_session.meta_data['response']['teardown_hooks_duration'] = teardown_hooks_duration
logger.info(
"run teardown hooks end, duration: %s", teardown_hooks_duration
)
self.http_client_session.meta_data["response"][
"teardown_hooks_start"
] = teardown_hooks_start
self.http_client_session.meta_data["response"][
"teardown_hooks_duration"
] = teardown_hooks_duration

# extract
extracted_variables_mapping = resp_obj.extract_response(extractors, self.context)
self.context.update_testcase_runtime_variables_mapping(extracted_variables_mapping)
extracted_variables_mapping = resp_obj.extract_response(
extractors, self.context
)
self.context.extractors = _transform_to_list_of_dict(extractors, extracted_variables_mapping)
logger.info(
"source testcase_runtime_variables_mapping: %s",
dict(self.context.testcase_runtime_variables_mapping),
)
logger.info(
"source testcase_runtime_variables_mapping update with: %s",
dict(extracted_variables_mapping)
)
self.context.update_testcase_runtime_variables_mapping(
extracted_variables_mapping
)

# validate
try:
self.evaluated_validators = self.context.validate(validators, resp_obj)
except (exceptions.ParamsError, exceptions.ValidationFailure, exceptions.ExtractFailure):
except (
exceptions.ParamsError,
exceptions.ValidationFailure,
exceptions.ExtractFailure,
):
# log request
err_req_msg = "request: \n"
err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {}))
Expand All @@ -286,16 +346,16 @@ def run_test(self, teststep_dict):
logger.removeHandler(list_handler)

def extract_output(self, output_variables_list):
""" extract output variables
"""
"""extract output variables"""
variables_mapping = self.context.teststep_variables_mapping

output = {}
for variable in output_variables_list:
if variable not in variables_mapping:
logger.warning(
"variable '{}' can not be found in variables mapping, failed to output!"\
.format(variable)
"variable '{}' can not be found in variables mapping, failed to output!".format(
variable
)
)
continue

Expand Down Expand Up @@ -328,7 +388,9 @@ def set_config_header(name, value):
# 在运行时修改配置中请求头的信息
# 比如: 用例中需要切换账号,实现同时请求头中token和userId
current_context = Hrun.get_current_context()
pydash.set_(current_context.TESTCASE_SHARED_REQUEST_MAPPING, f'headers.{name}', value)
pydash.set_(
current_context.TESTCASE_SHARED_REQUEST_MAPPING, f"headers.{name}", value
)

@staticmethod
def set_step_var(name, value):
Expand Down
73 changes: 69 additions & 4 deletions templates/report_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
{% load custom_tags %}



<title>{{ html_report_name }} - 测试报告</title>

<style type='text/css'>
Expand Down Expand Up @@ -7881,6 +7880,12 @@
color: blue;
}

.my-collapsed {
height: 50px; /* 你可以根据需要调整这个值 */
overflow: hidden;
cursor: pointer;
}

</style>
</head>

Expand Down Expand Up @@ -8205,9 +8210,69 @@ <h5>Test Cases</h5>
class='material-icons'>low_priority</i></td>
<td class='timestamp'>Validators</td>
<td class='step-details'>
{% for validator in record.meta_data.validators %}
<pre class="code-block">check-{{ validator.desc }}: {{ validator.check }} - {{ validator.comparator }}:[{{ validator.expect }},&nbsp;{{ validator.check_value }}]</pre>
{% endfor %}
{% if not props.row.meta_data.extractors %}
哦豁,并没有validator,快去加一个吧,没有断言的用例不够健壮哦
{% else %}
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="width: 80px; border: 1px solid #ccc;">是否通过
</th>
<th style="width: 350px; border: 1px solid #ccc;">
取值表达式
</th>
<th style="border: 1px solid #ccc;">实际值</th>
<th style="border: 1px solid #ccc;">比较器</th>
<th style="border: 1px solid #ccc;">期望值</th>
<th style="border: 1px solid #ccc;">描述</th>
</tr>
</thead>
<tbody>
{% for validator in record.meta_data.validators %}
<tr>
<td style="border: 1px solid #ccc;">{{ validator.check_result }}</td>
<td style="border: 1px solid #ccc;">{{ validator.check }}</td>
<td style="border: 1px solid #ccc;">{{ validator.check_value }}</td>
<td style="border: 1px solid #ccc;">{{ validator.comparator }}</td>
<td style="border: 1px solid #ccc;">{{ validator.expect }}</td>
<td style="border: 1px solid #ccc;">{{ validator.desc }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</td>
</tr>
<tr class='pass' status='pass'>
<td class='status pass' title='抽取' alt='pass'><i
class='material-icons'>low_priority</i></td>
<td class='timestamp'>Extractors</td>
<td class='step-details'>
{% if not props.row.meta_data.extractors %}
哦豁,运行的时候并没有extractor
{% else %}
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="width: 350px; border: 1px solid #ccc;">
取值表达式
</th>
<th style="border: 1px solid #ccc;">实际值</th>
<th style="border: 1px solid #ccc;">输出的变量名</th>
</tr>
</thead>
<tbody>
{% for extractor in props.row.meta_data.extractors %}
<tr>
<td style="border: 1px solid #ccc;">{{ extractor.extract_expr }}</td>
<td style="border: 1px solid #ccc;">{{ extractor.actual_value }}</td>
<td style="border: 1px solid #ccc;">{{ extractor.output_variable_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

</td>
</tr>

Expand Down
Loading

0 comments on commit 932d628

Please sign in to comment.