From 41c695ec2d886b2238a047b64c656eaf838b93c3 Mon Sep 17 00:00:00 2001 From: youhuo Date: Thu, 23 Jun 2022 20:34:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0testlib=E7=9A=84servi?= =?UTF-8?q?ce=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/__init__.py | 0 services/auth_service.py | 334 ++++++++++++++++ services/case_service.py | 668 ++++++++++++++++++++++++++++++++ services/common_service.py | 45 +++ services/const.py | 20 + services/device_service.py | 96 +++++ services/outline_service.py | 85 ++++ services/plan_service.py | 310 +++++++++++++++ services/requirement_service.py | 87 +++++ services/task_service.py | 138 +++++++ services/tone_job_service.py | 156 ++++++++ services/tone_service.py | 63 +++ 12 files changed, 2002 insertions(+) create mode 100644 services/__init__.py create mode 100644 services/auth_service.py create mode 100644 services/case_service.py create mode 100644 services/common_service.py create mode 100644 services/const.py create mode 100644 services/device_service.py create mode 100644 services/outline_service.py create mode 100644 services/plan_service.py create mode 100644 services/requirement_service.py create mode 100644 services/task_service.py create mode 100644 services/tone_job_service.py create mode 100644 services/tone_service.py diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth_service.py b/services/auth_service.py new file mode 100644 index 0000000..d49c651 --- /dev/null +++ b/services/auth_service.py @@ -0,0 +1,334 @@ +import datetime +import random +import string +from functools import wraps + +import jwt +from jwt import exceptions +from sqlalchemy.exc import IntegrityError + +from app import redis +from app.conf import conf +from common.enums import User_Role, User_Role_Review_Status, User_Role_Op_Method +from common.http import anolis_headers, http_request, handle_no_auth +from common.http import rsp +from models.user_models import find_user_exists, User, UserRoleOpRecord, upgrade_user_roles, batch_delete, \ + get_review_count_infos, User_Community +from services.const import ERROR_NO_OP_PERMISSION + +ERROR_WRONG_USER_INFO = '用户不存在/密码错误' +ERROR_DUPLICATED_USER_NAME = '用户名已被使用' +ERROR_EXISTED_EMAIL_ADDRESS = '邮箱已经使用' +ERROR_INVALID_OP = '违法操作' +ERROR_DUPLICATED_APPLY = '重复申请' + + +async def check_user(data): + login_name = data.get('name') + password = data.get('password') + user = await find_user_exists(login_name) + if not user or user.password != password: + return ERROR_WRONG_USER_INFO, False + token = create_jwt_token(user.nick_name) + key = get_user_token_key(user.nick_name) + await redis.conn.hset('testlib', key, token) + return token, True + + +async def logout_jwt(name): + if not await redis.conn.hexists('testlib', get_user_token_key(name)): + return '用户未登录', False + await redis.conn.hdel('testlib', get_user_token_key(name)) + return None, True + + +async def register_user(data): + data.update(dict({'token': create_private_secret()})) + try: + await User().save(data) + except IntegrityError: + if await User.query_obj_one(User.nick_name == data['nick_name']): + return ERROR_DUPLICATED_USER_NAME, False + if await User.query_obj_one(User.email == data['email']): + return ERROR_EXISTED_EMAIL_ADDRESS, False + return None, True + + +async def get_user_info(name): + return await User.query_dict_one(User.nick_name == name) + + +async def refresh_private_token(name): + user = await User.query_obj_one(User.nick_name == name) + user.token = create_private_secret() + await user.update() + return user.token + + +async def update_user_info(name, data): + user = await User.query_obj_one(User.nick_name == name) + if 'avatar_url' in data: + user.avatar_url = data['avatar_url'] + if 'name' in data: + user.user_name = data['name'] + await user.update() + return user.to_dict() + + +async def get_user_list(data, page_num=1, page_size=10): + search, match = dict(), dict() + if 'name' in data: + match = {'nick_name': data.get('name')} + if 'role' in data: + search = {'role': data.get('role')} + result = await User.query_page(page_num, page_size, search=search, match=match) + for item in result['data']: + item.pop('password') + item.pop('token') + return result + + +async def apply_senior_role(name, data): + user = await User.query_obj_one(User.nick_name == name) + if user.role == User_Role.ADMIN: + return ERROR_INVALID_OP, False + apply_reason = data.get('apply_reason', None) if data else None + apply_record = await UserRoleOpRecord.query_obj_one( + UserRoleOpRecord.applicant_id == user.id, UserRoleOpRecord.method == User_Role_Op_Method.UPGRADE, + UserRoleOpRecord.review_result == User_Role_Review_Status.INIT) + if apply_record: + return ERROR_DUPLICATED_APPLY, False + await UserRoleOpRecord(applicant=user.nick_name, applicant_id=user.id, apply_reason=apply_reason).save() + return None, True + + +async def get_reviewers(name): + users = await User.query_obj_all(User.nick_name != name, User.role.in_([User_Role.SENIOR, User_Role.COMMON])) + return [user.nick_name for user in users] + + +async def get_review_count(): + return await get_review_count_infos() + + +async def upgrade_role(name, data): + apply_list, user_list = list(), list() + for item in data.get('review_list'): + apply_list.append(item['review_id']) + user_list.append(item['user_id']) + conditions = list() + conditions.append(User.id.in_(user_list)) + conditions.append(User.role == User_Role.SENIOR) + senior_users = await User.query_obj_all(*conditions) + msg = ','.join([user.nick_name for user in senior_users]) + if senior_users: + return f'{msg}已经是高级用户,禁止重复添加', False + return await upgrade_user_roles(name, apply_list, user_list, data.get('review_result'), + data.get('review_reason', None)) + + +async def update_user_role(data, name): + user = await User.query_obj_one(User.id == data.get('user_id')) + if user.role == User_Role.ADMIN: + return ERROR_INVALID_OP, False + if user.role == data['role']: + return f'无效操作,{user.nick_name}已获得当前权限', False + await UserRoleOpRecord(applicant=user.nick_name, applicant_id=user.id, signer=name, has_review=True, + review_result=User_Role_Review_Status.PASS, review_reason='管理员主动变更权限', + method=data['method']).save() + user.role = data['role'] + await user.update() + return None, True + + +async def get_operator_log(page_num, page_size, has_review): + return await UserRoleOpRecord.query_page(page_num=page_num, page_size=page_size, search={'has_review': has_review}) + + +async def delete_user(id_list, name): + conditions = list() + conditions.append(User.id.in_(id_list)) + conditions.append(User.role == User_Role.ADMIN) + users = await User.query_obj_all(User.id.in_(id_list)) + for user in users: + if user.role == User_Role.ADMIN: + return '禁止删除管理员', False + return await batch_delete(users, name) + + +def create_private_secret(): + return ''.join(random.sample(string.ascii_letters + string.digits, 32)) + + +def create_jwt_token(name): + current_time = datetime.datetime.now() + payload = { + "user_name": name, + "exp": current_time + datetime.timedelta(hours=1), + 'iat': current_time, + 'iss': name + } + token = jwt.encode(payload=payload, key=conf.config['JWT_SECRET_KEY'], algorithm='HS256') + return token + + +def validate_jwt_token(token): + try: + payload = jwt.decode(jwt=token, key=conf.config['JWT_SECRET_KEY'], algorithms='HS256') + except exceptions.ExpiredSignatureError: + return 'token已失效', False + except jwt.DecodeError: + return 'token认证失败', False + except jwt.InvalidTokenError: + return '非法的token', False + return payload, True + + +async def validate_access_token(token): + user = await User.query_obj_one(User.token == token) + if not user: + return 'token认证失败', False + return {'user_name': user.nick_name, 'iss': user.nick_name}, True + + +async def validate_admin(name): + # 开源是nickname, 社区是user_name + user = await User.query_obj_one(User.nick_name == name) + if user.role == User_Role.ADMIN: + return True + return False + + +def login_auth(wrapped): + def decorator(func): + @wraps(func) + async def decorated_function(request, *args, **kwargs): + is_use_jwt = True + result, is_authenticated = validate_jwt_token(request.headers.get('testlib')) + if not is_authenticated: + result, is_authenticated = await validate_access_token(request.headers.get('X-Sanic-Token')) + is_use_jwt = False + if is_authenticated: + redis_token = await redis.conn.hget('testlib', get_user_token_key(result['user_name'])) + if is_use_jwt and redis_token.decode('utf-8') != request.headers.get('testlib'): + return rsp(code=401, msg='用户已登出/在其他地点登录') + kwargs['user_infos'] = result + response = await func(request, *args, **kwargs) + return response + else: + return rsp(code=401, msg=result) + + return decorated_function + + return decorator(wrapped) + + +def check_admin(wrapped): + def decorator(func): + @wraps(func) + async def decorated_function(request, *args, **kwargs): + if await validate_admin(kwargs['user_infos']['user_name']): + response = await func(request, *args, **kwargs) + return response + else: + return rsp(code=401, msg=ERROR_NO_OP_PERMISSION) + + return decorated_function + + return decorator(wrapped) + + +def read_auth(wrapped): + def decorator(func): + @wraps(func) + async def decorated_function(request, *args, **kwargs): + is_use_jwt = True + result, is_authenticated = validate_jwt_token(request.headers.get('testlib')) + if not is_authenticated: + result, is_authenticated = await validate_access_token(request.headers.get('X-Sanic-Token')) + is_use_jwt = False + if not is_authenticated: + return rsp(code=401, msg=result) + redis_token = await redis.conn.hget('testlib', get_user_token_key(result['user_name'])) + if is_use_jwt and redis_token.decode('utf-8') != request.headers.get('testlib'): + return rsp(code=401, msg='用户已登出/在其他地点登录') + user = await User.query_obj_one(User.nick_name == result['user_name']) + if user.role == User_Role.COMMON: + return rsp(code=401, msg=ERROR_NO_OP_PERMISSION) + user = user.to_dict() + user['user_name'] = user['nick_name'] + kwargs['user_infos'] = user + return await func(request, *args, **kwargs) + + return decorated_function + + return decorator(wrapped) + + +def login_auth_community(wrapped): + def decorator(func): + @wraps(func) + async def decorated_function(request, *args, **kwargs): + info = await get_anolis_user_info(request.cookies) + if info: + user_info = await check_user_info(info) + kwargs['user_infos'] = user_info + return await func(request, *args, **kwargs) + return handle_no_auth(request) + + return decorated_function + + return decorator(wrapped) + + +def check_community_admin(wrapped): + def decorator(func): + @wraps(func) + async def decorated_function(request, *args, **kwargs): + if kwargs['user_infos']['role'] == User_Role.ADMIN: + response = await func(request, *args, **kwargs) + return response + else: + return rsp(code=401, msg=ERROR_NO_OP_PERMISSION) + + return decorated_function + + return decorator(wrapped) + + +async def get_anolis_user_info(cookies): + auth = cookies.get('_oc_ut', '') + headers = anolis_headers(conf.config['UCENTER_SERVER_KEY']) + headers.setdefault('Authorization', auth) + url = conf.config['UCENTER_SERVER_ADDR'] + f'/ucenter/verifyToken.json' + _, res = await http_request(url, 'get', headers=headers) + if res.get('code') == '00000': + return res.get('data').get('userProfile') + else: + return None + + +async def check_anolis_user_info(user): + res = dict() + res['username'] = user + auth = user in conf.config['BUILD_ADMIN'].split(',') + res['type'] = 'admin' if auth else 'user' + return res + + +def get_user_token_key(user): + return f'user-key-{user}' + + +async def check_user_info(user_info): + user = await User_Community.query_obj_one(User_Community.user_name == user_info['username']) + if not user: + user_dict = dict( + user_name=user_info['username'], + email=user_info['email'], + avatar_url=user_info['avatarUrl'] + ) + if user_info['nickname']: + user_dict.update({'nick_name': user_info['nickname']}) + user = await User_Community().save(user_dict) + return user.to_dict() diff --git a/services/case_service.py b/services/case_service.py new file mode 100644 index 0000000..dfafbf8 --- /dev/null +++ b/services/case_service.py @@ -0,0 +1,668 @@ +import datetime +import tarfile + +import sqlalchemy +import yaml +from sanic.log import logger + +from common.enums import User_Role +from common.utils.excel import write_excel, read_excel +from models.case_model import * +from services.const import ERROR_UN_EXISTED_CASE, ERROR_UN_EXISTED_TS, ERROR_DUPLICATED_NAME, ERROR_LACK_PERMISSION + +ERROR_UN_EXISTED_NODE = '该模块/用例不存在' +ERROR_NO_CASE_EDIT_PERMISSION = '缺少用例编辑权限' +ERROR_NO_CASE_DELETE_PERMISSION = '缺少用例编辑权限' +ERROR_DELETE_CASE_TREE_FAILED = '删除模块失败' +ERROR_CONN_TONE_TIMEOUT = '连接TONE超时' +ERROR_EXISTED_NODE_IN_MODULE = '同一模块下子模块已存在' +ERROR_VALID_CREATE_CASE = '禁止在当前目录创建用例,请在该目录的子目录下创建' +ERROR_VALID_CREATE_MODULE = '禁止在当前目录创建目录,当前目录已经存在用例' +ERROR_VALID_MOVE = '已在当前目录' +ERROR_INVALID_YAML = 'yaml文件格式错误' + + +async def get_case_by_id(case_id): + case = await Case.query_dict_one(Case.id == case_id) + if not case: + return ERROR_UN_EXISTED_CASE, False + return case, True + + +async def get_cases(page_num=1, page_size=50, mod_id=None, is_recursion=False): + if mod_id: + node_list = [mod_id] + node = await CaseTree.query_obj_one(CaseTree.id == mod_id) + if not node: + return ERROR_UN_EXISTED_NODE, False + if is_recursion: + node_list = await __search_child_nodes_by_path(node.path) + return await Case.query_page(page_num, page_size, search={'parent': ','.join([str(i) for i in node_list])}) + result = await Case.query_page(page_num, page_size) + return result + + +async def create_case(data, owner): + parent, path = 0, '/' + if 'parent' in data and data['parent'] != 0: + parent_node = await CaseTree.query_obj_one(CaseTree.id == data['parent']) + if not parent_node: + return ERROR_UN_EXISTED_NODE, False + parent, path = data['parent'], parent_node.path + + data['priority'] = int(data['priority']) + data.update(dict({'creator': owner, 'parent': parent})) + if 'custom_fields' in data: + data.update(dict({'custom_fields': data['custom_fields']})) + if 'test_type' in data: + data.update(dict({'type': data['test_type']})) + if 'labels' in data: + data.update({'labels': ','.join(data['labels'])}) + if not await TestSuite.query_obj_one(TestSuite.name == data['suite_name']): + return ERROR_UN_EXISTED_TS, False + try: + result = await Case().save(data) + if 'labels' in data: + await insert_label_map([{'label_name': label, 'case_id': result.id} for label in data['labels']]) + return result.to_dict(), True + except sqlalchemy.exc.IntegrityError: + return ERROR_DUPLICATED_NAME, False + + +async def rename_case(data, user, case_id): + case = await Case.query_obj_one(Case.id == case_id) + if not case: + return ERROR_UN_EXISTED_CASE, False + if case.creator != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_CASE_EDIT_PERMISSION, False + case.name = data['name'] + await case.update() + return case.to_dict(), True + + +async def move_case(data, case_id): + case = await Case.query_obj_one(Case.id == case_id) + if not case: + return ERROR_UN_EXISTED_CASE, False + if case.parent == data['parent']: + return ERROR_VALID_MOVE, False + if data['parent'] != 0: + if not await CaseTree.query_obj_one(CaseTree.id == data['parent']): + return ERROR_UN_EXISTED_NODE, False + case.parent = data['parent'] + await case.update() + return case.to_dict(), True + + +# todo: 前端限制下当前目录下不能移动 +async def move_cases(data): + node = await CaseTree.query_obj_one(CaseTree.id == data['parent']) + if not node: + return ERROR_UN_EXISTED_NODE, False + await update_cases_list(data['parent'], data['cases']) + return None, True + + +async def update_case_content(data, user, case_id): + case = await Case.query_obj_one(Case.id == case_id) + if not case: + return ERROR_UN_EXISTED_CASE, False + if case.creator != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_CASE_EDIT_PERMISSION, False + case.is_available = data['is_available'] + case.run_method = data['run_method'] + case.run_model = data['run_model'] + if data['run_method'] == Case_Run_Method.AUTO.value: + case.tone_case = data['tone_case'] + if 'desc' in data: + case.desc = data['desc'] + if 'pre_condition' in data: + case.pre_condition = data['pre_condition'] + if 'steps' in data: + case.steps = data['steps'] + if 'test_type' in data: + case.type = data['test_type'] + if 'device_type' in data: + case.device_type = data['device_type'] + if 'priority' in data: + case.priority = data['priority'] + if 'custom_fields' in data: + case.custom_fields = data['custom_fields'] + if 'suite_name' in data: + result, ok = await __update_case_suite(data['suite_name'], case) + if not ok: + return result, False + if 'labels' in data: + if await __update_case_labels(data['labels'], case): + case.labels = ','.join(data['labels']) + await case.update() + return case.to_dict(), True + + +async def __update_case_suite(suite, case): + if suite != case.suite_name: + suites = await TestSuite.query_obj_all(TestSuite.name.in_([case.suite_name, suite])) + if len(suites) < 2: + return ERROR_UN_EXISTED_TS, False + case.suite_name = suite + return None, True + + +async def __update_case_labels(curr_labels, case): + prev_labels = [] + if case.labels: + prev_labels = case.labels.split(',') + update_labels = list(set(curr_labels).difference(set(prev_labels))) + delete_labels = list(set(prev_labels).difference(set(curr_labels))) + if len(update_labels) == 0 and len(delete_labels) == 0: + return False + if len(delete_labels) > 0: + await remove_label_map([CaseLabelMap.label_name.in_(delete_labels), CaseLabelMap.case_id == case.id]) + if len(update_labels) > 0: + await insert_label_map([{'label_name': label, 'case_id': case.id} for label in update_labels]) + return True + + +async def update_cases(data, user): + need_update = False + case_ids = data['case_ids'] + if user['role'] == User_Role.JUNIOR.value: + cases = await Case.query_obj_all(Case.creator == user['user_name']) + case_ids = [case.id for case in cases] + return f'无权编辑序号为[{",".join(case_ids)}]的用例', False + + values = dict() + # 是否判断suite存在 + if 'suite_name' in data: + values['suite_name'] = data['suite_name'] + need_update = True + # 是否判断labels存在 + if 'labels' in data: + values['labels'] = ','.join(data['labels']) + need_update = True + + for item in ['type', 'priority', 'run_model', 'is_available']: + if item in data: + values[item] = data[item] + need_update = True + if not need_update: + return '没有需要更新的字段', False + await batch_update_cases(case_ids, values) + if 'labels' in data: + await __batch_update_case_labels(data['labels'], case_ids) + return None, True + + +async def __batch_update_case_labels(curr_labels, case_ids): + await CaseLabelMap.batch_delete(CaseLabelMap.case_id.in_(case_ids)) + insert_list = list() + for case_id in case_ids: + insert_list.extend([{'label_name': label, 'case_id': case_id} for label in curr_labels]) + await insert_label_map(insert_list) + + +async def delete_cases(ids, user): + if user['role'] == User_Role.JUNIOR.value: + case = await Case.query_obj_one(Case.id.in_(ids), Case.creator != user['user_name']) + if not case: + return f'无权删除用例{case.name}', False + await Case.batch_delete(Case.id.in_(ids)) + await remove_label_map([CaseLabelMap.case_id.in_(ids)]) + return None, True + + +async def search_modules(start): + result = await get_distinct_path(start) + return result + + +async def get_follow_modules(node_id, key=None): + conditions = list() + conditions.append(CaseTree.parent == node_id) + if key: + conditions.append(CaseTree.name.contains(str(key))) + result = await CaseTree.query_dict_all(*conditions) + if int(node_id) == 0: + return result, True + node = await CaseTree.query_dict_one(CaseTree.id == node_id) + if not node: + return ERROR_UN_EXISTED_NODE, False + + if node['children_nums'] > 0 and await Case.query_obj_one(Case.parent == node_id): + node.update({'name': '未归类模块', 'children_nums': 0}) + node.update({'path': f'{node["path"]}/', 'parent': node['id'], 'level': node['level'] + 1}) + result.append(node) + return result, True + + +async def get_module_path_list(): + return await get_path_by_group() + + +async def get_follow_module_tree(node_id): + root = { + "level": 0, + "path": "/", + } + if node_id != '0': + root = await CaseTree.query_dict_one(CaseTree.id == node_id) + if not root: + return ERROR_UN_EXISTED_NODE, False + results = await CaseTree.query_dict_all(CaseTree.path.startswith(root['path'])) + module_tree = dict() + for result in results: + if result['parent'] not in module_tree: + module_tree[result['parent']] = list() + module_tree[result['parent']].append(result) + __build_module_tree(module_tree[int(node_id)], module_tree) + root['children'] = module_tree[int(node_id)] + return root, True + + +def __build_module_tree(children, module_tree): + for node in children: + if node['id'] in module_tree: + node['children'] = module_tree[node["id"]] + __build_module_tree(node['children'], module_tree) + + +async def create_case_tree_node(data): + path = f'/{data["name"]}' + level = 1 + if data['parent'] != 0: + parent = await CaseTree.query_obj_one(CaseTree.id == data['parent']) + if not parent: + return ERROR_UN_EXISTED_NODE, False + parent.children_nums += 1 + path = f'{parent.path}/{data["name"]}' + if await CaseTree.query_obj_one(CaseTree.path == path): + return ERROR_EXISTED_NODE_IN_MODULE, False + level = parent.level + 1 + await parent.update() + else: + if await CaseTree.query_obj_one(CaseTree.path == path): + return ERROR_EXISTED_NODE_IN_MODULE, False + + data.update(dict({'path': path, 'level': level})) + case = await CaseTree().save(data) + return case.to_dict(), True + + +async def update_case_tree_node(data, mod_id): + node = await CaseTree.query_obj_one(CaseTree.id == mod_id) + if not node: + return ERROR_UN_EXISTED_NODE, False + if data['parent'] != 0: + parent = await CaseTree.query_obj_one(CaseTree.id == data['parent']) + if not parent: + return ERROR_UN_EXISTED_NODE, False + node.level = parent.level + 1 + node.path = f'{parent.path}/{node.name}' + parent.children_nums += 1 + await parent.update() + else: + node.path = f'/{node.name}' + node.level = 1 + if node.parent != 0: + old_parent = await CaseTree.query_obj_one(CaseTree.id == node.parent) + old_parent.children_nums -= 1 + await old_parent.update() + node.parent = data['parent'] + await node.update() + return node.to_dict(), True + + +async def rename_case_tree_node(data, mod_id): + node = await CaseTree.query_obj_one(CaseTree.id == mod_id) + if not node: + return ERROR_UN_EXISTED_NODE, False + node.path = f'{node.path[:-len(node.name)]}{data["name"]}' + node.name = data['name'] + await node.update() + return node.to_dict(), True + + +# 删除分类只删除目录,原先用例移动被删除目录的上层目录 +async def remove_module(mod_id): + node = await CaseTree.query_obj_one(CaseTree.id == mod_id) + if not node: + return ERROR_UN_EXISTED_NODE, False + node_list = await __search_child_nodes_by_path(node.path) + result, ok = await remove_case_tree_nodes(node.parent, node_list) + if not ok: + logger.error(f'删除分类失败:{result}') + return ERROR_DELETE_CASE_TREE_FAILED, False + if node.parent != 0: + parent = await CaseTree.query_obj_one(CaseTree.id == node.parent) + parent.children_nums += (result - 1) + await parent.update() + return await get_follow_modules(node.parent) + + +async def __search_child_nodes(parent_id, node_list): + for node in await CaseTree.query_obj_all(CaseTree.parent == parent_id): + node_list.append(node.id) + await __search_child_nodes(node.id, node_list) + + +async def __search_child_nodes_by_path(parent_path): + results = await CaseTree.query_obj_all(CaseTree.path.startswith(parent_path)) + return [result.id for result in results] + + +async def import_yaml(file_body, file_name): + yaml_info = yaml.load(file_body, Loader=yaml.FullLoader) + result, ok = await yaml_save_to_case(yaml_info, file_name, 0) + return result, ok + + +async def import_tar(file_body, file_name): + file_name = 'common/static/' + file_name + open(file_name, 'wb').write(file_body) + tar_file = tarfile.open(file_name, 'r') + for file_info in tar_file.getmembers(): + if file_info.isfile(): + if file_info.name.count('/') > 0: + folder = file_info.name.split('/') + level = file_info.name.count('/') + parent = 0 + path = '/' + for dir_name in folder: + if dir_name.count('.yaml') > 0: + yaml_info = yaml.load(tar_file.extractfile(file_info.name).read(), Loader=yaml.FullLoader) + await yaml_save_to_case(yaml_info, dir_name, parent) + else: + path += dir_name + '/' + case_tree = CaseTree( + name=dir_name, + parent=parent, + level=level, + path=path, + children_nums=0 + ) + case_tree_exist = await CaseTree.query_obj_one(CaseTree.name == dir_name) + if case_tree_exist: + parent = case_tree_exist.id + else: + node = await CaseTree.save(case_tree) + parent = node.id + level -= 1 + else: + yaml_info = yaml.load(tar_file.extractfile(file_info.name).read(), Loader=yaml.FullLoader) + await yaml_save_to_case(yaml_info, file_info.name, 0) + return None, True + + +async def yaml_save_to_case(yaml_info, file_name, parent): + if len(yaml_info['测试步骤']) != len(yaml_info['期望结果']): + return ERROR_INVALID_YAML, False + case_name = file_name[:-5] + case_exist = await Case.query_obj_one(Case.name == case_name) + if case_exist: + return ERROR_DUPLICATED_NAME, False + case = dict() + case['name'] = case_name + case['creator'] = yaml_info['作者'] + case['parent'] = parent + case['priority'] = Case_Level(int(yaml_info['优先级'][1:])).value + case['device_arch'] = Device_Arch(yaml_info['支持架构']).value + case['run_method'] = Case_Run_Method.AUTO.value if yaml_info['执行方式'] == '自动' else Case_Run_Method.MANUAL.value + case_type = Case_Type.OTHERS + if yaml_info['测试类型'] == '功能测试': + case_type = Case_Type.FUNCTIONAL + elif yaml_info['测试类型'] == '性能测试': + case_type = Case_Type.PERFORMANCE + case['type'] = case_type.value + case['suite_name'] = yaml_info.get('测试套', 'default') + case['labels'] = yaml_info['通用标签'] + case['desc'] = yaml_info['用例描述'] + case['pre_condition'] = yaml_info['前置条件'] + steps = list() + step_index = 1 + for step in yaml_info['测试步骤']: + step_info = dict() + step_info['id'] = step_index + step_info['step'] = step + step_info['result'] = yaml_info['期望结果'][step_index - 1] + steps.append(step_info) + step_index += 1 + case['steps'] = steps + case['gmt_created'] = datetime.datetime.now() + case['gmt_modified'] = datetime.datetime.now() + result = await Case().save(case) + return result, True + + +async def import_excel(file_body): + columns_map = { + '用例名称': 'name', + '创建者': 'creator', + '用例类型': 'type', + '优先级': 'priority', + '执行方式': 'run_method', + '执行模式': 'run_model', + '是否可用': 'is_available', + '关联的tone用例': 'tone_case', + '设备类型': 'device_type', + '设备架构': 'device_arch', + '所属模块': 'parent', + '创建时间': 'gmt_created', + '改动时间': 'gmt_modified', + '用例描述': 'base_fields', + '扩展': 'custom_fields', + } + cases = await read_excel(file_body, columns_map) + case_nodes = await CaseTree.query_obj_all() + case_nodes_dict = {node.path: node.id for node in case_nodes} + case_nodes_dict['/'] = 0 + for case in cases: + if case['parent'] not in case_nodes_dict: + node = await CaseTree.save(CaseTree(name=case['parent'][1:], path=case['parent'])) + case_nodes_dict[node.path] = node.id + else: + case['parent'] = case_nodes_dict[case['parent']] + case['gmt_created'] = case['gmt_created'].to_pydatetime() + case['gmt_modified'] = case['gmt_modified'].to_pydatetime() + await insert_case_list(cases) + return + + +async def export_excel(case_ids): + case_nodes = await CaseTree.query_obj_all() + case_nodes_dict = {node.id: node.path for node in case_nodes} + case_nodes_dict[0] = '/' + cases = await Case.query_obj_all(Case.id.in_(case_ids)) + content = { + 'IDS': list(), + '用例名称': list(), + '创建者': list(), + '用例类型': list(), + '优先级': list(), + '执行方式': list(), + '执行模式': list(), + '是否可用': list(), + '关联的tone用例': list(), + '设备类型': list(), + '设备架构': list(), + '所属模块': list(), + '创建时间': list(), + '改动时间': list(), + '用例描述': list(), + '扩展': list(), + } + for case in cases: + content['IDS'].append(case.id) + content['用例名称'].append(case.name) + content['创建者'].append(case.creator) + content['用例类型'].append(case.type.value) + content['优先级'].append(case.priority.value) + content['执行方式'].append(case.run_method.value) + content['执行模式'].append(case.run_model.value) + content['是否可用'].append(case.is_available) + content['关联的tone用例'].append(case.tone_case) + content['设备类型'].append(case.device_type.value) + content['设备架构'].append(case.device_arch.value) + content['所属模块'].append(case_nodes_dict[case.parent]) + content['创建时间'].append(case.gmt_created) + content['改动时间'].append(case.gmt_modified) + content['用例描述'].append(case.base_fields) + content['扩展'].append(case.custom_fields) + return await write_excel(content) + + +async def search_case(data, page_num, page_size): + conditions = dict() + path, level = '', 1 + if 'mod_id' in data: + node = await CaseTree.query_obj_one(CaseTree.id == data.get('mod_id')) + if not node: + return ERROR_UN_EXISTED_NODE, False + path, level = node.path, node.level + if 'is_recursion' in data and not data.get('is_recursion'): + conditions['parent'] = data.get('mod_id') + else: + node_list = await __search_child_nodes_by_path(node.path) + conditions['parent'] = ','.join([str(i) for i in node_list]) + if 'labels' in data: + case_ids = await get_cases_by_label_names(data['labels']) + conditions['id'] = ','.join(case_ids) + if 'is_available' in data: + conditions['is_available'] = '1' if data.get('is_available') == 'true' else '0' + if 'priority' in data: + pr_list = list() + for pr in data['priority']: + if __get_priority(pr): + pr_list.append(__get_priority(pr)) + conditions['priority'] = ','.join(pr_list) + advanced_search_condition = ['run_model', 'device_arch', 'start_time', 'end_time'] + multi_advanced_search_condition = ['type', 'suite_name', 'creator', 'run_method', 'device_type'] + for c in advanced_search_condition: + if c in data: + conditions[c] = data.get(c) + for c in multi_advanced_search_condition: + if c in data: + conditions[c] = ','.join(data[c]) + if not data.get('key'): + result = await Case.query_page(page_num, page_size, conditions) + else: + result = await Case.query_page(page_num, page_size, conditions, {'name': data.get('key')}) + result.update({'path': path, 'level': level}) + return result + + +def __get_priority(pr): + pr_dict = { + '0': 'PRIORITY_0', + '1': 'PRIORITY_1', + '2': 'PRIORITY_2', + '3': 'PRIORITY_3' + } + return pr_dict.get(pr, None) + + +async def excel_template(): + filepath = 'common/static/temp.xlsx' + with open(filepath, "rb") as f: + content = f.read() + return content + + +async def get_test_suites(is_paginate=True, name=None, page_num=1, page_size=20): + if not is_paginate: + return await get_total_suite_name(name) + match = None + if name: + match = {'name': f'{name}%'} + return await TestSuite.query_page(page_num, page_size, match=match) + + +async def create_test_suite(data, name): + data.update({'creator': name}) + try: + ts = await TestSuite().save(data) + return ts.to_dict(), True + except sqlalchemy.exc.IntegrityError: + return ERROR_DUPLICATED_NAME, False + + +async def rename_test_suite(data, user): + ts = await TestSuite.query_obj_one(TestSuite.id == data['id']) + if not ts: + return ERROR_UN_EXISTED_TS, False + if ts.creator != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_LACK_PERMISSION, False + ts.name = data['name'] + try: + await ts.update() + return ts.to_dict(), False + except sqlalchemy.exc.IntegrityError: + return ERROR_DUPLICATED_NAME, False + + +async def remove_test_suite(ids, user): + conditions = [TestSuite.id.in_(ids)] + if user['role'] == User_Role.JUNIOR.value: + conditions.append(TestSuite.creator == user['user_name']) + tss = await TestSuite.query_obj_all(*conditions) + ts_ids = list() + for ts in tss: + ts_ids.append(f'{ts.id}') + if await Case.query_obj_one(Case.suite_name == ts.name): + return f'测试套f{ts.name}中测试不为空,请清空后删除', False + if user['role'] == User_Role.JUNIOR.value: + diff_list = list(set(ids).difference(set(ts_ids))) + if len(diff_list) > 0: + return f'无权删除测试套{",".join(diff_list)}', True + await TestSuite.batch_delete(TestSuite.id.in_(ids)) + return None, True + + +async def get_label_list(is_paginate=True, name=None, page_num=1, page_size=20): + if not is_paginate: + return await get_total_label_name(name) + match = None + if name: + match = {'name': f'{name}%'} + return await CaseLabel.query_page(page_num, page_size, match=match) + + +async def create_labels(data, name): + data.update(dict({'creator': name})) + try: + label = await CaseLabel().save(data) + return label.to_dict(), True + except sqlalchemy.exc.IntegrityError: + return ERROR_DUPLICATED_NAME, False + + +async def rename_labels(data, user): + label = await CaseLabel.query_obj_one(CaseLabel.id == data['id']) + if not label: + return ERROR_UN_EXISTED_TS, False + if label.creator != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_LACK_PERMISSION, False + label.name = data['name'] + try: + await label.update() + return label.to_dict(), False + except sqlalchemy.exc.IntegrityError: + return ERROR_DUPLICATED_NAME, False + + +async def remove_labels(ids, user): + conditions = [CaseLabel.id.in_(ids)] + if user['role'] == User_Role.JUNIOR.value: + conditions.append(CaseLabel.creator == user['user_name']) + labels = await CaseLabel.query_obj_all(*conditions) + label_ids, label_names = list(), list() + for label in labels: + label_ids.append(f'{label.id}') + label_names.append(label.name) + for item in await search_label_map_group_by_name(label_names): + return f'使用标签{item[0]}的测试用例不为0,请清空后删除', False + if user['role'] == User_Role.JUNIOR.value: + diff_list = list(set(ids).difference(set(label_ids))) + if len(diff_list) > 0: + return f'无权删除标签{",".join(diff_list)}', True + await CaseLabel.batch_delete(TestSuite.id.in_(ids)) + return None, True diff --git a/services/common_service.py b/services/common_service.py new file mode 100644 index 0000000..6e7707a --- /dev/null +++ b/services/common_service.py @@ -0,0 +1,45 @@ +import re +from uuid import uuid4 + +from app import redis +from app.conf import conf +from app.oss import alg_oss +from common.token import common_sign + + +def check_token(data): + token = data.get('X-Sanic-Token') + timestamp = data.get('X-Sanic-Timestamp') + key = conf.config['SANIC_KEY'] + if not key: + return True + + _, sign = common_sign(key, timestamp) + if token == sign: + return True + else: + return False + + +async def store(content, content_type): + key = get_content_type_key(content_type) + await redis.conn.hset('common', key, content) + return key + + +async def download(key): + file_body = await redis.conn.hget('common', key) + return file_body + + +def get_content_type_key(content_type): + if content_type == 'image': + return 'image-{}'.format(uuid4()) + return 'others-{}'.format(uuid4()) + + +def upload_static_file_to_oss(filename, file_bytes): + alg_oss.put_object_from_bytes(filename, file_bytes) + oss_link = alg_oss.get_sign_url(filename) + oss_link = re.sub(r'&Expires=.*&', '&', oss_link) + return oss_link diff --git a/services/const.py b/services/const.py new file mode 100644 index 0000000..d06c703 --- /dev/null +++ b/services/const.py @@ -0,0 +1,20 @@ +# 常用常量 +MAX_CASE_TERR_LEVEL = 5 + +# 错误常量 +ERROR_NO_REQUIREMENT_PERMISSION = '缺少文档操作权限' +ERROR_FAILED_OUTLINE_DELETE = '删除大纲失败' +ERROR_UN_EXISTED_FILE = '文档不存在' +ERROR_UN_EXISTED_PLAN = '方案不存在' +ERROR_UN_EXISTED_CASE = '用例不存在' +ERROR_UN_EXISTED_TS = '测试套不存在' +ERROR_UN_EXISTED_DEVICE = '设备不存在' +ERROR_UN_EXISTED_TASK = '任务不存在' +ERROR_UN_EXISTED_REQUIREMENT = '任务不存在' +ERROR_UN_EXISTED_OUTLINE = '测试大纲不存在' +ERROR_NO_OP_PERMISSION = '缺少操作权限' +ERROR_UN_EXISTED_USER = '用户不存在' +ERROR_LACK_PERMISSION = '缺少权限' +ERROR_DUPLICATED_NAME = '该命名已使用' + +ERROR_UN_EXISTED_TONE_JOB = 'tone job不存在' diff --git a/services/device_service.py b/services/device_service.py new file mode 100644 index 0000000..373c89d --- /dev/null +++ b/services/device_service.py @@ -0,0 +1,96 @@ +from common.enums import User_Role +from models.device_model import Device, DeviceLabel +from services.const import ERROR_NO_OP_PERMISSION, ERROR_UN_EXISTED_DEVICE + + +async def get_devices(page_num=1, page_size=10): + return await Device.query_page(page_num=page_num, page_size=page_size) + + +async def get_devices_by_type(types): + return await Device.query_dict_all(Device.type.in_(types)) + + +async def get_device_by_id(device_id): + device = await Device.query_obj_one(Device.id == device_id) + if not device: + return ERROR_UN_EXISTED_DEVICE, False + return device.to_dict(), True + + +async def add_device(data, creator): + data.update(dict({'owner': creator})) + # todo 缺少事务 + labels = None + if 'label' in data: + labels = await DeviceLabel.query_obj_all(DeviceLabel.id.in_(data['label'])) + device_label = list() + for label in labels: + device_label.append({'id': label.id, 'name': label.name, 'color': label.color}) + data.update(dict({'label': device_label})) + device = await Device().save(data) + # if labels: + # relations = [{"label_id": label.id, "device_id": device.id} for label in labels] + # if len(relations): + # await DeviceLabelRelationship.batch_add(relations) + return device.to_dict() + + +async def modify_device(data, device_id, user): + device = await Device.query_obj_one(Device.id == device_id) + if not device: + return ERROR_UN_EXISTED_DEVICE, False + if device.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_OP_PERMISSION, False + device.name = data['name'] + device.arch = data['arch'] + device.ip = data['ip'] + device.type = data['type'] + if 'sn' in data: + device.sn = data['sn'] + labels = None + if 'label' in data: + labels = await DeviceLabel.query_obj_all(DeviceLabel.id.in_(data['label'])) + device_label = list() + for label in labels: + device_label.append({'id': label.id, 'name': label.name, 'color': label.color}) + device.label = device_label + await device.update() + # if labels: + # await update_relationships(device_id) + # relations = [{"label_id": label.id, "device_id": device.id} for label in labels] + # if len(relations) > 0: + # await DeviceLabelRelationship.batch_add(relations) + return device.to_dict(), True + + +async def remove_device(device_id, user): + device = await Device.query_obj_one(Device.id == device_id) + if not device: + return ERROR_UN_EXISTED_DEVICE, False + if device.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_OP_PERMISSION, False + await device.remove() + return None, True + + +async def create_new_label(data): + label = await DeviceLabel().save(data) + return label.to_dict() + + +async def query_labels(prefix=None): + if not prefix: + return await DeviceLabel.query_dict_all() + else: + return await DeviceLabel.query_dict_all(DeviceLabel.name.like(f'{prefix}%')) + + +async def remove_label(label_id): + label = await DeviceLabel.query_obj_one(DeviceLabel.id == label_id) + if not label: + return ERROR_NO_OP_PERMISSION, False + # todo: 标签下仍有机器,禁止删除 + # relations = await DeviceLabelRelationship.query_obj_all(DeviceLabelRelationship.label_id == label.id) + await label.remove() + return None, True diff --git a/services/outline_service.py b/services/outline_service.py new file mode 100644 index 0000000..896f961 --- /dev/null +++ b/services/outline_service.py @@ -0,0 +1,85 @@ +import re +from datetime import datetime +from uuid import uuid4 + +from app import alg_oss +from common.enums import User_Role +from common.http import read_file_content +from models.outline_model import Outline +from services.const import ERROR_NO_REQUIREMENT_PERMISSION, ERROR_UN_EXISTED_OUTLINE, ERROR_FAILED_OUTLINE_DELETE + +OUTLINE_KEY = 'outline_dict' + + +async def get_outlines(page_num=1, page_size=10): + return await Outline.query_page(page_num=page_num, page_size=page_size) + + +async def query_outlines(prefix): + if prefix == '': + return await Outline.query_dict_all() + else: + return await Outline.query_dict_all(Outline.name.like(f'{prefix}%')) + + +async def store(file_name, file_body, file_type, owner, title, remark): + tid = f'{uuid4()}{file_type}' + alg_oss.put_object_from_bytes(tid, file_body) + result = await Outline.save( + Outline(name=file_name, title=title, owner=owner, tid=tid, remark=remark)) + return result.to_dict() + + +async def update(file_id, file, data, user): + outline = await Outline.query_obj_one(Outline.id == file_id) + if not outline: + return ERROR_UN_EXISTED_OUTLINE, False + if outline.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + + if file: + alg_oss.put_object_from_bytes(outline.tid, file.body) + outline.name = file.name + if data.get('remark'): + outline.remark = data.get('remark') + outline.title = data.get('title') + outline.gmt_modified = datetime.now() + await outline.update() + return 'success', True + + +async def modify_title(title, file_id, user): + outline = await Outline.query_obj_one(Outline.id == file_id) + if not outline: + return ERROR_UN_EXISTED_OUTLINE, False + if outline.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + outline.title = title + outline.gmt_modified = datetime.now() + await outline.update() + return outline.to_dict(), True + + +async def extract(file_id): + outline = await Outline.query_obj_one(Outline.id == file_id) + if not outline: + return ERROR_UN_EXISTED_OUTLINE, False + oss_link = alg_oss.get_sign_url(outline.tid) + oss_link = re.sub(r'&Expires=.*&', '&', oss_link) + oss_link = oss_link.replace('https', 'http') + file_body = await read_file_content(oss_link) + if not file_body: + return '下载失败', False + return (outline.name, file_body), True + + +async def remove(file_id, user): + outline = await Outline.query_obj_one(Outline.id == file_id) + if not outline: + return ERROR_UN_EXISTED_OUTLINE, False + if outline.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if not alg_oss.remove_object(outline.tid): + return ERROR_FAILED_OUTLINE_DELETE, False + await outline.remove() + return 'success', True diff --git a/services/plan_service.py b/services/plan_service.py new file mode 100644 index 0000000..db1210b --- /dev/null +++ b/services/plan_service.py @@ -0,0 +1,310 @@ +from datetime import datetime + +import sqlalchemy + +from app import conf +from common.enums import Status_EN, Task_Run_Method, Case_Type, Case_Run_Method, User_Role +from models.case_model import Case +from models.device_model import Device +from models.job_model import FuncResult +from models.plan_model import Plan, PlanReview +from models.requirement_model import Requirement +from models.task_model import Task, update_manual_task_status +from services.const import ERROR_UN_EXISTED_PLAN, ERROR_NO_REQUIREMENT_PERMISSION, ERROR_UN_EXISTED_REQUIREMENT +from services.tone_job_service import create_tone_job + +ERROR_UN_LIMIT_RE_REVIEW = '无须重新发起评审' +ERROR_NO_AUTO_TASK = '没有需要执行的任务' + +PLAN_KEY = 'plan_dict' + + +async def get_plans(content=None, page_num=1, page_size=10): + if not content: + return await Plan.query_page(page_num=page_num, page_size=page_size) + return await Plan.query_page(page_num, page_size, match={'title': content}) + + +async def create_plan(data, owner): + requirement = await Requirement.query_obj_one(Requirement.id == data['req_id']) + if not requirement: + return ERROR_UN_EXISTED_REQUIREMENT, False + data.update(dict({'owner': owner, "status": Status_EN.INIT.value, 'req_title': requirement.title})) + if 'tasks' in data: + tasks = [str(task) for task in data['tasks']] + data['tasks'] = ','.join(tasks) + try: + result = await Plan().save(data) + return result.to_dict(), True + # for reviewer in data['reviewers'].split(): + # await PlanReview.save(PlanReview(plan_id=result.id, reviewer=reviewer)) + except sqlalchemy.exc.IntegrityError: + return f'{data["title"]}已经存在', False + + +async def get_plan_tasks(plan_id): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + if not plan.tasks: + return {'tasks': [], 'cases': []}, True + tasks = await Task.query_dict_all(Task.id.in_(plan.tasks.split(','))) + task_id_list, case_id_list = list(), list() + for task in tasks: + task_id_list.append(str(task['id'])) + if task['cases']: + case_id_list.extend(task['cases'].split(',')) + plan.tasks = ','.join(task_id_list) + await plan.update() + cases = await Case.query_dict_all(Case.id.in_(case_id_list)) + return {'tasks': tasks, 'cases': cases}, True + + +async def add_tasks(data, plan_id): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + tasks = ','.join(str(x) for x in data['tasks']) + if not plan.tasks: + plan.tasks = tasks + else: + plan.tasks = f'{plan.tasks},{tasks}' + await plan.update() + return plan.to_dict(), True + + +async def get_plan_by_id(plan_id): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + if plan.status != Status_EN.FINISH.value and plan.tasks: + tasks = await Task.query_obj_all(Task.id.in_(plan.tasks.split(',')), Task.status.in_( + [Status_EN.SUCCESS.value, Status_EN.FAIL.value, Status_EN.SKIP.value])) + if len(tasks) == len(plan.tasks.split(',')): + plan.status = Status_EN.FINISH.value + await plan.update() + # plan_review_list = await PlanReview.query_obj_all(PlanReview.plan_id == plan_id) + # plan['review_status'] = list() + # for plan_review in plan_review_list: + # plan['review_status'].append({ + # 'reviewer': plan_review.reviewer, + # 'status': plan_review.status, + # 'desc': plan_review.desc, + # }) + return plan.to_dict(), True + + +# 当前设置自己的文件只有自己能更新 +# 用户组设计好为一个用户组的都可以操作该文件 +async def update_plan(data, plan_id, user): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + if plan.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if 'reviewer' in data: + plan.reviewer = data['reviewer'] + if 'tasks' in data: + plan.tasks = ','.join([str(task_id) for task_id in data['tasks']]) + plan.content = data['content'] + plan.gmt_modified = datetime.now() + plan.status = Status_EN.INIT.value + plan.title = data['title'] + await plan.update() + return plan.to_dict(), True + + +async def review_plan(is_accept, plan_id, reviewer): + plan_log = await PlanReview.query_obj_one(Plan.id == plan_id, reviewer == reviewer) + if not plan_log: + return ERROR_UN_EXISTED_PLAN, False + plan_log.status = Status_EN.ACCEPTED.value if is_accept else Status_EN.REFUSED.value + await plan_log.update() + plan = await Plan.query_obj_one(Plan.id == plan_id) + plan.status = Status_EN.RUNNING.value if is_accept else Status_EN.ANALYZING.value + await plan.update() + return plan.to_dict(), True + + +async def re_review_plan(plan_id, owner): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + if plan.status != Status_EN.ANALYZING.value: + return ERROR_UN_LIMIT_RE_REVIEW, False + if owner != plan.owner: + return ERROR_NO_REQUIREMENT_PERMISSION, False + for reviewer in plan.reviewers.split(): + await PlanReview.save(PlanReview(plan_id=plan_id, reviewer=reviewer)) + plan.status = Status_EN.INIT.value + await plan.update() + return plan.to_dict(), True + + +async def delete(plan_id, user): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + if plan.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + await plan.remove() + if plan.tasks: + await Task.batch_delete(Task.id.in_(plan.tasks.split(','))) + return 'success', True + + +async def run_task(plan_id, user): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if plan.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if not plan.tasks: + return ERROR_NO_AUTO_TASK, False + task_ids = plan.tasks.split(',') + auto_task = await Task.query_obj_all(Task.id.in_(task_ids), Task.run_method == Task_Run_Method.AUTO) + await __start_auto_task(auto_task) + await __start_manual_task(task_ids) + plan.status = Status_EN.RUNNING + await plan.update() + return None, True + + +async def generate_plan_report(plan_id): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if plan.report: + return '报告已生成', False + plan.report = True + await plan.update() + return True, False + + +async def __start_auto_task(auto_tasks): + for task in auto_tasks: + case_id_list = task.cases + if not task.cases: + task.status = Status_EN.FINISH.value + await task.update() + continue + case_list = await Case.query_obj_all(Case.id.in_(case_id_list.split(','))) + test_config = list() + for case in case_list: + if case.custom_fields: + suite_name = case.custom_fields.get('tone_suite_name') + case_name = case.custom_fields.get('tone_case_name') + exist_suite = [suite for suite in test_config if suite['test_suite'] == suite_name] + if len(exist_suite) > 0: + exist_case = [case for case in exist_suite[0]['cases'] == case_name] + if len(exist_case) > 0: + continue + else: + exist_suite[0]['cases'].append(dict({'test_case': case_name})) + else: + test_suite = dict({'test_suite': suite_name, 'cases': [{'test_case': case_name}]}) + test_config.append(test_suite) + data = dict({ + 'task_id': task.id, + 'test_config': test_config, + 'job_type': conf.config['TONE_FUNC_JOB'] + }) + await create_tone_job(data) + task.status = Status_EN.RUNNING.value + await task.update() + + +async def __start_manual_task(task_ids): + await update_manual_task_status(task_ids) + + +def gen_plan_key(uuid): + return 'plan-{}'.format(uuid) + + +async def export_plan(plan_id): + plan = await Plan.query_obj_one(Plan.id == plan_id) + if not plan: + return ERROR_UN_EXISTED_PLAN, False + result = plan.to_dict() + result['test_result'] = get_plan_template() + if not plan.tasks: + return result + task_dict, case_list, device_list = dict(), list(), list() + tasks = await Task.query_obj_all(Task.id.in_(plan.tasks.split(','))) + for task in tasks: + task_dict[task.id] = task + device_list.append(task.device_id) + if task.cases: + case_list.extend(task.cases.split(',')) + result['device_info'] = await Device.query_dict_all(Device.id.in_(device_list)) + cases = await Case.query_obj_all(Case.id.in_(case_list), Case.is_available == True) + case_id_dict, case_name_dict = dict(), dict() + for case in cases: + case_id_dict[case.id] = case + case_name_dict[case.name] = case + + auto_task_ids = list() + for task in tasks: + if task.run_method == Task_Run_Method.AUTO: + auto_task_ids.append(task.id) + result['test_result']['task']['auto_task'].append(task.to_dict()) + else: + # 处理手动功能测试用例结果 + for case in task.run_result['case_infos']: + case['status'] = case['result'] + __update_func_result_count(case, result) + result['test_result']['task']['manual_task'].append(task.to_dict()) + + # 处理自动的功能测试用例结果 + # todo 建立tone用例和testlib用例之间的关联,通建立一个hash map + de_dict = dict() + func_cases = await FuncResult.query_obj_all(FuncResult.task_id.in_(auto_task_ids)) + for func_case in func_cases: + case_name = func_case.sub_case_name + if case_name not in de_dict: + de_dict[case_name] = '' + else: + case_name = f'task-{func_case.task_id}-{case_name}' + auto_case = { + "name": case_name, + "type": Case_Type.FUNCTIONAL.value, + "result": func_case.sub_case_result, + "creator": task_dict[func_case.task_id].owner, + "priority": '0', + "run_method": Case_Run_Method.AUTO.value, + "suite_name": '/default', + "status": func_case.sub_case_result.value + } + __update_func_result_count(auto_case, result) + return result + + +def __update_func_result_count(case, result): + result['test_result']['func_test']['cases'].append(case) + if case['status'] in ('success', 'pass', '通过', '成功'): + case['status'] = 'success' + result['test_result']['func_test']['pass_rate']['pass'] += 1 + elif case['status'] in ('fail', '失败', '不通过'): + case['status'] = 'fail' + result['test_result']['func_test']['pass_rate']['fail'] += 1 + else: + result['test_result']['func_test']['pass_rate']['invalid'] += 1 + + +def get_plan_template(): + return { + 'device_info': list(), + 'func_test': { + 'cases': list(), + 'pass_rate': { + 'pass': 0, + 'fail': 0, + 'invalid': 0, + } + }, + 'task': { + 'auto_task': list(), + 'manual_task': list() + } + } diff --git a/services/requirement_service.py b/services/requirement_service.py new file mode 100644 index 0000000..6b2b21c --- /dev/null +++ b/services/requirement_service.py @@ -0,0 +1,87 @@ +from datetime import datetime +from uuid import uuid4 + +from common.enums import Status_EN, User_Role +from models.outline_model import Outline +from models.requirement_model import Requirement +from services.const import ERROR_NO_REQUIREMENT_PERMISSION, ERROR_UN_EXISTED_TASK, ERROR_UN_EXISTED_REQUIREMENT + +REQUIREMENT_KEY = 'requirement_dict' + + +async def get_requirements(page_num=1, page_size=10): + return await Requirement.query_page(page_num=page_num, page_size=page_size) + + +async def query_requirements(user, prefix): + if prefix == '': + return await Requirement.query_dict_all() + else: + return await Requirement.query_dict_all(Requirement.title.like(f'{prefix}%')) + + +async def create_requirement(data, owner): + key = gen_requirement_key(uuid4()) + if 'outline_id' in data: + outline = await Outline.query_obj_one(Outline.id == data['outline_id']) + if not outline: + return ERROR_UN_EXISTED_TASK, False + data.update(dict({"outline_title": outline.title})) + data.update(dict(content_id=key, owner=owner)) + result = await Requirement().save(data) + return result.to_dict(), True + + +async def get_requirement_by_id(req_id, person): + rq = await Requirement.query_dict_one(Requirement.id == req_id) + if not rq: + return ERROR_NO_REQUIREMENT_PERMISSION, False + person_list = rq['assignee'].split() + person_list.append(rq['owner']) + if person not in person_list: + return ERROR_NO_REQUIREMENT_PERMISSION, False + return rq, True + + +async def update(req_id, data): + rq = await Requirement.query_obj_one(Requirement.id == req_id) + if not rq: + return ERROR_UN_EXISTED_REQUIREMENT, False + if 'content' in data: + rq.content = data['content'] + rq.title = data['title'] + rq.assignee = data['assignee'] + outline = await Outline.query_obj_one(Outline.id == data['outline_id']) + if not outline: + return ERROR_UN_EXISTED_TASK, False + rq.outline_title = outline.title + rq.outline_id = data['outline_id'] + rq.gmt_modified = datetime.now() + rq.status = Status_EN.INIT.value + await rq.update() + return rq.to_dict(), True + + +async def review(rq_id, is_accept, reviewer): + rq = await Requirement.query_obj_one(Requirement.id == rq_id) + if not rq: + return ERROR_NO_REQUIREMENT_PERMISSION, False + if reviewer not in rq.assignee: + return ERROR_NO_REQUIREMENT_PERMISSION, False + rq.status = Status_EN.ACCEPTED.value if is_accept else Status_EN.REFUSE.value + await rq.update() + return rq.to_dict(), True + + +async def delete(rq_id, user): + rq = await Requirement.query_obj_one(Requirement.id == rq_id) + if not rq: + return ERROR_UN_EXISTED_REQUIREMENT, False + if rq.owner != user['user_name'] and user['role'] in (User_Role.JUNIOR.value, User_Role.COMMON.value): + return ERROR_NO_REQUIREMENT_PERMISSION, False + await rq.remove() + return 'success', True + + +def gen_requirement_key(uuid): + return 'requirement-{}'.format(uuid) diff --git a/services/task_service.py b/services/task_service.py new file mode 100644 index 0000000..2724dd2 --- /dev/null +++ b/services/task_service.py @@ -0,0 +1,138 @@ +from datetime import datetime + +from common.enums import Task_Run_Method, User_Role +from models.case_model import Case +from models.plan_model import Plan +from models.task_model import Task +from services.const import ERROR_NO_OP_PERMISSION, ERROR_UN_EXISTED_TASK + +ERROR_UNEXISTED_CASE_IN_PLAN = '该用例不存在于当前方案中,禁止关联' + + +async def get_tasks(name=None, plan_id=None, is_paginate='', page_num=1, page_size=10): + conditions_list, conditions_dict = [], dict() + if name: + conditions_list.append(Task.name.startswith(name)) + conditions_dict['name'] = f'{name}%' + if plan_id: + conditions_list.append(Task.id == plan_id) + conditions_dict['plan_id'] = plan_id + if is_paginate == 'false': + return Task.query_dict_all(*conditions_list) + return await Task.query_page(page_num=page_num, page_size=page_size, search=conditions_dict) + + +async def get_task_by_id(task_id): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + if task.run_method != Task_Run_Method.MANUAL: + return task.to_dict(), True + return task.to_dict(), True + + +async def __gen_run_result_case_infos(case_ids): + case_infos = list() + cases = await Case.query_obj_all(Case.id.in_(case_ids)) + for case in cases: + case_infos.append({ + 'name': case.name, + 'suite_name': case.suite_name, + 'priority': case.priority, + 'creator': case.creator, + 'type': case.type, + 'run_method': 'manual', + 'result': '' + }) + return case_infos + + +async def create_task(data, creator): + plan = await Plan.query_obj_one(Plan.id == data['plan_id']) + case_ids = ','.join(str(x) for x in data['cases']) + case_infos = await __gen_run_result_case_infos(data['cases']) + data.update({'run_result': {'case_infos': case_infos, 'remark': {}}}) + data.update(dict({'owner': creator, 'cases': case_ids, 'plan_title': plan.title})) + result = await Task().save(data) + if plan.tasks == '': + plan.tasks = f'{result.id}' + else: + plan.tasks = f'{plan.tasks},{result.id}' + await plan.update() + return result.to_dict(), True + + +async def found_cases(task_id): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + plan = await Plan.query_obj_one(Plan.id == task.plan_id) + plan_cases = plan.cases.split(',') + task_cases = task.cases.split(',') + return list(set(plan_cases).difference(task_cases)), True + + +async def modify_task(data, task_id, user): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + if task.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_OP_PERMISSION, False + if 'name' in data: + task.name = data['name'] + if 'desc' in data: + task.desc = data['desc'] + if 'run_method' in data: + task.run_method = data['run_method'] + if 'device_id' in data and 'device_ip' in data: + # todo: 校验ID和IP是否匹配 + task.device_id = data['device_id'] + task.device_ip = data['device_ip'] + if 'config' in data: + task.config = data['config'] + if 'cases' in data: + if data['cases']: + case_infos = __gen_run_result_case_infos(data['cases'].split(',')) + task.run_result['case_infos'] = case_infos + else: + task.run_result['case_infos'] = [] + task.gmt_modified = datetime.now() + await task.update() + return task.to_dict(), True + + +async def remove_task(task_id, user): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + if task.owner != user['user_name'] and user['role'] == User_Role.JUNIOR.value: + return ERROR_NO_OP_PERMISSION, False + plan = await Plan.query_obj_one(Plan.id == task.plan_id) + if plan: + plan_task_list = plan.tasks.split(',') + plan_task_list.remove(str(task.id)) + plan.tasks = ','.join(plan_task_list) + await plan.update() + await task.remove() + return None, True + + +async def add_run_result(data, task_id): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + if task.run_method == Task_Run_Method.MANUAL.value: + task.run_result = data['result'] + task.status = data['state'] + task.gmt_modified = datetime.now() + await task.update() + return task.to_dict(), True + + +async def update_task_status(status, task_id): + task = await Task.query_obj_one(Task.id == task_id) + if not task: + return ERROR_UN_EXISTED_TASK, False + task.status = status + await task.update() + return task.to_dict(), True diff --git a/services/tone_job_service.py b/services/tone_job_service.py new file mode 100644 index 0000000..8efb9d9 --- /dev/null +++ b/services/tone_job_service.py @@ -0,0 +1,156 @@ +import time + +from app.conf import conf +from common.enums import Tone_Job_State, Track_Result, Case_Result, Status_EN +from models.job_model import ToneJob, PerfResult, FuncResult +from services.const import ERROR_UN_EXISTED_TONE_JOB +from services.task_service import update_task_status +from common.tone.tone_request import get_res_from_tone +from common.tone.api import TONE_JOB_QUERY, TONE_CREATE_JOB + + +async def create_job(data, task_id): + tone_job = {'name': data.get('job_name'), 'state': Tone_Job_State.PENDING.value, + 'test_type': data.get('test_type'), 'tone_job_id': data.get('job_id'), 'task_id': task_id} + result = await ToneJob().save(tone_job) + await update_task_status(Status_EN.RUNNING.value, task_id) + return result.to_dict(), True + + +async def update_job(data, is_finished): + tone_job = await ToneJob.query_obj_one(ToneJob.tone_job_id == data.get('job_id')) + if not tone_job: + return ERROR_UN_EXISTED_TONE_JOB, False + tone_job.tone_job_link = data.get('job_link') + try: + tone_job.state = Tone_Job_State(data.get('job_status')).value + except: + tone_job.state = Tone_Job_State.SUCCESS.value + result = await tone_job.update() + if is_finished: + unfinished_job = await ToneJob.query_obj_one(ToneJob.task_id == tone_job.task_id and + ToneJob.state != Tone_Job_State.SUCCESS.value) + if not unfinished_job: + await update_task_status(Status_EN.FINISH.value, tone_job.task_id) + return result.to_dict(), True + + +async def save_job_result(data): + test_type = data.get('test_type') + tone_job = await ToneJob.query_obj_one(ToneJob.tone_job_id == data.get('job_id')) + if not tone_job: + return ERROR_UN_EXISTED_TONE_JOB, False + if test_type == 'functional': + await save_func_detail(tone_job.task_id, data.get('job_result'), data.get('job_id')) + else: + await save_perf_detail(tone_job.task_id, data.get('job_result'), data.get('job_id')) + return None, True + + +async def save_func_detail(task_id, job_result, tone_job_id): + func_results = await FuncResult.query_obj_all(FuncResult.tone_job_id == tone_job_id) + if len(func_results) > 0: + func_results.remove() + func_result_list = list() + for func_result in job_result: + test_suite_id = func_result.get('test_suite_id') + test_case_id = func_result.get('test_case_id') + if len(func_result['case_result']) == 0 and func_result['case_state'] == 'fail': + func_result_list.append({ + 'sub_case_name': f'{func_result["test_suite"]}:{func_result["test_case"]}', + 'sub_case_result': Case_Result.SKIP, + 'tone_suite_id': test_suite_id, + 'tone_case_id': test_case_id, + 'tone_job_id': tone_job_id, + 'task_id': task_id + }) + continue + for case_result in func_result.get('case_result'): + if case_result.get('sub_case_result') == 2: + case_state = Case_Result.FAIL.value + elif case_result.get('sub_case_result') == 1: + case_state = Case_Result.SUCCESS.value + else: + case_state = Case_Result.SKIP.value + func_result_obj = dict({ + 'sub_case_name': case_result.get('sub_case_name'), + 'sub_case_result': case_state, + 'tone_suite_id': test_suite_id, + 'tone_case_id': test_case_id, + 'tone_job_id': tone_job_id, + 'task_id': task_id + }) + func_result_list.append(func_result_obj) + await FuncResult().batch_add(func_result_list) + + +async def save_perf_detail(task_id, job_result, tone_job_id): + perf_results = await PerfResult.query_obj_all(PerfResult.tone_job_id == tone_job_id) + if len(perf_results) > 0: + perf_results.remove() + perf_result_list = list() + for perf_result in job_result: + test_suite_id = perf_result.get('test_suite_id') + test_case_id = perf_result.get('test_case_id') + for case_result in perf_result.get('case_result'): + perf_result_obj = dict({ + 'metric': case_result.get('metric'), + 'test_value': case_result.get('test_value'), + 'cv_value': case_result.get('cv_value'), + 'max_value': case_result.get('max_value'), + 'min_value': case_result.get('min_value'), + 'unit': case_result.get('unit'), + 'track_result': Track_Result(case_result.get('track_result')).value, + 'tone_suite_id': test_suite_id, + 'tone_case_id': test_case_id, + 'tone_job_id': tone_job_id, + 'task_id': task_id + }) + perf_result_list.append(perf_result_obj) + await PerfResult().batch_add(perf_result_list) + + +async def create_tone_job(data): + req_data = { + 'workspace': conf.config['TONE_WORKSPACE'], + 'name': "test_lib_job" + str(time.time()), + 'job_type': data.get('job_type'), + 'project': conf.config['TONE_PROJECT'], + 'test_config': data.get('test_config'), + 'callback_api': conf.config['MAIN_DOMAIN'] + 'api/tone/callback' + } + status, result = await get_res_from_tone('post', TONE_CREATE_JOB, req_data) + if status == 200: + if result['success']: + await create_job(result['data'], data.get('task_id')) + else: + return result['msg'], False + return None, False + + +async def query_tone_job(tone_job_id): + req_data = { + 'job_id': tone_job_id + } + status, result = await get_res_from_tone('post', TONE_JOB_QUERY, req_data) + if status == 200: + return result, True + return None, False + + +async def tone_callback(data): + callback_data = data.get('callback_data') + if callback_data: + is_finished = True if data.get('callback_type') == 'job_completed' else False + result, ok = await update_job(callback_data, is_finished) + if not ok: + return result, ok + if data.get('callback_type') == 'job_completed': + result, status = await query_tone_job(callback_data.get('job_id')) + if status and result.get('success'): + detail_data = result.get('data') + if detail_data: + result, ok = await save_job_result(detail_data) + return result, ok + else: + return result, status diff --git a/services/tone_service.py b/services/tone_service.py new file mode 100644 index 0000000..7ce8882 --- /dev/null +++ b/services/tone_service.py @@ -0,0 +1,63 @@ +import asyncio +import json +from models.tone_model import ToneCase, query_tone_case_type, query_tone_suite, ToneSyncPull +from common.tone.api import TONE_SUITE_QUERY +from common.tone.tone_request import get_res_from_tone + + +async def query_suite(data, page_num=1, page_size=20): + conditions = list() + if data.get('test_type'): + conditions.append(ToneCase.test_type == data.get('test_type')) + if data.get('key'): + conditions.append(ToneCase.tone_case_name.startswith(data.get('key'))) + return await query_tone_suite(page_num, page_size, conditions) + + +async def query_tone_cases(data, suite_name): + conditions = [ToneCase.suite_name == suite_name] + if data.get('test_type'): + conditions.append(ToneCase.test_type == data.get('test_type')) + if data.get('key'): + conditions.append(ToneCase.tone_case_name.contains(data.get('key'))) + return await ToneCase.query_dict_all(*conditions) + + +async def get_tone_case_type(): + results = await query_tone_case_type() + return [result for result in results] + + +async def sync_suite(): + while True: + await asyncio.sleep(36000) + last_sync_pull = await ToneSyncPull.query_obj_one() + if last_sync_pull: + last_sync_time = last_sync_pull.gmt_created.strftime('%Y-%m-%d %H:%M:%S') + else: + last_sync_time = '2000-01-01 00:00:00' + result, ok = await query_suite_from_tone(last_sync_time) + if ok and result['data']: + case_list = list() + for case in result['data']: + exist_case = await ToneCase.query_obj_one(ToneCase.tone_case_id == case['case_id']) + if not exist_case: + case_obj = {'tone_case_id': case['case_id'], + 'tone_case_name': case['case_name'], + 'suite_id': case['suite_id'], + 'suite_name': case['suite_name'], + 'test_type': case['test_type']} + case_list.append(case_obj) + if len(case_list) > 0: + await ToneCase().batch_add(case_list) + await ToneSyncPull().save(dict()) + + +async def query_suite_from_tone(last_sync_time): + req_data = { + 'last_sync_time': last_sync_time + } + res = await get_res_from_tone('get', TONE_SUITE_QUERY, req_data) + if res.status_code == 200: + return json.loads(res.text), True + return {}, False -- Gitee