v3 feat: support function calls

This commit is contained in:
debugtalk
2020-04-20 17:06:21 +08:00
parent 86d8f052e3
commit 823a5ad77c
6 changed files with 338 additions and 18 deletions

View File

@@ -3,3 +3,7 @@ from httprunner import __version__
def get_httprunner_version():
return __version__
def sum_two(m, n):
return m + n

View File

@@ -0,0 +1,61 @@
config:
name: "request methods testcase with functions"
variables:
foo1: session_bar1
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: get with params
variables:
foo1: bar1
foo2: session_bar2
sum_v: "${sum_two(1, 2)}"
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
sum_v: $sum_v
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
extract:
session_foo2: "body.args.foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.args.foo1", "session_bar1"]
- eq: ["body.args.foo2", "session_bar2"]
- eq: ["body.args.sum_v", "3"]
-
name: post raw text
variables:
foo1: "hello world"
foo3: "$session_foo2"
request:
method: POST
url: /post
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo3."
validate:
- eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: session_bar1-session_bar2."]
-
name: post form data
variables:
foo1: bar1
foo2: bar2
request:
method: POST
url: /post
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.form.foo1", "session_bar1"]
- eq: ["body.form.foo2", "bar2"]

View File

@@ -0,0 +1,98 @@
from httprunner.v3.runner import TestCaseRunner
from httprunner.v3.schema import TestsConfig, TestStep
from examples.postman_echo import debugtalk
class TestCaseRequestMethodsWithFunctions(TestCaseRunner):
config = TestsConfig(**{
"name": "request methods testcase with functions",
"variables": {
"foo1": "session_bar1"
},
"functions": {
"get_httprunner_version": debugtalk.get_httprunner_version,
"sum_two": debugtalk.sum_two
},
"base_url": "https://postman-echo.com",
"verify": False
})
teststeps = [
TestStep(**{
"name": "get with params",
"variables": {
"foo1": "bar1",
"foo2": "session_bar2",
"sum_v": "${sum_two(1, 2)}"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"sum_v": "$sum_v"
},
"headers": {
"User-Agent": "HttpRunner/${get_httprunner_version()}"
}
},
"extract": {
"session_foo2": "body.args.foo2"
},
"validate": [
{"eq": ["status_code", 200]},
{"eq": ["body.args.foo1", "session_bar1"]},
{"eq": ["body.args.foo2", "session_bar2"]},
{"eq": ["body.args.sum_v", "3"]}
]
}),
TestStep(**{
"name": "post raw text",
"variables": {
"foo1": "hello world",
"foo3": "$session_foo2"
},
"request": {
"method": "POST",
"url": "/post",
"data": "This is expected to be sent back as part of response body: $foo1-$foo3.",
"headers": {
"User-Agent": "HttpRunner/${get_httprunner_version()}",
"Content-Type": "text/plain"
}
},
"validate": [
{"eq": ["status_code", 200]},
{"eq": [
"body.data",
"This is expected to be sent back as part of response body: session_bar1-session_bar2."
]},
]
}),
TestStep(**{
"name": "post form data",
"variables": {
"foo1": "session_bar1",
"foo2": "bar2"
},
"request": {
"method": "POST",
"url": "/post",
"data": "foo1=$foo1&foo2=$foo2",
"headers": {
"User-Agent": "HttpRunner/${get_httprunner_version()}",
"Content-Type": "application/x-www-form-urlencoded"
}
},
"validate": [
{"eq": ["status_code", 200]},
{"eq": ["body.form.foo1", "session_bar1"]},
{"eq": ["body.form.foo2", "bar2"]}
]
})
]
if __name__ == '__main__':
TestCaseRequestMethodsWithFunctions().run()

View File

@@ -1,9 +1,9 @@
import ast
import re
from typing import Any, Set, Text
from typing import Dict
from typing import Any, Set, Text, Callable, Tuple, List, Dict, Union
from httprunner.v3 import exceptions
from httprunner.v3.exceptions import VariableNotFound
from httprunner.v3.exceptions import VariableNotFound, FunctionNotFound
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
@@ -15,6 +15,22 @@ variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
def parse_string_value(str_value: Text) -> Any:
""" parse string to number if possible
e.g. "123" => 123
"12.2" => 12.3
"abc" => "abc"
"$var" => "$var"
"""
try:
return ast.literal_eval(str_value)
except ValueError:
return str_value
except SyntaxError:
# e.g. $var, ${func}
return str_value
def build_url(base_url, path):
""" prepend url with base_url unless it's already an absolute URL """
if absolute_http_url_regexp.match(path):
@@ -25,7 +41,7 @@ def build_url(base_url, path):
raise exceptions.ParamsError("base url missed!")
def regex_findall_variables(content):
def regex_findall_variables(content: Text) -> List[Text]:
""" extract all variable names from content, which is in format $variable
Args:
@@ -59,6 +75,88 @@ def regex_findall_variables(content):
return []
def regex_findall_functions(content: Text) -> List[Text]:
""" extract all functions from string content, which are in format ${fun()}
Args:
content (str): string content
Returns:
list: functions list extracted from string content
Examples:
>>> regex_findall_functions("${func(5)}")
["func(5)"]
>>> regex_findall_functions("${func(a=1, b=2)}")
["func(a=1, b=2)"]
>>> regex_findall_functions("/api/1000?_t=${get_timestamp()}")
["get_timestamp()"]
>>> regex_findall_functions("/api/${add(1, 2)}")
["add(1, 2)"]
>>> regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}")
["add(1, 2)", "get_timestamp()"]
"""
try:
return function_regex_compile.findall(content)
except TypeError:
return []
def parse_args_str(arg_str: Text) -> Tuple[List, Dict]:
""" parse function args and kwargs from function.
Args:
arg_str (str): function str contains args and kwargs
Returns:
dict: function meta dict
{
"func_name": "xxx",
"args": [],
"kwargs": {}
}
Examples:
>>> parse_args_str("")
{'args': [], 'kwargs': {}}
>>> parse_args_str("5")
{'args': [5], 'kwargs': {}}
>>> parse_args_str("1, 2")
{'args': [1, 2], 'kwargs': {}}
>>> parse_args_str("a=1, b=2")
{'args': [], 'kwargs': {'a': 1, 'b': 2}}
>>> parse_args_str("1, 2, a=3, b=4")
{'args': [1, 2], 'kwargs': {'a':3, 'b':4}}
"""
args = []
kwargs = {}
arg_str = arg_str.strip()
if arg_str == "":
return args, kwargs
arg_list = arg_str.split(',')
for arg in arg_list:
arg = arg.strip()
if '=' in arg:
key, value = arg.split('=')
kwargs[key.strip()] = parse_string_value(value.strip())
else:
args.append(parse_string_value(arg))
return args, kwargs
def extract_variables(content: Any) -> Set:
""" extract all variables in content recursively.
"""
@@ -80,7 +178,59 @@ def extract_variables(content: Any) -> Set:
return set()
def parse_string_variables(content, variables_mapping):
def parse_string_functions(
content: Text,
variables_mapping: Dict[Text, Any],
functions_mapping: Dict[Text, Callable]) -> Text:
""" parse string content with functions mapping.
Args:
content (str): string content to be parsed.
variables_mapping (dict): variables mapping.
functions_mapping (dict): functions mapping.
Returns:
str: parsed string content.
Examples:
>>> content = "abc${add_one(3)}def"
>>> functions_mapping = {"add_one": lambda x: x + 1}
>>> parse_string_functions(content, {}, functions_mapping)
"abc4def"
"""
functions_list = regex_findall_functions(content)
for func_meta_tuple in functions_list:
func_name, args_str = func_meta_tuple
args, kwargs = parse_args_str(args_str)
args = parse_content(args, variables_mapping, functions_mapping)
kwargs = parse_content(kwargs, variables_mapping, functions_mapping)
try:
func = functions_mapping[func_name]
except KeyError:
raise FunctionNotFound(f"{func_name} not found in {functions_mapping}")
eval_value = func(*args, **kwargs)
func_content = "${" + func_name + f"({args_str})" + "}"
if func_content == content:
# content is a function, e.g. "${add_one(3)}"
content = eval_value
else:
# content contains one or many functions, e.g. "abc${add_one(3)}def"
content = content.replace(
func_content,
str(eval_value), 1
)
return content
def parse_string_variables(
content: Text,
variables_mapping: Dict[Text, Any]) -> Text:
""" parse string content with variables mapping.
Args:
@@ -92,7 +242,7 @@ def parse_string_variables(content, variables_mapping):
Examples:
>>> content = "/api/users/$uid"
>>> variables_mapping = {"$uid": 1000}
>>> variables_mapping = {"uid": 1000}
>>> parse_string_variables(content, variables_mapping)
"/api/users/1000"
@@ -102,7 +252,7 @@ def parse_string_variables(content, variables_mapping):
try:
variable_value = variables_mapping[variable_name]
except KeyError:
raise VariableNotFound(f"{variable_name} not in {variables_mapping}")
raise VariableNotFound(f"{variable_name} not found in {variables_mapping}")
# TODO: replace variable label from $var to {{var}}
if f"${variable_name}" == content:
@@ -121,7 +271,10 @@ def parse_string_variables(content, variables_mapping):
return content
def parse_content(content: Any, variables_mapping: Dict[str, Any] = None, functions_mapping=None):
def parse_content(
content: Any,
variables_mapping: Dict[Text, Any] = None,
functions_mapping: Dict[Text, Callable] = None) -> Any:
""" parse content with evaluated variables mapping.
Notice: variables_mapping should not contain any variable or function.
"""
@@ -136,8 +289,8 @@ def parse_content(content: Any, variables_mapping: Dict[str, Any] = None, functi
content = content.strip()
# replace functions with evaluated value
# Notice: _eval_content_functions must be called before _eval_content_variables
# content = parse_string_functions(content, variables_mapping, functions_mapping)
# Notice: parse_string_functions must be called before parse_string_variables
content = parse_string_functions(content, variables_mapping, functions_mapping)
# replace variables with binding value
content = parse_string_variables(content, variables_mapping)
@@ -146,15 +299,15 @@ def parse_content(content: Any, variables_mapping: Dict[str, Any] = None, functi
elif isinstance(content, (list, set, tuple)):
return [
parse_content(item, variables_mapping)
parse_content(item, variables_mapping, functions_mapping)
for item in content
]
elif isinstance(content, dict):
parsed_content = {}
for key, value in content.items():
parsed_key = parse_content(key, variables_mapping)
parsed_value = parse_content(value, variables_mapping)
parsed_key = parse_content(key, variables_mapping, functions_mapping)
parsed_value = parse_content(value, variables_mapping, functions_mapping)
parsed_content[parsed_key] = parsed_value
return parsed_content
@@ -162,7 +315,9 @@ def parse_content(content: Any, variables_mapping: Dict[str, Any] = None, functi
return content
def parse_variables_mapping(variables_mapping: Dict[Text, Any]) -> Dict[Text, Any]:
def parse_variables_mapping(
variables_mapping: Dict[Text, Any],
functions_mapping: Dict[Text, Callable] = None) -> Dict[Text, Any]:
parsed_variables: Dict[Text, Any] = {}
@@ -194,7 +349,8 @@ def parse_variables_mapping(variables_mapping: Dict[Text, Any]) -> Dict[Text, An
raise VariableNotFound(not_defined_variables)
try:
parsed_value = parse_content(var_value, parsed_variables)
parsed_value = parse_content(
var_value, parsed_variables, functions_mapping)
except VariableNotFound:
continue

View File

@@ -27,7 +27,7 @@ class TestCaseRunner(object):
# parse
request_dict = step.request.dict()
parsed_request_dict = parse_content(request_dict, step.variables)
parsed_request_dict = parse_content(request_dict, step.variables, self.config.functions)
# prepare arguments
method = parsed_request_dict.pop("method")
@@ -62,7 +62,7 @@ class TestCaseRunner(object):
# update with session variables extracted from former step
step.variables.update(session_variables)
# parse variables
step.variables = parse_variables_mapping(step.variables)
step.variables = parse_variables_mapping(step.variables, self.config.functions)
# run step
extract_mapping = self.run_step(step)
# save extracted variables to session variables

View File

@@ -1,6 +1,6 @@
from enum import Enum
from typing import Any
from typing import Dict, List, Text, Union
from typing import Dict, List, Text, Union, Callable
from pydantic import BaseModel, Field
from pydantic import HttpUrl
@@ -34,6 +34,7 @@ class TestsConfig(BaseModel):
verify: Verify = False
base_url: BaseUrl = ""
variables: Variables = {}
functions: Dict[Text, Callable]
setup_hooks: Hook = []
teardown_hooks: Hook = []
export: Export = []