diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py
index 0c269b0b..c23d1fc1 100644
--- a/examples/postman_echo/request_methods/hardcode_test.py
+++ b/examples/postman_echo/request_methods/hardcode_test.py
@@ -1,5 +1,5 @@
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsConfig, TestStep
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsConfig, TestStep
class TestCaseRequestMethodsHardcode(TestCaseRunner):
diff --git a/examples/postman_echo/request_methods/request_with_functions.yml b/examples/postman_echo/request_methods/request_with_functions.yml
index 5391a785..66a94ba4 100644
--- a/examples/postman_echo/request_methods/request_with_functions.yml
+++ b/examples/postman_echo/request_methods/request_with_functions.yml
@@ -26,8 +26,8 @@ teststeps:
validate:
- eq: ["status_code", 200]
- eq: ["body.args.foo1", "session_bar1"]
+ - eq: ["body.args.sum_v", 3]
- eq: ["body.args.foo2", "session_bar2"]
- - eq: ["body.args.sum_v", "3"]
-
name: post raw text
variables:
diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py
index 3de4c44a..66c73d66 100644
--- a/examples/postman_echo/request_methods/request_with_functions_test.py
+++ b/examples/postman_echo/request_methods/request_with_functions_test.py
@@ -1,5 +1,5 @@
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsConfig, TestStep
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsConfig, TestStep
from examples.postman_echo import debugtalk
diff --git a/examples/postman_echo/request_methods/request_with_variables_test.py b/examples/postman_echo/request_methods/request_with_variables_test.py
index df29953f..5456b681 100644
--- a/examples/postman_echo/request_methods/request_with_variables_test.py
+++ b/examples/postman_echo/request_methods/request_with_variables_test.py
@@ -1,5 +1,5 @@
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsConfig, TestStep
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsConfig, TestStep
class TestCaseRequestMethodsWithVariables(TestCaseRunner):
diff --git a/examples/postman_echo/request_methods/validate_with_functions_test.py b/examples/postman_echo/request_methods/validate_with_functions_test.py
index 8f600dfb..a580040c 100644
--- a/examples/postman_echo/request_methods/validate_with_functions_test.py
+++ b/examples/postman_echo/request_methods/validate_with_functions_test.py
@@ -1,5 +1,5 @@
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsConfig, TestStep
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsConfig, TestStep
from examples.postman_echo import debugtalk
diff --git a/examples/postman_echo/request_methods/validate_with_variables_test.py b/examples/postman_echo/request_methods/validate_with_variables_test.py
index 8582a3ba..9252c239 100644
--- a/examples/postman_echo/request_methods/validate_with_variables_test.py
+++ b/examples/postman_echo/request_methods/validate_with_variables_test.py
@@ -1,5 +1,5 @@
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsConfig, TestStep
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsConfig, TestStep
class TestCaseRequestMethodsValidateWithVariables(TestCaseRunner):
diff --git a/httprunner/api.py b/httprunner/api.py
index b973381e..a7a70fbf 100644
--- a/httprunner/api.py
+++ b/httprunner/api.py
@@ -1,11 +1,14 @@
import os
import sys
import unittest
+from typing import List
from loguru import logger
-from httprunner import (__version__, exceptions, loader, parser,
- report, runner, utils)
+from httprunner import report, loader, utils, exceptions, __version__
+from httprunner.report import gen_html_report
+from httprunner.runner import TestCaseRunner
+from httprunner.schema import TestsMapping, TestCaseSummary, TestSuiteSummary
class HttpRunner(object):
@@ -23,11 +26,10 @@ class HttpRunner(object):
"""
- def __init__(self, failfast=False, save_tests=False, log_level="WARNING", log_file=None):
+ def __init__(self, save_tests=False, log_level="WARNING", log_file=None):
""" initialize HttpRunner.
Args:
- failfast (bool): stop the test run on the first error or failure.
save_tests (bool): save loaded/parsed tests to JSON file.
log_level (str): logging level.
log_file (str): log file path.
@@ -35,7 +37,7 @@ class HttpRunner(object):
"""
self.exception_stage = "initialize HttpRunner()"
kwargs = {
- "failfast": failfast,
+ "failfast": True,
"resultclass": report.HtmlTestResult
}
@@ -51,90 +53,48 @@ class HttpRunner(object):
self._summary = None
self.test_path = None
- def _add_tests(self, testcases):
- """ initialize testcase with Runner() and add to test suite.
-
- Args:
- testcases (list): testcases list.
-
- Returns:
- unittest.TestSuite()
-
- """
- def _add_test(test_runner, test_dict):
+ def _prepare_tests(self, tests: TestsMapping) -> List[unittest.TestSuite]:
+ def _add_test(test_runner: TestCaseRunner):
""" add test to testcase.
"""
def test(self):
try:
- test_runner.run_test(test_dict)
+ test_runner.run()
except exceptions.MyBaseFailure as ex:
self.fail(str(ex))
finally:
self.step_datas = test_runner.step_datas
- if "config" in test_dict:
- # run nested testcase
- test.__doc__ = test_dict["config"].get("name")
- variables = test_dict["config"].get("variables", {})
- else:
- # run api test
- test.__doc__ = test_dict.get("name")
- variables = test_dict.get("variables", {})
-
- if isinstance(test.__doc__, parser.LazyString):
- try:
- parsed_variables = parser.parse_variables_mapping(variables)
- test.__doc__ = parser.parse_lazy_data(
- test.__doc__, parsed_variables
- )
- except exceptions.VariableNotFound:
- test.__doc__ = str(test.__doc__)
-
+ test.__doc__ = test_runner.config.name
return test
- test_suite = unittest.TestSuite()
+ project_meta = tests.project_meta
+ testcases = tests.testcases
+
+ prepared_testcases: List[unittest.TestSuite] = []
+
for testcase in testcases:
- config = testcase.get("config", {})
- test_runner = runner.Runner(config)
+ testcase.config.variables.update(project_meta.variables)
+ testcase.config.functions.update(project_meta.functions)
+
+ test_runner = TestCaseRunner().init(testcase)
+
TestSequense = type('TestSequense', (unittest.TestCase,), {})
-
- tests = testcase.get("teststeps", [])
- for index, test_dict in enumerate(tests):
- times = test_dict.get("times", 1)
- try:
- times = int(times)
- except ValueError:
- raise exceptions.ParamsError(
- f"times should be digit, given: {times}")
-
- for times_index in range(times):
- # suppose one testcase should not have more than 9999 steps,
- # and one step should not run more than 999 times.
- test_method_name = 'test_{:04}_{:03}'.format(index, times_index)
- test_method = _add_test(test_runner, test_dict)
- setattr(TestSequense, test_method_name, test_method)
+ test_method = _add_test(test_runner)
+ setattr(TestSequense, "test_method_name", test_method)
loaded_testcase = self.test_loader.loadTestsFromTestCase(TestSequense)
- setattr(loaded_testcase, "config", config)
- setattr(loaded_testcase, "teststeps", tests)
- setattr(loaded_testcase, "runner", test_runner)
- test_suite.addTest(loaded_testcase)
+ setattr(loaded_testcase, "config", testcase.config)
+ prepared_testcases.append(loaded_testcase)
- return test_suite
-
- def _run_suite(self, test_suite):
- """ run tests in test_suite
-
- Args:
- test_suite: unittest.TestSuite()
-
- Returns:
- list: tests_results
+ return prepared_testcases
+ def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[TestCaseSummary]:
+ """ run prepared testcases
"""
- tests_results = []
+ tests_results: List[TestCaseSummary] = []
- for index, testcase in enumerate(test_suite):
+ for index, testcase in enumerate(prepared_testcases):
log_handler = None
if self.save_tests:
logs_file_abs_path = utils.prepare_log_file_abs_path(
@@ -142,72 +102,71 @@ class HttpRunner(object):
)
log_handler = logger.add(logs_file_abs_path, level="DEBUG")
- testcase_name = testcase.config.get("name")
- logger.info(f"Start to run testcase: {testcase_name}")
+ logger.info(f"Start to run testcase: {testcase.config.name}")
result = self.unittest_runner.run(testcase)
- if result.wasSuccessful():
- tests_results.append((testcase, result))
- else:
- tests_results.insert(0, (testcase, result))
+ testcase_summary = report.get_summary(result)
+ testcase_summary.in_out.vars = testcase.config.variables
+ testcase_summary.in_out.out = testcase.config.export
if self.save_tests and log_handler:
logger.remove(log_handler)
-
- return tests_results
-
- def _aggregate(self, tests_results):
- """ aggregate results
-
- Args:
- tests_results (list): list of (testcase, result)
-
- """
- summary = {
- "success": True,
- "stat": {
- "testcases": {
- "total": len(tests_results),
- "success": 0,
- "fail": 0
- },
- "teststeps": {}
- },
- "time": {},
- "platform": report.get_platform(),
- "details": []
- }
-
- for index, tests_result in enumerate(tests_results):
- testcase, result = tests_result
- testcase_summary = report.get_summary(result)
-
- if testcase_summary["success"]:
- summary["stat"]["testcases"]["success"] += 1
- else:
- summary["stat"]["testcases"]["fail"] += 1
-
- summary["success"] &= testcase_summary["success"]
- testcase_summary["name"] = testcase.config.get("name")
- testcase_summary["in_out"] = utils.get_testcase_io(testcase)
-
- report.aggregate_stat(summary["stat"]["teststeps"], testcase_summary["stat"])
- report.aggregate_stat(summary["time"], testcase_summary["time"])
-
- if self.save_tests:
logs_file_abs_path = utils.prepare_log_file_abs_path(
self.test_path, f"testcase_{index+1}.log"
)
- testcase_summary["log"] = logs_file_abs_path
+ testcase_summary.log = logs_file_abs_path
- summary["details"].append(testcase_summary)
+ if result.wasSuccessful():
+ tests_results.append(testcase_summary)
+ else:
+ tests_results.insert(0, testcase_summary)
- return summary
+ return tests_results
- def run_tests(self, tests_mapping):
+ def _aggregate(self, tests_results: List[TestCaseSummary]) -> TestSuiteSummary:
+ """ aggregate multiple testcase results
+
+ Args:
+ tests_results (list): list of testcase summary
+
+ """
+ testsuite_summary = {
+ "success": True,
+ "stat": {
+ "total": len(tests_results),
+ "success": 0,
+ "fail": 0
+ },
+ "time": {},
+ "platform": report.get_platform(),
+ "testcases": []
+ }
+
+ for testcase_summary in tests_results:
+ if testcase_summary.success:
+ testsuite_summary["stat"]["success"] += 1
+ else:
+ testsuite_summary["stat"]["fail"] += 1
+
+ testsuite_summary["success"] &= testcase_summary.success
+
+ testsuite_summary["testcases"].append(testcase_summary)
+
+ total_duration = tests_results[-1].time.start_at + tests_results[-1].time.duration \
+ - tests_results[0].time.start_at
+ testsuite_summary["time"] = {
+ "start_at": tests_results[0].time.start_at,
+ "start_at_iso_format": tests_results[0].time.start_at_iso_format,
+ "duration": total_duration
+ }
+
+ return TestSuiteSummary.parse_obj(testsuite_summary)
+
+ def run_tests(self, tests_mapping) -> TestSuiteSummary:
""" run testcase/testsuite data
"""
- self.test_path = tests_mapping.get("project_meta", {}).get("test_path", "")
+ tests = TestsMapping.parse_obj(tests_mapping)
+ self.test_path = tests.project_meta.test_path
if self.save_tests:
utils.dump_json_file(
@@ -215,34 +174,13 @@ class HttpRunner(object):
utils.prepare_log_file_abs_path(self.test_path, "loaded.json")
)
- # parse tests
- self.exception_stage = "parse tests"
- parsed_testcases = parser.parse_tests(tests_mapping)
- parse_failed_testfiles = parser.get_parse_failed_testfiles()
- if parse_failed_testfiles:
- logger.warning("parse failures occurred ...")
- utils.dump_json_file(
- parse_failed_testfiles,
- utils.prepare_log_file_abs_path(self.test_path, "parse_failed.json")
- )
+ # prepare testcases
+ self.exception_stage = "prepare testcases"
+ prepared_testcases = self._prepare_tests(tests)
- if len(parsed_testcases) == 0:
- logger.error("failed to parse all cases, abort.")
- raise exceptions.ParseTestsFailure
-
- if self.save_tests:
- utils.dump_json_file(
- parsed_testcases,
- utils.prepare_log_file_abs_path(self.test_path, "parsed.json")
- )
-
- # add tests to test suite
- self.exception_stage = "add tests to test suite"
- test_suite = self._add_tests(parsed_testcases)
-
- # run test suite
- self.exception_stage = "run test suite"
- results = self._run_suite(test_suite)
+ # run prepared testcases
+ self.exception_stage = "run prepared testcases"
+ results = self._run_suite(prepared_testcases)
# aggregate results
self.exception_stage = "aggregate results"
@@ -254,7 +192,7 @@ class HttpRunner(object):
if self.save_tests:
utils.dump_json_file(
- self._summary,
+ self._summary.dict(),
utils.prepare_log_file_abs_path(self.test_path, "summary.json")
)
# save variables and export data
@@ -291,11 +229,11 @@ class HttpRunner(object):
return None
return [
- summary["in_out"]
- for summary in self._summary["details"]
+ testcase_summary.in_out.dict()
+ for testcase_summary in self._summary.testcases
]
- def run_path(self, path, dot_env_path=None, mapping=None):
+ def run_path(self, path, dot_env_path=None, mapping=None) -> TestSuiteSummary:
""" run testcase/testsuite file or folder.
Args:
@@ -308,6 +246,7 @@ class HttpRunner(object):
"""
# load tests
+ logger.info(f"HttpRunner version: {__version__}")
self.exception_stage = "load tests"
tests_mapping = loader.load_cases(path, dot_env_path)
@@ -330,12 +269,15 @@ class HttpRunner(object):
dict: result summary
"""
- logger.info(f"HttpRunner version: {__version__}")
if loader.is_test_path(path_or_tests):
return self.run_path(path_or_tests, dot_env_path, mapping)
- elif loader.is_test_content(path_or_tests):
- project_working_directory = path_or_tests.get("project_meta", {}).get("PWD", os.getcwd())
- loader.init_pwd(project_working_directory)
- return self.run_tests(path_or_tests)
- else:
- raise exceptions.ParamsError(f"Invalid testcase path or testcases: {path_or_tests}")
+
+ project_working_directory = path_or_tests.get("project_meta", {}).get("PWD", os.getcwd())
+ loader.init_pwd(project_working_directory)
+ return self.run_tests(path_or_tests)
+
+ def gen_html_report(self, report_template=None, report_dir=None, report_file=None):
+ if not self._summary:
+ return None
+
+ return gen_html_report(self._summary, report_template, report_dir, report_file)
diff --git a/httprunner/v3/api_test.py b/httprunner/api_test.py
similarity index 91%
rename from httprunner/v3/api_test.py
rename to httprunner/api_test.py
index 7db740b2..fbacd3ae 100644
--- a/httprunner/v3/api_test.py
+++ b/httprunner/api_test.py
@@ -1,6 +1,6 @@
import unittest
-from httprunner.v3.api import HttpRunner
+from httprunner.api import HttpRunner
class TestHttpRunner(unittest.TestCase):
diff --git a/httprunner/cli.py b/httprunner/cli.py
index d8f04a4e..a0c620ed 100644
--- a/httprunner/cli.py
+++ b/httprunner/cli.py
@@ -20,7 +20,7 @@ $ pip install locustio
from loguru import logger
from httprunner import __description__, __version__
-from httprunner.v3.api import HttpRunner
+from httprunner.api import HttpRunner
from httprunner.ext.har2case import init_har2case_parser, main_har2case
from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold
from httprunner.ext.locusts import init_parser_locusts, main_locusts
diff --git a/httprunner/client.py b/httprunner/client.py
index 9fea6eaf..8c437439 100644
--- a/httprunner/client.py
+++ b/httprunner/client.py
@@ -9,7 +9,7 @@ from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
from httprunner import response
from httprunner.utils import lower_dict_keys, omit_long_data
-from httprunner.v3.schema import SessionData
+from httprunner.schema import SessionData
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
diff --git a/httprunner/context.py b/httprunner/context.py
deleted file mode 100644
index e19aea5a..00000000
--- a/httprunner/context.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import copy
-
-from httprunner import parser, utils
-
-
-class SessionContext(object):
- """ HttpRunner session, store runtime variables.
-
- Examples:
- >>> variables = {"SECRET_KEY": "DebugTalk"}
- >>> context = SessionContext(variables)
-
- Equivalent to:
- >>> context = SessionContext()
- >>> context.update_session_variables(variables)
-
- """
-
- def __init__(self, variables=None):
- variables_mapping = utils.ensure_mapping_format(variables or {})
- self.session_variables_mapping = parser.parse_variables_mapping(variables_mapping)
- self.test_variables_mapping = {}
- self.init_test_variables()
-
- def init_test_variables(self, variables_mapping=None):
- """ init test variables, called when each test(api) starts.
- variables_mapping will be evaluated first.
-
- Args:
- variables_mapping (dict)
- {
- "random": "${gen_random_string(5)}",
- "authorization": "${gen_md5($TOKEN, $data, $random)}",
- "data": '{"name": "user", "password": "123456"}',
- "TOKEN": "debugtalk",
- }
-
- """
- variables_mapping = copy.deepcopy(variables_mapping or {})
- variables_mapping = utils.ensure_mapping_format(variables_mapping)
- variables_mapping.update(self.session_variables_mapping)
- parsed_variables_mapping = parser.parse_variables_mapping(variables_mapping)
-
- self.test_variables_mapping = {}
- # priority: extracted variable > teststep variable
- self.test_variables_mapping.update(parsed_variables_mapping)
- self.test_variables_mapping.update(self.session_variables_mapping)
-
- def update_test_variables(self, variable_name, variable_value):
- """ update test variables, these variables are only valid in the current test.
- """
- self.test_variables_mapping[variable_name] = variable_value
-
- def update_session_variables(self, variables_mapping):
- """ update session with extracted variables mapping.
- these variables are valid in the whole running session.
- """
- variables_mapping = utils.ensure_mapping_format(variables_mapping)
- self.session_variables_mapping.update(variables_mapping)
- self.test_variables_mapping.update(self.session_variables_mapping)
-
- def eval_content(self, content):
- """ evaluate content recursively, take effect on each variable and function in content.
- content may be in any data structure, include dict, list, tuple, number, string, etc.
- """
- return parser.parse_lazy_data(content, self.test_variables_mapping)
diff --git a/httprunner/loader/__init__.py b/httprunner/loader/__init__.py
index 022cf410..bbdc6d50 100644
--- a/httprunner/loader/__init__.py
+++ b/httprunner/loader/__init__.py
@@ -8,16 +8,14 @@ HttpRunner loader
"""
-from httprunner.loader.check import is_test_path, is_test_content, JsonSchemaChecker
+from httprunner.loader.buildup import load_cases, load_project_data
+from httprunner.loader.check import is_test_path
+from httprunner.loader.load import load_csv_file, load_builtin_functions
from httprunner.loader.locate import get_project_working_directory as get_pwd, \
init_project_working_directory as init_pwd
-from httprunner.loader.load import load_csv_file, load_builtin_functions
-from httprunner.loader.buildup import load_cases, load_project_data
__all__ = [
"is_test_path",
- "is_test_content",
- "JsonSchemaChecker",
"get_pwd",
"init_pwd",
"load_csv_file",
diff --git a/httprunner/loader/buildup.py b/httprunner/loader/buildup.py
index e3b62a33..e11236c3 100644
--- a/httprunner/loader/buildup.py
+++ b/httprunner/loader/buildup.py
@@ -4,7 +4,6 @@ import os
from loguru import logger
from httprunner import exceptions, utils
-from httprunner.loader.check import JsonSchemaChecker
from httprunner.loader.load import load_module_functions, load_file, load_dot_env_file, \
load_folder_files
from httprunner.loader.locate import init_project_working_directory, get_project_working_directory
@@ -174,7 +173,6 @@ def load_testcase(raw_testcase):
}
"""
- JsonSchemaChecker.validate_testcase_format(raw_testcase)
raw_teststeps = raw_testcase.pop("teststeps")
raw_testcase["teststeps"] = [
load_teststep(teststep)
@@ -220,7 +218,6 @@ def load_testsuite(raw_testsuite):
# invalid format
raise exceptions.FileFormatError("Invalid testsuite format!")
- JsonSchemaChecker.validate_testsuite_format(raw_testsuite)
raw_testsuite["testcases"] = {}
for raw_testcase in raw_testcases:
__extend_with_testcase_ref(raw_testcase)
@@ -284,7 +281,6 @@ def load_test_file(path: str) -> dict:
elif "request" in raw_content:
# file_type: api
- JsonSchemaChecker.validate_api_format(raw_content)
loaded_content = raw_content
loaded_content["path"] = path
loaded_content["type"] = "api"
diff --git a/httprunner/loader/check.py b/httprunner/loader/check.py
index d428b75e..cb4c538c 100644
--- a/httprunner/loader/check.py
+++ b/httprunner/loader/check.py
@@ -1,44 +1,5 @@
import os
-from loguru import logger
-from pydantic import ValidationError
-
-from httprunner import exceptions
-from httprunner.schema import Api, TestCase, TestSuite
-
-
-class JsonSchemaChecker(object):
-
- @staticmethod
- def validate_api_format(content):
- """ check api format if valid
- """
- try:
- Api.parse_obj(content)
- except ValidationError as ex:
- logger.error(ex)
- raise exceptions.FileFormatError(ex)
-
- @staticmethod
- def validate_testcase_format(content):
- """ check testcase format if valid
- """
- try:
- TestCase.parse_obj(content)
- except ValidationError as ex:
- logger.error(ex)
- raise exceptions.FileFormatError(ex)
-
- @staticmethod
- def validate_testsuite_format(content):
- """ check testsuite format if valid
- """
- try:
- TestSuite.parse_obj(content)
- except ValidationError as ex:
- logger.error(ex)
- raise exceptions.FileFormatError(ex)
-
def is_test_path(path):
""" check if path is valid json/yaml file path or a existed directory.
@@ -80,77 +41,3 @@ def is_test_path(path):
else:
# path is neither a folder nor a file, maybe a symbol link or something else
return False
-
-
-def is_test_content(data_structure):
- """ check if data_structure is apis/testcases/testsuites.
-
- Args:
- data_structure (dict): should include keys, apis or testcases or testsuites
-
- Returns:
- bool: True if data_structure is valid apis/testcases/testsuites, otherwise False.
-
- """
- if not isinstance(data_structure, dict):
- return False
-
- if "apis" in data_structure:
- # maybe a group of api content
- apis = data_structure["apis"]
- if not isinstance(apis, list):
- return False
-
- for item in apis:
- is_testcase = False
- try:
- JsonSchemaChecker.validate_api_format(item)
- is_testcase = True
- except exceptions.FileFormatError:
- pass
-
- if not is_testcase:
- return False
-
- return True
-
- elif "testcases" in data_structure:
- # maybe a testsuite, containing a group of testcases
- testcases = data_structure["testcases"]
- if not isinstance(testcases, list):
- return False
-
- for item in testcases:
- is_testcase = False
- try:
- JsonSchemaChecker.validate_testcase_format(item)
- is_testcase = True
- except exceptions.FileFormatError:
- pass
-
- if not is_testcase:
- return False
-
- return True
-
- elif "testsuites" in data_structure:
- # maybe a group of testsuites
- testsuites = data_structure["testsuites"]
- if not isinstance(testsuites, list):
- return False
-
- for item in testsuites:
- is_testcase = False
- try:
- JsonSchemaChecker.validate_testsuite_format(item)
- is_testcase = True
- except exceptions.FileFormatError:
- pass
-
- if not is_testcase:
- return False
-
- return True
-
- else:
- return False
diff --git a/httprunner/loader/check_test.py b/httprunner/loader/check_test.py
deleted file mode 100644
index e6c54597..00000000
--- a/httprunner/loader/check_test.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import unittest
-
-from httprunner.loader import check
-
-
-class TestLoaderCheck(unittest.TestCase):
-
- def test_is_testcases(self):
- data_structure = "path/to/file"
- self.assertFalse(check.is_test_content(data_structure))
- data_structure = ["path/to/file1", "path/to/file2"]
- self.assertFalse(check.is_test_content(data_structure))
-
- data_structure = {
- "project_meta": {
- "PWD": "XXXXX",
- "functions": {},
- "env": {}
- },
- "testcases": [
- { # testcase data structure
- "config": {
- "name": "desc1",
- "path": "testcase1_path",
- "variables": [], # optional
- },
- "teststeps": [
- # test data structure
- {
- 'name': 'test step desc1',
- 'variables': [], # optional
- 'extract': {}, # optional
- 'validate': [],
- 'request': {
- "method": "GET",
- "url": "https://docs.httprunner.org"
- }
- },
- # test_dict2 # another test dict
- ]
- },
- # testcase_dict_2 # another testcase dict
- ]
- }
- self.assertTrue(check.is_test_content(data_structure))
diff --git a/httprunner/parser.py b/httprunner/parser.py
index bc939519..c3d27ae3 100644
--- a/httprunner/parser.py
+++ b/httprunner/parser.py
@@ -1,14 +1,12 @@
-# encoding: utf-8
-
import ast
import builtins
-import collections
-import json
import re
+from typing import Any, Set, Text, Callable, List, Dict
-from loguru import logger
+from httprunner import loader, utils, exceptions
+from httprunner.schema import VariablesMapping, FunctionsMapping
-from httprunner import exceptions, utils, loader
+absolute_http_url_regexp = re.compile(r"^https?://", re.I)
# use $$ to escape $ notation
dolloar_regex_compile = re.compile(r"\$\$")
@@ -17,16 +15,8 @@ variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
# function notation, e.g. ${func1($var_1, $var_3)}
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
-""" Store parse failed api/testcase/testsuite file path
-"""
-parse_failed_testfiles = {}
-
-def get_parse_failed_testfiles():
- return parse_failed_testfiles
-
-
-def parse_string_value(str_value):
+def parse_string_value(str_value: Text) -> Any:
""" parse string to number if possible
e.g. "123" => 123
"12.2" => 12.3
@@ -42,35 +32,17 @@ def parse_string_value(str_value):
return str_value
-def is_var_or_func_exist(content):
- """ check if variable or function exist
- """
- if not isinstance(content, str):
- return False
-
- try:
- match_start_position = content.index("$", 0)
- except ValueError:
- return False
-
- while match_start_position < len(content):
- dollar_match = dolloar_regex_compile.match(content, match_start_position)
- if dollar_match:
- match_start_position = dollar_match.end()
- continue
-
- func_match = function_regex_compile.match(content, match_start_position)
- if func_match:
- return True
-
- var_match = variable_regex_compile.match(content, match_start_position)
- if var_match:
- return True
-
- return False
+def build_url(base_url, path):
+ """ prepend url with base_url unless it's already an absolute URL """
+ if absolute_http_url_regexp.match(path):
+ return path
+ elif base_url:
+ return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/"))
+ else:
+ raise exceptions.ParamsError("base url missed!")
-def regex_findall_variables(content):
+def regex_findall_variables(content: Text) -> List[Text]:
""" extract all variable names from content, which is in format $variable
Args:
@@ -104,7 +76,7 @@ def regex_findall_variables(content):
return []
-def regex_findall_functions(content):
+def regex_findall_functions(content: Text) -> List[Text]:
""" extract all functions from string content, which are in format ${fun()}
Args:
@@ -136,329 +108,28 @@ def regex_findall_functions(content):
return []
-def parse_parameters(parameters, variables_mapping=None, functions_mapping=None):
- """ parse parameters and generate cartesian product.
-
- Args:
- parameters (list) parameters: parameter name and value in list
- parameter value may be in three types:
- (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
- (2) call built-in parameterize function, "${parameterize(account.csv)}"
- (3) call custom function in debugtalk.py, "${gen_app_version()}"
-
- variables_mapping (dict): variables mapping loaded from testcase config
- functions_mapping (dict): functions mapping loaded from debugtalk.py
-
- Returns:
- list: cartesian product list
-
- Examples:
- >>> parameters = [
- {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]},
- {"username-password": "${parameterize(account.csv)}"},
- {"app_version": "${gen_app_version()}"}
- ]
- >>> parse_parameters(parameters)
-
+def extract_variables(content: Any) -> Set:
+ """ extract all variables in content recursively.
"""
- variables_mapping = variables_mapping or {}
- functions_mapping = functions_mapping or {}
- parsed_parameters_list = []
+ if isinstance(content, (list, set, tuple)):
+ variables = set()
+ for item in content:
+ variables = variables | extract_variables(item)
+ return variables
- parameters = utils.ensure_mapping_format(parameters)
- for parameter_name, parameter_content in parameters.items():
- parameter_name_list = parameter_name.split("-")
+ elif isinstance(content, dict):
+ variables = set()
+ for key, value in content.items():
+ variables = variables | extract_variables(value)
+ return variables
- if isinstance(parameter_content, list):
- # (1) data list
- # e.g. {"app_version": ["2.8.5", "2.8.6"]}
- # => [{"app_version": "2.8.5", "app_version": "2.8.6"}]
- # e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]}
- # => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}]
- parameter_content_list = []
- for parameter_item in parameter_content:
- if not isinstance(parameter_item, (list, tuple)):
- # "2.8.5" => ["2.8.5"]
- parameter_item = [parameter_item]
+ elif isinstance(content, str):
+ return set(regex_findall_variables(content))
- # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"}
- # ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"}
- parameter_content_dict = dict(zip(parameter_name_list, parameter_item))
-
- parameter_content_list.append(parameter_content_dict)
- else:
- # (2) & (3)
- parsed_variables_mapping = parse_variables_mapping(
- variables_mapping
- )
- parsed_parameter_content = eval_lazy_data(
- parameter_content,
- parsed_variables_mapping,
- functions_mapping
- )
- if not isinstance(parsed_parameter_content, list):
- raise exceptions.ParamsError("parameters syntax error!")
-
- parameter_content_list = []
- for parameter_item in parsed_parameter_content:
- if isinstance(parameter_item, dict):
- # get subset by parameter name
- # {"app_version": "${gen_app_version()}"}
- # gen_app_version() => [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}]
- # {"username-password": "${get_account()}"}
- # get_account() => [
- # {"username": "user1", "password": "111111"},
- # {"username": "user2", "password": "222222"}
- # ]
- parameter_dict = {key: parameter_item[key] for key in parameter_name_list}
- elif isinstance(parameter_item, (list, tuple)):
- # {"username-password": "${get_account()}"}
- # get_account() => [("user1", "111111"), ("user2", "222222")]
- parameter_dict = dict(zip(parameter_name_list, parameter_item))
- elif len(parameter_name_list) == 1:
- # {"user_agent": "${get_user_agent()}"}
- # get_user_agent() => ["iOS/10.1", "iOS/10.2"]
- parameter_dict = {
- parameter_name_list[0]: parameter_item
- }
-
- parameter_content_list.append(parameter_dict)
-
- parsed_parameters_list.append(parameter_content_list)
-
- return utils.gen_cartesian_product(*parsed_parameters_list)
+ return set()
-def get_uniform_comparator(comparator):
- """ convert comparator alias to uniform name
- """
- if comparator in ["eq", "equals", "==", "is"]:
- return "equals"
- elif comparator in ["lt", "less_than"]:
- return "less_than"
- elif comparator in ["le", "less_than_or_equals"]:
- return "less_than_or_equals"
- elif comparator in ["gt", "greater_than"]:
- return "greater_than"
- elif comparator in ["ge", "greater_than_or_equals"]:
- return "greater_than_or_equals"
- elif comparator in ["ne", "not_equals"]:
- return "not_equals"
- elif comparator in ["str_eq", "string_equals"]:
- return "string_equals"
- elif comparator in ["len_eq", "length_equals", "count_eq"]:
- return "length_equals"
- elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]:
- return "length_greater_than"
- elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals",
- "count_greater_than_or_equals"]:
- return "length_greater_than_or_equals"
- elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
- return "length_less_than"
- elif comparator in ["len_le", "count_le", "length_less_than_or_equals",
- "count_less_than_or_equals"]:
- return "length_less_than_or_equals"
- else:
- return comparator
-
-
-def uniform_validator(validator):
- """ unify validator
-
- Args:
- validator (dict): validator maybe in two formats:
-
- format1: this is kept for compatiblity with the previous versions.
- {"check": "status_code", "comparator": "eq", "expect": 201}
- {"check": "$resp_body_success", "comparator": "eq", "expect": True}
- format2: recommended new version, {comparator: [check_item, expected_value]}
- {'eq': ['status_code', 201]}
- {'eq': ['$resp_body_success', True]}
-
- Returns
- dict: validator info
-
- {
- "check": "status_code",
- "expect": 201,
- "comparator": "equals"
- }
-
- """
- if not isinstance(validator, dict):
- raise exceptions.ParamsError(f"invalid validator: {validator}")
-
- if "check" in validator and "expect" in validator:
- # format1
- check_item = validator["check"]
- expect_value = validator["expect"]
- comparator = validator.get("comparator", "eq")
-
- elif len(validator) == 1:
- # format2
- comparator = list(validator.keys())[0]
- compare_values = validator[comparator]
-
- if not isinstance(compare_values, list) or len(compare_values) != 2:
- raise exceptions.ParamsError(f"invalid validator: {validator}")
-
- check_item, expect_value = compare_values
-
- else:
- raise exceptions.ParamsError(f"invalid validator: {validator}")
-
- # uniform comparator, e.g. lt => less_than, eq => equals
- comparator = get_uniform_comparator(comparator)
-
- return {
- "check": check_item,
- "expect": expect_value,
- "comparator": comparator
- }
-
-
-def _convert_validators_to_mapping(validators):
- """ convert validators list to mapping.
-
- Args:
- validators (list): validators in list
-
- Returns:
- dict: validators mapping, use (check, comparator) as key.
-
- Examples:
- >>> validators = [
- {"check": "v1", "expect": 201, "comparator": "eq"},
- {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
- ]
- >>> print(_convert_validators_to_mapping(validators))
- {
- ("v1", "eq"): {"check": "v1", "expect": 201, "comparator": "eq"},
- ('{"b": 1}', "eq"): {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
- }
-
- """
- validators_mapping = {}
-
- for validator in validators:
- if not isinstance(validator["check"], collections.Hashable):
- check = json.dumps(validator["check"])
- else:
- check = validator["check"]
-
- key = (check, validator["comparator"])
- validators_mapping[key] = validator
-
- return validators_mapping
-
-
-def extend_validators(raw_validators, override_validators):
- """ extend raw_validators with override_validators.
- override_validators will merge and override raw_validators.
-
- Args:
- raw_validators (dict):
- override_validators (dict):
-
- Returns:
- list: extended validators
-
- Examples:
- >>> raw_validators = [{'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"}]
- >>> override_validators = [{"check": "v1", "expect": 201}, {'len_eq': ['s3', 12]}]
- >>> extend_validators(raw_validators, override_validators)
- [
- {"check": "v1", "expect": 201, "comparator": "eq"},
- {"check": "s2", "expect": 16, "comparator": "len_eq"},
- {"check": "s3", "expect": 12, "comparator": "len_eq"}
- ]
-
- """
-
- if not raw_validators:
- return override_validators
-
- elif not override_validators:
- return raw_validators
-
- else:
- def_validators_mapping = _convert_validators_to_mapping(raw_validators)
- ref_validators_mapping = _convert_validators_to_mapping(override_validators)
-
- def_validators_mapping.update(ref_validators_mapping)
- return list(def_validators_mapping.values())
-
-
-###############################################################################
-## parse content with variables and functions mapping
-###############################################################################
-
-def get_mapping_variable(variable_name, variables_mapping):
- """ get variable from variables_mapping.
-
- Args:
- variable_name (str): variable name
- variables_mapping (dict): variables mapping
-
- Returns:
- mapping variable value.
-
- Raises:
- exceptions.VariableNotFound: variable is not found.
-
- """
- try:
- return variables_mapping[variable_name]
- except KeyError:
- raise exceptions.VariableNotFound(f"{variable_name} is not found.")
-
-
-def get_mapping_function(function_name, functions_mapping):
- """ get function from functions_mapping,
- if not found, then try to check if builtin function.
-
- Args:
- function_name (str): function name
- functions_mapping (dict): functions mapping
-
- Returns:
- mapping function object.
-
- Raises:
- exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
-
- """
- if function_name in functions_mapping:
- return functions_mapping[function_name]
-
- elif function_name in ["parameterize", "P"]:
- return loader.load_csv_file
-
- elif function_name in ["environ", "ENV"]:
- return utils.get_os_environ
-
- elif function_name in ["multipart_encoder", "multipart_content_type"]:
- # extension for upload test
- from httprunner.ext import uploader
- return getattr(uploader, function_name)
-
- try:
- # check if HttpRunner builtin functions
- built_in_functions = loader.load_builtin_functions()
- return built_in_functions[function_name]
- except KeyError:
- pass
-
- try:
- # check if Python builtin functions
- return getattr(builtins, function_name)
- except AttributeError:
- pass
-
- raise exceptions.FunctionNotFound(f"{function_name} is not found.")
-
-
-def parse_function_params(params):
+def parse_function_params(params: Text) -> Dict:
""" parse function params to args and kwargs.
Args:
@@ -510,998 +181,241 @@ def parse_function_params(params):
return function_meta
-class LazyFunction(object):
- """ call function lazily.
- """
-
- def __init__(self, function_meta, functions_mapping=None, check_variables_set=None):
- """ init LazyFunction object with function_meta
-
- Args:
- function_meta (dict): function name, args and kwargs.
- {
- "func_name": "func",
- "args": [1, 2]
- "kwargs": {"a": 3, "b": 4}
- }
-
- """
- self.functions_mapping = functions_mapping or {}
- self.check_variables_set = check_variables_set or set()
- self.cache_key = None
- self.__parse(function_meta)
-
- def __parse(self, function_meta):
- """ init func as lazy functon instance
-
- Args:
- function_meta (dict): function meta including name, args and kwargs
- """
- self._func = get_mapping_function(
- function_meta["func_name"],
- self.functions_mapping
- )
- self.func_name = self._func.__name__
- self._args = prepare_lazy_data(
- function_meta.get("args", []),
- self.functions_mapping,
- self.check_variables_set
- )
- self._kwargs = prepare_lazy_data(
- function_meta.get("kwargs", {}),
- self.functions_mapping,
- self.check_variables_set
- )
-
- if self.func_name == "load_csv_file":
- if len(self._args) != 1 or self._kwargs:
- raise exceptions.ParamsError("P() should only pass in one argument!")
- self._args = [self._args[0]]
- elif self.func_name == "get_os_environ":
- if len(self._args) != 1 or self._kwargs:
- raise exceptions.ParamsError("ENV() should only pass in one argument!")
- self._args = [self._args[0]]
-
- def get_args(self):
- return self._args
-
- def update_args(self, args):
- self._args = args
-
- def __repr__(self):
- args_string = ""
-
- if self._args:
- str_args = [str(arg) for arg in self._args]
- args_string += ", ".join(str_args)
-
- if self._kwargs:
- args_string += ", "
- str_kwargs = [
- f"{key}={str(value)}"
- for key, value in self._kwargs.items()
- ]
- args_string += ", ".join(str_kwargs)
-
- return f"LazyFunction({self.func_name}({args_string}))"
-
- def __prepare_cache_key(self, args, kwargs):
- return self.func_name, repr(args), repr(kwargs)
-
- def to_value(self, variables_mapping=None):
- """ parse lazy data with evaluated variables mapping.
- Notice: variables_mapping should not contain any variable or function.
- """
- variables_mapping = variables_mapping or {}
- args = parse_lazy_data(self._args, variables_mapping)
- kwargs = parse_lazy_data(self._kwargs, variables_mapping)
- self.cache_key = self.__prepare_cache_key(args, kwargs)
- return self._func(*args, **kwargs)
-
-
-cached_functions_mapping = {}
-""" cached function calling results.
-"""
-
-
-class LazyString(object):
- """ evaluate string lazily.
- """
-
- def __init__(self, raw_string, functions_mapping=None, check_variables_set=None, cached=False):
- """ make raw_string as lazy object with functions_mapping
- check if any variable undefined in check_variables_set
- """
- self.raw_string = raw_string
- self.functions_mapping = functions_mapping or {}
- self.check_variables_set = check_variables_set or set()
- self.cached = cached
- self.__parse(raw_string)
-
- def __parse(self, raw_string):
- """ parse raw string, replace function and variable with {}
-
- Args:
- raw_string(str): string with functions or varialbes
- e.g. "ABC${func2($a, $b)}DE$c"
-
- Returns:
- string: "ABC{}DE{}"
- args: ["${func2($a, $b)}", "$c"]
-
- """
- self._args = []
-
- def escape_braces(origin_string):
- return origin_string.replace("{", "{{").replace("}", "}}")
-
- try:
- match_start_position = raw_string.index("$", 0)
- begin_string = raw_string[0:match_start_position]
- self._string = escape_braces(begin_string)
- except ValueError:
- self._string = escape_braces(raw_string)
- return
-
- while match_start_position < len(raw_string):
-
- # Notice: notation priority
- # $$ > ${func($a, $b)} > $var
-
- # search $$
- dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
- if dollar_match:
- match_start_position = dollar_match.end()
- self._string += "$"
- continue
-
- # search function like ${func($a, $b)}
- func_match = function_regex_compile.match(raw_string, match_start_position)
- if func_match:
- function_meta = {
- "func_name": func_match.group(1)
- }
- function_meta.update(parse_function_params(func_match.group(2)))
- lazy_func = LazyFunction(
- function_meta,
- self.functions_mapping,
- self.check_variables_set
- )
- self._args.append(lazy_func)
- match_start_position = func_match.end()
- self._string += "{}"
- continue
-
- # search variable like ${var} or $var
- var_match = variable_regex_compile.match(raw_string, match_start_position)
- if var_match:
- var_name = var_match.group(1) or var_match.group(2)
- # check if any variable undefined in check_variables_set
- if var_name not in self.check_variables_set:
- raise exceptions.VariableNotFound(var_name)
-
- self._args.append(var_name)
- match_start_position = var_match.end()
- self._string += "{}"
- continue
-
- curr_position = match_start_position
- try:
- # find next $ location
- match_start_position = raw_string.index("$", curr_position + 1)
- remain_string = raw_string[curr_position:match_start_position]
- except ValueError:
- remain_string = raw_string[curr_position:]
- # break while loop
- match_start_position = len(raw_string)
-
- self._string += escape_braces(remain_string)
-
- def __repr__(self):
- return f"LazyString({self.raw_string})"
-
- def to_value(self, variables_mapping=None):
- """ parse lazy data with evaluated variables mapping.
- Notice: variables_mapping should not contain any variable or function.
- """
- variables_mapping = variables_mapping or {}
-
- args = []
- for arg in self._args:
- if isinstance(arg, LazyFunction):
- if self.cached and arg.cache_key and arg.cache_key in cached_functions_mapping:
- value = cached_functions_mapping[arg.cache_key]
- else:
- value = arg.to_value(variables_mapping)
- cached_functions_mapping[arg.cache_key] = value
- args.append(value)
- else:
- # variable
- var_value = get_mapping_variable(arg, variables_mapping)
- args.append(var_value)
-
- if self._string == "{}":
- return args[0]
- else:
- return self._string.format(*args)
-
-
-def prepare_lazy_data(content, functions_mapping=None, check_variables_set=None, cached=False):
- """ make string in content as lazy object with functions_mapping
-
- Raises:
- exceptions.VariableNotFound: if any variable undefined in check_variables_set
-
- """
- # TODO: refactor type check
- if content is None or isinstance(content, (int, float, bool, type)):
- return content
-
- elif isinstance(content, (list, set, tuple)):
- return [
- prepare_lazy_data(
- item,
- functions_mapping,
- check_variables_set,
- cached
- )
- for item in content
- ]
-
- elif isinstance(content, dict):
- parsed_content = {}
- for key, value in content.items():
- parsed_key = prepare_lazy_data(
- key,
- functions_mapping,
- check_variables_set,
- cached
- )
- parsed_value = prepare_lazy_data(
- value,
- functions_mapping,
- check_variables_set,
- cached
- )
- parsed_content[parsed_key] = parsed_value
-
- return parsed_content
-
- elif isinstance(content, str):
- # content is in string format here
- if not is_var_or_func_exist(content):
- # content is neither variable nor function
- # replace $$ notation with $ and consider it as normal char.
- # e.g. abc => abc, abc$$def => abc$def, abc$$$$def$$h => abc$$def$h
- return content.replace("$$", "$")
-
- functions_mapping = functions_mapping or {}
- check_variables_set = check_variables_set or set()
- content = LazyString(content, functions_mapping, check_variables_set, cached)
-
- return content
-
-
-def parse_lazy_data(content, variables_mapping=None):
- """ parse lazy data with evaluated variables mapping.
- Notice: variables_mapping should not contain any variable or function.
- """
- # TODO: refactor type check
- if content is None or isinstance(content, (int, float, bool, type)):
- return content
-
- elif isinstance(content, LazyString):
- variables_mapping = utils.ensure_mapping_format(variables_mapping or {})
- return content.to_value(variables_mapping)
-
- elif isinstance(content, (list, set, tuple)):
- return [
- parse_lazy_data(item, variables_mapping)
- for item in content
- ]
-
- elif isinstance(content, dict):
- parsed_content = {}
- for key, value in content.items():
- parsed_key = parse_lazy_data(key, variables_mapping)
- parsed_value = parse_lazy_data(value, variables_mapping)
- parsed_content[parsed_key] = parsed_value
-
- return parsed_content
-
- return content
-
-
-def eval_lazy_data(content, variables_mapping=None, functions_mapping=None):
- """ evaluate data instantly.
- Notice: variables_mapping should not contain any variable or function.
- """
- variables_mapping = variables_mapping or {}
- check_variables_set = set(variables_mapping.keys())
- return parse_lazy_data(
- prepare_lazy_data(
- content,
- functions_mapping,
- check_variables_set
- ),
- variables_mapping
- )
-
-
-def extract_variables(content):
- """ extract all variables in content recursively.
- """
- if isinstance(content, (list, set, tuple)):
- variables = set()
- for item in content:
- variables = variables | extract_variables(item)
- return variables
-
- elif isinstance(content, dict):
- variables = set()
- for key, value in content.items():
- variables = variables | extract_variables(value)
- return variables
-
- elif isinstance(content, LazyString):
- return set(regex_findall_variables(content.raw_string))
-
- return set()
-
-
-def parse_variables_mapping(variables_mapping):
- """ eval each prepared variable and function in variables_mapping.
+def get_mapping_variable(variable_name: Text, variables_mapping: VariablesMapping) -> Any:
+ """ get variable from variables_mapping.
Args:
- variables_mapping (dict):
- {
- "varA": LazyString(123$varB),
- "varB": LazyString(456$varC),
- "varC": LazyString(${sum_two($a, $b)}),
- "a": 1,
- "b": 2,
- "c": {"key": LazyString($b)},
- "d": [LazyString($a), 3]
- }
+ variable_name (str): variable name
+ variables_mapping (dict): variables mapping
Returns:
- dict: parsed variables_mapping should not contain any variable or function.
- {
- "varA": "1234563",
- "varB": "4563",
- "varC": "3",
- "a": 1,
- "b": 2,
- "c": {"key": 2},
- "d": [1, 3]
- }
+ mapping variable value.
+
+ Raises:
+ exceptions.VariableNotFound: variable is not found.
"""
- run_times = 0
- parsed_variables_mapping = {}
+ # TODO: get variable from debugtalk module and environ
+ try:
+ return variables_mapping[variable_name]
+ except KeyError:
+ raise exceptions.VariableNotFound(f"{variable_name} not found in {variables_mapping}")
- while len(parsed_variables_mapping) != len(variables_mapping):
+
+def get_mapping_function(function_name: Text, functions_mapping: FunctionsMapping) -> Callable:
+ """ get function from functions_mapping,
+ if not found, then try to check if builtin function.
+
+ Args:
+ function_name (str): function name
+ functions_mapping (dict): functions mapping
+
+ Returns:
+ mapping function object.
+
+ Raises:
+ exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
+
+ """
+ if function_name in functions_mapping:
+ return functions_mapping[function_name]
+
+ elif function_name in ["parameterize", "P"]:
+ return loader.load_csv_file
+
+ elif function_name in ["environ", "ENV"]:
+ return utils.get_os_environ
+
+ elif function_name in ["multipart_encoder", "multipart_content_type"]:
+ # extension for upload test
+ from httprunner.ext import uploader
+ return getattr(uploader, function_name)
+
+ try:
+ # check if HttpRunner builtin functions
+ built_in_functions = loader.load_builtin_functions()
+ return built_in_functions[function_name]
+ except KeyError:
+ pass
+
+ try:
+ # check if Python builtin functions
+ return getattr(builtins, function_name)
+ except AttributeError:
+ pass
+
+ raise exceptions.FunctionNotFound(f"{function_name} is not found.")
+
+
+def parse_string(
+ raw_string: Text,
+ variables_mapping: VariablesMapping,
+ functions_mapping: FunctionsMapping) -> Any:
+ """ parse string content with variables and functions mapping.
+
+ Args:
+ raw_string: raw string content to be parsed.
+ variables_mapping: variables mapping.
+ functions_mapping: functions mapping.
+
+ Returns:
+ str: parsed string content.
+
+ Examples:
+ >>> raw_string = "abc${add_one($num)}def"
+ >>> variables_mapping = {"num": 3}
+ >>> functions_mapping = {"add_one": lambda x: x + 1}
+ >>> parse_string(raw_string, variables_mapping, functions_mapping)
+ "abc4def"
+
+ """
+ try:
+ match_start_position = raw_string.index("$", 0)
+ parsed_string = raw_string[0:match_start_position]
+ except ValueError:
+ parsed_string = raw_string
+ return parsed_string
+
+ while match_start_position < len(raw_string):
+
+ # Notice: notation priority
+ # $$ > ${func($a, $b)} > $var
+
+ # search $$
+ dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
+ if dollar_match:
+ match_start_position = dollar_match.end()
+ parsed_string += "$"
+ continue
+
+ # search function like ${func($a, $b)}
+ func_match = function_regex_compile.match(raw_string, match_start_position)
+ if func_match:
+ func_name = func_match.group(1)
+ func = get_mapping_function(func_name, functions_mapping)
+
+ func_params_str = func_match.group(2)
+ function_meta = parse_function_params(func_params_str)
+ args = function_meta["args"]
+ kwargs = function_meta["kwargs"]
+
+ parsed_args = parse_data(args, variables_mapping, functions_mapping)
+ parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
+ func_eval_value = func(*parsed_args, **parsed_kwargs)
+
+ func_raw_str = "${" + func_name + f"({func_params_str})" + "}"
+ if func_raw_str == raw_string:
+ # raw_string is a function, e.g. "${add_one(3)}", return its eval value directly
+ return func_eval_value
+
+ # raw_string contains one or many functions, e.g. "abc${add_one(3)}def"
+ parsed_string += str(func_eval_value)
+ match_start_position = func_match.end()
+ continue
+
+ # search variable like ${var} or $var
+ var_match = variable_regex_compile.match(raw_string, match_start_position)
+ if var_match:
+ var_name = var_match.group(1) or var_match.group(2)
+ var_value = get_mapping_variable(var_name, variables_mapping)
+
+ if f"${var_name}" == raw_string or "${" + var_name + "}" == raw_string:
+ # raw_string is a variable, $var or ${var}, return its value directly
+ return var_value
+
+ # raw_string contains one or many variables, e.g. "abc${var}def"
+ parsed_string += str(var_value)
+ match_start_position = var_match.end()
+ continue
+
+ curr_position = match_start_position
+ try:
+ # find next $ location
+ match_start_position = raw_string.index("$", curr_position + 1)
+ remain_string = raw_string[curr_position:match_start_position]
+ except ValueError:
+ remain_string = raw_string[curr_position:]
+ # break while loop
+ match_start_position = len(raw_string)
+
+ parsed_string += remain_string
+
+ return parsed_string
+
+
+def parse_data(
+ raw_data: Any,
+ variables_mapping: VariablesMapping = None,
+ functions_mapping: FunctionsMapping = None) -> Any:
+ """ parse raw data with evaluated variables mapping.
+ Notice: variables_mapping should not contain any variable or function.
+ """
+ if isinstance(raw_data, str):
+ # content in string format may contains variables and functions
+ variables_mapping = variables_mapping or {}
+ functions_mapping = functions_mapping or {}
+ raw_data = raw_data.strip()
+ return parse_string(raw_data, variables_mapping, functions_mapping)
+
+ elif isinstance(raw_data, (list, set, tuple)):
+ return [
+ parse_data(item, variables_mapping, functions_mapping)
+ for item in raw_data
+ ]
+
+ elif isinstance(raw_data, dict):
+ parsed_data = {}
+ for key, value in raw_data.items():
+ parsed_key = parse_data(key, variables_mapping, functions_mapping)
+ parsed_value = parse_data(value, variables_mapping, functions_mapping)
+ parsed_data[parsed_key] = parsed_value
+
+ return parsed_data
+
+ else:
+ # other types, e.g. None, int, float, bool
+ return raw_data
+
+
+def parse_variables_mapping(
+ variables_mapping: VariablesMapping,
+ functions_mapping: FunctionsMapping = None) -> VariablesMapping:
+
+ parsed_variables: VariablesMapping = {}
+
+ while len(parsed_variables) != len(variables_mapping):
for var_name in variables_mapping:
- run_times += 1
- if run_times > len(variables_mapping) * 4:
- not_found_variables = {
- key: variables_mapping[key]
- for key in variables_mapping
- if key not in parsed_variables_mapping
- }
- raise exceptions.VariableNotFound(not_found_variables)
-
- if var_name in parsed_variables_mapping:
+ if var_name in parsed_variables:
continue
- value = variables_mapping[var_name]
- variables = extract_variables(value)
+ var_value = variables_mapping[var_name]
+ variables = extract_variables(var_value)
# check if reference variable itself
if var_name in variables:
# e.g.
- # var_name = "token"
- # variables_mapping = {"token": LazyString($token)}
- # var_name = "key"
- # variables_mapping = {"key": [LazyString($key), 2]}
+ # variables_mapping = {"token": "abc$token"}
+ # variables_mapping = {"key": ["$key", 2]}
raise exceptions.VariableNotFound(var_name)
- if variables:
- # reference other variable, or function call with other variable
+ # check if reference variable not in variables_mapping
+ not_defined_variables = [
+ v_name
+ for v_name in variables
+ if v_name not in variables_mapping
+ ]
+ if not_defined_variables:
# e.g. {"varA": "123$varB", "varB": "456$varC"}
# e.g. {"varC": "${sum_two($a, $b)}"}
- if any([_var_name not in parsed_variables_mapping for _var_name in variables]):
- # reference variable not parsed
- continue
+ raise exceptions.VariableNotFound(not_defined_variables)
- parsed_value = parse_lazy_data(value, parsed_variables_mapping)
- parsed_variables_mapping[var_name] = parsed_value
-
- return parsed_variables_mapping
-
-
-def _extend_with_api(test_dict, api_def_dict):
- """ extend test with api definition, test will merge and override api definition.
-
- Args:
- test_dict (dict): test block, this will override api_def_dict
- api_def_dict (dict): api definition
-
- Examples:
- >>> api_def_dict = {
- "name": "get token 1",
- "request": {...},
- "validate": [{'eq': ['status_code', 200]}]
- }
- >>> test_dict = {
- "name": "get token 2",
- "extract": {"token": "content.token"},
- "validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
- }
- >>> _extend_with_api(test_dict, api_def_dict)
- >>> print(test_dict)
- {
- "name": "get token 2",
- "request": {...},
- "extract": {"token": "content.token"},
- "validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
- }
-
- """
- # override api name
- test_dict.setdefault("name", api_def_dict.pop("name", "api name undefined"))
-
- # override variables
- def_variables = api_def_dict.pop("variables", [])
- test_dict["variables"] = utils.extend_variables(
- def_variables,
- test_dict.get("variables", {})
- )
-
- # merge & override validators TODO: relocate
- def_raw_validators = api_def_dict.pop("validate", [])
- def_validators = [
- uniform_validator(_validator)
- for _validator in def_raw_validators
- ]
- ref_validators = test_dict.pop("validate", [])
- test_dict["validate"] = extend_validators(
- def_validators,
- ref_validators
- )
-
- # merge & override extractors
- def_extrators = api_def_dict.pop("extract", {})
- test_dict["extract"] = utils.extend_variables(
- def_extrators,
- test_dict.get("extract", {})
- )
-
- # merge & override request
- test_dict["request"] = api_def_dict.pop("request", {})
-
- # base_url & verify: priority api_def_dict > test_dict
- if api_def_dict.get("base_url"):
- test_dict["base_url"] = api_def_dict["base_url"]
-
- if "verify" in api_def_dict:
- test_dict["request"]["verify"] = api_def_dict["verify"]
-
- # merge & override setup_hooks
- def_setup_hooks = api_def_dict.pop("setup_hooks", [])
- ref_setup_hooks = test_dict.get("setup_hooks", [])
- extended_setup_hooks_tmp = def_setup_hooks + ref_setup_hooks
- extended_setup_hooks = list(set(extended_setup_hooks_tmp))
- extended_setup_hooks.sort(key=extended_setup_hooks_tmp.index)
- if extended_setup_hooks:
- test_dict["setup_hooks"] = extended_setup_hooks
- # merge & override teardown_hooks
- def_teardown_hooks = api_def_dict.pop("teardown_hooks", [])
- ref_teardown_hooks = test_dict.get("teardown_hooks", [])
- extended_teardown_hooks_tmp = def_teardown_hooks + ref_teardown_hooks
- extended_teardown_hooks = list(set(extended_teardown_hooks_tmp))
- extended_teardown_hooks.sort(key=extended_teardown_hooks_tmp.index)
- if extended_teardown_hooks:
- test_dict["teardown_hooks"] = extended_teardown_hooks
-
- # TODO: extend with other api definition items, e.g. times
- test_dict.update(api_def_dict)
-
-
-def _extend_with_testcase(test_dict, testcase_def_dict):
- """ extend test with testcase definition
- test will merge and override testcase config definition.
-
- Args:
- test_dict (dict): test block
- testcase_def_dict (dict): testcase definition
-
- Returns:
- dict: extended test dict.
-
- """
- # override testcase config variables
- testcase_def_dict["config"].setdefault("variables", {})
- testcase_def_variables = utils.ensure_mapping_format(
- testcase_def_dict["config"].get("variables", {}))
- testcase_def_variables.update(test_dict.pop("variables", {}))
- testcase_def_dict["config"]["variables"] = testcase_def_variables
-
- # override base_url, verify
- # priority: testcase config > testsuite tests
- test_base_url = test_dict.pop("base_url", "")
- if not testcase_def_dict["config"].get("base_url"):
- testcase_def_dict["config"]["base_url"] = test_base_url
-
- # override name
- test_name = test_dict.pop("name", None) \
- or testcase_def_dict["config"].pop("name", None) \
- or "testcase name undefined"
-
- # override testcase config name, output, etc.
- testcase_def_dict["config"].update(test_dict)
- testcase_def_dict["config"]["name"] = test_name
-
- test_dict.clear()
- test_dict.update(testcase_def_dict)
-
-
-def __prepare_config(config, project_meta, session_variables_set=None):
- """ parse testcase/testsuite config.
- """
- # get config variables
- raw_config_variables = config.pop("variables", {})
-
- override_variables = utils.deepcopy_dict(project_meta.get("variables", {}))
- functions = project_meta.get("functions", {})
-
- if isinstance(raw_config_variables, str) and function_regex_compile.match(
- raw_config_variables):
- # config variables are generated by calling function
- # e.g.
- # "config": {
- # "name": "basic test with httpbin",
- # "variables": "${gen_variables()}"
- # }
- raw_config_variables_mapping = parse_lazy_data(
- prepare_lazy_data(raw_config_variables, functions_mapping=functions)
- )
- else:
- raw_config_variables_mapping = utils.ensure_mapping_format(raw_config_variables)
-
- # override config variables with passed in variables
- raw_config_variables_mapping.update(override_variables)
-
- if raw_config_variables_mapping:
- config["variables"] = raw_config_variables_mapping
-
- if "setup_testcase" in functions:
- config.setdefault("setup_hooks", [])
- config["setup_hooks"].insert(0, "${setup_testcase($variables)}")
-
- if "teardown_testcase" in functions:
- config.setdefault("teardown_hooks", [])
- config["teardown_hooks"].append("${teardown_testcase()}")
-
- check_variables_set = set(raw_config_variables_mapping.keys())
- check_variables_set |= (session_variables_set or set())
- check_variables_set.add("variables")
- prepared_config = prepare_lazy_data(config, functions, check_variables_set, cached=True)
- return prepared_config
-
-
-def __prepare_testcase_tests(tests, config, project_meta, session_variables_set=None):
- """ override tests with testcase config variables, base_url and verify.
- test maybe nested testcase.
-
- variables priority:
- testcase config > testcase test > testcase_def config > testcase_def test > api
-
- base_url priority:
- testcase test > testcase config > testsuite test > testsuite config > api
-
- verify priority:
- testcase teststep (api) > testcase config > testsuite config
-
- Args:
- tests (list):
- config (dict):
- project_meta (dict):
-
- """
- config_variables = config.get("variables", {})
- config_base_url = config.get("base_url", "")
- config_verify = config.get("verify", True)
- functions = project_meta.get("functions", {})
-
- prepared_testcase_tests = []
- session_variables_set = set(config_variables.keys()) | (session_variables_set or set())
- for test_dict in tests:
-
- teststep_variables_set = {"request", "response", "variables"}
-
- # 1, testcase config => testcase tests
- # override test_dict variables
- test_dict_variables = utils.extend_variables(
- test_dict.pop("variables", {}),
- config_variables
- )
- test_dict["variables"] = test_dict_variables
-
- if "setup_teststep" in functions:
- test_dict.setdefault("setup_hooks", [])
- test_dict["setup_hooks"].insert(0, "${setup_teststep($request, $variables)}")
-
- if "teardown_teststep" in functions:
- test_dict.setdefault("teardown_hooks", [])
- test_dict["teardown_hooks"].append("${teardown_teststep($response)}")
-
- # base_url & verify: priority test_dict > config
- if (not test_dict.get("base_url")) and config_base_url:
- test_dict["base_url"] = config_base_url
-
- # unify validators' format
- if "validate" in test_dict:
- ref_raw_validators = test_dict.pop("validate", [])
- test_dict["validate"] = [
- uniform_validator(_validator)
- for _validator in ref_raw_validators
- ]
-
- if "testcase_def" in test_dict:
- # test_dict is nested testcase
-
- # pass former teststep's (as a testcase) export value to next teststep
- # Since V2.2.2, `extract` is used to replace `output`,
- # `output` is also kept for compatibility
- if "extract" in test_dict:
- session_variables_set |= set(test_dict["extract"])
- elif "output" in test_dict:
- # kept for compatibility
- session_variables_set |= set(test_dict["output"])
-
- # 2, testcase test_dict => testcase_def config
- testcase_def = test_dict.pop("testcase_def")
- _extend_with_testcase(test_dict, testcase_def)
-
- # verify priority: nested testcase config > testcase config
- test_dict["config"].setdefault("verify", config_verify)
-
- # 3, testcase_def config => testcase_def test_dict
- test_dict = _parse_testcase(test_dict, project_meta, session_variables_set)
- if not test_dict:
+ try:
+ parsed_value = parse_data(
+ var_value, parsed_variables, functions_mapping)
+ except exceptions.VariableNotFound:
continue
- elif "api_def" in test_dict:
- # test_dict has API reference
- # 2, test_dict => api
- api_def_dict = test_dict.pop("api_def")
- _extend_with_api(test_dict, api_def_dict)
+ parsed_variables[var_name] = parsed_value
- # verify priority: testcase teststep > testcase config
- if "request" in test_dict:
- if "verify" not in test_dict["request"]:
- test_dict["request"]["verify"] = config_verify
-
- if "upload" in test_dict["request"]:
- from httprunner.ext.uploader import prepare_upload_test
- prepare_upload_test(test_dict)
-
- # current teststep variables
- teststep_variables_set |= set(test_dict.get("variables", {}).keys())
-
- # move extracted variable to session variables
- if "extract" in test_dict:
- extract_mapping = utils.ensure_mapping_format(test_dict["extract"])
- session_variables_set |= set(extract_mapping.keys())
-
- teststep_variables_set |= session_variables_set
-
- # convert validators to lazy function
- validators = test_dict.pop("validate", [])
- prepared_validators = []
- for _validator in validators:
- function_meta = {
- "func_name": _validator["comparator"],
- "args": [
- _validator["check"],
- _validator["expect"]
- ],
- "kwargs": {}
- }
- prepared_validators.append(
- LazyFunction(
- function_meta,
- functions,
- teststep_variables_set
- )
- )
- test_dict["validate"] = prepared_validators
-
- # convert variables and functions to lazy object.
- # raises VariableNotFound if undefined variable exists in test_dict
- prepared_test_dict = prepare_lazy_data(
- test_dict,
- functions,
- teststep_variables_set
- )
- prepared_testcase_tests.append(prepared_test_dict)
-
- return prepared_testcase_tests
-
-
-def _parse_testcase(testcase, project_meta, session_variables_set=None):
- """ parse testcase
-
- Args:
- testcase (dict):
- {
- "config": {},
- "teststeps": []
- }
-
- """
- testcase.setdefault("config", {})
-
- try:
- prepared_config = __prepare_config(
- testcase["config"],
- project_meta,
- session_variables_set
- )
- prepared_testcase_tests = __prepare_testcase_tests(
- testcase["teststeps"],
- prepared_config,
- project_meta,
- session_variables_set
- )
- return {
- "config": prepared_config,
- "teststeps": prepared_testcase_tests
- }
- except (exceptions.MyBaseFailure, exceptions.MyBaseError) as ex:
- testcase_type = testcase["type"]
- testcase_path = testcase.get("path")
-
- logger.error(f"failed to parse testcase: {testcase_path}, error: {ex}")
-
- global parse_failed_testfiles
- if testcase_type not in parse_failed_testfiles:
- parse_failed_testfiles[testcase_type] = []
-
- parse_failed_testfiles[testcase_type].append(testcase_path)
-
- return None
-
-
-def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_meta):
- """ override testscases with testsuite config variables, base_url and verify.
-
- variables priority:
- parameters > testsuite config > testcase config > testcase_def config > testcase_def tests > api
-
- base_url priority:
- testcase_def tests > testcase_def config > testcase config > testsuite config
-
- Args:
- testcases (dict):
- {
- "testcase1 name": {
- "testcase": "testcases/create_user.yml",
- "weight": 2,
- "variables": {
- "uid": 1000
- },
- "parameters": {
- "uid": [100, 101, 102]
- },
- "testcase_def": {
- "config": {},
- "teststeps": []
- }
- },
- "testcase2 name": {}
- }
- testsuite_config (dict):
- {
- "name": "testsuite name",
- "variables": {
- "device_sn": "${gen_random_string(15)}"
- },
- "base_url": "http://127.0.0.1:5000"
- }
- project_meta (dict):
- {
- "env": {},
- "functions": {}
- }
-
- """
- testsuite_base_url = testsuite_config.get("base_url")
- testsuite_config_variables = testsuite_config.get("variables", {})
- functions = project_meta.get("functions", {})
- parsed_testcase_list = []
-
- for testcase_name, testcase in testcases.items():
-
- parsed_testcase = testcase.pop("testcase_def")
- parsed_testcase.setdefault("config", {})
- parsed_testcase["path"] = testcase["testcase"]
- parsed_testcase["type"] = "testcase"
- parsed_testcase["config"]["name"] = testcase_name
-
- if "weight" in testcase:
- parsed_testcase["config"]["weight"] = testcase["weight"]
-
- # base_url priority: testcase config > testsuite config
- parsed_testcase["config"].setdefault("base_url", testsuite_base_url)
-
- # 1, testsuite config => testcase config
- # override test_dict variables
- testcase_config_variables = utils.extend_variables(
- testcase.pop("variables", {}),
- testsuite_config_variables
- )
-
- # 2, testcase config > testcase_def config
- # override testcase_def config variables
- overrided_testcase_config_variables = utils.extend_variables(
- parsed_testcase["config"].pop("variables", {}),
- testcase_config_variables
- )
-
- if overrided_testcase_config_variables:
- parsed_testcase["config"]["variables"] = overrided_testcase_config_variables
-
- # parse config variables
- parsed_config_variables = parse_variables_mapping(overrided_testcase_config_variables)
-
- # parse parameters
- if "parameters" in testcase and testcase["parameters"]:
- cartesian_product_parameters = parse_parameters(
- testcase["parameters"],
- parsed_config_variables,
- functions
- )
-
- for parameter_variables in cartesian_product_parameters:
- # deepcopy to avoid influence between parameters
- testcase_copied = utils.deepcopy_dict(parsed_testcase)
- parsed_config_variables_copied = utils.deepcopy_dict(parsed_config_variables)
- testcase_copied["config"]["variables"] = utils.extend_variables(
- parsed_config_variables_copied,
- parameter_variables
- )
- parsed_testcase_copied = _parse_testcase(testcase_copied, project_meta)
- if not parsed_testcase_copied:
- continue
- parsed_testcase_copied["config"]["name"] = parse_lazy_data(
- parsed_testcase_copied["config"]["name"],
- testcase_copied["config"]["variables"]
- )
- parsed_testcase_list.append(parsed_testcase_copied)
-
- else:
- parsed_testcase = _parse_testcase(parsed_testcase, project_meta)
- if not parsed_testcase:
- continue
- parsed_testcase_list.append(parsed_testcase)
-
- return parsed_testcase_list
-
-
-def _parse_testsuite(testsuite, project_meta):
- testsuite.setdefault("config", {})
- prepared_config = __prepare_config(testsuite["config"], project_meta)
- parsed_testcase_list = __get_parsed_testsuite_testcases(
- testsuite["testcases"],
- prepared_config,
- project_meta
- )
- return parsed_testcase_list
-
-
-def parse_tests(tests_mapping):
- """ parse tests and load to parsed testcases
- tests include api, testcases and testsuites.
-
- Args:
- tests_mapping (dict): project info and testcases list.
-
- {
- "project_meta": {
- "PWD": "XXXXX",
- "functions": {},
- "variables": {}, # optional, priority 1
- "env": {}
- },
- "testsuites": [
- { # testsuite data structure
- "config": {},
- "testcases": {
- "testcase1 name": {
- "variables": {
- "uid": 1000
- },
- "parameters": {
- "uid": [100, 101, 102]
- },
- "testcase_def": {
- "config": {},
- "teststeps": []
- }
- },
- "testcase2 name": {}
- }
- }
- ],
- "testcases": [
- { # testcase data structure
- "config": {
- "name": "desc1",
- "path": "testcase1_path",
- "variables": {}, # optional, priority 2
- },
- "teststeps": [
- # test data structure
- {
- 'name': 'test step desc1',
- 'variables': [], # optional, priority 3
- 'extract': [],
- 'validate': [],
- 'api_def': {
- "variables": {} # optional, priority 4
- 'request': {},
- }
- },
- test_dict_2 # another test dict
- ]
- },
- testcase_dict_2 # another testcase dict
- ],
- "api": {
- "variables": {},
- "request": {}
- }
- }
-
- """
- project_meta = tests_mapping.get("project_meta", {})
- testcases = []
-
- for test_type in tests_mapping:
-
- if test_type == "testsuites":
- # load testcases of testsuite
- testsuites = tests_mapping["testsuites"]
- for testsuite in testsuites:
- parsed_testcases = _parse_testsuite(testsuite, project_meta)
- for parsed_testcase in parsed_testcases:
- testcases.append(parsed_testcase)
-
- elif test_type == "testcases":
- for testcase in tests_mapping["testcases"]:
- testcase["type"] = "testcase"
- parsed_testcase = _parse_testcase(testcase, project_meta)
- if not parsed_testcase:
- continue
- testcases.append(parsed_testcase)
-
- elif test_type == "apis":
- # encapsulate api as a testcase
- for api_content in tests_mapping["apis"]:
- testcase = {
- "config": {
- "name": api_content.get("name")
- },
- "teststeps": [api_content],
- "path": api_content.pop("path", None),
- "type": api_content.pop("type", "api")
- }
- parsed_testcase = _parse_testcase(testcase, project_meta)
- if not parsed_testcase:
- continue
- testcases.append(parsed_testcase)
-
- return testcases
+ return parsed_variables
diff --git a/httprunner/parser_test.py b/httprunner/parser_test.py
index d8bef5a6..426c76a6 100644
--- a/httprunner/parser_test.py
+++ b/httprunner/parser_test.py
@@ -1,14 +1,35 @@
-import os
import time
import unittest
-from httprunner import exceptions, loader, parser
-from httprunner.loader import load
-from tests.debugtalk import gen_random_string, sum_two
+from httprunner import parser
+from httprunner.exceptions import VariableNotFound, FunctionNotFound
class TestParserBasic(unittest.TestCase):
+ def test_parse_variables_mapping(self):
+ variables = {
+ "varA": "$varB",
+ "varB": "$varC",
+ "varC": "123",
+ "a": 1,
+ "b": 2
+ }
+ parsed_variables = parser.parse_variables_mapping(variables)
+ print(parsed_variables)
+ self.assertEqual(parsed_variables["varA"], "123")
+ self.assertEqual(parsed_variables["varB"], "123")
+
+ def test_parse_variables_mapping_exception(self):
+ variables = {
+ "varA": "$varB",
+ "varB": "$varC",
+ "a": 1,
+ "b": 2
+ }
+ with self.assertRaises(VariableNotFound):
+ parser.parse_variables_mapping(variables)
+
def test_parse_string_value(self):
self.assertEqual(parser.parse_string_value("123"), 123)
self.assertEqual(parser.parse_string_value("12.3"), 12.3)
@@ -16,54 +37,50 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual(parser.parse_string_value("$var"), "$var")
self.assertEqual(parser.parse_string_value("${func}"), "${func}")
- def test_regex_findall_variables(self):
+ def test_extract_variables(self):
self.assertEqual(
- parser.regex_findall_variables("$var"),
- ["var"]
+ parser.extract_variables("$var"),
+ {"var"}
)
self.assertEqual(
- parser.regex_findall_variables("$var123"),
- ["var123"]
+ parser.extract_variables("$var123"),
+ {"var123"}
)
self.assertEqual(
- parser.regex_findall_variables("$var_name"),
- ["var_name"]
+ parser.extract_variables("$var_name"),
+ {"var_name"}
)
self.assertEqual(
- parser.regex_findall_variables("var"),
- []
+ parser.extract_variables("var"),
+ set()
)
self.assertEqual(
- parser.regex_findall_variables("a$var"),
- ["var"]
+ parser.extract_variables("a$var"),
+ {"var"}
)
self.assertEqual(
- parser.regex_findall_variables("a$var${var2}$var3${var4}"),
- ["var", "var2", "var3", "var4"]
+ parser.extract_variables("$v ar"),
+ {"v"}
)
self.assertEqual(
- parser.regex_findall_variables("$v ar"),
- ["v"]
+ parser.extract_variables(" "),
+ set()
)
self.assertEqual(
- parser.regex_findall_variables(" "),
- []
+ parser.extract_variables("$abc*"),
+ {"abc"}
)
self.assertEqual(
- parser.regex_findall_variables("$abc*"),
- ["abc"]
+ parser.extract_variables("${func()}"),
+ set()
)
self.assertEqual(
- parser.regex_findall_variables("${func()}"),
- []
+ parser.extract_variables("${func(1,2)}"),
+ set()
)
self.assertEqual(
- parser.regex_findall_variables("${func(1,2)}"),
- []
- )
- self.assertEqual(
- parser.regex_findall_variables("${gen_md5($TOKEN, $data, $random)}"),
- ["TOKEN", "data", "random"]
+ parser.extract_variables("${gen_md5($TOKEN, $data, $random)}"),
+ {"TOKEN", "data", "random"}
)
def test_parse_function_params(self):
@@ -108,74 +125,30 @@ class TestParserBasic(unittest.TestCase):
{'args': ["$request", '12 3'], 'kwargs': {}}
)
- def test_extract_variables(self):
- prepared_content = parser.prepare_lazy_data("123$a", {}, {"a"})
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"a"}
- )
- prepared_content = parser.prepare_lazy_data("$a$b", {}, {"a", "b"})
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"a", "b"}
- )
- prepared_content = parser.prepare_lazy_data(["$a$b", "$c", "d"], {}, {"a", "b", "c", "d"})
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"a", "b", "c"}
- )
- prepared_content = parser.prepare_lazy_data(
- {"a": 1, "b": {"c": "$d", "e": 3}},
- {},
- {"d"}
- )
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"d"}
- )
- prepared_content = parser.prepare_lazy_data(
- {"a": ["$b"], "b": {"c": "$d", "e": 3}},
- {},
- {"b", "d"}
- )
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"b", "d"}
- )
- prepared_content = parser.prepare_lazy_data(
- ["$a$b", "$c", {"c": "$d"}],
- {},
- {"a", "b", "c", "d"}
- )
- self.assertEqual(
- parser.extract_variables(prepared_content),
- {"a", "b", "c", "d"}
- )
-
def test_extract_functions(self):
self.assertEqual(
parser.regex_findall_functions("${func()}"),
- [('func', '')]
+ [("func", "")]
)
self.assertEqual(
parser.regex_findall_functions("${func(5)}"),
- [('func', '5')]
+ [("func", "5")]
)
self.assertEqual(
parser.regex_findall_functions("${func(a=1, b=2)}"),
- [('func', 'a=1, b=2')]
+ [("func", "a=1, b=2")]
)
self.assertEqual(
parser.regex_findall_functions("${func(1, $b, c=$x, d=4)}"),
- [('func', '1, $b, c=$x, d=4')]
+ [("func", "1, $b, c=$x, d=4")]
)
self.assertEqual(
parser.regex_findall_functions("/api/1000?_t=${get_timestamp()}"),
- [('get_timestamp', '')]
+ [("get_timestamp", "")]
)
self.assertEqual(
parser.regex_findall_functions("/api/${add(1, 2)}"),
- [('add', '1, 2')]
+ [("add", "1, 2")]
)
self.assertEqual(
parser.regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}"),
@@ -186,7 +159,298 @@ class TestParserBasic(unittest.TestCase):
[('func', '1, 2, a=3, b=4')]
)
- def test_parse_data(self):
+ def test_parse_data_string_with_variables(self):
+ variables_mapping = {
+ "var_1": "abc",
+ "var_2": "def",
+ "var_3": 123,
+ "var_4": {"a": 1},
+ "var_5": True,
+ "var_6": None
+ }
+ self.assertEqual(
+ parser.parse_data("$var_1", variables_mapping),
+ "abc"
+ )
+ self.assertEqual(
+ parser.parse_data("${var_1}", variables_mapping),
+ "abc"
+ )
+ self.assertEqual(
+ parser.parse_data("var_1", variables_mapping),
+ "var_1"
+ )
+ self.assertEqual(
+ parser.parse_data("$var_1#XYZ", variables_mapping),
+ "abc#XYZ"
+ )
+ self.assertEqual(
+ parser.parse_data("${var_1}#XYZ", variables_mapping),
+ "abc#XYZ"
+ )
+ self.assertEqual(
+ parser.parse_data("/$var_1/$var_2/var3", variables_mapping),
+ "/abc/def/var3"
+ )
+ self.assertEqual(
+ parser.parse_data("$var_3", variables_mapping),
+ 123
+ )
+ self.assertEqual(
+ parser.parse_data("$var_4", variables_mapping),
+ {"a": 1}
+ )
+ self.assertEqual(
+ parser.parse_data("$var_5", variables_mapping),
+ True
+ )
+ self.assertEqual(
+ parser.parse_data("abc$var_5", variables_mapping),
+ "abcTrue"
+ )
+ self.assertEqual(
+ parser.parse_data("abc$var_4", variables_mapping),
+ "abc{'a': 1}"
+ )
+ self.assertEqual(
+ parser.parse_data("$var_6", variables_mapping),
+ None
+ )
+
+ with self.assertRaises(VariableNotFound):
+ parser.parse_data("/api/$SECRET_KEY", variables_mapping)
+
+ self.assertEqual(
+ parser.parse_data(["$var_1", "$var_2"], variables_mapping),
+ ["abc", "def"]
+ )
+ self.assertEqual(
+ parser.parse_data({"$var_1": "$var_2"}, variables_mapping),
+ {"abc": "def"}
+ )
+
+ # format: $var
+ value = parser.parse_data("ABC$var_1", variables_mapping)
+ self.assertEqual(value, "ABCabc")
+
+ value = parser.parse_data("ABC$var_1$var_3", variables_mapping)
+ self.assertEqual(value, "ABCabc123")
+
+ value = parser.parse_data("ABC$var_1/$var_3", variables_mapping)
+ self.assertEqual(value, "ABCabc/123")
+
+ value = parser.parse_data("ABC$var_1/", variables_mapping)
+ self.assertEqual(value, "ABCabc/")
+
+ value = parser.parse_data("ABC$var_1$", variables_mapping)
+ self.assertEqual(value, "ABCabc$")
+
+ value = parser.parse_data("ABC$var_1/123$var_1/456", variables_mapping)
+ self.assertEqual(value, "ABCabc/123abc/456")
+
+ value = parser.parse_data("ABC$var_1/$var_2/$var_1", variables_mapping)
+ self.assertEqual(value, "ABCabc/def/abc")
+
+ value = parser.parse_data("func1($var_1, $var_3)", variables_mapping)
+ self.assertEqual(value, "func1(abc, 123)")
+
+ # format: ${var}
+ value = parser.parse_data("ABC${var_1}", variables_mapping)
+ self.assertEqual(value, "ABCabc")
+
+ value = parser.parse_data("ABC${var_1}${var_3}", variables_mapping)
+ self.assertEqual(value, "ABCabc123")
+
+ value = parser.parse_data("ABC${var_1}/${var_3}", variables_mapping)
+ self.assertEqual(value, "ABCabc/123")
+
+ value = parser.parse_data("ABC${var_1}/", variables_mapping)
+ self.assertEqual(value, "ABCabc/")
+
+ value = parser.parse_data("ABC${var_1}123", variables_mapping)
+ self.assertEqual(value, "ABCabc123")
+
+ value = parser.parse_data("ABC${var_1}/123${var_1}/456", variables_mapping)
+ self.assertEqual(value, "ABCabc/123abc/456")
+
+ value = parser.parse_data("ABC${var_1}/${var_2}/${var_1}", variables_mapping)
+ self.assertEqual(value, "ABCabc/def/abc")
+
+ value = parser.parse_data("func1(${var_1}, ${var_3})", variables_mapping)
+ self.assertEqual(value, "func1(abc, 123)")
+
+ def test_parse_data_multiple_identical_variables(self):
+ variables_mapping = {
+ "var_1": "abc",
+ "var_2": "def",
+ }
+ self.assertEqual(
+ parser.parse_data("/$var_1/$var_2/$var_1", variables_mapping),
+ "/abc/def/abc"
+ )
+
+ variables_mapping = {
+ "userid": 100,
+ "data": 1498
+ }
+ content = "/users/$userid/training/$data?userId=$userid&data=$data"
+ self.assertEqual(
+ parser.parse_data(content, variables_mapping),
+ "/users/100/training/1498?userId=100&data=1498"
+ )
+
+ variables_mapping = {
+ "user": 100,
+ "userid": 1000,
+ "data": 1498
+ }
+ content = "/users/$user/$userid/$data?userId=$userid&data=$data"
+ self.assertEqual(
+ parser.parse_data(content, variables_mapping),
+ "/users/100/1000/1498?userId=1000&data=1498"
+ )
+
+ def test_parse_data_string_with_functions(self):
+ import random, string
+ functions_mapping = {
+ "gen_random_string": lambda str_len: ''.join(random.choice(string.ascii_letters + string.digits) \
+ for _ in range(str_len))
+ }
+ result = parser.parse_data("${gen_random_string(5)}", functions_mapping=functions_mapping)
+ self.assertEqual(len(result), 5)
+
+ add_two_nums = lambda a, b=1: a + b
+ functions_mapping["add_two_nums"] = add_two_nums
+ self.assertEqual(
+ parser.parse_data("${add_two_nums(1)}", functions_mapping=functions_mapping),
+ 2
+ )
+ self.assertEqual(
+ parser.parse_data("${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
+ 3
+ )
+ self.assertEqual(
+ parser.parse_data("/api/${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
+ "/api/3"
+ )
+
+ with self.assertRaises(FunctionNotFound):
+ parser.parse_data("/api/${gen_md5(abc)}")
+
+ variables_mapping = {
+ "var_1": "abc",
+ "var_2": "def",
+ "var_3": 123,
+ "var_4": {"a": 1},
+ "var_5": True,
+ "var_6": None
+ }
+ functions_mapping = {
+ "func1": lambda x, y: str(x) + str(y)
+ }
+
+ value = parser.parse_data("${func1($var_1, $var_3)}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "abc123")
+
+ value = parser.parse_data("ABC${func1($var_1, $var_3)}DE", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123DE")
+
+ value = parser.parse_data("ABC${func1($var_1, $var_3)}$var_5", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123True")
+
+ value = parser.parse_data("ABC${func1($var_1, $var_3)}DE$var_4", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123DE{'a': 1}")
+
+ value = parser.parse_data("ABC$var_5${func1($var_1, $var_3)}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCTrueabc123")
+
+ value = parser.parse_data("ABC${ord(a)}DEF${len(abcd)}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABC97DEF4")
+
+ def test_parse_data_func_var_duplicate(self):
+ variables_mapping = {
+ "var_1": "abc",
+ "var_2": "def",
+ "var_3": 123,
+ "var_4": {"a": 1},
+ "var_5": True,
+ "var_6": None
+ }
+ functions_mapping = {
+ "func1": lambda x, y: str(x) + str(y)
+ }
+ value = parser.parse_data(
+ "ABC${func1($var_1, $var_3)}--${func1($var_1, $var_3)}",
+ variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123--abc123")
+
+ value = parser.parse_data("ABC${func1($var_1, $var_3)}$var_1", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123abc")
+
+ value = parser.parse_data(
+ "ABC${func1($var_1, $var_3)}$var_1--${func1($var_1, $var_3)}$var_1",
+ variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc123abc--abc123abc")
+
+ def test_parse_data_func_abnormal(self):
+ variables_mapping = {
+ "var_1": "abc",
+ "var_2": "def",
+ "var_3": 123,
+ "var_4": {"a": 1},
+ "var_5": True,
+ "var_6": None
+ }
+ functions_mapping = {
+ "func1": lambda x, y: str(x) + str(y)
+ }
+
+ # {
+ value = parser.parse_data("ABC$var_1{", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc{")
+
+ value = parser.parse_data("{ABC$var_1{}a}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "{ABCabc{}a}")
+
+ value = parser.parse_data("AB{C$var_1{}a}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "AB{Cabc{}a}")
+
+ # }
+ value = parser.parse_data("ABC$var_1}", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc}")
+
+ # $$
+ value = parser.parse_data("ABC$$var_1{", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABC$var_1{")
+
+ # $$$
+ value = parser.parse_data("ABC$$$var_1{", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABC$abc{")
+
+ # $$$$
+ value = parser.parse_data("ABC$$$$var_1{", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABC$$var_1{")
+
+ # ${
+ value = parser.parse_data("ABC$var_1${", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc${")
+
+ value = parser.parse_data("ABC$var_1${a", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc${a")
+
+ # $}
+ value = parser.parse_data("ABC$var_1$}a", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc$}a")
+
+ # }{
+ value = parser.parse_data("ABC$var_1}{a", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc}{a")
+
+ # {}
+ value = parser.parse_data("ABC$var_1{}a", variables_mapping, functions_mapping)
+ self.assertEqual(value, "ABCabc{}a")
+
+ def test_parse_data_request(self):
content = {
'request': {
'url': '/api/users/$uid',
@@ -209,7 +473,7 @@ class TestParserBasic(unittest.TestCase):
functions_mapping = {
"add_one": lambda x: x + 1
}
- result = parser.eval_lazy_data(content, variables_mapping, functions_mapping)
+ result = parser.parse_data(content, variables_mapping, functions_mapping)
self.assertEqual("/api/users/1000", result["request"]["url"])
self.assertEqual("abc123", result["request"]["headers"]["token"])
self.assertEqual("POST", result["request"]["method"])
@@ -219,372 +483,6 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual("", result["request"]["data"]["empty_str"])
self.assertEqual("abc4def", result["request"]["data"]["value"])
- def test_eval_lazy_data(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- self.assertEqual(
- parser.eval_lazy_data("$var_1", variables_mapping=variables_mapping),
- "abc"
- )
- self.assertEqual(
- parser.eval_lazy_data("var_1", variables_mapping=variables_mapping),
- "var_1"
- )
- self.assertEqual(
- parser.eval_lazy_data("$var_1#XYZ", variables_mapping=variables_mapping),
- "abc#XYZ"
- )
- self.assertEqual(
- parser.eval_lazy_data("/$var_1/$var_2/var3", variables_mapping=variables_mapping),
- "/abc/def/var3"
- )
- self.assertEqual(
- parser.eval_lazy_data("/$var_1/$var_2/$var_1", variables_mapping=variables_mapping),
- "/abc/def/abc"
- )
- self.assertEqual(
- parser.eval_lazy_data("$var_3", variables_mapping=variables_mapping),
- 123
- )
- self.assertEqual(
- parser.eval_lazy_data("$var_4", variables_mapping=variables_mapping),
- {"a": 1}
- )
- self.assertEqual(
- parser.eval_lazy_data("$var_5", variables_mapping=variables_mapping),
- True
- )
- self.assertEqual(
- parser.eval_lazy_data("abc$var_5", variables_mapping=variables_mapping),
- "abcTrue"
- )
- self.assertEqual(
- parser.eval_lazy_data("abc$var_4", variables_mapping=variables_mapping),
- "abc{'a': 1}"
- )
- self.assertEqual(
- parser.eval_lazy_data("$var_6", variables_mapping=variables_mapping),
- None
- )
-
- with self.assertRaises(exceptions.VariableNotFound):
- parser.eval_lazy_data("/api/$SECRET_KEY", variables_mapping=variables_mapping)
-
- self.assertEqual(
- parser.eval_lazy_data(["$var_1", "$var_2"], variables_mapping=variables_mapping),
- ["abc", "def"]
- )
- self.assertEqual(
- parser.eval_lazy_data({"$var_1": "$var_2"}, variables_mapping=variables_mapping),
- {"abc": "def"}
- )
-
- def test_parse_func_var_abnormal(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- check_variables_set = variables_mapping.keys()
- functions_mapping = {
- "func1": lambda x,y: str(x) + str(y)
- }
-
- # {
- var = parser.LazyString("ABC$var_1{", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{{")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc{")
-
- var = parser.LazyString("{ABC$var_1{}a}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "{{ABC{}{{}}a}}")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "{ABCabc{}a}")
-
- var = parser.LazyString("AB{C$var_1{}a}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "AB{{C{}{{}}a}}")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "AB{Cabc{}a}")
-
- # }
- var = parser.LazyString("ABC$var_1}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}}}")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc}")
-
- # $$
- var = parser.LazyString("ABC$$var_1{", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC$var_1{{")
- self.assertEqual(var._args, [])
- self.assertEqual(var.to_value(variables_mapping), "ABC$var_1{")
-
- # $$$
- var = parser.LazyString("ABC$$$var_1{", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC${}{{")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABC$abc{")
-
- # $$$$
- var = parser.LazyString("ABC$$$$var_1{", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC$$var_1{{")
- self.assertEqual(var._args, [])
- self.assertEqual(var.to_value(variables_mapping), "ABC$$var_1{")
-
- # ${
- var = parser.LazyString("ABC$var_1${", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}${{")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc${")
-
- var = parser.LazyString("ABC$var_1${a", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}${{a")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc${a")
-
- # $}
- var = parser.LazyString("ABC$var_1$}a", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}$}}a")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc$}a")
-
- # }{
- var = parser.LazyString("ABC$var_1}{a", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}}}{{a")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc}{a")
-
- # {}
- var = parser.LazyString("ABC$var_1{}a", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{{}}a")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc{}a")
-
- def test_parse_func_var_duplicate(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- check_variables_set = variables_mapping.keys()
- functions_mapping = {
- "func1": lambda x,y: str(x) + str(y)
- }
- var = parser.LazyString(
- "ABC${func1($var_1, $var_3)}--${func1($var_1, $var_3)}",
- functions_mapping,
- check_variables_set
- )
- self.assertEqual(var._string, "ABC{}--{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123--abc123")
-
- var = parser.LazyString("ABC${func1($var_1, $var_3)}$var_1", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123abc")
-
- var = parser.LazyString(
- "ABC${func1($var_1, $var_3)}$var_1--${func1($var_1, $var_3)}$var_1",
- functions_mapping,
- check_variables_set
- )
- self.assertEqual(var._string, "ABC{}{}--{}{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123abc--abc123abc")
-
- def test_parse_function(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- check_variables_set = variables_mapping.keys()
- functions_mapping = {
- "func1": lambda x,y: str(x) + str(y)
- }
-
- var = parser.LazyString("${func1($var_1, $var_3)}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "{}")
- self.assertIsInstance(var._args[0], parser.LazyFunction)
- self.assertEqual(var.to_value(variables_mapping), "abc123")
-
- var = parser.LazyString("ABC${func1($var_1, $var_3)}DE", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}DE")
- self.assertIsInstance(var._args[0], parser.LazyFunction)
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123DE")
-
- var = parser.LazyString("ABC${func1($var_1, $var_3)}$var_5", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123True")
-
- var = parser.LazyString("ABC${func1($var_1, $var_3)}DE$var_4", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}DE{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123DE{'a': 1}")
-
- var = parser.LazyString("ABC$var_5${func1($var_1, $var_3)}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{}")
- self.assertEqual(var.to_value(variables_mapping), "ABCTrueabc123")
-
- # Python builtin functions
- var = parser.LazyString("ABC${ord(a)}DEF${len(abcd)}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}DEF{}")
- self.assertEqual(var.to_value(variables_mapping), "ABC97DEF4")
-
- def test_parse_variable(self):
- """ variable format ${var} and $var
- """
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- check_variables_set = variables_mapping.keys()
- functions_mapping = {}
-
- # format: $var
- var = parser.LazyString("ABC$var_1", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc")
-
- var = parser.LazyString("ABC$var_1$var_3", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{}")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123")
-
- var = parser.LazyString("ABC$var_1/$var_3", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/{}")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/123")
-
- var = parser.LazyString("ABC$var_1/", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/")
-
- var = parser.LazyString("ABC$var_1$", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}$")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc$")
-
- var = parser.LazyString("ABC$var_1/123$var_1/456", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/123{}/456")
- self.assertEqual(var._args, ["var_1", "var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/123abc/456")
-
- var = parser.LazyString("ABC$var_1/$var_2/$var_1", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/{}/{}")
- self.assertEqual(var._args, ["var_1", "var_2", "var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/def/abc")
-
- var = parser.LazyString("func1($var_1, $var_3)", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "func1({}, {})")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "func1(abc, 123)")
-
- # format: ${var}
- var = parser.LazyString("ABC${var_1}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc")
-
- var = parser.LazyString("ABC${var_1}${var_3}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}{}")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123")
-
- var = parser.LazyString("ABC${var_1}/${var_3}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/{}")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/123")
-
- var = parser.LazyString("ABC${var_1}/", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/")
-
- var = parser.LazyString("ABC${var_1}123", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}123")
- self.assertEqual(var._args, ["var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc123")
-
- var = parser.LazyString("ABC${var_1}/123${var_1}/456", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/123{}/456")
- self.assertEqual(var._args, ["var_1", "var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/123abc/456")
-
- var = parser.LazyString("ABC${var_1}/${var_2}/${var_1}", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "ABC{}/{}/{}")
- self.assertEqual(var._args, ["var_1", "var_2", "var_1"])
- self.assertEqual(var.to_value(variables_mapping), "ABCabc/def/abc")
-
- var = parser.LazyString("func1(${var_1}, ${var_3})", functions_mapping, check_variables_set)
- self.assertEqual(var._string, "func1({}, {})")
- self.assertEqual(var._args, ["var_1", "var_3"])
- self.assertEqual(var.to_value(variables_mapping), "func1(abc, 123)")
-
- def test_parse_data_multiple_identical_variables(self):
- variables_mapping = {
- "userid": 100,
- "data": 1498
- }
- content = "/users/$userid/training/$data?userId=$userid&data=$data"
- self.assertEqual(
- parser.eval_lazy_data(content, variables_mapping=variables_mapping),
- "/users/100/training/1498?userId=100&data=1498"
- )
-
- variables_mapping = {
- "user": 100,
- "userid": 1000,
- "data": 1498
- }
- content = "/users/$user/$userid/$data?userId=$userid&data=$data"
- self.assertEqual(
- parser.eval_lazy_data(content, variables_mapping=variables_mapping),
- "/users/100/1000/1498?userId=1000&data=1498"
- )
-
- def test_parse_data_functions(self):
- functions_mapping = {
- "gen_random_string": gen_random_string
- }
- result = parser.eval_lazy_data("${gen_random_string(5)}", functions_mapping=functions_mapping)
- self.assertEqual(len(result), 5)
-
- add_two_nums = lambda a, b=1: a + b
- functions_mapping["add_two_nums"] = add_two_nums
- self.assertEqual(
- parser.eval_lazy_data("${add_two_nums(1)}", functions_mapping=functions_mapping),
- 2
- )
- self.assertEqual(
- parser.eval_lazy_data("${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
- 3
- )
- self.assertEqual(
- parser.eval_lazy_data("/api/${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
- "/api/3"
- )
-
- with self.assertRaises(exceptions.FunctionNotFound):
- parser.eval_lazy_data("/api/${gen_md5(abc)}", functions_mapping=functions_mapping)
-
def test_parse_data_testcase(self):
variables = {
"uid": "1000",
@@ -607,11 +505,7 @@ class TestParserBasic(unittest.TestCase):
},
"body": "$data"
}
- parsed_testcase = parser.eval_lazy_data(
- testcase_template,
- variables_mapping=variables,
- functions_mapping=functions
- )
+ parsed_testcase = parser.parse_data(testcase_template, variables, functions)
self.assertEqual(
parsed_testcase["url"],
"http://127.0.0.1:5000/api/users/1000/3"
@@ -632,775 +526,3 @@ class TestParserBasic(unittest.TestCase):
parsed_testcase["headers"]["sum"],
3
)
-
- def test_parse_variables_mapping(self):
- variables = {
- "varA": "123$varB",
- "varB": "456$varC",
- "varC": "${sum_two($a, $b)}",
- "a": 1,
- "b": 2
- }
- functions = {
- "sum_two": sum_two
- }
- prepared_variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- parsed_variables = parser.parse_variables_mapping(prepared_variables)
- self.assertEqual(parsed_variables["varA"], "1234563")
- self.assertEqual(parsed_variables["varB"], "4563")
- self.assertEqual(parsed_variables["varC"], 3)
-
- def test_parse_variables_mapping_fix_duplicate_function_call(self):
- # fix duplicate function calling
- variables = {
- "varA": "$varB",
- "varB": "${gen_random_string(5)}"
- }
- functions = {
- "gen_random_string": gen_random_string
- }
- prepared_variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- parsed_variables = parser.parse_variables_mapping(prepared_variables)
- self.assertEqual(parsed_variables["varA"], parsed_variables["varB"])
-
- def test_parse_variables_mapping_dead_circle(self):
- variables = {
- "varA": "$varB",
- "varB": "123$varC"
- }
- check_variables_set = {"varA", "varB", "varC"}
- prepared_variables = parser.prepare_lazy_data(variables, {}, check_variables_set)
- with self.assertRaises(exceptions.VariableNotFound):
- parser.parse_variables_mapping(prepared_variables)
-
- def test_parse_variables_mapping_not_found(self):
- variables = {
- "varA": "123$varB",
- "varB": "456$varC",
- "varC": "${sum_two($a, $b)}",
- "b": 2
- }
- functions = {
- "sum_two": sum_two
- }
- with self.assertRaises(exceptions.VariableNotFound):
- parser.prepare_lazy_data(variables, functions, variables.keys())
-
- def test_parse_variables_mapping_ref_self(self):
- variables = {
- "varC": "${sum_two($a, $b)}",
- "a": 1,
- "b": 2,
- "token": "$token"
- }
- functions = {
- "sum_two": sum_two
- }
- prepared_variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- with self.assertRaises(exceptions.VariableNotFound):
- parser.parse_variables_mapping(prepared_variables)
-
- def test_parse_variables_mapping_2(self):
- variables = {
- "host2": "https://httprunner.org",
- "num3": "${sum_two($num2, 4)}",
- "num2": "${sum_two($num1, 3)}",
- "num1": "${sum_two(1, 2)}"
- }
- functions = {
- "sum_two": sum_two
- }
- prepared_variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- parsed_testcase = parser.parse_variables_mapping(prepared_variables)
- self.assertEqual(parsed_testcase["num3"], 10)
- self.assertEqual(parsed_testcase["num2"], 6)
- self.assertEqual(parsed_testcase["num1"], 3)
-
- def test_is_var_or_func_exist(self):
- self.assertTrue(parser.is_var_or_func_exist("$var"))
- self.assertTrue(parser.is_var_or_func_exist("${var}"))
- self.assertTrue(parser.is_var_or_func_exist("$var${var}"))
- self.assertFalse(parser.is_var_or_func_exist("${var"))
- self.assertFalse(parser.is_var_or_func_exist("$$var"))
- self.assertFalse(parser.is_var_or_func_exist("var$$0"))
- self.assertTrue(parser.is_var_or_func_exist("var$$$0"))
- self.assertFalse(parser.is_var_or_func_exist("var$$$$0"))
- self.assertTrue(parser.is_var_or_func_exist("${func()}"))
- self.assertTrue(parser.is_var_or_func_exist("${func($a)}"))
- self.assertTrue(parser.is_var_or_func_exist("${func($a)}$b"))
-
- def test_parse_variables_mapping_dollar_notation(self):
- variables = {
- "varA": "123$varB",
- "varB": "456$$0",
- "varC": "${sum_two($a, $b)}",
- "a": 1,
- "b": 2,
- "c": "abc"
- }
- functions = {
- "sum_two": sum_two
- }
- prepared_variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- parsed_testcase = parser.parse_variables_mapping(prepared_variables)
- self.assertEqual(parsed_testcase["varA"], "123456$0")
- self.assertEqual(parsed_testcase["varB"], "456$0")
- self.assertEqual(parsed_testcase["varC"], 3)
-
- def test_prepare_lazy_data(self):
- variables = {
- "host": "https://httprunner.org",
- "num4": "${sum_two($num0, 5)}",
- "num3": "${sum_two($num2, 4)}",
- "num2": "${sum_two($num1, 3)}",
- "num1": "${sum_two(1, 2)}",
- "num0": 0
- }
- functions = {
- "sum_two": sum_two
- }
- parser.prepare_lazy_data(
- variables,
- functions,
- variables.keys()
- )
-
- def test_prepare_lazy_data_not_found(self):
- variables = {
- "host": "https://httprunner.org",
- "num4": "${sum_two($num0, 5)}",
- "num3": "${sum_two($num2, 4)}",
- "num2": "${sum_two($num1, 3)}",
- "num1": "${sum_two(1, 2)}"
- }
- functions = {
- "sum_two": sum_two
- }
- with self.assertRaises(exceptions.VariableNotFound):
- parser.prepare_lazy_data(
- variables,
- functions,
- variables.keys()
- )
-
- def test_prepare_lazy_data_dual_dollar(self):
- variables = {
- "num0": 123,
- "var1": "abc$$num0",
- "var2": "abc$$$num0",
- "var3": "abc$$$$num0",
- }
- functions = {
- "sum_two": sum_two
- }
- prepared_variables = parser.prepare_lazy_data(
- variables,
- functions,
- variables.keys()
- )
- self.assertEqual(prepared_variables["var1"], "abc$num0")
- self.assertIsInstance(prepared_variables["var2"], parser.LazyString)
- self.assertEqual(prepared_variables["var3"], "abc$$num0")
-
- parsed_variables = parser.parse_variables_mapping(prepared_variables)
- self.assertEqual(parsed_variables["var1"], "abc$num0")
- self.assertEqual(parsed_variables["var2"], "abc$123")
- self.assertEqual(parsed_variables["var3"], "abc$$num0")
-
- def test_get_uniform_comparator(self):
- self.assertEqual(parser.get_uniform_comparator("eq"), "equals")
- self.assertEqual(parser.get_uniform_comparator("=="), "equals")
- self.assertEqual(parser.get_uniform_comparator("lt"), "less_than")
- self.assertEqual(parser.get_uniform_comparator("le"), "less_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("gt"), "greater_than")
- self.assertEqual(parser.get_uniform_comparator("ge"), "greater_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("ne"), "not_equals")
-
- self.assertEqual(parser.get_uniform_comparator("str_eq"), "string_equals")
- self.assertEqual(parser.get_uniform_comparator("len_eq"), "length_equals")
- self.assertEqual(parser.get_uniform_comparator("count_eq"), "length_equals")
-
- self.assertEqual(parser.get_uniform_comparator("len_gt"), "length_greater_than")
- self.assertEqual(parser.get_uniform_comparator("count_gt"), "length_greater_than")
- self.assertEqual(parser.get_uniform_comparator("count_greater_than"), "length_greater_than")
-
- self.assertEqual(parser.get_uniform_comparator("len_ge"), "length_greater_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("count_ge"), "length_greater_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("count_greater_than_or_equals"), "length_greater_than_or_equals")
-
- self.assertEqual(parser.get_uniform_comparator("len_lt"), "length_less_than")
- self.assertEqual(parser.get_uniform_comparator("count_lt"), "length_less_than")
- self.assertEqual(parser.get_uniform_comparator("count_less_than"), "length_less_than")
-
- self.assertEqual(parser.get_uniform_comparator("len_le"), "length_less_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("count_le"), "length_less_than_or_equals")
- self.assertEqual(parser.get_uniform_comparator("count_less_than_or_equals"), "length_less_than_or_equals")
-
- def test_parse_validator(self):
- _validator = {"check": "status_code", "comparator": "eq", "expect": 201}
- self.assertEqual(
- parser.uniform_validator(_validator),
- {"check": "status_code", "comparator": "equals", "expect": 201}
- )
-
- _validator = {'eq': ['status_code', 201]}
- self.assertEqual(
- parser.uniform_validator(_validator),
- {"check": "status_code", "comparator": "equals", "expect": 201}
- )
-
- def test_extend_validators(self):
- def_validators = [
- {'eq': ['v1', 200]},
- {"check": "s2", "expect": 16, "comparator": "len_eq"}
- ]
- current_validators = [
- {"check": "v1", "expect": 201},
- {'len_eq': ['s3', 12]}
- ]
- def_validators = [
- parser.uniform_validator(_validator)
- for _validator in def_validators
- ]
- ref_validators = [
- parser.uniform_validator(_validator)
- for _validator in current_validators
- ]
-
- extended_validators = parser.extend_validators(def_validators, ref_validators)
- self.assertIn(
- {"check": "v1", "expect": 201, "comparator": "equals"},
- extended_validators
- )
- self.assertIn(
- {"check": "s2", "expect": 16, "comparator": "length_equals"},
- extended_validators
- )
- self.assertIn(
- {"check": "s3", "expect": 12, "comparator": "length_equals"},
- extended_validators
- )
-
- def test_extend_validators_with_dict(self):
- def_validators = [
- {'eq': ["a", {"v": 1}]},
- {'eq': [{"b": 1}, 200]}
- ]
- current_validators = [
- {'len_eq': ['s3', 12]},
- {'eq': [{"b": 1}, 201]}
- ]
- def_validators = [
- parser.uniform_validator(_validator)
- for _validator in def_validators
- ]
- ref_validators = [
- parser.uniform_validator(_validator)
- for _validator in current_validators
- ]
-
- extended_validators = parser.extend_validators(def_validators, ref_validators)
- self.assertEqual(len(extended_validators), 3)
- self.assertIn({'check': {'b': 1}, 'expect': 201, 'comparator': 'equals'}, extended_validators)
- self.assertNotIn({'check': {'b': 1}, 'expect': 200, 'comparator': 'equals'}, extended_validators)
-
-
-class TestParser(unittest.TestCase):
-
- def test_parse_parameters_raw_list(self):
- parameters = [
- {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]},
- {"username-password": [("user1", "111111"), ["test2", "222222"]]}
- ]
- cartesian_product_parameters = parser.parse_parameters(parameters)
- self.assertEqual(
- len(cartesian_product_parameters),
- 3 * 2
- )
- self.assertEqual(
- cartesian_product_parameters[0],
- {'user_agent': 'iOS/10.1', 'username': 'user1', 'password': '111111'}
- )
-
- def test_parse_parameters_custom_function(self):
- parameters = [
- {"user_agent": "${get_user_agent()}"},
- {"app_version": "${gen_app_version()}"},
- {"username-password": "${get_account()}"},
- {"username2-password2": "${get_account_in_tuple()}"}
- ]
- dot_env_path = os.path.join(
- os.getcwd(), "tests", ".env"
- )
- load.load_dot_env_file(dot_env_path)
- from tests import debugtalk
- cartesian_product_parameters = parser.parse_parameters(
- parameters,
- functions_mapping=load.load_module_functions(debugtalk)
- )
- self.assertIn(
- {
- 'user_agent': 'iOS/10.1',
- 'app_version': '2.8.5',
- 'username': 'user1',
- 'password': '111111',
- 'username2': 'user1',
- 'password2': '111111'
- },
- cartesian_product_parameters
- )
- self.assertEqual(
- len(cartesian_product_parameters),
- 2 * 2 * 2 * 2
- )
-
- def test_parse_parameters_parameterize(self):
- loader.load_project_data(os.path.join(os.getcwd(), "tests"))
- parameters = [
- {"app_version": "${parameterize(data/app_version.csv)}"},
- {"username-password": "${parameterize(data/account.csv)}"}
- ]
- cartesian_product_parameters = parser.parse_parameters(parameters)
- self.assertEqual(
- len(cartesian_product_parameters),
- 2 * 3
- )
-
- def test_parse_parameters_mix(self):
- project_meta = loader.load_project_data(os.path.join(os.getcwd(), "tests"))
-
- parameters = [
- {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]},
- {"app_version": "${gen_app_version()}"},
- {"username-password": "${parameterize(data/account.csv)}"}
- ]
- cartesian_product_parameters = parser.parse_parameters(
- parameters, functions_mapping=project_meta["functions"])
- self.assertEqual(
- len(cartesian_product_parameters),
- 3 * 2 * 3
- )
-
- def test_parse_tests_testcase(self):
- testcase_file_path = os.path.join(
- os.getcwd(), 'tests/data/demo_testcase.yml')
- tests_mapping = loader.load_cases(testcase_file_path)
- testcases = tests_mapping["testcases"]
- self.assertEqual(
- testcases[0]["config"]["variables"]["var_c"],
- "${sum_two($var_a, $var_b)}"
- )
- self.assertEqual(
- testcases[0]["config"]["variables"]["PROJECT_KEY"],
- "${ENV(PROJECT_KEY)}"
- )
- parsed_testcases = parser.parse_tests(tests_mapping)
- self.assertIsInstance(parsed_testcases, list)
- test_dict1 = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict1["variables"]["var_c"].raw_string, "${sum_two($var_a, $var_b)}")
- self.assertEqual(test_dict1["variables"]["PROJECT_KEY"].raw_string, "${ENV(PROJECT_KEY)}")
- self.assertIsInstance(parsed_testcases[0]["config"]["name"], parser.LazyString)
-
- def test_parse_tests_override_variables(self):
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': '',
- 'variables': [
- {"password": "123456"},
- {"creator": "user_test_001"}
- ]
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "variables": [
- {"creator": "user_test_002"},
- {"username": "$creator"}
- ],
- 'request': {'url': '/api1', 'method': 'GET'}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict1_variables = parsed_testcases[0]["teststeps"][0]["variables"]
- self.assertEqual(test_dict1_variables["creator"], "user_test_001")
- self.assertEqual(test_dict1_variables["username"].raw_string, "$creator")
-
- def test_parse_tests_base_url_priority(self):
- """ base_url & verify: priority test_dict > config
- """
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host",
- 'variables': {
- "host": "https://github.com"
- },
- "verify": False
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "base_url": "https://httprunner.org",
- 'request': {'url': '/api1', 'method': 'GET', "verify": True}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["request"]["url"], "/api1")
- self.assertEqual(test_dict["request"]["verify"], True)
-
- def test_parse_tests_base_url_path_with_variable(self):
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host1",
- 'variables': {
- "host1": "https://github.com"
- }
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "variables": {
- "host2": "https://httprunner.org"
- },
- 'request': {'url': '$host2/api1', 'method': 'GET'}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["variables"]["host2"], "https://httprunner.org")
- parsed_test_dict = parser.parse_lazy_data(test_dict, test_dict["variables"])
- self.assertEqual(parsed_test_dict["request"]["url"], "https://httprunner.org/api1")
-
- def test_parse_tests_base_url_test_dict(self):
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host1",
- 'variables': {
- "host1": "https://github.com"
- }
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "base_url": "$host2",
- "variables": {
- "host2": "https://httprunner.org"
- },
- 'request': {'url': '/api1', 'method': 'GET'}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- parsed_test_dict = parser.parse_lazy_data(test_dict, test_dict["variables"])
- self.assertEqual(parsed_test_dict["base_url"], "https://httprunner.org")
-
- def test_parse_tests_variable_with_function(self):
- tests_mapping = {
- "project_meta": {
- "functions": {
- "sum_two": sum_two,
- "gen_random_string": gen_random_string
- }
- },
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host1",
- 'variables': {
- "host1": "https://github.com",
- "var_a": "${gen_random_string(5)}",
- "var_b": "$var_a"
- }
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "base_url": "$host2",
- "variables": {
- "host2": "https://httprunner.org",
- "num3": "${sum_two($num2, 4)}",
- "num2": "${sum_two($num1, 3)}",
- "num1": "${sum_two(1, 2)}",
- "str1": "${gen_random_string(5)}",
- "str2": "$str1"
- },
- 'request': {
- 'url': '/api1/?num1=$num1&num2=$num2&num3=$num3',
- 'method': 'GET'
- }
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- variables = parser.parse_variables_mapping(test_dict["variables"])
- self.assertEqual(variables["num3"], 10)
- self.assertEqual(variables["num2"], 6)
- parsed_test_dict = parser.parse_lazy_data(test_dict, variables)
- self.assertEqual(parsed_test_dict["base_url"], "https://httprunner.org")
- self.assertEqual(
- parsed_test_dict["request"]["url"],
- "/api1/?num1=3&num2=6&num3=10"
- )
- self.assertEqual(variables["str1"], variables["str2"])
-
- def test_parse_tests_variable_not_found(self):
- tests_mapping = {
- "project_meta": {
- "functions": {
- "sum_two": sum_two
- }
- },
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host1",
- 'variables': {
- "host1": "https://github.com"
- }
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "base_url": "$host2",
- "variables": {
- "host2": "https://httprunner.org",
- "num4": "${sum_two($num0, 5)}",
- "num3": "${sum_two($num2, 4)}",
- "num2": "${sum_two($num1, 3)}",
- "num1": "${sum_two(1, 2)}"
- },
- 'request': {
- 'url': '/api1/?num1=$num1&num2=$num2&num3=$num3&num4=$num4',
- 'method': 'GET'
- }
- }
- ]
- }
- ]
- }
- parser.parse_tests(tests_mapping)
- parse_failed_testfiles = parser.get_parse_failed_testfiles()
- self.assertIn("testcase", parse_failed_testfiles)
-
- def test_parse_tests_base_url_teststep_empty(self):
- """ base_url & verify: priority test_dict > config
- """
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': '',
- "base_url": "$host",
- 'variables': {
- "host": "https://github.com"
- },
- "verify": False
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- "base_url": "",
- 'request': {'url': '/api1', 'method': 'GET', "verify": True}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(str(test_dict["base_url"]), 'LazyString($host)')
- self.assertEqual(test_dict["request"]["verify"], True)
-
- def test_parse_tests_verify_config_set(self):
- """ verify priority: test_dict > config
- """
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': 'bugfix verify',
- "base_url": "https://httpbin.org/",
- "verify": False
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- 'request': {'url': '/headers', 'method': 'GET'}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["request"]["verify"], False)
-
- def test_parse_tests_verify_config_unset(self):
- """ verify priority: test_dict > config
- """
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': 'bugfix verify',
- "base_url": "https://httpbin.org/",
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- 'request': {'url': '/headers', 'method': 'GET'}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["request"]["verify"], True)
-
- def test_parse_tests_verify_step_set_false(self):
- """ verify priority: test_dict > config
- """
- tests_mapping = {
- 'testcases': [
- {
- "config": {
- 'name': 'bugfix verify',
- "base_url": "https://httpbin.org/",
- "verify": True
- },
- "teststeps": [
- {
- 'name': 'testcase1',
- 'request': {'url': '/headers', 'method': 'GET', "verify": False}
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["request"]["verify"], False)
-
- def test_parse_tests_verify_nested_testcase_unset(self):
- tests_mapping = {
- 'testcases': [
- {
- 'config': {
- 'name': 'inquiry price',
- 'verify': False
- },
- 'teststeps': [
- {
- 'name': 'login system',
- 'testcase': 'testcases/deps/login.yml',
- 'testcase_def': {
- 'config': {
- 'name': 'login system'
- },
- 'teststeps': [
- {
- 'name': '/',
- 'request': {
- 'method': 'GET',
- 'url': 'https://httpbin.org/'
- }
- }
- ]
- }
- }
- ]
- }
- ]
- }
- parsed_testcases = parser.parse_tests(tests_mapping)
- test_dict = parsed_testcases[0]["teststeps"][0]
- self.assertEqual(test_dict["teststeps"][0]["request"]["verify"], False)
-
- def test_parse_environ(self):
- os.environ["PROJECT_KEY"] = "ABCDEFGH"
- content = {
- "variables": [
- {"PROJECT_KEY": "${ENV(PROJECT_KEY)}"}
- ]
- }
- result = parser.eval_lazy_data(content)
-
- content = {
- "variables": [
- {"PROJECT_KEY": "${ENV(PROJECT_KEY, abc)}"}
- ]
- }
- with self.assertRaises(exceptions.ParamsError):
- parser.eval_lazy_data(content)
-
- content = {
- "variables": [
- {"PROJECT_KEY": "${ENV(abc=123)}"}
- ]
- }
- with self.assertRaises(exceptions.ParamsError):
- parser.eval_lazy_data(content)
-
- def test_extend_with_api(self):
- loader.load_project_data(os.path.join(os.getcwd(), "tests"))
- raw_testinfo = {
- "name": "get token",
- "base_url": "https://github.com",
- "api": "api/get_token.yml",
- }
- api_def_dict = loader.buildup.load_teststep(raw_testinfo)
- test_block = {
- "name": "override block",
- "times": 3,
- "variables": [
- {"var": 123}
- ],
- "base_url": "https://httprunner.org",
- 'request': {
- 'url': '/api/get-token',
- 'method': 'POST',
- 'headers': {'user_agent': '$user_agent', 'device_sn': '$device_sn', 'os_platform': '$os_platform', 'app_version': '$app_version'},
- 'json': {'sign': '${get_sign($device_sn, $os_platform, $app_version)}'}
- },
- 'validate': [
- {"check": "status_code", "comparator": "equals", "expect": 201},
- {"check": "content.token", "comparator": "length_equals", "expect": 32}
- ]
- }
-
- parser._extend_with_api(test_block, api_def_dict)
- self.assertEqual(test_block["base_url"], "https://github.com")
- self.assertEqual(test_block["name"], "override block")
- self.assertEqual({'var': 123}, test_block["variables"])
- self.assertIn({'check': 'status_code', 'expect': 201, 'comparator': 'equals'}, test_block["validate"])
- self.assertIn({'check': 'content.token', 'comparator': 'length_equals', 'expect': 32}, test_block["validate"])
- self.assertEqual(test_block["times"], 3)
diff --git a/httprunner/report/html/gen_report.py b/httprunner/report/html/gen_report.py
index f76c668c..03e2bbf4 100644
--- a/httprunner/report/html/gen_report.py
+++ b/httprunner/report/html/gen_report.py
@@ -5,7 +5,7 @@ from jinja2 import Template
from loguru import logger
from httprunner.exceptions import SummaryEmpty
-from httprunner.v3.schema import TestSuiteSummary
+from httprunner.schema import TestSuiteSummary
def gen_html_report(testsuite_summary: TestSuiteSummary, report_template=None, report_dir=None, report_file=None):
diff --git a/httprunner/report/stringify.py b/httprunner/report/stringify.py
index 8754f946..91072643 100644
--- a/httprunner/report/stringify.py
+++ b/httprunner/report/stringify.py
@@ -1,12 +1,11 @@
import json
from base64 import b64encode
from collections import Iterable
-from typing import List
from jinja2 import escape
from requests.cookies import RequestsCookieJar
-from httprunner.v3.schema import TestSuiteSummary, SessionData
+from httprunner.schema import TestSuiteSummary
def dumps_json(value):
diff --git a/httprunner/report/summarize.py b/httprunner/report/summarize.py
index 773a9c3d..3de3936e 100644
--- a/httprunner/report/summarize.py
+++ b/httprunner/report/summarize.py
@@ -3,7 +3,7 @@ from datetime import datetime
from httprunner import __version__
from httprunner.report.html.result import HtmlTestResult
-from httprunner.v3.schema import TestCaseSummary, TestCaseTime, TestCaseInOut
+from httprunner.schema import TestCaseSummary, TestCaseTime, TestCaseInOut
def get_platform():
diff --git a/httprunner/response.py b/httprunner/response.py
index d4d815fb..a77bc9dc 100644
--- a/httprunner/response.py
+++ b/httprunner/response.py
@@ -1,18 +1,105 @@
-import json
-import re
-from collections import OrderedDict
+from typing import Dict, Text, Any, NoReturn
-import jsonpath
+import jmespath
+import requests
from loguru import logger
-from httprunner import exceptions, utils
+from httprunner.exceptions import ValidationFailure, ParamsError
+from httprunner.parser import parse_data, parse_string_value, get_mapping_function
+from httprunner.schema import VariablesMapping, Validators, FunctionsMapping
-text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
+
+def get_uniform_comparator(comparator: Text):
+ """ convert comparator alias to uniform name
+ """
+ if comparator in ["eq", "equals", "==", "is"]:
+ return "equals"
+ elif comparator in ["lt", "less_than"]:
+ return "less_than"
+ elif comparator in ["le", "less_than_or_equals"]:
+ return "less_than_or_equals"
+ elif comparator in ["gt", "greater_than"]:
+ return "greater_than"
+ elif comparator in ["ge", "greater_than_or_equals"]:
+ return "greater_than_or_equals"
+ elif comparator in ["ne", "not_equals"]:
+ return "not_equals"
+ elif comparator in ["str_eq", "string_equals"]:
+ return "string_equals"
+ elif comparator in ["len_eq", "length_equals", "count_eq"]:
+ return "length_equals"
+ elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]:
+ return "length_greater_than"
+ elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals",
+ "count_greater_than_or_equals"]:
+ return "length_greater_than_or_equals"
+ elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
+ return "length_less_than"
+ elif comparator in ["len_le", "count_le", "length_less_than_or_equals",
+ "count_less_than_or_equals"]:
+ return "length_less_than_or_equals"
+ else:
+ return comparator
+
+
+def uniform_validator(validator):
+ """ unify validator
+
+ Args:
+ validator (dict): validator maybe in two formats:
+
+ format1: this is kept for compatiblity with the previous versions.
+ {"check": "status_code", "assert": "eq", "expect": 201}
+ {"check": "$resp_body_success", "assert": "eq", "expect": True}
+ format2: recommended new version, {assert: [check_item, expected_value]}
+ {'eq': ['status_code', 201]}
+ {'eq': ['$resp_body_success', True]}
+
+ Returns
+ dict: validator info
+
+ {
+ "check": "status_code",
+ "expect": 201,
+ "assert": "equals"
+ }
+
+ """
+ if not isinstance(validator, dict):
+ raise ParamsError(f"invalid validator: {validator}")
+
+ if "check" in validator and "expect" in validator:
+ # format1
+ check_item = validator["check"]
+ expect_value = validator["expect"]
+ comparator = validator.get("comparator", "eq")
+
+ elif len(validator) == 1:
+ # format2
+ comparator = list(validator.keys())[0]
+ compare_values = validator[comparator]
+
+ if not isinstance(compare_values, list) or len(compare_values) != 2:
+ raise ParamsError(f"invalid validator: {validator}")
+
+ check_item, expect_value = compare_values
+
+ else:
+ raise ParamsError(f"invalid validator: {validator}")
+
+ # uniform comparator, e.g. lt => less_than, eq => equals
+ assert_method = get_uniform_comparator(comparator)
+
+ return {
+ "check": check_item,
+ "expect": expect_value,
+ "assert": assert_method
+ }
class ResponseObject(object):
- def __init__(self, resp_obj):
+ def __init__(self, resp_obj: requests.Response):
""" initialize with a requests.Response object
Args:
@@ -20,6 +107,12 @@ class ResponseObject(object):
"""
self.resp_obj = resp_obj
+ self.resp_obj_meta = {
+ "status_code": resp_obj.status_code,
+ "headers": resp_obj.headers,
+ "body": resp_obj.json()
+ }
+ self.validation_results: Dict = {}
def __getattr__(self, key):
try:
@@ -35,269 +128,82 @@ class ResponseObject(object):
except AttributeError:
err_msg = f"ResponseObject does not have attribute: {key}"
logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
+ raise ParamsError(err_msg)
- def _extract_field_with_jsonpath(self, field: str) -> list:
- """ extract field from response content with jsonpath expression.
- JSONPath Docs: https://goessner.net/articles/JsonPath/
-
- Args:
- field: jsonpath expression, e.g. $.code, $..items.*.id
-
- Returns:
- A list that extracted from json response example. 1) [200] 2) [1, 2]
-
- Raises:
- exceptions.ExtractFailure: If no content matched with jsonpath expression.
-
- Examples:
- For example, response body like below:
- {
- "code": 200,
- "data": {
- "items": [{
- "id": 1,
- "name": "Bob"
- },
- {
- "id": 2,
- "name": "James"
- }
- ]
- },
- "message": "success"
- }
-
- >>> _extract_field_with_regex("$.code")
- [200]
- >>> _extract_field_with_regex("$..items.*.id")
- [1, 2]
-
- """
- try:
- json_body = self.json
- assert json_body
-
- result = jsonpath.jsonpath(json_body, field)
- assert result
- return result
- except (AssertionError, exceptions.JSONDecodeError):
- err_msg = f"Failed to extract data with jsonpath! => {field}\n"
- err_msg += f"response body: {self.text}\n"
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- def _extract_field_with_regex(self, field):
- """ extract field from response content with regex.
- requests.Response body could be json or html text.
-
- Args:
- field (str): regex string that matched r".*\(.*\).*"
-
- Returns:
- str: matched content.
-
- Raises:
- exceptions.ExtractFailure: If no content matched with regex.
-
- Examples:
- >>> # self.text: "LB123abcRB789"
- >>> filed = "LB[\d]*(.*)RB[\d]*"
- >>> _extract_field_with_regex(field)
- abc
-
- """
- matched = re.search(field, self.text)
- if not matched:
- err_msg = f"Failed to extract data with regex! => {field}\n"
- err_msg += f"response body: {self.text}\n"
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- return matched.group(1)
-
- def _extract_field_with_delimiter(self, field):
- """ response content could be json or html text.
-
- Args:
- field (str): string joined by delimiter.
- e.g.
- "status_code"
- "headers"
- "cookies"
- "content"
- "headers.content-type"
- "content.person.name.first_name"
-
- """
- # string.split(sep=None, maxsplit=1) -> list of strings
- # e.g. "content.person.name" => ["content", "person.name"]
- try:
- top_query, sub_query = field.split('.', 1)
- except ValueError:
- top_query = field
- sub_query = None
-
- # status_code
- if top_query in ["status_code", "encoding", "ok", "reason", "url"]:
- if sub_query:
- # status_code.XX
- err_msg = f"Failed to extract: {field}\n"
- logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
-
- return getattr(self, top_query)
-
- # cookies
- elif top_query == "cookies":
- cookies = self.cookies
- if not sub_query:
- # extract cookies
- return cookies
-
- try:
- return cookies[sub_query]
- except KeyError:
- err_msg = f"Failed to extract cookie! => {field}\n"
- err_msg += f"response cookies: {cookies}\n"
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- # elapsed
- elif top_query == "elapsed":
- available_attributes = u"available attributes: days, seconds, microseconds, total_seconds"
- if not sub_query:
- err_msg = "elapsed is datetime.timedelta instance, attribute should also be specified!\n"
- err_msg += available_attributes
- logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
- elif sub_query in ["days", "seconds", "microseconds"]:
- return getattr(self.elapsed, sub_query)
- elif sub_query == "total_seconds":
- return self.elapsed.total_seconds()
- else:
- err_msg = f"{sub_query} is not valid datetime.timedelta attribute.\n"
- err_msg += available_attributes
- logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
-
- # headers
- elif top_query == "headers":
- headers = self.headers
- if not sub_query:
- # extract headers
- return headers
-
- try:
- return headers[sub_query]
- except KeyError:
- err_msg = f"Failed to extract header! => {field}\n"
- err_msg += f"response headers: {headers}\n"
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- # response body
- elif top_query in ["body", "content", "text", "json"]:
- try:
- body = self.json
- except json.JSONDecodeError:
- body = self.text
-
- if not sub_query:
- # extract response body
- return body
-
- if isinstance(body, (dict, list)):
- # content = {"xxx": 123}, content.xxx
- return utils.query_json(body, sub_query)
- elif sub_query.isdigit():
- # content = "abcdefg", content.3 => d
- return utils.query_json(body, sub_query)
- else:
- # content = "abcdefg", content.xxx
- err_msg = f"Failed to extract attribute from response body! => {field}\n"
- err_msg += f"response body: {body}\n"
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- # new set response attributes in teardown_hooks
- elif top_query in self.__dict__:
- attributes = self.__dict__[top_query]
-
- if not sub_query:
- # extract response attributes
- return attributes
-
- if isinstance(attributes, (dict, list)):
- # attributes = {"xxx": 123}, content.xxx
- return utils.query_json(attributes, sub_query)
- elif sub_query.isdigit():
- # attributes = "abcdefg", attributes.3 => d
- return utils.query_json(attributes, sub_query)
- else:
- # content = "attributes.new_attribute_not_exist"
- err_msg = f"Failed to extract cumstom set attribute from teardown hooks! => {field}\n"
- err_msg += f"response set attributes: {attributes}\n"
- logger.error(err_msg)
- raise exceptions.TeardownHooksFailure(err_msg)
-
- # others
- else:
- err_msg = f"Failed to extract attribute from response! => {field}\n"
- err_msg += "available response attributes: status_code, cookies, elapsed, headers, content, " \
- "text, json, encoding, ok, reason, url.\n\n"
- err_msg += "If you want to set attribute in teardown_hooks, take the following example as reference:\n"
- err_msg += "response.new_attribute = 'new_attribute_value'\n"
- logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
-
- def extract_field(self, field):
- """ extract value from requests.Response.
- """
- if not isinstance(field, str):
- err_msg = f"Invalid extractor! => {field}\n"
- logger.error(err_msg)
- raise exceptions.ParamsError(err_msg)
-
- msg = f"extract: {field}"
-
- if field.startswith("$"):
- value = self._extract_field_with_jsonpath(field)
- elif text_extractor_regexp_compile.match(field):
- value = self._extract_field_with_regex(field)
- else:
- value = self._extract_field_with_delimiter(field)
-
- msg += f"\t=> {value}"
- logger.debug(msg)
-
- return value
-
- def extract_response(self, extractors):
- """ extract value from requests.Response and store in OrderedDict.
-
- Args:
- extractors (list):
-
- [
- {"resp_status_code": "status_code"},
- {"resp_headers_content_type": "headers.content-type"},
- {"resp_content": "content"},
- {"resp_content_person_first_name": "content.person.name.first_name"}
- ]
-
- Returns:
- OrderDict: variable binds ordered dict
-
- """
+ def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
if not extractors:
return {}
- logger.debug("start to extract from response object.")
- extracted_variables_mapping = OrderedDict()
- extract_binds_order_dict = utils.ensure_mapping_format(extractors)
+ extract_mapping = {}
+ for key, field in extractors.items():
+ field_value = jmespath.search(field, self.resp_obj_meta)
+ extract_mapping[key] = field_value
- for key, field in extract_binds_order_dict.items():
- extracted_variables_mapping[key] = self.extract_field(field)
+ logger.info(f"extract mapping: {extract_mapping}")
+ return extract_mapping
- return extracted_variables_mapping
+ def validate(self,
+ validators: Validators,
+ variables_mapping: VariablesMapping = None,
+ functions_mapping: FunctionsMapping = None) -> NoReturn:
+
+ self.validation_results = {}
+ if not validators:
+ return
+
+ validate_pass = True
+ failures = []
+
+ for v in validators:
+
+ if "validate_extractor" not in self.validation_results:
+ self.validation_results["validate_extractor"] = []
+
+ u_validator = uniform_validator(v)
+
+ # check item
+ check_item = u_validator["check"]
+ check_value = jmespath.search(check_item, self.resp_obj_meta)
+ check_value = parse_string_value(check_value)
+
+ # comparator
+ assert_method = u_validator["assert"]
+ assert_func = get_mapping_function(assert_method, functions_mapping)
+
+ # expect item
+ expect_item = u_validator["expect"]
+ # parse expected value with config/teststep/extracted variables
+ expect_value = parse_data(expect_item, variables_mapping, functions_mapping)
+
+ validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})"
+
+ validator_dict = {
+ "comparator": assert_method,
+ "check": check_item,
+ "check_value": check_value,
+ "expect": expect_item,
+ "expect_value": expect_value
+ }
+
+ try:
+ assert_func(check_value, expect_value)
+ validate_msg += "\t==> pass"
+ logger.info(validate_msg)
+ validator_dict["check_result"] = "pass"
+ except AssertionError:
+ validate_pass = False
+ validator_dict["check_result"] = "fail"
+ validate_msg += "\t==> fail"
+ validate_msg += f"\n" \
+ f"check_item: {check_item}\n" \
+ f"check_value: {check_value}({type(check_value).__name__})\n" \
+ f"assert_method: {assert_method}\n" \
+ f"expect_value: {expect_value}({type(expect_value).__name__})"
+ logger.error(validate_msg)
+ failures.append(validate_msg)
+
+ self.validation_results["validate_extractor"].append(validator_dict)
+
+ if not validate_pass:
+ failures_string = "\n".join([failure for failure in failures])
+ raise ValidationFailure(failures_string)
diff --git a/httprunner/runner.py b/httprunner/runner.py
index a84f9e99..ffc2cab2 100644
--- a/httprunner/runner.py
+++ b/httprunner/runner.py
@@ -1,261 +1,68 @@
-from enum import Enum
-from unittest.case import SkipTest
+from typing import List, Dict
from loguru import logger
-from httprunner import exceptions, response, utils
+from httprunner import utils
from httprunner.client import HttpSession
-from httprunner.context import SessionContext
-from httprunner.validator import Validator
+from httprunner.exceptions import ValidationFailure
+from httprunner.parser import build_url, parse_data, parse_variables_mapping
+from httprunner.response import ResponseObject
+from httprunner.schema import TestsConfig, TestStep, VariablesMapping, TestCase, SessionData
-class HookTypeEnum(Enum):
- SETUP = 1
- TEARDOWN = 2
+class TestCaseRunner(object):
+ config: TestsConfig = {}
+ teststeps: List[TestStep] = []
+ session: HttpSession = None
+ step_datas: List[SessionData] = []
+ validation_results: Dict = {}
-class Runner(object):
- """ Running testcases.
+ def init(self, testcase: TestCase) -> "TestCaseRunner":
+ self.config = testcase.config
+ self.teststeps = testcase.teststeps
+ return self
- Examples:
- >>> tests_mapping = {
- "project_meta": {
- "functions": {}
- },
- "testcases": [
- {
- "config": {
- "name": "XXXX",
- "base_url": "http://127.0.0.1",
- "verify": False
- },
- "teststeps": [
- {
- "name": "test description",
- "variables": [], # optional
- "request": {
- "url": "http://127.0.0.1:5000/api/users/1000",
- "method": "GET"
- }
- }
- ]
- }
- ]
- }
+ def with_session(self, s: HttpSession) -> "TestCaseRunner":
+ self.session = s
+ return self
- >>> testcases = parser.parse_tests(tests_mapping)
- >>> parsed_testcase = testcases[0]
+ def with_variables(self, **variables: VariablesMapping) -> "TestCaseRunner":
+ self.config.variables.update(variables)
+ return self
- >>> test_runner = runner.Runner(parsed_testcase["config"])
- >>> test_runner.run_test(parsed_testcase["teststeps"][0])
+ def __run_step(self, step: TestStep):
+ logger.info(f"run step: {step.name}")
- """
+ # parse
+ request_dict = step.request.dict()
+ parsed_request_dict = parse_data(request_dict, step.variables, self.config.functions)
- def __init__(self, config, http_client_session=None):
- """ run testcase or testsuite.
+ # prepare arguments
+ method = parsed_request_dict.pop("method")
+ url_path = parsed_request_dict.pop("url")
+ url = build_url(self.config.base_url, url_path)
- Args:
- config (dict): testcase/testsuite config dict
+ parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
- {
- "name": "ABC",
- "variables": {},
- "setup_hooks", [],
- "teardown_hooks", []
- }
-
- http_client_session (instance): requests.Session(), or locust.client.Session() instance.
-
- """
- self.verify = config.get("verify", True)
- self.export = config.get("export") or config.get("output", [])
- config_variables = config.get("variables", {})
-
- # testcase setup hooks
- testcase_setup_hooks = config.get("setup_hooks", [])
- # testcase teardown hooks
- self.testcase_teardown_hooks = config.get("teardown_hooks", [])
-
- self.http_client_session = http_client_session or HttpSession()
- self.session_context = SessionContext(config_variables)
-
- self.session_context.update_session_variables({
- "variables": config_variables
- })
-
- if testcase_setup_hooks:
- self.do_hook_actions(testcase_setup_hooks, HookTypeEnum.SETUP)
-
- def __del__(self):
- if self.testcase_teardown_hooks:
- self.do_hook_actions(self.testcase_teardown_hooks, HookTypeEnum.TEARDOWN)
-
- def __clear_test_data(self):
- """ clear request and response data
- """
- if not isinstance(self.http_client_session, HttpSession):
- return
-
- self.http_client_session.init_session_data()
-
- def _handle_skip_feature(self, test_dict):
- """ handle skip feature for test
- - skip: skip current test unconditionally
- - skipIf: skip current test if condition is true
- - skipUnless: skip current test unless condition is true
-
- Args:
- test_dict (dict): test info
-
- Raises:
- SkipTest: skip test
-
- """
- # TODO: move skip to initialize
- skip_reason = None
-
- if "skip" in test_dict:
- skip_reason = test_dict["skip"]
-
- elif "skipIf" in test_dict:
- skip_if_condition = test_dict["skipIf"]
- if self.session_context.eval_content(skip_if_condition):
- skip_reason = f"{skip_if_condition} evaluate to True"
-
- elif "skipUnless" in test_dict:
- skip_unless_condition = test_dict["skipUnless"]
- if not self.session_context.eval_content(skip_unless_condition):
- skip_reason = f"{skip_unless_condition} evaluate to False"
-
- if skip_reason:
- raise SkipTest(skip_reason)
-
- def do_hook_actions(self, actions, hook_type):
- """ call hook actions.
-
- Args:
- actions (list): each action in actions list maybe in two format.
-
- format1 (dict): assignment, the value returned by hook function will be assigned to variable.
- {"var": "${func()}"}
- format2 (str): only call hook functions.
- ${func()}
-
- hook_type (HookTypeEnum): setup/teardown
-
- """
- logger.debug(f"call {hook_type.name} hook actions.")
- for action in actions:
-
- if isinstance(action, dict) and len(action) == 1:
- # format 1
- # {"var": "${func()}"}
- var_name, hook_content = list(action.items())[0]
- hook_content_eval = self.session_context.eval_content(hook_content)
- logger.debug(
- f"assignment with hook: {var_name} = {hook_content} => {hook_content_eval}")
- self.session_context.update_test_variables(
- var_name, hook_content_eval
- )
- else:
- # format 2
- logger.debug(f"call hook function: {action}")
- # TODO: check hook function if valid
- self.session_context.eval_content(action)
-
- def _run_test(self, test_dict):
- """ run single teststep.
-
- Args:
- test_dict (dict): teststep info
- {
- "name": "teststep description",
- "skip": "skip this test unconditionally",
- "times": 3,
- "variables": [], # optional, override
- "request": {
- "url": "http://127.0.0.1:5000/api/users/1000",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json",
- "authorization": "$authorization",
- "random": "$random"
- },
- "json": {"name": "user", "password": "123456"}
- },
- "extract": {}, # optional
- "validate": [], # optional
- "setup_hooks": [], # optional
- "teardown_hooks": [] # optional
- }
-
- Raises:
- exceptions.ParamsError
- exceptions.ValidationFailure
- exceptions.ExtractFailure
-
- """
- # clear meta data first to ensure independence for each test
- self.__clear_test_data()
-
- # check skip
- self._handle_skip_feature(test_dict)
-
- # prepare
- test_dict = utils.lower_test_dict_keys(test_dict)
- test_variables = test_dict.get("variables", {})
- self.session_context.init_test_variables(test_variables)
-
- # teststep name
- test_name = self.session_context.eval_content(test_dict.get("name", ""))
-
- # parse test request
- raw_request = test_dict.get('request', {})
- parsed_test_request = self.session_context.eval_content(raw_request)
- self.session_context.update_test_variables("request", parsed_test_request)
-
- test_variables.update(self.session_context.session_variables_mapping["variables"])
- self.session_context.update_test_variables("variables", test_variables)
-
- # setup hooks
- setup_hooks = test_dict.get("setup_hooks", [])
- if setup_hooks:
- self.do_hook_actions(setup_hooks, HookTypeEnum.SETUP)
-
- # prepend url with base_url unless it's already an absolute URL
- url = parsed_test_request.pop('url')
- base_url = self.session_context.eval_content(test_dict.get("base_url", ""))
- parsed_url = utils.build_url(base_url, url)
-
- try:
- method = parsed_test_request.pop('method')
- parsed_test_request.setdefault("verify", self.verify)
- group_name = parsed_test_request.pop("group", None)
- except KeyError:
- raise exceptions.ParamsError("URL or METHOD missed!")
-
- logger.info(f"{method} {parsed_url}")
- logger.debug(f"request kwargs(raw): {parsed_test_request}")
+ logger.info(f"{method} {url}")
+ logger.debug(f"request kwargs(raw): {parsed_request_dict}")
# request
- resp = self.http_client_session.request(
- method,
- parsed_url,
- name=(group_name or test_name),
- **parsed_test_request
- )
- resp_obj = response.ResponseObject(resp)
+ self.session = self.session or HttpSession()
+ resp = self.session.request(method, url, **parsed_request_dict)
+ resp_obj = ResponseObject(resp)
def log_req_resp_details():
- err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
+ err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
# log request
err_msg += "====== request details ======\n"
- err_msg += f"url: {parsed_url}\n"
+ err_msg += f"url: {url}\n"
err_msg += f"method: {method}\n"
- headers = parsed_test_request.pop("headers", {})
+ headers = parsed_request_dict.pop("headers", {})
err_msg += f"headers: {headers}\n"
- for k, v in parsed_test_request.items():
+ for k, v in parsed_request_dict.items():
v = utils.omit_long_data(v)
err_msg += f"{k}: {repr(v)}\n"
@@ -268,144 +75,49 @@ class Runner(object):
err_msg += f"body: {repr(resp_obj.text)}\n"
logger.error(err_msg)
- # teardown hooks
- teardown_hooks = test_dict.get("teardown_hooks", [])
- if teardown_hooks:
- self.session_context.update_test_variables("response", resp_obj)
- self.do_hook_actions(teardown_hooks, HookTypeEnum.TEARDOWN)
- self.http_client_session.update_last_req_resp_record(resp_obj)
-
# extract
- extractors = test_dict.get("extract", {})
- try:
- extracted_variables_mapping = resp_obj.extract_response(extractors)
- self.session_context.update_session_variables(extracted_variables_mapping)
- except (exceptions.ParamsError, exceptions.ExtractFailure):
- log_req_resp_details()
- raise
+ extractors = step.extract
+ extract_mapping = resp_obj.extract(extractors)
+
+ variables_mapping = step.variables
+ variables_mapping.update(extract_mapping)
# validate
- validators = test_dict.get("validate") or test_dict.get("validators") or []
- validate_script = test_dict.get("validate_script", [])
- if validate_script:
- validators.append({
- "type": "python_script",
- "script": validate_script
- })
-
- validator = Validator(self.session_context, resp_obj)
+ validators = step.validators
try:
- validator.validate(validators)
- except exceptions.ValidationFailure:
+ resp_obj.validate(validators, variables_mapping, self.config.functions)
+ self.session.data.status = "passed"
+ except ValidationFailure:
+ self.session.data.status = "failed"
log_req_resp_details()
raise
finally:
- self.validation_results = validator.validation_results
+ self.validation_results = resp_obj.validation_results
+ # save request & response meta data
+ self.session.data.validators = self.validation_results
+ self.session.data.name = step.name
+ self.step_datas.append(self.session.data)
- def _run_testcase(self, testcase_dict):
- """ run single testcase.
- """
- self.meta_datas = []
- config = testcase_dict.get("config", {})
+ return extract_mapping
- # each teststeps in one testcase (YAML/JSON) share the same session.
- test_runner = Runner(config, self.http_client_session)
+ def test_start(self):
+ """main entrance"""
+ self.step_datas.clear()
+ session_variables = {}
+ for step in self.teststeps:
+ # update with config variables
+ step.variables.update(self.config.variables)
+ # update with session variables extracted from former step
+ step.variables.update(session_variables)
+ # parse variables
+ step.variables = parse_variables_mapping(step.variables, self.config.functions)
+ # run step
+ extract_mapping = self.__run_step(step)
+ # save extracted variables to session variables
+ session_variables.update(extract_mapping)
- tests = testcase_dict.get("teststeps", [])
+ return self
- for index, test_dict in enumerate(tests):
-
- # override current teststep variables with former testcase output variables
- former_output_variables = self.session_context.test_variables_mapping
- if former_output_variables:
- test_dict.setdefault("variables", {})
- test_dict["variables"].update(former_output_variables)
-
- try:
- test_runner.run_test(test_dict)
- except Exception:
- # log exception request_type and name for locust stat
- self.exception_request_type = test_runner.exception_request_type
- self.exception_name = test_runner.exception_name
- raise
- finally:
- _meta_datas = test_runner.meta_datas
- self.meta_datas.append(_meta_datas)
-
- self.session_context.update_session_variables(
- test_runner.export_variables(test_runner.export)
- )
-
- def run_test(self, test_dict):
- """ run single teststep of testcase.
- test_dict may be in 3 types.
-
- Args:
- test_dict (dict):
-
- # teststep
- {
- "name": "teststep description",
- "variables": [], # optional
- "request": {
- "url": "http://127.0.0.1:5000/api/users/1000",
- "method": "GET"
- }
- }
-
- # nested testcase
- {
- "config": {...},
- "teststeps": [
- {...},
- {...}
- ]
- }
-
- # TODO: function
- {
- "name": "exec function",
- "function": "${func()}"
- }
-
- """
- self.meta_datas = None
- if "teststeps" in test_dict:
- # nested testcase
- test_dict.setdefault("config", {}).setdefault("variables", {})
- test_dict["config"]["variables"].update(
- self.session_context.session_variables_mapping)
- self._run_testcase(test_dict)
- else:
- # api
- self.validation_results = {}
- try:
- self._run_test(test_dict)
- except Exception:
- # log exception request_type and name for locust stat
- self.exception_request_type = test_dict["request"]["method"]
- self.exception_name = test_dict.get("name")
- raise
- finally:
- # get request/response data and validate results
- self.meta_datas = getattr(self.http_client_session, "data", {})
- self.meta_datas["validators"] = self.validation_results
-
- def export_variables(self, output_variables_list):
- """ export current testcase variables
- """
- variables_mapping = self.session_context.session_variables_mapping
-
- output = {}
- for variable in output_variables_list:
- if variable not in variables_mapping:
- logger.warning(
- f"variable '{variable}' can not be found in variables mapping, "
- "failed to export!"
- )
- continue
-
- output[variable] = variables_mapping[variable]
-
- utils.print_info(output)
- return output
+ def run(self):
+ """main entrance alias for test_start"""
+ return self.test_start()
diff --git a/httprunner/v3/schema.py b/httprunner/schema.py
similarity index 100%
rename from httprunner/v3/schema.py
rename to httprunner/schema.py
diff --git a/httprunner/schema/__init__.py b/httprunner/schema/__init__.py
deleted file mode 100644
index 6ff26ad9..00000000
--- a/httprunner/schema/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .api import Api
-from .testcase import ProjectMeta, TestCase
-from .testsuite import TestSuite
diff --git a/httprunner/schema/api.py b/httprunner/schema/api.py
deleted file mode 100644
index 6744dd79..00000000
--- a/httprunner/schema/api.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from typing import Dict, Text
-
-from pydantic import BaseModel, Field
-
-from httprunner.schema import common
-
-
-class Api(BaseModel):
- name: common.Name
- request: common.Request
- variables: common.Variables = {}
- base_url: common.BaseUrl = ""
- setup_hooks: common.Hook = []
- teardown_hooks: common.Hook = []
- extract: Dict[Text, Text] = {}
- validation: common.Validate = Field([], alias="validate")
diff --git a/httprunner/schema/common.py b/httprunner/schema/common.py
deleted file mode 100644
index 40dcbb09..00000000
--- a/httprunner/schema/common.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from enum import Enum
-from typing import Dict, List, Any, Text, Union
-
-from pydantic import BaseModel, HttpUrl, Field
-
-Name = Text
-Url = Text
-BaseUrl = Union[HttpUrl, Text]
-Variables = Dict[Text, Any]
-Headers = Dict[Text, Text]
-Verify = bool
-Hook = List[Text]
-Export = List[Text]
-Validate = List[Dict]
-Env = Dict[Text, Any]
-
-
-class MethodEnum(Text, Enum):
- GET = 'GET'
- POST = 'POST'
- PUT = "PUT"
- DELETE = "DELETE"
- HEAD = "HEAD"
- OPTIONS = "OPTIONS"
- PATCH = "PATCH"
- CONNECT = "CONNECT"
- TRACE = "TRACE"
-
-
-class TestsConfig(BaseModel):
- name: Name
- verify: Verify = False
- base_url: BaseUrl = ""
- variables: Variables = {}
- setup_hooks: Hook = []
- teardown_hooks: Hook = []
- export: Export = []
-
- class Config:
- schema_extra = {
- "examples": [
- {
- "name": "used in testcase/testsuite to configure common fields",
- "verify": False,
- "base_url": "https://httpbin.org"
- }
- ]
- }
-
-
-class Request(BaseModel):
- method: MethodEnum = MethodEnum.GET
- url: Url
- params: Dict[Text, Text] = {}
- headers: Headers = {}
- req_json: Dict = Field({}, alias="json")
- cookies: Dict[Text, Text] = {}
- timeout: int = 120
- allow_redirects: bool = True
- verify: Verify = False
diff --git a/httprunner/schema/testcase.py b/httprunner/schema/testcase.py
deleted file mode 100644
index 16bdea29..00000000
--- a/httprunner/schema/testcase.py
+++ /dev/null
@@ -1,83 +0,0 @@
-from typing import Dict, List, Text, Union
-
-from pydantic import BaseModel, Field
-
-from httprunner.schema import common
-
-
-class ProjectMeta(BaseModel):
- debugtalk_py: Text = ""
- variables: common.Variables = {}
- env: common.Env = {}
-
-
-class TestStep(BaseModel):
- name: common.Name
- api: Text = None
- testcase: Text = None
- request: common.Request = None
- variables: common.Variables = {}
- extract: Union[Dict[Text, Text], List[Text]] = {}
- validation: common.Validate = Field([], alias="validate")
-
-
-class TestCase(BaseModel):
- config: common.TestsConfig
- teststeps: List[TestStep]
-
- class Config:
- schema_extra = {
- "examples": [
- {
- "config": {
- "name": "testcase name"
- },
- "teststeps": [
- {
- "name": "api 1",
- "api": "/path/to/api1"
- },
- {
- "name": "api 2",
- "api": "/path/to/api2"
- }
- ]
- },
- {
- "config": {
- "name": "demo testcase",
- "variables": {
- "device_sn": "ABC",
- "username": "${ENV(USERNAME)}",
- "password": "${ENV(PASSWORD)}"
- },
- "base_url": "http://127.0.0.1:5000"
- },
- "teststeps": [
- {
- "name": "demo step 1",
- "api": "path/to/api1.yml",
- "variables": {
- "user_agent": "iOS/10.3",
- "device_sn": "$device_sn"
- },
- "extract": {
- "token": "content.token"
- },
- "validate": [
- {
- "eq": ["status_code", 200]
- }
- ]
- },
- {
- "name": "demo step 2",
- "api": "path/to/api2.yml",
- "variables": {
- "token": "$token"
- }
- }
- ]
- }
- ]
- }
diff --git a/httprunner/schema/testsuite.py b/httprunner/schema/testsuite.py
deleted file mode 100644
index f973824a..00000000
--- a/httprunner/schema/testsuite.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from typing import List, Text
-
-from pydantic import BaseModel
-
-from httprunner.schema import common
-
-
-class TestCase(BaseModel):
- name: common.Name
- testcase: Text
- weight: int = 1
- variables: common.Variables = {}
-
-
-class TestSuite(BaseModel):
- config: common.TestsConfig
- testcases: List[TestCase]
diff --git a/httprunner/utils.py b/httprunner/utils.py
index 5d8601b2..670e34b5 100644
--- a/httprunner/utils.py
+++ b/httprunner/utils.py
@@ -6,15 +6,11 @@ import io
import itertools
import json
import os.path
-import re
from typing import Union
from loguru import logger
from httprunner import exceptions
-from httprunner.exceptions import ParamsError
-
-absolute_http_url_regexp = re.compile(r"^https?://", re.I)
def set_os_environ(variables_mapping):
@@ -52,74 +48,6 @@ def get_os_environ(variable_name):
raise exceptions.EnvNotFound(variable_name)
-def build_url(base_url, path):
- """ prepend url with base_url unless it's already an absolute URL """
- if absolute_http_url_regexp.match(path):
- return path
- elif base_url:
- return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/"))
- else:
- raise ParamsError("base url missed!")
-
-
-def query_json(json_content, query, delimiter='.'):
- """ Do an xpath-like query with json_content.
-
- Args:
- json_content (dict/list/string): content to be queried.
- query (str): query string.
- delimiter (str): delimiter symbol.
-
- Returns:
- str: queried result.
-
- Examples:
- >>> json_content = {
- "ids": [1, 2, 3, 4],
- "person": {
- "name": {
- "first_name": "Leo",
- "last_name": "Lee",
- },
- "age": 29,
- "cities": ["Guangzhou", "Shenzhen"]
- }
- }
- >>>
- >>> query_json(json_content, "person.name.first_name")
- >>> Leo
- >>>
- >>> query_json(json_content, "person.name.first_name.0")
- >>> L
- >>>
- >>> query_json(json_content, "person.cities.0")
- >>> Guangzhou
-
- """
- raise_flag = False
- response_body = f"response body: {json_content}\n"
- try:
- for key in query.split(delimiter):
- if isinstance(json_content, (list, str, bytes)):
- json_content = json_content[int(key)]
- elif isinstance(json_content, dict):
- json_content = json_content[key]
- else:
- logger.error(
- f"invalid type value: {json_content}({type(json_content)})")
- raise_flag = True
- except (KeyError, ValueError, IndexError):
- raise_flag = True
-
- if raise_flag:
- err_msg = f"Failed to extract! => {query}\n"
- err_msg += response_body
- logger.error(err_msg)
- raise exceptions.ExtractFailure(err_msg)
-
- return json_content
-
-
def lower_dict_keys(origin_dict):
""" convert keys in dict to lower case
@@ -158,21 +86,6 @@ def lower_dict_keys(origin_dict):
}
-def lower_test_dict_keys(test_dict):
- """ convert keys in test_dict to lower case, convertion will occur in two places:
- 1, all keys in test_dict;
- 2, all keys in test_dict["request"]
- """
- # convert keys in test_dict
- test_dict = lower_dict_keys(test_dict)
-
- if "request" in test_dict:
- # convert keys in test_dict["request"]
- test_dict["request"] = lower_dict_keys(test_dict["request"])
-
- return test_dict
-
-
def deepcopy_dict(data):
""" deepcopy dict data, ignore file object (_io.BufferedReader)
@@ -209,101 +122,6 @@ def deepcopy_dict(data):
return copied_data
-def ensure_mapping_format(variables):
- """ ensure variables are in mapping format.
-
- Args:
- variables (list/dict): original variables
-
- Returns:
- dict: ensured variables in dict format
-
- Examples:
- >>> variables = [
- {"a": 1},
- {"b": 2}
- ]
- >>> print(ensure_mapping_format(variables))
- {
- "a": 1,
- "b": 2
- }
-
- """
- if isinstance(variables, list):
- variables_dict = {}
- for map_dict in variables:
- variables_dict.update(map_dict)
-
- return variables_dict
-
- elif isinstance(variables, dict):
- return variables
-
- else:
- raise exceptions.ParamsError("variables format error!")
-
-
-def extend_variables(raw_variables, override_variables):
- """ extend raw_variables with override_variables.
- override_variables will merge and override raw_variables.
-
- Args:
- raw_variables (list):
- override_variables (list):
-
- Returns:
- dict: extended variables mapping
-
- Examples:
- >>> raw_variables = [{"var1": "val1"}, {"var2": "val2"}]
- >>> override_variables = [{"var1": "val111"}, {"var3": "val3"}]
- >>> extend_variables(raw_variables, override_variables)
- {
- 'var1', 'val111',
- 'var2', 'val2',
- 'var3', 'val3'
- }
-
- """
- if not raw_variables:
- override_variables_mapping = ensure_mapping_format(override_variables)
- return override_variables_mapping
-
- elif not override_variables:
- raw_variables_mapping = ensure_mapping_format(raw_variables)
- return raw_variables_mapping
-
- else:
- raw_variables_mapping = ensure_mapping_format(raw_variables)
- override_variables_mapping = ensure_mapping_format(override_variables)
- raw_variables_mapping.update(override_variables_mapping)
- return raw_variables_mapping
-
-
-def get_testcase_io(testcase):
- """ get and print testcase input(variables) and output(export).
-
- Args:
- testcase (unittest.suite.TestSuite): corresponding to one YAML/JSON file, it has been set two attributes:
- config: parsed config block
- runner: initialized runner.Runner() with config
- Returns:
- dict: input(variables) and output mapping.
-
- """
- test_runner = testcase.runner
- variables = testcase.config.get("variables", {})
- output_list = testcase.config.get("export") \
- or testcase.config.get("output", [])
- export_mapping = test_runner.export_variables(output_list)
-
- return {
- "in": variables,
- "out": export_mapping
- }
-
-
def print_info(info_mapping):
""" print info in mapping.
diff --git a/httprunner/utils_test.py b/httprunner/utils_test.py
index d37dce91..3ec389b4 100644
--- a/httprunner/utils_test.py
+++ b/httprunner/utils_test.py
@@ -2,7 +2,7 @@ import io
import os
import unittest
-from httprunner import exceptions, loader, utils
+from httprunner import loader, utils
class TestUtils(unittest.TestCase):
@@ -16,50 +16,6 @@ class TestUtils(unittest.TestCase):
self.assertIn("abc", os.environ)
self.assertEqual(os.environ["abc"], "123")
- def test_query_json(self):
- json_content = {
- "ids": [1, 2, 3, 4],
- "person": {
- "name": {
- "first_name": "Leo",
- "last_name": "Lee",
- },
- "age": 29,
- "cities": ["Guangzhou", "Shenzhen"]
- }
- }
- query = "ids.2"
- result = utils.query_json(json_content, query)
- self.assertEqual(result, 3)
-
- query = "ids.str_key"
- with self.assertRaises(exceptions.ExtractFailure):
- utils.query_json(json_content, query)
-
- query = "ids.5"
- with self.assertRaises(exceptions.ExtractFailure):
- utils.query_json(json_content, query)
-
- query = "person.age"
- result = utils.query_json(json_content, query)
- self.assertEqual(result, 29)
-
- query = "person.not_exist_key"
- with self.assertRaises(exceptions.ExtractFailure):
- utils.query_json(json_content, query)
-
- query = "person.cities.0"
- result = utils.query_json(json_content, query)
- self.assertEqual(result, "Guangzhou")
-
- query = "person.name.first_name"
- result = utils.query_json(json_content, query)
- self.assertEqual(result, "Leo")
-
- query = "person.name.first_name.0"
- result = utils.query_json(json_content, query)
- self.assertEqual(result, "L")
-
def current_validators(self):
from httprunner.builtin import comparators
functions_mapping = loader.load.load_module_functions(comparators)
@@ -112,33 +68,6 @@ class TestUtils(unittest.TestCase):
functions_mapping["type_match"]({}, "dict")
functions_mapping["type_match"]({"a": 1}, "dict")
- def test_handle_config_key_case(self):
- origin_dict = {
- "Name": "test",
- "Request": {
- "url": "http://127.0.0.1:5000",
- "METHOD": "POST",
- "Headers": {
- "Accept": "application/json",
- "User-Agent": "ios/9.3"
- }
- }
- }
- new_dict = utils.lower_test_dict_keys(origin_dict)
- self.assertIn("name", new_dict)
- self.assertIn("request", new_dict)
- self.assertIn("method", new_dict["request"])
- self.assertIn("headers", new_dict["request"])
- self.assertIn("Accept", new_dict["request"]["headers"])
- self.assertIn("User-Agent", new_dict["request"]["headers"])
-
- origin_dict = {
- "Name": "test",
- "Request": "$default_request"
- }
- new_dict = utils.lower_test_dict_keys(origin_dict)
- self.assertIn("$default_request", new_dict["request"])
-
def test_lower_dict_keys(self):
request_dict = {
"url": "http://127.0.0.1:5000",
@@ -162,29 +91,6 @@ class TestUtils(unittest.TestCase):
new_request_dict = utils.lower_dict_keys(request_dict)
self.assertEqual(None, request_dict)
- def test_ensure_mapping_format(self):
- map_list = [
- {"a": 1},
- {"b": 2}
- ]
- ordered_dict = utils.ensure_mapping_format(map_list)
- self.assertIsInstance(ordered_dict, dict)
- self.assertIn("a", ordered_dict)
-
- def test_extend_variables(self):
- raw_variables = [{"var1": "val1"}, {"var2": "val2"}]
- override_variables = [{"var1": "val111"}, {"var3": "val3"}]
- extended_variables_mapping = utils.extend_variables(raw_variables, override_variables)
- self.assertEqual(extended_variables_mapping["var1"], "val111")
- self.assertEqual(extended_variables_mapping["var2"], "val2")
- self.assertEqual(extended_variables_mapping["var3"], "val3")
-
- def test_extend_variables_fix(self):
- raw_variables = [{"var1": "val1"}, {"var2": "val2"}]
- override_variables = {}
- extended_variables_mapping = utils.extend_variables(raw_variables, override_variables)
- self.assertEqual(extended_variables_mapping["var1"], "val1")
-
def test_deepcopy_dict(self):
license_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
diff --git a/httprunner/v3/__init__.py b/httprunner/v3/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/httprunner/v3/api.py b/httprunner/v3/api.py
deleted file mode 100644
index b844f9a0..00000000
--- a/httprunner/v3/api.py
+++ /dev/null
@@ -1,287 +0,0 @@
-import os
-import sys
-import unittest
-from typing import List, Dict
-
-from loguru import logger
-
-from httprunner import report, loader, utils, exceptions, __version__
-from httprunner.report import gen_html_report
-from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsMapping, TestCaseSummary, TestSuiteSummary
-
-
-class HttpRunner(object):
- """ Developer Interface: Main Interface
- Usage:
-
- from httprunner.api import HttpRunner
- runner = HttpRunner(
- failfast=True,
- save_tests=True,
- log_level="INFO",
- log_file="test.log"
- )
- summary = runner.run(path_or_tests)
-
- """
-
- def __init__(self, save_tests=False, log_level="WARNING", log_file=None):
- """ initialize HttpRunner.
-
- Args:
- save_tests (bool): save loaded/parsed tests to JSON file.
- log_level (str): logging level.
- log_file (str): log file path.
-
- """
- self.exception_stage = "initialize HttpRunner()"
- kwargs = {
- "failfast": True,
- "resultclass": report.HtmlTestResult
- }
-
- logger.remove()
- log_level = log_level.upper()
- logger.add(sys.stdout, level=log_level)
- if log_file:
- logger.add(log_file, level=log_level)
-
- self.unittest_runner = unittest.TextTestRunner(**kwargs)
- self.test_loader = unittest.TestLoader()
- self.save_tests = save_tests
- self._summary = None
- self.test_path = None
-
- def _prepare_tests(self, tests: TestsMapping) -> List[unittest.TestSuite]:
- def _add_test(test_runner: TestCaseRunner):
- """ add test to testcase.
- """
- def test(self):
- try:
- test_runner.run()
- except exceptions.MyBaseFailure as ex:
- self.fail(str(ex))
- finally:
- self.step_datas = test_runner.step_datas
-
- test.__doc__ = test_runner.config.name
- return test
-
- project_meta = tests.project_meta
- testcases = tests.testcases
-
- prepared_testcases: List[unittest.TestSuite] = []
-
- for testcase in testcases:
- testcase.config.variables.update(project_meta.variables)
- testcase.config.functions.update(project_meta.functions)
-
- test_runner = TestCaseRunner().init(testcase)
-
- TestSequense = type('TestSequense', (unittest.TestCase,), {})
- test_method = _add_test(test_runner)
- setattr(TestSequense, "test_method_name", test_method)
-
- loaded_testcase = self.test_loader.loadTestsFromTestCase(TestSequense)
- setattr(loaded_testcase, "config", testcase.config)
- # setattr(loaded_testcase, "teststeps", testcase.teststeps)
- # setattr(loaded_testcase, "runner", test_runner)
- prepared_testcases.append(loaded_testcase)
-
- return prepared_testcases
-
- def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[TestCaseSummary]:
- """ run prepared testcases
- """
- tests_results: List[TestCaseSummary] = []
-
- for index, testcase in enumerate(prepared_testcases):
- log_handler = None
- if self.save_tests:
- logs_file_abs_path = utils.prepare_log_file_abs_path(
- self.test_path, f"testcase_{index+1}.log"
- )
- log_handler = logger.add(logs_file_abs_path, level="DEBUG")
-
- logger.info(f"Start to run testcase: {testcase.config.name}")
-
- result = self.unittest_runner.run(testcase)
- testcase_summary = report.get_summary(result)
- testcase_summary.in_out.vars = testcase.config.variables
- testcase_summary.in_out.out = testcase.config.export
-
- if self.save_tests and log_handler:
- logger.remove(log_handler)
- logs_file_abs_path = utils.prepare_log_file_abs_path(
- self.test_path, f"testcase_{index+1}.log"
- )
- testcase_summary.log = logs_file_abs_path
-
- if result.wasSuccessful():
- tests_results.append(testcase_summary)
- else:
- tests_results.insert(0, testcase_summary)
-
- return tests_results
-
- def _aggregate(self, tests_results: List[TestCaseSummary]) -> TestSuiteSummary:
- """ aggregate multiple testcase results
-
- Args:
- tests_results (list): list of testcase summary
-
- """
- testsuite_summary = {
- "success": True,
- "stat": {
- "total": len(tests_results),
- "success": 0,
- "fail": 0
- },
- "time": {},
- "platform": report.get_platform(),
- "testcases": []
- }
-
- for testcase_summary in tests_results:
- if testcase_summary.success:
- testsuite_summary["stat"]["success"] += 1
- else:
- testsuite_summary["stat"]["fail"] += 1
-
- testsuite_summary["success"] &= testcase_summary.success
-
- testsuite_summary["testcases"].append(testcase_summary)
-
- total_duration = tests_results[-1].time.start_at + tests_results[-1].time.duration \
- - tests_results[0].time.start_at
- testsuite_summary["time"] = {
- "start_at": tests_results[0].time.start_at,
- "start_at_iso_format": tests_results[0].time.start_at_iso_format,
- "duration": total_duration
- }
-
- return TestSuiteSummary.parse_obj(testsuite_summary)
-
- def run_tests(self, tests_mapping) -> TestSuiteSummary:
- """ run testcase/testsuite data
- """
- tests = TestsMapping.parse_obj(tests_mapping)
- self.test_path = tests.project_meta.test_path
-
- if self.save_tests:
- utils.dump_json_file(
- tests_mapping,
- utils.prepare_log_file_abs_path(self.test_path, "loaded.json")
- )
-
- # prepare testcases
- self.exception_stage = "prepare testcases"
- prepared_testcases = self._prepare_tests(tests)
-
- # run prepared testcases
- self.exception_stage = "run prepared testcases"
- results = self._run_suite(prepared_testcases)
-
- # aggregate results
- self.exception_stage = "aggregate results"
- self._summary = self._aggregate(results)
-
- # generate html report
- self.exception_stage = "generate html report"
- report.stringify_summary(self._summary)
-
- if self.save_tests:
- utils.dump_json_file(
- self._summary.dict(),
- utils.prepare_log_file_abs_path(self.test_path, "summary.json")
- )
- # save variables and export data
- vars_out = self.get_vars_out()
- utils.dump_json_file(
- vars_out,
- utils.prepare_log_file_abs_path(self.test_path, "io.json")
- )
-
- return self._summary
-
- def get_vars_out(self):
- """ get variables and output
- Returns:
- list: list of variables and output.
- if tests are parameterized, list items are corresponded to parameters.
-
- [
- {
- "in": {
- "user1": "leo"
- },
- "out": {
- "out1": "out_value_1"
- }
- },
- {...}
- ]
-
- None: returns None if tests not started or finished or corrupted.
-
- """
- if not self._summary:
- return None
-
- return [
- testcase_summary.in_out.dict()
- for testcase_summary in self._summary.testcases
- ]
-
- def run_path(self, path, dot_env_path=None, mapping=None) -> TestSuiteSummary:
- """ run testcase/testsuite file or folder.
-
- Args:
- path (str): testcase/testsuite file/foler path.
- dot_env_path (str): specified .env file path.
- mapping (dict): if mapping is specified, it will override variables in config block.
-
- Returns:
- dict: result summary
-
- """
- # load tests
- logger.info(f"HttpRunner version: {__version__}")
- self.exception_stage = "load tests"
- tests_mapping = loader.load_cases(path, dot_env_path)
-
- if mapping:
- tests_mapping["project_meta"]["variables"] = mapping
-
- return self.run_tests(tests_mapping)
-
- def run(self, path_or_tests, dot_env_path=None, mapping=None):
- """ main interface.
-
- Args:
- path_or_tests:
- str: testcase/testsuite file/foler path
- dict: valid testcase/testsuite data
- dot_env_path (str): specified .env file path.
- mapping (dict): if mapping is specified, it will override variables in config block.
-
- Returns:
- dict: result summary
-
- """
- if loader.is_test_path(path_or_tests):
- return self.run_path(path_or_tests, dot_env_path, mapping)
- elif loader.is_test_content(path_or_tests):
- project_working_directory = path_or_tests.get("project_meta", {}).get("PWD", os.getcwd())
- loader.init_pwd(project_working_directory)
- return self.run_tests(path_or_tests)
- else:
- raise exceptions.ParamsError(f"Invalid testcase path or testcases: {path_or_tests}")
-
- def gen_html_report(self, report_template=None, report_dir=None, report_file=None):
- if not self._summary:
- return None
-
- return gen_html_report(self._summary, report_template, report_dir, report_file)
diff --git a/httprunner/v3/parser.py b/httprunner/v3/parser.py
deleted file mode 100644
index d44b6bcd..00000000
--- a/httprunner/v3/parser.py
+++ /dev/null
@@ -1,421 +0,0 @@
-import ast
-import builtins
-import re
-from typing import Any, Set, Text, Callable, List, Dict
-
-from httprunner import loader, utils, exceptions
-from httprunner.v3.schema import VariablesMapping, FunctionsMapping
-
-absolute_http_url_regexp = re.compile(r"^https?://", re.I)
-
-# use $$ to escape $ notation
-dolloar_regex_compile = re.compile(r"\$\$")
-# variable notation, e.g. ${var} or $var
-variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
-# function notation, e.g. ${func1($var_1, $var_3)}
-function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
-
-
-def parse_string_value(str_value: Text) -> Any:
- """ parse string to number if possible
- e.g. "123" => 123
- "12.2" => 12.3
- "abc" => "abc"
- "$var" => "$var"
- """
- try:
- return ast.literal_eval(str_value)
- except ValueError:
- return str_value
- except SyntaxError:
- # e.g. $var, ${func}
- return str_value
-
-
-def build_url(base_url, path):
- """ prepend url with base_url unless it's already an absolute URL """
- if absolute_http_url_regexp.match(path):
- return path
- elif base_url:
- return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/"))
- else:
- raise exceptions.ParamsError("base url missed!")
-
-
-def regex_findall_variables(content: Text) -> List[Text]:
- """ extract all variable names from content, which is in format $variable
-
- Args:
- content (str): string content
-
- Returns:
- list: variables list extracted from string content
-
- Examples:
- >>> regex_findall_variables("$variable")
- ["variable"]
-
- >>> regex_findall_variables("/blog/$postid")
- ["postid"]
-
- >>> regex_findall_variables("/$var1/$var2")
- ["var1", "var2"]
-
- >>> regex_findall_variables("abc")
- []
-
- """
- try:
- vars_list = []
- for var_tuple in variable_regex_compile.findall(content):
- vars_list.append(
- var_tuple[0] or var_tuple[1]
- )
- return vars_list
- except TypeError:
- return []
-
-
-def regex_findall_functions(content: Text) -> List[Text]:
- """ extract all functions from string content, which are in format ${fun()}
-
- Args:
- content (str): string content
-
- Returns:
- list: functions list extracted from string content
-
- Examples:
- >>> regex_findall_functions("${func(5)}")
- ["func(5)"]
-
- >>> regex_findall_functions("${func(a=1, b=2)}")
- ["func(a=1, b=2)"]
-
- >>> regex_findall_functions("/api/1000?_t=${get_timestamp()}")
- ["get_timestamp()"]
-
- >>> regex_findall_functions("/api/${add(1, 2)}")
- ["add(1, 2)"]
-
- >>> regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}")
- ["add(1, 2)", "get_timestamp()"]
-
- """
- try:
- return function_regex_compile.findall(content)
- except TypeError:
- return []
-
-
-def extract_variables(content: Any) -> Set:
- """ extract all variables in content recursively.
- """
- if isinstance(content, (list, set, tuple)):
- variables = set()
- for item in content:
- variables = variables | extract_variables(item)
- return variables
-
- elif isinstance(content, dict):
- variables = set()
- for key, value in content.items():
- variables = variables | extract_variables(value)
- return variables
-
- elif isinstance(content, str):
- return set(regex_findall_variables(content))
-
- return set()
-
-
-def parse_function_params(params: Text) -> Dict:
- """ parse function params to args and kwargs.
-
- Args:
- params (str): function param in string
-
- Returns:
- dict: function meta dict
-
- {
- "args": [],
- "kwargs": {}
- }
-
- Examples:
- >>> parse_function_params("")
- {'args': [], 'kwargs': {}}
-
- >>> parse_function_params("5")
- {'args': [5], 'kwargs': {}}
-
- >>> parse_function_params("1, 2")
- {'args': [1, 2], 'kwargs': {}}
-
- >>> parse_function_params("a=1, b=2")
- {'args': [], 'kwargs': {'a': 1, 'b': 2}}
-
- >>> parse_function_params("1, 2, a=3, b=4")
- {'args': [1, 2], 'kwargs': {'a':3, 'b':4}}
-
- """
- function_meta = {
- "args": [],
- "kwargs": {}
- }
-
- params_str = params.strip()
- if params_str == "":
- return function_meta
-
- args_list = params_str.split(',')
- for arg in args_list:
- arg = arg.strip()
- if '=' in arg:
- key, value = arg.split('=')
- function_meta["kwargs"][key.strip()] = parse_string_value(value.strip())
- else:
- function_meta["args"].append(parse_string_value(arg))
-
- return function_meta
-
-
-def get_mapping_variable(variable_name: Text, variables_mapping: VariablesMapping) -> Any:
- """ get variable from variables_mapping.
-
- Args:
- variable_name (str): variable name
- variables_mapping (dict): variables mapping
-
- Returns:
- mapping variable value.
-
- Raises:
- exceptions.VariableNotFound: variable is not found.
-
- """
- # TODO: get variable from debugtalk module and environ
- try:
- return variables_mapping[variable_name]
- except KeyError:
- raise exceptions.VariableNotFound(f"{variable_name} not found in {variables_mapping}")
-
-
-def get_mapping_function(function_name: Text, functions_mapping: FunctionsMapping) -> Callable:
- """ get function from functions_mapping,
- if not found, then try to check if builtin function.
-
- Args:
- function_name (str): function name
- functions_mapping (dict): functions mapping
-
- Returns:
- mapping function object.
-
- Raises:
- exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
-
- """
- if function_name in functions_mapping:
- return functions_mapping[function_name]
-
- elif function_name in ["parameterize", "P"]:
- return loader.load_csv_file
-
- elif function_name in ["environ", "ENV"]:
- return utils.get_os_environ
-
- elif function_name in ["multipart_encoder", "multipart_content_type"]:
- # extension for upload test
- from httprunner.ext import uploader
- return getattr(uploader, function_name)
-
- try:
- # check if HttpRunner builtin functions
- built_in_functions = loader.load_builtin_functions()
- return built_in_functions[function_name]
- except KeyError:
- pass
-
- try:
- # check if Python builtin functions
- return getattr(builtins, function_name)
- except AttributeError:
- pass
-
- raise exceptions.FunctionNotFound(f"{function_name} is not found.")
-
-
-def parse_string(
- raw_string: Text,
- variables_mapping: VariablesMapping,
- functions_mapping: FunctionsMapping) -> Any:
- """ parse string content with variables and functions mapping.
-
- Args:
- raw_string: raw string content to be parsed.
- variables_mapping: variables mapping.
- functions_mapping: functions mapping.
-
- Returns:
- str: parsed string content.
-
- Examples:
- >>> raw_string = "abc${add_one($num)}def"
- >>> variables_mapping = {"num": 3}
- >>> functions_mapping = {"add_one": lambda x: x + 1}
- >>> parse_string(raw_string, variables_mapping, functions_mapping)
- "abc4def"
-
- """
- try:
- match_start_position = raw_string.index("$", 0)
- parsed_string = raw_string[0:match_start_position]
- except ValueError:
- parsed_string = raw_string
- return parsed_string
-
- while match_start_position < len(raw_string):
-
- # Notice: notation priority
- # $$ > ${func($a, $b)} > $var
-
- # search $$
- dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
- if dollar_match:
- match_start_position = dollar_match.end()
- parsed_string += "$"
- continue
-
- # search function like ${func($a, $b)}
- func_match = function_regex_compile.match(raw_string, match_start_position)
- if func_match:
- func_name = func_match.group(1)
- func = get_mapping_function(func_name, functions_mapping)
-
- func_params_str = func_match.group(2)
- function_meta = parse_function_params(func_params_str)
- args = function_meta["args"]
- kwargs = function_meta["kwargs"]
-
- parsed_args = parse_data(args, variables_mapping, functions_mapping)
- parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
- func_eval_value = func(*parsed_args, **parsed_kwargs)
-
- func_raw_str = "${" + func_name + f"({func_params_str})" + "}"
- if func_raw_str == raw_string:
- # raw_string is a function, e.g. "${add_one(3)}", return its eval value directly
- return func_eval_value
-
- # raw_string contains one or many functions, e.g. "abc${add_one(3)}def"
- parsed_string += str(func_eval_value)
- match_start_position = func_match.end()
- continue
-
- # search variable like ${var} or $var
- var_match = variable_regex_compile.match(raw_string, match_start_position)
- if var_match:
- var_name = var_match.group(1) or var_match.group(2)
- var_value = get_mapping_variable(var_name, variables_mapping)
-
- if f"${var_name}" == raw_string or "${" + var_name + "}" == raw_string:
- # raw_string is a variable, $var or ${var}, return its value directly
- return var_value
-
- # raw_string contains one or many variables, e.g. "abc${var}def"
- parsed_string += str(var_value)
- match_start_position = var_match.end()
- continue
-
- curr_position = match_start_position
- try:
- # find next $ location
- match_start_position = raw_string.index("$", curr_position + 1)
- remain_string = raw_string[curr_position:match_start_position]
- except ValueError:
- remain_string = raw_string[curr_position:]
- # break while loop
- match_start_position = len(raw_string)
-
- parsed_string += remain_string
-
- return parsed_string
-
-
-def parse_data(
- raw_data: Any,
- variables_mapping: VariablesMapping = None,
- functions_mapping: FunctionsMapping = None) -> Any:
- """ parse raw data with evaluated variables mapping.
- Notice: variables_mapping should not contain any variable or function.
- """
- if isinstance(raw_data, str):
- # content in string format may contains variables and functions
- variables_mapping = variables_mapping or {}
- functions_mapping = functions_mapping or {}
- raw_data = raw_data.strip()
- return parse_string(raw_data, variables_mapping, functions_mapping)
-
- elif isinstance(raw_data, (list, set, tuple)):
- return [
- parse_data(item, variables_mapping, functions_mapping)
- for item in raw_data
- ]
-
- elif isinstance(raw_data, dict):
- parsed_data = {}
- for key, value in raw_data.items():
- parsed_key = parse_data(key, variables_mapping, functions_mapping)
- parsed_value = parse_data(value, variables_mapping, functions_mapping)
- parsed_data[parsed_key] = parsed_value
-
- return parsed_data
-
- else:
- # other types, e.g. None, int, float, bool
- return raw_data
-
-
-def parse_variables_mapping(
- variables_mapping: VariablesMapping,
- functions_mapping: FunctionsMapping = None) -> VariablesMapping:
-
- parsed_variables: VariablesMapping = {}
-
- while len(parsed_variables) != len(variables_mapping):
- for var_name in variables_mapping:
-
- if var_name in parsed_variables:
- continue
-
- var_value = variables_mapping[var_name]
- variables = extract_variables(var_value)
-
- # check if reference variable itself
- if var_name in variables:
- # e.g.
- # variables_mapping = {"token": "abc$token"}
- # variables_mapping = {"key": ["$key", 2]}
- raise exceptions.VariableNotFound(var_name)
-
- # check if reference variable not in variables_mapping
- not_defined_variables = [
- v_name
- for v_name in variables
- if v_name not in variables_mapping
- ]
- if not_defined_variables:
- # e.g. {"varA": "123$varB", "varB": "456$varC"}
- # e.g. {"varC": "${sum_two($a, $b)}"}
- raise exceptions.VariableNotFound(not_defined_variables)
-
- try:
- parsed_value = parse_data(
- var_value, parsed_variables, functions_mapping)
- except exceptions.VariableNotFound:
- continue
-
- parsed_variables[var_name] = parsed_value
-
- return parsed_variables
diff --git a/httprunner/v3/parser_test.py b/httprunner/v3/parser_test.py
deleted file mode 100644
index 9d9ec9e6..00000000
--- a/httprunner/v3/parser_test.py
+++ /dev/null
@@ -1,528 +0,0 @@
-import time
-import unittest
-
-from httprunner.v3 import parser
-from httprunner.exceptions import VariableNotFound, FunctionNotFound
-
-
-class TestParserBasic(unittest.TestCase):
-
- def test_parse_variables_mapping(self):
- variables = {
- "varA": "$varB",
- "varB": "$varC",
- "varC": "123",
- "a": 1,
- "b": 2
- }
- parsed_variables = parser.parse_variables_mapping(variables)
- print(parsed_variables)
- self.assertEqual(parsed_variables["varA"], "123")
- self.assertEqual(parsed_variables["varB"], "123")
-
- def test_parse_variables_mapping_exception(self):
- variables = {
- "varA": "$varB",
- "varB": "$varC",
- "a": 1,
- "b": 2
- }
- with self.assertRaises(VariableNotFound):
- parser.parse_variables_mapping(variables)
-
- def test_parse_string_value(self):
- self.assertEqual(parser.parse_string_value("123"), 123)
- self.assertEqual(parser.parse_string_value("12.3"), 12.3)
- self.assertEqual(parser.parse_string_value("a123"), "a123")
- self.assertEqual(parser.parse_string_value("$var"), "$var")
- self.assertEqual(parser.parse_string_value("${func}"), "${func}")
-
- def test_extract_variables(self):
- self.assertEqual(
- parser.extract_variables("$var"),
- {"var"}
- )
- self.assertEqual(
- parser.extract_variables("$var123"),
- {"var123"}
- )
- self.assertEqual(
- parser.extract_variables("$var_name"),
- {"var_name"}
- )
- self.assertEqual(
- parser.extract_variables("var"),
- set()
- )
- self.assertEqual(
- parser.extract_variables("a$var"),
- {"var"}
- )
- self.assertEqual(
- parser.extract_variables("$v ar"),
- {"v"}
- )
- self.assertEqual(
- parser.extract_variables(" "),
- set()
- )
- self.assertEqual(
- parser.extract_variables("$abc*"),
- {"abc"}
- )
- self.assertEqual(
- parser.extract_variables("${func()}"),
- set()
- )
- self.assertEqual(
- parser.extract_variables("${func(1,2)}"),
- set()
- )
- self.assertEqual(
- parser.extract_variables("${gen_md5($TOKEN, $data, $random)}"),
- {"TOKEN", "data", "random"}
- )
-
- def test_parse_function_params(self):
- self.assertEqual(
- parser.parse_function_params(""),
- {'args': [], 'kwargs': {}}
- )
- self.assertEqual(
- parser.parse_function_params("5"),
- {'args': [5], 'kwargs': {}}
- )
- self.assertEqual(
- parser.parse_function_params("1, 2"),
- {'args': [1, 2], 'kwargs': {}}
- )
- self.assertEqual(
- parser.parse_function_params("a=1, b=2"),
- {'args': [], 'kwargs': {'a': 1, 'b': 2}}
- )
- self.assertEqual(
- parser.parse_function_params("a= 1, b =2"),
- {'args': [], 'kwargs': {'a': 1, 'b': 2}}
- )
- self.assertEqual(
- parser.parse_function_params("1, 2, a=3, b=4"),
- {'args': [1, 2], 'kwargs': {'a': 3, 'b': 4}}
- )
- self.assertEqual(
- parser.parse_function_params("$request, 123"),
- {'args': ["$request", 123], 'kwargs': {}}
- )
- self.assertEqual(
- parser.parse_function_params(" "),
- {'args': [], 'kwargs': {}}
- )
- self.assertEqual(
- parser.parse_function_params("hello world, a=3, b=4"),
- {'args': ["hello world"], 'kwargs': {'a': 3, 'b': 4}}
- )
- self.assertEqual(
- parser.parse_function_params("$request, 12 3"),
- {'args': ["$request", '12 3'], 'kwargs': {}}
- )
-
- def test_extract_functions(self):
- self.assertEqual(
- parser.regex_findall_functions("${func()}"),
- [("func", "")]
- )
- self.assertEqual(
- parser.regex_findall_functions("${func(5)}"),
- [("func", "5")]
- )
- self.assertEqual(
- parser.regex_findall_functions("${func(a=1, b=2)}"),
- [("func", "a=1, b=2")]
- )
- self.assertEqual(
- parser.regex_findall_functions("${func(1, $b, c=$x, d=4)}"),
- [("func", "1, $b, c=$x, d=4")]
- )
- self.assertEqual(
- parser.regex_findall_functions("/api/1000?_t=${get_timestamp()}"),
- [("get_timestamp", "")]
- )
- self.assertEqual(
- parser.regex_findall_functions("/api/${add(1, 2)}"),
- [("add", "1, 2")]
- )
- self.assertEqual(
- parser.regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}"),
- [('add', '1, 2'), ('get_timestamp', '')]
- )
- self.assertEqual(
- parser.regex_findall_functions("abc${func(1, 2, a=3, b=4)}def"),
- [('func', '1, 2, a=3, b=4')]
- )
-
- def test_parse_data_string_with_variables(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- self.assertEqual(
- parser.parse_data("$var_1", variables_mapping),
- "abc"
- )
- self.assertEqual(
- parser.parse_data("${var_1}", variables_mapping),
- "abc"
- )
- self.assertEqual(
- parser.parse_data("var_1", variables_mapping),
- "var_1"
- )
- self.assertEqual(
- parser.parse_data("$var_1#XYZ", variables_mapping),
- "abc#XYZ"
- )
- self.assertEqual(
- parser.parse_data("${var_1}#XYZ", variables_mapping),
- "abc#XYZ"
- )
- self.assertEqual(
- parser.parse_data("/$var_1/$var_2/var3", variables_mapping),
- "/abc/def/var3"
- )
- self.assertEqual(
- parser.parse_data("$var_3", variables_mapping),
- 123
- )
- self.assertEqual(
- parser.parse_data("$var_4", variables_mapping),
- {"a": 1}
- )
- self.assertEqual(
- parser.parse_data("$var_5", variables_mapping),
- True
- )
- self.assertEqual(
- parser.parse_data("abc$var_5", variables_mapping),
- "abcTrue"
- )
- self.assertEqual(
- parser.parse_data("abc$var_4", variables_mapping),
- "abc{'a': 1}"
- )
- self.assertEqual(
- parser.parse_data("$var_6", variables_mapping),
- None
- )
-
- with self.assertRaises(VariableNotFound):
- parser.parse_data("/api/$SECRET_KEY", variables_mapping)
-
- self.assertEqual(
- parser.parse_data(["$var_1", "$var_2"], variables_mapping),
- ["abc", "def"]
- )
- self.assertEqual(
- parser.parse_data({"$var_1": "$var_2"}, variables_mapping),
- {"abc": "def"}
- )
-
- # format: $var
- value = parser.parse_data("ABC$var_1", variables_mapping)
- self.assertEqual(value, "ABCabc")
-
- value = parser.parse_data("ABC$var_1$var_3", variables_mapping)
- self.assertEqual(value, "ABCabc123")
-
- value = parser.parse_data("ABC$var_1/$var_3", variables_mapping)
- self.assertEqual(value, "ABCabc/123")
-
- value = parser.parse_data("ABC$var_1/", variables_mapping)
- self.assertEqual(value, "ABCabc/")
-
- value = parser.parse_data("ABC$var_1$", variables_mapping)
- self.assertEqual(value, "ABCabc$")
-
- value = parser.parse_data("ABC$var_1/123$var_1/456", variables_mapping)
- self.assertEqual(value, "ABCabc/123abc/456")
-
- value = parser.parse_data("ABC$var_1/$var_2/$var_1", variables_mapping)
- self.assertEqual(value, "ABCabc/def/abc")
-
- value = parser.parse_data("func1($var_1, $var_3)", variables_mapping)
- self.assertEqual(value, "func1(abc, 123)")
-
- # format: ${var}
- value = parser.parse_data("ABC${var_1}", variables_mapping)
- self.assertEqual(value, "ABCabc")
-
- value = parser.parse_data("ABC${var_1}${var_3}", variables_mapping)
- self.assertEqual(value, "ABCabc123")
-
- value = parser.parse_data("ABC${var_1}/${var_3}", variables_mapping)
- self.assertEqual(value, "ABCabc/123")
-
- value = parser.parse_data("ABC${var_1}/", variables_mapping)
- self.assertEqual(value, "ABCabc/")
-
- value = parser.parse_data("ABC${var_1}123", variables_mapping)
- self.assertEqual(value, "ABCabc123")
-
- value = parser.parse_data("ABC${var_1}/123${var_1}/456", variables_mapping)
- self.assertEqual(value, "ABCabc/123abc/456")
-
- value = parser.parse_data("ABC${var_1}/${var_2}/${var_1}", variables_mapping)
- self.assertEqual(value, "ABCabc/def/abc")
-
- value = parser.parse_data("func1(${var_1}, ${var_3})", variables_mapping)
- self.assertEqual(value, "func1(abc, 123)")
-
- def test_parse_data_multiple_identical_variables(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- }
- self.assertEqual(
- parser.parse_data("/$var_1/$var_2/$var_1", variables_mapping),
- "/abc/def/abc"
- )
-
- variables_mapping = {
- "userid": 100,
- "data": 1498
- }
- content = "/users/$userid/training/$data?userId=$userid&data=$data"
- self.assertEqual(
- parser.parse_data(content, variables_mapping),
- "/users/100/training/1498?userId=100&data=1498"
- )
-
- variables_mapping = {
- "user": 100,
- "userid": 1000,
- "data": 1498
- }
- content = "/users/$user/$userid/$data?userId=$userid&data=$data"
- self.assertEqual(
- parser.parse_data(content, variables_mapping),
- "/users/100/1000/1498?userId=1000&data=1498"
- )
-
- def test_parse_data_string_with_functions(self):
- import random, string
- functions_mapping = {
- "gen_random_string": lambda str_len: ''.join(random.choice(string.ascii_letters + string.digits) \
- for _ in range(str_len))
- }
- result = parser.parse_data("${gen_random_string(5)}", functions_mapping=functions_mapping)
- self.assertEqual(len(result), 5)
-
- add_two_nums = lambda a, b=1: a + b
- functions_mapping["add_two_nums"] = add_two_nums
- self.assertEqual(
- parser.parse_data("${add_two_nums(1)}", functions_mapping=functions_mapping),
- 2
- )
- self.assertEqual(
- parser.parse_data("${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
- 3
- )
- self.assertEqual(
- parser.parse_data("/api/${add_two_nums(1, 2)}", functions_mapping=functions_mapping),
- "/api/3"
- )
-
- with self.assertRaises(FunctionNotFound):
- parser.parse_data("/api/${gen_md5(abc)}")
-
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- functions_mapping = {
- "func1": lambda x, y: str(x) + str(y)
- }
-
- value = parser.parse_data("${func1($var_1, $var_3)}", variables_mapping, functions_mapping)
- self.assertEqual(value, "abc123")
-
- value = parser.parse_data("ABC${func1($var_1, $var_3)}DE", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123DE")
-
- value = parser.parse_data("ABC${func1($var_1, $var_3)}$var_5", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123True")
-
- value = parser.parse_data("ABC${func1($var_1, $var_3)}DE$var_4", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123DE{'a': 1}")
-
- value = parser.parse_data("ABC$var_5${func1($var_1, $var_3)}", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCTrueabc123")
-
- value = parser.parse_data("ABC${ord(a)}DEF${len(abcd)}", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABC97DEF4")
-
- def test_parse_data_func_var_duplicate(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- functions_mapping = {
- "func1": lambda x, y: str(x) + str(y)
- }
- value = parser.parse_data(
- "ABC${func1($var_1, $var_3)}--${func1($var_1, $var_3)}",
- variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123--abc123")
-
- value = parser.parse_data("ABC${func1($var_1, $var_3)}$var_1", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123abc")
-
- value = parser.parse_data(
- "ABC${func1($var_1, $var_3)}$var_1--${func1($var_1, $var_3)}$var_1",
- variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc123abc--abc123abc")
-
- def test_parse_data_func_abnormal(self):
- variables_mapping = {
- "var_1": "abc",
- "var_2": "def",
- "var_3": 123,
- "var_4": {"a": 1},
- "var_5": True,
- "var_6": None
- }
- functions_mapping = {
- "func1": lambda x, y: str(x) + str(y)
- }
-
- # {
- value = parser.parse_data("ABC$var_1{", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc{")
-
- value = parser.parse_data("{ABC$var_1{}a}", variables_mapping, functions_mapping)
- self.assertEqual(value, "{ABCabc{}a}")
-
- value = parser.parse_data("AB{C$var_1{}a}", variables_mapping, functions_mapping)
- self.assertEqual(value, "AB{Cabc{}a}")
-
- # }
- value = parser.parse_data("ABC$var_1}", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc}")
-
- # $$
- value = parser.parse_data("ABC$$var_1{", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABC$var_1{")
-
- # $$$
- value = parser.parse_data("ABC$$$var_1{", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABC$abc{")
-
- # $$$$
- value = parser.parse_data("ABC$$$$var_1{", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABC$$var_1{")
-
- # ${
- value = parser.parse_data("ABC$var_1${", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc${")
-
- value = parser.parse_data("ABC$var_1${a", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc${a")
-
- # $}
- value = parser.parse_data("ABC$var_1$}a", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc$}a")
-
- # }{
- value = parser.parse_data("ABC$var_1}{a", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc}{a")
-
- # {}
- value = parser.parse_data("ABC$var_1{}a", variables_mapping, functions_mapping)
- self.assertEqual(value, "ABCabc{}a")
-
- def test_parse_data_request(self):
- content = {
- 'request': {
- 'url': '/api/users/$uid',
- 'method': "$method",
- 'headers': {'token': '$token'},
- 'data': {
- "null": None,
- "true": True,
- "false": False,
- "empty_str": "",
- "value": "abc${add_one(3)}def"
- }
- }
- }
- variables_mapping = {
- "uid": 1000,
- "method": "POST",
- "token": "abc123"
- }
- functions_mapping = {
- "add_one": lambda x: x + 1
- }
- result = parser.parse_data(content, variables_mapping, functions_mapping)
- self.assertEqual("/api/users/1000", result["request"]["url"])
- self.assertEqual("abc123", result["request"]["headers"]["token"])
- self.assertEqual("POST", result["request"]["method"])
- self.assertIsNone(result["request"]["data"]["null"])
- self.assertTrue(result["request"]["data"]["true"])
- self.assertFalse(result["request"]["data"]["false"])
- self.assertEqual("", result["request"]["data"]["empty_str"])
- self.assertEqual("abc4def", result["request"]["data"]["value"])
-
- def test_parse_data_testcase(self):
- variables = {
- "uid": "1000",
- "random": "A2dEx",
- "authorization": "a83de0ff8d2e896dbd8efb81ba14e17d",
- "data": {"name": "user", "password": "123456"}
- }
- functions = {
- "add_two_nums": lambda a, b=1: a + b,
- "get_timestamp": lambda: int(time.time() * 1000)
- }
- testcase_template = {
- "url": "http://127.0.0.1:5000/api/users/$uid/${add_two_nums(1,2)}",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json",
- "authorization": "$authorization",
- "random": "$random",
- "sum": "${add_two_nums(1, 2)}"
- },
- "body": "$data"
- }
- parsed_testcase = parser.parse_data(testcase_template, variables, functions)
- self.assertEqual(
- parsed_testcase["url"],
- "http://127.0.0.1:5000/api/users/1000/3"
- )
- self.assertEqual(
- parsed_testcase["headers"]["authorization"],
- variables["authorization"]
- )
- self.assertEqual(
- parsed_testcase["headers"]["random"],
- variables["random"]
- )
- self.assertEqual(
- parsed_testcase["body"],
- variables["data"]
- )
- self.assertEqual(
- parsed_testcase["headers"]["sum"],
- 3
- )
diff --git a/httprunner/v3/response.py b/httprunner/v3/response.py
deleted file mode 100644
index 01af0bd8..00000000
--- a/httprunner/v3/response.py
+++ /dev/null
@@ -1,122 +0,0 @@
-from typing import Dict, Text, Any, NoReturn
-
-import jmespath
-import requests
-from loguru import logger
-
-from httprunner.exceptions import ValidationFailure, ParamsError
-from httprunner.v3.parser import parse_data, parse_string_value, get_mapping_function
-from httprunner.v3.schema import VariablesMapping, Validators, FunctionsMapping
-from httprunner.v3.validator import uniform_validator
-
-
-class ResponseObject(object):
-
- def __init__(self, resp_obj: requests.Response):
- """ initialize with a requests.Response object
-
- Args:
- resp_obj (instance): requests.Response instance
-
- """
- self.resp_obj = resp_obj
- self.resp_obj_meta = {
- "status_code": resp_obj.status_code,
- "headers": resp_obj.headers,
- "body": resp_obj.json()
- }
- self.validation_results: Dict = {}
-
- def __getattr__(self, key):
- try:
- if key == "json":
- value = self.resp_obj.json()
- elif key == "cookies":
- value = self.resp_obj.cookies.get_dict()
- else:
- value = getattr(self.resp_obj, key)
-
- self.__dict__[key] = value
- return value
- except AttributeError:
- err_msg = f"ResponseObject does not have attribute: {key}"
- logger.error(err_msg)
- raise ParamsError(err_msg)
-
- def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
- if not extractors:
- return {}
-
- extract_mapping = {}
- for key, field in extractors.items():
- field_value = jmespath.search(field, self.resp_obj_meta)
- extract_mapping[key] = field_value
-
- logger.info(f"extract mapping: {extract_mapping}")
- return extract_mapping
-
- def validate(self,
- validators: Validators,
- variables_mapping: VariablesMapping = None,
- functions_mapping: FunctionsMapping = None) -> NoReturn:
-
- self.validation_results = {}
- if not validators:
- return
-
- validate_pass = True
- failures = []
-
- for v in validators:
-
- if "validate_extractor" not in self.validation_results:
- self.validation_results["validate_extractor"] = []
-
- u_validator = uniform_validator(v)
-
- # check item
- check_item = u_validator["check"]
- check_value = jmespath.search(check_item, self.resp_obj_meta)
- check_value = parse_string_value(check_value)
-
- # comparator
- assert_method = u_validator["assert"]
- assert_func = get_mapping_function(assert_method, functions_mapping)
-
- # expect item
- expect_item = u_validator["expect"]
- # parse expected value with config/teststep/extracted variables
- expect_value = parse_data(expect_item, variables_mapping, functions_mapping)
-
- validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})"
-
- validator_dict = {
- "comparator": assert_method,
- "check": check_item,
- "check_value": check_value,
- "expect": expect_item,
- "expect_value": expect_value
- }
-
- try:
- assert_func(check_value, expect_value)
- validate_msg += "\t==> pass"
- logger.info(validate_msg)
- validator_dict["check_result"] = "pass"
- except AssertionError:
- validate_pass = False
- validator_dict["check_result"] = "fail"
- validate_msg += "\t==> fail"
- validate_msg += f"\n" \
- f"check_item: {check_item}\n" \
- f"check_value: {check_value}({type(check_value).__name__})\n" \
- f"assert_method: {assert_method}\n" \
- f"expect_value: {expect_value}({type(expect_value).__name__})"
- logger.error(validate_msg)
- failures.append(validate_msg)
-
- self.validation_results["validate_extractor"].append(validator_dict)
-
- if not validate_pass:
- failures_string = "\n".join([failure for failure in failures])
- raise ValidationFailure(failures_string)
diff --git a/httprunner/v3/runner.py b/httprunner/v3/runner.py
deleted file mode 100644
index d2fc3da4..00000000
--- a/httprunner/v3/runner.py
+++ /dev/null
@@ -1,123 +0,0 @@
-from typing import List, Dict
-
-from loguru import logger
-
-from httprunner import utils
-from httprunner.client import HttpSession
-from httprunner.exceptions import ValidationFailure
-from httprunner.v3.parser import build_url, parse_data, parse_variables_mapping
-from httprunner.v3.response import ResponseObject
-from httprunner.v3.schema import TestsConfig, TestStep, VariablesMapping, TestCase, SessionData
-
-
-class TestCaseRunner(object):
-
- config: TestsConfig = {}
- teststeps: List[TestStep] = []
- session: HttpSession = None
- step_datas: List[SessionData] = []
- validation_results: Dict = {}
-
- def init(self, testcase: TestCase) -> "TestCaseRunner":
- self.config = testcase.config
- self.teststeps = testcase.teststeps
- return self
-
- def with_session(self, s: HttpSession) -> "TestCaseRunner":
- self.session = s
- return self
-
- def with_variables(self, **variables: VariablesMapping) -> "TestCaseRunner":
- self.config.variables.update(variables)
- return self
-
- def __run_step(self, step: TestStep):
- logger.info(f"run step: {step.name}")
-
- # parse
- request_dict = step.request.dict()
- parsed_request_dict = parse_data(request_dict, step.variables, self.config.functions)
-
- # prepare arguments
- method = parsed_request_dict.pop("method")
- url_path = parsed_request_dict.pop("url")
- url = build_url(self.config.base_url, url_path)
-
- parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
-
- logger.info(f"{method} {url}")
- logger.debug(f"request kwargs(raw): {parsed_request_dict}")
-
- # request
- self.session = self.session or HttpSession()
- resp = self.session.request(method, url, **parsed_request_dict)
- resp_obj = ResponseObject(resp)
-
- def log_req_resp_details():
- err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
-
- # log request
- err_msg += "====== request details ======\n"
- err_msg += f"url: {url}\n"
- err_msg += f"method: {method}\n"
- headers = parsed_request_dict.pop("headers", {})
- err_msg += f"headers: {headers}\n"
- for k, v in parsed_request_dict.items():
- v = utils.omit_long_data(v)
- err_msg += f"{k}: {repr(v)}\n"
-
- err_msg += "\n"
-
- # log response
- err_msg += "====== response details ======\n"
- err_msg += f"status_code: {resp_obj.status_code}\n"
- err_msg += f"headers: {resp_obj.headers}\n"
- err_msg += f"body: {repr(resp_obj.text)}\n"
- logger.error(err_msg)
-
- # extract
- extractors = step.extract
- extract_mapping = resp_obj.extract(extractors)
-
- variables_mapping = step.variables
- variables_mapping.update(extract_mapping)
-
- # validate
- validators = step.validators
- try:
- resp_obj.validate(validators, variables_mapping, self.config.functions)
- self.session.data.status = "passed"
- except ValidationFailure:
- self.session.data.status = "failed"
- log_req_resp_details()
- raise
- finally:
- self.validation_results = resp_obj.validation_results
- # save request & response meta data
- self.session.data.validators = self.validation_results
- self.session.data.name = step.name
- self.step_datas.append(self.session.data)
-
- return extract_mapping
-
- def test_start(self):
- """main entrance"""
- self.step_datas.clear()
- session_variables = {}
- for step in self.teststeps:
- # update with config variables
- step.variables.update(self.config.variables)
- # update with session variables extracted from former step
- step.variables.update(session_variables)
- # parse variables
- step.variables = parse_variables_mapping(step.variables, self.config.functions)
- # run step
- extract_mapping = self.__run_step(step)
- # save extracted variables to session variables
- session_variables.update(extract_mapping)
-
- return self
-
- def run(self):
- """main entrance alias for test_start"""
- return self.test_start()
diff --git a/httprunner/v3/validator.py b/httprunner/v3/validator.py
deleted file mode 100644
index 3df458f3..00000000
--- a/httprunner/v3/validator.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from typing import Text
-
-from httprunner.exceptions import ParamsError
-
-
-def get_uniform_comparator(comparator: Text):
- """ convert comparator alias to uniform name
- """
- if comparator in ["eq", "equals", "==", "is"]:
- return "equals"
- elif comparator in ["lt", "less_than"]:
- return "less_than"
- elif comparator in ["le", "less_than_or_equals"]:
- return "less_than_or_equals"
- elif comparator in ["gt", "greater_than"]:
- return "greater_than"
- elif comparator in ["ge", "greater_than_or_equals"]:
- return "greater_than_or_equals"
- elif comparator in ["ne", "not_equals"]:
- return "not_equals"
- elif comparator in ["str_eq", "string_equals"]:
- return "string_equals"
- elif comparator in ["len_eq", "length_equals", "count_eq"]:
- return "length_equals"
- elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]:
- return "length_greater_than"
- elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals",
- "count_greater_than_or_equals"]:
- return "length_greater_than_or_equals"
- elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
- return "length_less_than"
- elif comparator in ["len_le", "count_le", "length_less_than_or_equals",
- "count_less_than_or_equals"]:
- return "length_less_than_or_equals"
- else:
- return comparator
-
-
-def uniform_validator(validator):
- """ unify validator
-
- Args:
- validator (dict): validator maybe in two formats:
-
- format1: this is kept for compatiblity with the previous versions.
- {"check": "status_code", "assert": "eq", "expect": 201}
- {"check": "$resp_body_success", "assert": "eq", "expect": True}
- format2: recommended new version, {assert: [check_item, expected_value]}
- {'eq': ['status_code', 201]}
- {'eq': ['$resp_body_success', True]}
-
- Returns
- dict: validator info
-
- {
- "check": "status_code",
- "expect": 201,
- "assert": "equals"
- }
-
- """
- if not isinstance(validator, dict):
- raise ParamsError(f"invalid validator: {validator}")
-
- if "check" in validator and "expect" in validator:
- # format1
- check_item = validator["check"]
- expect_value = validator["expect"]
- comparator = validator.get("comparator", "eq")
-
- elif len(validator) == 1:
- # format2
- comparator = list(validator.keys())[0]
- compare_values = validator[comparator]
-
- if not isinstance(compare_values, list) or len(compare_values) != 2:
- raise ParamsError(f"invalid validator: {validator}")
-
- check_item, expect_value = compare_values
-
- else:
- raise ParamsError(f"invalid validator: {validator}")
-
- # uniform comparator, e.g. lt => less_than, eq => equals
- assert_method = get_uniform_comparator(comparator)
-
- return {
- "check": check_item,
- "expect": expect_value,
- "assert": assert_method
- }
diff --git a/httprunner/validator.py b/httprunner/validator.py
deleted file mode 100644
index 9abe5699..00000000
--- a/httprunner/validator.py
+++ /dev/null
@@ -1,204 +0,0 @@
-# encoding: utf-8
-
-import sys
-import traceback
-
-from loguru import logger
-
-from httprunner import exceptions, parser
-
-
-class Validator(object):
- """Validate tests
-
- Attributes:
- validation_results (dict): store validation results,
- including validate_extractor and validate_script.
-
- """
-
- def __init__(self, session_context, resp_obj):
- """ initialize a Validator for each teststep (API request)
-
- Args:
- session_context: HttpRunner session context
- resp_obj: ResponseObject instance
- """
- self.session_context = session_context
- self.resp_obj = resp_obj
- self.validation_results = {}
-
- def __eval_validator_check(self, check_item):
- """ evaluate check item in validator.
-
- Args:
- check_item: check_item should only be the following 5 formats:
- 1, variable reference, e.g. $token
- 2, function reference, e.g. ${is_status_code_200($status_code)}
- 3, dict or list, maybe containing variable/function reference, e.g. {"var": "$abc"}
- 4, string joined by delimiter. e.g. "status_code", "headers.content-type"
- 5, regex string, e.g. "LB[\d]*(.*)RB[\d]*"
-
- """
- if isinstance(check_item, (dict, list)) \
- or isinstance(check_item, parser.LazyString):
- # format 1/2/3
- check_value = self.session_context.eval_content(check_item)
- else:
- # format 4/5
- check_value = self.resp_obj.extract_field(check_item)
-
- return check_value
-
- def __eval_validator_expect(self, expect_item):
- """ evaluate expect item in validator.
-
- Args:
- expect_item: expect_item should only be in 2 types:
- 1, variable reference, e.g. $expect_status_code
- 2, actual value, e.g. 200
-
- """
- expect_value = self.session_context.eval_content(expect_item)
- return expect_value
-
- def validate_script(self, script):
- """ make validation with python script
- """
- result = {
- "validate_script": "
".join(script),
- "check_result": "pass",
- "output": ""
- }
-
- script = "\n ".join(script)
- code = f"""
-# encoding: utf-8
-
-def run_validate_script():
- {script}
-"""
-
- variables = {
- "status_code": self.resp_obj.status_code,
- "response_json": self.resp_obj.json,
- "response": self.resp_obj
- }
- variables.update(self.session_context.test_variables_mapping)
- variables.update(globals())
-
- try:
- exec(code, variables)
- except SyntaxError as ex:
- logger.warning(f"SyntaxError in python validate script: {ex}")
- result["check_result"] = "fail"
- result["output"] = "
".join([
- f"ErrorMessage: {ex.msg}",
- f"ErrorLine: {ex.lineno}",
- f"ErrorText: {ex.text}"
- ])
- return result
-
- try:
- # run python validate script
- variables["run_validate_script"]()
- except Exception as ex:
- logger.warning(f"run python validate script failed: {ex}")
- result["check_result"] = "fail"
-
- _type, _value, _tb = sys.exc_info()
-
- _lineno = -1
- if _tb.tb_next:
- _lineno = _tb.tb_next.tb_lineno
- line_no = _lineno - 4
- elif len(traceback.extract_tb(_tb)) > 0:
- # filename, lineno, name, line
- _, _lineno, _, _ = traceback.extract_tb(_tb)[-1]
- line_no = _lineno - 4
- else:
- line_no = "N/A"
-
- result["output"] = "
".join([
- f"ErrorType: {_type.__name__}",
- f"ErrorLine: {line_no}"
- ])
-
- return result
-
- def validate(self, validators):
- """ make validation with comparators
- """
- self.validation_results = {}
- if not validators:
- return
-
- logger.debug("start to validate.")
-
- validate_pass = True
- failures = []
-
- for validator in validators:
-
- if isinstance(validator, dict) and validator.get("type") == "python_script":
- script = self.session_context.eval_content(validator["script"])
- result = self.validate_script(script)
- if result["check_result"] == "fail":
- validate_pass = False
- failures.append(result["output"])
-
- self.validation_results["validate_script"] = result
- continue
-
- if "validate_extractor" not in self.validation_results:
- self.validation_results["validate_extractor"] = []
-
- # validator should be LazyFunction object
- if not isinstance(validator, parser.LazyFunction):
- raise exceptions.ValidationFailure(
- f"validator should be parsed first: {validators}")
-
- # evaluate validator args with context variable mapping.
- validator_args = validator.get_args()
- check_item, expect_item = validator_args
- check_value = self.__eval_validator_check(check_item)
- expect_value = self.__eval_validator_expect(expect_item)
- validator.update_args([check_value, expect_value])
-
- comparator = validator.func_name
- validator_dict = {
- "comparator": comparator,
- "check": check_item,
- "check_value": check_value,
- "expect": expect_item,
- "expect_value": expect_value
- }
- validate_msg = f"\nvalidate: {check_item} {comparator} {expect_value}({type(expect_value).__name__})"
-
- try:
- validator.to_value(self.session_context.test_variables_mapping)
- validator_dict["check_result"] = "pass"
- validate_msg += "\t==> pass"
- logger.debug(validate_msg)
- except (AssertionError, TypeError):
- validate_pass = False
- validator_dict["check_result"] = "fail"
- validate_msg += "\t==> fail"
- validate_msg += "\n{}({}) {} {}({})".format(
- check_value,
- type(check_value).__name__,
- comparator,
- expect_value,
- type(expect_value).__name__
- )
- logger.error(validate_msg)
- failures.append(validate_msg)
-
- self.validation_results["validate_extractor"].append(validator_dict)
-
- # restore validator args, in case of running multiple times
- validator.update_args(validator_args)
-
- if not validate_pass:
- failures_string = "\n".join([failure for failure in failures])
- raise exceptions.ValidationFailure(failures_string)
diff --git a/tests/test_context.py b/tests/test_context.py
deleted file mode 100644
index 46bbf343..00000000
--- a/tests/test_context.py
+++ /dev/null
@@ -1,188 +0,0 @@
-import os
-
-from httprunner import context, exceptions, loader, parser, runner
-from tests.api_server import gen_md5
-from tests.base import ApiServerUnittest, gen_random_string
-
-
-class TestContext(ApiServerUnittest):
-
- def setUp(self):
- loader.load_project_data(os.path.join(os.getcwd(), "tests"))
- self.context = context.SessionContext(
- variables={"SECRET_KEY": "DebugTalk"}
- )
-
- def test_init_test_variables_initialize(self):
- self.assertEqual(
- self.context.test_variables_mapping,
- {'SECRET_KEY': 'DebugTalk'}
- )
-
- def test_init_test_variables(self):
- variables = {
- "random": "${gen_random_string($num)}",
- "authorization": "${gen_md5($TOKEN, $data, $random)}",
- "data": "$username",
- # TODO: escape '{' and '}'
- # "data": '{"name": "$username", "password": "123456"}',
- "TOKEN": "debugtalk",
- "username": "user1",
- "num": 6
- }
- functions = {
- "gen_random_string": gen_random_string,
- "gen_md5": gen_md5
- }
- variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- variables = parser.parse_variables_mapping(variables)
- self.context.init_test_variables(variables)
- variables_mapping = self.context.test_variables_mapping
- self.assertEqual(len(variables_mapping["random"]), 6)
- self.assertEqual(len(variables_mapping["authorization"]), 32)
- self.assertEqual(variables_mapping["data"], 'user1')
-
- def test_update_seesion_variables(self):
- self.context.update_session_variables({"TOKEN": "debugtalk"})
- self.assertEqual(
- self.context.session_variables_mapping["TOKEN"],
- "debugtalk"
- )
-
- def test_eval_content_variables(self):
- variables = {
- "SECRET_KEY": "DebugTalk"
- }
- content = parser.prepare_lazy_data("abc$SECRET_KEY", {}, variables.keys())
- self.assertEqual(
- self.context.eval_content(content),
- "abcDebugTalk"
- )
-
- # TODO: fix variable extraction
- # content = "abc$SECRET_KEYdef"
- # self.assertEqual(
- # self.context.eval_content(content),
- # "abcDebugTalkdef"
- # )
-
- def test_get_parsed_request(self):
- variables = {
- "random": "${gen_random_string(5)}",
- "data": '{"name": "user", "password": "123456"}',
- "authorization": "${gen_md5($TOKEN, $data, $random)}",
- "TOKEN": "debugtalk"
- }
- functions = {
- "gen_random_string": gen_random_string,
- "gen_md5": gen_md5
- }
- variables = parser.prepare_lazy_data(variables, functions, variables.keys())
- variables = parser.parse_variables_mapping(variables)
- self.context.init_test_variables(variables)
-
- request = {
- "url": "http://127.0.0.1:5000/api/users/1000",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json",
- "authorization": "$authorization",
- "random": "$random",
- "secret_key": "$SECRET_KEY"
- },
- "data": "$data"
- }
- prepared_request = parser.prepare_lazy_data(
- request,
- functions,
- {"authorization", "random", "SECRET_KEY", "data"}
- )
- parsed_request = self.context.eval_content(prepared_request)
- self.assertIn("authorization", parsed_request["headers"])
- self.assertEqual(len(parsed_request["headers"]["authorization"]), 32)
- self.assertIn("random", parsed_request["headers"])
- self.assertEqual(len(parsed_request["headers"]["random"]), 5)
- self.assertIn("data", parsed_request)
- self.assertEqual(
- parsed_request["data"],
- '{"name": "user", "password": "123456"}'
- )
- self.assertEqual(parsed_request["headers"]["secret_key"], "DebugTalk")
-
- def test_validate(self):
- testcases = [
- {
- "config": {
- 'name': "test validation"
- },
- "teststeps": [
- {
- "name": "test validation",
- "request": {
- "url": "http://127.0.0.1:5000/",
- "method": "GET",
- },
- "variables": {
- "resp_status_code": 200,
- "resp_body_success": True
- },
- "validate": [
- {"eq": ["$resp_status_code", 200]},
- {"check": "$resp_status_code", "comparator": "eq", "expect": 200},
- {"check": "$resp_body_success", "expect": True},
- {"check": "${is_status_code_200($resp_status_code)}", "expect": True}
- ]
- }
- ]
- }
- ]
- from tests.debugtalk import is_status_code_200
- tests_mapping = {
- "project_mapping": {
- "functions": {
- "is_status_code_200": is_status_code_200
- }
- },
- "testcases": testcases
- }
- testcases = parser.parse_tests(tests_mapping)
- parsed_testcase = testcases[0]
- test_runner = runner.Runner(parsed_testcase["config"])
- teststep = parsed_testcase["teststeps"][0]
- test_runner.run_test(teststep)
-
- def test_validate_exception(self):
- testcases = [
- {
- "config": {
- 'name': "test validation"
- },
- "teststeps": [
- {
- "name": "test validation",
- "request": {
- "url": "http://127.0.0.1:5000/",
- "method": "GET",
- },
- "variables": {
- "resp_status_code": 200,
- "resp_body_success": True
- },
- "validate": [
- {"eq": ["$resp_status_code", 201]},
- {"check": "$resp_status_code", "expect": 201},
- {"check": "$resp_body_success", "comparator": "eq", "expect": True}
- ]
- }
- ]
- }
- ]
- tests_mapping = {
- "testcases": testcases
- }
- testcases = parser.parse_tests(tests_mapping)
- parsed_testcase = testcases[0]
- test_runner = runner.Runner(parsed_testcase["config"])
- teststep = parsed_testcase["teststeps"][0]
- with self.assertRaises(exceptions.ValidationFailure):
- test_runner.run_test(teststep)