mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
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:
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ teststeps:
|
||||
variables:
|
||||
foo1: override_bar1
|
||||
testcase: request_methods/request_with_functions.yml
|
||||
extract:
|
||||
export:
|
||||
- session_foo2
|
||||
-
|
||||
name: post form data
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
30
tests/data/a-b.c/1.yml
Normal 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
30
tests/data/a-b.c/2 3.yml
Normal 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"]
|
||||
13
tests/data/a-b.c/debugtalk.py
Normal file
13
tests/data/a-b.c/debugtalk.py
Normal 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"}
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user