def convert_variables(raw_variables: Union[Dict, List, Text], test_path: Text) -> Dict[Text, Any]: if isinstance(raw_variables, Dict): return raw_variables if isinstance(raw_variables, List): # [{"var1": 1}, {"var2": 2}] variables: Dict[Text, Any] = {} for var_item in raw_variables: if not isinstance(var_item, Dict) or len(var_item) != 1: raise exceptions.TestCaseFormatError( f"Invalid variables format: {raw_variables}") variables.update(var_item) return variables elif isinstance(raw_variables, Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(test_path) variables = parse_data(raw_variables, {}, project_meta.functions) return variables else: raise exceptions.TestCaseFormatError( f"Invalid variables format: {raw_variables}")
def test_start(self) -> "HttpRunner": """main entrance, discovered by pytest""" self.__init_tests__() self.__project_meta = self.__project_meta or load_project_meta( self.__config.path) self.__case_id = self.__case_id or str(uuid.uuid4()) self.__log_path = self.__log_path or os.path.join( self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log") log_handler = logger.add(self.__log_path, level="DEBUG") # parse config name config_variables = self.__config.variables config_variables.update(self.__session_variables) self.__config.name = parse_data(self.__config.name, config_variables, self.__project_meta.functions) if USE_ALLURE: # update allure report meta allure.dynamic.title(self.__config.name) allure.dynamic.description(f"TestCase ID: {self.__case_id}") logger.info( f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}" ) try: return self.run_testcase( TestCase(config=self.__config, teststeps=self.__teststeps)) finally: logger.remove(log_handler) logger.info(f"generate testcase log: {self.__log_path}")
def __ensure_absolute(path: Text) -> Text: logger.info(f"[__ensure_absolute] in path={path}") if path.startswith("./"): # Linux/Darwin, hrun ./test.yml path = path[len("./"):] elif path.startswith(".\\"): # Windows, hrun .\\test.yml path = path[len(".\\"):] logger.debug(f"[__ensure_absolute] befor ensure_path_sep, path={path}") path = ensure_path_sep(path) project_meta = load_project_meta(path) logger.debug(f"[__ensure_absolute] path={path}") logger.debug(f"[__ensure_absolute] project_meta={project_meta}") logger.debug(f"[__ensure_absolute] type project_meta={type(project_meta)}") if os.path.isabs(path): absolute_path = path else: absolute_path = os.path.join(project_meta.RootDir, path) logger.debug(f"[__ensure_absolute] absolute_path={absolute_path}") if not os.path.isfile(absolute_path): logger.error(f"Invalid testcase file path: {absolute_path}") move_pytest_files_to_target(project_meta.RootDir) sys.exit(1) return absolute_path
def run(self, testcase: TestCase): """run testcase""" self.config = testcase.config self.teststeps = testcase.teststeps # prepare self.__project_meta = self.__project_meta or load_project_meta( self.config.path) self.__parse_config(self.config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() self.__session_variables = {} # run teststeps for step in self.teststeps: # update with config variables step.variables.update(self.config.variables) # update with session variables extracted from pre step step.variables.update(self.__session_variables) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions) # run step with allure.step(f"step: {step.name}"): extract_mapping = self.__run_step(step) # save extracted variables to session variables self.__session_variables.update(extract_mapping) self.__duration = time.time() - self.__start_at return self
def __ensure_absolute(path: Text) -> Text: project_meta = load_project_meta(path) if os.path.isabs(path): absolute_path = path else: absolute_path = os.path.join(project_meta.PWD, path) return absolute_path
def make_testsuite(testsuite: Dict) -> List[Text]: """convert valid testsuite dict to pytest folder with testcases""" try: # validate testcase format load_testsuite(testsuite) except exceptions.TestSuiteFormatError as ex: logger.error(f"TestSuiteFormatError: {ex}") raise config = testsuite["config"] testsuite_path = config["path"] project_meta = load_project_meta(testsuite_path) project_working_directory = project_meta.PWD testsuite_variables = config.get("variables", {}) if isinstance(testsuite_variables, Text): # get variables by function, e.g. ${get_variables()} testsuite_variables = parse_data( testsuite_variables, {}, project_meta.functions ) logger.info(f"start to make testsuite: {testsuite_path}") # create directory with testsuite file name, put its testcases under this directory testsuite_dir = testsuite_path.replace(".", "_") os.makedirs(testsuite_dir, exist_ok=True) testcase_files = [] for testcase in testsuite["testcases"]: # get referenced testcase content testcase_file = testcase["testcase"] testcase_path = os.path.join(project_working_directory, testcase_file) testcase_dict = load_test_file(testcase_path) testcase_dict.setdefault("config", {}) testcase_dict["config"]["path"] = os.path.join( testsuite_dir, os.path.basename(testcase_path) ) # override testcase name testcase_dict["config"]["name"] = testcase["name"] # override base_url base_url = testsuite["config"].get("base_url") or testcase.get("base_url") if base_url: testcase_dict["config"]["base_url"] = base_url # override variables testcase_dict["config"].setdefault("variables", {}) testcase_dict["config"]["variables"].update(testcase.get("variables", {})) testcase_dict["config"]["variables"].update(testsuite_variables) # make testcase testcase_path = make_testcase(testcase_dict) testcase_files.append(testcase_path) return testcase_files
def __ensure_absolute(path: Text) -> Text: project_meta = load_project_meta(path) if os.path.isabs(path): absolute_path = path else: absolute_path = os.path.join(project_meta.RootDir, path) if not os.path.isfile(absolute_path): raise exceptions.ParamsError( f"Invalid testcase file path: {absolute_path}") return absolute_path
def run_testcase(self, testcase: TestCase) -> "HttpRunner": """run specified testcase Examples: >>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)]) >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) """ self.__config = testcase.config self.__teststeps = testcase.teststeps # prepare self.__project_meta = self.__project_meta or load_project_meta( self.__config.path ) self.__parse_config(self.__config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() self.__session_variables = {} # run teststeps for step in self.__teststeps: # override variables # session variables (extracted from pre step) > step variables step.variables.update(self.__session_variables) # step variables > testcase config variables step.variables = override_config_variables( step.variables, self.__config.variables ) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions ) # run step if USE_ALLURE: with allure.step(f"step: {step.name}"): extract_mapping = self.__run_step(step) else: extract_mapping = self.__run_step(step) # save extracted variables to session variables self.__session_variables.update(extract_mapping) self.__duration = time.time() - self.__start_at return self
def convert_variables(raw_variables: Union[Dict, Text], test_path: Text) -> Dict[Text, Any]: if isinstance(raw_variables, Dict): return raw_variables elif isinstance(raw_variables, Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(test_path) variables = parse_data(raw_variables, {}, project_meta.functions) return variables else: raise exceptions.TestCaseFormatError( f"Invalid variables format: {raw_variables}")
def __make_testsuite(testsuite: Dict) -> NoReturn: """convert valid testsuite dict to pytest folder with testcases""" # validate testsuite format load_testsuite(testsuite) config = testsuite["config"] testsuite_path = config["path"] testsuite_variables = config.get("variables", {}) if isinstance(testsuite_variables, Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(testsuite_path) testsuite_variables = parse_data(testsuite_variables, {}, project_meta.functions) logger.info(f"start to make testsuite: {testsuite_path}") # create directory with testsuite file name, put its testcases under this directory testsuite_dir = os.path.join( os.path.dirname(testsuite_path), os.path.basename(testsuite_path).replace(".", "_"), ) os.makedirs(testsuite_dir, exist_ok=True) for testcase in testsuite["testcases"]: # get referenced testcase content testcase_file = testcase["testcase"] testcase_path = __ensure_absolute(testcase_file) testcase_dict = load_test_file(testcase_path) testcase_dict.setdefault("config", {}) testcase_dict["config"]["path"] = testcase_path # override testcase name testcase_dict["config"]["name"] = testcase["name"] # override base_url base_url = testsuite["config"].get("base_url") or testcase.get( "base_url") if base_url: testcase_dict["config"]["base_url"] = base_url # override variables testcase_dict["config"].setdefault("variables", {}) testcase_dict["config"]["variables"].update( testcase.get("variables", {})) testcase_dict["config"]["variables"].update(testsuite_variables) # make testcase __make_testcase(testcase_dict, testsuite_dir)
def __init(self): init_logger() self.__config = self.config.struct() self.__session_variables = {} self.__start_at = 0 self.__duration = 0 self.__project_meta = self.__project_meta or load_project_meta( self.__config.path) self.case_id = self.case_id or str(uuid.uuid4()) self.root_dir = self.root_dir or self.__project_meta.RootDir self.__log_path = os.path.join(self.root_dir, "logs", f"{self.case_id}.run.log") self.__step_results.clear() self.session = self.session or HttpSession() self.parser = self.parser or Parser(self.__project_meta.functions)
def multipart_encoder(**kwargs): """initialize MultipartEncoder with uploading fields. Returns: MultipartEncoder: initialized MultipartEncoder object """ def get_filetype(file_path): file_type = filetype.guess(file_path) if file_type: return file_type.mime else: return "text/html" ensure_upload_ready() fields_dict = {} for key, value in kwargs.items(): if os.path.isabs(value): # value is absolute file path _file_path = value is_exists_file = os.path.isfile(value) else: # value is not absolute file path, check if it is relative file path from httprunner.loader import load_project_meta project_meta = load_project_meta("") _file_path = os.path.join(project_meta.RootDir, value) is_exists_file = os.path.isfile(_file_path) if is_exists_file: # value is file path to upload filename = os.path.basename(_file_path) mime_type = get_filetype(_file_path) # TODO: fix ResourceWarning for unclosed file file_handler = open(_file_path, "rb") fields_dict[key] = (filename, file_handler, mime_type) else: fields_dict[key] = value return MultipartEncoder(fields=fields_dict)
def __ensure_absolute(path: Text) -> Text: if path.startswith("./"): # Linux/Darwin, hrun ./test.yml path = path[len("./"):] elif path.startswith(".\\"): # Windows, hrun .\\test.yml path = path[len(".\\"):] path = ensure_path_sep(path) project_meta = load_project_meta(path) if os.path.isabs(path): absolute_path = path else: absolute_path = os.path.join(project_meta.RootDir, path) if not os.path.isfile(absolute_path): logger.error(f"Invalid testcase file path: {absolute_path}") sys.exit(1) return absolute_path
def make_testcase(testcase: Dict) -> Union[str, None]: """convert valid testcase dict to pytest file path""" try: # validate testcase format load_testcase(testcase) except exceptions.TestCaseFormatError as ex: logger.error(f"TestCaseFormatError: {ex}") raise testcase_path = testcase["config"]["path"] logger.info(f"start to make testcase: {testcase_path}") template = jinja2.Template(__TMPL__) testcase_python_path, name_in_title_case = convert_testcase_path(testcase_path) config = testcase["config"] config.setdefault("variables", {}) if isinstance(config["variables"], Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(testcase_path) config["variables"] = parse_data( config["variables"], {}, project_meta.functions ) config["path"] = testcase_python_path data = { "testcase_path": testcase_path, "class_name": f"TestCase{name_in_title_case}", "config": config, "teststeps": testcase["teststeps"], } content = template.render(data) with open(testcase_python_path, "w") as f: f.write(content) logger.info(f"generated testcase: {testcase_python_path}") return testcase_python_path
def ensure_file_abs_path_valid(file_abs_path: Text) -> Text: """ ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space Args: file_abs_path: absolute file path Returns: ensured valid absolute file path """ project_meta = load_project_meta(file_abs_path) raw_abs_file_name, file_suffix = os.path.splitext(file_abs_path) file_suffix = file_suffix.lower() raw_file_relative_name = convert_relative_project_root_dir( raw_abs_file_name) if raw_file_relative_name == "": return file_abs_path path_names = [] for name in raw_file_relative_name.rstrip(os.sep).split(os.sep): if name[0] in string.digits: # ensure file name not startswith digit # 19 => T19, 2C => T2C name = f"T{name}" if name.startswith("."): # avoid ".csv" been converted to "_csv" pass else: # handle cases when directory name includes dot/hyphen/space name = name.replace(" ", "_").replace(".", "_").replace("-", "_") path_names.append(name) new_file_path = os.path.join(project_meta.RootDir, f"{os.sep.join(path_names)}{file_suffix}") return new_file_path
def run(self, testcase: TestCase): """main entrance""" self.config = testcase.config self.teststeps = testcase.teststeps self.config.variables.update(self.__session_variables) if self.config.path: self.__project_meta = load_project_meta(self.config.path) elif not self.__project_meta: self.__project_meta = ProjectMeta() def parse_config(config: TConfig): config.variables = parse_variables_mapping( config.variables, self.__project_meta.functions) config.name = parse_data(config.name, config.variables, self.__project_meta.functions) config.base_url = parse_data(config.base_url, config.variables, self.__project_meta.functions) parse_config(self.config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session_variables = {} for step in self.teststeps: # update with config variables step.variables.update(self.config.variables) # update with session variables extracted from pre step step.variables.update(self.__session_variables) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions) # run step extract_mapping = self.__run_step(step) # save extracted variables to session variables self.__session_variables.update(extract_mapping) self.__duration = time.time() - self.__start_at return self
def __ensure_project_meta_files(tests_path: Text) -> NoReturn: """ ensure project meta files exist in generated pytest folder files include debugtalk.py and .env """ project_meta = load_project_meta(tests_path) # handle cases when generated pytest directory are different from original yaml/json testcases debugtalk_path = project_meta.debugtalk_path if debugtalk_path: debugtalk_new_path = ensure_file_path_valid(debugtalk_path) if debugtalk_new_path != debugtalk_path: logger.info(f"copy debugtalk.py to {debugtalk_new_path}") copyfile(debugtalk_path, debugtalk_new_path) global pytest_files_made_cache_mapping pytest_files_made_cache_mapping[debugtalk_new_path] = "" dot_csv_path = project_meta.dot_env_path if dot_csv_path: dot_csv_new_path = ensure_file_path_valid(dot_csv_path) if dot_csv_new_path != dot_csv_path: logger.info(f"copy .env to {dot_csv_new_path}") copyfile(dot_csv_path, dot_csv_new_path)
def __init__(self, testdata): ''' 根据用户在django系统操作的选中数据使用httprunner发送请求数据、进行前置后置处理,根据断言校验响应报文等 :param testdata: 单条测试用例数据 ''' Saver.caseno = testdata['caseno'] # 请求的基本配置 self.config = (Config(testdata['caseno'] + '-' + testdata['casename']).variables(**{}).base_url( testdata['baseurl']).verify(False)) # 使用对应项目的debugtalk文件 self.with_project_meta( load_project_meta(filedir + '/projectdata/' + testdata['project'] + '/')) self.teststeps = [] # 根据输入值构建请求信息 running = RunRequest(testdata['casename'])\ .with_variables() \ .setup_hook('${prerequest($request)}') \ .setup_hook(testdata['setupfunc']) \ .__getattribute__(testdata['method'].lower())(testdata['url']) \ .with_json(testdata['data'])\ .with_data(testdata['formdata'])\ .with_params(**testdata['params'])\ .with_headers(**testdata['headers']) \ .teardown_hook('${afterresponse($response)}') \ .teardown_hook(testdata['teardownfunc']) \ .validate() \ # 根据校验参数输入值构建断言 for ast in testdata['asserts']: running = running.__getattribute__(ast[0].lower())(ast[1], ast[2]) self.teststeps.append(Step(running))
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": "******"}, {"app_version": "${gen_app_version()}"} ] >>> parse_parameters(parameters) """ variables_mapping = variables_mapping or {} functions_mapping = functions_mapping or {} parsed_parameters_list = [] # load project_meta functions from httprunner.loader import load_project_meta project_meta = load_project_meta("") functions_mapping.update(project_meta.functions) parameters = utils.ensure_mapping_format(parameters) for parameter_name, parameter_content in parameters.items(): parameter_name_list = parameter_name.split("-") 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": "******", "password": "******"}, {"username": "******", "password": "******"}] 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] # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"} # ["username", "password"], ["user1", "111111"] => {"username": "******", "password": "******"} 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, functions_mapping) parsed_parameter_content = parse_data(parameter_content, parsed_variables_mapping, functions_mapping) if not isinstance(parsed_parameter_content, list): raise exceptions.ParamsError( f"{parsed_parameter_content} 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() => [ # {"username": "******", "password": "******"}, # {"username": "******", "password": "******"} # ] parameter_dict = { key: parameter_item[key] for key in parameter_name_list } # elif isinstance(parameter_item, (list, tuple)): # # {"username-password": "******"} # # 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)
def run_testcase(self, testcase: TestCase) -> "HttpRunner": """run specified testcase Examples: >>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)]) >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) """ self.__config = testcase.config self.__teststeps = testcase.teststeps # prepare self.__project_meta = self.__project_meta or load_project_meta( self.__config.path) self.__parse_config(self.__config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() # save extracted variables of teststeps extracted_variables: VariablesMapping = {} # run teststeps for step in self.__teststeps: # override variables # step variables > extracted variables from previous steps step.variables = merge_variables(step.variables, extracted_variables) # step variables > testcase config variables step.variables = merge_variables(step.variables, self.__config.variables) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions) while True: # run step if USE_ALLURE: with allure.step(f"step: {step.name}"): extract_mapping = self.__run_step(step) else: extract_mapping = self.__run_step(step) if step.retry_whens: variables_mapping = step.variables variables_mapping.update(extract_mapping) try: response = step.variables.get( 'response') or ResponseObject(requests.Response()) if isinstance(response, ResponseObject): response.validate(step.retry_whens, variables_mapping, self.__project_meta.functions) else: break except ValidationFailure: break else: break # save extracted variables to session variables extracted_variables.update(extract_mapping) self.__session_variables.update(extracted_variables) self.__duration = time.time() - self.__start_at return self
def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 testcase = ensure_testcase_v3(testcase) # validate testcase format load_testcase(testcase) testcase_path = __ensure_absolute(testcase["config"]["path"]) logger.info(f"start to make testcase: {testcase_path}") testcase_python_path, testcase_cls_name = convert_testcase_path( testcase_path) if dir_path: testcase_python_path = os.path.join( dir_path, os.path.basename(testcase_python_path)) global make_files_cache_set if testcase_python_path in make_files_cache_set: return config = testcase["config"] config["path"] = __ensure_cwd_relative(testcase_python_path) # parse config variables config.setdefault("variables", {}) if isinstance(config["variables"], Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(testcase_path) config["variables"] = parse_data(config["variables"], {}, project_meta.functions) # prepare reference testcase imports_list = [] teststeps = testcase["teststeps"] for teststep in teststeps: if not teststep.get("testcase"): continue # make ref testcase pytest file ref_testcase_path = __ensure_absolute(teststep["testcase"]) __make(ref_testcase_path) # prepare ref testcase class name ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path( ref_testcase_path) teststep["testcase"] = f"CLS_LB({ref_testcase_cls_name})CLS_RB" # prepare import ref testcase ref_testcase_python_path = ref_testcase_python_path[len(os.getcwd()) + 1:] ref_module_name, _ = os.path.splitext(ref_testcase_python_path) ref_module_name = ref_module_name.replace(os.sep, ".") imports_list.append( f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}" ) data = { "testcase_path": __ensure_cwd_relative(testcase_path), "class_name": f"TestCase{testcase_cls_name}", "config": config, "teststeps": teststeps, "imports_list": imports_list, } content = __TEMPLATE__.render(data) content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "") with open(testcase_python_path, "w", encoding="utf-8") as f: f.write(content) __ensure_testcase_module(testcase_python_path) logger.info(f"generated testcase: {testcase_python_path}") make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path))
def run_testcase(self, testcase: TestCase) -> "HttpRunner": """run specified testcase Examples: >>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)]) >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) """ self.__config = testcase.config self.__teststeps = testcase.teststeps # prepare self.__project_meta = self.__project_meta or load_project_meta( self.__config.path) self.__parse_config(self.__config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() # save extracted variables of teststeps extracted_variables: VariablesMapping = {} # run teststeps for step in self.__teststeps: # override variables # step variables > extracted variables from previous steps step.variables = merge_variables(step.variables, extracted_variables) # step variables > testcase config variables step.variables = merge_variables(step.variables, self.__config.variables) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions) step.skipif = parse_data(step.skipif, step.variables, self.__project_meta.functions) # 跳过满足条件的步骤 logger.debug(f"[跳过步骤] skipif={step.skipif}") if step.skipif == '': continue if step.skipif and eval(step.skipif): logger.debug( f"[满足条件,跳过步骤] skipif={step.skipif} | step_name={step.name}" ) continue # run step if USE_ALLURE: with allure.step(f"step: {step.name}"): extract_mapping = self.__run_step(step) else: extract_mapping = self.__run_step(step) # 每运行一个步骤就重新加载公共变量 logger.debug(f"step.variables={step.variables}") step.variables.clear() # save extracted variables to session variables extracted_variables.update(extract_mapping) self.__session_variables.update(extracted_variables) self.__duration = time.time() - self.__start_at return self
def generate_conftest_for_summary(args: List): for arg in args: if os.path.exists(arg): test_path = arg # FIXME: several test paths maybe specified break else: logger.error(f"No valid test path specified! \nargs: {args}") sys.exit(1) project_meta = load_project_meta(test_path) project_root_dir = ensure_file_path_valid(project_meta.RootDir) conftest_path = os.path.join(project_root_dir, "conftest.py") conftest_content = '''# NOTICE: Generated By HttpRunner. import json import os import time import pytest from loguru import logger from httprunner.utils import get_platform, ExtendJSONEncoder @pytest.fixture(scope="session", autouse=True) def session_fixture(request): """setup and teardown each task""" logger.info(f"start running testcases ...") start_at = time.time() yield logger.info(f"task finished, generate task summary for --save-tests") summary = { "success": True, "stat": { "testcases": {"total": 0, "success": 0, "fail": 0}, "teststeps": {"total": 0, "failures": 0, "successes": 0}, }, "time": {"start_at": start_at, "duration": time.time() - start_at}, "platform": get_platform(), "details": [], } for item in request.node.items: testcase_summary = item.instance.get_summary() summary["success"] &= testcase_summary.success summary["stat"]["testcases"]["total"] += 1 summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) if testcase_summary.success: summary["stat"]["testcases"]["success"] += 1 summary["stat"]["teststeps"]["successes"] += len( testcase_summary.step_datas ) else: summary["stat"]["testcases"]["fail"] += 1 summary["stat"]["teststeps"]["successes"] += ( len(testcase_summary.step_datas) - 1 ) summary["stat"]["teststeps"]["failures"] += 1 testcase_summary_json = testcase_summary.dict() testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") summary["details"].append(testcase_summary_json) summary_path = "{{SUMMARY_PATH_PLACEHOLDER}}" summary_dir = os.path.dirname(summary_path) os.makedirs(summary_dir, exist_ok=True) with open(summary_path, "w", encoding="utf-8") as f: json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder) logger.info(f"generated task summary: {summary_path}") ''' test_path = os.path.abspath(test_path) logs_dir_path = os.path.join(project_root_dir, "logs") test_path_relative_path = test_path[len(project_root_dir) + 1:] if os.path.isdir(test_path): file_foder_path = os.path.join(logs_dir_path, test_path_relative_path) dump_file_name = "all.summary.json" else: file_relative_folder_path, test_file = os.path.split( test_path_relative_path) file_foder_path = os.path.join(logs_dir_path, file_relative_folder_path) test_file_name, _ = os.path.splitext(test_file) dump_file_name = f"{test_file_name}.summary.json" summary_path = os.path.join(file_foder_path, dump_file_name) conftest_content = conftest_content.replace("{{SUMMARY_PATH_PLACEHOLDER}}", summary_path) dir_path = os.path.dirname(conftest_path) if not os.path.exists(dir_path): os.makedirs(dir_path) with open(conftest_path, "w", encoding="utf-8") as f: f.write(conftest_content) logger.info("generated conftest.py to generate summary.json")
def test_parse_parameters_testcase(self): parameters = { "user_agent": ["iOS/10.1", "iOS/10.2"], "username-password": "******", "sum": "${calculate_two_nums(1, 2)}", } load_project_meta( os.path.join( os.path.dirname(os.path.dirname(__file__)), "examples", "postman_echo", "request_methods", ), ) parsed_params = parser.parse_parameters(parameters) self.assertEqual(len(parsed_params), 2 * 3 * 2) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.1", "sum": 3, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.1", "sum": 1, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.2", "sum": 3, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.2", "sum": 1, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.1", "sum": 3, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.1", "sum": 1, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.2", "sum": 3, }, parsed_params, ) self.assertIn( { "username": "******", "password": "******", "user_agent": "iOS/10.2", "sum": 1, }, parsed_params, )
def parse_parameters(parameters: Dict, ) -> List[Dict]: """ parse parameters and generate cartesian product. Args: parameters (Dict) parameters: parameter name and value mapping 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()}" Returns: list: cartesian product list Examples: >>> parameters = { "user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], "username-password": "******", "app_version": "${gen_app_version()}", } >>> parse_parameters(parameters) """ parsed_parameters_list: List[List[Dict]] = [] # load project_meta functions project_meta = loader.load_project_meta(os.getcwd()) functions_mapping = project_meta.functions for parameter_name, parameter_content in parameters.items(): parameter_name_list = parameter_name.split("-") 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": "******", "password": "******"}, {"username": "******", "password": "******"}] parameter_content_list: List[Dict] = [] for parameter_item in parameter_content: if not isinstance(parameter_item, (list, tuple)): # "2.8.5" => ["2.8.5"] parameter_item = [parameter_item] # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"} # ["username", "password"], ["user1", "111111"] => {"username": "******", "password": "******"} parameter_content_dict = dict(zip(parameter_name_list, parameter_item)) parameter_content_list.append(parameter_content_dict) elif isinstance(parameter_content, Text): # (2) & (3) parsed_parameter_content: List = parse_data( parameter_content, {}, functions_mapping ) if not isinstance(parsed_parameter_content, List): raise exceptions.ParamsError( f"parameters content should be in List type, got {parsed_parameter_content} for {parameter_content}" ) parameter_content_list: List[Dict] = [] 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() => [ # {"username": "******", "password": "******"}, # {"username": "******", "password": "******"} # ] parameter_dict: Dict = { key: parameter_item[key] for key in parameter_name_list } elif isinstance(parameter_item, (List, tuple)): if len(parameter_name_list) == len(parameter_item): # {"username-password": "******"} # get_account() => [("user1", "111111"), ("user2", "222222")] parameter_dict = dict(zip(parameter_name_list, parameter_item)) else: raise exceptions.ParamsError( f"parameter names length are not equal to value length.\n" f"parameter names: {parameter_name_list}\n" f"parameter values: {parameter_item}" ) elif len(parameter_name_list) == 1: # {"user_agent": "${get_user_agent()}"} # get_user_agent() => ["iOS/10.1", "iOS/10.2"] # parameter_dict will get: {"user_agent": "iOS/10.1", "user_agent": "iOS/10.2"} parameter_dict = {parameter_name_list[0]: parameter_item} else: raise exceptions.ParamsError( f"Invalid parameter names and values:\n" f"parameter names: {parameter_name_list}\n" f"parameter values: {parameter_item}" ) parameter_content_list.append(parameter_dict) else: raise exceptions.ParamsError( f"parameter content should be List or Text(variables or functions call), got {parameter_content}" ) parsed_parameters_list.append(parameter_content_list) return utils.gen_cartesian_product(*parsed_parameters_list)