Merge pull request #922 from httprunner/v3

## 3.0.9 (2020-06-07)

**Fixed**

- fix: miss formatting referenced testcase
- fix: handle cases when parent directory name includes dot/hyphen/space

**Changed**

- change: add `export` keyword in TStep to export session variables from referenced testcase
- change: rename TestCaseInOut field, config_vars and export_vars
- change: rename StepData field, export_vars
- change: add `--tb=short` for `hrun` command to use shorter traceback format by default
- change: search debugtalk.py upward recursively until system root dir
This commit is contained in:
debugtalk
2020-06-07 13:00:24 +08:00
committed by GitHub
43 changed files with 501 additions and 468 deletions

View File

@@ -1,5 +1,20 @@
# Release History
## 3.0.9 (2020-06-07)
**Fixed**
- fix: miss formatting referenced testcase
- fix: handle cases when parent directory name includes dot/hyphen/space
**Changed**
- change: add `export` keyword in TStep to export session variables from referenced testcase
- change: rename TestCaseInOut field, config_vars and export_vars
- change: rename StepData field, export_vars
- change: add `--tb=short` for `hrun` command to use shorter traceback format by default
- change: search debugtalk.py upward recursively until system root dir
## 3.0.8 (2020-06-04)
**Added**

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/httpbin/basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/httpbin/hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/httpbin/load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/httpbin/upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/httpbin/validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,54 +0,0 @@
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
# FROM: examples/postman_echo/cookie_manipulation/hardcode.yml
from httprunner import HttpRunner, TConfig, TStep
class TestCaseHardcode(HttpRunner):
config = TConfig(
**{
"name": "set & delete cookies.",
"base_url": "https://postman-echo.com",
"verify": False,
"export": ["cookie_foo1", "cookie_foo3"],
"path": "examples/postman_echo/cookie_manipulation/hardcode_test.py",
}
)
teststeps = [
TStep(
**{
"name": "set cookie foo1 & foo2 & foo3",
"request": {
"method": "GET",
"url": "/cookies/set",
"params": {"foo1": "bar1", "foo2": "bar2"},
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"extract": {"cookie_foo1": "$.cookies.foo1"},
"validate": [
{"eq": ["status_code", 200]},
{"eq": ["cookies.foo1", "bar1"]},
],
}
),
TStep(
**{
"name": "delete cookie foo2",
"request": {
"method": "GET",
"url": "/cookies/delete?foo2",
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"validate": [
{"eq": ["status_code", 200]},
{"ne": ["$.cookies.foo1", "$foo1"]},
{"eq": ["$.cookies.foo1", "$cookie_foo1"]},
{"eq": ["$.cookies.foo3", "$cookie_foo3"]},
],
}
),
]
if __name__ == "__main__":
TestCaseHardcode().test_start()

View File

@@ -1,62 +0,0 @@
import unittest
import requests
from httprunner.runner import HttpRunner
from httprunner.schema import TConfig, TStep
class TestCaseSetDeleteCookies(unittest.TestCase):
config = TConfig(
**{
"name": "set & delete cookies.",
"base_url": "https://postman-echo.com",
"variables": {"foo1": "bar1", "foo2": "bar2"},
"verify": False,
"export": ["cookie_foo1", "cookie_foo3"],
}
)
teststeps = [
TStep(
**{
"name": "set cookie foo1 & foo2 & foo3",
"variables": {"foo3": "bar3"},
"request": {
"method": "GET",
"url": "/cookies/set",
"params": {"foo1": "bar111", "foo2": "$foo2", "foo3": "$foo3"},
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"extract": {
"cookie_foo1": "$.cookies.foo1",
"cookie_foo3": "$.cookies.foo3",
},
"validate": [
{"eq": ["status_code", 200]},
{"eq": ["$.cookies.foo3", "$foo3"]},
],
}
),
TStep(
**{
"name": "delete cookie foo2",
"request": {
"method": "GET",
"url": "/cookies/delete?foo2",
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"validate": [
{"eq": ["status_code", 200]},
{"ne": ["$.cookies.foo1", "$foo1"]},
{"eq": ["$.cookies.foo1", "$cookie_foo1"]},
{"eq": ["$.cookies.foo3", "$cookie_foo3"]},
],
}
),
]
def test_start(self):
s = requests.Session()
HttpRunner(self.config, self.teststeps, session=s).with_variables(
foo1="bar123", foo2="bar22"
).run()

View File

@@ -1,59 +0,0 @@
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
# FROM: examples/postman_echo/cookie_manipulation/set_delete_cookies.yml
from httprunner import HttpRunner, TConfig, TStep
class TestCaseSetDeleteCookies(HttpRunner):
config = TConfig(
**{
"name": "set & delete cookies.",
"variables": {"foo1": "bar1", "foo2": "bar2"},
"base_url": "https://postman-echo.com",
"verify": False,
"export": ["cookie_foo1", "cookie_foo3"],
"path": "examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py",
}
)
teststeps = [
TStep(
**{
"name": "set cookie foo1 & foo2 & foo3",
"variables": {"foo3": "bar3"},
"request": {
"method": "GET",
"url": "/cookies/set",
"params": {"foo1": "bar111", "foo2": "$foo2", "foo3": "$foo3"},
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"extract": {
"cookie_foo1": "$.cookies.foo1",
"cookie_foo3": "$.cookies.foo3",
},
"validate": [
{"eq": ["status_code", 200]},
{"ne": ["$.cookies.foo3", "$foo3"]},
],
}
),
TStep(
**{
"name": "delete cookie foo2",
"request": {
"method": "GET",
"url": "/cookies/delete?foo2",
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
},
"validate": [
{"eq": ["status_code", 200]},
{"ne": ["$.cookies.foo1", "$foo1"]},
{"eq": ["$.cookies.foo1", "$cookie_foo1"]},
{"eq": ["$.cookies.foo3", "$cookie_foo3"]},
],
}
),
]
if __name__ == "__main__":
TestCaseSetDeleteCookies().test_start()

View File

@@ -0,0 +1 @@
# NOTICE: Generated By HttpRunner. DO NOT EDIT!

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
import os
@@ -26,7 +26,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
RunTestCase("request with functions")
.with_variables(**{"foo1": "override_bar1"})
.call(RequestWithFunctions)
.extract(*["session_foo2"])
.export(*["session_foo2"])
),
Step(
RunRequest("post form data")

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -11,7 +11,7 @@ teststeps:
variables:
foo1: override_bar1
testcase: request_methods/request_with_functions.yml
extract:
export:
- session_foo2
-
name: post form data

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
import os
@@ -26,7 +26,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
RunTestCase("request with functions")
.with_variables(**{"foo1": "override_bar1"})
.call(RequestWithFunctions)
.extract(*["session_foo2"])
.export(*["session_foo2"])
),
Step(
RunRequest("post form data")

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8
# NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
__version__ = "3.0.8"
__version__ = "3.0.9"
__description__ = "One-stop solution for HTTP(S) testing."
from httprunner.runner import HttpRunner

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter
from httprunner.runner import HttpRunner
from httprunner.schema import ProjectMeta, TestCase
from httprunner.models import ProjectMeta, TestCase
router = APIRouter()
runner = HttpRunner()

View File

@@ -1,4 +1,5 @@
import argparse
import enum
import os
import sys
@@ -23,7 +24,7 @@ def init_parser_run(subparsers):
return sub_parser_run
def main_run(extra_args):
def main_run(extra_args) -> enum.IntEnum:
capture_message("start to run")
# keep compatibility with v2
extra_args = ensure_cli_args(extra_args)
@@ -48,8 +49,11 @@ def main_run(extra_args):
logger.error("No valid testcases found, exit 1.")
sys.exit(1)
if "--tb=short" not in extra_args_new:
extra_args_new.append("--tb=short")
extra_args_new.extend(testcase_path_list)
sys.exit(pytest.main(extra_args_new))
return pytest.main(extra_args_new)
def main():
@@ -109,7 +113,7 @@ def main():
sys.exit(0)
if sys.argv[1] == "run":
main_run(extra_args)
sys.exit(main_run(extra_args))
elif sys.argv[1] == "startproject":
main_scaffold(args)
elif sys.argv[1] == "har2case":

View File

@@ -11,10 +11,9 @@ from requests.exceptions import (
MissingSchema,
RequestException,
)
from sentry_sdk import capture_exception
from httprunner.schema import RequestData, ResponseData
from httprunner.schema import SessionData, ReqRespData
from httprunner.models import RequestData, ResponseData
from httprunner.models import SessionData, ReqRespData
from httprunner.utils import lower_dict_keys, omit_long_data
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -51,12 +50,12 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
except json.JSONDecodeError:
# str: a=1&b=2
pass
except UnicodeDecodeError as ex:
except UnicodeDecodeError:
# bytes/bytearray: request body in protobuf
capture_exception(ex)
except TypeError as ex:
pass
except TypeError:
# neither str nor bytes/bytearray, e.g. <MultipartEncoder>
capture_exception(ex)
pass
request_content_type = lower_dict_keys(request_headers).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:

View File

@@ -7,8 +7,9 @@ from typing import List, Dict, Text, Union
from loguru import logger
from httprunner import exceptions
from httprunner.loader import load_project_meta
from httprunner.utils import sort_dict_by_custom_order
from httprunner.utils import sort_dict_by_custom_order, ensure_file_path_valid
def convert_jmespath(raw: Text) -> Text:
@@ -53,10 +54,12 @@ def convert_extractors(extractors: Union[List, Dict]) -> Dict:
v3_extractors: Dict = {}
if isinstance(extractors, List):
# [{"varA": "content.varA"}, {"varB": "json.varB"}]
for extractor in extractors:
for k, v in extractor.items():
v3_extractors[k] = v
elif isinstance(extractors, Dict):
# {"varA": "body.varA", "varB": "body.varB"}
v3_extractors = extractors
else:
logger.error(f"Invalid extractor: {extractors}")
@@ -133,10 +136,10 @@ def ensure_step_attachment(step: Dict) -> Dict:
test_dict["teardown_hooks"] = step["teardown_hooks"]
if "extract" in step:
if step.get("request"):
test_dict["extract"] = convert_extractors(step["extract"])
elif step.get("testcase"):
test_dict["extract"] = step["extract"]
test_dict["extract"] = convert_extractors(step["extract"])
if "export" in step:
test_dict["export"] = step["export"]
if "validate" in step:
test_dict["validate"] = convert_validators(step["validate"])
@@ -167,14 +170,16 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
for step in test_content["teststeps"]:
teststep = {}
teststep.update(ensure_step_attachment(step))
if "request" in step:
teststep["request"] = step.pop("request")
elif "api" in step:
teststep["testcase"] = step.pop("api")
elif "testcase" in step:
teststep["testcase"] = step.pop("testcase")
else:
raise exceptions.TestCaseFormatError(f"Invalid teststep: {step}")
teststep.update(ensure_step_attachment(step))
teststep = sort_step_by_custom_order(teststep)
v3_content["teststeps"].append(teststep)
@@ -215,10 +220,8 @@ def generate_conftest_for_summary(args: List):
sys.exit(1)
project_meta = load_project_meta(test_path)
conftest_path = os.path.join(project_meta.PWD, "conftest.py")
if os.path.isfile(conftest_path):
return
project_root_dir = ensure_file_path_valid(project_meta.RootDir)
conftest_path = os.path.join(project_root_dir, "conftest.py")
conftest_content = '''# NOTICE: Generated By HttpRunner.
import json
import os
@@ -286,8 +289,8 @@ def session_fixture(request):
'''
test_path = os.path.abspath(test_path)
logs_dir_path = os.path.join(project_meta.PWD, "logs")
test_path_relative_path = test_path[len(project_meta.PWD) + 1 :]
logs_dir_path = os.path.join(project_root_dir, "logs")
test_path_relative_path = test_path[len(project_root_dir) + 1 :]
if os.path.isdir(test_path):
file_foder_path = os.path.join(logs_dir_path, test_path_relative_path)
@@ -303,6 +306,10 @@ def session_fixture(request):
"{{SUMMARY_PATH_PLACEHOLDER}}", summary_path
)
dir_path = os.path.dirname(conftest_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(conftest_path, "w", encoding="utf-8") as f:
f.write(conftest_content)

View File

@@ -49,7 +49,7 @@ from typing import Text, NoReturn
from loguru import logger
from httprunner.parser import parse_variables_mapping
from httprunner.schema import TStep, FunctionsMapping
from httprunner.models import TStep, FunctionsMapping
try:
import filetype
@@ -139,7 +139,7 @@ def multipart_encoder(**kwargs):
project_meta = load_project_meta(os.getcwd())
_file_path = os.path.join(project_meta.PWD, value)
_file_path = os.path.join(project_meta.RootDir, value)
is_exists_file = os.path.isfile(_file_path)
if is_exists_file:

View File

@@ -13,7 +13,7 @@ from pydantic import ValidationError
from httprunner import builtin, utils
from httprunner import exceptions
from httprunner.schema import TestCase, ProjectMeta, TestSuite
from httprunner.models import TestCase, ProjectMeta, TestSuite
try:
# PyYAML version >= 5.1
@@ -176,7 +176,7 @@ def load_csv_file(csv_file: Text) -> List[Dict]:
raise exceptions.MyBaseFailure("load_project_meta() has not been called!")
# make compatible with Windows/Linux
csv_file = os.path.join(project_meta.PWD, *csv_file.split("/"))
csv_file = os.path.join(project_meta.RootDir, *csv_file.split("/"))
if not os.path.isfile(csv_file):
# file path not exist
@@ -265,7 +265,7 @@ def load_builtin_functions() -> Dict[Text, Callable]:
def locate_file(start_path: Text, file_name: Text) -> Text:
""" locate filename and return absolute file path.
searching will be recursive upward until current working directory or system root dir.
searching will be recursive upward until system root dir.
Args:
file_name (str): target locate file name
@@ -289,13 +289,6 @@ def locate_file(start_path: Text, file_name: Text) -> Text:
if os.path.isfile(file_path):
return os.path.abspath(file_path)
# current working directory
cwd = os.getcwd()
if os.path.abspath(start_dir_path) == cwd:
raise exceptions.FileNotFound(
f"{file_name} not found for {start_path}\ncurrent working directory: {cwd}"
)
# system root dir
# Windows, e.g. 'E:\\'
# Linux/Darwin, '/'
@@ -327,14 +320,14 @@ def locate_debugtalk_py(start_path: Text) -> Text:
return debugtalk_path
def locate_project_working_directory(test_path: Text) -> Tuple[Text, Text]:
""" locate debugtalk.py path as project working directory
def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]:
""" locate debugtalk.py path as project root directory
Args:
test_path: specified testfile path
Returns:
(str, str): debugtalk.py path, project_working_directory
(str, str): debugtalk.py path, project_root_directory
"""
@@ -355,18 +348,18 @@ def locate_project_working_directory(test_path: Text) -> Tuple[Text, Text]:
debugtalk_path = locate_debugtalk_py(test_path)
if debugtalk_path:
# The folder contains debugtalk.py will be treated as PWD.
project_working_directory = os.path.dirname(debugtalk_path)
# The folder contains debugtalk.py will be treated as project RootDir.
project_root_directory = os.path.dirname(debugtalk_path)
else:
# debugtalk.py not found, use os.getcwd() as PWD.
project_working_directory = os.getcwd()
# debugtalk.py not found, use os.getcwd() as project RootDir.
project_root_directory = os.getcwd()
return debugtalk_path, project_working_directory
return debugtalk_path, project_root_directory
def load_debugtalk_functions() -> Dict[Text, Callable]:
""" load project debugtalk.py module functions
debugtalk.py should be located in project working directory.
debugtalk.py should be located in project root directory.
Returns:
dict: debugtalk module functions mapping
@@ -382,12 +375,12 @@ def load_debugtalk_functions() -> Dict[Text, Callable]:
def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
""" load api, testcases, .env, debugtalk.py functions.
api/testcases folder is relative to project_working_directory
""" load testcases, .env, debugtalk.py functions.
testcases folder is relative to project_root_directory
by default, project_meta will be loaded only once, unless set reload to true.
Args:
test_path (str): test file/folder path, locate pwd from this path.
test_path (str): test file/folder path, locate project RootDir from this path.
reload: reload project meta if set true, default to false
Returns:
@@ -404,19 +397,20 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
if not test_path:
return project_meta
debugtalk_path, project_working_directory = locate_project_working_directory(
test_path
)
debugtalk_path, project_root_directory = locate_project_root_directory(test_path)
# add PWD to sys.path
sys.path.insert(0, project_working_directory)
# add project RootDir to sys.path
sys.path.insert(0, project_root_directory)
# load .env file
# NOTICE:
# environment variable maybe loaded in debugtalk.py
# thus .env file should be loaded before loading debugtalk.py
dot_env_path = os.path.join(project_working_directory, ".env")
project_meta.env = load_dot_env_file(dot_env_path)
dot_env_path = os.path.join(project_root_directory, ".env")
dot_env = load_dot_env_file(dot_env_path)
if dot_env:
project_meta.env = dot_env
project_meta.dot_env_path = dot_env_path
if debugtalk_path:
# load debugtalk.py functions
@@ -424,11 +418,9 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
else:
debugtalk_functions = {}
# locate PWD and load debugtalk.py functions
project_meta.PWD = project_working_directory
# locate project RootDir and load debugtalk.py functions
project_meta.RootDir = project_root_directory
project_meta.functions = debugtalk_functions
project_meta.test_path = os.path.abspath(test_path)[
len(project_working_directory) + 1 :
]
project_meta.debugtalk_path = debugtalk_path
return project_meta

View File

@@ -1,6 +1,6 @@
import os
import string
import subprocess
from shutil import copyfile
from typing import Text, List, Tuple, Dict, Set, NoReturn
import jinja2
@@ -18,14 +18,18 @@ from httprunner.loader import (
)
from httprunner.parser import parse_data
from httprunner.response import uniform_validator
from httprunner.utils import ensure_file_path_valid
""" cache converted pytest files, avoid duplicate making
"""
make_files_cache_set: Set = set()
pytest_files_set: Set = set()
pytest_files_made_cache_mapping: Dict[Text, Text] = {}
""" save generated pytest files to run, except referenced testcase
"""
pytest_files_run_set: Set = set()
__TEMPLATE__ = jinja2.Template(
"""# NOTICE: Generated By HttpRunner v{{ version }}
"""# NOTE: Generated By HttpRunner v{{ version }}
# FROM: {{ testcase_path }}
{% if imports_list %}
import os
@@ -54,24 +58,16 @@ if __name__ == "__main__":
)
def __ensure_file_name(path: Text) -> Text:
""" ensure file name not startswith digit
testcases/19.json => testcases/T19.json
"""
filename = os.path.basename(path)
if filename[0] in string.digits:
path = os.path.join(os.path.dirname(path), f"T{filename}")
return path
def __ensure_absolute(path: Text) -> Text:
project_meta = load_project_meta(path)
if os.path.isabs(path):
absolute_path = path
else:
absolute_path = os.path.join(project_meta.PWD, path)
absolute_path = os.path.join(project_meta.RootDir, path)
if not os.path.isfile(absolute_path):
raise exceptions.ParamsError(f"Invalid testcase file path: {absolute_path}")
return absolute_path
@@ -102,24 +98,38 @@ def __ensure_testcase_module(path: Text) -> NoReturn:
f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n")
def __ensure_project_meta_files(tests_path: Text) -> NoReturn:
""" ensure project meta files exist in generated pytest folder files
include debugtalk.py and .env
"""
project_meta = load_project_meta(tests_path)
# handle cases when generated pytest directory are different from original yaml/json testcases
debugtalk_path = project_meta.debugtalk_path
if debugtalk_path:
debugtalk_new_path = ensure_file_path_valid(debugtalk_path)
if debugtalk_new_path != debugtalk_path:
logger.info(f"copy debugtalk.py to {debugtalk_new_path}")
copyfile(debugtalk_path, debugtalk_new_path)
global pytest_files_made_cache_mapping
pytest_files_made_cache_mapping[debugtalk_new_path] = ""
dot_csv_path = project_meta.dot_env_path
if dot_csv_path:
dot_csv_new_path = ensure_file_path_valid(dot_csv_path)
if dot_csv_new_path != dot_csv_path:
logger.info(f"copy .env to {dot_csv_new_path}")
copyfile(dot_csv_path, dot_csv_new_path)
def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
"""convert single YAML/JSON testcase path to python file"""
if os.path.isdir(testcase_path):
# folder does not need to convert
return testcase_path, ""
testcase_new_path = ensure_file_path_valid(testcase_path)
testcase_path = __ensure_file_name(testcase_path)
raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_path))
file_suffix = file_suffix.lower()
if file_suffix not in [".json", ".yml", ".yaml", ".har"]:
raise exceptions.ParamsError(
"testcase file should have .yaml/.yml/.json suffix"
)
file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_")
testcase_dir = os.path.dirname(testcase_path)
testcase_python_path = os.path.join(testcase_dir, f"{file_name}_test.py")
dir_path = os.path.dirname(testcase_new_path)
file_name, _ = os.path.splitext(os.path.basename(testcase_new_path))
testcase_python_path = os.path.join(dir_path, f"{file_name}_test.py")
# convert title case, e.g. request_with_variables => RequestWithVariables
name_in_title_case = file_name.title().replace("_", "")
@@ -220,18 +230,16 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
call_ref_testcase = f".call({testcase})"
step_info += call_ref_testcase
extract_info = teststep.get("extract")
if extract_info:
if isinstance(extract_info, Dict):
# request step
step_info += ".extract()"
for extract_name, extract_path in extract_info.items():
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
elif isinstance(extract_info, List):
# reference testcase step
step_info += f".extract(*{extract_info})"
else:
raise exceptions.TestCaseFormatError(f"Invalid extract: {extract_info}")
if "extract" in teststep:
# request step
step_info += ".extract()"
for extract_name, extract_path in teststep["extract"].items():
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
if "export" in teststep:
# reference testcase step
export: List[Text] = teststep["export"]
step_info += f".export(*{export})"
if "validate" in teststep:
step_info += ".validate()"
@@ -253,9 +261,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
return f"Step({step_info})"
def make_testcase(
testcase: Dict, dir_path: Text = None, ref_flag: bool = False,
) -> Text:
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
"""convert valid testcase dict to pytest file path"""
# ensure compatibility with testcase format v2
testcase = ensure_testcase_v3(testcase)
@@ -263,17 +269,17 @@ def make_testcase(
# validate testcase format
load_testcase(testcase)
testcase_path = __ensure_absolute(testcase["config"]["path"])
logger.info(f"start to make testcase: {testcase_path}")
testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
logger.info(f"start to make testcase: {testcase_abs_path}")
testcase_python_path, testcase_cls_name = convert_testcase_path(testcase_path)
testcase_python_path, testcase_cls_name = convert_testcase_path(testcase_abs_path)
if dir_path:
testcase_python_path = os.path.join(
dir_path, os.path.basename(testcase_python_path)
)
global make_files_cache_set
if testcase_python_path in make_files_cache_set:
global pytest_files_made_cache_mapping
if testcase_python_path in pytest_files_made_cache_mapping:
return testcase_python_path
config = testcase["config"]
@@ -283,7 +289,7 @@ def make_testcase(
config.setdefault("variables", {})
if isinstance(config["variables"], Text):
# get variables by function, e.g. ${get_variables()}
project_meta = load_project_meta(testcase_path)
project_meta = load_project_meta(testcase_abs_path)
config["variables"] = parse_data(
config["variables"], {}, project_meta.functions
)
@@ -297,12 +303,14 @@ def make_testcase(
# make ref testcase pytest file
ref_testcase_path = __ensure_absolute(teststep["testcase"])
__make(ref_testcase_path, ref_flag=True)
test_content = load_test_file(ref_testcase_path)
test_content.setdefault("config", {})["path"] = ref_testcase_path
ref_testcase_python_path = make_testcase(test_content)
# prepare ref testcase class name
ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path(
ref_testcase_path
)
ref_testcase_cls_name = pytest_files_made_cache_mapping[
ref_testcase_python_path
]
teststep["testcase"] = ref_testcase_cls_name
# prepare import ref testcase
@@ -315,7 +323,7 @@ def make_testcase(
data = {
"version": __version__,
"testcase_path": __ensure_cwd_relative(testcase_path),
"testcase_path": __ensure_cwd_relative(testcase_abs_path),
"class_name": f"TestCase{testcase_cls_name}",
"imports_list": imports_list,
"config_chain_style": make_config_chain_style(config),
@@ -325,16 +333,19 @@ def make_testcase(
}
content = __TEMPLATE__.render(data)
# ensure new file's directory exists
dir_path = os.path.dirname(testcase_python_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(testcase_python_path, "w", encoding="utf-8") as f:
f.write(content)
pytest_files_made_cache_mapping[testcase_python_path] = testcase_cls_name
__ensure_testcase_module(testcase_python_path)
logger.info(f"generated testcase: {testcase_python_path}")
if not ref_flag:
make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path))
return testcase_python_path
@@ -357,11 +368,10 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
logger.info(f"start to make testsuite: {testsuite_path}")
# create directory with testsuite file name, put its testcases under this directory
testsuite_dir = os.path.join(
os.path.dirname(testsuite_path),
os.path.basename(testsuite_path).replace(".", "_"),
)
os.makedirs(testsuite_dir, exist_ok=True)
testsuite_path = ensure_file_path_valid(testsuite_path)
testsuite_dir, file_suffix = os.path.splitext(testsuite_path)
# demo_testsuite.yml => demo_testsuite_yml
testsuite_dir = f"{testsuite_dir}_{file_suffix.lstrip('.')}"
for testcase in testsuite["testcases"]:
# get referenced testcase content
@@ -386,16 +396,16 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
testcase_dict["config"]["variables"].update(testsuite_variables)
# make testcase
make_testcase(testcase_dict, testsuite_dir)
testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
pytest_files_run_set.add(testcase_pytest_path)
def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn:
def __make(tests_path: Text) -> NoReturn:
""" make testcase(s) with testcase/testsuite/folder absolute path
generated pytest file path will be cached in make_files_cache_set
generated pytest file path will be cached in pytest_files_made_cache_mapping
Args:
tests_path: should be in absolute path
ref_flag: flag if referenced test path
"""
test_files = []
@@ -409,7 +419,7 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn:
for test_file in test_files:
if test_file.lower().endswith("_test.py"):
pytest_files_set.add(test_file)
pytest_files_run_set.add(test_file)
continue
try:
@@ -422,12 +432,16 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn:
if "request" in test_content:
test_content = ensure_testcase_v3_api(test_content)
if not (isinstance(test_content, Dict) and "config" in test_content):
raise exceptions.FileFormatError("Invalid testcase/testsuite v2/v3 format!")
test_content.setdefault("config", {})["path"] = test_file
# testcase
if "teststeps" in test_content:
try:
make_testcase(test_content, ref_flag=ref_flag)
testcase_pytest_path = make_testcase(test_content)
pytest_files_run_set.add(testcase_pytest_path)
except exceptions.TestCaseFormatError:
continue
@@ -452,11 +466,13 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
tests_path = os.path.join(os.getcwd(), tests_path)
__make(tests_path)
__ensure_project_meta_files(tests_path)
pytest_files_set.update(make_files_cache_set)
pytest_files_list = list(pytest_files_set)
format_pytest_with_black(*pytest_files_list)
return pytest_files_list
# format pytest files
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
format_pytest_with_black(*pytest_files_format_list)
return list(pytest_files_run_set)
def init_make_parser(subparsers):

View File

@@ -66,7 +66,10 @@ class TStep(BaseModel):
variables: VariablesMapping = {}
setup_hooks: Hook = []
teardown_hooks: Hook = []
extract: Union[Dict[Text, Text], List[Text]] = {}
# used to extract request's response field
extract: VariablesMapping = {}
# used to export session variables from referenced testcase
export: Export = []
validators: Validators = Field([], alias="validate")
validate_script: List[Text] = []
@@ -78,10 +81,11 @@ class TestCase(BaseModel):
class ProjectMeta(BaseModel):
debugtalk_py: Text = "" # debugtalk.py file content
functions: FunctionsMapping = {}
debugtalk_path: Text = "" # debugtalk.py file path
dot_env_path: Text = "" # .env file path
functions: FunctionsMapping = {} # functions defined in debugtalk.py
env: Env = {}
PWD: Text = os.getcwd()
test_path: Text = None # run with specified test path
RootDir: Text = os.getcwd() # project root directory, the path debugtalk.py located
class TestsMapping(BaseModel):
@@ -96,8 +100,8 @@ class TestCaseTime(BaseModel):
class TestCaseInOut(BaseModel):
vars: VariablesMapping = {}
export: Dict = {}
config_vars: VariablesMapping = {}
export_vars: Dict = {}
class RequestStat(BaseModel):
@@ -145,7 +149,7 @@ class StepData(BaseModel):
success: bool = False
name: Text = "" # teststep name
data: Union[SessionData, List[SessionData]] = None
export: Dict = {}
export_vars: VariablesMapping = {}
class TestCaseSummary(BaseModel):

View File

@@ -6,7 +6,7 @@ from typing import Any, Set, Text, Callable, List, Dict
from sentry_sdk import capture_exception
from httprunner import loader, utils, exceptions
from httprunner.schema import VariablesMapping, FunctionsMapping
from httprunner.models import VariablesMapping, FunctionsMapping
absolute_http_url_regexp = re.compile(r"^https?://", re.I)

View File

@@ -6,7 +6,7 @@ from loguru import logger
from httprunner.exceptions import ValidationFailure, ParamsError
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
from httprunner.schema import VariablesMapping, Validators, FunctionsMapping
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
def get_uniform_comparator(comparator: Text):

View File

@@ -21,7 +21,7 @@ from httprunner.loader import load_project_meta, load_testcase_file
from httprunner.parser import build_url, parse_data, parse_variables_mapping
from httprunner.response import ResponseObject
from httprunner.testcase import Config, Step
from httprunner.schema import (
from httprunner.models import (
TConfig,
TStep,
VariablesMapping,
@@ -43,10 +43,10 @@ class HttpRunner(object):
__teststeps: List[TStep]
__project_meta: ProjectMeta = None
__case_id: Text = ""
__export: List[Text] = []
__step_datas: List[StepData] = None
__session: HttpSession = None
__session_variables: VariablesMapping = {}
__export_variables: VariablesMapping = {}
# time
__start_at: float = 0
__duration: float = 0
@@ -82,6 +82,10 @@ class HttpRunner(object):
self.__session_variables = variables
return self
def with_export(self, export: List[Text]) -> "HttpRunner":
self.__export = export
return self
def __run_step_request(self, step: TStep) -> StepData:
"""run teststep: request"""
step_data = StepData(name=step.name)
@@ -134,7 +138,7 @@ class HttpRunner(object):
# extract
extractors = step.extract
extract_mapping = resp_obj.extract(extractors)
step_data.export = extract_mapping
step_data.export_vars = extract_mapping
variables_mapping = step.variables
variables_mapping.update(extract_mapping)
@@ -166,6 +170,7 @@ class HttpRunner(object):
"""run teststep: referenced testcase"""
step_data = StepData(name=step.name)
step_variables = step.variables
step_export = step.export
if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
testcase_cls = step.testcase
@@ -174,6 +179,7 @@ class HttpRunner(object):
.with_session(self.__session)
.with_case_id(self.__case_id)
.with_variables(step_variables)
.with_export(step_export)
.run()
)
@@ -181,13 +187,16 @@ class HttpRunner(object):
if os.path.isabs(step.testcase):
ref_testcase_path = step.testcase
else:
ref_testcase_path = os.path.join(self.__project_meta.PWD, step.testcase)
ref_testcase_path = os.path.join(
self.__project_meta.RootDir, step.testcase
)
case_result = (
HttpRunner()
.with_session(self.__session)
.with_case_id(self.__case_id)
.with_variables(step_variables)
.with_export(step_export)
.run_path(ref_testcase_path)
)
@@ -197,10 +206,13 @@ class HttpRunner(object):
)
step_data.data = case_result.get_step_datas() # list of step data
step_data.export = case_result.get_export_variables()
step_data.export_vars = case_result.get_export_variables()
step_data.success = case_result.success
self.success &= case_result.success
if step_data.export_vars:
logger.info(f"export variables: {step_data.export_vars}")
return step_data
def __run_step(self, step: TStep) -> Dict:
@@ -218,7 +230,7 @@ class HttpRunner(object):
self.__step_datas.append(step_data)
logger.info(f"run step end: {step.name} <<<<<<\n")
return step_data.export
return step_data.export_vars
def __parse_config(self, config: TConfig) -> NoReturn:
config.variables.update(self.__session_variables)
@@ -252,7 +264,6 @@ class HttpRunner(object):
self.__step_datas: List[StepData] = []
self.__session = self.__session or HttpSession()
self.__session_variables = {}
self.__export_variables = {}
# run teststeps
for step in self.__teststeps:
@@ -274,11 +285,6 @@ class HttpRunner(object):
self.__session_variables.update(extract_mapping)
self.__duration = time.time() - self.__start_at
self.__export_variables = self.get_export_variables()
if self.__export_variables:
logger.info(f"export variables: {self.__export_variables}")
return self
def run_path(self, path: Text) -> "HttpRunner":
@@ -303,11 +309,10 @@ class HttpRunner(object):
return self.__step_datas
def get_export_variables(self) -> Dict:
if self.__export_variables:
return self.__export_variables
# override testcase export vars with step export
export_var_names = self.__export or self.__config.export
export_vars_mapping = {}
for var_name in self.__config.export:
for var_name in export_var_names:
if var_name not in self.__session_variables:
raise ParamsError(
f"failed to export variable {var_name} from session variables {self.__session_variables}"
@@ -331,7 +336,8 @@ class HttpRunner(object):
duration=self.__duration,
),
in_out=TestCaseInOut(
vars=self.__config.variables, export=self.get_export_variables()
config_vars=self.__config.variables,
export_vars=self.get_export_variables(),
),
log=self.__log_path,
step_datas=self.__step_datas,
@@ -345,7 +351,7 @@ class HttpRunner(object):
)
self.__case_id = self.__case_id or str(uuid.uuid4())
self.__log_path = self.__log_path or os.path.join(
self.__project_meta.PWD, "logs", f"{self.__case_id}.run.log"
self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log"
)
log_handler = logger.add(self.__log_path, level="DEBUG")

View File

@@ -1,7 +1,7 @@
import inspect
from typing import Text, Any, Union, Callable
from httprunner.schema import (
from httprunner.models import (
TConfig,
TStep,
TRequest,
@@ -57,37 +57,43 @@ class Config(object):
class StepRequestValidation(object):
def __init__(self, step: TStep):
self.__t_step = step
def __init__(self, step_context: TStep):
self.__step_context = step_context
def assert_equal(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"equal": [jmes_path, expected_value]})
self.__step_context.validators.append({"equal": [jmes_path, expected_value]})
return self
def assert_not_equal(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"not_equal": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"not_equal": [jmes_path, expected_value]}
)
return self
def assert_greater_than(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepRequestValidation":
self.__t_step.validators.append({"greater_than": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"greater_than": [jmes_path, expected_value]}
)
return self
def assert_less_than(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepRequestValidation":
self.__t_step.validators.append({"less_than": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"less_than": [jmes_path, expected_value]}
)
return self
def assert_greater_or_equals(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepRequestValidation":
self.__t_step.validators.append(
self.__step_context.validators.append(
{"greater_or_equals": [jmes_path, expected_value]}
)
return self
@@ -95,19 +101,23 @@ class StepRequestValidation(object):
def assert_less_or_equals(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepRequestValidation":
self.__t_step.validators.append({"less_or_equals": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"less_or_equals": [jmes_path, expected_value]}
)
return self
def assert_length_equal(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append({"length_equal": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"length_equal": [jmes_path, expected_value]}
)
return self
def assert_length_greater_than(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append(
self.__step_context.validators.append(
{"length_greater_than": [jmes_path, expected_value]}
)
return self
@@ -115,7 +125,7 @@ class StepRequestValidation(object):
def assert_length_less_than(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append(
self.__step_context.validators.append(
{"length_less_than": [jmes_path, expected_value]}
)
return self
@@ -123,7 +133,7 @@ class StepRequestValidation(object):
def assert_length_greater_or_equals(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append(
self.__step_context.validators.append(
{"length_greater_or_equals": [jmes_path, expected_value]}
)
return self
@@ -131,7 +141,7 @@ class StepRequestValidation(object):
def assert_length_less_or_equals(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append(
self.__step_context.validators.append(
{"length_less_or_equals": [jmes_path, expected_value]}
)
return self
@@ -139,55 +149,65 @@ class StepRequestValidation(object):
def assert_string_equals(
self, jmes_path: Text, expected_value: int
) -> "StepRequestValidation":
self.__t_step.validators.append({"string_equals": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"string_equals": [jmes_path, expected_value]}
)
return self
def assert_startswith(
self, jmes_path: Text, expected_value: Text
) -> "StepRequestValidation":
self.__t_step.validators.append({"startswith": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"startswith": [jmes_path, expected_value]}
)
return self
def assert_endswith(
self, jmes_path: Text, expected_value: Text
) -> "StepRequestValidation":
self.__t_step.validators.append({"endswith": [jmes_path, expected_value]})
self.__step_context.validators.append({"endswith": [jmes_path, expected_value]})
return self
def assert_regex_match(
self, jmes_path: Text, expected_value: Text
) -> "StepRequestValidation":
self.__t_step.validators.append({"regex_match": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"regex_match": [jmes_path, expected_value]}
)
return self
def assert_contains(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"contains": [jmes_path, expected_value]})
self.__step_context.validators.append({"contains": [jmes_path, expected_value]})
return self
def assert_contained_by(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"contained_by": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"contained_by": [jmes_path, expected_value]}
)
return self
def assert_type_match(
self, jmes_path: Text, expected_value: Text
) -> "StepRequestValidation":
self.__t_step.validators.append({"type_match": [jmes_path, expected_value]})
self.__step_context.validators.append(
{"type_match": [jmes_path, expected_value]}
)
return self
def perform(self) -> TStep:
return self.__t_step
return self.__step_context
class StepRequestExtraction(object):
def __init__(self, step: TStep):
self.__t_step = step
def __init__(self, step_context: TStep):
self.__step_context = step_context
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
self.__t_step.extract[var_name] = jmes_path
self.__step_context.extract[var_name] = jmes_path
return self
# def with_regex(self):
@@ -199,135 +219,134 @@ class StepRequestExtraction(object):
# pass
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__t_step)
return StepRequestValidation(self.__step_context)
def perform(self) -> TStep:
return self.__t_step
return self.__step_context
class RequestWithOptionalArgs(object):
def __init__(self, step: TStep):
self.__t_step = step
def __init__(self, step_context: TStep):
self.__step_context = step_context
def with_params(self, **params) -> "RequestWithOptionalArgs":
self.__t_step.request.params.update(params)
self.__step_context.request.params.update(params)
return self
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
self.__t_step.request.headers.update(headers)
self.__step_context.request.headers.update(headers)
return self
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
self.__t_step.request.cookies.update(cookies)
self.__step_context.request.cookies.update(cookies)
return self
def with_data(self, data) -> "RequestWithOptionalArgs":
self.__t_step.request.data = data
self.__step_context.request.data = data
return self
def with_json(self, req_json) -> "RequestWithOptionalArgs":
self.__t_step.request.req_json = req_json
self.__step_context.request.req_json = req_json
return self
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
self.__t_step.request.timeout = timeout
self.__step_context.request.timeout = timeout
return self
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
self.__t_step.request.verify = verify
self.__step_context.request.verify = verify
return self
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
self.__t_step.request.allow_redirects = allow_redirects
self.__step_context.request.allow_redirects = allow_redirects
return self
def upload(self, **file_info) -> "RequestWithOptionalArgs":
self.__t_step.request.upload.update(file_info)
self.__step_context.request.upload.update(file_info)
return self
# def hooks(self):
# pass
def extract(self) -> StepRequestExtraction:
return StepRequestExtraction(self.__t_step)
return StepRequestExtraction(self.__step_context)
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__t_step)
return StepRequestValidation(self.__step_context)
def perform(self) -> TStep:
return self.__t_step
return self.__step_context
class RunRequest(object):
def __init__(self, name: Text):
self.__t_step = TStep(name=name)
self.__step_context = TStep(name=name)
def with_variables(self, **variables) -> "RunRequest":
self.__t_step.variables.update(variables)
self.__step_context.variables.update(variables)
return self
def get(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.GET, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
return RequestWithOptionalArgs(self.__step_context)
def post(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.POST, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.POST, url=url)
return RequestWithOptionalArgs(self.__step_context)
def put(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.PUT, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url)
return RequestWithOptionalArgs(self.__step_context)
def head(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.HEAD, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url)
return RequestWithOptionalArgs(self.__step_context)
def delete(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.DELETE, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url)
return RequestWithOptionalArgs(self.__step_context)
def options(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.OPTIONS, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url)
return RequestWithOptionalArgs(self.__step_context)
def patch(self, url: Text) -> RequestWithOptionalArgs:
self.__t_step.request = TRequest(method=MethodEnum.PATCH, url=url)
return RequestWithOptionalArgs(self.__t_step)
self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url)
return RequestWithOptionalArgs(self.__step_context)
class StepRefCase(object):
def __init__(self, step: TStep):
self.__t_step = step
self.__t_step.extract = []
def __init__(self, step_context: TStep):
self.__step_context = step_context
def extract(self, *var_name: Text) -> "StepRefCase":
self.__t_step.extract.extend(var_name)
def export(self, *var_name: Text) -> "StepRefCase":
self.__step_context.export.extend(var_name)
return self
def perform(self) -> TStep:
return self.__t_step
return self.__step_context
class RunTestCase(object):
def __init__(self, name: Text):
self.__t_step = TStep(name=name)
self.__step_context = TStep(name=name)
def with_variables(self, **variables) -> "RunTestCase":
self.__t_step.variables.update(variables)
self.__step_context.variables.update(variables)
return self
def call(self, testcase: Callable) -> StepRefCase:
self.__t_step.testcase = testcase
return StepRefCase(self.__t_step)
self.__step_context.testcase = testcase
return StepRefCase(self.__step_context)
def perform(self) -> TStep:
return self.__t_step
return self.__step_context
class Step(object):
def __init__(
self,
step: Union[
step_context: Union[
StepRequestValidation,
StepRequestExtraction,
RequestWithOptionalArgs,
@@ -335,15 +354,15 @@ class Step(object):
StepRefCase,
],
):
self.__t_step = step.perform()
self.__step_context = step_context.perform()
@property
def request(self) -> TRequest:
return self.__t_step.request
return self.__step_context.request
@property
def testcase(self) -> TestCase:
return self.__t_step.testcase
return self.__step_context.testcase
def perform(self) -> TStep:
return self.__t_step
return self.__step_context

View File

@@ -2,8 +2,9 @@ import collections
import json
import os.path
import platform
import string
import uuid
from typing import Dict, List, Any
from typing import Dict, List, Any, Text
import sentry_sdk
from loguru import logger
@@ -176,3 +177,38 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
return dict(
sorted(raw_dict.items(), key=lambda i: get_index_from_list(custom_order, i[0]))
)
def ensure_file_path_valid(file_path: Text) -> Text:
""" ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space
Args:
file_path: absolute or relative file path
Returns:
ensured valid absolute file path
"""
raw_file_name, file_suffix = os.path.splitext(file_path)
file_suffix = file_suffix.lower()
if os.path.isabs(file_path):
raw_file_relative_name = raw_file_name[len(os.getcwd()) + 1 :]
else:
raw_file_relative_name = raw_file_name
path_names = []
for name in raw_file_relative_name.rstrip(os.sep).split(os.sep):
if name[0] in string.digits:
# ensure file name not startswith digit
# 19 => T19, 2C => T2C
name = f"T{name}"
# handle cases when directory name includes dot/hyphen/space
name = name.replace(" ", "_").replace(".", "_").replace("-", "_")
path_names.append(name)
new_file_path = os.path.join(os.getcwd(), f"{os.sep.join(path_names)}{file_suffix}")
return new_file_path

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "httprunner"
version = "3.0.8"
version = "3.0.9"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"

View File

@@ -40,9 +40,10 @@ class TestCli(unittest.TestCase):
self.assertIn(__description__, self.captured_output.getvalue().strip())
def test_debug_pytest(self):
pytest.main(
exit_code = pytest.main(
[
"-s",
"examples/postman_echo/request_methods/request_with_testcase_reference_test.py",
]
)
self.assertEqual(exit_code, 0)

View File

@@ -153,9 +153,7 @@ class TestCompat(unittest.TestCase):
compat.ensure_cli_args(args2),
["examples/postman_echo/request_methods/hardcode.yml"],
)
self.assertTrue(
os.path.isfile("examples/postman_echo/request_methods/conftest.py")
)
self.assertTrue(os.path.isfile("examples/postman_echo/conftest.py"))
args3 = [
"examples/postman_echo/request_methods/hardcode.yml",

30
tests/data/a-b.c/1.yml Normal file
View File

@@ -0,0 +1,30 @@
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.sum_v", "3"]
- eq: ["body.args.foo2", "session_bar2"]

30
tests/data/a-b.c/2 3.yml Normal file
View File

@@ -0,0 +1,30 @@
config:
name: "reference testcase unittest for abnormal folder path"
variables:
foo1: session_bar1
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: request with functions
variables:
foo1: override_bar1
testcase: 1.yml
export:
- session_foo2
-
name: post form data
variables:
foo1: bar1
request:
method: POST
url: /post
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$session_foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.form.foo1", "session_bar1"]
- eq: ["body.form.foo2", "session_bar2"]

View File

@@ -0,0 +1,13 @@
from httprunner import __version__
def get_httprunner_version():
return __version__
def sum_two(m, n):
return m + n
def get_variables():
return {"foo1": "session_bar1"}

View File

@@ -1,34 +1,45 @@
import os
import unittest
from httprunner.make import (
main_make,
convert_testcase_path,
make_files_cache_set,
pytest_files_made_cache_mapping,
make_config_chain_style,
make_teststep_chain_style,
pytest_files_set,
pytest_files_run_set,
)
from httprunner import loader
class TestMake(unittest.TestCase):
def setUp(self) -> None:
pytest_files_made_cache_mapping.clear()
pytest_files_run_set.clear()
loader.project_meta = None
def test_make_testcase(self):
path = ["examples/postman_echo/request_methods/request_with_variables.yml"]
testcase_python_list = main_make(path)
self.assertEqual(
testcase_python_list[0],
"examples/postman_echo/request_methods/request_with_variables_test.py",
os.path.join(
os.getcwd(),
"examples/postman_echo/request_methods/request_with_variables_test.py",
),
)
def test_make_testcase_with_ref(self):
path = [
"examples/postman_echo/request_methods/request_with_testcase_reference.yml"
]
make_files_cache_set.clear()
pytest_files_set.clear()
testcase_python_list = main_make(path)
self.assertEqual(len(testcase_python_list), 1)
self.assertIn(
"examples/postman_echo/request_methods/request_with_testcase_reference_test.py",
os.path.join(
os.getcwd(),
"examples/postman_echo/request_methods/request_with_testcase_reference_test.py",
),
testcase_python_list,
)
@@ -52,56 +63,51 @@ from examples.postman_echo.request_methods.request_with_functions_test import (
path = ["examples/postman_echo/request_methods/"]
testcase_python_list = main_make(path)
self.assertIn(
"examples/postman_echo/request_methods/request_with_functions_test.py",
os.path.join(
os.getcwd(),
"examples/postman_echo/request_methods/request_with_functions_test.py",
),
testcase_python_list,
)
def test_convert_testcase_path(self):
self.assertEqual(
convert_testcase_path("mubu.login.yml")[0], "mubu_login_test.py"
convert_testcase_path("mubu.login.yml"),
(os.path.join(os.getcwd(), "mubu_login_test.py"), "MubuLogin"),
)
self.assertEqual(
convert_testcase_path("/path/to/mubu.login.yml")[0],
"/path/to/mubu_login_test.py",
convert_testcase_path(os.path.join(os.getcwd(), "path/to/mubu.login.yml")),
(os.path.join(os.getcwd(), "path/to/mubu_login_test.py"), "MubuLogin"),
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu.login.yml")[0],
"/path/to 2/mubu_login_test.py",
convert_testcase_path("path/to 2/mubu.login.yml"),
(os.path.join(os.getcwd(), "path/to_2/mubu_login_test.py"), "MubuLogin"),
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu.login.yml")[1], "MubuLogin"
convert_testcase_path("path/to-2/mubu login.yml"),
(os.path.join(os.getcwd(), "path/to_2/mubu_login_test.py"), "MubuLogin"),
)
self.assertEqual(
convert_testcase_path("mubu login.yml")[0], "mubu_login_test.py"
convert_testcase_path("path/to.2/幕布login.yml"),
(os.path.join(os.getcwd(), "path/to_2/幕布login_test.py"), "幕布Login"),
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu login.yml")[1], "MubuLogin"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu-login.yml")[0],
"/path/to 2/mubu_login_test.py",
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu-login.yml")[1], "MubuLogin"
)
self.assertEqual(
convert_testcase_path("/path/to 2/幕布login.yml")[0],
"/path/to 2/幕布login_test.py",
)
self.assertEqual(convert_testcase_path("/path/to/幕布login.yml")[1], "幕布Login")
def test_make_testsuite(self):
path = ["examples/postman_echo/request_methods/demo_testsuite.yml"]
make_files_cache_set.clear()
pytest_files_set.clear()
testcase_python_list = main_make(path)
self.assertEqual(len(testcase_python_list), 2)
self.assertIn(
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py",
os.path.join(
os.getcwd(),
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py",
),
testcase_python_list,
)
self.assertIn(
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py",
os.path.join(
os.getcwd(),
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py",
),
testcase_python_list,
)
@@ -109,13 +115,13 @@ from examples.postman_echo.request_methods.request_with_functions_test import (
config = {
"name": "request methods testcase: validate with functions",
"variables": {"foo1": "bar1", "foo2": 22},
"base_url": "https://postman-echo.com",
"base_url": "https://postman_echo.com",
"verify": False,
"path": "examples/postman_echo/request_methods/validate_with_functions_test.py",
}
self.assertEqual(
make_config_chain_style(config),
"""Config("request methods testcase: validate with functions").variables(**{'foo1': 'bar1', 'foo2': 22}).base_url("https://postman-echo.com").verify(False)""",
"""Config("request methods testcase: validate with functions").variables(**{'foo1': 'bar1', 'foo2': 22}).base_url("https://postman_echo.com").verify(False)""",
)
def test_make_teststep_chain_style(self):

View File

@@ -1,10 +1,14 @@
import os
import unittest
from httprunner import loader
from httprunner.cli import main_run
from httprunner.runner import HttpRunner
class TestHttpRunner(unittest.TestCase):
def setUp(self):
loader.project_meta = None
self.runner = HttpRunner()
def test_run_testcase_by_path_request_only(self):
@@ -26,3 +30,11 @@ class TestHttpRunner(unittest.TestCase):
self.assertEqual(result.name, "request methods testcase: reference testcase")
self.assertEqual(result.step_datas[0].name, "request with functions")
self.assertEqual(len(result.step_datas), 2)
def test_run_testcase_with_abnormal_path(self):
exit_code = main_run(["tests/data/a-b.c/2 3.yml"])
self.assertEqual(exit_code, 0)
self.assertTrue(os.path.exists("tests/data/a_b_c/__init__.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/debugtalk.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/T1_test.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/T2_3_test.py"))

View File

@@ -2,6 +2,7 @@ import os
import unittest
from httprunner import loader, utils
from httprunner.utils import ensure_file_path_valid
class TestUtils(unittest.TestCase):
@@ -97,3 +98,21 @@ class TestUtils(unittest.TestCase):
),
["A", "D", "C", "B"],
)
def test_ensure_file_path_valid(self):
self.assertEqual(
ensure_file_path_valid("examples/a-b.c/d f/hardcode.yml"),
os.path.join(os.getcwd(), "examples/a_b_c/d_f/hardcode.yml"),
)
self.assertEqual(
ensure_file_path_valid("1/2B/3.yml"),
os.path.join(os.getcwd(), "T1/T2B/T3.yml"),
)
self.assertEqual(
ensure_file_path_valid("examples/a-b.c/2B/hardcode.yml"),
os.path.join(os.getcwd(), "examples/a_b_c/T2B/hardcode.yml"),
)
self.assertEqual(
ensure_file_path_valid("examples/postman_echo/request_methods/"),
os.path.join(os.getcwd(), "examples/postman_echo/request_methods"),
)