def display(*args, format_print=True, mode=None, log_level='info'): """ + 说明: 打印输出到console和日志 + 用法: 输入:display('dcn','test',format_print=True) 输出:False >>> ################################################################################ >>> # dcn # >>> # test # >>> ################################################################################ 输入:display('dcn','test',format_print=False) 输出:dcn test :param mode: display模式默认为log输出,如果为print方式,指定mode='print' :param args: 显示到console/log文件的message :param format_print: 是否统一格式化输出,默认为True :param log_level: 日志级别 """ output = (print_format(*args),) if format_print else (str(o) for o in args) if mode == 'print': print(*output) else: from .log import log log(*output, level=log_level)
def color(exclude=None): """ + 说明: 在串口返回随机颜色字符串,随机范围如下(可以通过exclude缩小随机范围) { 'BLACK', 'BLUE', 'CYAN', 'GREEN', 'LIGHTBLACK_EX', 'LIGHTBLUE_EX', 'LIGHTCYAN_EX', 'LIGHTGREEN_EX', 'LIGHTMAGENTA_EX', 'LIGHTRED_EX', 'LIGHTWHITE_EX', 'LIGHTYELLOW_EX', 'MAGENTA', 'RED', 'RESET', 'WHITE', 'YELLOW' } :param exclude: 随机颜色中排除指定颜色 :return: 颜色字符串 """ try: import random colors = ['BLACK', 'BLUE', 'CYAN', 'GREEN', 'LIGHTBLACK_EX', 'LIGHTBLUE_EX', 'LIGHTCYAN_EX', 'LIGHTGREEN_EX', 'LIGHTMAGENTA_EX', 'LIGHTRED_EX', 'LIGHTWHITE_EX', 'LIGHTYELLOW_EX', 'MAGENTA', 'RED', 'RESET', 'WHITE', 'YELLOW'] if exclude: assert isinstance(exclude, (str,)), '参数非String类型' if isinstance(exclude, (str,)) and exclude.upper() in colors: colors.remove(exclude.upper()) return random.choice(colors) except Exception as e: from .log import log log("{} error: {}".format(color.__name__, str(e)))
def __new__(mcs, name, bases, attr): funcs, cases = Tool.filter_test_case(attr) for raw_case_name, raw_case in cases: if not hasattr(raw_case, CASE_TAG_FLAG): setattr(raw_case, CASE_TAG_FLAG, {const.Tag.SMOKE, const.Tag.FULL }) # 没有指定tag的用例,默认有SMOKE和FULL标记 # 注入用例信息 case_info = "{}.{}".format(raw_case.__module__, raw_case.__name__) setattr(raw_case, CASE_INFO_FLAG, case_info) # 检查用例描述 if const.CHECK_CASE_DOC and not raw_case.__doc__: log("{}没有用例描述".format(case_info), level='warning') # 过滤不执行的用例 if not getattr(raw_case, CASE_TAG_FLAG) & set(const.RUN_CASE): continue # 注入测试数据 if hasattr(raw_case, CASE_DATA_FLAG): funcs.update( Tool.create_case_with_case_data(raw_case_name, raw_case)) else: funcs.update( Tool.create_case_without_case_data(raw_case_name, raw_case)) return super(Meta, mcs).__new__(mcs, name, bases, funcs)
def setup_project(start_path=os.getcwd()): """ 从指定起始路径扫描指定后缀配置文件 :param start_path: 默认值为当前根路径 :return: None """ def parse_conf(conf_file_path): log(f'开始从{conf_file_path}中解析配置文件') _config = load_file(conf_file_path) for key, value in _config.items(): if conf_file_path.suffix == '.ini': for sub_key, sub_value in value.items(): settings.set(sub_key, sub_value) else: settings.set(key, value) # 解析json??或者其他配置文件 log(f'开始从{start_path}扫描...', level='debug') # 扫描当前项目文件夹,根据项目需求只扫描外面2层目录即可,正则过滤无效的.或者__打头的隐藏或者无效目录 target_dir = scan_depth_dir(start_path, depth=2, condition='[A-Za-z]+') for dir_path in target_dir: # 魔改目录名称 settings.set(f"DCN_{dir_path.stem.upper()}_PATH", dir_path) # 扫描当前项目config中的ini配置文件(目前只扫描ini/json文件,后续可以进行扩展) target_conf_path = scan_file(settings.get('DCN_CONFIG_PATH'), condition=('[\w]+\.ini', '[\w]+\.json')) for conf in target_conf_path: parse_conf(conf) # 扫描当前项目testdata中的xls和xlsx的文件,正则排除以~$开头的临时文件 target_file = scan_file(settings.get('DCN_TESTDATA_PATH'), condition=('[\w]+\.xls', '[\w]+\.xlsx')) for _file in target_file: settings.set(_file.stem, _file) def modify_log_report_config(): """ 根据项目实际需要修改log和report目录,存放结构如下 test_report/current_time/console.log test_report/current_time/report.html :return: None """ import time current_time = time.strftime("%Y%m%d%H%M%S") if settings.get('report_file_modify'): # 根据配置文件中的策略修改日志和报告路径 report_path = settings.get('DCN_TESTREPORT_PATH') settings.set('report_dir_path', (report_path / current_time).__fspath__()) settings.set('report_file', (report_path / current_time / settings.get('report_name')).__fspath__()) if settings.log_file_modify: settings.set('log_dir_path', (report_path / current_time).__fspath__()) settings.set('log_file', (report_path / current_time / settings.get('log_name')).__fspath__()) del time modify_log_report_config() # 动态修改日志和报告存放路径和信息 import sys sys.modules['library.conf'].settings = settings
def wrap(*args, **kwargs): time.sleep(const.EXECUTE_INTERVAL) msg = "start to test {} ({}/{})".format(getattr(func, CASE_INFO_FLAG), getattr(func, CASE_ID_FLAG), Tool.total_case_num) log(msg, level='info') result = func(*args, **kwargs) return result
def load(self, discovery): if discovery: self.parse(discovery) else: # 默认加载方式 先从全局settings中获取,失败再从当前python文件中获取 if settings.get('testcase_ini'): # 尝试全局扫描到的配置文件 log("使用自动扫描到的配置文件加载测试用例", level='info') else: # 尝试通过py加载 log("使用当前环境中的py变量加载测试用例", level='info')
def parse_conf(conf_file_path): log(f'开始从{conf_file_path}中解析配置文件') _config = load_file(conf_file_path) for key, value in _config.items(): if conf_file_path.suffix == '.ini': for sub_key, sub_value in value.items(): settings.set(sub_key, sub_value) else: settings.set(key, value) # 解析json??或者其他配置文件
def setup(): log('项目初始化环境设置中...') # 动态扫描整个项目目录,读取配置文件中的ini文件 setup_project() # 动态读取配置文件中日志配置 setup_log() # 动态读取配置文件中unittest配置 setup_unittest() log('项目初始化完成...', color='yellow')
def setup_log(): log_instance.logger = log_instance.setup( name=settings.logger_name, log_file=settings.log_file if settings.file_log_on else None, # 用于判断是否生成日志文件 console_level=settings.log_level_in_console, fmt=settings.log_fmt, max_bytes=settings.max_bytes_each, backup_count=settings.backup_count, logfile_level=settings.log_level_in_logfile, display_to_console=settings.console_log_on, ) log('根据项目ini文件重新设置日志默认格式')
def check_dict_conflict(source): """ 检查判断字典value是否存在冲突 :param source: :return: """ _check = {} for _k, _v in source.items(): if _v in _check: err_msg = f'{_v}冲突 {_check[_v]}--->{_k}存在冲突键值对,请注意检查' log('存在冲突键值对,请注意检查', level='error') raise Exception(err_msg) _check[_v] = _k
def assert_dir(dir_path): """ + 说明: 探测dir_path是否为存在的有目录路径,如果不是返回InvalidPathDir异常。 :param dir_path: 文件目录路径名称(string)/WindowPath实例。 :return: WindowsPath实例 """ dir_path = as_path(dir_path) if not dir_path.is_dir(): err_msg = f'{dir_path} 不是有效目录' log(err_msg, level='error') raise InvalidPathDir(err_msg) return dir_path
def assert_file(file_path): """ + 说明: 测file_path是否为存在的有效文件路径,如果不是返回FileNotFound异常。 :param file_path: 文件路径名称(string)/WindowPath实例。 :return: WindowsPath instance """ file_path = as_path(file_path) if not file_path.is_file(): err_msg = f'{file_path} 不是有效文件目录' log(err_msg, level='error') raise FileNotFound(err_msg) return file_path
def set(self, key, value): """优先选用的设置值的方式 :param key: The key to store :param value: The value to store """ value = parse_conf_data(value) key = key.strip() if key in self.store and value != self.store[key]: from library.log import log log(f'注意 {key} {self(key)} 修改成 {key} {value}', level='warning') setattr(self, key, value) self.store[key] = value self._deleted.discard(key)
def print_check_case(sheet_name, result): """ + 说明: 检查测试例step对错,打印并记录 :param sheet_name: 测试用例sheet名称 :param result: 测试结果列表 """ if 0 in result: # 如果result列表中存在0表示测试用例失败 log(f'{sheet_name} is FAILED!', result, level='error') r = False else: log(sheet_name + ' ' + 'is PASSED', result, level='info') r = True return r
def parse(self, discovery): if isinstance(discovery, str): log("===解析字符串===") self._parse_str(discovery) elif isinstance(discovery, os.PathLike): log("===解析PathLike路径===") self._parse_path(discovery) elif ismodule(discovery): log("===解析模块===") self._suite.addTests( self.loadTestsFromModule(discovery, pattern=None)) elif isclass(discovery): log("===解析类===") self._suite.addTests(self.loadTestsFromTestCase(discovery)) elif isinstance(discovery, (list, tuple)): log("===解析Sequence===") for _discovery in discovery: self.parse(_discovery)
def _send_request_safe_mode(self, method, url, **kwargs): """ Send a HTTP request, and catch any exception that might occur due to connection problems. Safe mode has been removed from requests 1.x. """ try: msg = "processed request:\n" msg += "> {method} {url}\n".format(method=method, url=url) msg += "> kwargs: {kwargs}".format(kwargs=kwargs) log(msg) return requests.Session.request(self, method, url, **kwargs) except (MissingSchema, InvalidSchema, InvalidURL): raise except RequestException as ex: resp = ApiResponse() resp.error = ex resp.status_code = 0 # with this status_code, content returns None resp.request = Request(method, url).prepare() return resp
def display(self, response_json_or_text, method='POST'): """ 测试用例步骤输入输出显示 :param method: http request method :param response_json_or_text: response json or text data :return: """ msg = "\n[输入]:\n" msg += "> {method} {url}\n".format(method=method, url=self.url) if isinstance(response_json_or_text, dict): msg += "> kwargs: {kwargs}".format( kwargs=response_json_or_text.get('file') or self.data) dcn_raw_code = dcn_codes.get(response_json_or_text['status']) display_response = deepcopy(response_json_or_text) if dcn_raw_code: display_response['status'] = (display_response['status'], dcn_raw_code[0]) else: display_response = response_json_or_text from pprint import pformat log(msg, level='info') log(f'\n[输出]:\n> response: {pformat(display_response)}', level='info')
def print_timer_context(test_case_name, *args, **kwargs): """ + 说明: 测试例开始/结束计时 >>> with print_timer_context('hello, world') as doctest: >>> print(doctest) :param test_case_name: 测试用例名称 :param args: msg :param kwargs: other args :return: 格式化输出 """ start_time = datetime.datetime.now() try: log(r'{title} {switch} {time}'.format(title=test_case_name, switch='start at', time=str(start_time)), format_print=True, level='info', *args, **kwargs) yield 'for doctest use' finally: stop_time = datetime.datetime.now() log(r'{title} {switch} {time}'.format(title=test_case_name, switch='end at', time=str(stop_time)), 'TestCase Duration Time:{time}'.format(time=duration(start_time, stop_time)), format_print=True, level='info', *args, **kwargs)
def ensure_dir(dir_path, path_type='parent'): """ + 说明: 确保指定路径文件的目录存在(不存在创建)。 :param dir_path: 需要确保的路径参数。 :param path_type: current或者parent。 """ path_mapping = { 'current': as_path(dir_path), 'parent': as_path(dir_path).parent } try: dir_abs = path_mapping[path_type] if not dir_abs.exists(): msg = f'\n目标 -> {dir_path}\n创建 -> {dir_abs}' log(msg, level='info') dir_abs.mkdir(parents=True, exist_ok=True) except KeyError: err_msg = f"{path_type} 不是有效参数,有效参数为'parent' or 'current'" log(err_msg, level='error') raise InvalidOption(err_msg)
def as_path(path_name): """ + 说明: 1. string类型或者Path实例路径进行格式化和统一化,如果是string转换成Path实例, 2. Path实例解析成绝对路径, 3. string中存在 windows下文件名称非法字符为抛出异常,记录日志。 >>> as_path('E:/1/2/3') WindowsPath('E:/1/2/3') >>> as_path('E:/1') WindowsPath('E:/1') :param path_name: 文件路径名称(string)/WindowPath实例。 :return: WindowsPath实例。 """ try: if isinstance(path_name, Path): return path_name.resolve() return Path(path_name).resolve() except OSError as e: err_msg = f'路径名称{path_name}含有如下非法字符(\/:*?"<>|)' log(err_msg, level='error') raise InvalidPath(err_msg) from e
def _parse_str(self, discovery): if discovery in sys.modules: # 尝试本地命名空间中是否存在该模块存在 log(f'\n===sys.modules===\n通过sys.modules导入{discovery}') self._suite.addTests( self.loadTestsFromModule(sys.modules[discovery], pattern=None)) elif DOT_MODULE_MATH.match(discovery): # 校验字符串合法性,匹配绝对路径 if any([ not _.isidentifier() and _ not in kwlist for _ in discovery.split('.') ]): raise InvalidArgument(f'非法参数{discovery}') else: name = discovery try: self._suite.addTests( self.loadTestsFromName(name)) # 直接进行解析 log(f'\n===绝对路径===\n通过loadTestsFromNames导入{discovery}') except (ModuleNotFoundError, Exception): # no qa可能路径带有具体方法??? parts = discovery.split('.') from importlib import import_module while parts: try: mod = import_module('.'.join(parts)) if callable(getattr(mod, _class)): self._suite.addTest( getattr(mod, _class)(_method)) log(f'\n===实例化对象引入===\n通过addTest导入{mod}{_class}{_method}' ) break except ImportError: _class = parts[-2] _method = parts[-1] parts = parts[:-2] elif discovery.startswith('.'): # 如果为相对引入路径,结合top_level_dir进行相对引入 name = valid_import_path(self.top_level_dir) + discovery self._suite.addTests(self.loadTestsFromName(name)) log(f'\n===相对路径===\n通过loadTestsFromName导入{name}') else: try: if Path(discovery).resolve().is_dir() or Path( discovery).resolve().is_file(): # 如果传入路径进行路径解析 self._parse_path(discovery) # 委托给parse_path进行解析 else: raise InvalidPath(f'无效路径 {discovery}') # 必须为有效路径 except OSError: if '*' in discovery or '?' in discovery or '.' in discovery: # 尝试解析例如E://testcase/test_*.py parent = Path(discovery).parent # 父路径 pattern = Path(discovery).parts[-1] # 正则 if parent.is_dir(): glob_path_list = list(parent.glob(pattern)) if len(glob_path_list): from pprint import pformat log(f'\n===加载如下测试用例===\n{pformat(glob_path_list)}') for _ in glob_path_list: valid_import_path(_.parent) self._suite.addTests( self.loadTestsFromName('.'.join( [valid_import_path(_.parent), _.stem]))) else: raise TestCaseNotFound('没有找到测试用例') else: raise InvalidPath(f'无效路径 {discovery}') # 必须为有效路径 else: raise InvalidArgument(f'无法解析参数 {discovery}')
def _parse_path(self, discovery): log(" parse path", level='info') discovery = Path(discovery).resolve() if discovery.is_dir(): # 如果传入dir要求必须是测试用例路,不能是配置文件 self._suite.addTests( self.discover(discovery, top_level_dir=self.top_level_dir)) log("文件夹", level='info') elif discovery.is_file(): # 如果传入file要求必须是配置文件 # todo if '.ini' == discovery.suffix: log("parse ini", level='info') elif '.json' == discovery.suffix: log('parse json', level='info') elif '.ymal' == discovery.suffix: log('parse toml', level='info') elif '.toml' == discovery.suffix: log('parse toml', level='info') elif '.py' == discovery.suffix: log('parse py', level='info') else: log('error') else: raise ParsePathError(f'无法解析{discovery}路径')
def request(self, method, url, name=None, **kwargs): """ Constructs and sends a :py:class:`requests.Request`. Returns :py:class:`requests.Response` object. :param method: method for the new :class:`Request` object. :param url: URL for the new :class:`Request` object. :param name: (optional) Placeholder, make compatible with Locust's HttpSession :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. :param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload. :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. :param timeout: (optional) How long to wait for the server to send data before giving up, as a float, or \ a (`connect timeout, read timeout <user/advanced.html#timeouts>`_) tuple. :type timeout: float or tuple :param allow_redirects: (optional) Set to True by default. :type allow_redirects: bool :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. :param stream: (optional) whether to immediately download the response content. Defaults to ``False``. :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. """ # record test name self.meta_data["name"] = name # record original request info self.meta_data["data"][0]["request"]["method"] = method self.meta_data["data"][0]["request"]["url"] = url kwargs.setdefault("timeout", 120) self.meta_data["data"][0]["request"].update(kwargs) # prepend url with hostname unless it's already an absolute URL url = build_url(self.base_url, url) start_timestamp = time.time() response = self._send_request_safe_mode(method, url, **kwargs) response_time_ms = round((time.time() - start_timestamp) * 1000, 2) # get the length of the content, but if the argument stream is set to True, we take # the size from the content-length header, in order to not trigger fetching of the body if kwargs.get("stream", False): content_size = int(dict(response.headers).get("content-length") or 0) else: content_size = len(response.content or "") # record the consumed time self.meta_data["stat"] = { "response_time_ms": response_time_ms, "elapsed_ms": response.elapsed.microseconds / 1000.0, "content_size": content_size } # record request and response histories, include 30X redirection response_list = response.history + [response] self.meta_data["data"] = [ self.get_req_resp_record(resp_obj) for resp_obj in response_list ] try: response.raise_for_status() except RequestException as e: log(u"{exception}".format(exception=str(e)), level='error') else: log( """status_code: {}, response_time(ms): {} ms, response_length: {} bytes\n""".format( response.status_code, response_time_ms, content_size ), ) return response
def api(self): """ + 说明: 输入需要测试的excel名称和excel中sheet名称,获取sheet表中每行的测试用例编号,测试用例名称,预制条件,url, data,request方法,响应码和错误检测码,测试每张sheet表中不同的测试例。 + 测试结果判断: 根据获取的响应码code[i]或者错误检测码error_code[i]来判断测试例的测试结果是否pass。 1.当获取的code[i]不为空且获取返回结果的"status"和code[i]相等,这个时候是通过code[i]即响应码来判断测试结果的。 2.当获取的code[i]为空且error_code[i]不为空且获取返回结果的”status“不为0,这个时候是通过error_code[i]即错误响应码来判断 测试结果的,这时根据返回结果又分为三种情况: 情况1: 返回的字典结果中字典的key等于"errors" or "addErrors" or "delErrors" or "updateErrors"且这些key对应的值为一个 列表,如{'status': 7, 'errors': [{'id': 14, 'status': 1}, {'id': 7, 'status': 163}]},取key对应列表中的"status" 或者"code"和error_code[i]做对比得出测试结果是否pass; 情况2: 返回字典的key等于“result”且这个key对应的结果是一个字典,如{"status":4,"result":{"count":4,"errors": [{"index":5,"code":251},{"index":6,"code":252},{"index":7,"code":252},{"index":8,"code":252}]}}, 这时是通过key“count"或者key“result”对应字典的key“errors”对应的值key"index"或者“code”和error_code[i]做对比得出测试结果是否pass; 情况3: 返回字典的key等于“result”且这个key对应的结果不是一个字典,这是一个容错判断,直接判断测试结果为fail; 3.当获取的code[i]为空且获取返回结果的"status"等于0,这是个容错判断,直接判断由于功能问题导致的测试结果为fail; 4.除上述外的其他情况,判断测试结果为fail。 :return: 返回每张sheet中各个测试例结果的列表 """ # noinspection PyTypeChecker arguments = zip(self.seqs, self.names, self.urls, self.data, self.methods, self.codes, self.error_codes) res = [] for seq, name, url, data, method, code, error_code in arguments: full_url = settings.base_url + url with print_timer_context(f'{seq} {name}'): api = SessionMethod(full_url, data, seq) stat = {'status': None} request = { "post": api.post, "get": api.get, "put": api.put, "delete": api.delete, "head": api.head, "patch": api.patch, "option": api.option, "postuser": api.post_user, "getuser": api.get_user, "putuser": api.put_user, "deleteuser": api.delete_user, "postadmin": api.post_admin, "getadmin": api.get_admin, "putadmin": api.put_admin, "deleteadmin": api.delete_admin, "postnologin": api.post_no_login, "getnologin": api.get_no_login, "putnologin": api.put_no_login, "deletenologin": api.delete_no_login } if method not in request: log('interface test method is error', level='info') else: stat = request[method]() # 下面代码作用为根据返回结果判断测试是否通过 if code != "" and stat["status"] == code: # 使用状态码code[]来判断的情况 log( f'\n[结果]:\n' f'> [Except Code: {int(code)} ] | [Actual Code: {stat["status"]}] TestCase {name}({seq}) is ' f'Passed', level='info') # log(f'[Match Code: {code} is OK!] TestCase {name} {seq} is Passed', level='info') res.append(1) elif code == "" and error_code != "" and stat["status"] != 0: # 使用状态码error_code[]来判断的情况,有一种特殊情况,预期不能创建成功的接口,在测试中创建成功会返回代码0,例如: # {'status': 0, 'addErrors': None, 'updateErrors': None, 'delErrors': None} for key in stat: if (key == "errors" or key == "addErrors" or key == "delErrors" or key == "updateErrors") \ and isinstance(stat[key], list): # 用于user->“组织机构用户组批量操作”or“批量删除用户组”测试判断 # {'status': 7, 'errors': [{'id': 14, 'status': 1}, {'id': 7, 'status': 163}]} for d in range(len(stat[key])): # 遍历关键字为"errors"or"addErrors"or"delErrors"or"updateErrors"的字典所对应值的列表,这里列表所对应 # 的元素个数是变化的 if stat[key][d].get("status") == error_code \ or stat[key][d].get("code") == error_code: # 列表中每一个元素都是字典类型,取字典中关键字"status"对应的错误码,用来做判断 log(f'[Match ErrorCode: {error_code} is OK!] TestCase {seq} is Passed', level='info') res.append(1) # 如果错误码和预期一致,目前遇到的返回码只涉及到关键字"errors"or"addErrors"or"delErrors" # or"updateErrors"中的一种,所以一旦找到就退出循环 break else: log(f'[Not Match ErrorCode: {error_code}] TestCase {seq} is Failed', level='error') res.append(0) # 整个错误码对应列表中的字典元素都和预期错误码不一致,记为测试fail elif (key == 'result') and isinstance(stat[key], dict): # 用于user->"用户账号批量导入"测试判断 # {"status":4,"result":{"count":4,"errors":[{"index":5,"code":251},{"index":6,"code":252}, # {"index":7,"code":252},{"index":8,"code":252}]}} if stat[key]['count'] == error_code or \ ('errors' in stat[key] and error_code == 'errors'): # 判断count或者errors是否与错误检查点一致 log(f'[Match ErrorCode: {error_code} is OK!] TestCase {seq} is Passed', level='info') res.append(1) else: for b in range(len(stat[key]['errors'])): # 遍历关键字为"errors"的字典所对应值的列表,这里列表所对应的元素个数是变化的 if stat[key]['errors'][b].get("index") == error_code \ or stat[key]['errors'][b].get("code") == error_code: # 列表中每一个元素都是字典类型,取字典中关键字"index"或者“code”对应的错误码,用来做判断 log(f'[Match ErrorCode: {error_code} is OK!] TestCase {seq} is Passed', level='info') res.append(1) # 如果错误码和预期一致,目前遇到的返回码只涉及到关键字"code"or"index"中的一种, # 所以一旦找到就退出循环 break else: log(f'[Not Match ErrorCode: {error_code}] TestCase {seq} is Failed', level='error') res.append(0) # 整个错误码对应列表中的字典元素都和预期错误码不一致,记为测试fail elif (key == 'result') and not isinstance(stat[key], dict): # org->组织机构导入-IP格式不正确{"status":3,"result":null} log(f'[Not Match ErrorCode: {error_code}] TestCase {seq} is Failed', level='error') res.append(0) elif code == "" and stat["status"] == 0: log(f'[The Reason for TestCase {seq} Failure is function problem', level='error') res.append(0) else: log( f'\n[结果]:\n' f"> [Except Code: {str(code).strip('.0')} ] | [Actual Code: {stat['status']}] " f"TestCase {name}({seq}) is Failed", level='error') res.append(0) return res
def wrap(func): if hasattr(func, CASE_DATA_FLAG): log("{}的测试数据只能初始化一次".format(func.__name__), level='error') setattr(func, CASE_DATA_FLAG, values) setattr(func, CASE_DATA_UNPACK_FLAG, unpack) return func
def _log(self): log(Template(LOG_TEMPLATE).render(items=enumerate(self.log_content)))
def log_print(req_resp_dict, r_type): msg = "\n================== {} details ==================\n".format(r_type) for key, value in req_resp_dict[r_type].items(): msg += "{:<16} : {}\n".format(key, repr(value)) log(msg)