Merge pull request #922 from httprunner/v3

## 3.0.9 (2020-06-07)

**Fixed**

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

**Changed**

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

View File

@@ -1,5 +1,20 @@
# Release History # 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**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_functions.yml # FROM: 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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml # 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")

View File

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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_functions.yml # FROM: 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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml # 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")

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/request_with_variables.yml # FROM: 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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml # FROM: 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

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner v3.0.8 # NOTE: Generated By HttpRunner v3.0.9
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml # FROM: 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

View File

@@ -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

View File

@@ -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()

View File

@@ -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":

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View 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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)

View File

@@ -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):

View File

@@ -21,7 +21,7 @@ from httprunner.loader import load_project_meta, load_testcase_file
from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.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")

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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
View File

@@ -0,0 +1,30 @@
config:
name: "request methods testcase with functions"
variables:
foo1: session_bar1
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: get with params
variables:
foo1: bar1
foo2: session_bar2
sum_v: "${sum_two(1, 2)}"
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
sum_v: $sum_v
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
extract:
session_foo2: "body.args.foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.args.foo1", "session_bar1"]
- eq: ["body.args.sum_v", "3"]
- eq: ["body.args.foo2", "session_bar2"]

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

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

View File

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

View File

@@ -1,34 +1,45 @@
import os
import unittest 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):

View File

@@ -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"))

View File

@@ -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"),
)