diff --git a/conftest.py b/conftest.py index 268c97b..9a5f3cc 100644 --- a/conftest.py +++ b/conftest.py @@ -63,12 +63,13 @@ from src.pms.suite import Suite from src.pms.send2pms import Send2Pms from src.recording_screen import recording_screen -FLAG_FEEL = '=' * 10 +FLAG_FEEL = "=" * 10 LN = "\n" class LabelType(Enum): """用例级别对应报告级别""" + L1 = allure.severity_level.BLOCKER L2 = allure.severity_level.CRITICAL L3 = allure.severity_level.NORMAL @@ -90,9 +91,7 @@ def write_json(session): def auto_send(session): """auto send""" - return bool( - session.config.option.send_pms and session.config.option.trigger - ) + return bool(session.config.option.send_pms and session.config.option.trigger) def async_send(session): @@ -113,62 +112,43 @@ def finish_send(session): def pytest_addoption(parser): """pytest_cmdline_main""" - parser.addoption( - "--clean", action="store", default="no", help="是否清理环境&杀进程" - ) + parser.addoption("--clean", action="store", default="no", help="是否清理环境&杀进程") parser.addoption( "--log_level", action="store", default=GlobalConfig.LOG_LEVEL, help="终端日志输出级别" ) - parser.addoption( - "--noskip", action="store", default="", help="skip-xxx标签不生效" - ) - parser.addoption( - "--ifixed", action="store", default="", help="fixed-xxx标签不生效" - ) - parser.addoption( - "--max_fail", action="store", default="", help="最大失败次数" - ) + parser.addoption("--noskip", action="store", default="", help="skip-xxx标签不生效") + parser.addoption("--ifixed", action="store", default="", help="fixed-xxx标签不生效") + parser.addoption("--max_fail", action="store", default="", help="最大失败次数") parser.addoption( "--record_failed_case", action="store", default="", help="失败录屏从第几次失败开始录制视频" ) + parser.addoption("--send_pms", action="store", default="", help="用例数据回填") + parser.addoption("--task_id", action="store", default="", help="测试单id") + parser.addoption("--trigger", action="store", default="", help="数据回填的触发者") + parser.addoption("--suite_id", action="store", default="", help="pms的测试套件ID") + parser.addoption("--pms_user", action="store", default="", help="登录pms的账号") + parser.addoption("--pms_password", action="store", default="", help="登录pms的密码") + parser.addoption("--top", action="store", default="", help="过程中记录top命令中的值") parser.addoption( - "--asan", action="store", default="", help="执行安全测试用例" + "--duringfail", + action="store_true", + dest="duringfail", + default=False, + help="出现错误时立即显示", + ) + parser.addoption("--repeat", action="store", default=1, type=int, help="用例重复执行的次数") + parser.addoption("--export_csv_file", action="store", default="", help="导出csv文件") + parser.addoption("--line", action="store", default="", help="业务线(CI)") + parser.addoption("--app_name", action="store", default="", help="执行的应用名称") + parser.addoption( + "--autostart", action="store", default="", help="重启类场景开启letmego执行方案" ) parser.addoption( - "--send_pms", action="store", default="", help="用例数据回填" - ) - parser.addoption( - "--task_id", action="store", default="", help="测试单id" - ) - parser.addoption( - "--trigger", action="store", default="", help="数据回填的触发者" - ) - parser.addoption( - "--suite_id", action="store", default="", help="pms的测试套件ID" - ) - parser.addoption( - "--pms_user", action="store", default="", help="登录pms的账号" - ) - parser.addoption( - "--pms_password", action="store", default="", help="登录pms的密码" - ) - parser.addoption( - "--top", action="store", default="", help="过程中记录top命令中的值" - ) - parser.addoption( - "--duringfail", action="store_true", dest="duringfail", default=False, help="出现错误时立即显示" - ) - parser.addoption( - '--repeat', action='store', default=1, type=int, help="用例重复执行的次数" - ) - parser.addoption( - '--exportcsv', action='store', default="", help="导出测试用例文件" - ) - parser.addoption( - '--line', action='store', default="", help="业务线(CI)" - ) - parser.addoption( - '--autostart', action='store', default="", help="用例执行程序注册到开机自启服务" + "--pyid2csv", + action="store_true", + dest="pyid2csv", + default=False, + help="将用例py文件的case id同步到对应的csv文件中", ) @@ -204,13 +184,13 @@ def pytest_sessionstart(session): """pytest_sessionstart""" # 批量执行之前修改主题 if ( - CmdCtl.run_cmd( - "gsettings get com.deepin.dde.appearance gtk-theme", - interrupt=False, - out_debug_flag=False, - command_log=False, - ).strip("'") - != GlobalConfig.SYS_THEME + CmdCtl.run_cmd( + "gsettings get com.deepin.dde.appearance gtk-theme", + interrupt=False, + out_debug_flag=False, + command_log=False, + ).strip("'") + != GlobalConfig.SYS_THEME ): CmdCtl.run_cmd( f"gsettings set com.deepin.dde.appearance gtk-theme {GlobalConfig.SYS_THEME}", @@ -218,7 +198,11 @@ def pytest_sessionstart(session): out_debug_flag=False, command_log=False, ) - _display = GlobalConfig.DisplayServer.wayland if GlobalConfig.IS_WAYLAND else GlobalConfig.DisplayServer.x11 + _display = ( + GlobalConfig.DisplayServer.wayland + if GlobalConfig.IS_WAYLAND + else GlobalConfig.DisplayServer.x11 + ) logger.info(f"当前系统显示协议为 {_display.title()}.") # 设置任务栏方向 popen("gsettings set com.deepin.dde.dock position bottom") @@ -231,13 +215,16 @@ def pytest_sessionstart(session): suite_id = session.config.option.suite_id if write_json(session): session.case_res_path = Send2Pms.case_res_path(task_id or suite_id) - session.data_send_result_csv = Send2Pms.data_send_result_csv(task_id or suite_id) + session.data_send_result_csv = Send2Pms.data_send_result_csv( + task_id or suite_id + ) if user and password and async_send(session): session.all_thread_task = [] session.t_executor = ThreadPoolExecutor() if not session.config.option.collectonly and session.config.option.top: + def record_top(): top_log_path = f"{GlobalConfig.REPORT_PATH}/logs" if not exists(top_log_path): @@ -265,7 +252,7 @@ def pytest_generate_tests(metafunc): return f"{i + 1}-{number}" metafunc.parametrize( - '__pytest_repeat_step_number', + "__pytest_repeat_step_number", range(repeat), indirect=True, ids=ids, @@ -274,15 +261,57 @@ def pytest_generate_tests(metafunc): def pytest_collection_modifyitems(session): """pytest_collection_modifyitems""" - no_youqu_mark = {} - csv_path_dict = {} - for root, _, files in walk(GlobalConfig.APPS_PATH): - if "NOYOUQUMARK" in files and not no_youqu_mark.get(root): - no_youqu_mark[root] = True - continue - for file in files: - if file.endswith(".csv") and file != "case_list.csv": - csv_path_dict[splitext(file)[0]] = f"{root}/{file}" + + walk_dir = ( + f"{GlobalConfig.APPS_PATH}/{session.config.option.app_name}" + if session.config.option.app_name + else GlobalConfig.APPS_PATH + ) + csv_path_dict, no_youqu_mark = walk_apps(walk_dir) + + if session.config.option.collectonly and session.config.option.pyid2csv: + for item in session.items: + _case_id = findall(r"test_.*?_(\d+)", item.fspath.purebasename) + case_id = _case_id[0] if _case_id else "No match found for case id" + _csv_name = findall(r"test_(.*?)_\d+", item.fspath.purebasename) + csv_name = _csv_name[0] if _csv_name else None + + csv_path = csv_path_dict.get(csv_name) + if not csv_path_dict or not csv_path_dict.get(csv_name): + _dir_name = item.fspath.dirname + if str(_dir_name).endswith("case"): + dir_name = _dir_name.rstrip("/case") + else: + dir_name = _dir_name.replace("/case/", "/tag/") + if not exists(dir_name): + makedirs(dir_name) + csv_path = f"{dir_name}/{csv_name}.csv" + with open(csv_path, "w+", encoding="utf-8") as f: + f.write(",".join([i.value for i in FixedCsvTitle]) + LN) + + csv_path_dict, no_youqu_mark = walk_apps(walk_dir) + + with open(csv_path, "r", encoding="utf-8") as f: + csv_txt_list = f.readlines() + try: + csv_head = csv_txt_list[0] + comma_num = csv_head.count(",") + except IndexError: + with open(csv_path, "w+", encoding="utf-8") as f: + f.write(",".join([i.value for i in FixedCsvTitle]) + LN) + comma_num = len(FixedCsvTitle) - 1 + csv_taglines = [txt.strip().split(",") for txt in csv_txt_list[1:]] + if not csv_taglines: + with open(csv_path, "a+", encoding="utf-8") as f: + f.write(f"{case_id}{comma_num * ','}" + LN) + else: + for i in csv_taglines: + if i[0] == case_id or int(i[0]) == int(case_id): + break + else: + with open(csv_path, "a+", encoding="utf-8") as f: + f.write(f"{case_id}{comma_num * ','}" + LN) + if not csv_path_dict: return @@ -331,8 +360,15 @@ def pytest_collection_modifyitems(session): try: csv_name, _id = findall(r"test_(.*?)_(\d+)", item.name)[0] + _case_id = findall(r"test_.*?_(\d+)", item.fspath.purebasename)[0] + if _id != _case_id: + raise ValueError except IndexError: - skip_text = f"{item.nodeid} 用例名称缺少用例id, 跳过执行" + skip_text = f"\n用例名称缺少用例id,跳过处理:[{item.nodeid}]" + logger.error(skip_text) + add_mark(item, ConfStr.SKIP.value, (skip_text,), {}) + except ValueError: + skip_text = f"\n用例py文件的id和用例函数的id不一致,跳过处理:[{item.nodeid}]" logger.error(skip_text) add_mark(item, ConfStr.SKIP.value, (skip_text,), {}) else: @@ -377,10 +413,15 @@ def pytest_collection_modifyitems(session): tags = containers.get(csv_path).get(_id) if tags: try: - if containers[csv_path][ConfStr.REMOVED_INDEX.value] is not None \ - and tags[containers[csv_path][ConfStr.REMOVED_INDEX.value]] \ - .strip('"').startswith( - f"{ConfStr.REMOVED.value}-"): + if containers[csv_path][ + ConfStr.REMOVED_INDEX.value + ] is not None and tags[ + containers[csv_path][ConfStr.REMOVED_INDEX.value] + ].strip( + '"' + ).startswith( + f"{ConfStr.REMOVED.value}-" + ): session.items.remove(item) continue except IndexError as exc: @@ -395,17 +436,26 @@ def pytest_collection_modifyitems(session): # 先处理“跳过原因”列 if index == containers[csv_path][ConfStr.SKIP_INDEX.value]: # 标签是以 “skip-” 开头, noskip 用于解除所有的skip - if not session.config.option.noskip \ - and tag.startswith(f"{ConfStr.SKIP.value}-"): + if not session.config.option.noskip and tag.startswith( + f"{ConfStr.SKIP.value}-" + ): # 标签以 “fixed-” 开头, ifixed表示ignore fixed, 用于忽略所有的fixed # 1. 不给ifixed参数时,只要标记了fixed的用例,即使标记了skip-,也会执行; # 2. 给ifixed 参数时(--ifixed yes),fixed不生效,仅通过skip跳过用例; try: if ( - not session.config.option.ifixed - and containers[csv_path][ConfStr.FIXED_INDEX.value] is not None - and tags[containers[csv_path][ConfStr.FIXED_INDEX.value]].strip( - '"').startswith(f"{ConfStr.FIXED.value}-") + not session.config.option.ifixed + and containers[csv_path][ + ConfStr.FIXED_INDEX.value + ] + is not None + and tags[ + containers[csv_path][ + ConfStr.FIXED_INDEX.value + ] + ] + .strip('"') + .startswith(f"{ConfStr.FIXED.value}-") ): continue except IndexError: @@ -432,7 +482,10 @@ def pytest_collection_modifyitems(session): ) else: # 非跳过列 # 处理 pms id - if containers[csv_path][ConfStr.PMS_ID_INDEX.value] == index: + if ( + containers[csv_path][ConfStr.PMS_ID_INDEX.value] + == index + ): if suite_runs_ids: if tag not in suit_id_deque: session.items.remove(item) @@ -453,7 +506,9 @@ def pytest_collection_modifyitems(session): add_mark(item, tag, (mark_title,), {}) else: # tag为空 # 处理 pmd id 为空的情况 - if (task_id or suite_id) and containers[csv_path][ConfStr.PMS_ID_INDEX.value] == index: + if (task_id or suite_id) and containers[csv_path][ + ConfStr.PMS_ID_INDEX.value + ] == index: session.items.remove(item) continue else: @@ -480,6 +535,19 @@ def pytest_collection_modifyitems(session): print() # 处理日志换行 +def walk_apps(walk_dir): + no_youqu_mark = {} + csv_path_dict = {} + for root, _, files in walk(walk_dir): + if "NOYOUQUMARK" in files and not no_youqu_mark.get(root): + no_youqu_mark[root] = True + continue + for file in files: + if file.endswith(".csv") and file != "case_list.csv": + csv_path_dict[splitext(file)[0]] = f"{root}/{file}" + return csv_path_dict, no_youqu_mark + + def pytest_collection_finish(session): """pytest_collection_finish""" session.item_count = len(session.items) @@ -487,14 +555,17 @@ def pytest_collection_finish(session): if session.config.option.reruns and not session.config.option.collectonly: print(f"失败重跑次数:\t{session.config.option.reruns}") if session.config.option.max_fail and not session.config.option.collectonly: - session.config.option.maxfail = int(float(session.config.option.max_fail) * session.item_count) + session.config.option.maxfail = int( + float(session.config.option.max_fail) * session.item_count + ) print(f"最大失败次数:\t{session.config.option.maxfail}") session.sessiontimeout = 0 if session.config.option.timeout and not session.config.option.collectonly: _min, sec = divmod(int(session.config.option.timeout), 60) hour, _min = divmod(_min, 60) print( - f"用例超时时间:\t{session.config.option.timeout}s ({hour}{'小时' if hour else ''}{_min}{'分' if _min else ''}{sec}秒)") + f"用例超时时间:\t{session.config.option.timeout}s ({hour}{'小时' if hour else ''}{_min}{'分' if _min else ''}{sec}秒)" + ) # sessiontimeout _n = 0 items_timeout = 0 @@ -508,7 +579,9 @@ def pytest_collection_finish(session): item_timeout = 0 items_timeout += item_timeout break - session.sessiontimeout = ((session.item_count - _n) * session.config.option.timeout) + items_timeout + session.sessiontimeout = ( + (session.item_count - _n) * session.config.option.timeout + ) + items_timeout _min, sec = divmod(int(session.sessiontimeout), 60) hour, _min = divmod(_min, 60) print( @@ -516,12 +589,12 @@ def pytest_collection_finish(session): ) # 生成 case_list.csv - if session.config.option.collectonly: + if session.config.option.collectonly and session.config.option.export_csv_file: execute = [] - execute.append("用例名称," + GlobalConfig.CSV_HEARD + LN) + execute.append("用例名称," + GlobalConfig.EXPORT_CSV_HEARD + LN) for item in session.items: node_id = item.nodeid.split("[")[0] - header = GlobalConfig.CSV_HEARD.split(",") + header = GlobalConfig.EXPORT_CSV_HEARD.split(",") case_info = ["" for _ in header] case_info.insert(0, node_id) for mark in item.own_markers: @@ -537,7 +610,11 @@ def pytest_collection_finish(session): execute2.sort(key=execute.index) if not exists(GlobalConfig.REPORT_PATH): makedirs(GlobalConfig.REPORT_PATH) - with open(f"{GlobalConfig.REPORT_PATH}/{GlobalConfig.CSV_FILE}", "w", encoding="utf-8") as _f: + with open( + f"{GlobalConfig.REPORT_PATH}/{session.config.option.export_csv_file}", + "w+", + encoding="utf-8", + ) as _f: _f.writelines(execute2) @@ -574,9 +651,11 @@ def pytest_runtest_setup(item): pass if item.config.option.pms_user and item.config.option.pms_password: + def send2pms(case_res_path, data_send_result_csv): Send2Pms( - user=item.config.option.pms_user, password=item.config.option.pms_password + user=item.config.option.pms_user, + password=item.config.option.pms_password, ).send2pms(case_res_path, data_send_result_csv) if async_send(item.session): @@ -620,7 +699,9 @@ def pytest_runtest_makereport(item, call): allure.dynamic.severity(LabelType.L3.value) elif mark.args[0] == FixedCsvTitle.pms_case_id.value: # if mark.name: - testcase_url = f"https://pms.uniontech.com/testcase-view-{mark.name}.html" + testcase_url = ( + f"https://pms.uniontech.com/testcase-view-{mark.name}.html" + ) allure.dynamic.testcase(testcase_url) logger.info(testcase_url) else: @@ -683,7 +764,9 @@ def pytest_runtest_makereport(item, call): # 非图像识别错误 pass try: - template = f"{splitext(item.record['image_path'])[0]}_ocr_.png" + template = ( + f"{splitext(item.record['image_path'])[0]}_ocr_.png" + ) CmdCtl.run_cmd(f"cp {item.record['ocr']} {template}") allure.attach.file( template, @@ -712,14 +795,12 @@ def pytest_report_teststatus(report, config): if report.when in ("setup", "teardown"): if report.failed: short, verbose = config.hook.pytest_emoji_error( - config=config, - head_line=report.head_line + config=config, head_line=report.head_line ) return "error", short, verbose if report.skipped: short, verbose = config.hook.pytest_emoji_skipped( - config=config, - head_line=report.head_line + config=config, head_line=report.head_line ) return "skipped", short, verbose # 在用例执行阶段处理 passed skipped failed @@ -727,18 +808,15 @@ def pytest_report_teststatus(report, config): short = verbose = "" if report.passed: short, verbose = config.hook.pytest_emoji_passed( - config=config, - head_line=report.head_line + config=config, head_line=report.head_line ) elif report.skipped: short, verbose = config.hook.pytest_emoji_skipped( - config=config, - head_line=report.head_line + config=config, head_line=report.head_line ) elif report.failed: short, verbose = config.hook.pytest_emoji_failed( - config=config, - head_line=report.head_line + config=config, head_line=report.head_line ) return report.outcome, short, verbose return None @@ -764,19 +842,20 @@ def pytest_sessionfinish(session): default_result["result"] = "fail" item_name = item.nodeid.split("[")[0] if not execute.get(item_name) or ( - item.outcome != ConfStr.PASSED.value - and execute.get(item_name).get("result") == "pass" + item.outcome != ConfStr.PASSED.value + and execute.get(item_name).get("result") == "pass" ): execute[item_name] = default_result except AttributeError: pass if execute: with open( - f"{GlobalConfig.ROOT_DIR}/ci_result.json", "w", encoding="utf-8" + f"{GlobalConfig.ROOT_DIR}/ci_result.json", "w", encoding="utf-8" ) as _f: _f.write(dumps(execute, indent=2, ensure_ascii=False)) if session.config.option.pms_user and session.config.option.pms_password: + def send2pms(case_res_path, data_send_result_csv): Send2Pms( user=session.config.option.pms_user, @@ -805,7 +884,7 @@ def pytest_sessionfinish(session): f"echo '{GlobalConfig.PASSWORD}' | sudo -S rm -rf {GlobalConfig.TMPDIR}", interrupt=False, out_debug_flag=False, - command_log=False + command_log=False, ) @@ -815,7 +894,7 @@ def pytest_emoji_passed(config, head_line): # 笑脸 return ( f"【 {datetime.now()} {head_line} || 😃 】\n", - f"【 {datetime.now()} {head_line} || PASSED 😃 】\n" + f"【 {datetime.now()} {head_line} || PASSED 😃 】\n", ) @@ -825,7 +904,7 @@ def pytest_emoji_failed(config, head_line): # 哭笑不得 return ( f"【 {datetime.now()} {head_line} || 😰 】\n", - f"【 {datetime.now()} {head_line} || FAILED 😰 】\n" + f"【 {datetime.now()} {head_line} || FAILED 😰 】\n", ) @@ -835,7 +914,7 @@ def pytest_emoji_skipped(config, head_line): # 翻白眼儿 return ( f"【 {datetime.now()} {head_line} || 🙄 】\n", - f"【 {datetime.now()} {head_line} || SKIPPED 🙄 】\n" + f"【 {datetime.now()} {head_line} || SKIPPED 🙄 】\n", ) @@ -845,7 +924,7 @@ def pytest_emoji_error(config, head_line): # 哭哭 return ( f"【 {datetime.now()} {head_line} || 😡 】\n", - f"【 {datetime.now()} {head_line} || ERROR 😡 】\n" + f"【 {datetime.now()} {head_line} || ERROR 😡 】\n", ) diff --git a/docs/AT开发规范.md b/docs/AT开发规范.md index 781a88d..32658b0 100644 --- a/docs/AT开发规范.md +++ b/docs/AT开发规范.md @@ -29,9 +29,9 @@ AT 开发规范是根据自动化测试运行两年多来,遇到问题解决 举例: ```python - # test_music_001.py + # test_music_679537.py - def test_music_001(): + def test_music_679537(): """用例标题""" pass ``` @@ -45,9 +45,9 @@ AT 开发规范是根据自动化测试运行两年多来,遇到问题解决 如果你将上例写成了这样: ```python - # test_music_001.py + # test_music_679537.py - def test_movie_001(): + def test_movie_679537(): """用例标题""" pass ``` @@ -191,7 +191,7 @@ class TitleWidget(BaseWidget): class TestMusic(BaseCase): """音乐用例""" - def test_music_001(self): + def test_music_679537(self): """音乐启动""" ``` @@ -209,13 +209,13 @@ class TestMusic(BaseCase): class TestMusic(BaseCase): """音乐用例""" - def test_music_001_1(self): + def test_music_679537_1(self): """任务栏启动音乐""" - def test_music_001_2(self): + def test_music_679537_2(self): """启动器启动音乐""" - def test_music_001_3(self): + def test_music_679537_3(self): """桌面启动音乐""" ``` @@ -223,7 +223,7 @@ class TestMusic(BaseCase): - 用例函数以 test 开头,遵循蛇形命名规范,中间为用例的模块名称,后面加用例 ID,最后加测试点序号,即 `test_${module}_${case_id}[_${index}]` ; - 比如:`test_music_001_1`,index 从 1 开始。 + 比如:`test_music_679537_1`,index 从 1 开始。 - 函数功能说明里面写用例标题,直接复制 PMS 上用例标题即可,注意用三对双引号,不要用其他注释,更不要用井号注释写用例标题; @@ -241,7 +241,7 @@ class TestMusic(BaseCase): class TestMusic(BaseCase): """音乐用例""" - def test_music_182(self): + def test_music_679537(self): """演唱者-平铺视图下进入演唱者详情页""" # 1 @@ -258,7 +258,7 @@ class TestMusic(BaseCase): class TestMusic(BaseCase): """音乐用例""" - def test_music_182(self): + def test_music_679537(self): """演唱者-平铺视图下进入演唱者详情页""" music = DeepinMusicWidget() music.click_singer_btn_in_music_by_ui() diff --git a/docs/RELEASE.md b/docs/RELEASE.md index c387225..1528b15 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -9,6 +9,7 @@ new fix - 对 docs 里面细化了远程执行章节的描述; +- 多 docs 里面优化了标签化管理章节的描述; ## 2.1.5(2023/8/31) @@ -369,7 +370,7 @@ new ```python @pytest.mark.count(2) - def test_music_001(): + def test_music_679537(): pass ``` @@ -382,7 +383,7 @@ new - ​ image_utils 增加函数 save_temporary_picture,支持指定屏幕区域截图并返回图片存放的本地路径,后续使用 assert_image_exist 进行断言 - ```Python - def test_music_001(self): + def test_music_679537(self): pic_path = DeepinMusicWidget.save_temporary_picture(x, y, width, height) ...... # 中间操作 self.assert_image_exit(pic_path) @@ -391,7 +392,7 @@ new - button_center 新增 btn_size 获取控件左上角坐标及长宽,用于动态的截取元素的图片,可用于定位断言 - ```python - def test_music_001(self): + def test_music_679537(self): pic_path = DeepinMusicWidget.save_temporary_picture(*DeepinMusicWidget().ui.btn_size("所有音乐按钮")) ...... # 中间操作 self.assert_image_exit(pic_path) diff --git a/docs/index.md b/docs/index.md index 51130ca..2af403f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -109,9 +109,7 @@ youqu 基础框架工程目录建议放在 `~` 目录下,放在其他目录也可以运行,但是在自动化用例执行过程中可能需要做环境清理,放在其他目录存在代码被删除的风险。 -如果你的机器上不同目录下存在多个 YouQu 工程,那么在运行之前,请先执行 env.sh 校正相关环境。 - -### 2. 执行器 +如果你的机器上不同目录下存在多个 `YouQu` 工程,那么在运行之前,请先执行 `env.sh` 校正相关环境。 在项目根目录下有一个 `manage.py` ,它是一个执行器入口,提供了本地执行、远程执行等的功能。 @@ -121,252 +119,17 @@ youqu youqu manage.py run -a deepin-music ``` -#### 2.1. 本地执行 +### 2. 本地执行 ```shell youqu manage.py run ``` -##### 2.1.1. 配置文件 - -通过配置文件配置参数 - -在配置文件 `setting/globalconfig.ini` 里面支持配置对执行的一些参数进行配置,常用的如: - -```ini -;=============================== CASE CONFIG =================================== -[case] -;执行的应用名称 -;为空表示执行 apps/ 目录下所有应用的用例 -APP_NAME = - -;执行包含关键词的用例 -KEYWORDS = - -;执行包含用例标签的用例 -TAGS = -;----------------------------------------------- -;1.KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例 -;2.KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式 -;e.g. TAGS = L1 or smoke -;----------------------------------------------- - -;本地文件测试套,将要执行的用例写入指定的 csv 文件 -;默认为空,从基础框架根目录开始:e.g. CASE_FILE = case_list.txt -;如果这里有值,APP_NAME KEYWORDS TAGS 的配置均不生效 -CASE_FILE = - -;=============================== RUNNER CONFIG =================================== -[runner] -;最大失败用例数量的占比 -;比如:总执行用例数为 100, 若 MAX_FAIL = 0.5,则失败用例数达到 50 就会终止测试。 -MAX_FAIL = 1 - -;单条用例的超时时间,如果一条用例的执行时间超时,这条用例会被停止,后续用例继续执行。 -;单位为秒 -;这是一个全局统一配置,如果某条用例需要单独配置超时时间,可以在用例中这样写: -;@pytest.mark.timeout(500) -;def test_xxx_001(): -; ... -;会话超时(所有用例执行的超时时间)是根据全局超时配置和用例单独超时配置自动计算的。 -CASE_TIME_OUT = 200 - -;失败用例重跑次数 -;注意,RERUN = 1 表示重跑 1 次,即第一次用例执行失败会自动重跑 1 次,总共执行 2 次; -;如果第 2 次执行成功,结果成功,失败亦为失败。 -RERUN = 1 - -;失败录屏从第几次失败开始录制视频。 -;比如 RECORD_FAILED_CASE = 1 ,表示用例第 1 次执行失败之后开始录屏,RERUN >= RECORD_FAILED_CASE。 -;1.关闭录屏:RECORD_FAILED_CASE > RERUN -;2.每条用例都录屏:RECORD_FAILED_CASE = 0 -RECORD_FAILED_CASE = 1 - -;yes 每条用例执行之后进行环境清理 -CLEAN_ALL = yes - -;检查测试机分辨率, 比如:1920x1080 -;no: 表示不做分辨率校验 -RESOLUTION = 1920x1080 - -;不跳过用例,csv文件里面标记了 skip-xxx的用例不跳过 -NOSKIP = no - -;ignore fixed -;no,只要标记了fixed的用例,即使标记了skip-,也会执行; -;yes,fixed不生效,仅通过skip跳过用例; -IFIXED = no - -;要安装deb包的路径 -;e.g : ~/Downloads/ 安装下载目录下的deb包,如果是远程执行,会自动拷贝到远程并安装。 -DEB_PATH = - -;DEBUG 模式执行用例,只收集不执行用例,也不做设备分辨率的检查。 -DEBUG = no - -;记录top命令查询的系统资源占用情况,TOP = 3 表示记录前3个进程。 -TOP = - -;指定用例执行次数 -REPEAT = - -;默认在所有测试完成之后输出报错信息. -;yes, 测试过程中立即显示报错 -DURING_FAIL = no - -;注册自启服务 -AUTOSTART = no - -;=============================== REPORT CONFIG =================================== -[report] -;测试报告的title -REPORT_TITLE = YouQu Report - -;测试报告的name -REPORT_NAME = YouQu Report - -;测试报告的默认语言 -;en:English -;ru:Русский -;zh:中文 -;de:Deutsch -;nl:Nederlands -;he:Hebrew -;br:Brazil -;pl:Polski -;ja:日本語 -;es:Español -;kr:한국어 -;fr:Français -;az:Azərbaycanca -REPORT_LANGUAGE = zh - -;用例执行完后生成的测试报告格式 -;目前支持 allure, xml, json (支持同时生成) -REPORT_FORMAT = allure, xml, json - -;指定报告生成的路径(相对项目根目录下) -ALLURE_REPORT_PATH = report/ -XML_REPORT_PATH = report/ -JSON_REPORT_PATH = report/ - -;=============================== GLOBAL CONFIG =================================== -[globalconfig] -;测试机的密码 -PASSWORD = 1 - -;图像识别重试次数 -IMAGE_MATCH_NUMBER = 1 - -;图像识别重试每次间隔等待时间 -IMAGE_MATCH_WAIT_TIME = 1 - -;图像识别匹配度 -IMAGE_RATE = 0.9 - -;截取当前屏幕实时图像保存路径,用于图像识别坐标 -SCREEN_CACHE = /tmp/screen.png - -;截取屏幕上指定区域图片,保存临时图片的路径 -TMPDIR = /tmp/tmpdir - -;系统主题 -SYS_THEME = deepin - -;OCR服务端地址(不可随意修改) -OCR_SERVER_HOST = youqu-dev.uniontech.com - -;OpenCV服务端地址 -OPENCV_SERVER_HOST = youqu-dev.uniontech.com - -;=============================== PMS CONFIG =================================== -;PMS相关配置,包含以下几个方面: -;1.PMS测试套执行 -;2.自动从PMS爬取数据并同步本地CSV文件 -;3.PMS数据回填 -[pms] -;PMS的用户名,如: ut001234 -PMS_USER = - -;PMS的密码 -PMS_PASSWORD = - -;PMS测试套的ID -;在PMS上查看用例“套件”链接: https://pms.uniontech.com/testsuite-view-495.html -;测试套ID为: 495 -SUITE_ID = - -;数据回填必须关联PMS测试单 -;在PMS上查看测试单链接: https://pms.uniontech.com/testtask-cases-20747.html -;测试单ID为: 20747 -TASK_ID = - -;将测试结果数据回填到PMS -;为空: 表示不回填,不会在每条用例执行完之后生成json结果文件; -;async: 表示逐条异步回填,后面一条执行开始时通过子线程对前一条用例的执行结果进行回填,如此实现时间效率最大化; -;finish: 表示所有用例执行完成之后逐个回填(PMS不支持并发); -SEND_PMS = - -;数据回填的触发者 -;auto: 框架自动回填,配合SEND_PMS配置使用,你可以选择在不同的阶段进行数据回填; -;hand: 手动回填,每条用例仍然会生成json文件,但框架不会进行数据回填,需要你可以在你想要发送的时间点手动触发回填; -TRIGGER = auto - -;PMS回填的重试次数 -;如果接口请求失败,会进行重试 -SEND_PMS_RETRY_NUMBER = 2 - -[csv_link_pms_lib] -;caselib: 用例库 -;testcase: 产品库用例 -CASE_FROM = caselib - -[csv_link_pms_id] -;同步PMS数据到本地CSV文件,必须要配置的配置项 -;key是本地CSV文件的文件名称; -;value是对应PMS上的模块ID; -;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, -;CSV文件名称为music.csv,其在PMS上的用例为: https://pms.uniontech.com/caselib-browse-81.html -;因此应该配置为: music = 81 -;这样才能将PMS与本地CSV文件建立联系。 -;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 -music = - -[export_csv] -;导出的csv文件名称,默认 case_list.csv -CSV_FILE = case_list.csv - -;exportcsv 命令导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加 -CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过 - -[log_cli] -;日志相关配置(不打印构造函数和魔法函数的功能说明) -;批量执行时,终端输出的日志级别 DEBUG/INFO/ERROR -LOG_LEVEL = DEBUG - -# ============= 自动输出日志的配置 ================ -;支持类名以 xxx 开头的,自动将函数说明打印为日志, 多个参数以逗号隔开 -CLASS_NAME_STARTSWITH = Assert - -;支持类名以 xxx 结尾的,自动将函数说明打印为日志,多个参数以逗号隔开 -CLASS_NAME_ENDSWITH = Widget - -;支持类名包含 xxx 的,自动将函数说明打印为日志,多个参数以逗号隔开 -CLASS_NAME_CONTAIN = ShortCut -# ============================================== -``` - -配置完成之后,直接在命令行执行 `manage.py` 就好了。 - -```shell -youqu manage.py run -``` - -##### 2.1.2. 命令行参数 +#### 2.1. 命令行参数 通过命令行参数配置参数 -以下为 `youqu manage.py run` 提供的一些参数选项: +以下为 `youqu manage.py run` 提供的一些常用的参数选项: ```coffeescript -h, --help show this help message and exit @@ -426,7 +189,236 @@ youqu manage.py run --app deepin-music --keywords "xxx" --tags "xxx" --app 入参还支持 `autotest_xxx` 和 `apps/autotest_xxx` 两种写法,方便在输入命令的过程中使用补全,下面的远程执行功能同样支持。 -#### 2.2. 远程执行 +#### 2.2. 配置文件 + +通过配置文件配置参数 + +在配置文件 `setting/globalconfig.ini` 里面支持配置对执行的一些参数进行配置,常用的如: + +```ini +;=============================== RUN CONFIG =================================== +[run] +;执行的应用名称 +;为空表示执行 apps/ 目录下所有应用的用例 +APP_NAME = + +;执行包含关键词的用例 +KEYWORDS = + +;执行包含用例标签的用例 +;----------------------------------------------------------- +;1.KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例 +;2.KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式 +;比如:TAGS = L1 or smoke ,表示执行标签带有 L1 或 somke 标签的用例; +;这两个参数也可以同时使用,可以组合出任意的用例集合,只有想不到没有办不到。 +;----------------------------------------------------------- +TAGS = + +;本地文件测试套,将要执行的用例写入指定的 csv 文件 +;默认为空,从基础框架根目录开始:e.g. CASE_FILE = case_list.txt +;如果这里有值,APP_NAME KEYWORDS TAGS 的配置均不生效 +CASE_FILE = + +;最大失败用例数量的占比 +;比如:总执行用例数为 100, 若 MAX_FAIL = 0.5,则失败用例数达到 50 就会终止测试。 +MAX_FAIL = 1 + +;单条用例的超时时间,如果一条用例的执行时间超时,这条用例会被停止,后续用例继续执行。 +;单位为秒 +;这是一个全局统一配置,如果某条用例需要单独配置超时时间,可以在用例中这样写: +;@pytest.mark.timeout(500) +;def test_xxx_001(): +; ... +;会话超时(所有用例执行的超时时间)是根据全局超时配置和用例单独超时配置自动计算的。 +CASE_TIME_OUT = 200 + +;失败用例重跑次数 +;注意,RERUN = 1 表示重跑 1 次,即第一次用例执行失败会自动重跑 1 次,总共执行 2 次; +;如果第 2 次执行成功,结果成功,失败亦为失败。 +RERUN = 1 + +;失败录屏从第几次失败开始录制视频。 +;比如 RECORD_FAILED_CASE = 1 ,表示用例第 1 次执行失败之后开始录屏,RERUN >= RECORD_FAILED_CASE。 +;1.关闭录屏:RECORD_FAILED_CASE > RERUN +;2.每条用例都录屏:RECORD_FAILED_CASE = 0 +RECORD_FAILED_CASE = 1 + +;yes 每条用例执行之后进行环境清理 +CLEAN_ALL = yes + +;检查测试机分辨率, 比如:1920x1080 +;no: 表示不做分辨率校验 +RESOLUTION = 1920x1080 + +;不跳过用例,csv文件里面标记了 skip-xxx的用例不跳过 +NOSKIP = no + +;ignore fixed +;no,只要标记了fixed的用例,即使标记了skip-,也会执行; +;yes,fixed不生效,仅通过skip跳过用例; +IFIXED = no + +;要安装deb包的路径 +;e.g : ~/Downloads/ 安装下载目录下的deb包,如果是远程执行,会自动拷贝到远程并安装。 +DEB_PATH = + +;DEBUG 模式执行用例,只收集不执行用例,也不做设备分辨率的检查。 +DEBUG = no + +;记录top命令查询的系统资源占用情况,TOP = 3 表示记录前3个进程。 +TOP = + +;指定用例执行次数 +REPEAT = + +;默认在所有测试完成之后输出报错信息. +;yes, 测试过程中立即显示报错 +DURING_FAIL = no + +;注册自启服务 +AUTOSTART = no + +;测试机的密码 +PASSWORD = 1 + +;图像识别重试次数 +IMAGE_MATCH_NUMBER = 1 + +;图像识别重试每次间隔等待时间 +IMAGE_MATCH_WAIT_TIME = 1 + +;图像识别匹配度 +IMAGE_RATE = 0.9 + +;截取当前屏幕实时图像保存路径,用于图像识别坐标 +SCREEN_CACHE = /tmp/screen.png + +;截取屏幕上指定区域图片,保存临时图片的路径 +TMPDIR = /tmp/tmpdir + +;系统主题 +SYS_THEME = deepin + +;OCR服务端地址(不可随意修改) +OCR_SERVER_HOST = youqu-dev.uniontech.com + +;OpenCV服务端地址 +OPENCV_SERVER_HOST = youqu-dev.uniontech.com + +;=============================== REPORT CONFIG =================================== +[report] +;测试报告的title +REPORT_TITLE = YouQu Report + +;测试报告的name +REPORT_NAME = YouQu Report + +;测试报告的默认语言 +;en:English +;ru:Русский +;zh:中文 +;de:Deutsch +;nl:Nederlands +;he:Hebrew +;br:Brazil +;pl:Polski +;ja:日本語 +;es:Español +;kr:한국어 +;fr:Français +;az:Azərbaycanca +REPORT_LANGUAGE = zh + +;用例执行完后生成的测试报告格式 +;目前支持 allure, xml, json (支持同时生成) +REPORT_FORMAT = allure, xml, json + +;指定报告生成的路径(相对项目根目录下) +ALLURE_REPORT_PATH = report/ +XML_REPORT_PATH = report/ +JSON_REPORT_PATH = report/ + +;=============================== PMS CONFIG =================================== +;PMS相关配置,包含以下几个方面: +;1.PMS测试套执行 +;2.自动从PMS爬取数据并同步本地CSV文件 +;3.PMS数据回填 +[pmsctl] +;PMS的用户名,如: ut001234 +PMS_USER = + +;PMS的密码 +PMS_PASSWORD = + +;PMS测试套的ID +;在PMS上查看用例“套件”链接: https://pms.uniontech.com/testsuite-view-495.html +;测试套ID为: 495 +SUITE_ID = + +;数据回填必须关联PMS测试单 +;在PMS上查看测试单链接: https://pms.uniontech.com/testtask-cases-20747.html +;测试单ID为: 20747 +TASK_ID = + +;将测试结果数据回填到PMS +;为空: 表示不回填,不会在每条用例执行完之后生成json结果文件; +;async: 表示逐条异步回填,后面一条执行开始时通过子线程对前一条用例的执行结果进行回填,如此实现时间效率最大化; +;finish: 表示所有用例执行完成之后逐个回填(PMS不支持并发); +SEND_PMS = + +;数据回填的触发者 +;auto: 框架自动回填,配合SEND_PMS配置使用,你可以选择在不同的阶段进行数据回填; +;hand: 手动回填,每条用例仍然会生成json文件,但框架不会进行数据回填,需要你可以在你想要发送的时间点手动触发回填; +TRIGGER = auto + +;PMS回填的重试次数 +;如果接口请求失败,会进行重试 +SEND_PMS_RETRY_NUMBER = 2 + +;caselib: 用例库 +;testcase: 产品库用例 +CASE_FROM = caselib + +[pmsctl-pms_link_csv] +;同步PMS数据到本地CSV文件,必须要配置的配置项 +;key是本地CSV文件的文件名称; +;value是对应PMS上的模块ID; +;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, +;CSV文件名称为music.csv,其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html +;因此应该配置为: music = 81 +;这样才能将PMS与本地CSV文件建立联系。 +;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 +music = + +[csvctl] +;将py文件的case id同步到csv文件 +;yes, 开启同步 +PY_ID_TO_CSV = no + +;导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加 +EXPORT_CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过 + + +[log_cli] +;日志相关配置(不打印构造函数和魔法函数的功能说明) +;批量执行时,终端输出的日志级别 DEBUG/INFO/ERROR +LOG_LEVEL = DEBUG + +# ============= 自动输出日志的配置 ================ +;支持类名以 xxx 开头的,自动将函数说明打印为日志, 多个参数以逗号隔开 +CLASS_NAME_STARTSWITH = Assert + +;支持类名以 xxx 结尾的,自动将函数说明打印为日志,多个参数以逗号隔开 +CLASS_NAME_ENDSWITH = Widget + +;支持类名包含 xxx 的,自动将函数说明打印为日志,多个参数以逗号隔开 +CLASS_NAME_CONTAIN = ShortCut +# ============================================== +``` + +配置完成之后,直接在命令行执行 `manage.py` 就好了。 + +### 3. 远程执行 远程执行就是用本地作为服务端控制远程机器执行,远程机器执行的用例相同; @@ -436,7 +428,7 @@ youqu manage.py run --app deepin-music --keywords "xxx" --tags "xxx" youqu manage.py remote ``` -##### 2.2.1. 远程多机器分布式异步执行 +#### 3.1. 远程多机器分布式异步执行 ![](https://pic.imgdb.cn/item/64f6d3c0661c6c8e549f8ca5.png) @@ -523,7 +515,7 @@ sudo systemctl enable ssh 配置文件其他相关配置项详细说明,请查看配置文件中的注释内容。 -##### 2.2.2. 远程多机器分布式异步负载均衡执行 +#### 3.2. 远程多机器分布式异步负载均衡执行 多机器分布式异步负载均衡执行也是用本地作为服务端控制远程机器执行,但远程机器执行的用例不同,而是所有远程机器执行的用例之和,为你想要执行的用例集; @@ -533,92 +525,15 @@ sudo systemctl enable ssh ![](https://pic.imgdb.cn/item/64f6d694661c6c8e54a1025b.png) -使用方法和前面一样,只是需要增加一个参数: +使用方法和前面一样,只是需要增加一个参数 `--parallel`: ```shell youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" --parallel no ``` -#### 2.3. PMS 数据回填 +## 四、脚手架创建工程 -测试单关联的用例,自动化测试对应的去跑这些关联的用例,并且将执行的结果回填的测试用例的状态里面。 - -PMS 数据回填主要有三种方式: - -(1)异步回填 - -在用例执行的过程中,采用异步的方式去进行数据回填,直白的讲就是,第二条用例开始跑的时候,通过子线程去做第一条用例的数据回填,如此循环,直到所有用例执行结束; - -这种方案的时间效率最高的,因为理论上用例的执行时间是大于数据回填的接口请求时间的,也就是说,当用例执行完之后,数据回填也完成了。 - -使用方法,在 `globalconfig.ini` 里面配置以下参数:(以下涉及到的参数配置都是在配置文件里面进行配置) - -```ini -PMS_USER = PMS账号 -PMS_PASSWORD = PMS密码 -SEND_PMS = async -TASK_ID = 测试单ID -TRIGGER = auto -APP_NAME = 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同) -``` - -(2)用例执行完之后回填 - -等所有用例执行完之后,再逐个进行回填的接口请求,此方案时间效率比较低。 - -使用方法: - -```ini -PMS_USER = PMS账号 -PMS_PASSWORD = PMS密码 -SEND_PMS = finish -TASK_ID = 测试单ID -TRIGGER = auto -APP_NAME = -``` - -(3)手动回填 - -所有用例执行完之后不做回填的接口请求,后续手动将结果进行回填请求。 - -用例执行时配置: - -```ini -PMS_USER = PMS账号 -PMS_PASSWORD = PMS密码 -SEND_PMS = finish -TASK_ID = 测试单ID -TRIGGER = hand -APP_NAME = -``` - -后续手动回填方法: - -```shell -youqu manage.py pms --send2task yes -``` - -##### 可能遇到的问题 - -由同学可能会发现,怎么回填一次之后,后面想再次回填就不生效了; - -这是因为为了应对前面提到的多种数据回填的方式,在 `report` 目录下会由 `pms_xxx` 开头的目录,记录了用例的执行结果和回填情况,如果这条用例之前已经回填过了,后续就不会再此触发回填了; - -如果你想重新做回填,你可以把 `report/pms_xxx` 目录删掉,这样就可以重新做数据回填了; - -#### 2.4. 导出 CSV 文件 - -框架提供导出指定标签用例的功能: - -```shell -youqu manage.py exportcsv -a deepin-album -t CICD -``` - -表示导出 `deepin-album` 的用例中标记了 `CICD` 标签的用例,导出 `CSV` 文件的字段格式已经适配了 `CICD` 的要求。 - -#### 2.5. 脚手架新建 APP 工程 - -新建一个 APP 工程: +创建一个 APP 工程: ```shell youqu manage.py startapp autotest_deepin_some @@ -656,37 +571,8 @@ apps `autotest_deepin_some` 是你的工程名称,比如:`autotest_deepin_music` ; -在此基础上,你可以快速的开始你的 AT 项目。 +在此基础上,你可以快速的开始你的 AT 项目,更重要的是确保创建工程的规范性。 -#### 2.6. 用例标签自动同步 +--------------------------- -用于自动同步 `PMS` 用例标签数据至本地 `CSV` 文件,主要通过以下几个配置来控制: - -```ini -APP_NAME = # 指定要同步的应用名称 -PMS_USER = # PMS的用户名 -PMS_PASSWORD = # PMS的密码 -``` - -在 `[csv_link_pms_id]` 节点下指定 `csv` 文件名与 `PMS` 用例模块的对应关系,比如: - -```ini -[csv_link_pms_id] -;同步PMS数据到本地CSV文件,必须要配置的配置项 -;key是本地CSV文件的文件名称; -;value是对应PMS上的模块ID; -;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, -;CSV文件名称为music.csv,其在PMS上的用例为: https://pms.uniontech.com/testcase-browse-53.html -;因此应该配置为: music = 53 -;这样才能将PMS与本地CSV文件建立联系。 -;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 -music = 53 -``` - -将以上信息配置好之后,在命令行执行: - -```shell -youqu manage.py pms --pms2csv yes -``` - -每次执行时原 `csv` 文件会自动备份在 `report/csv_back` 目录下,因此你不用担心脚本执行导致你的数据丢失。 +更多内容请查看【框架功能介绍】 diff --git a/docs/框架功能介绍.md b/docs/框架功能介绍.md index 9b2e5e8..345e108 100644 --- a/docs/框架功能介绍.md +++ b/docs/框架功能介绍.md @@ -8,17 +8,473 @@ # ============================================= ``` -## 一、一句话简介 +## 一句话简介 此文档主要想对 `YouQu` 框架一些重点的功能做详细的介绍,包括对一些功能开发的背景、要解决的问题、实现逻辑、代码等方面的介绍,以此让大家能更加清晰的理解各项功能,从而在使用的时候能更加的得心应手。 -## 二、图像识别 +## 执行管理器 -图像识别在 UI 自动化中是不可缺少的,市面上甚至有完全基于图像识别的自动化测试框架,比如 `Airtest`、`Sikuli` 等,在游戏等特定领域也有不错的效果,这些工具实际上也是用的 `OpenCV` 进行了封装,`YouQu` 框架基于 `OpenCV` 开发了自己的图像识别功能,它可以方便的用于界面元素的定位和断言; +`YouQu` 的执行管理器 `manage.py` 提供了丰富的配置和命令行参数,可用于本地用例驱动执行、远程用例驱动执行、`CSV` 文件管理、`PMS` 与本地 `CSV` 文件标签关联管理、脚手架等功能; + +### 1. 如何使用 + +【命令行使用】 + +所有功能的驱动执行都是通过 `manage.py` 进行的,它是全局的入口文件,后面提到的一些命令行参数也都默认是在 `manage.py` 之后添加使用; + + 你可以使用 `-h` 或 `--help` 查看它的帮助: + +```shell +youqu manage -h +``` + +这样可以查看它支持的子命令; + +然后再通过子命令 `-h` 或 `--help` 查看子命令的帮助,以子命令 `run` 举例: + +```shell +youqu manage run -h +``` + +这样可以查看到子命令支持的各项参数及参数使用说明。 + +【配置文件】 + +配置文件在 `setting` 目录下,绝大部分的配置项均在 `globalconfig.ini` 文件中,为了方便描述后面经常提到的“配置文件”、“配置项”几乎都是指的 `setting/globalconfig.ini `。 + +你可以在配置文件中每一个配置项上面看到该配置项的使用说明。 + +几乎所有的命令行参数都对应提供了配置文件配置项; + +**命令行参数的优先级高于配置文件配置,也就是说通过命令行参数指定了对应的参数,配置文件中不管是否配置均不生效。** + +下面介绍两个常用的用例执行的功能: + +### 2. 本地执行 + +本地执行子命令为:`run` + +```shell +youqu manage.py run +``` + +#### 2.1. 命令行参数 + +通过命令行参数配置参数 + +以下为 `youqu manage.py run` 提供的一些常用的参数选项: + +```coffeescript + -h, --help show this help message and exit + -a APP, --app APP 应用名称:deepin-music 或 autotest_deepin_music 或 + apps/autotest_deepin_music + -k KEYWORDS, --keywords KEYWORDS + 用例的关键词,支持and/or/not逻辑组合 + -t TAGS, --tags TAGS 用例的标签,支持and/or/not逻辑组合 + --rerun RERUN 失败重跑次数 + --record_failed_case RECORD_FAILED_CASE + 失败录屏从第几次失败开始录制视频 + --clean {yes,} 清理环境 + --report_formats REPORT_FORMATS + 测试报告格式 + --max_fail MAX_FAIL 最大失败率 + --log_level LOG_LEVEL + 日志输出级别 + --timeout TIMEOUT 单条用例超时时间 + --resolution RESOLUTION + 检查分辨率 + --debug DEBUG 调试模式 + --noskip {yes,} csv文件里面标记了skip跳过的用例不生效 + --ifixed {yes,} fixed不生效,仅通过skip跳过用例 + --send_pms {,async,finish} + 数据回填 + --task_id TASK_ID 测试单ID + --trigger {,auto,hand} + 触发者 + -f CASE_FILE, --case_file CASE_FILE + 根据文件执行用例 + --deb_path DEB_PATH 需要安装deb包的本地路径 + --pms_user PMS_USER pms 用户名 + --pms_password PMS_PASSWORD + pms 密码 + --suite_id SUITE_ID pms 测试套ID + --pms_info_file PMS_INFO_FILE + pms 信息文件 + --top TOP 过程中记录top命令中的值 + --lastfailed 仅执行上次失败用例 + --duringfail 测试过程中立即显示报错 + --repeat REPEAT 指定用例执行次数 + --project_name PROJECT_NAME + 工程名称(写入json文件) + --build_location BUILD_LOCATION + 构建地区(写入json文件) + --line LINE 执行的业务线(写入json文件) + --autostart AUTOSTART 用例执行程序注册到开机自启服务 +``` + +在一些 `CI` 环境下使用命令行参数会更加方便: + +```shell +youqu manage.py run --app deepin-music --keywords "xxx" --tags "xxx" +``` + +--app 入参还支持 `autotest_xxx` 和 `apps/autotest_xxx` 两种写法,方便在输入命令的过程中使用补全,下面的远程执行功能同样支持。 + +#### 2.2. 配置文件 + +通过配置文件配置参数 + +在配置文件 `setting/globalconfig.ini` 里面支持配置对执行的一些参数进行配置,常用的如: + +```ini +;=============================== RUN CONFIG =================================== +[run] +;执行的应用名称 +;为空表示执行 apps/ 目录下所有应用的用例 +APP_NAME = + +;执行包含关键词的用例 +KEYWORDS = + +;执行包含用例标签的用例 +;----------------------------------------------------------- +;1.KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例 +;2.KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式 +;比如:TAGS = L1 or smoke ,表示执行标签带有 L1 或 somke 标签的用例; +;这两个参数也可以同时使用,可以组合出任意的用例集合,只有想不到没有办不到。 +;----------------------------------------------------------- +TAGS = + +;本地文件测试套,将要执行的用例写入指定的 csv 文件 +;默认为空,从基础框架根目录开始:e.g. CASE_FILE = case_list.txt +;如果这里有值,APP_NAME KEYWORDS TAGS 的配置均不生效 +CASE_FILE = + +;最大失败用例数量的占比 +;比如:总执行用例数为 100, 若 MAX_FAIL = 0.5,则失败用例数达到 50 就会终止测试。 +MAX_FAIL = 1 + +;单条用例的超时时间,如果一条用例的执行时间超时,这条用例会被停止,后续用例继续执行。 +;单位为秒 +;这是一个全局统一配置,如果某条用例需要单独配置超时时间,可以在用例中这样写: +;@pytest.mark.timeout(500) +;def test_xxx_001(): +; ... +;会话超时(所有用例执行的超时时间)是根据全局超时配置和用例单独超时配置自动计算的。 +CASE_TIME_OUT = 200 + +;失败用例重跑次数 +;注意,RERUN = 1 表示重跑 1 次,即第一次用例执行失败会自动重跑 1 次,总共执行 2 次; +;如果第 2 次执行成功,结果成功,失败亦为失败。 +RERUN = 1 + +;失败录屏从第几次失败开始录制视频。 +;比如 RECORD_FAILED_CASE = 1 ,表示用例第 1 次执行失败之后开始录屏,RERUN >= RECORD_FAILED_CASE。 +;1.关闭录屏:RECORD_FAILED_CASE > RERUN +;2.每条用例都录屏:RECORD_FAILED_CASE = 0 +RECORD_FAILED_CASE = 1 + +;yes 每条用例执行之后进行环境清理 +CLEAN_ALL = yes + +;检查测试机分辨率, 比如:1920x1080 +;no: 表示不做分辨率校验 +RESOLUTION = 1920x1080 + +;不跳过用例,csv文件里面标记了 skip-xxx的用例不跳过 +NOSKIP = no + +;ignore fixed +;no,只要标记了fixed的用例,即使标记了skip-,也会执行; +;yes,fixed不生效,仅通过skip跳过用例; +IFIXED = no + +;要安装deb包的路径 +;e.g : ~/Downloads/ 安装下载目录下的deb包,如果是远程执行,会自动拷贝到远程并安装。 +DEB_PATH = + +;DEBUG 模式执行用例,只收集不执行用例,也不做设备分辨率的检查。 +DEBUG = no + +;记录top命令查询的系统资源占用情况,TOP = 3 表示记录前3个进程。 +TOP = + +;指定用例执行次数 +REPEAT = + +;默认在所有测试完成之后输出报错信息. +;yes, 测试过程中立即显示报错 +DURING_FAIL = no + +;注册自启服务 +AUTOSTART = no + +;测试机的密码 +PASSWORD = 1 + +;图像识别重试次数 +IMAGE_MATCH_NUMBER = 1 + +;图像识别重试每次间隔等待时间 +IMAGE_MATCH_WAIT_TIME = 1 + +;图像识别匹配度 +IMAGE_RATE = 0.9 + +;截取当前屏幕实时图像保存路径,用于图像识别坐标 +SCREEN_CACHE = /tmp/screen.png + +;截取屏幕上指定区域图片,保存临时图片的路径 +TMPDIR = /tmp/tmpdir + +;系统主题 +SYS_THEME = deepin + +;OCR服务端地址(不可随意修改) +OCR_SERVER_HOST = youqu-dev.uniontech.com + +;OpenCV服务端地址 +OPENCV_SERVER_HOST = youqu-dev.uniontech.com + +;=============================== REPORT CONFIG =================================== +[report] +;测试报告的title +REPORT_TITLE = YouQu Report + +;测试报告的name +REPORT_NAME = YouQu Report + +;测试报告的默认语言 +;en:English +;ru:Русский +;zh:中文 +;de:Deutsch +;nl:Nederlands +;he:Hebrew +;br:Brazil +;pl:Polski +;ja:日本語 +;es:Español +;kr:한국어 +;fr:Français +;az:Azərbaycanca +REPORT_LANGUAGE = zh + +;用例执行完后生成的测试报告格式 +;目前支持 allure, xml, json (支持同时生成) +REPORT_FORMAT = allure, xml, json + +;指定报告生成的路径(相对项目根目录下) +ALLURE_REPORT_PATH = report/ +XML_REPORT_PATH = report/ +JSON_REPORT_PATH = report/ + +;=============================== PMS CONFIG =================================== +;PMS相关配置,包含以下几个方面: +;1.PMS测试套执行 +;2.自动从PMS爬取数据并同步本地CSV文件 +;3.PMS数据回填 +[pmsctl] +;PMS的用户名,如: ut001234 +PMS_USER = + +;PMS的密码 +PMS_PASSWORD = + +;PMS测试套的ID +;在PMS上查看用例“套件”链接: https://pms.uniontech.com/testsuite-view-495.html +;测试套ID为: 495 +SUITE_ID = + +;数据回填必须关联PMS测试单 +;在PMS上查看测试单链接: https://pms.uniontech.com/testtask-cases-20747.html +;测试单ID为: 20747 +TASK_ID = + +;将测试结果数据回填到PMS +;为空: 表示不回填,不会在每条用例执行完之后生成json结果文件; +;async: 表示逐条异步回填,后面一条执行开始时通过子线程对前一条用例的执行结果进行回填,如此实现时间效率最大化; +;finish: 表示所有用例执行完成之后逐个回填(PMS不支持并发); +SEND_PMS = + +;数据回填的触发者 +;auto: 框架自动回填,配合SEND_PMS配置使用,你可以选择在不同的阶段进行数据回填; +;hand: 手动回填,每条用例仍然会生成json文件,但框架不会进行数据回填,需要你可以在你想要发送的时间点手动触发回填; +TRIGGER = auto + +;PMS回填的重试次数 +;如果接口请求失败,会进行重试 +SEND_PMS_RETRY_NUMBER = 2 + +;caselib: 用例库 +;testcase: 产品库用例 +CASE_FROM = caselib + +[pmsctl-pms_link_csv] +;同步PMS数据到本地CSV文件,必须要配置的配置项 +;key是本地CSV文件的文件名称; +;value是对应PMS上的模块ID; +;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, +;CSV文件名称为music.csv,其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html +;因此应该配置为: music = 81 +;这样才能将PMS与本地CSV文件建立联系。 +;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 +music = + +[csvctl] +;将py文件的case id同步到csv文件 +;yes, 开启同步 +PY_ID_TO_CSV = no + +;导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加 +EXPORT_CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过 + + +[log_cli] +;日志相关配置(不打印构造函数和魔法函数的功能说明) +;批量执行时,终端输出的日志级别 DEBUG/INFO/ERROR +LOG_LEVEL = DEBUG + +# ============= 自动输出日志的配置 ================ +;支持类名以 xxx 开头的,自动将函数说明打印为日志, 多个参数以逗号隔开 +CLASS_NAME_STARTSWITH = Assert + +;支持类名以 xxx 结尾的,自动将函数说明打印为日志,多个参数以逗号隔开 +CLASS_NAME_ENDSWITH = Widget + +;支持类名包含 xxx 的,自动将函数说明打印为日志,多个参数以逗号隔开 +CLASS_NAME_CONTAIN = ShortCut +# ============================================== +``` + +配置完成之后,直接在命令行执行 `manage.py` 就好了。 + +```shell +youqu manage.py run +``` + +### 3. 远程执行 + +远程执行就是用本地作为服务端控制远程机器执行,远程机器执行的用例相同; + +使用 `remote` 命令: + +```shell +youqu manage.py remote +``` + +#### 3.1. 多机器分布式异步执行 + +![](https://pic.imgdb.cn/item/64f6d3c0661c6c8e549f8ca5.png) + +多机器分布式异步执行就是由本地 `YouQu` 作为服务端,控制远程 N 台机器执行相同的用例,执行完之后所有测试机的测试结果会返回给服务端 report 目录下; + +远程执行同样通过配置文件 `setting/globalconfig.ini` 进行用例相关配置; + +需要重点说一下远程执行时的测试机信息配置,在配置文件 `setting/remote.ini` 里面配置测试机的用户名、IP、密码。 + +```ini +;=============================== CLIENT LIST ===================================== +; 测试机配置列表 +;[client{number}] ;测试机别名,有多少台测试机就写多少个 client,别名必须包含 client 字符,且不能重复。 +;user = ;测试机 user +;ip = ;测试机 ip +;password = 1 ;测试机的密码, 可以不配置此项,默认取 CLIENT_PASSWORD 的值; + ;如果你所有测试机密码都相同,那么只需要配置 CLIENT_PASSWORD 就可以了 +;================================================================================= + +[client1] +user = uos +ip = 10.8.15.xx + +[client2] +user = uos +ip = 10.8.15.xx + +[client3] +user = uos +ip = 10.8.11.xx +``` + +有多少台机器就像这样参考上面的格式写就行了。 + +然后在命令行: + +```shell +youqu manage.py remote +``` + +这样运行是从配置文件去读取相关配置。 + +如果你不想通过配置文件,你仍然通过命令行参数进行传参, + +以下为 `python3 manage.py remote` 提供的一些参数选项: + +```coffeescript + -h, --help show this help message and exit + -c CLIENTS, --clients CLIENTS + 远程机器的user@ip:password,多个机器用'/'连接,如果password不传入,默认取sett + ing/remote.ini中CLIENT_PASSWORD的值,比如: uos@10.8.13.33:1 + 或 uos@10.8.13.33 + -s, --send_code 发送代码到测试机(不含report目录) + -e, --build_env 搭建测试环境,如果为yes,不管send_code是否为yes都会发送代码到测试机. + -p CLIENT_PASSWORD, --client_password CLIENT_PASSWORD + 测试机密码(全局) + -y PARALLEL, --parallel PARALLEL + yes:表示所有测试机并行跑,执行相同的测试用例;no:表示测试机分布式执行,服务端会根据收集到的测试用例自 + 动分配给各个测试机执行。 +``` + +除了这些特有参数以外,它同样支持本地执行的所有参数; + +在命令行这样运行: + +```shell +youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" +``` + +所有用例执行完之后会在 `report` 目录下回收各个测试机执行的测试报告。 + +注意,如果远程机器没有搭建自动化测试环境,记得加上参数 `-e` : + +```shell +youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" -e +``` + +执行前确保远程机器已经开启了 ssh 服务,否则会提示无法连接,如果没有开启,请手动开启: + +```shell +sudo systemctl restart ssh +sudo systemctl enable ssh +``` + +配置文件其他相关配置项详细说明,请查看配置文件中的注释内容。 + +#### 3.2. 多机器分布式异步负载均衡执行 + +多机器分布式异步负载均衡执行也是用本地作为服务端控制远程机器执行,但远程机器执行的用例不同,而是所有远程机器执行的用例之和,为你想要执行的用例集; + +似乎有点难以理解,我用大白话举例描述下就是,服务端想要执行 10 条用例,现在远程机器有 5 台; + +然后服务端就先拿着第 1 条用例给远程 1 号机执行,拿第 2 条用例给远程 2 号机执行...,如此循环直到所有用例执行完,这就是负载均衡执行。 + +![](https://pic.imgdb.cn/item/64f6d694661c6c8e54a1025b.png) + +使用方法和前面一样,只是需要增加一个参数 `--parallel`: + +```shell +youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" --parallel no +``` + + + +## 图像识别 + +图像识别在 `UI` 自动化中是不可缺少的,市面上甚至有完全基于图像识别的自动化测试框架,比如 `Airtest`、`Sikuli` 等,在游戏等特定领域也有不错的效果,这些工具实际上也是用的 `OpenCV` 进行了封装,`YouQu` 框架基于 `OpenCV` 开发了自己的图像识别功能,它可以方便的用于界面元素的定位和断言; `YouQu` 的图像识别功能几乎满足了你的所有要求,我们在长时间的思考和摸索中,针对常规场景及一些特殊场景探索除了一些实用且有效的方案,且听我慢慢道来。 -### 1、常规识别 +### 1. 常规识别 【背景】 @@ -32,11 +488,11 @@ ![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4c33.png) -那么实际上,元素定位的问题就转换为,将截图的小图(play_btn)拿到整个屏幕的大图(screen)中去做匹配,如果匹配成功,返回小图在大图中的坐标( x, y )即可。 +那么实际上,元素定位的问题就转换为,将截图的小图(`play_btn`)拿到整个屏幕的大图(`screen`)中去做匹配,如果匹配成功,返回小图在大图中的坐标( x, y )即可。 为了方便描述,以下我将整个屏幕的截图称为:大图,某个元素图片的截图称为:小图。 -基于 OpenCV 的模板匹配 `cv.matchTemplate()` 功能,我们实现了图像定位的功能,框架提供了一个图像识别的底层接口(一般不对上层提供调用): +基于 `OpenCV` 的模板匹配 `cv.matchTemplate()` 功能,我们实现了图像定位的功能,框架提供了一个图像识别的底层接口(一般不对上层提供调用): ```python def _match_image_by_opencv( @@ -61,7 +517,7 @@ def _match_image_by_opencv( 【参数介绍】 -1、image_path +**(1)image_path** `image_path` 是小图的绝对路径; @@ -71,21 +527,21 @@ def _match_image_by_opencv( 这样是为了方便管理和维护。 -2、rate +**(2)rate** 图像识别的的匹配度或者说相似度,框架默认的配置为 `0.9`,也就是说小图在大图中存在一个相似度 90% 的图标即返回其在大图中的坐标; 如果你在用例中需要调整识别度,你可以在调用函数的时候,传入不同的识别度的值。 -3、multiple +**(3)multiple** 默认情况下 `multiple=False`,表示只返回识别到的第一个,如果 `multiple=True` 返回匹配到的多个目标,因为大图中可能存在多个相同的小图,在某些场景下你可能需要全部获取到所有匹配到的坐标。 -4、picture_abspath +**(4)picture_abspath** 默认情况下 `picture_abspath=None` 表示大图为截取的屏幕截图,如果你不希望大图是屏幕的截图,而是你自定义传入的某个图片,你只需要将你的图片路径传递给这个参数就行,比如: `picture_abspath="~/Desktop/big.png"` ; -5、screen_bbox +**(5)screen_bbox** 大图默认情况下是截取整个屏幕,`screen_bbox = [x, y, w, h]` 可以指定截取屏幕中的固定区域,某些场景下,可以排除部分区域对识别结果的影响。 @@ -140,7 +596,7 @@ def find_image( -### 2、气泡识别 +### 2. 气泡识别 【背景】 @@ -180,19 +636,19 @@ def get_during( 【参数介绍】 -1、screen_time +(1)screen_time 截取屏幕图片的时间,在此时间内会不断的进行截图操作,就像录制视频一样; -2、pause +(2)pause 每次截取图片的间隔时间,默认情况下是一刻不停的截图,如果你想每次截图存在一些间隔时间传入对应的时间间隔即可,单位是秒,比如:pause = 0.03,表示 30 ms,相当于帧率为 30 帧; -### 3、不依赖 OpenCV 的图像识别方案 +### 3. 不依赖 OpenCV 的图像识别方案 -#### 3.1、自研图像识别技术 +#### 3.1. 自研图像识别技术 【原理】 @@ -200,7 +656,7 @@ def get_during( ![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4d85.png) -1、读取小图和大图的RGB值 +**读取小图和大图的RGB值** (1)小图的RGB值 @@ -215,7 +671,7 @@ small_data = small_pic.load() big_data = big_pic.load() ``` -2、将小图与大图的RGB值进行匹配 +**将小图与大图的RGB值进行匹配** (1)匹配从大图的坐标(0,0)开始匹配,匹配小图里面所有的坐标点(0,0)—(small_pic.width,small_pic.height); @@ -274,15 +730,15 @@ class ImageRgb: 通过 `match_image_by_rgb()` 这个函数,传入目标小图的文件名称,即可返回在当前屏幕中的中心坐标。 -有同学要问了,有 OpenCV 干嘛不用,有必要自己实现一个图像识别的功能吗,你们是不是闲的啊? +有同学要问了,有 `OpenCV `干嘛不用,有必要自己实现一个图像识别的功能吗,你们是不是闲的啊? 这么问的话,小了,格局小了;我们自己实现主要有几方面原因: -- 减少环境依赖,即不用安装 OpenCV,我们也能实现其功能,环境依赖这块后面会单独详细讲,减少环境依赖对于任何软件工程都非常重要; -- OpenCV 在其他国产 CPU 架构上安装并不能保证100%成功,甚至有没有可能在一些架构上压根儿就不能安装使用 OpenCV ? -- 有没有可能有一天国内无法使用 OpenCV ?就像有没有可能有一天国内无法使用 Windows 呢?这些问题值得思考。 +- 减少环境依赖,不用安装 `OpenCV` 我们也能实现其功能,环境依赖这块后面会单独详细讲,减少环境依赖对于任何软件工程都非常重要; +- `OpenCV` 在其他国产 CPU 架构上安装并不能保证100%成功,甚至有没有可能在一些架构上压根儿就不能安装使用 `OpenCV` ? +- 有没有可能有一天国内无法使用 `OpenCV` ?就像有没有可能有一天国内无法使用 Windows 呢?这些问题值得思考。 -当然,我们承认这套方案,虽然识别准确率没问题,但在识别效率上还没有达到 OpenCV 模板匹配的效果,我们的方案每次识别在 1.5s 左右,而 OpenCV 在 1s 左右; +当然,我们承认这套方案,虽然识别准确率没问题,但在识别效率上还没有达到 `OpenCV` 模板匹配的效果,我们的方案每次识别在 `1.5s` 左右,而 `OpenCV `在 `1s` 左右; 整体识别效果来讲,我认为还是可以接受的,也希望有志之士能一起优化此方案,一起技术报国。 @@ -371,9 +827,9 @@ except OSError as exc: -### 4、右键菜单前后对比图像识别 +### 4. 右键菜单前后对比图像识别 -#### 4.1、右键菜单定位的方案及问题 +#### 4.1. 右键菜单定位的方案及问题 右键菜单的元素定位是一个难点,过去我们调研和使用过的元素定位操作方法有 4 种: @@ -485,7 +941,7 @@ action = ( -### 5、动态图像识别 +### 5. 动态图像识别 【背景】 @@ -527,15 +983,15 @@ def save_temporary_picture(_x: int, _y: int, width: int, height: int): -## 三、基于 UI 的元素定位方案 +## 基于 UI 的元素定位方案 -### 1、背景 +### 1. 背景 基于 UI 的元素定位方案是我们自研的一种使用简单,且效率极高、稳定性好的元素定位方案,基于元素按钮在应用中的相对位置,动态获取元素在当前屏幕中的位置,适用于各种屏幕分辨率(包括高分屏、宽屏、带鱼屏),当元素按钮位置相对于应用界面位置发生修改之后,只需要根据 UI 设计图上的源数据修改对应坐标数据就好,维护非常的方便。 此类元素定位方案适用于一些元素位置相对与应用界面比较固定的应用,比如音乐(99% 的元素定位采用这种,效果非常好),不适用于界面不固定的应用,比如截图录屏,很明显不适用于这类元素定位方案。这种全新的元素定位方案有它的适用条件,如果你发现使用常规的(属性定位、图像定位)不好做时,不妨考虑使用这种,其效果一定能惊讶到你,并且迅速爱上他。 -### 2、实现原理 +### 2. 实现原理 在 UI 设计图中我们是可以获取到元素按钮相对于应用边框的距离的,然后我们可以通过技术手段获取到应用界面在当前屏幕中的位置及应用窗口的大小,示意图如下: @@ -631,13 +1087,13 @@ def click_add_music_list_btn_in_music_by_ui(self): -## 四、OCR 定位 +## OCR 定位 -### 1、背景 +### 1. 背景 传统的 OCR 方案大多采用谷歌 OCR(`Tesseract`)方案,但是它对于中文的识别非常差,经过大量的调研,我们使用 `PaddleOCR`,它是一个开源的基于深度学习的 OCR 识别工具,也是 `PaddlePaddle` 最有名的一个开源项目,感兴趣的可以点[这里](https://github.com/PaddlePaddle/PaddleOCR)了解,多的不说了,你只需要知道它就是中文识别的天花板。 -### 2、实现原理 +### 2. 实现原理 安装他是个很麻烦的事情,虽然操作很简单,但其实安装包有点大,因此我们不希望直接在 `env.sh` 中加入它,这会让整个自动化环境变得非常臃肿; @@ -649,7 +1105,7 @@ RPC 的调用逻辑: 这样我们只需要在服务端部署好 OCR 识别的服务,然后通过 RPC 服务将功能提供出来,框架里面只需要调用对应的 RPC 接口就行了。 -### 3、使用说明 +### 3. 使用说明 服务端代码示意(Service): @@ -753,11 +1209,11 @@ OCR.ocr(*target_strings, picture_abspath=None, similarity=0.6, return_default=Fa self.assert_ocr_exist("取消收藏") ``` -### 4、服务端部署 +### 4. 服务端部署 我们目前是将 OCR 服务部署在普通的办公机上的,如果你觉得现有的 OCR 识别性能不够好,恰好你有更好的机器,可以考虑将其私有化部署。 -#### 4.1、环境安装 +#### 4.1. 环境安装 推荐使用 `pipenv` 进行环境搭建; @@ -790,7 +1246,7 @@ pipenv install "paddleocr>=2.0.1" -i https://mirror.baidu.com/pypi/simple 不出意外,这样就把依赖安装好了。 -#### 4.2、启动服务 +#### 4.2. 启动服务 将基础框架中的 `scr/ocr/pdocr_rpc_server.py` 文件拷贝到 `ocr_env` 目录,后台执行它就好了: @@ -799,7 +1255,7 @@ cd ocr_env nohup pipenv run python pdocr_rpc_server.py & ``` -#### 4.3、配置开机自启(通用) +#### 4.3. 配置开机自启(通用) 你肯定不想每次机器重启之后都需要手动启动服务,因此我们需要配置开机自启。 @@ -850,9 +1306,9 @@ sudo systemctl status ocr.service 你可以再重启下电脑,看看服务是不是正常启动了,没报错就 OK 了。 -## 五、属性定位 +## 属性定位 -### 1、背景 +### 1. 背景 传统的 UI 自动化大多都是基于浏览器的,核心是在网页的 DOM 树上查找元素,并对其进行定位操作; @@ -862,9 +1318,9 @@ Linux 桌面应用大多是采用 Qt 编写的,在 Qt 中也是从最顶层的 借助开源工具 `dogtail` 我们可以对元素进行获取,从而进行定位操作。`dogtail` 的项目介绍可以[猛戳这里](https://gitlab.com/dogtail/dogtail/)。 -### 2、使用方法 +### 2. 使用方法 -#### 2.1、sniff(嗅探器)使用 +#### 2.1. sniff(嗅探器)使用 在终端输入 sniff 启动 AT-SPI Browser @@ -892,7 +1348,7 @@ mikigo@mikigo-PC:~$ sniff ![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4f0b.png) -#### 2.2、元素操作 +#### 2.2. 元素操作 获取应用对象 @@ -969,7 +1425,7 @@ element.typeText(string) element.keyCombo(comboString) ``` -#### 2.3、框架封装 +#### 2.3. 框架封装 代码示例: @@ -1010,15 +1466,88 @@ self.dog.element_center("播放") 这样就能获取到此元素的中心坐标。 -## 六、标签化管理 +## 重启类场景 -### 1、标签说明 +对于重启类场景的用例需要解决的核心问题是,重启之后如何让用例能继续重启前的步骤继续执行,`YouQu` 集成了自研的 [letmego](https://linuxdeepin.github.io/letmego/) 技术方案; + +详细技术方案、实现细节、Demo可以看 [letmego](https://linuxdeepin.github.io/letmego/) 官方在线文档; + +### 1. 使用方法 + +使用方法很简单,只需要给应用方法层的唯一出口类加一个装饰器(`@letmego.mark`)即可: + +```python +import letmego + +@letmego.mark +class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget): + """音乐业务层""" +``` + +### 2. 用例注意事项 + +这类用例相对特殊,这里主要介绍写用例的时候注意事项: + +(1)用例的前置和后置要写在同一个用例文件里面;这点如果了解方案实现原理很容易理解; + +(2)重启步骤前面的步骤,如果有对象实例化的,需要处理实例化存在异常;因为 `YouQu` 的对象实例化默认会检测应用是否启动,重启之后虽然重启步骤前面的步骤函数不会执行,但是方法类同样会进行实例化,所以需要处理这个问题; + +```python +# ignore import +class TestMusic(BaseCase): + """ + 音乐用例 + """ + def test_music_679537(self): + try: + music = DeepinMusicWidget() + music.click_singer_btn_in_music_by_ui() + music.click_icon_mode_in_music_by_ui() + except ApplicationStartError: + pass + # ========== reboot ========== + DeepinMusicWidget.reboot() + # ========== 重启之后继续执行 ========= + DdeDockPublicWidget().open_music_in_dock_by_attr() + DeepinMusicWidget.recovery_music_by_cmd() + music = DeepinMusicWidget() + music.first_add_music_by_ui() + music = DeepinMusicWidget() + music.click_singer_btn_in_music_by_ui() + music.click_icon_mode_in_music_by_ui() + self.assert_music_image_exist("music_679537") +``` + +(3)重启步骤最好是一个简单的reboot操作,不建议在组合步骤中间插入一个reboot; + +```python +@letmego.mark +class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget): + + @staticmethod + def reboot(): + """letmego reboot""" + os.system("echo '1' | sudo -S reboot") +``` + +### 3. 驱动执行 + +因为重启类场景需要注册自启服务以及对用例执行过程的处理,驱动执行的时候加 `--autostart yes` : + +```shell +youqu manage.py run --autostart yes +``` + + + +## 标签化管理 + +### 1. 标签说明 根据现有业务需要,用例需要添加的标签有: - 脚本 ID:自动化用例脚本/函数 ID; -- `PMS用例ID`:`PMS` 上对应的用例 ID(用例库 ID); - - 默认使用用例库 ID,对于暂时没有使用用例库管理用例的项目,可以使用产品库用例 ID; +- `PMS用例ID`:`PMS` 上对应的用例 ID(用例库 ID);默认使用用例库 ID,对于暂时没有使用用例库管理用例的项目,可以使用产品库用例 ID; - 用例级别:对应 `PMS` 上用例级别,分别用 `L1、L2、L3、L4` 表示; - 用例类型:`FUNC`(功能)、`PERF`(性能)、`STR`(压力)、`SEC`(安全)、`CTS`(兼容性)、`API`(接口)、`BASELINE`(基线-预留) - 设备类型:`PPL`(依赖外设的用例)、`COL`(依赖主控机的用例) @@ -1035,7 +1564,7 @@ self.dog.element_center("播放") | :----: | :-------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :-------: | -------------- | ---- | | 679537 | 679537 | L1 | FUNC | PPL | | CICD | C1 | skip-XXX | fixed-XXX | removed-已废弃 | ... | -### 2、操作步骤 +### 2. 操作步骤 2.1、在子项目目录下新建 `csv` 文件,用于保存用例标签,以用例的 `py` 文件除去首字符串 "test_" 和用例序号后的字符串作为 `csv` 的文件名。 @@ -1049,15 +1578,15 @@ self.dog.element_center("播放") 2.2、第一列为脚本 ID,从第二列开始及之后的列,每一列都是一个用例标签;后续需要新增用例标签,可以直接在 `csv` 文件里面添加对应的列即可;用例标签可以无序。 -### 3、增加说明 +### 3. 增加说明 -#### 3.1、跳过用例 +#### 3.1. 跳过用例 传统跳过用例的方式是在用例脚本里面给用例添加装饰器(`@pytest.mark.skip`),解除跳过时将装饰器代码删掉,这种方式需要修改用例代码,而通过 `csv` 文件来管理跳过用例则会方便很多; 将跳过用例操作也整合进入用例标签,在 `csv` 文件中新增一列为“跳过原因”; -##### 3.1.1、固定跳过 +##### 3.1.1. 固定跳过 示例: @@ -1068,7 +1597,7 @@ self.dog.element_center("播放") - 如果应用受到新需求影响需要跳过,则在此列备注具体的跳过原因。跳过的原因统一标签开头为 “`skip-XXX`”; - 用例执行时判断 `csv` 文件里面跳过原因列是否存在跳过标签,存在跳过标签则用例也不会被执行,最终的用例状态会被标签为 `SKIPED`。 -##### 3.1.2、条件判断跳过 +##### 3.1.2. 条件判断跳过 示例: @@ -1105,7 +1634,7 @@ def skipif_platform(args: str): 若函数需要多个参数,可自定义多个参数之间的连接符,连接符号不可使用下划线和逗号,推荐统一使用 `&` 符号。 -#### 3.2、确认修复 +#### 3.2. 确认修复 针对于某些用例修复后,但不能立即删除跳过原因(`skip-XXX`)的用例,新增一列标签名为 “确认修复”,作为标记该用例是否已经修复,固定填入字段为 “`fixed-已修复`”。这样这条用例即使同时标记了 `skip-XXX` 也会正常执行。 @@ -1139,33 +1668,124 @@ python3 manage.py run --ifixed yes 这就是“确认修复”这个标签的背景,需要各位看官稍微品一品。 -#### 3.3、废弃用例 +#### 3.3. 废弃用例 针对某些用例,由于需求变更,环境影响或评估不再适用于自动化测试时,用例需要废弃,则新增一列标签名为 “废弃用例”,该列存在 “removed-{废弃原因}”,则用例不会执行。 | 用例ID | ...(各种用例标签) | 跳过原因 | 确认修复 | 废弃用例 | | :----: | :-----------------: | :-------------------: | :----------: | :------------: | -| 001 | ... | skip-受到某新需求影响 | fixed-已修复 | removed-已废弃 | +| 679537 | ... | skip-受到某新需求影响 | fixed-已修复 | removed-已废弃 | ![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4f70.png) -### 4、设计思路 +### 4. 设计思路 上面介绍 `Pytest` 框架提供的标签功能 mark,使用时需要为每一个用例添加标签装饰器,则操作复杂,可维护性差,其根本问题就是标签分散在每一条用例的装饰器上,难以集中维护;于是乎将所有标签使用 `csv` 文件进行集中管理,并通过 `Pytest` 的钩子函数,读取 `csv` 文件,动态添加标签到用中。 -### 5、CSV文件格式 +### 5. CSV文件格式 此配置文件需要维护大量的标签数据,且要方便能使用 `Excel` 打开进行编辑查看,更重要的是我们不想引入三方依赖,CSV 文件几乎是唯一能满足所有的要求的文件格式。 -## 七、日志系统 -### 1、背景 -基于 YouQu 自动化测试框架操作方法封装写法,通常是这样的:类里面一个函数只包含一个操作或多次调用的一系列可合并的操作; +## 标签自动同步 -传统的日志输出方式,需要在每个函数里面主动编写日志输出代码 ,例如: +### 1. 自动同步脚本ID到CSV文件 + +支持自动同步脚本ID(用例 `py` 文件的 `ID`)到 `CSV` 文件; + +【使用方法一】 + +配置文件方式,通过一下几个配置来控制: + +```ini +[csvctl] +;将py文件的case id同步到csv文件 +;yes, 开启同步 +;no, 关闭同步 +PY_ID_TO_CSV = yes +``` + +如果不存在 `CSV` 文件会直接创建一个并写入用例脚本的ID; + +此功能默认会将 `CSV` 文件中多余的ID行删掉,以处理人工删除了用例脚本文件,但 `CSV` 文件里面对应的`ID` 行未删除的问题; + +【使用方法二】 + +命令行参数的方式: + +```shell +youqu manage.py csvctl -p2c +``` + +不管配置文件是否配置,通过命令行参数的方式执行优先级总是最高的。 + +### 2. 从PMS自动同步标签信息 + +用于自动同步 `PMS` 用例标签数据至本地 `CSV` 文件; + +【使用方法一】 + +配置文件方式,通过以下几个配置来控制: + +```ini +APP_NAME = # 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同) +PMS_USER = # PMS的用户名 +PMS_PASSWORD = # PMS的密码 +``` + +在 `[pmsctl-pms_link_csv]` 节点下指定 `CSV` 文件名与 `PMS` 用例模块的对应关系,比如: + +```ini +[pmsctl-pms_link_csv] +;同步PMS数据到本地CSV文件,必须要配置的配置项 +;key是本地CSV文件的文件名称; +;value是对应PMS上的模块ID; +;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, +;CSV文件名称为music.csv,其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html +;因此应该配置为: music = 81 +;这样才能将PMS与本地CSV文件建立联系。 +;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 +music = 53 +``` + +将以上信息配置好之后,在命令行执行: + +```shell +youqu manage.py pmsctl --p2c +``` + +每次执行时原 `CSV` 文件会自动备份在 `report` 目录下,因此你不用担心脚本执行导致你的数据丢失。 + +【使用方法二】 + +按照我们一贯的风格,你也可以不去管配置文件,完全通过命令行参数传入: + +``` +youqu manage.py pmsctl --p2c -u ut00xxxx -p you_password -pls music:81 +``` + +## 导出 CSV 文件 + +框架提供导出指定标签用例的功能: + +```shell +youqu manage.py csvctl -a deepin-album -t CICD -ec case_list.csv +``` + +表示导出 `deepin-album` 的用例中标记了 `CICD` 标签的用例,导出 `CSV` 文件的字段格式已经适配了 `CICD` 的要求。 + + + +## 日志系统 + +### 1. 背景 + +基于 `YouQu` 自动化测试框架操作方法封装写法,通常是这样的:类里面一个函数只包含一个操作或多次调用的一系列可合并的操作; + +**传统的**日志输出方式,需要在每个函数里面主动编写日志输出代码 ,例如: ```python class XXXWidget: @@ -1183,9 +1803,9 @@ class XXXWidget: 通过经验观察,我们发现,函数说明以及函数操作日志,具有较高的重复度(从上面的例子也可以看出来),因此我们大胆的设想,能不能基于框架执行时,自动的将函数说明作为日志打印出来,从而减少大量日志代码量和重复编写,那真是妙啊~。 -基于此“天真”的想法,我们设计出了 `youqu` 的日志系统。 +基于此“天真”的想法,我们设计出了 `YouQu` 的日志系统。 -### 2、实现原理 +### 2. 实现原理 核心原理: @@ -1196,7 +1816,7 @@ class XXXWidget: 1. 通过 ```inspect.getmembers``` 获取被装饰的类下所有函数,包含静态方法,类方法,实例方法; 2. 通过 ```setattr(类, 方法, _trace)``` 的方式,给符合条件的函数,动态添加日志装饰器; -### 3、日志配置 +### 3. 日志配置 ```ini [log_cli] @@ -1216,9 +1836,9 @@ CLASS_NAME_CONTAIN = ShortCut # ============================================== ``` -### 4、使用方法 +### 4. 使用方法 -在应用库 widget 方法库里面,只需要在出口文件加上**类装饰器**,就可以实现自动输出日志了; +在应用库 `widget` 方法库里面,只需要在出口文件加上**类装饰器**,就可以实现自动输出日志了; ```python # dfm_widget.py @@ -1276,9 +1896,78 @@ DfmWidget.find_dfm_image("dfm_001") 没错,这就是我们参考`Django` 和 `jinja2` 的模板语法设计出的**日志模板语法**,使用方法很简单,**用两对大括号把函数的参数括起来**,这样在日志输出的时候就能把调用函数时参数的值输出出来。 -## 八、环境部署 +## PMS数据回填 -### 1、原则 +测试单关联的用例,自动化测试对应的去跑这些关联的用例,并且将执行的结果回填的测试用例的状态里面。 + +PMS 数据回填主要有三种方式: + +**(1)异步回填** + +在用例执行的过程中,采用异步的方式去进行数据回填,直白的讲就是,第二条用例开始跑的时候,通过子线程去做第一条用例的数据回填,如此循环,直到所有用例执行结束; + +这种方案的时间**效率最高**的,因为理论上用例的执行时间是大于数据回填的接口请求时间的,也就是说,当用例执行完之后,数据回填也完成了。 + +使用方法,在 `globalconfig.ini` 里面配置以下参数:(以下涉及到的参数配置都是在配置文件里面进行配置) + +```ini +PMS_USER = PMS账号 +PMS_PASSWORD = PMS密码 +SEND_PMS = async +TASK_ID = 测试单ID +TRIGGER = auto +APP_NAME = 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同) +``` + +**(2)用例执行完之后回填** + +等所有用例执行完之后,再逐个进行回填的接口请求,此方案时间效率比较低。 + +使用方法: + +```ini +PMS_USER = PMS账号 +PMS_PASSWORD = PMS密码 +SEND_PMS = finish +TASK_ID = 测试单ID +TRIGGER = auto +APP_NAME = +``` + +**(3)手动回填** + +所有用例执行完之后不做回填的接口请求,后续手动将结果进行回填请求。 + +用例执行时配置: + +```ini +PMS_USER = PMS账号 +PMS_PASSWORD = PMS密码 +SEND_PMS = finish +TASK_ID = 测试单ID +TRIGGER = hand +APP_NAME = +``` + +后续手动回填方法: + +```shell +youqu manage.py pms --send2task yes +``` + +【可能遇到的“严重问题”】 + +有同学可能会发现,怎么回填一次之后,后面想再次回填就不生效了; + +这是因为为了应对前面提到的多种数据回填的方式,在 `report` 目录下会有 `pms_xxx` 开头的目录,用于记录了用例的执行结果和回填情况,如果这条用例之前已经回填过了,后续就不会再此触发回填了; + +如果你想重新做回填,你可以把 `report/pms_xxx` 目录删掉,这样就可以重新做数据回填了; + + + +## 环境部署 + +### 1. 原则 YouQu 的环境依赖一直坚持 2 个原则: @@ -1304,7 +1993,7 @@ YouQu 的环境依赖一直坚持 2 个原则: 因此我们选择将 YouQu 的文档工程涉及到的图片资源都采用外链加载; -### 2、安装 +### 2. 安装 项目根目录下运行 `env.sh` 即可。 @@ -1314,9 +2003,9 @@ bash env.sh > 注意,如果你的测试机密码不是 1 ,那你需要在全局配置文件 `globalconfig.ini` 里面将 `PASSWORD` 配置项修改为当前测试机的密码。 -### 3、定制依赖 +### 3. 定制依赖 -#### 3.1、新增依赖 +#### 3.1. 新增依赖 如果应用库还需要其他 `Python` 依赖库,只需要在应用库根目录下保存一个 `requirement.txt` 文件; @@ -1339,7 +2028,7 @@ requests # 未指定版本则安装最新版 如果多个应用库都存在 `requirement.txt` 文件,执行 `env.sh` 时会将多个 `requirement.txt` 文件一并加载;那么一定要注意多个 `requirement.txt` 文件可能存在相同的依赖被指定安装不同版本等等兼容性问题。 -#### 3.2、裁剪依赖 +#### 3.2. 裁剪依赖 在某些情况下,可能你只需要安装一些最最基础的依赖,其他的都不需要,比如纯接口自动化的项目,它不需要 `UI` 自动化相关的依赖。 @@ -1358,8 +2047,7 @@ autotest_xxx pytest # pytest pytest-rerunfailures # 失败重跑插件 pytest-timeout # 用例超时插件 -allure-pytest # 生成 allure 报告插件 -allure.deb # allure 报告查看工具 +allure-pytest # 生成原始报告文件插件 ``` `裁剪依赖` 和 `新增依赖` 是不冲突的,可以同时使用。 @@ -1370,11 +2058,11 @@ allure.deb # allure 报告查看工具 bash env_dev.sh ``` -### 4、虚拟化部署 +### 4. 虚拟化部署 YouQu默认采用虚拟化部分,虚拟化环境实际安装的位置是在 `$HOME/.local/share/virtualenvs/youqu-oHTM7l7G` 目录下 -`youqu-oHTM7l7G` 此目录名称前面部分是你的代码根目录的名称,后面部分是生成的随机字符串; +`youqu-oHTM7l7G` 此目录名称前面部分是你的代码根目录的名称,后面部分是生成的随机字符串,同学们在部署的时候随机字符串肯定和我这里的例子不一样; 同学们在远程机器上定位问题的时候,如果使用 Pycharm 调试,就将解释器指定到这个目录的就行了; @@ -1386,7 +2074,7 @@ bash env_dev.sh 如果你是本地开发环境可以用它,区别就是驱动执行的时候使用:`python3 manage.py xxx` -## 九、失败录屏 +## 失败录屏 录屏其实是一种视频形式的日志,因为很多时候我们在查看日志之后仍然无法准确的定位到用例失败的具体原因,因此用例的录屏能让我们看到用例在执行过程; @@ -1416,7 +2104,7 @@ RECORD_FAILED_CASE = 1 -## 十、Wayland 适配 +## Wayland 适配 `Wayland` 下自动化主要问题是 `X11` 下的键鼠操作方法无法使用,比如 `Xdotool`、 `PyAutoGUI`、`Xwininfo` 等等; @@ -1452,7 +2140,7 @@ else: -## 十一、测试报告 +## 测试报告 ### 1. 目录结构 @@ -1479,7 +2167,7 @@ else: ├── record # 录屏 │   └── 2022-11-09 │   ├── ... - │   └── 15时14分09秒_test_music_303_2_autotest.mp4 + │   └── 15时14分09秒_test_music_679537_2_autotest.mp4 └── xml # xml报告 └── autotest_deepin_music-20221109134736.xml ``` @@ -1527,7 +2215,7 @@ allure open report/allure_html -十二、静态代码扫描 +静态代码扫描 -------------- ### 1. 提前解决代码问题 @@ -1574,7 +2262,7 @@ bash pylint.sh -十三、提交代码 +提交代码 ---------- ### 1. 安装依赖 @@ -1735,10 +2423,10 @@ git checkout -b -十四、常见问题 +常见问题 ------------------- -### 1. 提交代码时提示邮箱或者名称不对 +【提交代码时提示邮箱或者名称不对】 重新配置邮箱或者名称,然后重置生效: @@ -1746,7 +2434,9 @@ git checkout -b git commit --amend --reset-author ``` -### 2. 怎么回滚到之前的版本 + + +【怎么回滚到之前的版本】 (1)查询历史提交记录 @@ -1770,17 +2460,17 @@ git reset --soft ${hash} git reset --hard ${hash} ``` -### 3. 测试报告里面 F S . E 是什么意思 -`F` 表示失败,`.` (点)表示通过,`S` 表示跳过,`E` 表示 `error` 有报错 -### 4. 解决 git status 中文显示的问题 +【解决 git status 中文显示的问题】 ```shell git config --global core.quotePath false ``` -### 5. `apps` 目录下颜色有些是黄色的 + + +【`apps` 目录下颜色有些是黄色的】 在 `Pycharm` 中 `apps` 目录下应用库文件是黄色的,编辑器识别不到代码新增和修改; @@ -1792,7 +2482,9 @@ git config --global core.quotePath false 专业版 `Pycharm` 不存在这个问题。 -### 6. 执行 `env.sh` 报错 `$'\r':未找到命令` + + +【执行 `env.sh` 报错 `$'\r':未找到命令`】 出现这个问题你应该是在 windows 上打开或编辑过 `env.sh` 脚本,windows下的换行是回车符+换行符,也就是`\r\n`,而 `Linxu` 下是换行符 `\n`,`Linux` 下不识别 `\r`,因此报错。 @@ -1803,7 +2495,9 @@ git config --global core.quotePath false sudo sed -i 's/\r//' env.sh ``` -### 7.怎样为单独某一条用例配置执行超时时间 + + +【怎样为单独某一条用例配置执行超时时间】 在用例脚本中添加装饰器,如下: @@ -1813,44 +2507,9 @@ def test_xxx_001(): pass ``` -### 8. 如何拉取 YouQu 自动化项目所有代码,包含子仓库 -前置条件:已申请访问 YouQu 权限群组 -```shell -# 第一次存储密码,提供后面拉代码(gerrit: ldap账号和密码,git拉取第一次输入保存,后面直接使用) -git config --global credential.helper store - -# 拉取YouQu框架代码,并递归拉取子仓库代码 -git clone "http://gerrit.uniontech.com/youqu" --recursive -``` - -### 9. 如何拉取 YouQu 自动化项目中指定子仓库的代码 - -前置条件: - -- 已申请访问 YouQu 权限群组 -- 已存储访问密码 - -```shell -# 拉取YouQu框架代码 -git clone "http://gerrit.uniontech.com/youqu" - -# 指定拉取子仓库代码 -cd youqu -git submodule update --init apps/autotest_xxx_xxx -``` - -### 10. 如何更新 YouQu 自动化项目中,所有子仓库的代码 - -更新所有子仓库代码 - -```shell -cd youqu -git pull & git submodule foreach git pull -``` - -### 11. 如何修复 YouQu 所有子仓库 master 分支游离头(detached head) +【如何修复子仓库 master 分支游离头(detached head)】 修复所有子仓库默认master 分支游离头 diff --git a/manage.py b/manage.py index 1ad21fc..7870f46 100644 --- a/manage.py +++ b/manage.py @@ -78,6 +78,11 @@ class Manage: client_password=None, parallel=None, autostart=None, + pyid2csv=None, + export_csv_file=None, + pms2csv=None, + pms_link_csv=None, + send2task=None, ): self.default_app = app self.default_keywords = keywords @@ -116,6 +121,11 @@ class Manage: self.default_client_password = client_password self.default_parallel = parallel self.default_autostart = autostart + self.default_pyid2csv = pyid2csv + self.default_export_csv_file = export_csv_file + self.default_pms2csv = pms2csv + self.default_pms_link_csv = pms_link_csv + self.default_send2task = send2task say(GlobalConfig.PROJECT_NAME) version_font = "slick" @@ -135,8 +145,8 @@ class Manage: subparsers = parser.add_subparsers(help="子命令") sub_parser_remote = subparsers.add_parser(SubCmd.remote.value) sub_parser_run = subparsers.add_parser(SubCmd.run.value) - sub_parser_pms = subparsers.add_parser(SubCmd.pms.value) - sub_parser_export_csv = subparsers.add_parser(SubCmd.exportcsv.value) + sub_parser_pms = subparsers.add_parser(SubCmd.pmsctl.value) + sub_parser_csv = subparsers.add_parser(SubCmd.csvctl.value) help_tip = ( f"\033[0;32mmanage.py\033[0m 支持 \033[0;32m{[i.value for i in SubCmd]}\033[0m 命令, " @@ -152,15 +162,19 @@ class Manage: elif self.cmd_args[0] == SubCmd.run.value: _local_kwargs, _ = self.local_runner(parser, sub_parser_run) LocalRunner(**_local_kwargs).local_run() - elif self.cmd_args[0] == SubCmd.pms.value: + elif self.cmd_args[0] == SubCmd.pmsctl.value: self.pms_control(parser, sub_parser_pms) - elif self.cmd_args[0] == SubCmd.exportcsv.value: - self.export_csv(parser, sub_parser_export_csv) + elif self.cmd_args[0] == SubCmd.csvctl.value: + self.csv_control(parser, sub_parser_csv) elif self.cmd_args[0] == SubCmd.startapp.value: + start_config_log = f"{SubCmd.startapp.value} 后面直接加工程名称,工程名称以 'autotest_' 开头" try: + if self.cmd_args[1] in ("-h", "--help"): + print(start_config_log) + sys.exit(0) self.start_app(self.cmd_args[1]) except IndexError: - print(f"参数异常 {SubCmd.startapp.value} 后面需要跟参数") + logger.error(f"参数异常: {start_config_log}") elif self.cmd_args[0] in ["-h", "--help"]: print(help_tip) else: @@ -185,7 +199,7 @@ class Manage: help="搭建测试环境,如果为yes,不管send_code是否为yes都会发送代码到测试机." ) sub_parser_remote.add_argument( - "-p", "--client_password", default="", help="测试机密码(全局)" + "-cp", "--client_password", default="", help="测试机密码(全局)" ) sub_parser_remote.add_argument( "-y", "--parallel", default="", @@ -274,10 +288,10 @@ class Manage: "--deb_path", default="", help="需要安装deb包的本地路径" ) sub_parser_run.add_argument( - "--pms_user", default="", help="pms 用户名" + "-u", "--pms_user", default="", help="pms 用户名" ) sub_parser_run.add_argument( - "--pms_password", default="", help="pms 密码" + "-p", "--pms_password", default="", help="pms 密码" ) sub_parser_run.add_argument( "--suite_id", default="", help="pms 测试套ID" @@ -309,7 +323,7 @@ class Manage: "--line", default="", help="执行的业务线(写入json文件)" ) sub_parser_run.add_argument( - "--autostart", default="", help="用例执行程序注册到开机自启服务" + "--autostart", default="", help="重启类场景开启letmego执行方案" ) args = parser.parse_args() local_kwargs = { @@ -359,7 +373,22 @@ class Manage: def pms_control(self, parser=None, sub_parser_pms=None): """pms相关功能命令行参数""" sub_parser_pms.add_argument( - "--pms2csv", choices=["yes", ""], default="", help="爬取数据到csv" + "-a", "--app", default="", help="应用名称:deepin-music" + ) + + sub_parser_pms.add_argument( + "-u", "--pms_user", default="", help="pms 用户名" + ) + sub_parser_pms.add_argument( + "-p", "--pms_password", default="", help="pms 密码" + ) + sub_parser_pms.add_argument( + "-pls", "--pms_link_csv", default="", + help="pms 和 csv 的映射关系,比如:music:81/album:82,多个配置使用'/'分隔" + ) + sub_parser_pms.add_argument( + "-p2c", "--pms2csv", action='store_const', const=True, default=False, + help="从PMS爬取用例标签到csv文件" ) sub_parser_pms.add_argument( "--send2task", @@ -374,16 +403,29 @@ class Manage: help="触发者" ) args = parser.parse_args() - csv = args.pms2csv - send_to_task = args.send2task - task_id = args.task_id if args.task_id else GlobalConfig.TASK_ID - trigger = args.trigger if args.trigger else GlobalConfig.TRIGGER - if csv: - Pms2Csv().write_new_csv() - elif send_to_task and task_id and trigger == "hand": + pms_kwargs = { + Args.app_name.value: args.app or self.default_app, + Args.pms_user.value: args.pms_user or self.default_pms_user, + Args.pms_password.value: args.pms_password or self.default_pms_password, + Args.pms2csv.value: args.pms2csv or self.default_pms2csv, + Args.pms_link_csv.value: args.pms_link_csv or self.default_pms_link_csv, + Args.send2task.value: args.send2task or self.default_send2task, + Args.task_id.value: args.task_id or GlobalConfig.TASK_ID, + Args.trigger.value: args.trigger or GlobalConfig.TRIGGER, + } + if pms_kwargs.get(Args.pms2csv.value): + Pms2Csv( + app_name=pms_kwargs.get(Args.app_name.value), + user=pms_kwargs.get(Args.pms_user.value) or GlobalConfig.PMS_USER, + password=pms_kwargs.get(Args.pms_password.value) or GlobalConfig.PMS_PASSWORD, + pms_link_csv=pms_kwargs.get(Args.pms_link_csv.value), + ).write_new_csv() + elif pms_kwargs.get(Args.send2task.value) \ + and pms_kwargs.get(Args.task_id.value) \ + and pms_kwargs.get(Args.trigger.value) == "hand": Send2Pms().send2pms( - Send2Pms.case_res_path(task_id), - Send2Pms.data_send_result_csv(task_id) + Send2Pms.case_res_path(pms_kwargs.get(Args.task_id.value)), + Send2Pms.data_send_result_csv(pms_kwargs.get(Args.trigger.value)) ) @staticmethod @@ -394,26 +436,40 @@ class Manage: start.copy_template_to_apps() start.rewrite() - def export_csv(self, parser=None, sub_parser_export_csv=None): - """导出 csv""" - sub_parser_export_csv.add_argument( + def csv_control(self, parser=None, sub_parser_csv=None): + """csv相关功能命令参数""" + sub_parser_csv.add_argument( "-a", "--app", default="", help="应用名称:deepin-music" ) - sub_parser_export_csv.add_argument( + sub_parser_csv.add_argument( "-k", "--keywords", default="", help="用例的关键词" ) - sub_parser_export_csv.add_argument( + sub_parser_csv.add_argument( "-t", "--tags", default="", help="用例的标签" ) + sub_parser_csv.add_argument( + "-p2c", "--pyid2csv", action='store_const', const=True, default=False, + help="将用例py文件的case id同步到对应的csv文件中" + ) + sub_parser_csv.add_argument( + "-ec", "--export_csv_file", default="",help="导出csv文件名称,比如:case_list.csv" + ) args = parser.parse_args() - export_kwargs = { + csv_kwargs = { Args.app_name.value: args.app or self.default_app, Args.keywords.value: args.keywords or self.default_keywords, Args.tags.value: args.tags or self.default_tags, - "exportcsv": True + Args.pyid2csv.value: args.pyid2csv or self.default_pyid2csv, + Args.export_csv_file.value: args.export_csv_file or self.default_export_csv_file, + "collection_only": True } - LocalRunner(**export_kwargs).local_run() - + if csv_kwargs.get(Args.pyid2csv.value) or GlobalConfig.PY_ID_TO_CSV: + from src.csvctl import CsvControl + CsvControl(csv_kwargs.get(Args.app_name.value)).delete_mark_in_csv_if_not_exists_py() + if csv_kwargs.get(Args.pyid2csv.value) or csv_kwargs.get(Args.export_csv_file.value): + LocalRunner(**csv_kwargs).local_run() + else: + logger.error("需要传递一些参数,您可以使用 -h 或 --help 查看支持的参数") if __name__ == "__main__": try: diff --git a/setting/globalconfig.ini b/setting/globalconfig.ini index 81732ca..4e55de4 100644 --- a/setting/globalconfig.ini +++ b/setting/globalconfig.ini @@ -1,5 +1,5 @@ -;=============================== CASE CONFIG =================================== -[case] +;=============================== RUN CONFIG =================================== +[run] ;执行的应用名称 ;为空表示执行 apps/ 目录下所有应用的用例 APP_NAME = @@ -8,20 +8,19 @@ APP_NAME = KEYWORDS = ;执行包含用例标签的用例 -TAGS = -;----------------------------------------------- +;----------------------------------------------------------- ;1.KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例 ;2.KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式 -;e.g. TAGS = L1 or smoke -;----------------------------------------------- +;比如:TAGS = L1 or smoke ,表示执行标签带有 L1 或 somke 标签的用例; +;这两个参数也可以同时使用,可以组合出任意的用例集合,只有想不到没有办不到。 +;----------------------------------------------------------- +TAGS = ;本地文件测试套,将要执行的用例写入指定的 csv 文件 ;默认为空,从基础框架根目录开始:e.g. CASE_FILE = case_list.txt ;如果这里有值,APP_NAME KEYWORDS TAGS 的配置均不生效 CASE_FILE = -;=============================== RUNNER CONFIG =================================== -[runner] ;最大失败用例数量的占比 ;比如:总执行用例数为 100, 若 MAX_FAIL = 0.5,则失败用例数达到 50 就会终止测试。 MAX_FAIL = 1 @@ -81,6 +80,33 @@ DURING_FAIL = no ;注册自启服务 AUTOSTART = no +;测试机的密码 +PASSWORD = 1 + +;图像识别重试次数 +IMAGE_MATCH_NUMBER = 1 + +;图像识别重试每次间隔等待时间 +IMAGE_MATCH_WAIT_TIME = 1 + +;图像识别匹配度 +IMAGE_RATE = 0.9 + +;截取当前屏幕实时图像保存路径,用于图像识别坐标 +SCREEN_CACHE = /tmp/screen.png + +;截取屏幕上指定区域图片,保存临时图片的路径 +TMPDIR = /tmp/tmpdir + +;系统主题 +SYS_THEME = deepin + +;OCR服务端地址(不可随意修改) +OCR_SERVER_HOST = youqu-dev.uniontech.com + +;OpenCV服务端地址 +OPENCV_SERVER_HOST = youqu-dev.uniontech.com + ;=============================== REPORT CONFIG =================================== [report] ;测试报告的title @@ -114,41 +140,12 @@ ALLURE_REPORT_PATH = report/ XML_REPORT_PATH = report/ JSON_REPORT_PATH = report/ -;=============================== GLOBAL CONFIG =================================== -[globalconfig] -;测试机的密码 -PASSWORD = 1 - -;图像识别重试次数 -IMAGE_MATCH_NUMBER = 1 - -;图像识别重试每次间隔等待时间 -IMAGE_MATCH_WAIT_TIME = 1 - -;图像识别匹配度 -IMAGE_RATE = 0.9 - -;截取当前屏幕实时图像保存路径,用于图像识别坐标 -SCREEN_CACHE = /tmp/screen.png - -;截取屏幕上指定区域图片,保存临时图片的路径 -TMPDIR = /tmp/tmpdir - -;系统主题 -SYS_THEME = deepin - -;OCR服务端地址(不可随意修改) -OCR_SERVER_HOST = youqu-dev.uniontech.com - -;OpenCV服务端地址 -OPENCV_SERVER_HOST = youqu-dev.uniontech.com - ;=============================== PMS CONFIG =================================== ;PMS相关配置,包含以下几个方面: ;1.PMS测试套执行 ;2.自动从PMS爬取数据并同步本地CSV文件 ;3.PMS数据回填 -[pms] +[pmsctl] ;PMS的用户名,如: ut001234 PMS_USER = @@ -180,28 +177,29 @@ TRIGGER = auto ;如果接口请求失败,会进行重试 SEND_PMS_RETRY_NUMBER = 2 -[csv_link_pms_lib] ;caselib: 用例库 ;testcase: 产品库用例 CASE_FROM = caselib -[csv_link_pms_id] +[pmsctl-pms_link_csv] ;同步PMS数据到本地CSV文件,必须要配置的配置项 ;key是本地CSV文件的文件名称; ;value是对应PMS上的模块ID; ;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music, -;CSV文件名称为music.csv,其在PMS上的用例为: https://pms.uniontech.com/caselib-browse-81.html +;CSV文件名称为music.csv,其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html ;因此应该配置为: music = 81 ;这样才能将PMS与本地CSV文件建立联系。 ;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。 music = -[export_csv] -;导出的csv文件名称,默认 case_list.csv -CSV_FILE = case_list.csv +[csvctl] +;将py文件的case id同步到csv文件 +;yes, 开启同步 +PY_ID_TO_CSV = no + +;导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加 +EXPORT_CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过 -;exportcsv 命令导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加 -CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过 [log_cli] ;日志相关配置(不打印构造函数和魔法函数的功能说明) diff --git a/setting/globalconfig.py b/setting/globalconfig.py index 8ce7c99..a983996 100644 --- a/setting/globalconfig.py +++ b/setting/globalconfig.py @@ -68,28 +68,38 @@ class _GlobalConfig: # ====================== GLOBAL CONFIG INI ====================== # Get config file object GLOBAL_CONFIG_FILE_PATH = join(SETTING_PATH, "globalconfig.ini") - # [case] - case_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "case") - APP_NAME = case_cfg.get("APP_NAME", default="") - KEYWORDS = case_cfg.get("KEYWORDS", default="") - TAGS = case_cfg.get("TAGS", default="") - CASE_FILE = case_cfg.get("CASE_FILE", default="") - # [runner] - runner_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "runner") - RERUN = runner_cfg.get("RERUN", default=1) - RECORD_FAILED_CASE = runner_cfg.get("RECORD_FAILED_CASE", default=1) - MAX_FAIL = runner_cfg.get("MAX_FAIL", default=1) - CASE_TIME_OUT = runner_cfg.get("CASE_TIME_OUT", default=200) - CLEAN_ALL = runner_cfg.get("CLEAN_ALL", default="yes") - RESOLUTION = runner_cfg.get("RESOLUTION", default="1920x1080") - NOSKIP = runner_cfg.get_bool("NOSKIP", default=False) - IFIXED = runner_cfg.get_bool("IFIXED", default=False) - DURING_FAIL = runner_cfg.get_bool("DURING_FAIL", default=False) - AUTOSTART = runner_cfg.get_bool("AUTOSTART", default=False) - TOP = runner_cfg.get("TOP", default="") - REPEAT = runner_cfg.get("REPEAT", default="") - DEB_PATH = runner_cfg.get("DEB_PATH", default="~/Downloads/") - DEBUG = runner_cfg.get_bool("DEBUG", default=False) + # [run] + run_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "run") + APP_NAME = run_cfg.get("APP_NAME", default="") + KEYWORDS = run_cfg.get("KEYWORDS", default="") + TAGS = run_cfg.get("TAGS", default="") + CASE_FILE = run_cfg.get("CASE_FILE", default="") + RERUN = run_cfg.get("RERUN", default=1) + RECORD_FAILED_CASE = run_cfg.get("RECORD_FAILED_CASE", default=1) + MAX_FAIL = run_cfg.get("MAX_FAIL", default=1) + CASE_TIME_OUT = run_cfg.get("CASE_TIME_OUT", default=200) + CLEAN_ALL = run_cfg.get("CLEAN_ALL", default="yes") + RESOLUTION = run_cfg.get("RESOLUTION", default="1920x1080") + NOSKIP = run_cfg.get_bool("NOSKIP", default=False) + IFIXED = run_cfg.get_bool("IFIXED", default=False) + DURING_FAIL = run_cfg.get_bool("DURING_FAIL", default=False) + AUTOSTART = run_cfg.get_bool("AUTOSTART", default=False) + TOP = run_cfg.get("TOP", default="") + REPEAT = run_cfg.get("REPEAT", default="") + DEB_PATH = run_cfg.get("DEB_PATH", default="~/Downloads/") + DEBUG = run_cfg.get_bool("DEBUG", default=False) + PASSWORD = run_cfg.get("PASSWORD", default="1") + if not PASSWORD: + raise ValueError("测试机密码不能未空") + IMAGE_MATCH_NUMBER = run_cfg.get("IMAGE_MATCH_NUMBER", default=1) + IMAGE_MATCH_WAIT_TIME = run_cfg.get("IMAGE_MATCH_WAIT_TIME", default=1) + IMAGE_RATE = run_cfg.get("IMAGE_RATE", default=0.9) + SCREEN_CACHE = run_cfg.get("SCREEN_CACHE", default="/tmp/screen.png") + TMPDIR = run_cfg.get("TMPDIR", default="/tmp/tmpdir") + SYS_THEME = run_cfg.get("SYS_THEME", default="deepin") + OCR_SERVER_HOST = run_cfg.get("OCR_SERVER_HOST", default="localhost") + OPENCV_SERVER_HOST = run_cfg.get("OPENCV_SERVER_HOST", default="localhost") + # [report] report_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "report") REPORT_TITLE = report_cfg.get("REPORT_TITLE", default="YouQu Report") @@ -105,22 +115,9 @@ class _GlobalConfig: JSON_REPORT_PATH = join( ROOT_DIR, report_cfg.get("JSON_REPORT_PATH", default="report/") ) - # [globalconfig] - global_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "globalconfig") - PASSWORD = global_cfg.get("PASSWORD", default="1") - if not PASSWORD: - raise ValueError("测试机密码不能未空") - IMAGE_MATCH_NUMBER = global_cfg.get("IMAGE_MATCH_NUMBER", default=1) - IMAGE_MATCH_WAIT_TIME = global_cfg.get("IMAGE_MATCH_WAIT_TIME", default=1) - IMAGE_RATE = global_cfg.get("IMAGE_RATE", default=0.9) - SCREEN_CACHE = global_cfg.get("SCREEN_CACHE", default="/tmp/screen.png") - TMPDIR = global_cfg.get("TMPDIR", default="/tmp/tmpdir") - SYS_THEME = global_cfg.get("SYS_THEME", default="deepin") - OCR_SERVER_HOST = global_cfg.get("OCR_SERVER_HOST", default="localhost") - OPENCV_SERVER_HOST = global_cfg.get("OPENCV_SERVER_HOST", default="localhost") - # [pms] - pms_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "pms") + # [pmsctl] + pms_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "pmsctl") PMS_USER = pms_cfg.get("PMS_USER", default="") PMS_PASSWORD = pms_cfg.get("PMS_PASSWORD", default="") SUITE_ID = pms_cfg.get("SUITE_ID", default="") @@ -132,6 +129,26 @@ class _GlobalConfig: if TRIGGER not in ("auto", "hand"): raise ValueError SEND_PMS_RETRY_NUMBER = pms_cfg.get("SEND_PMS_RETRY_NUMBER", default=2) + CASE_FROM = pms_cfg.get("CASE_FROM", default="caselib") + + # [csvctl] + csv_cfg = GetCfg(GLOBAL_CONFIG_FILE_PATH, "csvctl") + PY_ID_TO_CSV = csv_cfg.get_bool("PY_ID_TO_CSV", default=False) + EXPORT_CSV_HEARD = csv_cfg.get("EXPORT_CSV_HEARD", default="用例级别,用例类型,测试级别,是否跳过").replace(" ", "") + + # [log_cli] + log_cli = GetCfg(GLOBAL_CONFIG_FILE_PATH, "log_cli") + LOG_LEVEL = log_cli.get("LOG_LEVEL", default="INFO") + CLASS_NAME_STARTSWITH = tuple( + log_cli.get("CLASS_NAME_STARTSWITH", default="Assert").replace(" ", "").split(",") + ) + CLASS_NAME_ENDSWITH = tuple( + log_cli.get("CLASS_NAME_ENDSWITH", default="Widget").replace(" ", "").split(",") + ) + CLASS_NAME_CONTAIN = tuple( + log_cli.get("CLASS_NAME_CONTAIN", default="ShortCut").replace(" ", "").split(",") + ) + # ====================== 动态获取变量 ====================== # username USERNAME = getuser() @@ -183,22 +200,6 @@ class _GlobalConfig: top_cmd = "top -b -d 3 -w 512" - # [export_csv] - export_csv = GetCfg(GLOBAL_CONFIG_FILE_PATH, "export_csv") - CSV_FILE = export_csv.get("CSV_FILE", default="case_list.csv") - CSV_HEARD = export_csv.get("CSV_HEARD", default="用例级别,用例类型,测试级别,是否跳过").replace(" ", "") - # [log_cli] - log_cli = GetCfg(GLOBAL_CONFIG_FILE_PATH, "log_cli") - LOG_LEVEL = log_cli.get("LOG_LEVEL", default="INFO") - CLASS_NAME_STARTSWITH = tuple( - log_cli.get("CLASS_NAME_STARTSWITH", default="Assert").replace(" ", "").split(",") - ) - CLASS_NAME_ENDSWITH = tuple( - log_cli.get("CLASS_NAME_ENDSWITH", default="Widget").replace(" ", "").split(",") - ) - CLASS_NAME_CONTAIN = tuple( - log_cli.get("CLASS_NAME_CONTAIN", default="ShortCut").replace(" ", "").split(",") - ) GITHUB_URL = "https://github.com/linuxdeepin/deepin-autotest-framework" DOCS_URL = "https://linuxdeepin.github.io/deepin-autotest-framework" PyPI_URL = "https://pypi.org/project/youqu" @@ -228,11 +229,17 @@ class ConfStr(Enum): @unique class FixedCsvTitle(Enum): + case_id = "脚本ID" + pms_case_id = "PMS用例ID" + case_level = "用例级别" + case_type = "用例类型" + device_type = "设备类型" + case_from = "用例来源" + online_obj = "上线对象" + test_level = "测试级别" skip_reason = "跳过原因" fixed = "确认修复" removed = "废弃用例" - pms_case_id = "PMS用例ID" - case_level = "用例级别" @unique diff --git a/setting/pmsmark.ini b/setting/pmsmark.ini new file mode 100644 index 0000000..7d93a94 --- /dev/null +++ b/setting/pmsmark.ini @@ -0,0 +1,14 @@ +[pms-mark-to-csv-mark] +;压力 +reliability = STR +;功能 +feature = FUNC +;安全 +security = SEC +;兼容性 +compatibility = CTS +;PERF +performance = PERF +;接口 +interface = API + diff --git a/setting/template/app_template/${app_name}.csv-tpl b/setting/template/app_template/${app_name}.csv-tpl index c2e58de..a2ab705 100644 --- a/setting/template/app_template/${app_name}.csv-tpl +++ b/setting/template/app_template/${app_name}.csv-tpl @@ -1 +1 @@ -脚本ID,PMS用例ID,用例级别,跳过原因,确认修复,废弃用例 \ No newline at end of file +${FIXEDCSVTITLE} diff --git a/setting/template/app_template/config.py-tpl b/setting/template/app_template/config.py-tpl index b19d85b..70349bb 100644 --- a/setting/template/app_template/config.py-tpl +++ b/setting/template/app_template/config.py-tpl @@ -65,7 +65,12 @@ ## basic_frame_tag = dep_cfg.get("autotest-basic-frame") ## if _GlobalConfig.current_tag != basic_frame_tag: -## logger.error("应用库与基础框架对应版本不一致!") +## logger.error( +## "应用库与基础框架对应版本不一致!" +## f"YouQu版本为:{_GlobalConfig.current_tag},应用库版本为:{basic_frame_tag}" +## "如需如果您确认当前使用的YouQu版本正确,请检查您的应用工程根目录下control文件" +## "记录的依赖的YouQu版本是否一致~" +## ) ##Config = _Config() diff --git a/src/csvctl.py b/src/csvctl.py new file mode 100644 index 0000000..1bb4974 --- /dev/null +++ b/src/csvctl.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# _*_ coding:utf-8 _*_ + +# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. + +# SPDX-License-Identifier: GPL-2.0-only +import os +import re + +from setting.globalconfig import GlobalConfig +from src.rtk._base import transform_app_name + + +class CsvControl: + """csv control""" + + def __init__(self, app_name=None): + self.walk_dir = ( + f"{GlobalConfig.APPS_PATH}/autotest_{transform_app_name(app_name).replace('-', '_')}" + if app_name + else GlobalConfig.APPS_PATH + ) + print(1) + + def scan_csv_and_py(self): + """scan csv and case py""" + csv_path_dict = {} + py_path_dict = {} + for root, _, files in os.walk(self.walk_dir): + py_files = [] + for file in files: + if file.endswith(".csv") and file != "case_list.csv": + csv_path_dict[os.path.splitext(file)[0]] = f"{root}/{file}" + if file.startswith("test_") and file.endswith(".py"): + case_name = [] + _case_name = re.findall( + r"test_(.*?)_(\d+)_\d+.py|test_(.*?)_(\d+).py", file + ) + if _case_name: + _case_name = _case_name[0] + if isinstance(_case_name, tuple): + for i in _case_name: + if i: + case_name.append(i) + py_files.append([f"{root}/{file}", case_name[-1]]) + py_path_dict[case_name[0]] = py_files + if not (csv_path_dict and py_path_dict): + return None + return csv_path_dict, py_path_dict + + def delete_mark_in_csv_if_not_exists_py(self): + """delete mark in csv if not exists case py""" + res = self.scan_csv_and_py() + if res is None: + return + csv_path_dict, py_path_dict = res + for csv_name in csv_path_dict: + for case_name in py_path_dict: + if csv_name == case_name: + csv_path = csv_path_dict.get(csv_name) + with open(csv_path, "r", encoding="utf-8") as f: + csv_txt_list = f.readlines() + taglines = [txt.strip().split(",") for txt in csv_txt_list[1:]] + new_taglines = [] + py_case_paths = py_path_dict.get(case_name) + for tag in taglines: + try: + csv_case_id = f"{int(tag[0]):0>3}" + except ValueError as e: + raise ValueError(f"文件:{csv_path} 里面似乎格式有点问题,出现了一个报错:{e}") + for py_case in py_case_paths: + py_case_id = py_case[-1] + if csv_case_id == py_case_id: + new_taglines.append(tag) + break + else: + print(f"{tag} will remove from {csv_path}") + if new_taglines != taglines: + bak_path = f"{GlobalConfig.REPORT_PATH}/pyid2csv_back" + if not os.path.exists(bak_path): + os.makedirs(bak_path) + os.system( + f"cp {csv_path} {bak_path}/{GlobalConfig.TIME_STRING}_{csv_name}.csv" + ) + new_csv_list = [csv_txt_list[0]] + [ + ",".join(i) + "\n" for i in new_taglines + ] + with open(csv_path, "w+", encoding="utf-8") as f: + f.writelines(new_csv_list) + + +if __name__ == "__main__": + CsvControl().delete_mark_in_csv_if_not_exists_py() diff --git a/src/pms/pms2csv.py b/src/pms/pms2csv.py index 15270cf..c3f56fd 100644 --- a/src/pms/pms2csv.py +++ b/src/pms/pms2csv.py @@ -8,23 +8,17 @@ # pylint: disable=C0301,C0115,R0903,C0103,C0201,R1710,R0914,W1514,R0914,R1702 import json import os -import re from configparser import ConfigParser from os.path import splitext -from time import strftime -from pprint import pprint +from setting.globalconfig import FixedCsvTitle from setting.globalconfig import GetCfg from setting.globalconfig import GlobalConfig -from src import logger +from src import logger from src.pms._base import MAX_CASE_NUMBER from src.pms._base import _Base from src.pms._base import _unicode_to_cn - - -class CsvTitle: - case_id = "PMS用例ID" - case_level = "用例级别" +from src.rtk._base import transform_app_name class Pms2Csv(_Base): @@ -32,33 +26,51 @@ class Pms2Csv(_Base): __author__ = "huangmingqiang@uniontech.com" - def __init__(self): - super().__init__() - self.APP_NAME = GlobalConfig.APP_NAME - self.project_dir = f"autotest_{self.APP_NAME.replace('-', '_')}" - if not os.path.exists(f"{GlobalConfig.APPS_PATH}/{self.project_dir}"): - logger.error(f"{self.project_dir} 似乎不存在 !") - raise ValueError + config_error_log = "请检查您传递的 '命令行参数' 或 setting/globalconfig.ini 里的配置项" - self.PMS_USER = GlobalConfig.PMS_USER - self.PMS_PASSWORD = GlobalConfig.PMS_PASSWORD + def __init__(self, app_name=None, user=None, password=None, pms_link_csv=None): + super().__init__(user=user, password=password) + self.walk_dir = ( + f"{GlobalConfig.APPS_PATH}/{transform_app_name(app_name)}" + if app_name + else GlobalConfig.APPS_PATH + ) conf = ConfigParser() conf.read(GlobalConfig.GLOBAL_CONFIG_FILE_PATH) - self.csv_names = conf.options("csv_link_pms_id") - self.csv_link_cfg = GetCfg( - GlobalConfig.GLOBAL_CONFIG_FILE_PATH, "csv_link_pms_id" + ini_csv_names = conf.options("pmsctl-pms_link_csv") + ini_csv_pms_map = GetCfg( + GlobalConfig.GLOBAL_CONFIG_FILE_PATH, "pmsctl-pms_link_csv" ) - csv_link_lib_cfg = GetCfg( - GlobalConfig.GLOBAL_CONFIG_FILE_PATH, "csv_link_pms_lib" - ) - self.CASE_FROM = csv_link_lib_cfg.get("CASE_FROM", default="caselib") + cli_csv_pms_map = {} + cli_csv_names = [] + if pms_link_csv: + _cli_csv_names = pms_link_csv.split("/") + for i in _cli_csv_names: + pls = i.split(":") + if len(pls) != 2: + raise ValueError("--pms_link_csv 参数的值可能有问题") + csv_name, pms_product_id = pls + cli_csv_names.append(csv_name.strip()) + cli_csv_pms_map[csv_name.strip()] = pms_product_id.strip() - def get_data(self, app_case_id): + self.csv_names = cli_csv_names or ini_csv_names + self.csv_link_cfg = cli_csv_pms_map or ini_csv_pms_map + + if not self.csv_names: + raise ValueError(self.config_error_log) + + self.pms_mark = GetCfg( + f"{GlobalConfig.SETTING_PATH}/pmsmark.ini", "pms-mark-to-csv-mark" + ) + + def get_data_from_pms(self, app_case_id): """获取pms上数据""" + if not app_case_id: + raise ValueError(self.config_error_log) case_url = ( - f"https://pms.uniontech.com/{self.CASE_FROM}-browse-" - f'{app_case_id}-{"-" if self.CASE_FROM == "testcase" else ""}all-0-id_desc-0-{MAX_CASE_NUMBER}.json' + f"https://pms.uniontech.com/{GlobalConfig.CASE_FROM}-browse-" + f'{app_case_id}-{"-" if GlobalConfig.CASE_FROM == "testcase" else ""}all-0-id_desc-0-{MAX_CASE_NUMBER}.json' ) res = self.rx.open_url(case_url) res_str = _unicode_to_cn(res) @@ -66,149 +78,142 @@ class Pms2Csv(_Base): try: res_dict = json.loads(res_str) except json.decoder.JSONDecodeError: - logger.error(f"爬取pms数据失败, 请检查模块 id 是否为: {app_case_id}") + logger.error(f"获取pms数据失败, {self.config_error_log}") return cases = res_dict.get("data").get("cases") res_data = {} for i in cases: - case_id = cases.get(i).get("id") - case_level = cases.get(i).get("pri") - case_title = cases.get(i).get("title") - # 从用例标题中取出自动化用例id [001] - at_case_id = re.findall(r"\[(\d{3})\]", case_title) - # 如果id存在,并且之前没有出现过 - # 如果出现相同的id,只取第一个 - if at_case_id and at_case_id[0] not in res_data.keys(): - # 组装成一个字典 - res_data[at_case_id[0]] = { - "case_id": case_id, # 用例在PMS上ID - "case_level": case_level, # 用例级别 - "case_title": case_title, # 用例标题 + case = cases.get(i) + case_id = case.get("id") + case_level = case.get("pri") + case_type = self.pms_mark.get(case.get("type")) + if case_type: + res_data[case_id] = { + "case_level": f"L{case_level}", + "case_type": case_type, } if not res_data: - logger.error("未从pms获取到数据, 请检查配置") + logger.error(f"未从pms获取到数据, {self.config_error_log}") raise ValueError return res_data def read_csv(self): """读取本地csv文件数据""" csv_path_dict = {} - # 默认的csv文件备份路径 - csv_bak_path = f"{GlobalConfig.REPORT_PATH}/csv_back" + csv_bak_path = f"{GlobalConfig.REPORT_PATH}/pms2csv_back" if not os.path.exists(csv_bak_path): os.makedirs(csv_bak_path) - for root, _, files in os.walk(f"{GlobalConfig.APPS_PATH}/{self.project_dir}"): + for root, _, files in os.walk(self.walk_dir): for file in files: - # 必须是标签csv文件,排除一些ddt的csv文件 if file.endswith(".csv") and splitext(file)[0] in self.csv_names: csv_path_dict[splitext(file)[0]] = f"{root}/{file}" - # 备份csv文件 os.system( - f"cp {root}/{file} {csv_bak_path}/{strftime('%Y%m%d%H%M%S')}_{file}" + f"cp {root}/{file} {csv_bak_path}/{GlobalConfig.TIME_STRING}_{file}" ) if not csv_path_dict: - logger.error(f"{self.APP_NAME} 目录下未找到csv文件") - raise ValueError + raise ValueError(f"{self.walk_dir} 目录下未找到csv文件") - pms_id_index = None - level_index = None res_tags = {} - csv_title_dict = {} + csv_heads_dict = {} for csv_name in csv_path_dict: - with open(csv_path_dict.get(csv_name), "r") as f: + with open(csv_path_dict.get(csv_name), "r", encoding="utf-8") as f: txt_list = f.readlines() - csv_titles = txt_list[0].strip().split(",") - for index, title in enumerate(csv_titles): - # 找到在表头中对应的索引 - if title.strip() == CsvTitle.case_id: - pms_id_index = index - 1 - elif title.strip() == CsvTitle.case_level: - level_index = index - 1 + csv_heads = txt_list[0].strip().split(",") + + csv_head_index_map = {} + for index, title in enumerate(csv_heads): + for i in FixedCsvTitle: + if i.value == title.strip(): + csv_head_index_map[i.name] = { + "head_name": i.value, + "head_index": index, + } - # 读取到所有的标签 taglines = [txt.strip().split(",") for txt in txt_list[1:]] - id_tags_dict = {f"{int(i[0]):0>3}": i[1:] for i in taglines if i[0]} + id_tags_dict = {i[0]: i for i in taglines if i[0]} res_tags[csv_name] = id_tags_dict - # csv文件的表头 - csv_title_dict[csv_name] = csv_titles - # 将这些数据无情的返回, 其实可以进一步将这些返回数据整合一下, 但是累了, 就这样吧 - return pms_id_index, level_index, res_tags, csv_path_dict, csv_title_dict + csv_heads_dict[csv_name] = csv_head_index_map + return res_tags, csv_heads_dict, csv_path_dict def compare_pms_to_csv(self): """对比pms上数据和本地csv文件数据""" - # 接收csv文件里面的值 - ( - pms_id_index, - level_index, - _res_tags, - csv_path_dict, - csv_title_dict, - ) = self.read_csv() - # 将csv文件里面的数据和pms上爬取的数据进行对比 - new_csv_tags_map = {} - for csv_name in _res_tags: - # 每个csv文件处理一次 - pms_tags_dict = self.get_data(self.csv_link_cfg.get(csv_name)) - # 如果pms上没有爬取到,继续处理下一个csv文件 + (res_tags, csv_heads_dict, csv_path_dict) = self.read_csv() + new_csv_file_tags = {} + for csv_name in res_tags: + product_id = self.csv_link_cfg.get(csv_name) + pms_tags_dict = self.get_data_from_pms(product_id) if pms_tags_dict is None: continue - csv_tags_dict = _res_tags.get(csv_name) - new_csv_tags = {} - for csv_tag_id in csv_tags_dict: - csv_tags = csv_tags_dict.get(csv_tag_id) - # 拿着csv里面的id去和pms上的id匹配 - for pms_tag_id in pms_tags_dict: - if pms_tag_id == csv_tag_id: - pms_tags = pms_tags_dict.get(pms_tag_id) - case_id = pms_tags.get("case_id") + csv_tags_dict = res_tags.get(csv_name) + csv_head_dict = csv_heads_dict.get(csv_name) + + pms_case_id_index = case_level_index = case_type_index = None + + pms_case_id_name = csv_head_dict.get(FixedCsvTitle.pms_case_id.name) + if pms_case_id_name: + pms_case_id_index = pms_case_id_name.get("head_index") + case_level_name = csv_head_dict.get(FixedCsvTitle.case_level.name) + if case_level_name: + case_level_index = case_level_name.get("head_index") + case_type_name = csv_head_dict.get(FixedCsvTitle.case_type.name) + if case_type_name: + case_type_index = case_type_name.get("head_index") + + new_csv_tags = [] + new_csv_tags.append( + [i.get("head_name") for i in list(csv_head_dict.values())] + ) + for csv_case_id in csv_tags_dict: + for pms_case_id in pms_tags_dict: + if pms_case_id == csv_case_id: + pms_tags = pms_tags_dict.get(pms_case_id) case_level = pms_tags.get("case_level") - case_level = f"L{case_level}" - # 循环处理每个字段 - for target_index, value, title_name in [ - [pms_id_index, case_id, CsvTitle.case_id], - [level_index, case_level, CsvTitle.case_level], - ]: - # 如果没有索引,说明原来csv文件中没有这一列,直接添加到最后一列 - if target_index is None: - csv_tags.append(value) - if CsvTitle.case_id not in csv_title_dict.get(csv_name): - csv_title_dict[csv_name].append(title_name) - else: - # 如果有索引,直接修改原来的数据 - csv_tags[target_index] = value + case_type = pms_tags.get("case_type") + flag = False + if ( + pms_case_id_index + and csv_tags_dict[csv_case_id][pms_case_id_index] + != pms_case_id + ): + csv_tags_dict[csv_case_id][pms_case_id_index] = pms_case_id + flag = True + if ( + case_level_index + and csv_tags_dict[csv_case_id][case_level_index] + != case_level + ): + csv_tags_dict[csv_case_id][case_level_index] = case_level + flag = True + if ( + case_type_index + and csv_tags_dict[csv_case_id][case_type_index] != case_type + ): + csv_tags_dict[csv_case_id][case_type_index] = case_type + flag = True + + new_tags = csv_tags_dict[csv_case_id] + if flag: + logger.info( + f"pms case id: {pms_case_id}, new tags:{new_tags}" + ) + new_csv_tags.append(new_tags) break else: - # 如果此AT id没有找到对应的,那需要将csv此行补位空字符串 - for target_index in [pms_id_index, level_index]: - # 如果没有索引,说明原来csv文件中没有这一列, - # 添加一个空字符串到最后一列 - if target_index is None: - csv_tags.append("") - new_csv_tags[csv_tag_id] = csv_tags - new_csv_tags_map[csv_name] = new_csv_tags + new_csv_tags.append(csv_tags_dict[csv_case_id]) - return new_csv_tags_map, csv_path_dict, csv_title_dict + new_csv_file_tags[csv_path_dict.get(csv_name)] = new_csv_tags + return new_csv_file_tags def write_new_csv(self): """写新的csv文件""" - new_csv_tags_map, csv_path_dict, csv_title_dict = self.compare_pms_to_csv() - # 将 new_csv_tags_map 里面的数据写成一个新的csv文件 - for csv_name in new_csv_tags_map: - csv_path = csv_path_dict.get(csv_name) - new_csv_tags = new_csv_tags_map.get(csv_name) - new_csv_tags_list = [] - # 先把表头放进去 - new_csv_tags_list.append(",".join(csv_title_dict.get(csv_name)) + "\n") - # 组装成一行行的数据 - for _id in new_csv_tags: - new_csv_list = new_csv_tags.get(_id) - new_csv_list.insert(0, _id) - new_csv_tags_list.append(",".join(new_csv_list) + "\n") - with open(csv_path, "w+", encoding="utf-8") as f: - f.writelines(new_csv_tags_list) - pprint(new_csv_tags_list, indent=4) - logger.info("同步完成") + new_csv_file_tags = self.compare_pms_to_csv() + for csv_file in new_csv_file_tags: + new_csv_tags = new_csv_file_tags.get(csv_file) + with open(csv_file, "w+", encoding="utf-8") as f: + for tags in new_csv_tags: + f.write(",".join(tags) + "\n") + logger.info(f"同步完成: {csv_file}") if __name__ == "__main__": diff --git a/src/rtk/_base.py b/src/rtk/_base.py index 17b467c..cdde06c 100644 --- a/src/rtk/_base.py +++ b/src/rtk/_base.py @@ -17,9 +17,9 @@ class SubCmd(Enum): """SubCmd""" run = "run" remote = "remote" - pms = "pms" + pmsctl = "pmsctl" startapp = "startapp" - exportcsv = "exportcsv" + csvctl = "csvctl" @unique @@ -62,9 +62,14 @@ class Args(Enum): client_password = "client_password" parallel = "parallel" autostart = "autostart" + pyid2csv = "pyid2csv" + export_csv_file = "export_csv_file" + pms2csv = "pms2csv" + pms_link_csv = "pms_link_csv" + send2task = "send2task" -def transform_app_name(real_app_name): +def transform_app_name(real_app_name): """转换 app_name""" if not real_app_name: return None diff --git a/src/rtk/local_runner.py b/src/rtk/local_runner.py index 113cd32..2fa6999 100644 --- a/src/rtk/local_runner.py +++ b/src/rtk/local_runner.py @@ -14,9 +14,9 @@ from os import listdir from os import makedirs from os import system from os.path import exists +from os.path import expanduser from os.path import isfile from os.path import join -from os.path import expanduser from time import sleep from tkinter import Tk @@ -81,8 +81,10 @@ class LocalRunner: project_name=None, build_location=None, line=None, - exportcsv=None, + collection_only=None, autostart=None, + pyid2csv=None, + export_csv_file=None, **kwargs, ): logger("INFO") @@ -124,9 +126,11 @@ class LocalRunner: self.project_name = project_name self.build_location = build_location self.line = line - self.exportcsv = exportcsv + self.collection_only = collection_only + self.pyid2csv = pyid2csv or GlobalConfig.PY_ID_TO_CSV + self.export_csv_file = export_csv_file or GlobalConfig.EXPORT_CSV_FILE - if not self.default.get(Args.debug.value) and not self.exportcsv: + if not self.default.get(Args.debug.value) and not self.collection_only: screen = Tk() x = screen.winfo_screenwidth() y = screen.winfo_screenheight() @@ -231,8 +235,13 @@ class LocalRunner: cmd.extend(["-k", f"'{default.get(Args.keywords.value)}'"]) if default.get(Args.tags.value): cmd.extend(["-m", f"'{default.get(Args.tags.value)}'"]) - - if self.exportcsv: + if app_dir and app_dir != GlobalConfig.APPS_PATH: + cmd.extend(["--app_name", app_dir]) + if self.pyid2csv: + cmd.extend(["--pyid2csv", "--verbosity=-1"]) + if self.export_csv_file: + cmd.extend(["--export_csv_file", self.export_csv_file]) + if self.collection_only: cmd.append("--co") return cmd @@ -305,7 +314,6 @@ class LocalRunner: self.make_dir( join(GlobalConfig.REPORT_PATH, GlobalConfig.ReportFormat.JSON) ) - return cmd def change_working_dir(self): @@ -324,7 +332,7 @@ class LocalRunner: print(f"WorkSpace: \n{case_path}") chdir(case_path) return working_dir - raise EnvironmentError(f"apps目录下未找到指定的{app_name}") + raise EnvironmentError(f"apps目录下未找到指定的 {app_name}") return GlobalConfig.APPS_PATH def local_run(self): @@ -353,7 +361,7 @@ class LocalRunner: return pytest.main([i.strip("'") for i in run_test_cmd_list[1:]]) - if self.exportcsv: + if self.collection_only: return if self.project_name and self.build_location and self.line: self.write_json( @@ -393,7 +401,9 @@ class LocalRunner: if exists(letmego.conf.setting.RUNNING_MAN_FILE): letmego.unregister_autostart_service() - letmego.clean_running_man() + letmego.clean_running_man( + copy_to=f"{GlobalConfig.REPORT_PATH}/_running_man_{GlobalConfig.TIME_STRING}.log" + ) @staticmethod def get_result(): @@ -406,7 +416,7 @@ class LocalRunner: res = Counter([results_dict.get(i).get("result") for i in results_dict]) total = sum(res.values()) skiped = res.get("skip", 0) - total = total - skiped # 剔除skip的用例 + total = total - skiped passed = res.get("pass", 0) failed = total - passed pass_rate = f"{round((passed / total) * 100, 1)}%" if passed else "0%" diff --git a/src/rtk/remote_runner.py b/src/rtk/remote_runner.py index f40c31a..39b53ff 100644 --- a/src/rtk/remote_runner.py +++ b/src/rtk/remote_runner.py @@ -96,7 +96,7 @@ class RemoteRunner: if not cli_client_dict and not self.ini_client_dict: raise ValueError( "未获取到测试机信息,请检查 setting/remote.ini 中 CLIENT LIST 是否配置," - "或通过命令行 python3 manage.py remote -c user@ip:password 传入。" + "或通过命令行 remote -c user@ip:password 传入。" ) self.default = { diff --git a/src/startapp.py b/src/startapp.py index 616622a..b19828c 100644 --- a/src/startapp.py +++ b/src/startapp.py @@ -12,6 +12,7 @@ from time import strftime from configparser import ConfigParser from setting.globalconfig import GlobalConfig +from setting.globalconfig import FixedCsvTitle class StartApp: @@ -53,7 +54,7 @@ class StartApp: shutil.move(f"{root}/{file}", f"{root}/{new_file}") file = new_file - if ".py" in file: + if ".py" in file or ".csv" in file: with open(f"{root}/{file}", "r") as f: codes = f.readlines() new_codes = [] @@ -78,6 +79,10 @@ class StartApp: code = re.sub(r"\${DATE}", strftime("%Y/%m/%d"), code) if "${TIME}" in code: code = re.sub(r"\${TIME}", strftime("%H:%M:%S"), code) + if "${FIXEDCSVTITLE}" in code: + code = re.sub( + r"\${FIXEDCSVTITLE}", ",".join([i.value for i in FixedCsvTitle]), code + ) new_codes.append(code) with open(f"{root}/{file}", "w") as f: f.writelines([i for i in new_codes])