From 826fca83c2116c4ce32727a7e76645e26385f46f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 23 Apr 2020 21:10:59 +0800 Subject: [PATCH] refactor: merge v3 --- .../request_methods/hardcode_test.py | 4 +- .../request_with_functions.yml | 2 +- .../request_with_functions_test.py | 4 +- .../request_with_variables_test.py | 4 +- .../validate_with_functions_test.py | 4 +- .../validate_with_variables_test.py | 4 +- httprunner/api.py | 264 ++- httprunner/{v3 => }/api_test.py | 2 +- httprunner/cli.py | 2 +- httprunner/client.py | 2 +- httprunner/context.py | 66 - httprunner/loader/__init__.py | 8 +- httprunner/loader/buildup.py | 4 - httprunner/loader/check.py | 113 -- httprunner/loader/check_test.py | 45 - httprunner/parser.py | 1578 +++-------------- httprunner/parser_test.py | 1574 ++++------------ httprunner/report/html/gen_report.py | 2 +- httprunner/report/stringify.py | 3 +- httprunner/report/summarize.py | 2 +- httprunner/response.py | 440 ++--- httprunner/runner.py | 440 +---- httprunner/{v3 => }/schema.py | 0 httprunner/schema/__init__.py | 3 - httprunner/schema/api.py | 16 - httprunner/schema/common.py | 60 - httprunner/schema/testcase.py | 83 - httprunner/schema/testsuite.py | 17 - httprunner/utils.py | 182 -- httprunner/utils_test.py | 96 +- httprunner/v3/__init__.py | 0 httprunner/v3/api.py | 287 --- httprunner/v3/parser.py | 421 ----- httprunner/v3/parser_test.py | 528 ------ httprunner/v3/response.py | 122 -- httprunner/v3/runner.py | 123 -- httprunner/v3/validator.py | 91 - httprunner/validator.py | 204 --- tests/test_context.py | 188 -- 39 files changed, 967 insertions(+), 6021 deletions(-) rename httprunner/{v3 => }/api_test.py (91%) delete mode 100644 httprunner/context.py delete mode 100644 httprunner/loader/check_test.py rename httprunner/{v3 => }/schema.py (100%) delete mode 100644 httprunner/schema/__init__.py delete mode 100644 httprunner/schema/api.py delete mode 100644 httprunner/schema/common.py delete mode 100644 httprunner/schema/testcase.py delete mode 100644 httprunner/schema/testsuite.py delete mode 100644 httprunner/v3/__init__.py delete mode 100644 httprunner/v3/api.py delete mode 100644 httprunner/v3/parser.py delete mode 100644 httprunner/v3/parser_test.py delete mode 100644 httprunner/v3/response.py delete mode 100644 httprunner/v3/runner.py delete mode 100644 httprunner/v3/validator.py delete mode 100644 httprunner/validator.py delete mode 100644 tests/test_context.py 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)