From b00fc04f2aad7cf378176882b7b215f68297b3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B1=B3=E6=B4=9B?= <619434176@qq.com> Date: Mon, 22 Jan 2024 00:54:49 +0800 Subject: [PATCH] update gconfig replace way --- app/core/constructor/case_constructor.py | 4 +- app/core/executor.py | 112 ++++++++++++++--------- app/core/render.py | 2 + app/crud/config/GConfigDao.py | 16 ++++ app/crud/test_case/TestCaseDao.py | 6 +- app/utils/gconfig_parser.py | 10 +- 6 files changed, 99 insertions(+), 51 deletions(-) diff --git a/app/core/constructor/case_constructor.py b/app/core/constructor/case_constructor.py index 77c6aba5..7b06a505 100644 --- a/app/core/constructor/case_constructor.py +++ b/app/core/constructor/case_constructor.py @@ -14,9 +14,7 @@ async def run(executor, env, index, path, params, req_params, constructor: Const case_id = data.get("constructor_case_id") if not case_id: raise Exception("未获取到前/后置条件的用例id, 请检查前置条件") - testcase, err = await TestCaseDao.async_query_test_case(case_id) - if err: - raise Exception(f"用例: [{case_id}]不存在:") + testcase = await TestCaseDao.async_query_test_case(case_id) executor.append(f"当前路径: {path}, 第{index + 1}条{ConstructorAbstract.get_name(constructor)}") # 说明是case executor_class = kwargs.get('executor_class')(executor.logger) diff --git a/app/core/executor.py b/app/core/executor.py index 0fee8a2b..a137f3b8 100755 --- a/app/core/executor.py +++ b/app/core/executor.py @@ -4,7 +4,7 @@ import time from collections import defaultdict from datetime import datetime -from typing import List, Any +from typing import List, Any, Callable from app.core.constructor.case_constructor import TestcaseConstructor from app.core.constructor.http_constructor import HttpConstructor @@ -14,6 +14,7 @@ from app.core.msg.dingtalk import DingTalk from app.core.msg.mail import Email from app.core.paramters import parameters_parser +from app.core.render import Render from app.core.ws_connection_manager import ws_manage from app.crud.auth.UserDao import UserDao from app.crud.config.AddressDao import PityGatewayDao @@ -45,15 +46,33 @@ from app.utils.logger import Log from config import Config +# construct method mapping +construct_type = { + ConstructorType.testcase: TestcaseConstructor, + ConstructorType.sql: SqlConstructor, + ConstructorType.redis: RedisConstructor, + ConstructorType.py_script: PythonConstructor, + ConstructorType.http: HttpConstructor, +} + +# gconfig parser mapping +gconfig_parser = { + GConfigParserEnum.string: StringGConfigParser.get_data, + GConfigParserEnum.json: JSONGConfigParser.get_data, + GConfigParserEnum.yaml: YamlGConfigParser.get_data, +} + class Executor(object): log = Log("Executor") el_exp = r"\$\{(.+?)\}" pattern = re.compile(el_exp) # 需要替换全局变量的字段 - fields = ['body', 'url', 'request_headers'] + fields = ['url', 'request_headers'] def __init__(self, log: CaseLog = None): + # 这里是一个彩蛋, 奔驰大G LB(括弧1.3T) + self.glb = None if log is None: self._logger = CaseLog() self._main = True @@ -67,17 +86,7 @@ def logger(self): @staticmethod def get_constructor_type(c: Constructor): - if c.type == ConstructorType.testcase: - return TestcaseConstructor - if c.type == ConstructorType.sql: - return SqlConstructor - if c.type == ConstructorType.redis: - return RedisConstructor - if c.type == ConstructorType.py_script: - return PythonConstructor - if c.type == ConstructorType.http: - return HttpConstructor - return None + return construct_type.get(c.type) def append(self, content, end=False): if end: @@ -93,17 +102,36 @@ async def parse_gconfig(self, data, type_, env, *fields): for f in fields: await self.parse_field(data, f, GconfigType.text(type_), env) + async def parse_gconfig_v2(self, data, type_, *fields): + """upgrade for replacing global variables""" + for f in fields: + self.append("解析{}: [{}]中的全局变量".format(GconfigType.text(type_), data, f)) + origin_field = getattr(data, f) + # if not None or "" + if origin_field: + rendered = Render.render(self.glb, origin_field) + if rendered != origin_field: + self.append("替换全局变量成功, [{}]:\n\n[{}] -> [{}]\n".format(f, origin_field, rendered)) + setattr(data, f, rendered) + @case_log - def get_parser(self, key_type): + async def query_gconfig(self, env: int): + """加载全局变量""" + gconfig_list = await GConfigDao.list_gconfig(env) + gconfig_map = dict() + for g in gconfig_list: + parser = Executor.get_parser(g.key_type) + gconfig_map[g.key] = parser(g.value) + self.glb = gconfig_map + + @staticmethod + def get_parser(key_type) -> Callable: """获取变量解析器 """ - if key_type == GConfigParserEnum.string: - return StringGConfigParser.parse - if key_type == GConfigParserEnum.json: - return JSONGConfigParser.parse - if key_type == GConfigParserEnum.yaml: - return YamlGConfigParser.parse - raise Exception(f"全局变量类型: {key_type}不合法, 请检查!") + parser = gconfig_parser.get(key_type) + if parser is None: + raise Exception(f"全局变量类型: {key_type}不合法, 请检查!") + return parser async def parse_field(self, data, field, name, env): """ @@ -283,26 +311,26 @@ async def run(self, env: int, case_id: int, params_pool: dict = None, request_pa if req_params is None: req_params = dict() + # 加载全局变量 + await self.query_gconfig(env) + + # 挂载全局变量 + case_params.update(self.glb) + request_param.update(self.glb) + try: - case_info, err = await TestCaseDao.async_query_test_case(case_id) - if err: - return response_info, err + case_info = await TestCaseDao.async_query_test_case(case_id) response_info['case_id'] = case_info.id response_info["case_name"] = case_info.name method = case_info.request_method.upper() response_info["request_method"] = method - # Step1: 替换全局变量 - await self.parse_gconfig(case_info, GconfigType.case, env, *Executor.fields) - - self.append("解析全局变量", True) - # Step2: 获取构造数据 constructors = await self.get_constructor(case_id) - # Step3: 解析前后置条件的全局变量 - for c in constructors: - await self.parse_gconfig(c, GconfigType.constructor, env, "constructor_json") + # # Step3: 解析前后置条件的全局变量 + # for c in constructors: + # await self.parse_gconfig_v2(c, GconfigType.constructor, "constructor_json") # Step4: 获取断言 asserts = await TestCaseAssertsDao.async_list_test_case_asserts(case_id) @@ -310,8 +338,8 @@ async def run(self, env: int, case_id: int, params_pool: dict = None, request_pa # 获取出参信息 out_parameters = await PityTestCaseOutParametersDao.select_list(case_id=case_id) - for ast in asserts: - await self.parse_gconfig(ast, GconfigType.asserts, env, "expected", "actually") + # for ast in asserts: + # await self.parse_gconfig_v2(ast, GconfigType.asserts, "expected", "actually") # Step5: 替换参数 self.replace_args(req_params, case_info, constructors, asserts) @@ -319,6 +347,9 @@ async def run(self, env: int, case_id: int, params_pool: dict = None, request_pa # Step6: 执行前置条件 await self.execute_constructors(env, path, case_info, case_params, req_params, constructors, asserts) + # 获取全局变量更新body url headers + await self.parse_gconfig_v2(case_info, GconfigType.case, *Executor.fields) + # Step7: 批量改写主方法参数 await self.parse_params(case_info, case_params) @@ -338,6 +369,7 @@ async def run(self, env: int, case_id: int, params_pool: dict = None, request_pa case_info.url = f"{base_path}{case_info.url}" response_info["url"] = case_info.url + response_info["request_data"] = body # Step9: 完成http请求 request_obj = await AsyncRequest.client(url=case_info.url, body_type=case_info.body_type, headers=headers, @@ -356,7 +388,6 @@ async def run(self, env: int, case_id: int, params_pool: dict = None, request_pa # 写入response # TODO - # req_params["response"] = res.get("response", "") self.replace_asserts(asserts, req_params, case_params) self.replace_constructors(constructors, req_params, case_params) @@ -472,17 +503,10 @@ async def run_single(env: int, data, report_id, case_id, params_pool: dict = Non @case_log def replace_body(self, req_params, body, body_type=1): """根据传入的构造参数进行参数替换""" - if body_type != BodyType.json: - self.append("当前请求数据不为json, 跳过替换") - return body try: if body: - data = json.loads(body) - if req_params is not None: - for k, v in req_params.items(): - if data.get(k) is not None: - data[k] = v - return json.dumps(data, ensure_ascii=False) + # 2024-01-22 switch to Render + return Render.render(req_params, body) self.append(f"body为空, 不进行替换") except Exception as e: self.append(f"替换请求body失败, {e}") diff --git a/app/core/render.py b/app/core/render.py index a0781dfa..06fa12ae 100644 --- a/app/core/render.py +++ b/app/core/render.py @@ -15,6 +15,8 @@ def get_env(): if func.startswith("__"): continue my_env.globals[func] = getattr(PityFunction, func) + my_env.variable_start_string = "${" + my_env.variable_end_string = "}" return my_env diff --git a/app/crud/config/GConfigDao.py b/app/crud/config/GConfigDao.py index 711294e6..ae6e5b90 100644 --- a/app/crud/config/GConfigDao.py +++ b/app/crud/config/GConfigDao.py @@ -1,3 +1,5 @@ +from typing import List + from sqlalchemy import select from app.crud import Mapper, ModelWrapper @@ -39,3 +41,17 @@ async def async_get_gconfig_by_key(key: str, env: int) -> GConfig: return result.scalars().first() except Exception as e: raise Exception(f"查询全局变量失败: {str(e)}") + + @staticmethod + async def list_gconfig(env: int) -> List[GConfig]: + """ + get available gconfig + """ + try: + filters = [GConfig.deleted_at == 0, GConfig.enable == True, GConfig.env == env] + async with async_session() as session: + sql = select(GConfig).where(*filters) + result = await session.execute(sql) + return result.scalars().all() + except Exception as e: + raise Exception(f"查询全局变量失败: {str(e)}") diff --git a/app/crud/test_case/TestCaseDao.py b/app/crud/test_case/TestCaseDao.py index c60381ff..8edbddc8 100644 --- a/app/crud/test_case/TestCaseDao.py +++ b/app/crud/test_case/TestCaseDao.py @@ -224,11 +224,11 @@ async def async_query_test_case(case_id) -> [TestCase, str]: select(TestCase).where(TestCase.id == case_id, TestCase.deleted_at == 0)) data = result.scalars().first() if data is None: - return None, "用例不存在" - return data, None + raise Exception(f"用例id: {case_id}不存在, 可能已经被删除") + return data except Exception as e: TestCaseDao.__log__.error(f"查询用例失败: {str(e)}") - return None, f"查询用例失败: {str(e)}" + raise Exception(f"查询用例失败: {str(e)}") @classmethod async def list_testcase_tree(cls, projects: List[Project]) -> [List, dict]: diff --git a/app/utils/gconfig_parser.py b/app/utils/gconfig_parser.py index 6ffeef07..72e06c34 100755 --- a/app/utils/gconfig_parser.py +++ b/app/utils/gconfig_parser.py @@ -16,7 +16,11 @@ class GConfigParser(object): @staticmethod def parse(value, jsonpath): - pass + raise NotImplementedError + + @staticmethod + def get_data(value): + raise NotImplementedError @staticmethod def get(data, key): @@ -67,6 +71,10 @@ def parse(value, jsonpath): """String解析器""" return value + @staticmethod + def get_data(value): + return value + class JSONGConfigParser(GConfigParser): @staticmethod