mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-12 03:09:43 +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
|
# 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)
|
## 3.0.8 (2020-06-04)
|
||||||
|
|
||||||
**Added**
|
**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: examples/httpbin/basic.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/httpbin/hooks.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/httpbin/load_image.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/httpbin/upload.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/httpbin/validate.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/postman_echo/request_methods/request_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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
|
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -26,7 +26,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
|
|||||||
RunTestCase("request with functions")
|
RunTestCase("request with functions")
|
||||||
.with_variables(**{"foo1": "override_bar1"})
|
.with_variables(**{"foo1": "override_bar1"})
|
||||||
.call(RequestWithFunctions)
|
.call(RequestWithFunctions)
|
||||||
.extract(*["session_foo2"])
|
.export(*["session_foo2"])
|
||||||
),
|
),
|
||||||
Step(
|
Step(
|
||||||
RunRequest("post form data")
|
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: examples/postman_echo/request_methods/hardcode.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/postman_echo/request_methods/request_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ teststeps:
|
|||||||
variables:
|
variables:
|
||||||
foo1: override_bar1
|
foo1: override_bar1
|
||||||
testcase: request_methods/request_with_functions.yml
|
testcase: request_methods/request_with_functions.yml
|
||||||
extract:
|
export:
|
||||||
- session_foo2
|
- session_foo2
|
||||||
-
|
-
|
||||||
name: post form data
|
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
|
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -26,7 +26,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
|
|||||||
RunTestCase("request with functions")
|
RunTestCase("request with functions")
|
||||||
.with_variables(**{"foo1": "override_bar1"})
|
.with_variables(**{"foo1": "override_bar1"})
|
||||||
.call(RequestWithFunctions)
|
.call(RequestWithFunctions)
|
||||||
.extract(*["session_foo2"])
|
.export(*["session_foo2"])
|
||||||
),
|
),
|
||||||
Step(
|
Step(
|
||||||
RunRequest("post form data")
|
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: examples/postman_echo/request_methods/request_with_variables.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/postman_echo/request_methods/validate_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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: examples/postman_echo/request_methods/validate_with_variables.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
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."
|
__description__ = "One-stop solution for HTTP(S) testing."
|
||||||
|
|
||||||
from httprunner.runner import HttpRunner
|
from httprunner.runner import HttpRunner
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from httprunner.runner import HttpRunner
|
from httprunner.runner import HttpRunner
|
||||||
from httprunner.schema import ProjectMeta, TestCase
|
from httprunner.models import ProjectMeta, TestCase
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
runner = HttpRunner()
|
runner = HttpRunner()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import enum
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ def init_parser_run(subparsers):
|
|||||||
return sub_parser_run
|
return sub_parser_run
|
||||||
|
|
||||||
|
|
||||||
def main_run(extra_args):
|
def main_run(extra_args) -> enum.IntEnum:
|
||||||
capture_message("start to run")
|
capture_message("start to run")
|
||||||
# keep compatibility with v2
|
# keep compatibility with v2
|
||||||
extra_args = ensure_cli_args(extra_args)
|
extra_args = ensure_cli_args(extra_args)
|
||||||
@@ -48,8 +49,11 @@ def main_run(extra_args):
|
|||||||
logger.error("No valid testcases found, exit 1.")
|
logger.error("No valid testcases found, exit 1.")
|
||||||
sys.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)
|
extra_args_new.extend(testcase_path_list)
|
||||||
sys.exit(pytest.main(extra_args_new))
|
return pytest.main(extra_args_new)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -109,7 +113,7 @@ def main():
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if sys.argv[1] == "run":
|
if sys.argv[1] == "run":
|
||||||
main_run(extra_args)
|
sys.exit(main_run(extra_args))
|
||||||
elif sys.argv[1] == "startproject":
|
elif sys.argv[1] == "startproject":
|
||||||
main_scaffold(args)
|
main_scaffold(args)
|
||||||
elif sys.argv[1] == "har2case":
|
elif sys.argv[1] == "har2case":
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ from requests.exceptions import (
|
|||||||
MissingSchema,
|
MissingSchema,
|
||||||
RequestException,
|
RequestException,
|
||||||
)
|
)
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
from httprunner.schema import RequestData, ResponseData
|
from httprunner.models import RequestData, ResponseData
|
||||||
from httprunner.schema import SessionData, ReqRespData
|
from httprunner.models import SessionData, ReqRespData
|
||||||
from httprunner.utils import lower_dict_keys, omit_long_data
|
from httprunner.utils import lower_dict_keys, omit_long_data
|
||||||
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -51,12 +50,12 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# str: a=1&b=2
|
# str: a=1&b=2
|
||||||
pass
|
pass
|
||||||
except UnicodeDecodeError as ex:
|
except UnicodeDecodeError:
|
||||||
# bytes/bytearray: request body in protobuf
|
# bytes/bytearray: request body in protobuf
|
||||||
capture_exception(ex)
|
pass
|
||||||
except TypeError as ex:
|
except TypeError:
|
||||||
# neither str nor bytes/bytearray, e.g. <MultipartEncoder>
|
# neither str nor bytes/bytearray, e.g. <MultipartEncoder>
|
||||||
capture_exception(ex)
|
pass
|
||||||
|
|
||||||
request_content_type = lower_dict_keys(request_headers).get("content-type")
|
request_content_type = lower_dict_keys(request_headers).get("content-type")
|
||||||
if request_content_type and "multipart/form-data" in request_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 loguru import logger
|
||||||
|
|
||||||
|
from httprunner import exceptions
|
||||||
from httprunner.loader import load_project_meta
|
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:
|
def convert_jmespath(raw: Text) -> Text:
|
||||||
@@ -53,10 +54,12 @@ def convert_extractors(extractors: Union[List, Dict]) -> Dict:
|
|||||||
v3_extractors: Dict = {}
|
v3_extractors: Dict = {}
|
||||||
|
|
||||||
if isinstance(extractors, List):
|
if isinstance(extractors, List):
|
||||||
|
# [{"varA": "content.varA"}, {"varB": "json.varB"}]
|
||||||
for extractor in extractors:
|
for extractor in extractors:
|
||||||
for k, v in extractor.items():
|
for k, v in extractor.items():
|
||||||
v3_extractors[k] = v
|
v3_extractors[k] = v
|
||||||
elif isinstance(extractors, Dict):
|
elif isinstance(extractors, Dict):
|
||||||
|
# {"varA": "body.varA", "varB": "body.varB"}
|
||||||
v3_extractors = extractors
|
v3_extractors = extractors
|
||||||
else:
|
else:
|
||||||
logger.error(f"Invalid extractor: {extractors}")
|
logger.error(f"Invalid extractor: {extractors}")
|
||||||
@@ -133,10 +136,10 @@ def ensure_step_attachment(step: Dict) -> Dict:
|
|||||||
test_dict["teardown_hooks"] = step["teardown_hooks"]
|
test_dict["teardown_hooks"] = step["teardown_hooks"]
|
||||||
|
|
||||||
if "extract" in step:
|
if "extract" in step:
|
||||||
if step.get("request"):
|
test_dict["extract"] = convert_extractors(step["extract"])
|
||||||
test_dict["extract"] = convert_extractors(step["extract"])
|
|
||||||
elif step.get("testcase"):
|
if "export" in step:
|
||||||
test_dict["extract"] = step["extract"]
|
test_dict["export"] = step["export"]
|
||||||
|
|
||||||
if "validate" in step:
|
if "validate" in step:
|
||||||
test_dict["validate"] = convert_validators(step["validate"])
|
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"]:
|
for step in test_content["teststeps"]:
|
||||||
teststep = {}
|
teststep = {}
|
||||||
|
|
||||||
teststep.update(ensure_step_attachment(step))
|
|
||||||
|
|
||||||
if "request" in step:
|
if "request" in step:
|
||||||
teststep["request"] = step.pop("request")
|
teststep["request"] = step.pop("request")
|
||||||
elif "api" in step:
|
elif "api" in step:
|
||||||
teststep["testcase"] = step.pop("api")
|
teststep["testcase"] = step.pop("api")
|
||||||
elif "testcase" in step:
|
elif "testcase" in step:
|
||||||
teststep["testcase"] = step.pop("testcase")
|
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)
|
teststep = sort_step_by_custom_order(teststep)
|
||||||
v3_content["teststeps"].append(teststep)
|
v3_content["teststeps"].append(teststep)
|
||||||
@@ -215,10 +220,8 @@ def generate_conftest_for_summary(args: List):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
project_meta = load_project_meta(test_path)
|
project_meta = load_project_meta(test_path)
|
||||||
conftest_path = os.path.join(project_meta.PWD, "conftest.py")
|
project_root_dir = ensure_file_path_valid(project_meta.RootDir)
|
||||||
if os.path.isfile(conftest_path):
|
conftest_path = os.path.join(project_root_dir, "conftest.py")
|
||||||
return
|
|
||||||
|
|
||||||
conftest_content = '''# NOTICE: Generated By HttpRunner.
|
conftest_content = '''# NOTICE: Generated By HttpRunner.
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -286,8 +289,8 @@ def session_fixture(request):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
test_path = os.path.abspath(test_path)
|
test_path = os.path.abspath(test_path)
|
||||||
logs_dir_path = os.path.join(project_meta.PWD, "logs")
|
logs_dir_path = os.path.join(project_root_dir, "logs")
|
||||||
test_path_relative_path = test_path[len(project_meta.PWD) + 1 :]
|
test_path_relative_path = test_path[len(project_root_dir) + 1 :]
|
||||||
|
|
||||||
if os.path.isdir(test_path):
|
if os.path.isdir(test_path):
|
||||||
file_foder_path = os.path.join(logs_dir_path, test_path_relative_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
|
"{{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:
|
with open(conftest_path, "w", encoding="utf-8") as f:
|
||||||
f.write(conftest_content)
|
f.write(conftest_content)
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from typing import Text, NoReturn
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from httprunner.parser import parse_variables_mapping
|
from httprunner.parser import parse_variables_mapping
|
||||||
from httprunner.schema import TStep, FunctionsMapping
|
from httprunner.models import TStep, FunctionsMapping
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import filetype
|
import filetype
|
||||||
@@ -139,7 +139,7 @@ def multipart_encoder(**kwargs):
|
|||||||
|
|
||||||
project_meta = load_project_meta(os.getcwd())
|
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)
|
is_exists_file = os.path.isfile(_file_path)
|
||||||
|
|
||||||
if is_exists_file:
|
if is_exists_file:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from pydantic import ValidationError
|
|||||||
|
|
||||||
from httprunner import builtin, utils
|
from httprunner import builtin, utils
|
||||||
from httprunner import exceptions
|
from httprunner import exceptions
|
||||||
from httprunner.schema import TestCase, ProjectMeta, TestSuite
|
from httprunner.models import TestCase, ProjectMeta, TestSuite
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# PyYAML version >= 5.1
|
# 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!")
|
raise exceptions.MyBaseFailure("load_project_meta() has not been called!")
|
||||||
|
|
||||||
# make compatible with Windows/Linux
|
# 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):
|
if not os.path.isfile(csv_file):
|
||||||
# file path not exist
|
# 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:
|
def locate_file(start_path: Text, file_name: Text) -> Text:
|
||||||
""" locate filename and return absolute file path.
|
""" 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:
|
Args:
|
||||||
file_name (str): target locate file name
|
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):
|
if os.path.isfile(file_path):
|
||||||
return os.path.abspath(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
|
# system root dir
|
||||||
# Windows, e.g. 'E:\\'
|
# Windows, e.g. 'E:\\'
|
||||||
# Linux/Darwin, '/'
|
# Linux/Darwin, '/'
|
||||||
@@ -327,14 +320,14 @@ def locate_debugtalk_py(start_path: Text) -> Text:
|
|||||||
return debugtalk_path
|
return debugtalk_path
|
||||||
|
|
||||||
|
|
||||||
def locate_project_working_directory(test_path: Text) -> Tuple[Text, Text]:
|
def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]:
|
||||||
""" locate debugtalk.py path as project working directory
|
""" locate debugtalk.py path as project root directory
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
test_path: specified testfile path
|
test_path: specified testfile path
|
||||||
|
|
||||||
Returns:
|
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)
|
debugtalk_path = locate_debugtalk_py(test_path)
|
||||||
|
|
||||||
if debugtalk_path:
|
if debugtalk_path:
|
||||||
# The folder contains debugtalk.py will be treated as PWD.
|
# The folder contains debugtalk.py will be treated as project RootDir.
|
||||||
project_working_directory = os.path.dirname(debugtalk_path)
|
project_root_directory = os.path.dirname(debugtalk_path)
|
||||||
else:
|
else:
|
||||||
# debugtalk.py not found, use os.getcwd() as PWD.
|
# debugtalk.py not found, use os.getcwd() as project RootDir.
|
||||||
project_working_directory = os.getcwd()
|
project_root_directory = os.getcwd()
|
||||||
|
|
||||||
return debugtalk_path, project_working_directory
|
return debugtalk_path, project_root_directory
|
||||||
|
|
||||||
|
|
||||||
def load_debugtalk_functions() -> Dict[Text, Callable]:
|
def load_debugtalk_functions() -> Dict[Text, Callable]:
|
||||||
""" load project debugtalk.py module functions
|
""" 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:
|
Returns:
|
||||||
dict: debugtalk module functions mapping
|
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:
|
def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
|
||||||
""" load api, testcases, .env, debugtalk.py functions.
|
""" load testcases, .env, debugtalk.py functions.
|
||||||
api/testcases folder is relative to project_working_directory
|
testcases folder is relative to project_root_directory
|
||||||
by default, project_meta will be loaded only once, unless set reload to true.
|
by default, project_meta will be loaded only once, unless set reload to true.
|
||||||
|
|
||||||
Args:
|
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
|
reload: reload project meta if set true, default to false
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -404,19 +397,20 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
|
|||||||
if not test_path:
|
if not test_path:
|
||||||
return project_meta
|
return project_meta
|
||||||
|
|
||||||
debugtalk_path, project_working_directory = locate_project_working_directory(
|
debugtalk_path, project_root_directory = locate_project_root_directory(test_path)
|
||||||
test_path
|
|
||||||
)
|
|
||||||
|
|
||||||
# add PWD to sys.path
|
# add project RootDir to sys.path
|
||||||
sys.path.insert(0, project_working_directory)
|
sys.path.insert(0, project_root_directory)
|
||||||
|
|
||||||
# load .env file
|
# load .env file
|
||||||
# NOTICE:
|
# NOTICE:
|
||||||
# environment variable maybe loaded in debugtalk.py
|
# environment variable maybe loaded in debugtalk.py
|
||||||
# thus .env file should be loaded before loading debugtalk.py
|
# thus .env file should be loaded before loading debugtalk.py
|
||||||
dot_env_path = os.path.join(project_working_directory, ".env")
|
dot_env_path = os.path.join(project_root_directory, ".env")
|
||||||
project_meta.env = load_dot_env_file(dot_env_path)
|
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:
|
if debugtalk_path:
|
||||||
# load debugtalk.py functions
|
# load debugtalk.py functions
|
||||||
@@ -424,11 +418,9 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
|
|||||||
else:
|
else:
|
||||||
debugtalk_functions = {}
|
debugtalk_functions = {}
|
||||||
|
|
||||||
# locate PWD and load debugtalk.py functions
|
# locate project RootDir and load debugtalk.py functions
|
||||||
project_meta.PWD = project_working_directory
|
project_meta.RootDir = project_root_directory
|
||||||
project_meta.functions = debugtalk_functions
|
project_meta.functions = debugtalk_functions
|
||||||
project_meta.test_path = os.path.abspath(test_path)[
|
project_meta.debugtalk_path = debugtalk_path
|
||||||
len(project_working_directory) + 1 :
|
|
||||||
]
|
|
||||||
|
|
||||||
return project_meta
|
return project_meta
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import string
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from shutil import copyfile
|
||||||
from typing import Text, List, Tuple, Dict, Set, NoReturn
|
from typing import Text, List, Tuple, Dict, Set, NoReturn
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
@@ -18,14 +18,18 @@ from httprunner.loader import (
|
|||||||
)
|
)
|
||||||
from httprunner.parser import parse_data
|
from httprunner.parser import parse_data
|
||||||
from httprunner.response import uniform_validator
|
from httprunner.response import uniform_validator
|
||||||
|
from httprunner.utils import ensure_file_path_valid
|
||||||
|
|
||||||
""" cache converted pytest files, avoid duplicate making
|
""" cache converted pytest files, avoid duplicate making
|
||||||
"""
|
"""
|
||||||
make_files_cache_set: Set = set()
|
pytest_files_made_cache_mapping: Dict[Text, Text] = {}
|
||||||
pytest_files_set: Set = set()
|
|
||||||
|
""" save generated pytest files to run, except referenced testcase
|
||||||
|
"""
|
||||||
|
pytest_files_run_set: Set = set()
|
||||||
|
|
||||||
__TEMPLATE__ = jinja2.Template(
|
__TEMPLATE__ = jinja2.Template(
|
||||||
"""# NOTICE: Generated By HttpRunner v{{ version }}
|
"""# NOTE: Generated By HttpRunner v{{ version }}
|
||||||
# FROM: {{ testcase_path }}
|
# FROM: {{ testcase_path }}
|
||||||
{% if imports_list %}
|
{% if imports_list %}
|
||||||
import os
|
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:
|
def __ensure_absolute(path: Text) -> Text:
|
||||||
project_meta = load_project_meta(path)
|
project_meta = load_project_meta(path)
|
||||||
|
|
||||||
if os.path.isabs(path):
|
if os.path.isabs(path):
|
||||||
absolute_path = path
|
absolute_path = path
|
||||||
else:
|
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
|
return absolute_path
|
||||||
|
|
||||||
@@ -102,24 +98,38 @@ def __ensure_testcase_module(path: Text) -> NoReturn:
|
|||||||
f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n")
|
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]:
|
def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
|
||||||
"""convert single YAML/JSON testcase path to python file"""
|
"""convert single YAML/JSON testcase path to python file"""
|
||||||
if os.path.isdir(testcase_path):
|
testcase_new_path = ensure_file_path_valid(testcase_path)
|
||||||
# folder does not need to convert
|
|
||||||
return testcase_path, ""
|
|
||||||
|
|
||||||
testcase_path = __ensure_file_name(testcase_path)
|
dir_path = os.path.dirname(testcase_new_path)
|
||||||
raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_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")
|
||||||
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")
|
|
||||||
|
|
||||||
# convert title case, e.g. request_with_variables => RequestWithVariables
|
# convert title case, e.g. request_with_variables => RequestWithVariables
|
||||||
name_in_title_case = file_name.title().replace("_", "")
|
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})"
|
call_ref_testcase = f".call({testcase})"
|
||||||
step_info += call_ref_testcase
|
step_info += call_ref_testcase
|
||||||
|
|
||||||
extract_info = teststep.get("extract")
|
if "extract" in teststep:
|
||||||
if extract_info:
|
# request step
|
||||||
if isinstance(extract_info, Dict):
|
step_info += ".extract()"
|
||||||
# request step
|
for extract_name, extract_path in teststep["extract"].items():
|
||||||
step_info += ".extract()"
|
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
|
||||||
for extract_name, extract_path in extract_info.items():
|
|
||||||
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
|
if "export" in teststep:
|
||||||
elif isinstance(extract_info, List):
|
# reference testcase step
|
||||||
# reference testcase step
|
export: List[Text] = teststep["export"]
|
||||||
step_info += f".extract(*{extract_info})"
|
step_info += f".export(*{export})"
|
||||||
else:
|
|
||||||
raise exceptions.TestCaseFormatError(f"Invalid extract: {extract_info}")
|
|
||||||
|
|
||||||
if "validate" in teststep:
|
if "validate" in teststep:
|
||||||
step_info += ".validate()"
|
step_info += ".validate()"
|
||||||
@@ -253,9 +261,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
|||||||
return f"Step({step_info})"
|
return f"Step({step_info})"
|
||||||
|
|
||||||
|
|
||||||
def make_testcase(
|
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
||||||
testcase: Dict, dir_path: Text = None, ref_flag: bool = False,
|
|
||||||
) -> Text:
|
|
||||||
"""convert valid testcase dict to pytest file path"""
|
"""convert valid testcase dict to pytest file path"""
|
||||||
# ensure compatibility with testcase format v2
|
# ensure compatibility with testcase format v2
|
||||||
testcase = ensure_testcase_v3(testcase)
|
testcase = ensure_testcase_v3(testcase)
|
||||||
@@ -263,17 +269,17 @@ def make_testcase(
|
|||||||
# validate testcase format
|
# validate testcase format
|
||||||
load_testcase(testcase)
|
load_testcase(testcase)
|
||||||
|
|
||||||
testcase_path = __ensure_absolute(testcase["config"]["path"])
|
testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
|
||||||
logger.info(f"start to make testcase: {testcase_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:
|
if dir_path:
|
||||||
testcase_python_path = os.path.join(
|
testcase_python_path = os.path.join(
|
||||||
dir_path, os.path.basename(testcase_python_path)
|
dir_path, os.path.basename(testcase_python_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
global make_files_cache_set
|
global pytest_files_made_cache_mapping
|
||||||
if testcase_python_path in make_files_cache_set:
|
if testcase_python_path in pytest_files_made_cache_mapping:
|
||||||
return testcase_python_path
|
return testcase_python_path
|
||||||
|
|
||||||
config = testcase["config"]
|
config = testcase["config"]
|
||||||
@@ -283,7 +289,7 @@ def make_testcase(
|
|||||||
config.setdefault("variables", {})
|
config.setdefault("variables", {})
|
||||||
if isinstance(config["variables"], Text):
|
if isinstance(config["variables"], Text):
|
||||||
# get variables by function, e.g. ${get_variables()}
|
# 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"] = parse_data(
|
||||||
config["variables"], {}, project_meta.functions
|
config["variables"], {}, project_meta.functions
|
||||||
)
|
)
|
||||||
@@ -297,12 +303,14 @@ def make_testcase(
|
|||||||
|
|
||||||
# make ref testcase pytest file
|
# make ref testcase pytest file
|
||||||
ref_testcase_path = __ensure_absolute(teststep["testcase"])
|
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
|
# prepare ref testcase class name
|
||||||
ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path(
|
ref_testcase_cls_name = pytest_files_made_cache_mapping[
|
||||||
ref_testcase_path
|
ref_testcase_python_path
|
||||||
)
|
]
|
||||||
teststep["testcase"] = ref_testcase_cls_name
|
teststep["testcase"] = ref_testcase_cls_name
|
||||||
|
|
||||||
# prepare import ref testcase
|
# prepare import ref testcase
|
||||||
@@ -315,7 +323,7 @@ def make_testcase(
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"testcase_path": __ensure_cwd_relative(testcase_path),
|
"testcase_path": __ensure_cwd_relative(testcase_abs_path),
|
||||||
"class_name": f"TestCase{testcase_cls_name}",
|
"class_name": f"TestCase{testcase_cls_name}",
|
||||||
"imports_list": imports_list,
|
"imports_list": imports_list,
|
||||||
"config_chain_style": make_config_chain_style(config),
|
"config_chain_style": make_config_chain_style(config),
|
||||||
@@ -325,16 +333,19 @@ def make_testcase(
|
|||||||
}
|
}
|
||||||
content = __TEMPLATE__.render(data)
|
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:
|
with open(testcase_python_path, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
pytest_files_made_cache_mapping[testcase_python_path] = testcase_cls_name
|
||||||
__ensure_testcase_module(testcase_python_path)
|
__ensure_testcase_module(testcase_python_path)
|
||||||
|
|
||||||
logger.info(f"generated testcase: {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
|
return testcase_python_path
|
||||||
|
|
||||||
|
|
||||||
@@ -357,11 +368,10 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
|
|||||||
logger.info(f"start to make testsuite: {testsuite_path}")
|
logger.info(f"start to make testsuite: {testsuite_path}")
|
||||||
|
|
||||||
# create directory with testsuite file name, put its testcases under this directory
|
# create directory with testsuite file name, put its testcases under this directory
|
||||||
testsuite_dir = os.path.join(
|
testsuite_path = ensure_file_path_valid(testsuite_path)
|
||||||
os.path.dirname(testsuite_path),
|
testsuite_dir, file_suffix = os.path.splitext(testsuite_path)
|
||||||
os.path.basename(testsuite_path).replace(".", "_"),
|
# demo_testsuite.yml => demo_testsuite_yml
|
||||||
)
|
testsuite_dir = f"{testsuite_dir}_{file_suffix.lstrip('.')}"
|
||||||
os.makedirs(testsuite_dir, exist_ok=True)
|
|
||||||
|
|
||||||
for testcase in testsuite["testcases"]:
|
for testcase in testsuite["testcases"]:
|
||||||
# get referenced testcase content
|
# get referenced testcase content
|
||||||
@@ -386,16 +396,16 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
|
|||||||
testcase_dict["config"]["variables"].update(testsuite_variables)
|
testcase_dict["config"]["variables"].update(testsuite_variables)
|
||||||
|
|
||||||
# make testcase
|
# 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
|
""" 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:
|
Args:
|
||||||
tests_path: should be in absolute path
|
tests_path: should be in absolute path
|
||||||
ref_flag: flag if referenced test path
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
test_files = []
|
test_files = []
|
||||||
@@ -409,7 +419,7 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn:
|
|||||||
|
|
||||||
for test_file in test_files:
|
for test_file in test_files:
|
||||||
if test_file.lower().endswith("_test.py"):
|
if test_file.lower().endswith("_test.py"):
|
||||||
pytest_files_set.add(test_file)
|
pytest_files_run_set.add(test_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -422,12 +432,16 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn:
|
|||||||
if "request" in test_content:
|
if "request" in test_content:
|
||||||
test_content = ensure_testcase_v3_api(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
|
test_content.setdefault("config", {})["path"] = test_file
|
||||||
|
|
||||||
# testcase
|
# testcase
|
||||||
if "teststeps" in test_content:
|
if "teststeps" in test_content:
|
||||||
try:
|
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:
|
except exceptions.TestCaseFormatError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -452,11 +466,13 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
|
|||||||
tests_path = os.path.join(os.getcwd(), tests_path)
|
tests_path = os.path.join(os.getcwd(), tests_path)
|
||||||
|
|
||||||
__make(tests_path)
|
__make(tests_path)
|
||||||
|
__ensure_project_meta_files(tests_path)
|
||||||
|
|
||||||
pytest_files_set.update(make_files_cache_set)
|
# format pytest files
|
||||||
pytest_files_list = list(pytest_files_set)
|
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
|
||||||
format_pytest_with_black(*pytest_files_list)
|
format_pytest_with_black(*pytest_files_format_list)
|
||||||
return pytest_files_list
|
|
||||||
|
return list(pytest_files_run_set)
|
||||||
|
|
||||||
|
|
||||||
def init_make_parser(subparsers):
|
def init_make_parser(subparsers):
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class TStep(BaseModel):
|
|||||||
variables: VariablesMapping = {}
|
variables: VariablesMapping = {}
|
||||||
setup_hooks: Hook = []
|
setup_hooks: Hook = []
|
||||||
teardown_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")
|
validators: Validators = Field([], alias="validate")
|
||||||
validate_script: List[Text] = []
|
validate_script: List[Text] = []
|
||||||
|
|
||||||
@@ -78,10 +81,11 @@ class TestCase(BaseModel):
|
|||||||
|
|
||||||
class ProjectMeta(BaseModel):
|
class ProjectMeta(BaseModel):
|
||||||
debugtalk_py: Text = "" # debugtalk.py file content
|
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 = {}
|
env: Env = {}
|
||||||
PWD: Text = os.getcwd()
|
RootDir: Text = os.getcwd() # project root directory, the path debugtalk.py located
|
||||||
test_path: Text = None # run with specified test path
|
|
||||||
|
|
||||||
|
|
||||||
class TestsMapping(BaseModel):
|
class TestsMapping(BaseModel):
|
||||||
@@ -96,8 +100,8 @@ class TestCaseTime(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TestCaseInOut(BaseModel):
|
class TestCaseInOut(BaseModel):
|
||||||
vars: VariablesMapping = {}
|
config_vars: VariablesMapping = {}
|
||||||
export: Dict = {}
|
export_vars: Dict = {}
|
||||||
|
|
||||||
|
|
||||||
class RequestStat(BaseModel):
|
class RequestStat(BaseModel):
|
||||||
@@ -145,7 +149,7 @@ class StepData(BaseModel):
|
|||||||
success: bool = False
|
success: bool = False
|
||||||
name: Text = "" # teststep name
|
name: Text = "" # teststep name
|
||||||
data: Union[SessionData, List[SessionData]] = None
|
data: Union[SessionData, List[SessionData]] = None
|
||||||
export: Dict = {}
|
export_vars: VariablesMapping = {}
|
||||||
|
|
||||||
|
|
||||||
class TestCaseSummary(BaseModel):
|
class TestCaseSummary(BaseModel):
|
||||||
@@ -6,7 +6,7 @@ from typing import Any, Set, Text, Callable, List, Dict
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
from httprunner import loader, utils, exceptions
|
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)
|
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.exceptions import ValidationFailure, ParamsError
|
||||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
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):
|
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.parser import build_url, parse_data, parse_variables_mapping
|
||||||
from httprunner.response import ResponseObject
|
from httprunner.response import ResponseObject
|
||||||
from httprunner.testcase import Config, Step
|
from httprunner.testcase import Config, Step
|
||||||
from httprunner.schema import (
|
from httprunner.models import (
|
||||||
TConfig,
|
TConfig,
|
||||||
TStep,
|
TStep,
|
||||||
VariablesMapping,
|
VariablesMapping,
|
||||||
@@ -43,10 +43,10 @@ class HttpRunner(object):
|
|||||||
__teststeps: List[TStep]
|
__teststeps: List[TStep]
|
||||||
__project_meta: ProjectMeta = None
|
__project_meta: ProjectMeta = None
|
||||||
__case_id: Text = ""
|
__case_id: Text = ""
|
||||||
|
__export: List[Text] = []
|
||||||
__step_datas: List[StepData] = None
|
__step_datas: List[StepData] = None
|
||||||
__session: HttpSession = None
|
__session: HttpSession = None
|
||||||
__session_variables: VariablesMapping = {}
|
__session_variables: VariablesMapping = {}
|
||||||
__export_variables: VariablesMapping = {}
|
|
||||||
# time
|
# time
|
||||||
__start_at: float = 0
|
__start_at: float = 0
|
||||||
__duration: float = 0
|
__duration: float = 0
|
||||||
@@ -82,6 +82,10 @@ class HttpRunner(object):
|
|||||||
self.__session_variables = variables
|
self.__session_variables = variables
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def with_export(self, export: List[Text]) -> "HttpRunner":
|
||||||
|
self.__export = export
|
||||||
|
return self
|
||||||
|
|
||||||
def __run_step_request(self, step: TStep) -> StepData:
|
def __run_step_request(self, step: TStep) -> StepData:
|
||||||
"""run teststep: request"""
|
"""run teststep: request"""
|
||||||
step_data = StepData(name=step.name)
|
step_data = StepData(name=step.name)
|
||||||
@@ -134,7 +138,7 @@ class HttpRunner(object):
|
|||||||
# extract
|
# extract
|
||||||
extractors = step.extract
|
extractors = step.extract
|
||||||
extract_mapping = resp_obj.extract(extractors)
|
extract_mapping = resp_obj.extract(extractors)
|
||||||
step_data.export = extract_mapping
|
step_data.export_vars = extract_mapping
|
||||||
|
|
||||||
variables_mapping = step.variables
|
variables_mapping = step.variables
|
||||||
variables_mapping.update(extract_mapping)
|
variables_mapping.update(extract_mapping)
|
||||||
@@ -166,6 +170,7 @@ class HttpRunner(object):
|
|||||||
"""run teststep: referenced testcase"""
|
"""run teststep: referenced testcase"""
|
||||||
step_data = StepData(name=step.name)
|
step_data = StepData(name=step.name)
|
||||||
step_variables = step.variables
|
step_variables = step.variables
|
||||||
|
step_export = step.export
|
||||||
|
|
||||||
if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
|
if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
|
||||||
testcase_cls = step.testcase
|
testcase_cls = step.testcase
|
||||||
@@ -174,6 +179,7 @@ class HttpRunner(object):
|
|||||||
.with_session(self.__session)
|
.with_session(self.__session)
|
||||||
.with_case_id(self.__case_id)
|
.with_case_id(self.__case_id)
|
||||||
.with_variables(step_variables)
|
.with_variables(step_variables)
|
||||||
|
.with_export(step_export)
|
||||||
.run()
|
.run()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,13 +187,16 @@ class HttpRunner(object):
|
|||||||
if os.path.isabs(step.testcase):
|
if os.path.isabs(step.testcase):
|
||||||
ref_testcase_path = step.testcase
|
ref_testcase_path = step.testcase
|
||||||
else:
|
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 = (
|
case_result = (
|
||||||
HttpRunner()
|
HttpRunner()
|
||||||
.with_session(self.__session)
|
.with_session(self.__session)
|
||||||
.with_case_id(self.__case_id)
|
.with_case_id(self.__case_id)
|
||||||
.with_variables(step_variables)
|
.with_variables(step_variables)
|
||||||
|
.with_export(step_export)
|
||||||
.run_path(ref_testcase_path)
|
.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.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
|
step_data.success = case_result.success
|
||||||
self.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
|
return step_data
|
||||||
|
|
||||||
def __run_step(self, step: TStep) -> Dict:
|
def __run_step(self, step: TStep) -> Dict:
|
||||||
@@ -218,7 +230,7 @@ class HttpRunner(object):
|
|||||||
|
|
||||||
self.__step_datas.append(step_data)
|
self.__step_datas.append(step_data)
|
||||||
logger.info(f"run step end: {step.name} <<<<<<\n")
|
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:
|
def __parse_config(self, config: TConfig) -> NoReturn:
|
||||||
config.variables.update(self.__session_variables)
|
config.variables.update(self.__session_variables)
|
||||||
@@ -252,7 +264,6 @@ class HttpRunner(object):
|
|||||||
self.__step_datas: List[StepData] = []
|
self.__step_datas: List[StepData] = []
|
||||||
self.__session = self.__session or HttpSession()
|
self.__session = self.__session or HttpSession()
|
||||||
self.__session_variables = {}
|
self.__session_variables = {}
|
||||||
self.__export_variables = {}
|
|
||||||
|
|
||||||
# run teststeps
|
# run teststeps
|
||||||
for step in self.__teststeps:
|
for step in self.__teststeps:
|
||||||
@@ -274,11 +285,6 @@ class HttpRunner(object):
|
|||||||
self.__session_variables.update(extract_mapping)
|
self.__session_variables.update(extract_mapping)
|
||||||
|
|
||||||
self.__duration = time.time() - self.__start_at
|
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
|
return self
|
||||||
|
|
||||||
def run_path(self, path: Text) -> "HttpRunner":
|
def run_path(self, path: Text) -> "HttpRunner":
|
||||||
@@ -303,11 +309,10 @@ class HttpRunner(object):
|
|||||||
return self.__step_datas
|
return self.__step_datas
|
||||||
|
|
||||||
def get_export_variables(self) -> Dict:
|
def get_export_variables(self) -> Dict:
|
||||||
if self.__export_variables:
|
# override testcase export vars with step export
|
||||||
return self.__export_variables
|
export_var_names = self.__export or self.__config.export
|
||||||
|
|
||||||
export_vars_mapping = {}
|
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:
|
if var_name not in self.__session_variables:
|
||||||
raise ParamsError(
|
raise ParamsError(
|
||||||
f"failed to export variable {var_name} from session variables {self.__session_variables}"
|
f"failed to export variable {var_name} from session variables {self.__session_variables}"
|
||||||
@@ -331,7 +336,8 @@ class HttpRunner(object):
|
|||||||
duration=self.__duration,
|
duration=self.__duration,
|
||||||
),
|
),
|
||||||
in_out=TestCaseInOut(
|
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,
|
log=self.__log_path,
|
||||||
step_datas=self.__step_datas,
|
step_datas=self.__step_datas,
|
||||||
@@ -345,7 +351,7 @@ class HttpRunner(object):
|
|||||||
)
|
)
|
||||||
self.__case_id = self.__case_id or str(uuid.uuid4())
|
self.__case_id = self.__case_id or str(uuid.uuid4())
|
||||||
self.__log_path = self.__log_path or os.path.join(
|
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")
|
log_handler = logger.add(self.__log_path, level="DEBUG")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
from typing import Text, Any, Union, Callable
|
from typing import Text, Any, Union, Callable
|
||||||
|
|
||||||
from httprunner.schema import (
|
from httprunner.models import (
|
||||||
TConfig,
|
TConfig,
|
||||||
TStep,
|
TStep,
|
||||||
TRequest,
|
TRequest,
|
||||||
@@ -57,37 +57,43 @@ class Config(object):
|
|||||||
|
|
||||||
|
|
||||||
class StepRequestValidation(object):
|
class StepRequestValidation(object):
|
||||||
def __init__(self, step: TStep):
|
def __init__(self, step_context: TStep):
|
||||||
self.__t_step = step
|
self.__step_context = step_context
|
||||||
|
|
||||||
def assert_equal(
|
def assert_equal(
|
||||||
self, jmes_path: Text, expected_value: Any
|
self, jmes_path: Text, expected_value: Any
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append({"equal": [jmes_path, expected_value]})
|
self.__step_context.validators.append({"equal": [jmes_path, expected_value]})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def assert_not_equal(
|
def assert_not_equal(
|
||||||
self, jmes_path: Text, expected_value: Any
|
self, jmes_path: Text, expected_value: Any
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_greater_than(
|
def assert_greater_than(
|
||||||
self, jmes_path: Text, expected_value: Union[int, float]
|
self, jmes_path: Text, expected_value: Union[int, float]
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_less_than(
|
def assert_less_than(
|
||||||
self, jmes_path: Text, expected_value: Union[int, float]
|
self, jmes_path: Text, expected_value: Union[int, float]
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_greater_or_equals(
|
def assert_greater_or_equals(
|
||||||
self, jmes_path: Text, expected_value: Union[int, float]
|
self, jmes_path: Text, expected_value: Union[int, float]
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append(
|
self.__step_context.validators.append(
|
||||||
{"greater_or_equals": [jmes_path, expected_value]}
|
{"greater_or_equals": [jmes_path, expected_value]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@@ -95,19 +101,23 @@ class StepRequestValidation(object):
|
|||||||
def assert_less_or_equals(
|
def assert_less_or_equals(
|
||||||
self, jmes_path: Text, expected_value: Union[int, float]
|
self, jmes_path: Text, expected_value: Union[int, float]
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_length_equal(
|
def assert_length_equal(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_length_greater_than(
|
def assert_length_greater_than(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append(
|
self.__step_context.validators.append(
|
||||||
{"length_greater_than": [jmes_path, expected_value]}
|
{"length_greater_than": [jmes_path, expected_value]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@@ -115,7 +125,7 @@ class StepRequestValidation(object):
|
|||||||
def assert_length_less_than(
|
def assert_length_less_than(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append(
|
self.__step_context.validators.append(
|
||||||
{"length_less_than": [jmes_path, expected_value]}
|
{"length_less_than": [jmes_path, expected_value]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@@ -123,7 +133,7 @@ class StepRequestValidation(object):
|
|||||||
def assert_length_greater_or_equals(
|
def assert_length_greater_or_equals(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append(
|
self.__step_context.validators.append(
|
||||||
{"length_greater_or_equals": [jmes_path, expected_value]}
|
{"length_greater_or_equals": [jmes_path, expected_value]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@@ -131,7 +141,7 @@ class StepRequestValidation(object):
|
|||||||
def assert_length_less_or_equals(
|
def assert_length_less_or_equals(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append(
|
self.__step_context.validators.append(
|
||||||
{"length_less_or_equals": [jmes_path, expected_value]}
|
{"length_less_or_equals": [jmes_path, expected_value]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@@ -139,55 +149,65 @@ class StepRequestValidation(object):
|
|||||||
def assert_string_equals(
|
def assert_string_equals(
|
||||||
self, jmes_path: Text, expected_value: int
|
self, jmes_path: Text, expected_value: int
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_startswith(
|
def assert_startswith(
|
||||||
self, jmes_path: Text, expected_value: Text
|
self, jmes_path: Text, expected_value: Text
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append({"startswith": [jmes_path, expected_value]})
|
self.__step_context.validators.append(
|
||||||
|
{"startswith": [jmes_path, expected_value]}
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def assert_endswith(
|
def assert_endswith(
|
||||||
self, jmes_path: Text, expected_value: Text
|
self, jmes_path: Text, expected_value: Text
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append({"endswith": [jmes_path, expected_value]})
|
self.__step_context.validators.append({"endswith": [jmes_path, expected_value]})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def assert_regex_match(
|
def assert_regex_match(
|
||||||
self, jmes_path: Text, expected_value: Text
|
self, jmes_path: Text, expected_value: Text
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_contains(
|
def assert_contains(
|
||||||
self, jmes_path: Text, expected_value: Any
|
self, jmes_path: Text, expected_value: Any
|
||||||
) -> "StepRequestValidation":
|
) -> "StepRequestValidation":
|
||||||
self.__t_step.validators.append({"contains": [jmes_path, expected_value]})
|
self.__step_context.validators.append({"contains": [jmes_path, expected_value]})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def assert_contained_by(
|
def assert_contained_by(
|
||||||
self, jmes_path: Text, expected_value: Any
|
self, jmes_path: Text, expected_value: Any
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def assert_type_match(
|
def assert_type_match(
|
||||||
self, jmes_path: Text, expected_value: Text
|
self, jmes_path: Text, expected_value: Text
|
||||||
) -> "StepRequestValidation":
|
) -> "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
|
return self
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|
||||||
|
|
||||||
class StepRequestExtraction(object):
|
class StepRequestExtraction(object):
|
||||||
def __init__(self, step: TStep):
|
def __init__(self, step_context: TStep):
|
||||||
self.__t_step = step
|
self.__step_context = step_context
|
||||||
|
|
||||||
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
|
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
|
return self
|
||||||
|
|
||||||
# def with_regex(self):
|
# def with_regex(self):
|
||||||
@@ -199,135 +219,134 @@ class StepRequestExtraction(object):
|
|||||||
# pass
|
# pass
|
||||||
|
|
||||||
def validate(self) -> StepRequestValidation:
|
def validate(self) -> StepRequestValidation:
|
||||||
return StepRequestValidation(self.__t_step)
|
return StepRequestValidation(self.__step_context)
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|
||||||
|
|
||||||
class RequestWithOptionalArgs(object):
|
class RequestWithOptionalArgs(object):
|
||||||
def __init__(self, step: TStep):
|
def __init__(self, step_context: TStep):
|
||||||
self.__t_step = step
|
self.__step_context = step_context
|
||||||
|
|
||||||
def with_params(self, **params) -> "RequestWithOptionalArgs":
|
def with_params(self, **params) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.params.update(params)
|
self.__step_context.request.params.update(params)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
|
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.headers.update(headers)
|
self.__step_context.request.headers.update(headers)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
|
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.cookies.update(cookies)
|
self.__step_context.request.cookies.update(cookies)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_data(self, data) -> "RequestWithOptionalArgs":
|
def with_data(self, data) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.data = data
|
self.__step_context.request.data = data
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_json(self, req_json) -> "RequestWithOptionalArgs":
|
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
|
return self
|
||||||
|
|
||||||
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
|
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.timeout = timeout
|
self.__step_context.request.timeout = timeout
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
|
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.verify = verify
|
self.__step_context.request.verify = verify
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
|
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
|
return self
|
||||||
|
|
||||||
def upload(self, **file_info) -> "RequestWithOptionalArgs":
|
def upload(self, **file_info) -> "RequestWithOptionalArgs":
|
||||||
self.__t_step.request.upload.update(file_info)
|
self.__step_context.request.upload.update(file_info)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# def hooks(self):
|
# def hooks(self):
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
def extract(self) -> StepRequestExtraction:
|
def extract(self) -> StepRequestExtraction:
|
||||||
return StepRequestExtraction(self.__t_step)
|
return StepRequestExtraction(self.__step_context)
|
||||||
|
|
||||||
def validate(self) -> StepRequestValidation:
|
def validate(self) -> StepRequestValidation:
|
||||||
return StepRequestValidation(self.__t_step)
|
return StepRequestValidation(self.__step_context)
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|
||||||
|
|
||||||
class RunRequest(object):
|
class RunRequest(object):
|
||||||
def __init__(self, name: Text):
|
def __init__(self, name: Text):
|
||||||
self.__t_step = TStep(name=name)
|
self.__step_context = TStep(name=name)
|
||||||
|
|
||||||
def with_variables(self, **variables) -> "RunRequest":
|
def with_variables(self, **variables) -> "RunRequest":
|
||||||
self.__t_step.variables.update(variables)
|
self.__step_context.variables.update(variables)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get(self, url: Text) -> RequestWithOptionalArgs:
|
def get(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.GET, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def post(self, url: Text) -> RequestWithOptionalArgs:
|
def post(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.POST, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.POST, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def put(self, url: Text) -> RequestWithOptionalArgs:
|
def put(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.PUT, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def head(self, url: Text) -> RequestWithOptionalArgs:
|
def head(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.HEAD, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def delete(self, url: Text) -> RequestWithOptionalArgs:
|
def delete(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.DELETE, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def options(self, url: Text) -> RequestWithOptionalArgs:
|
def options(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.OPTIONS, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
def patch(self, url: Text) -> RequestWithOptionalArgs:
|
def patch(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__t_step.request = TRequest(method=MethodEnum.PATCH, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url)
|
||||||
return RequestWithOptionalArgs(self.__t_step)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|
||||||
|
|
||||||
class StepRefCase(object):
|
class StepRefCase(object):
|
||||||
def __init__(self, step: TStep):
|
def __init__(self, step_context: TStep):
|
||||||
self.__t_step = step
|
self.__step_context = step_context
|
||||||
self.__t_step.extract = []
|
|
||||||
|
|
||||||
def extract(self, *var_name: Text) -> "StepRefCase":
|
def export(self, *var_name: Text) -> "StepRefCase":
|
||||||
self.__t_step.extract.extend(var_name)
|
self.__step_context.export.extend(var_name)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|
||||||
|
|
||||||
class RunTestCase(object):
|
class RunTestCase(object):
|
||||||
def __init__(self, name: Text):
|
def __init__(self, name: Text):
|
||||||
self.__t_step = TStep(name=name)
|
self.__step_context = TStep(name=name)
|
||||||
|
|
||||||
def with_variables(self, **variables) -> "RunTestCase":
|
def with_variables(self, **variables) -> "RunTestCase":
|
||||||
self.__t_step.variables.update(variables)
|
self.__step_context.variables.update(variables)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def call(self, testcase: Callable) -> StepRefCase:
|
def call(self, testcase: Callable) -> StepRefCase:
|
||||||
self.__t_step.testcase = testcase
|
self.__step_context.testcase = testcase
|
||||||
return StepRefCase(self.__t_step)
|
return StepRefCase(self.__step_context)
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|
||||||
|
|
||||||
class Step(object):
|
class Step(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
step: Union[
|
step_context: Union[
|
||||||
StepRequestValidation,
|
StepRequestValidation,
|
||||||
StepRequestExtraction,
|
StepRequestExtraction,
|
||||||
RequestWithOptionalArgs,
|
RequestWithOptionalArgs,
|
||||||
@@ -335,15 +354,15 @@ class Step(object):
|
|||||||
StepRefCase,
|
StepRefCase,
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
self.__t_step = step.perform()
|
self.__step_context = step_context.perform()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def request(self) -> TRequest:
|
def request(self) -> TRequest:
|
||||||
return self.__t_step.request
|
return self.__step_context.request
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def testcase(self) -> TestCase:
|
def testcase(self) -> TestCase:
|
||||||
return self.__t_step.testcase
|
return self.__step_context.testcase
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__t_step
|
return self.__step_context
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import collections
|
|||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import platform
|
import platform
|
||||||
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any, Text
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -176,3 +177,38 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
|
|||||||
return dict(
|
return dict(
|
||||||
sorted(raw_dict.items(), key=lambda i: get_index_from_list(custom_order, i[0]))
|
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]
|
[tool.poetry]
|
||||||
name = "httprunner"
|
name = "httprunner"
|
||||||
version = "3.0.8"
|
version = "3.0.9"
|
||||||
description = "One-stop solution for HTTP(S) testing."
|
description = "One-stop solution for HTTP(S) testing."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ class TestCli(unittest.TestCase):
|
|||||||
self.assertIn(__description__, self.captured_output.getvalue().strip())
|
self.assertIn(__description__, self.captured_output.getvalue().strip())
|
||||||
|
|
||||||
def test_debug_pytest(self):
|
def test_debug_pytest(self):
|
||||||
pytest.main(
|
exit_code = pytest.main(
|
||||||
[
|
[
|
||||||
"-s",
|
"-s",
|
||||||
"examples/postman_echo/request_methods/request_with_testcase_reference_test.py",
|
"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),
|
compat.ensure_cli_args(args2),
|
||||||
["examples/postman_echo/request_methods/hardcode.yml"],
|
["examples/postman_echo/request_methods/hardcode.yml"],
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(os.path.isfile("examples/postman_echo/conftest.py"))
|
||||||
os.path.isfile("examples/postman_echo/request_methods/conftest.py")
|
|
||||||
)
|
|
||||||
|
|
||||||
args3 = [
|
args3 = [
|
||||||
"examples/postman_echo/request_methods/hardcode.yml",
|
"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
|
import unittest
|
||||||
|
|
||||||
from httprunner.make import (
|
from httprunner.make import (
|
||||||
main_make,
|
main_make,
|
||||||
convert_testcase_path,
|
convert_testcase_path,
|
||||||
make_files_cache_set,
|
pytest_files_made_cache_mapping,
|
||||||
make_config_chain_style,
|
make_config_chain_style,
|
||||||
make_teststep_chain_style,
|
make_teststep_chain_style,
|
||||||
pytest_files_set,
|
pytest_files_run_set,
|
||||||
)
|
)
|
||||||
|
from httprunner import loader
|
||||||
|
|
||||||
|
|
||||||
class TestMake(unittest.TestCase):
|
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):
|
def test_make_testcase(self):
|
||||||
path = ["examples/postman_echo/request_methods/request_with_variables.yml"]
|
path = ["examples/postman_echo/request_methods/request_with_variables.yml"]
|
||||||
testcase_python_list = main_make(path)
|
testcase_python_list = main_make(path)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
testcase_python_list[0],
|
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):
|
def test_make_testcase_with_ref(self):
|
||||||
path = [
|
path = [
|
||||||
"examples/postman_echo/request_methods/request_with_testcase_reference.yml"
|
"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)
|
testcase_python_list = main_make(path)
|
||||||
self.assertEqual(len(testcase_python_list), 1)
|
self.assertEqual(len(testcase_python_list), 1)
|
||||||
self.assertIn(
|
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,
|
testcase_python_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,56 +63,51 @@ from examples.postman_echo.request_methods.request_with_functions_test import (
|
|||||||
path = ["examples/postman_echo/request_methods/"]
|
path = ["examples/postman_echo/request_methods/"]
|
||||||
testcase_python_list = main_make(path)
|
testcase_python_list = main_make(path)
|
||||||
self.assertIn(
|
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,
|
testcase_python_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_convert_testcase_path(self):
|
def test_convert_testcase_path(self):
|
||||||
self.assertEqual(
|
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(
|
self.assertEqual(
|
||||||
convert_testcase_path("/path/to/mubu.login.yml")[0],
|
convert_testcase_path(os.path.join(os.getcwd(), "path/to/mubu.login.yml")),
|
||||||
"/path/to/mubu_login_test.py",
|
(os.path.join(os.getcwd(), "path/to/mubu_login_test.py"), "MubuLogin"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
convert_testcase_path("/path/to 2/mubu.login.yml")[0],
|
convert_testcase_path("path/to 2/mubu.login.yml"),
|
||||||
"/path/to 2/mubu_login_test.py",
|
(os.path.join(os.getcwd(), "path/to_2/mubu_login_test.py"), "MubuLogin"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
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(
|
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):
|
def test_make_testsuite(self):
|
||||||
path = ["examples/postman_echo/request_methods/demo_testsuite.yml"]
|
path = ["examples/postman_echo/request_methods/demo_testsuite.yml"]
|
||||||
make_files_cache_set.clear()
|
|
||||||
pytest_files_set.clear()
|
|
||||||
testcase_python_list = main_make(path)
|
testcase_python_list = main_make(path)
|
||||||
self.assertEqual(len(testcase_python_list), 2)
|
self.assertEqual(len(testcase_python_list), 2)
|
||||||
self.assertIn(
|
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,
|
testcase_python_list,
|
||||||
)
|
)
|
||||||
self.assertIn(
|
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,
|
testcase_python_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,13 +115,13 @@ from examples.postman_echo.request_methods.request_with_functions_test import (
|
|||||||
config = {
|
config = {
|
||||||
"name": "request methods testcase: validate with functions",
|
"name": "request methods testcase: validate with functions",
|
||||||
"variables": {"foo1": "bar1", "foo2": 22},
|
"variables": {"foo1": "bar1", "foo2": 22},
|
||||||
"base_url": "https://postman-echo.com",
|
"base_url": "https://postman_echo.com",
|
||||||
"verify": False,
|
"verify": False,
|
||||||
"path": "examples/postman_echo/request_methods/validate_with_functions_test.py",
|
"path": "examples/postman_echo/request_methods/validate_with_functions_test.py",
|
||||||
}
|
}
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
make_config_chain_style(config),
|
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):
|
def test_make_teststep_chain_style(self):
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from httprunner import loader
|
||||||
|
from httprunner.cli import main_run
|
||||||
from httprunner.runner import HttpRunner
|
from httprunner.runner import HttpRunner
|
||||||
|
|
||||||
|
|
||||||
class TestHttpRunner(unittest.TestCase):
|
class TestHttpRunner(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
loader.project_meta = None
|
||||||
self.runner = HttpRunner()
|
self.runner = HttpRunner()
|
||||||
|
|
||||||
def test_run_testcase_by_path_request_only(self):
|
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.name, "request methods testcase: reference testcase")
|
||||||
self.assertEqual(result.step_datas[0].name, "request with functions")
|
self.assertEqual(result.step_datas[0].name, "request with functions")
|
||||||
self.assertEqual(len(result.step_datas), 2)
|
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
|
import unittest
|
||||||
|
|
||||||
from httprunner import loader, utils
|
from httprunner import loader, utils
|
||||||
|
from httprunner.utils import ensure_file_path_valid
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@@ -97,3 +98,21 @@ class TestUtils(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
["A", "D", "C", "B"],
|
["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