import importlib import os from loguru import logger from httprunner import exceptions, utils 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 tests_def_mapping = { "api": {}, "testcases": {} } def load_debugtalk_functions(): """ load project debugtalk.py module functions debugtalk.py should be located in project working directory. Returns: dict: debugtalk module functions mapping { "func1_name": func1, "func2_name": func2 } """ # load debugtalk.py module imported_module = importlib.import_module("debugtalk") return load_module_functions(imported_module) def __extend_with_api_ref(raw_testinfo): """ extend with api reference Raises: exceptions.ApiNotFound: api not found """ api_name = raw_testinfo["api"] # api maybe defined in two types: # 1, individual file: each file is corresponding to one api definition # 2, api sets file: one file contains a list of api definitions if not os.path.isabs(api_name): # make compatible with Windows/Linux pwd = get_project_working_directory() api_path = os.path.join(pwd, *api_name.split("/")) if os.path.isfile(api_path): # type 1: api is defined in individual file api_name = api_path if api_name in tests_def_mapping["api"]: block = tests_def_mapping["api"][api_name] elif not os.path.isfile(api_name): raise exceptions.ApiNotFound(f"{api_name} not found!") else: block = load_file(api_name) # NOTICE: avoid project_meta been changed during iteration. raw_testinfo["api_def"] = utils.deepcopy_dict(block) tests_def_mapping["api"][api_name] = block def __extend_with_testcase_ref(raw_testinfo): """ extend with testcase reference """ testcase_path = raw_testinfo["testcase"] if testcase_path not in tests_def_mapping["testcases"]: # make compatible with Windows/Linux pwd = get_project_working_directory() testcase_path = os.path.join( pwd, *testcase_path.split("/") ) loaded_testcase = load_file(testcase_path) # TODO: validate with pydantic if isinstance(loaded_testcase, dict) and "teststeps" in loaded_testcase: testcase_dict = load_testcase(loaded_testcase) else: raise exceptions.FileFormatError( f"Invalid format testcase: {testcase_path}") tests_def_mapping["testcases"][testcase_path] = testcase_dict else: testcase_dict = tests_def_mapping["testcases"][testcase_path] raw_testinfo["testcase_def"] = testcase_dict def load_teststep(raw_testinfo): """ load testcase step content. teststep maybe defined directly, or reference api/testcase. Args: raw_testinfo (dict): test data, maybe in 3 formats. # api reference { "name": "add product to cart", "api": "/path/to/api", "variables": {}, "validate": [], "extract": {} } # testcase reference { "name": "add product to cart", "testcase": "/path/to/testcase", "variables": {} } # define directly { "name": "checkout cart", "request": {}, "variables": {}, "validate": [], "extract": {} } Returns: dict: loaded teststep content """ # reference api if "api" in raw_testinfo: __extend_with_api_ref(raw_testinfo) # TODO: reference proc functions # elif "func" in raw_testinfo: # pass # reference testcase elif "testcase" in raw_testinfo: __extend_with_testcase_ref(raw_testinfo) # define directly else: pass return raw_testinfo def load_testcase(raw_testcase): """ load testcase. Args: raw_testcase (dict): raw testcase content loaded from JSON/YAML file: { "config": { "name": "xxx", "variables": {} } "teststeps": [ { "name": "teststep 1", "request" {...} }, { "name": "teststep 2", "request" {...} }, ] } Returns: dict: loaded testcase content { "config": {}, "teststeps": [test11, test12] } """ raw_teststeps = raw_testcase.pop("teststeps") raw_testcase["teststeps"] = [ load_teststep(teststep) for teststep in raw_teststeps ] return raw_testcase def load_testsuite(raw_testsuite): """ load testsuite with testcase references. support two different formats. Args: raw_testsuite (dict): raw testsuite content loaded from JSON/YAML file: { "config": { "name": "xxx", "variables": {} } "testcases": [ { "name": "testcase1", "testcase": "/path/to/testcase", "variables": {...}, "parameters": {...} }, {} ] } Returns: dict: loaded testsuite content { "config": {}, "testcases": [testcase1, testcase2] } """ raw_testcases = raw_testsuite["testcases"] # TODO: validate with pydantic if not isinstance(raw_testcases, list): # invalid format raise exceptions.FileFormatError("Invalid testsuite format!") raw_testsuite["testcases"] = {} for raw_testcase in raw_testcases: __extend_with_testcase_ref(raw_testcase) testcase_name = raw_testcase["name"] raw_testsuite["testcases"][testcase_name] = raw_testcase return raw_testsuite def load_test_file(path: str) -> dict: """ load test file, file maybe testcase/testsuite/api Args: path (str): test file path Returns: dict: loaded test content # api { "path": path, "type": "api", "name": "", "request": {} } # testcase { "path": path, "type": "testcase", "config": {}, "teststeps": [] } # testsuite { "path": path, "type": "testsuite", "config": {}, "testcases": {} } """ raw_content = load_file(path) if not isinstance(raw_content, dict): # invalid format raise exceptions.FileFormatError("Invalid test file format!") if "testcases" in raw_content: # file_type: testsuite loaded_content = load_testsuite(raw_content) loaded_content["path"] = path loaded_content["type"] = "testsuite" elif "teststeps" in raw_content: # file_type: testcase loaded_content = load_testcase(raw_content) loaded_content["path"] = path loaded_content["type"] = "testcase" elif "request" in raw_content: # file_type: api loaded_content = raw_content loaded_content["path"] = path loaded_content["type"] = "api" else: # invalid format raise exceptions.FileFormatError("Invalid test file format!") return loaded_content def load_project_data(test_path, dot_env_path=None): """ load api, testcases, .env, debugtalk.py functions. api/testcases folder is relative to project_working_directory Args: test_path (str): test file/folder path, locate pwd from this path. dot_env_path (str): specified .env file path Returns: dict: project loaded api/testcases definitions, environments and debugtalk.py functions. """ debugtalk_path, project_working_directory = init_project_working_directory(test_path) project_meta = {} # load .env file # NOTICE: # environment variable maybe loaded in debugtalk.py # thus .env file should be loaded before loading debugtalk.py dot_env_path = dot_env_path or os.path.join(project_working_directory, ".env") project_meta["env"] = load_dot_env_file(dot_env_path) if debugtalk_path: # load debugtalk.py functions debugtalk_functions = load_debugtalk_functions() else: debugtalk_functions = {} # locate PWD and load debugtalk.py functions project_meta["PWD"] = project_working_directory project_meta["functions"] = debugtalk_functions project_meta["test_path"] = os.path.abspath(test_path)[len(project_working_directory) + 1:] return project_meta def load_cases(path: str, dot_env_path: str = None): """ load testcases from file path, extend and merge with api/testcase definitions. Args: path (str): testcase/testsuite file/foler path. path could be in 2 types: - absolute/relative file path - absolute/relative folder path dot_env_path (str): specified .env file path Returns: dict: tests mapping, include project_meta and testcases. each testcase is corresponding to a file. { "project_meta": { "PWD": "XXXXX", "functions": {}, "env": {} }, "testcases": [ { # testcase data structure "config": { "name": "desc1", "path": "testcase1_path", "variables": [], # optional }, "teststeps": [ # test data structure { 'name': 'test desc1', 'variables': [], # optional 'extract': [], # optional 'validate': [], 'request': {} }, test_dict_2 # another test dict ] }, testcase_2_dict # another testcase dict ], "testsuites": [ { # testsuite data structure "config": {}, "testcases": { "testcase1": {}, "testcase2": {}, } }, testsuite_2_dict ] } """ tests_mapping = { "project_meta": load_project_data(path, dot_env_path) } def __load_file_content(path): loaded_content = None try: loaded_content = load_test_file(path) except exceptions.ApiNotFound as ex: logger.warning(f"Invalid api reference in {path}: {ex}") except exceptions.FileFormatError: logger.warning(f"Invalid test file format: {path}") if not loaded_content: pass elif loaded_content["type"] == "testsuite": tests_mapping.setdefault("testsuites", []).append(loaded_content) elif loaded_content["type"] == "testcase": tests_mapping.setdefault("testcases", []).append(loaded_content) elif loaded_content["type"] == "api": tests_mapping.setdefault("apis", []).append(loaded_content) if os.path.isdir(path): files_list = load_folder_files(path) for path in files_list: __load_file_content(path) elif os.path.isfile(path): __load_file_content(path) return tests_mapping